Skip to content

Commit d1c69f1

Browse files
authored
feat: Enable eslint plugin to call Async methods. (#4080)
1 parent 607faef commit d1c69f1

17 files changed

+127
-299
lines changed

packages/cspell-eslint-plugin/cspell.json

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"words": [
66
"estree",
77
"pnpm",
8+
"synckit",
89
"treeshake",
910
"tsbuildinfo"
1011
]

packages/cspell-eslint-plugin/package.json

+6-11
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@
3333
"!**/*.map"
3434
],
3535
"scripts": {
36-
"build": "pnpm run build-schema && pnpm run build-rollup",
37-
"build-rollup": "rollup --config rollup.config.ts --configPlugin typescript",
38-
"build-schema": "ts-json-schema-generator --no-top-ref --expose none --path src/options.ts --type Options -o ./src/_auto_generated_/options.schema.json",
39-
"watch": "pnpm run build-rollup --watch",
36+
"build": "pnpm build:schema && pnpm build:src",
37+
"build:src": "tsc -p .",
38+
"build:schema": "ts-json-schema-generator --no-top-ref --expose none --path src/options.ts --type Options -o ./src/_auto_generated_/options.schema.json",
39+
"watch": "tsc -p . --watch",
4040
"clean": "shx rm -rf dist coverage .tsbuildinfo",
4141
"clean-build": "pnpm run clean && pnpm run build",
4242
"coverage": "echo coverage",
@@ -54,10 +54,6 @@
5454
"node": ">=14"
5555
},
5656
"devDependencies": {
57-
"@rollup/plugin-commonjs": "^24.0.1",
58-
"@rollup/plugin-json": "^6.0.0",
59-
"@rollup/plugin-node-resolve": "^15.0.1",
60-
"@rollup/plugin-typescript": "^11.0.0",
6157
"@types/eslint": "^8.4.10",
6258
"@types/estree": "^1.0.0",
6359
"@types/node": "^18.11.18",
@@ -67,12 +63,11 @@
6763
"eslint": "^8.33.0",
6864
"eslint-plugin-react": "^7.32.2",
6965
"mocha": "^10.2.0",
70-
"rollup": "^3.12.0",
71-
"rollup-plugin-dts": "^5.1.1",
7266
"ts-json-schema-generator": "^1.2.0"
7367
},
7468
"dependencies": {
7569
"cspell-lib": "workspace:*",
76-
"estree-walker": "^2.0.2"
70+
"estree-walker": "^2.0.2",
71+
"synckit": "^0.8.5"
7772
}
7873
}

packages/cspell-eslint-plugin/rollup.config.ts

-137
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { Comment, Literal, Node } from 'estree';
2+
3+
export interface JSXText extends Omit<Literal, 'type'> {
4+
type: 'JSXText';
5+
}
6+
7+
export type ASTNode = (Node | Comment | JSXText) & { parent?: Node };

packages/cspell-eslint-plugin/src/_auto_generated_/options.schema.json

-4
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,6 @@
3737
{
3838
"additionalProperties": false,
3939
"properties": {
40-
"addWords": {
41-
"description": "**Experimental**: Provide a fix option to add words to the file.\n\nNote: this does not yet work perfectly.",
42-
"type": "boolean"
43-
},
4440
"path": {
4541
"description": "Path to word list file. File format: 1 word per line",
4642
"type": "string"

packages/cspell-eslint-plugin/src/cspell-eslint-plugin.ts

+6-62
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
// cspell:ignore TSESTree
2-
import { refreshDictionaryCache } from 'cspell-lib';
32
import type { Rule } from 'eslint';
4-
import * as path from 'path';
3+
import { createSyncFn } from 'synckit';
54

65
import optionsSchema from './_auto_generated_/options.schema.json';
7-
import { addWordToCustomWordList } from './customWordList';
8-
import type { CustomWordListFile } from './options';
96
import { normalizeOptions } from './options';
10-
import { type Issue, spellCheck, walkTree } from './worker';
7+
import type { Issue, SpellCheckSyncFn } from './spellCheck';
8+
import { walkTree } from './walkTree';
119

1210
const schema = optionsSchema as unknown as Rule.RuleMetaData['schema'];
1311

12+
const spellCheck: SpellCheckSyncFn = createSyncFn(require.resolve('./worker'), undefined, 30000);
13+
1414
interface PluginRules {
1515
['spellchecker']: Rule.RuleModule;
1616
}
@@ -86,36 +86,9 @@ function create(context: Rule.RuleContext): Rule.RuleListener {
8686
};
8787
}
8888

89-
function createAddWordToDictionaryFix(word: string): Rule.SuggestionReportDescriptor | undefined {
90-
if (!isCustomWordListFile(options.customWordListFile) || !options.customWordListFile.addWords) {
91-
return undefined;
92-
}
93-
94-
const dictFile = path.resolve(context.getCwd(), options.customWordListFile.path);
95-
96-
const data = { word, dictionary: path.basename(dictFile) };
97-
const messageId: MessageIds = 'addWordToDictionary';
98-
99-
return {
100-
messageId,
101-
data,
102-
fix: (_fixer) => {
103-
// This wrapper is a hack to delay applying the fix until it is actually used.
104-
// But it is not reliable, since ESLint + extension will randomly read the value.
105-
return new WrapFix({ range: [start, end], text: word }, () => {
106-
refreshDictionaryCache(0);
107-
addWordToCustomWordList(dictFile, word);
108-
});
109-
},
110-
};
111-
}
112-
11389
log('Suggestions: %o', issue.suggestions);
11490
const suggestions: Rule.ReportDescriptorOptions['suggest'] = issue.suggestions?.map(createSug);
115-
const addWordFix = createAddWordToDictionaryFix(issue.word);
116-
117-
const suggest =
118-
suggestions || addWordFix ? (suggestions || []).concat(addWordFix ? [addWordFix] : []) : undefined;
91+
const suggest = suggestions;
11992

12093
const des: Rule.ReportDescriptor = {
12194
messageId,
@@ -169,32 +142,3 @@ export const configs = {
169142
},
170143
},
171144
};
172-
173-
/**
174-
* This wrapper is used to add a
175-
*/
176-
class WrapFix implements Rule.Fix {
177-
/**
178-
*
179-
* @param fix - the example Fix
180-
* @param onGetText - called when `fix.text` is accessed
181-
* @param limit - limit the number of times onGetText is called. Set it to `-1` for infinite.
182-
*/
183-
constructor(private fix: Rule.Fix, private onGetText: () => void, private limit = 1) {}
184-
185-
get range() {
186-
return this.fix.range;
187-
}
188-
189-
get text() {
190-
if (this.limit) {
191-
this.limit--;
192-
this.onGetText();
193-
}
194-
return this.fix.text;
195-
}
196-
}
197-
198-
function isCustomWordListFile(value: string | CustomWordListFile | undefined): value is CustomWordListFile {
199-
return !!value && typeof value === 'object';
200-
}

packages/cspell-eslint-plugin/src/options.ts

-6
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,6 @@ export interface CustomWordListFile {
8686
* File format: 1 word per line
8787
*/
8888
path: CustomWordListFilePath;
89-
/**
90-
* **Experimental**: Provide a fix option to add words to the file.
91-
*
92-
* Note: this does not yet work perfectly.
93-
*/
94-
addWords?: boolean;
9589
}
9690

9791
export const defaultCheckOptions: Required<Check> = {

packages/cspell-eslint-plugin/src/worker.ts packages/cspell-eslint-plugin/src/spellCheck.ts

+8-24
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,12 @@ import assert from 'assert';
44
import type { CSpellSettings, TextDocument, ValidationIssue } from 'cspell-lib';
55
import { createTextDocument, DocumentValidator, refreshDictionaryCache } from 'cspell-lib';
66
import type { Comment, Identifier, ImportSpecifier, Literal, Node, TemplateElement } from 'estree';
7-
import { walk } from 'estree-walker';
87
import * as path from 'path';
98
import { format } from 'util';
109

10+
import type { ASTNode, JSXText } from './ASTNode';
1111
import type { CustomWordListFile, WorkerOptions } from './options';
12-
13-
interface JSXText extends Omit<Literal, 'type'> {
14-
type: 'JSXText';
15-
}
12+
import { walkTree } from './walkTree';
1613

1714
export interface Issue {
1815
start: number;
@@ -22,8 +19,6 @@ export interface Issue {
2219
suggestions: string[] | undefined;
2320
}
2421

25-
type ASTNode = (Node | Comment | JSXText) & { parent?: Node };
26-
2722
const defaultSettings: CSpellSettings = {
2823
patterns: [
2924
// @todo: be able to use cooked / transformed strings.
@@ -41,12 +36,16 @@ function log(...args: Parameters<typeof console.log>) {
4136
console.log(...args);
4237
}
4338

44-
export function spellCheck(filename: string, text: string, root: Node, options: WorkerOptions): Issue[] {
39+
type SpellCheckFn = typeof spellCheck;
40+
41+
export type SpellCheckSyncFn = (...p: Parameters<SpellCheckFn>) => Awaited<ReturnType<SpellCheckFn>>;
42+
43+
export async function spellCheck(filename: string, text: string, root: Node, options: WorkerOptions): Promise<Issue[]> {
4544
const toIgnore = new Set<string>();
4645
const importedIdentifiers = new Set<string>();
4746
isDebugMode = options.debugMode || false;
4847
const validator = getDocValidator(filename, text, options);
49-
validator.prepareSync();
48+
await validator.prepare();
5049
const issues: Issue[] = [];
5150

5251
function checkLiteral(node: Literal | ASTNode) {
@@ -373,18 +372,3 @@ function getTextDocument(filename: string, content: string): TextDocument {
373372
function isCustomWordListFile(value: string | CustomWordListFile | undefined): value is CustomWordListFile {
374373
return !!value && typeof value === 'object';
375374
}
376-
377-
export function walkTree(node: ASTNode, enter: (node: ASTNode, parent: ASTNode | undefined, key: string) => void) {
378-
const visited = new Set<object>();
379-
380-
walk(node, {
381-
enter: function (node, parent, key) {
382-
if (visited.has(node) || key === 'tokens') {
383-
this.skip();
384-
return;
385-
}
386-
visited.add(node);
387-
enter(node as ASTNode, parent as ASTNode, key);
388-
},
389-
});
390-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { walk } from 'estree-walker';
2+
3+
import type { ASTNode } from './ASTNode';
4+
5+
export function walkTree(node: ASTNode, enter: (node: ASTNode, parent: ASTNode | undefined, key: string) => void) {
6+
const visited = new Set<object>();
7+
8+
walk(node, {
9+
enter: function (node, parent, key) {
10+
if (visited.has(node) || key === 'tokens') {
11+
this.skip();
12+
return;
13+
}
14+
visited.add(node);
15+
enter(node as ASTNode, parent as ASTNode, key);
16+
},
17+
});
18+
}

0 commit comments

Comments
 (0)