@cspell/eslint-plugin 8.8.4 → 8.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -192,6 +192,7 @@ interface Options {
192
192
  /** Define dictionaries. */
193
193
  dictionaryDefinitions?: DictionaryDefinition[];
194
194
  };
195
+
195
196
  /**
196
197
  * Specify a path to a custom word list file.
197
198
  *
@@ -201,6 +202,35 @@ interface Options {
201
202
  * ```
202
203
  */
203
204
  customWordListFile?: string | { path: string };
205
+
206
+ /**
207
+ * Scope selectors to spell check.
208
+ * This is a list of scope selectors to spell check.
209
+ *
210
+ * Example:
211
+ * ```js
212
+ * checkScope: [
213
+ * ['YAMLPair[key] YAMLScalar', true],
214
+ * ['YAMLPair[value] YAMLScalar', true],
215
+ * ['YAMLSequence[entries] YAMLScalar', true],
216
+ * ['JSONProperty[key] JSONLiteral', true],
217
+ * ['JSONProperty[value] JSONLiteral', true],
218
+ * ['JSONArrayExpression JSONLiteral', true],
219
+ * ],
220
+ * ```
221
+ *
222
+ * To turn off checking JSON keys, use the following:
223
+ *
224
+ * ```js
225
+ * checkScope: [
226
+ * ['JSONProperty[key] JSONLiteral', false],
227
+ * ],
228
+ * ```
229
+ *
230
+ * @since 8.9.0
231
+ */
232
+ checkScope?: ScopeSelectorList;
233
+
204
234
  /**
205
235
  * Output debug logs
206
236
  * @default false
@@ -294,6 +324,24 @@ When spell checking, if `colour` is not in one of the dictionaries, then `color`
294
324
 
295
325
  CSpell will match case, but not word stems. `blacklist` and `Blacklist` will get replaced, but not `blacklists`.
296
326
 
327
+ ## Checking Custom AST Nodes
328
+
329
+ The `checkScope` setting is used to enable / disable checking AST Nodes. ESLint uses parsers to generate the AST (Abstract Syntax Tree) to evaluate a document. Each PlugIn gets access to the AST. `checkScope` can be used to handle new AST nodes when a custom parser is added. Some knowledge of the AST output by the parser is necessary.
330
+
331
+ ```js
332
+ rules: {
333
+ '@cspell/spellchecker': ['warn', { checkScope: [
334
+ ['JSONLiteral': true], // will match AST Nodes of type `JSONLiteral` and spell check the value.
335
+ ['JSONProperty[key] JSONLiteral', false] // will turn off checking the JSON Property keys.
336
+ ['JSONProperty JSONLiteral', false] // will turn off checking the JSON Property keys and values.
337
+ ['JSONProperty[value] JSONLiteral', true] // will turn on checking the JSON Property values.
338
+ ['YAMLPair[key] YAMLScalar', true],
339
+ ['YAMLPair[value] YAMLScalar', true],
340
+ ['YAMLSequence YAMLScalar', true],
341
+ ] }],
342
+ },
343
+ ```
344
+
297
345
  ## In Combination with CSpell
298
346
 
299
347
  Due to the nature of how files are parsed, the `cspell` command line tool and this ESLint plugin will give different results.
@@ -23,6 +23,25 @@
23
23
  "description": "Spell check JSX Text",
24
24
  "type": "boolean"
25
25
  },
26
+ "checkScope": {
27
+ "description": "Scope selectors to spell check. This is a list of scope selectors to spell check.\n\nExample: ```js checkScope: [ ['YAMLPair[key] YAMLScalar', true], ['YAMLPair[value] YAMLScalar', true], ['YAMLSequence[entries] YAMLScalar', true], ['JSONProperty[key] JSONLiteral', true], ['JSONProperty[value] JSONLiteral', true], ['JSONArrayExpression JSONLiteral', true], ], ```",
28
+ "items": {
29
+ "description": "A scope selector entry is a tuple that defines a scope selector and whether to spell check it.",
30
+ "items": [
31
+ {
32
+ "description": "The scope selector is a string that defines the context in which a rule applies. Examples:\n- `YAMLPair[value] YAMLScalar` - check the value of a YAML pair.\n- `YAMLPair[key] YAMLScalar` - check the key of a YAML pair.",
33
+ "type": "string"
34
+ },
35
+ {
36
+ "type": "boolean"
37
+ }
38
+ ],
39
+ "maxItems": 2,
40
+ "minItems": 2,
41
+ "type": "array"
42
+ },
43
+ "type": "array"
44
+ },
26
45
  "checkStringTemplates": {
27
46
  "default": true,
28
47
  "description": "Spell check template strings",
@@ -90,6 +90,25 @@ export interface Check {
90
90
  * ```
91
91
  */
92
92
  customWordListFile?: CustomWordListFilePath | CustomWordListFile | undefined;
93
+ /**
94
+ * Scope selectors to spell check.
95
+ * This is a list of scope selectors to spell check.
96
+ *
97
+ * Example:
98
+ * ```js
99
+ * checkScope: [
100
+ * ['YAMLPair[key] YAMLScalar', true],
101
+ * ['YAMLPair[value] YAMLScalar', true],
102
+ * ['YAMLSequence[entries] YAMLScalar', true],
103
+ * ['JSONProperty[key] JSONLiteral', true],
104
+ * ['JSONProperty[value] JSONLiteral', true],
105
+ * ['JSONArrayExpression JSONLiteral', true],
106
+ * ],
107
+ * ```
108
+ *
109
+ * @since 8.9.0
110
+ */
111
+ checkScope?: ScopeSelectorList;
93
112
  }
94
113
  /**
95
114
  * Specify a path to a custom word list file
@@ -106,5 +125,20 @@ export type WorkerOptions = RequiredOptions & {
106
125
  cwd: string;
107
126
  };
108
127
  export declare const defaultOptions: Options;
128
+ /**
129
+ * The scope selector is a string that defines the context in which a rule applies.
130
+ * Examples:
131
+ * - `YAMLPair[value] YAMLScalar` - check the value of a YAML pair.
132
+ * - `YAMLPair[key] YAMLScalar` - check the key of a YAML pair.
133
+ */
134
+ export type ScopeSelector = string;
135
+ /**
136
+ * A scope selector entry is a tuple that defines a scope selector and whether to spell check it.
137
+ */
138
+ export type ScopeSelectorEntry = [ScopeSelector, boolean];
139
+ /**
140
+ * A list of scope selectors.
141
+ */
142
+ export type ScopeSelectorList = ScopeSelectorEntry[];
109
143
  export {};
110
144
  //# sourceMappingURL=options.d.cts.map
@@ -83,11 +83,12 @@ function create(context) {
83
83
  };
84
84
  context.report(des);
85
85
  }
86
- function checkProgram() {
86
+ function checkProgram(_node) {
87
+ const filename = context.filename || context.getFilename();
87
88
  const sc = context.sourceCode || context.getSourceCode();
88
89
  if (!sc)
89
90
  return;
90
- const { issues, errors } = spellCheck(context.filename || context.getFilename(), sc.text, sc.ast, options);
91
+ const { issues, errors } = spellCheck(filename, sc.text, sc.ast, options);
91
92
  if (errors && errors.length) {
92
93
  log('errors: %o', errors.map((e) => e.message));
93
94
  errors.forEach((error) => console.error('%s', error.message));
@@ -16,6 +16,7 @@ exports.defaultCheckOptions = {
16
16
  customWordListFile: undefined,
17
17
  ignoreImportProperties: true,
18
18
  ignoreImports: true,
19
+ checkScope: [],
19
20
  };
20
21
  exports.defaultOptions = {
21
22
  ...exports.defaultCheckOptions,
@@ -2,8 +2,8 @@ import type { Comment, Literal, Node } from 'estree';
2
2
  export interface JSXText extends Omit<Literal, 'type'> {
3
3
  type: 'JSXText';
4
4
  }
5
- export type ASTNode = (Node | Comment | JSXText) & {
5
+ export type ASTNode = Readonly<(Node | Comment | JSXText) & {
6
6
  parent?: Node;
7
- };
7
+ }>;
8
8
  export type NodeType = ASTNode['type'];
9
9
  //# sourceMappingURL=ASTNode.d.cts.map
@@ -0,0 +1,12 @@
1
+ import type { ASTNode } from './ASTNode.cjs';
2
+ export type Key = string | number | symbol | null | undefined;
3
+ export interface ASTPathElement {
4
+ node: ASTNode;
5
+ parent: ASTNode | undefined;
6
+ key: Key;
7
+ index?: number | undefined;
8
+ }
9
+ export interface ASTPath extends ASTPathElement {
10
+ prev: ASTPath | undefined;
11
+ }
12
+ //# sourceMappingURL=ASTPath.d.mts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ASTPath.mjs.map
@@ -0,0 +1,3 @@
1
+ import type { RequiredOptions } from '../common/options.cjs';
2
+ export declare const defaultCheckedScopes: RequiredOptions['checkScope'];
3
+ //# sourceMappingURL=customScopes.d.mts.map
@@ -0,0 +1,13 @@
1
+ export const defaultCheckedScopes = [
2
+ ['YAMLPair YAMLScalar', true],
3
+ ['YAMLSequence YAMLScalar', true],
4
+ ['JSONProperty JSONLiteral', true],
5
+ ['JSONArrayExpression JSONLiteral', true],
6
+ // ['YAMLPair[key] YAMLScalar', true],
7
+ // ['YAMLPair[value] YAMLScalar', true],
8
+ // ['YAMLSequence[entries] YAMLScalar', true],
9
+ // ['JSONProperty[key] JSONLiteral', true],
10
+ // ['JSONProperty[value] JSONLiteral', true],
11
+ // ['JSONArrayExpression[elements] JSONLiteral', true],
12
+ ];
13
+ //# sourceMappingURL=customScopes.mjs.map
@@ -0,0 +1,58 @@
1
+ import type { ASTPath, Key } from './ASTPath.mjs';
2
+ export type ScopeScore = number;
3
+ export declare class AstScopeMatcher {
4
+ readonly scope: ScopeItem[];
5
+ constructor(scope: ScopeItem[]);
6
+ static fromScopeSelector(scopeSelector: string): AstScopeMatcher;
7
+ /**
8
+ * Score the astScope based on the given scope.
9
+ * @param astScope The scope to score.
10
+ * @returns The score of the scope. 0 = no match, higher the score the better the match.
11
+ */
12
+ score(astScope: string[]): ScopeScore;
13
+ /**
14
+ * Score the astScope based on the given scope.
15
+ * @param astScope The scope to score.
16
+ * @returns The score of the scope. 0 = no match, higher the score the better the match.
17
+ */
18
+ scoreItems(scopeItems: ScopeItem[]): ScopeScore;
19
+ matchPath(path: ASTPath): ScopeScore;
20
+ scopeField(): string;
21
+ scopeType(): string;
22
+ }
23
+ export interface ScopeItem {
24
+ type: string;
25
+ childKey: string | undefined;
26
+ }
27
+ export declare function scopeItem(type: string, childKey?: string): ScopeItem;
28
+ export declare function parseScope(scope: string): ScopeItem[];
29
+ export interface ASTPathNodeToScope {
30
+ /**
31
+ * Convert a path node into a scope.
32
+ * @param node - The node to convert
33
+ * @param childKey - The key to the child node.
34
+ */
35
+ (node: ASTPath, childKey: Key | undefined): ScopeItem;
36
+ }
37
+ export declare function keyToString(key: Key): string | undefined;
38
+ export declare function mapNodeToScope(p: ASTPath, key: Key | undefined): ScopeItem;
39
+ export declare function mapNodeToScopeItem(p: ASTPath, childKey: Key | undefined): ScopeItem;
40
+ export declare function mapScopeItemToString(item: ScopeItem): string;
41
+ /**
42
+ * Convert an ASTPath to a scope.
43
+ * @param path - The path to convert to a scope.
44
+ * @returns
45
+ */
46
+ export declare function astPathToScope(path: ASTPath | undefined, mapFn?: ASTPathNodeToScope): string[];
47
+ export declare function astScopeToString(path: ASTPath | undefined, sep?: string, mapFn?: ASTPathNodeToScope): string;
48
+ export declare function astPathToScopeItems(path: ASTPath | undefined, mapFn?: ASTPathNodeToScope): ScopeItem[];
49
+ export declare class AstPathScope {
50
+ readonly path: ASTPath;
51
+ private items;
52
+ constructor(path: ASTPath);
53
+ get scope(): string[];
54
+ get scopeItems(): ScopeItem[];
55
+ get scopeString(): string;
56
+ score(matcher: AstScopeMatcher): ScopeScore;
57
+ }
58
+ //# sourceMappingURL=scope.d.mts.map
@@ -0,0 +1,149 @@
1
+ import assert from 'node:assert';
2
+ export class AstScopeMatcher {
3
+ scope;
4
+ constructor(scope) {
5
+ this.scope = scope;
6
+ }
7
+ static fromScopeSelector(scopeSelector) {
8
+ return new AstScopeMatcher(parseScope(scopeSelector).reverse());
9
+ }
10
+ /**
11
+ * Score the astScope based on the given scope.
12
+ * @param astScope The scope to score.
13
+ * @returns The score of the scope. 0 = no match, higher the score the better the match.
14
+ */
15
+ score(astScope) {
16
+ try {
17
+ const scopeItems = astScope.map(parseScopeItem).reverse();
18
+ return this.scoreItems(scopeItems);
19
+ }
20
+ catch {
21
+ console.error('Failed to parse scope: %o', astScope);
22
+ return 0;
23
+ }
24
+ }
25
+ /**
26
+ * Score the astScope based on the given scope.
27
+ * @param astScope The scope to score.
28
+ * @returns The score of the scope. 0 = no match, higher the score the better the match.
29
+ */
30
+ scoreItems(scopeItems) {
31
+ const scope = this.scope;
32
+ let score = 0;
33
+ let scale = 1;
34
+ let matchKey = false;
35
+ for (let i = 0; i < scope.length; i++) {
36
+ const item = scopeItems[i];
37
+ if (!item)
38
+ return 0;
39
+ const curr = scope[i];
40
+ if (curr.type !== item.type)
41
+ return 0;
42
+ if (curr.childKey && item.childKey && curr.childKey !== item.childKey)
43
+ return 0;
44
+ if (curr.childKey && !item.childKey && matchKey)
45
+ return 0;
46
+ if (curr.childKey && (curr.childKey == item.childKey || !matchKey)) {
47
+ score += scale;
48
+ }
49
+ score += scale * 2;
50
+ matchKey = true;
51
+ scale *= 4;
52
+ }
53
+ return score;
54
+ }
55
+ matchPath(path) {
56
+ const s = this.scope[0];
57
+ // Early out
58
+ if (s?.type !== path.node.type)
59
+ return 0;
60
+ const items = astPathToScopeItems(path);
61
+ return this.scoreItems(items);
62
+ }
63
+ scopeField() {
64
+ return this.scope[0]?.childKey || 'value';
65
+ }
66
+ scopeType() {
67
+ return this.scope[0]?.type || '';
68
+ }
69
+ }
70
+ export function scopeItem(type, childKey) {
71
+ return { type, childKey };
72
+ }
73
+ const regexValidScope = /^([\w.-]+)(?:\[([\w<>.-]*)\])?$/;
74
+ function parseScopeItem(item) {
75
+ const match = item.match(regexValidScope);
76
+ assert(match, `Invalid scope item: ${item}`);
77
+ const [_, type, key] = match;
78
+ return { type, childKey: key || undefined };
79
+ }
80
+ export function parseScope(scope) {
81
+ return scope
82
+ .split(' ')
83
+ .filter((s) => s)
84
+ .map(parseScopeItem);
85
+ }
86
+ export function keyToString(key) {
87
+ return key === undefined || key === null
88
+ ? undefined
89
+ : typeof key === 'symbol'
90
+ ? `<${Symbol.keyFor(key)}>`
91
+ : `${key}`;
92
+ }
93
+ export function mapNodeToScope(p, key) {
94
+ return mapNodeToScopeItem(p, key);
95
+ }
96
+ export function mapNodeToScopeItem(p, childKey) {
97
+ return scopeItem(p.node.type, keyToString(childKey));
98
+ }
99
+ export function mapScopeItemToString(item) {
100
+ const { type, childKey: k } = item;
101
+ return k === undefined ? type : `${type}[${k}]`;
102
+ }
103
+ /**
104
+ * Convert an ASTPath to a scope.
105
+ * @param path - The path to convert to a scope.
106
+ * @returns
107
+ */
108
+ export function astPathToScope(path, mapFn = mapNodeToScope) {
109
+ return astPathToScopeItems(path, mapFn).map(mapScopeItemToString).reverse();
110
+ }
111
+ export function astScopeToString(path, sep = ' ', mapFn) {
112
+ return astPathToScope(path, mapFn).join(sep);
113
+ }
114
+ export function astPathToScopeItems(path, mapFn = mapNodeToScope) {
115
+ const parts = [];
116
+ let key = undefined;
117
+ while (path) {
118
+ parts.push(mapFn(path, key));
119
+ key = path?.key;
120
+ path = path.prev;
121
+ }
122
+ return parts;
123
+ }
124
+ export class AstPathScope {
125
+ path;
126
+ items;
127
+ constructor(path) {
128
+ this.path = path;
129
+ this.items = astPathToScopeItems(path);
130
+ }
131
+ get scope() {
132
+ return this.items.map(mapScopeItemToString).reverse();
133
+ }
134
+ get scopeItems() {
135
+ return this.items;
136
+ }
137
+ get scopeString() {
138
+ return this.scope.join(' ');
139
+ }
140
+ score(matcher) {
141
+ const field = matcher.scopeField();
142
+ const node = this.path.node;
143
+ if (field in node && typeof node[field] === 'string') {
144
+ return matcher.scoreItems(this.items);
145
+ }
146
+ return 0;
147
+ }
148
+ }
149
+ //# sourceMappingURL=scope.mjs.map
@@ -1,9 +1,10 @@
1
1
  // cspell:ignore TSESTree
2
2
  import assert from 'node:assert';
3
3
  import * as path from 'node:path';
4
- import { format } from 'node:util';
5
4
  import { createTextDocument, DocumentValidator, extractImportErrors, getDictionary, refreshDictionaryCache, } from 'cspell-lib';
6
5
  import { getDefaultLogger } from '../common/logger.cjs';
6
+ import { defaultCheckedScopes } from './customScopes.mjs';
7
+ import { AstPathScope, AstScopeMatcher, astScopeToString, mapNodeToScope, scopeItem } from './scope.mjs';
7
8
  import { walkTree } from './walkTree.mjs';
8
9
  const defaultSettings = {
9
10
  patterns: [
@@ -16,12 +17,14 @@ const defaultSettings = {
16
17
  ],
17
18
  };
18
19
  const isDebugModeExtended = false;
20
+ const forceLogging = false;
19
21
  const knownConfigErrors = new Set();
20
22
  export async function spellCheck(filename, text, root, options) {
21
23
  const logger = getDefaultLogger();
22
- const debugMode = options.debugMode || false;
23
- logger.enabled = options.debugMode ?? (logger.enabled || isDebugModeExtended);
24
+ const debugMode = forceLogging || options.debugMode || false;
25
+ logger.enabled = forceLogging || (options.debugMode ?? (logger.enabled || isDebugModeExtended));
24
26
  const log = logger.log;
27
+ const mapScopes = groupScopes([...defaultCheckedScopes, ...(options.checkScope || [])]);
25
28
  log('options: %o', options);
26
29
  const toIgnore = new Set();
27
30
  const importedIdentifiers = new Set();
@@ -37,42 +40,46 @@ export async function spellCheck(filename, text, root, options) {
37
40
  found.forEach((err) => (debugMode ? log(err) : log('Error: %s', err.message)));
38
41
  return found;
39
42
  }
40
- function checkLiteral(node) {
43
+ function checkLiteral(path) {
44
+ const node = path.node;
41
45
  if (node.type !== 'Literal')
42
46
  return;
43
47
  if (!options.checkStrings)
44
48
  return;
45
49
  if (typeof node.value === 'string') {
46
- debugNode(node, node.value);
50
+ debugNode(path, node.value);
47
51
  if (options.ignoreImports && isImportOrRequired(node))
48
52
  return;
49
53
  if (options.ignoreImportProperties && isImportedProperty(node))
50
54
  return;
51
- checkNodeText(node, node.value);
55
+ checkNodeText(path, node.value);
52
56
  }
53
57
  }
54
- function checkJSXText(node) {
58
+ function checkJSXText(path) {
59
+ const node = path.node;
55
60
  if (node.type !== 'JSXText')
56
61
  return;
57
62
  if (!options.checkJSXText)
58
63
  return;
59
64
  if (typeof node.value === 'string') {
60
- debugNode(node, node.value);
61
- checkNodeText(node, node.value);
65
+ debugNode(path, node.value);
66
+ checkNodeText(path, node.value);
62
67
  }
63
68
  }
64
- function checkTemplateElement(node) {
69
+ function checkTemplateElement(path) {
70
+ const node = path.node;
65
71
  if (node.type !== 'TemplateElement')
66
72
  return;
67
73
  if (!options.checkStringTemplates)
68
74
  return;
69
- debugNode(node, node.value);
70
- checkNodeText(node, node.value.cooked || node.value.raw);
75
+ debugNode(path, node.value);
76
+ checkNodeText(path, node.value.cooked || node.value.raw);
71
77
  }
72
- function checkIdentifier(node) {
78
+ function checkIdentifier(path) {
79
+ const node = path.node;
73
80
  if (node.type !== 'Identifier')
74
81
  return;
75
- debugNode(node, node.name);
82
+ debugNode(path, node.name);
76
83
  if (options.ignoreImports) {
77
84
  if (isRawImportIdentifier(node)) {
78
85
  toIgnore.add(node.name);
@@ -81,7 +88,7 @@ export async function spellCheck(filename, text, root, options) {
81
88
  if (isImportIdentifier(node)) {
82
89
  importedIdentifiers.add(node.name);
83
90
  if (isLocalImportIdentifierUnique(node)) {
84
- checkNodeText(node, node.name);
91
+ checkNodeText(path, node.name);
85
92
  }
86
93
  return;
87
94
  }
@@ -95,26 +102,28 @@ export async function spellCheck(filename, text, root, options) {
95
102
  return;
96
103
  if (skipCheckForRawImportIdentifiers(node))
97
104
  return;
98
- checkNodeText(node, node.name);
105
+ checkNodeText(path, node.name);
99
106
  }
100
- function checkComment(node) {
107
+ function checkComment(path) {
108
+ const node = path.node;
101
109
  if (node.type !== 'Line' && node.type !== 'Block')
102
110
  return;
103
111
  if (!options.checkComments)
104
112
  return;
105
- debugNode(node, node.value);
106
- checkNodeText(node, node.value);
113
+ debugNode(path, node.value);
114
+ checkNodeText(path, node.value);
107
115
  }
108
- function checkNodeText(node, text) {
116
+ function checkNodeText(path, text) {
117
+ const node = path.node;
109
118
  if (!node.range)
110
119
  return;
111
120
  const adj = node.type === 'Literal' ? 1 : 0;
112
121
  const range = [node.range[0] + adj, node.range[1] - adj];
113
- const scope = calcScope(node);
122
+ const scope = calcScope(path);
114
123
  const result = validator.checkText(range, text, scope);
115
124
  result.forEach((issue) => reportIssue(issue, node.type));
116
125
  }
117
- function calcScope(_node) {
126
+ function calcScope(_path) {
118
127
  // inheritance(node);
119
128
  return [];
120
129
  }
@@ -176,74 +185,64 @@ export async function spellCheck(filename, text, root, options) {
176
185
  Identifier: checkIdentifier,
177
186
  JSXText: checkJSXText,
178
187
  };
179
- function checkNode(node) {
180
- processors[node.type]?.(node);
181
- }
182
- function mapNode(node, index, nodes) {
183
- const child = nodes[index + 1];
184
- if (node.type === 'ImportSpecifier') {
185
- const extra = node.imported === child ? '.imported' : node.local === child ? '.local' : '';
186
- return node.type + extra;
187
- }
188
- if (node.type === 'ImportDeclaration') {
189
- const extra = node.source === child ? '.source' : '';
190
- return node.type + extra;
191
- }
192
- if (node.type === 'ExportSpecifier') {
193
- const extra = node.exported === child ? '.exported' : node.local === child ? '.local' : '';
194
- return node.type + extra;
195
- }
196
- if (node.type === 'ExportNamedDeclaration') {
197
- const extra = node.source === child ? '.source' : '';
198
- return node.type + extra;
199
- }
200
- if (node.type === 'Property') {
201
- const extra = node.key === child ? 'key' : node.value === child ? 'value' : '';
202
- return [node.type, node.kind, extra].join('.');
203
- }
204
- if (node.type === 'MemberExpression') {
205
- const extra = node.property === child ? 'property' : node.object === child ? 'object' : '';
206
- return node.type + '.' + extra;
207
- }
208
- if (node.type === 'ArrowFunctionExpression') {
209
- const extra = node.body === child ? 'body' : 'param';
210
- return node.type + '.' + extra;
211
- }
212
- if (node.type === 'FunctionDeclaration') {
213
- const extra = node.id === child ? 'id' : node.body === child ? 'body' : 'params';
214
- return node.type + '.' + extra;
215
- }
216
- if (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') {
217
- const extra = node.id === child ? 'id' : node.body === child ? 'body' : 'superClass';
218
- return node.type + '.' + extra;
219
- }
220
- if (node.type === 'CallExpression') {
221
- const extra = node.callee === child ? 'callee' : 'arguments';
222
- return node.type + '.' + extra;
223
- }
224
- if (node.type === 'Literal') {
225
- return tagLiteral(node);
226
- }
227
- if (node.type === 'Block') {
228
- return node.value[0] === '*' ? 'Comment.docBlock' : 'Comment.block';
229
- }
230
- if (node.type === 'Line') {
231
- return 'Comment.line';
188
+ function needToCheckFields(path) {
189
+ const possibleScopes = mapScopes.get(path.node.type);
190
+ if (!possibleScopes) {
191
+ _dumpNode(path);
192
+ return undefined;
232
193
  }
233
- return node.type;
234
- }
235
- function inheritance(node) {
236
- const a = [...parents(node), node];
237
- return a.map(mapNode);
194
+ const scopePath = new AstPathScope(path);
195
+ const scores = possibleScopes
196
+ .map(({ scope, check }) => ({ score: scopePath.score(scope), check, scope }))
197
+ .filter((s) => s.score > 0);
198
+ const maxScore = Math.max(0, ...scores.map((s) => s.score));
199
+ const topScopes = scores.filter((s) => s.score === maxScore);
200
+ if (!topScopes.length)
201
+ return undefined;
202
+ return Object.fromEntries(topScopes.map((s) => [s.scope.scopeField(), s.check]));
238
203
  }
239
- function* parents(node) {
240
- while (node && node.parent) {
241
- yield node.parent;
242
- node = node.parent;
204
+ function defaultHandler(path) {
205
+ const fields = needToCheckFields(path);
206
+ if (!fields)
207
+ return;
208
+ for (const [field, check] of Object.entries(fields)) {
209
+ if (!check)
210
+ continue;
211
+ const node = path.node;
212
+ const value = node[field];
213
+ if (typeof value !== 'string')
214
+ continue;
215
+ debugNode(path, value);
216
+ checkNodeText(path, value);
243
217
  }
244
218
  }
245
- function inheritanceSummary(node) {
246
- return inheritance(node).join(' ');
219
+ function checkNode(path) {
220
+ // _dumpNode(path);
221
+ const handler = processors[path.node.type] ?? defaultHandler;
222
+ handler(path);
223
+ }
224
+ function _dumpNode(path) {
225
+ function value(v) {
226
+ if (['string', 'number', 'boolean'].includes(typeof v))
227
+ return v;
228
+ if (v && typeof v === 'object' && 'type' in v)
229
+ return `{ type: ${v.type} }`;
230
+ return `<${v}>`;
231
+ }
232
+ function dotValue(v) {
233
+ if (typeof v === 'object' && v) {
234
+ return Object.fromEntries(Object.entries(v).map(([k, v]) => [k, value(v)]));
235
+ }
236
+ return `<${typeof v}>`;
237
+ }
238
+ const { parent: _, ...n } = path.node;
239
+ const warn = log;
240
+ warn('Node: %o', {
241
+ key: path.key,
242
+ type: n.type,
243
+ path: inheritanceSummary(path),
244
+ node: dotValue(n),
245
+ });
247
246
  }
248
247
  /**
249
248
  * find the origin of a member expression
@@ -259,7 +258,9 @@ export async function spellCheck(filename, text, root, options) {
259
258
  return obj;
260
259
  }
261
260
  function isFunctionCall(node, name) {
262
- return node?.type === 'CallExpression' && node.callee.type === 'Identifier' && node.callee.name === name;
261
+ if (!node)
262
+ return false;
263
+ return node.type === 'CallExpression' && node.callee.type === 'Identifier' && node.callee.name === name;
263
264
  }
264
265
  function isRequireCall(node) {
265
266
  return isFunctionCall(node, 'require');
@@ -267,20 +268,36 @@ export async function spellCheck(filename, text, root, options) {
267
268
  function isImportOrRequired(node) {
268
269
  return isRequireCall(node.parent) || (node.parent?.type === 'ImportDeclaration' && node.parent.source === node);
269
270
  }
270
- function debugNode(node, value) {
271
- if (!isDebugModeExtended)
272
- return;
273
- const val = format('%o', value);
274
- log(`${inheritanceSummary(node)}: ${val}`);
271
+ function debugNode(path, value) {
272
+ log(`${inheritanceSummary(path)}: %o`, value);
273
+ debugMode && _dumpNode(path);
275
274
  }
275
+ // console.warn('root: %o', root);
276
276
  walkTree(root, checkNode);
277
277
  return { issues, errors };
278
278
  }
279
+ function mapNode(path, key) {
280
+ const node = path.node;
281
+ if (node.type === 'Literal') {
282
+ return scopeItem(tagLiteral(node));
283
+ }
284
+ if (node.type === 'Block') {
285
+ const value = typeof node.value === 'string' ? node.value : '';
286
+ return scopeItem(value[0] === '*' ? 'Comment.docBlock' : 'Comment.block');
287
+ }
288
+ if (node.type === 'Line') {
289
+ return scopeItem('Comment.line');
290
+ }
291
+ return mapNodeToScope(path, key);
292
+ }
293
+ function inheritanceSummary(path) {
294
+ return astScopeToString(path, ' ', mapNode);
295
+ }
279
296
  function tagLiteral(node) {
280
297
  assert(node.type === 'Literal');
281
298
  const kind = typeof node.value;
282
299
  const extra = kind === 'string'
283
- ? node.raw?.[0] === '"'
300
+ ? asStr(node.raw)?.[0] === '"'
284
301
  ? 'string.double'
285
302
  : 'string.single'
286
303
  : node.value === null
@@ -416,4 +433,19 @@ async function reportConfigurationErrors(config, knownConfigErrors) {
416
433
  });
417
434
  return errors;
418
435
  }
436
+ function groupScopes(scopes) {
437
+ const objScopes = Object.fromEntries(scopes);
438
+ const map = new Map();
439
+ for (const [selector, check] of Object.entries(objScopes)) {
440
+ const scope = AstScopeMatcher.fromScopeSelector(selector);
441
+ const key = scope.scopeType();
442
+ const list = map.get(key) || [];
443
+ list.push({ scope, check });
444
+ map.set(key, list);
445
+ }
446
+ return map;
447
+ }
448
+ function asStr(v) {
449
+ return typeof v === 'string' ? v : undefined;
450
+ }
419
451
  //# sourceMappingURL=spellCheck.mjs.map
@@ -1,5 +1,4 @@
1
1
  import type { ASTNode } from './ASTNode.cjs';
2
- type Key = string | number | symbol | null | undefined;
3
- export declare function walkTree(node: ASTNode, enter: (node: ASTNode, parent: ASTNode | undefined, key: Key) => void): void;
4
- export {};
2
+ import type { ASTPath } from './ASTPath.mjs';
3
+ export declare function walkTree(node: ASTNode, enter: (path: ASTPath) => void): void;
5
4
  //# sourceMappingURL=walkTree.d.mts.map
@@ -1,15 +1,64 @@
1
- import { walk } from 'estree-walker';
1
+ // const logger = getDefaultLogger();
2
+ // const log = logger.log;
2
3
  export function walkTree(node, enter) {
3
4
  const visited = new Set();
4
- walk(node, {
5
- enter: function (node, parent, key) {
6
- if (visited.has(node) || key === 'tokens') {
7
- this.skip();
8
- return;
9
- }
10
- visited.add(node);
11
- enter(node, parent, key);
12
- },
5
+ let pathNode = undefined;
6
+ function adjustPath(n) {
7
+ if (!n.parent || !pathNode) {
8
+ pathNode = n;
9
+ n.prev = undefined;
10
+ return n;
11
+ }
12
+ if (pathNode.node === n.parent) {
13
+ n.prev = pathNode;
14
+ pathNode = n;
15
+ return n;
16
+ }
17
+ while (pathNode && pathNode.node !== n.parent) {
18
+ pathNode = pathNode.prev;
19
+ }
20
+ n.prev = pathNode;
21
+ pathNode = n;
22
+ return n;
23
+ }
24
+ walk(node, ({ node, parent, key, index }) => {
25
+ if (key === 'tokens' || key === 'parent' || visited.has(node)) {
26
+ return false;
27
+ }
28
+ visited.add(node);
29
+ const path = adjustPath({ node, parent: parent, key, index, prev: undefined });
30
+ enter(path);
31
+ return true;
13
32
  });
14
33
  }
34
+ function walk(root, enter) {
35
+ function walkNodes(pfx, node, parent, key, index) {
36
+ const goIn = enter({ node, parent, key, index });
37
+ // log('walk: %o', { pfx, type: node.type, key, index, goIn });
38
+ if (!goIn)
39
+ return;
40
+ const n = node;
41
+ for (const key of Object.keys(n)) {
42
+ const v = n[key];
43
+ const fx = pfx + `.${node.type}[${key}]`;
44
+ if (Array.isArray(v)) {
45
+ for (let i = 0; i < v.length; ++i) {
46
+ const vv = v[i];
47
+ isNode(vv) && walkNodes(fx, vv, node, key, i);
48
+ }
49
+ }
50
+ else if (isNode(v)) {
51
+ walkNodes(fx, v, node, key, undefined);
52
+ }
53
+ }
54
+ return true;
55
+ }
56
+ walkNodes('root', root, undefined, undefined, undefined);
57
+ }
58
+ function isNode(node) {
59
+ if (!node)
60
+ return false;
61
+ const n = node;
62
+ return (typeof n === 'object' && typeof n['type'] === 'string') || false;
63
+ }
15
64
  //# sourceMappingURL=walkTree.mjs.map
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "8.8.4",
6
+ "version": "8.9.0",
7
7
  "description": "CSpell ESLint plugin",
8
8
  "keywords": [
9
9
  "cspell",
@@ -45,9 +45,10 @@
45
45
  "assets",
46
46
  "dist",
47
47
  "!**/__mocks__",
48
- "!**/*.tsbuildInfo",
49
- "!**/*.test.*",
50
48
  "!**/*.spec.*",
49
+ "!**/*.test.*",
50
+ "!**/test*/**",
51
+ "!**/*.tsbuildInfo",
51
52
  "!**/*.map"
52
53
  ],
53
54
  "scripts": {
@@ -60,6 +61,7 @@
60
61
  "clean-build": "pnpm run clean && pnpm run build",
61
62
  "coverage": "echo coverage",
62
63
  "test-watch": "pnpm run test -- --watch",
64
+ "test-yaml": "npx mocha --timeout 10000 \"dist/**/yaml.test.mjs\"",
63
65
  "test": "npx mocha --timeout 10000 \"dist/**/*.test.mjs\""
64
66
  },
65
67
  "repository": {
@@ -75,29 +77,33 @@
75
77
  },
76
78
  "devDependencies": {
77
79
  "@eslint/eslintrc": "^3.1.0",
78
- "@eslint/js": "^9.4.0",
80
+ "@eslint/js": "^9.5.0",
79
81
  "@types/estree": "^1.0.5",
80
82
  "@types/mocha": "^10.0.6",
81
- "@typescript-eslint/parser": "^7.11.0",
82
- "@typescript-eslint/types": "^7.11.0",
83
- "eslint": "^9.4.0",
84
- "eslint-plugin-n": "^17.7.0",
83
+ "@typescript-eslint/parser": "^7.13.1",
84
+ "@typescript-eslint/types": "^7.13.1",
85
+ "eslint": "^9.5.0",
86
+ "eslint-plugin-jsonc": "^2.16.0",
87
+ "eslint-plugin-mdx": "^3.1.5",
88
+ "eslint-plugin-n": "^17.9.0",
85
89
  "eslint-plugin-react": "^7.34.2",
86
90
  "eslint-plugin-simple-import-sort": "^12.1.0",
87
- "globals": "^15.3.0",
91
+ "eslint-plugin-yml": "^1.14.0",
92
+ "globals": "^15.6.0",
93
+ "jsonc-eslint-parser": "^2.4.0",
88
94
  "mocha": "^10.4.0",
89
- "ts-json-schema-generator": "^2.2.0",
95
+ "ts-json-schema-generator": "^2.3.0",
90
96
  "typescript": "^5.4.5",
91
- "typescript-eslint": "^7.11.0"
97
+ "typescript-eslint": "^7.13.1",
98
+ "yaml-eslint-parser": "^1.2.3"
92
99
  },
93
100
  "dependencies": {
94
- "@cspell/cspell-types": "8.8.4",
95
- "cspell-lib": "8.8.4",
96
- "estree-walker": "^3.0.3",
101
+ "@cspell/cspell-types": "8.9.0",
102
+ "cspell-lib": "8.9.0",
97
103
  "synckit": "^0.9.0"
98
104
  },
99
105
  "peerDependencies": {
100
106
  "eslint": "^7 || ^8 || ^9"
101
107
  },
102
- "gitHead": "e1df92825ed0dacedb1830eeb6d918f01690c69a"
108
+ "gitHead": "33c513cf848a61fb1ebcea1b9c67505a31447411"
103
109
  }