eslint-plugin-flawless 0.1.10 → 0.1.12
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 +25 -0
- package/dist/index.d.mts +106 -60
- package/dist/index.mjs +1 -3067
- package/dist/oxlint.d.mts +5 -0
- package/dist/oxlint.mjs +25 -0
- package/dist/plugin-DYVtpuio.mjs +4226 -0
- package/dist/rules/arrow-return-style/worker.d.mts +25 -0
- package/dist/rules/arrow-return-style/worker.mjs +90 -0
- package/package.json +11 -2
package/dist/index.mjs
CHANGED
|
@@ -1,3070 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ASTUtils, AST_NODE_TYPES, ESLintUtils, TSESLint } from "@typescript-eslint/utils";
|
|
3
|
-
import { DefinitionType, ImplicitLibVariable, PatternVisitor, ScopeType, Visitor } from "@typescript-eslint/scope-manager";
|
|
4
|
-
import { requiresQuoting } from "@typescript-eslint/type-utils";
|
|
5
|
-
import assert from "node:assert";
|
|
6
|
-
import * as core from "@eslint-react/core";
|
|
7
|
-
import { findVariable } from "@typescript-eslint/utils/ast-utils";
|
|
8
|
-
import { getStaticTOMLValue } from "toml-eslint-parser";
|
|
9
|
-
//#region package.json
|
|
10
|
-
var name = "eslint-plugin-flawless";
|
|
11
|
-
var version = "0.1.10";
|
|
12
|
-
var repository = {
|
|
13
|
-
"url": "git+https://github.com/christopher-buss/eslint-plugin-flawless.git",
|
|
14
|
-
"type": "git"
|
|
15
|
-
};
|
|
16
|
-
//#endregion
|
|
17
|
-
//#region src/util.ts
|
|
18
|
-
const createRule = RuleCreator((name) => {
|
|
19
|
-
return `${repository.url.replace(/^git\+/, "").replace(/\.git$/, "")}/blob/v${version}/src/rules/${name}/documentation.md`;
|
|
20
|
-
});
|
|
21
|
-
/**
|
|
22
|
-
* Creates a rule with a docs URL, allowing a rule's `create` to receive a
|
|
23
|
-
* context with a custom `sourceCode` type (such as a YAML source code). The
|
|
24
|
-
* `SourceCode` type parameter defaults to ESLint's `SourceCode`, so standard
|
|
25
|
-
* rules are unaffected.
|
|
26
|
-
*
|
|
27
|
-
* @template Options - The rule's options tuple.
|
|
28
|
-
* @template MessageIds - The rule's message identifiers.
|
|
29
|
-
* @template SourceCode - The source code type exposed on `context.sourceCode`.
|
|
30
|
-
* @param rule - The rule definition (meta, name, defaultOptions, create).
|
|
31
|
-
* @returns The created rule module.
|
|
32
|
-
*/
|
|
33
|
-
function createEslintRule(rule) {
|
|
34
|
-
return createRule(rule);
|
|
35
|
-
}
|
|
36
|
-
//#endregion
|
|
37
|
-
//#region src/rules/jsx-shorthand-boolean/rule.ts
|
|
38
|
-
const RULE_NAME$9 = "jsx-shorthand-boolean";
|
|
39
|
-
const MESSAGE_ID$4 = "setAttributeValue";
|
|
40
|
-
const messages$9 = { [MESSAGE_ID$4]: "Set an explicit value for boolean attribute '{{name}}'." };
|
|
41
|
-
function create$9(context) {
|
|
42
|
-
const { sourceCode } = context;
|
|
43
|
-
return { JSXAttribute(node) {
|
|
44
|
-
if (node.value !== null) return;
|
|
45
|
-
context.report({
|
|
46
|
-
data: { name: sourceCode.getText(node.name) },
|
|
47
|
-
fix: (fixer) => fixer.insertTextAfter(node.name, "={true}"),
|
|
48
|
-
messageId: MESSAGE_ID$4,
|
|
49
|
-
node
|
|
50
|
-
});
|
|
51
|
-
} };
|
|
52
|
-
}
|
|
53
|
-
const jsxShorthandBoolean = createEslintRule({
|
|
54
|
-
name: RULE_NAME$9,
|
|
55
|
-
create: create$9,
|
|
56
|
-
defaultOptions: [],
|
|
57
|
-
meta: {
|
|
58
|
-
docs: {
|
|
59
|
-
description: "Disallow shorthand boolean JSX attributes",
|
|
60
|
-
recommended: false,
|
|
61
|
-
requiresTypeChecking: false
|
|
62
|
-
},
|
|
63
|
-
fixable: "code",
|
|
64
|
-
hasSuggestions: false,
|
|
65
|
-
messages: messages$9,
|
|
66
|
-
schema: [],
|
|
67
|
-
type: "suggestion"
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
//#endregion
|
|
71
|
-
//#region src/rules/jsx-shorthand-fragment/rule.ts
|
|
72
|
-
const RULE_NAME$8 = "jsx-shorthand-fragment";
|
|
73
|
-
const MESSAGE_ID_NAMED = "useNamedFragment";
|
|
74
|
-
const MESSAGE_ID_SHORTHAND = "useShorthandFragment";
|
|
75
|
-
const DEFAULT_MODE = "syntax";
|
|
76
|
-
const DEFAULT_FRAGMENT_NAME = "Fragment";
|
|
77
|
-
const messages$8 = {
|
|
78
|
-
[MESSAGE_ID_NAMED]: "Use the '{{name}}' component instead of fragment shorthand syntax.",
|
|
79
|
-
[MESSAGE_ID_SHORTHAND]: "Use the fragment shorthand syntax '<>...</>' instead of the '{{name}}' component."
|
|
80
|
-
};
|
|
81
|
-
const schema$2 = [{
|
|
82
|
-
additionalProperties: false,
|
|
83
|
-
properties: {
|
|
84
|
-
fragmentName: {
|
|
85
|
-
description: "The identifier to use for the named fragment element in \"element\" mode.",
|
|
86
|
-
type: "string"
|
|
87
|
-
},
|
|
88
|
-
mode: {
|
|
89
|
-
description: "Which form to enforce: \"syntax\" (shorthand `<>...</>`, default) or \"element\" (a named fragment).",
|
|
90
|
-
enum: ["element", "syntax"],
|
|
91
|
-
type: "string"
|
|
92
|
-
}
|
|
93
|
-
},
|
|
94
|
-
type: "object"
|
|
95
|
-
}];
|
|
96
|
-
/**
|
|
97
|
-
* Flattens a JSX element name into a dotted string, e.g. `Fragment` or
|
|
98
|
-
* `React.Fragment`. Returns `null` for namespaced names (`<a:b>`) which cannot
|
|
99
|
-
* be a fragment.
|
|
100
|
-
*
|
|
101
|
-
* @param node - The JSX element name node.
|
|
102
|
-
* @returns The dotted name, or `null` when it is not a plain identifier chain.
|
|
103
|
-
*/
|
|
104
|
-
function jsxNameToString(node) {
|
|
105
|
-
if (node.type === AST_NODE_TYPES.JSXIdentifier) return node.name;
|
|
106
|
-
if (node.type === AST_NODE_TYPES.JSXMemberExpression) {
|
|
107
|
-
const object = jsxNameToString(node.object);
|
|
108
|
-
if (object === null) return null;
|
|
109
|
-
return `${object}.${node.property.name}`;
|
|
110
|
-
}
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
function create$8(context) {
|
|
114
|
-
const options = context.options[0] ?? {};
|
|
115
|
-
const mode = options.mode ?? DEFAULT_MODE;
|
|
116
|
-
const fragmentName = options.fragmentName ?? DEFAULT_FRAGMENT_NAME;
|
|
117
|
-
if (mode === "element") return { JSXFragment(node) {
|
|
118
|
-
const { closingFragment, openingFragment } = node;
|
|
119
|
-
context.report({
|
|
120
|
-
data: { name: fragmentName },
|
|
121
|
-
fix: (fixer) => {
|
|
122
|
-
return [fixer.replaceText(openingFragment, `<${fragmentName}>`), fixer.replaceText(closingFragment, `</${fragmentName}>`)];
|
|
123
|
-
},
|
|
124
|
-
messageId: MESSAGE_ID_NAMED,
|
|
125
|
-
node
|
|
126
|
-
});
|
|
127
|
-
} };
|
|
128
|
-
const namedFragments = /* @__PURE__ */ new Set([
|
|
129
|
-
"Fragment",
|
|
130
|
-
fragmentName,
|
|
131
|
-
"React.Fragment"
|
|
132
|
-
]);
|
|
133
|
-
return { JSXElement(node) {
|
|
134
|
-
const { openingElement } = node;
|
|
135
|
-
const name = jsxNameToString(openingElement.name);
|
|
136
|
-
if (name === null || !namedFragments.has(name)) return;
|
|
137
|
-
if (openingElement.attributes.length > 0) return;
|
|
138
|
-
context.report({
|
|
139
|
-
data: { name },
|
|
140
|
-
fix: (fixer) => {
|
|
141
|
-
const { closingElement } = node;
|
|
142
|
-
if (closingElement === null) return fixer.replaceText(node, "<></>");
|
|
143
|
-
return [fixer.replaceText(openingElement, "<>"), fixer.replaceText(closingElement, "</>")];
|
|
144
|
-
},
|
|
145
|
-
messageId: MESSAGE_ID_SHORTHAND,
|
|
146
|
-
node
|
|
147
|
-
});
|
|
148
|
-
} };
|
|
149
|
-
}
|
|
150
|
-
const jsxShorthandFragment = createEslintRule({
|
|
151
|
-
name: RULE_NAME$8,
|
|
152
|
-
create: create$8,
|
|
153
|
-
defaultOptions: [{
|
|
154
|
-
fragmentName: DEFAULT_FRAGMENT_NAME,
|
|
155
|
-
mode: DEFAULT_MODE
|
|
156
|
-
}],
|
|
157
|
-
meta: {
|
|
158
|
-
defaultOptions: [{
|
|
159
|
-
fragmentName: DEFAULT_FRAGMENT_NAME,
|
|
160
|
-
mode: DEFAULT_MODE
|
|
161
|
-
}],
|
|
162
|
-
docs: {
|
|
163
|
-
description: "Enforce a consistent fragment form: the shorthand `<>...</>` or a named fragment",
|
|
164
|
-
recommended: false,
|
|
165
|
-
requiresTypeChecking: false
|
|
166
|
-
},
|
|
167
|
-
fixable: "code",
|
|
168
|
-
hasSuggestions: false,
|
|
169
|
-
messages: messages$8,
|
|
170
|
-
schema: schema$2,
|
|
171
|
-
type: "suggestion"
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
//#endregion
|
|
175
|
-
//#region src/utils/is-type-import.ts
|
|
176
|
-
/**
|
|
177
|
-
* Determine whether a variable definition is a type import. E.g.:.
|
|
178
|
-
*
|
|
179
|
-
* ```ts
|
|
180
|
-
* import type { Foo } from 'foo';
|
|
181
|
-
* import { type Bar } from 'bar';
|
|
182
|
-
* ```
|
|
183
|
-
*
|
|
184
|
-
* @param definition - The variable definition to check.
|
|
185
|
-
* @returns True if the definition is a type import, false otherwise.
|
|
186
|
-
*/
|
|
187
|
-
function isTypeImport(definition) {
|
|
188
|
-
return definition?.type === DefinitionType.ImportBinding && (definition.parent.importKind === "type" || definition.node.type === AST_NODE_TYPES.ImportSpecifier && definition.node.importKind === "type");
|
|
189
|
-
}
|
|
190
|
-
//#endregion
|
|
191
|
-
//#region src/utils/reference-contains-type-query.ts
|
|
192
|
-
/**
|
|
193
|
-
* Recursively checks whether a given reference has a type query declaration among its parents.
|
|
194
|
-
* @param node - The AST node to check.
|
|
195
|
-
* @returns True if a TSTypeQuery is found in the parent chain, false otherwise.
|
|
196
|
-
*/
|
|
197
|
-
function referenceContainsTypeQuery(node) {
|
|
198
|
-
switch (node.type) {
|
|
199
|
-
case AST_NODE_TYPES.Identifier:
|
|
200
|
-
case AST_NODE_TYPES.TSQualifiedName: return referenceContainsTypeQuery(node.parent);
|
|
201
|
-
case AST_NODE_TYPES.TSTypeQuery: return true;
|
|
202
|
-
default: return false;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
//#endregion
|
|
206
|
-
//#region src/utils/collect-variables.ts
|
|
207
|
-
/**
|
|
208
|
-
* This class leverages an AST visitor to mark variables as used via the
|
|
209
|
-
* `eslintUsed` property.
|
|
210
|
-
*/
|
|
211
|
-
var UnusedVariablesVisitor = class extends Visitor {
|
|
212
|
-
/**
|
|
213
|
-
* We keep a weak cache so that multiple rules can share the calculation.
|
|
214
|
-
*/
|
|
215
|
-
static RESULTS_CACHE = /* @__PURE__ */ new WeakMap();
|
|
216
|
-
#scopeManager;
|
|
217
|
-
ClassDeclaration = this.visitClass;
|
|
218
|
-
ClassExpression = this.visitClass;
|
|
219
|
-
ForInStatement = this.visitForInForOf;
|
|
220
|
-
/**
|
|
221
|
-
* #region HELPERS
|
|
222
|
-
*/
|
|
223
|
-
ForOfStatement = this.visitForInForOf;
|
|
224
|
-
FunctionDeclaration = this.visitFunction;
|
|
225
|
-
FunctionExpression = this.visitFunction;
|
|
226
|
-
MethodDefinition = this.visitSetter;
|
|
227
|
-
Property = this.visitSetter;
|
|
228
|
-
TSCallSignatureDeclaration = this.visitFunctionTypeSignature;
|
|
229
|
-
TSConstructorType = this.visitFunctionTypeSignature;
|
|
230
|
-
TSConstructSignatureDeclaration = this.visitFunctionTypeSignature;
|
|
231
|
-
TSDeclareFunction = this.visitFunctionTypeSignature;
|
|
232
|
-
/**
|
|
233
|
-
* NOTE - This is a simple visitor - meaning it does not support selectors.
|
|
234
|
-
*/
|
|
235
|
-
TSEmptyBodyFunctionExpression = this.visitFunctionTypeSignature;
|
|
236
|
-
TSFunctionType = this.visitFunctionTypeSignature;
|
|
237
|
-
TSMethodSignature = this.visitFunctionTypeSignature;
|
|
238
|
-
constructor(scopeManager) {
|
|
239
|
-
super({ visitChildrenEvenIfSelectorExists: true });
|
|
240
|
-
this.#scopeManager = scopeManager;
|
|
241
|
-
}
|
|
242
|
-
static collectUnusedVariables(program, scopeManager) {
|
|
243
|
-
const cached = this.RESULTS_CACHE.get(program);
|
|
244
|
-
if (cached) return cached;
|
|
245
|
-
const visitor = new this(scopeManager);
|
|
246
|
-
visitor.visit(program);
|
|
247
|
-
const unusedVariables = visitor.collectUnusedVariables({ scope: visitor.getScope(program) });
|
|
248
|
-
this.RESULTS_CACHE.set(program, unusedVariables);
|
|
249
|
-
return unusedVariables;
|
|
250
|
-
}
|
|
251
|
-
Identifier(node) {
|
|
252
|
-
const scope = this.getScope(node);
|
|
253
|
-
if (scope.type === TSESLint.Scope.ScopeType.function && node.name === "this" && "params" in scope.block && scope.block.params.includes(node)) this.markVariableAsUsed(node);
|
|
254
|
-
}
|
|
255
|
-
TSEnumDeclaration(node) {
|
|
256
|
-
const scope = this.getScope(node);
|
|
257
|
-
for (const variable of scope.variables) this.markVariableAsUsed(variable);
|
|
258
|
-
}
|
|
259
|
-
TSMappedType(node) {
|
|
260
|
-
this.markVariableAsUsed(node.key);
|
|
261
|
-
}
|
|
262
|
-
TSModuleDeclaration(node) {
|
|
263
|
-
if (node.kind === "global") this.markVariableAsUsed("global", node.parent);
|
|
264
|
-
}
|
|
265
|
-
TSParameterProperty(node) {
|
|
266
|
-
let identifier;
|
|
267
|
-
switch (node.parameter.type) {
|
|
268
|
-
case AST_NODE_TYPES.AssignmentPattern:
|
|
269
|
-
identifier = node.parameter.left;
|
|
270
|
-
break;
|
|
271
|
-
case AST_NODE_TYPES.Identifier:
|
|
272
|
-
identifier = node.parameter;
|
|
273
|
-
break;
|
|
274
|
-
}
|
|
275
|
-
this.markVariableAsUsed(identifier);
|
|
276
|
-
}
|
|
277
|
-
collectUnusedVariables({ scope, variables = {
|
|
278
|
-
unusedVariables: /* @__PURE__ */ new Set(),
|
|
279
|
-
usedVariables: /* @__PURE__ */ new Set()
|
|
280
|
-
} }) {
|
|
281
|
-
if (!scope.functionExpressionScope) for (const variable of scope.variables) {
|
|
282
|
-
if (variable instanceof ImplicitLibVariable) continue;
|
|
283
|
-
if (variable.eslintUsed || isExported$1(variable) || isMergeableExported(variable) || isUsedVariable(variable)) variables.usedVariables.add(variable);
|
|
284
|
-
else variables.unusedVariables.add(variable);
|
|
285
|
-
}
|
|
286
|
-
for (const childScope of scope.childScopes) this.collectUnusedVariables({
|
|
287
|
-
scope: childScope,
|
|
288
|
-
variables
|
|
289
|
-
});
|
|
290
|
-
return variables;
|
|
291
|
-
}
|
|
292
|
-
getScope(currentNode) {
|
|
293
|
-
const inner = currentNode.type !== AST_NODE_TYPES.Program;
|
|
294
|
-
let node = currentNode;
|
|
295
|
-
while (node) {
|
|
296
|
-
const scope = this.#scopeManager.acquire(node, inner);
|
|
297
|
-
if (scope) {
|
|
298
|
-
if (scope.type === ScopeType.functionExpressionName) {
|
|
299
|
-
const returnValue = scope.childScopes[0];
|
|
300
|
-
assert(returnValue, "Function expression name scope should have a child scope");
|
|
301
|
-
return returnValue;
|
|
302
|
-
}
|
|
303
|
-
return scope;
|
|
304
|
-
}
|
|
305
|
-
node = node.parent;
|
|
306
|
-
}
|
|
307
|
-
const returnValue = this.#scopeManager.scopes[0];
|
|
308
|
-
assert(returnValue, "There should be at least one scope");
|
|
309
|
-
return returnValue;
|
|
310
|
-
}
|
|
311
|
-
markVariableAsUsed(variableOrIdentifierOrName, parent) {
|
|
312
|
-
if (typeof variableOrIdentifierOrName !== "string" && !("type" in variableOrIdentifierOrName)) {
|
|
313
|
-
variableOrIdentifierOrName.eslintUsed = true;
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
let name;
|
|
317
|
-
let node;
|
|
318
|
-
if (typeof variableOrIdentifierOrName === "string") {
|
|
319
|
-
name = variableOrIdentifierOrName;
|
|
320
|
-
assert(parent, "Parent node is required when marking by name");
|
|
321
|
-
node = parent;
|
|
322
|
-
} else {
|
|
323
|
-
({name} = variableOrIdentifierOrName);
|
|
324
|
-
node = variableOrIdentifierOrName;
|
|
325
|
-
}
|
|
326
|
-
let currentScope = this.getScope(node);
|
|
327
|
-
while (currentScope) {
|
|
328
|
-
const variable = currentScope.variables.find((scopeVariable) => scopeVariable.name === name);
|
|
329
|
-
if (variable) {
|
|
330
|
-
variable.eslintUsed = true;
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
currentScope = currentScope.upper ?? void 0;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
visitClass(node) {
|
|
337
|
-
const scope = this.getScope(node);
|
|
338
|
-
for (const variable of scope.variables) if (variable.identifiers[0] === scope.block.id) {
|
|
339
|
-
this.markVariableAsUsed(variable);
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
visitForInForOf(node) {
|
|
344
|
-
/**
|
|
345
|
-
* // cspell:ignore Zacher
|
|
346
|
-
* (Brad Zacher): I hate that this has to exist.
|
|
347
|
-
* But it is required for compat with the base ESLint rule.
|
|
348
|
-
*
|
|
349
|
-
* In 2015, ESLint decided to add an exception for these two specific cases.
|
|
350
|
-
* ```
|
|
351
|
-
* for (var key in object) return;
|
|
352
|
-
*
|
|
353
|
-
* var key;
|
|
354
|
-
* for (key in object) return;
|
|
355
|
-
* ```
|
|
356
|
-
*
|
|
357
|
-
* I disagree with it, but what are you going to do...
|
|
358
|
-
*
|
|
359
|
-
* Https://github.com/eslint/eslint/issues/2342.
|
|
360
|
-
*/
|
|
361
|
-
let idOrVariable;
|
|
362
|
-
if (node.left.type === AST_NODE_TYPES.VariableDeclaration) {
|
|
363
|
-
const variable = this.#scopeManager.getDeclaredVariables(node.left).at(0);
|
|
364
|
-
if (!variable) return;
|
|
365
|
-
idOrVariable = variable;
|
|
366
|
-
}
|
|
367
|
-
if (node.left.type === AST_NODE_TYPES.Identifier) idOrVariable = node.left;
|
|
368
|
-
if (idOrVariable === void 0) return;
|
|
369
|
-
let { body } = node;
|
|
370
|
-
if (body.type === AST_NODE_TYPES.BlockStatement) {
|
|
371
|
-
if (body.body.length !== 1) return;
|
|
372
|
-
body = body.body[0];
|
|
373
|
-
}
|
|
374
|
-
if (body.type !== AST_NODE_TYPES.ReturnStatement) return;
|
|
375
|
-
this.markVariableAsUsed(idOrVariable);
|
|
376
|
-
}
|
|
377
|
-
visitFunction(node) {
|
|
378
|
-
const variable = this.getScope(node).set.get("arguments");
|
|
379
|
-
if (variable?.defs.length === 0) this.markVariableAsUsed(variable);
|
|
380
|
-
}
|
|
381
|
-
visitFunctionTypeSignature(node) {
|
|
382
|
-
for (const parameter of node.params) this.visitPattern(parameter, (name) => {
|
|
383
|
-
this.markVariableAsUsed(name);
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
visitSetter(node) {
|
|
387
|
-
if (node.kind === "set") for (const parameter of node.value.params) this.visitPattern(parameter, (id) => {
|
|
388
|
-
this.markVariableAsUsed(id);
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
};
|
|
392
|
-
/**
|
|
393
|
-
* Checks the position of given nodes.
|
|
394
|
-
* @param inner - A node which is expected as inside.
|
|
395
|
-
* @param outer - A node which is expected as outside.
|
|
396
|
-
* @returns `true` if the `inner` node exists in the `outer` node.
|
|
397
|
-
*/
|
|
398
|
-
function isInside(inner, outer) {
|
|
399
|
-
return inner.range[0] >= outer.range[0] && inner.range[1] <= outer.range[1];
|
|
400
|
-
}
|
|
401
|
-
/**
|
|
402
|
-
* Determine if an identifier is referencing an enclosing name.
|
|
403
|
-
* This only applies to declarations that create their own scope (modules, functions, classes).
|
|
404
|
-
* @param ref - The reference to check.
|
|
405
|
-
* @param nodes - The candidate function nodes.
|
|
406
|
-
* @returns True if it's a self-reference, false if not.
|
|
407
|
-
*/
|
|
408
|
-
function isSelfReference(ref, nodes) {
|
|
409
|
-
let scope = ref.from;
|
|
410
|
-
while (scope) {
|
|
411
|
-
if (nodes.has(scope.block)) return true;
|
|
412
|
-
scope = scope.upper ?? void 0;
|
|
413
|
-
}
|
|
414
|
-
return false;
|
|
415
|
-
}
|
|
416
|
-
const MERGEABLE_TYPES = /* @__PURE__ */ new Set([
|
|
417
|
-
AST_NODE_TYPES.ClassDeclaration,
|
|
418
|
-
AST_NODE_TYPES.FunctionDeclaration,
|
|
419
|
-
AST_NODE_TYPES.TSInterfaceDeclaration,
|
|
420
|
-
AST_NODE_TYPES.TSModuleDeclaration,
|
|
421
|
-
AST_NODE_TYPES.TSTypeAliasDeclaration
|
|
422
|
-
]);
|
|
423
|
-
/**
|
|
424
|
-
* Determines if a given variable is being exported from a module.
|
|
425
|
-
* @param variable - Eslint-scope variable object.
|
|
426
|
-
* @returns True if the variable is exported, false if not.
|
|
427
|
-
*/
|
|
428
|
-
function isExported$1(variable) {
|
|
429
|
-
return variable.defs.some((definition) => {
|
|
430
|
-
let { node } = definition;
|
|
431
|
-
if (node.type === AST_NODE_TYPES.VariableDeclarator) node = node.parent;
|
|
432
|
-
else if (definition.type === TSESLint.Scope.DefinitionType.Parameter) return false;
|
|
433
|
-
return node.parent.type.startsWith("Export");
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
/**
|
|
437
|
-
* Determine if the variable is directly exported.
|
|
438
|
-
* @param variable - The variable to check.
|
|
439
|
-
* @returns True if the variable is exported via a merged declaration.
|
|
440
|
-
*/
|
|
441
|
-
function isMergeableExported(variable) {
|
|
442
|
-
for (const definition of variable.defs) {
|
|
443
|
-
if (definition.type === TSESLint.Scope.DefinitionType.Parameter) continue;
|
|
444
|
-
if (MERGEABLE_TYPES.has(definition.node.type) && definition.node.parent.type === AST_NODE_TYPES.ExportNamedDeclaration || definition.node.parent.type === AST_NODE_TYPES.ExportDefaultDeclaration) return true;
|
|
445
|
-
}
|
|
446
|
-
return false;
|
|
447
|
-
}
|
|
448
|
-
const LOGICAL_ASSIGNMENT_OPERATORS = /* @__PURE__ */ new Set([
|
|
449
|
-
"&&=",
|
|
450
|
-
"??=",
|
|
451
|
-
"||="
|
|
452
|
-
]);
|
|
453
|
-
/**
|
|
454
|
-
* Collects the set of unused variables for a given context.
|
|
455
|
-
*
|
|
456
|
-
* Due to complexity, this does not take into consideration:
|
|
457
|
-
* - variables within declaration files
|
|
458
|
-
* - variables within ambient module declarations.
|
|
459
|
-
* @param context - The rule context.
|
|
460
|
-
* @returns The collected variables.
|
|
461
|
-
* @template MessageIds
|
|
462
|
-
* @template Options
|
|
463
|
-
*/
|
|
464
|
-
function collectVariables(context) {
|
|
465
|
-
return UnusedVariablesVisitor.collectUnusedVariables(context.sourceCode.ast, ESLintUtils.nullThrows(context.sourceCode.scopeManager, "Missing required scope manager"));
|
|
466
|
-
}
|
|
467
|
-
/**
|
|
468
|
-
* Determines if the variable is used.
|
|
469
|
-
* @param variable - The variable to check.
|
|
470
|
-
* @returns True if the variable is used.
|
|
471
|
-
*/
|
|
472
|
-
function isUsedVariable(variable) {
|
|
473
|
-
/**
|
|
474
|
-
* Gets a list of function definitions for a specified variable.
|
|
475
|
-
* @param scopeVariable - Eslint-scope variable object.
|
|
476
|
-
* @returns Function nodes.
|
|
477
|
-
*/
|
|
478
|
-
function getFunctionDefinitions(scopeVariable) {
|
|
479
|
-
const functionDefinitions = /* @__PURE__ */ new Set();
|
|
480
|
-
for (const definition of scopeVariable.defs) {
|
|
481
|
-
if (definition.type === TSESLint.Scope.DefinitionType.FunctionName) functionDefinitions.add(definition.node);
|
|
482
|
-
if (definition.type === TSESLint.Scope.DefinitionType.Variable && (definition.node.init?.type === AST_NODE_TYPES.FunctionExpression || definition.node.init?.type === AST_NODE_TYPES.ArrowFunctionExpression)) functionDefinitions.add(definition.node.init);
|
|
483
|
-
}
|
|
484
|
-
return functionDefinitions;
|
|
485
|
-
}
|
|
486
|
-
function getTypeDeclarations(scopeVariable) {
|
|
487
|
-
const nodes = /* @__PURE__ */ new Set();
|
|
488
|
-
for (const definition of scopeVariable.defs) if (definition.node.type === AST_NODE_TYPES.TSInterfaceDeclaration || definition.node.type === AST_NODE_TYPES.TSTypeAliasDeclaration) nodes.add(definition.node);
|
|
489
|
-
return nodes;
|
|
490
|
-
}
|
|
491
|
-
function getModuleDeclarations(scopeVariable) {
|
|
492
|
-
const nodes = /* @__PURE__ */ new Set();
|
|
493
|
-
for (const definition of scopeVariable.defs) if (definition.node.type === AST_NODE_TYPES.TSModuleDeclaration) nodes.add(definition.node);
|
|
494
|
-
return nodes;
|
|
495
|
-
}
|
|
496
|
-
function getEnumDeclarations(scopeVariable) {
|
|
497
|
-
const nodes = /* @__PURE__ */ new Set();
|
|
498
|
-
for (const definition of scopeVariable.defs) if (definition.node.type === AST_NODE_TYPES.TSEnumDeclaration) nodes.add(definition.node);
|
|
499
|
-
return nodes;
|
|
500
|
-
}
|
|
501
|
-
/**
|
|
502
|
-
* Checks if the ref is contained within one of the given nodes.
|
|
503
|
-
* @param ref - A reference to check.
|
|
504
|
-
* @param nodes - A set of nodes.
|
|
505
|
-
* @returns `true` if the ref is inside one of the nodes.
|
|
506
|
-
*/
|
|
507
|
-
function isInsideOneOf(ref, nodes) {
|
|
508
|
-
for (const node of nodes) if (isInside(ref.identifier, node)) return true;
|
|
509
|
-
return false;
|
|
510
|
-
}
|
|
511
|
-
/**
|
|
512
|
-
* Checks whether a given node is unused expression or not.
|
|
513
|
-
* @param node - The node itself.
|
|
514
|
-
* @returns The node is an unused expression.
|
|
515
|
-
*/
|
|
516
|
-
function isUnusedExpression(node) {
|
|
517
|
-
const { parent } = node;
|
|
518
|
-
if (parent.type === AST_NODE_TYPES.ExpressionStatement) return true;
|
|
519
|
-
if (parent.type === AST_NODE_TYPES.SequenceExpression) {
|
|
520
|
-
if (!(parent.expressions[parent.expressions.length - 1] === node)) return true;
|
|
521
|
-
return isUnusedExpression(parent);
|
|
522
|
-
}
|
|
523
|
-
return false;
|
|
524
|
-
}
|
|
525
|
-
/**
|
|
526
|
-
* If a given reference is left-hand side of an assignment, this gets
|
|
527
|
-
* the right-hand side node of the assignment.
|
|
528
|
-
*
|
|
529
|
-
* In the following cases, this returns undefined.
|
|
530
|
-
*
|
|
531
|
-
* - The reference is not the LHS of an assignment expression.
|
|
532
|
-
* - The reference is inside of a loop.
|
|
533
|
-
* - The reference is inside of a function scope which is different from
|
|
534
|
-
* the declaration.
|
|
535
|
-
* @param ref - A reference to check.
|
|
536
|
-
* @param previousRhsNode - The previous RHS node. This is for `a = a + a`-like code.
|
|
537
|
-
* @returns The RHS node or undefined.
|
|
538
|
-
*/
|
|
539
|
-
function getRhsNode(ref, previousRhsNode) {
|
|
540
|
-
/**
|
|
541
|
-
* Checks whether the given node is in a loop or not.
|
|
542
|
-
* @param node - The node to check.
|
|
543
|
-
* @returns `true` if the node is in a loop.
|
|
544
|
-
*/
|
|
545
|
-
function isInLoop(node) {
|
|
546
|
-
let currentNode = node;
|
|
547
|
-
while (currentNode) {
|
|
548
|
-
if (ASTUtils.isFunction(currentNode)) break;
|
|
549
|
-
if (ASTUtils.isLoop(currentNode)) return true;
|
|
550
|
-
currentNode = currentNode.parent;
|
|
551
|
-
}
|
|
552
|
-
return false;
|
|
553
|
-
}
|
|
554
|
-
const id = ref.identifier;
|
|
555
|
-
const { parent } = id;
|
|
556
|
-
const refScope = ref.from.variableScope;
|
|
557
|
-
const { variableScope } = ref.resolved.scope;
|
|
558
|
-
const canBeUsedLater = refScope !== variableScope || isInLoop(id);
|
|
559
|
-
if (previousRhsNode && isInside(id, previousRhsNode)) return previousRhsNode;
|
|
560
|
-
if (parent.type === AST_NODE_TYPES.AssignmentExpression && isUnusedExpression(parent) && id === parent.left && !canBeUsedLater) return parent.right;
|
|
561
|
-
}
|
|
562
|
-
/**
|
|
563
|
-
* Checks whether a given reference is a read to update itself or not.
|
|
564
|
-
* @param ref - A reference to check.
|
|
565
|
-
* @param rhsNode - The RHS node of the previous assignment.
|
|
566
|
-
* @returns The reference is a read to update itself.
|
|
567
|
-
*/
|
|
568
|
-
function isReadForItself(ref, rhsNode) {
|
|
569
|
-
/**
|
|
570
|
-
* Checks whether a given Identifier node exists inside of a function node which can be used later.
|
|
571
|
-
*
|
|
572
|
-
* "can be used later" means:
|
|
573
|
-
* - the function is assigned to a variable.
|
|
574
|
-
* - the function is bound to a property and the object can be used later.
|
|
575
|
-
* - the function is bound as an argument of a function call.
|
|
576
|
-
*
|
|
577
|
-
* If a reference exists in a function which can be used later, the reference is read when the function is called.
|
|
578
|
-
* @param id - An Identifier node to check.
|
|
579
|
-
* @param rightHandSideNode - The RHS node of the previous assignment.
|
|
580
|
-
* @returns `true` if the `id` node exists inside of a function node which can be used later.
|
|
581
|
-
*/
|
|
582
|
-
function isInsideOfStorableFunction(id, rightHandSideNode) {
|
|
583
|
-
/**
|
|
584
|
-
* Finds a function node from ancestors of a node.
|
|
585
|
-
* @param node - A start node to find.
|
|
586
|
-
* @returns A found function node.
|
|
587
|
-
*/
|
|
588
|
-
function getUpperFunction(node) {
|
|
589
|
-
let currentNode = node;
|
|
590
|
-
while (currentNode) {
|
|
591
|
-
if (ASTUtils.isFunction(currentNode)) return currentNode;
|
|
592
|
-
currentNode = currentNode.parent;
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
/**
|
|
596
|
-
* Checks whether a given function node is stored to somewhere or not.
|
|
597
|
-
* If the function node is stored, the function can be used later.
|
|
598
|
-
* @param funcNode - A function node to check.
|
|
599
|
-
* @param storableRhsNode - The RHS node of the previous assignment.
|
|
600
|
-
* @returns `true` if under the following conditions:
|
|
601
|
-
* - the funcNode is assigned to a variable.
|
|
602
|
-
* - the funcNode is bound as an argument of a function call.
|
|
603
|
-
* - the function is bound to a property and the object satisfies above conditions.
|
|
604
|
-
*/
|
|
605
|
-
function isStorableFunction(funcNode, storableRhsNode) {
|
|
606
|
-
let node = funcNode;
|
|
607
|
-
let { parent } = funcNode;
|
|
608
|
-
while (parent && isInside(parent, storableRhsNode)) {
|
|
609
|
-
switch (parent.type) {
|
|
610
|
-
case AST_NODE_TYPES.AssignmentExpression:
|
|
611
|
-
case AST_NODE_TYPES.TaggedTemplateExpression:
|
|
612
|
-
case AST_NODE_TYPES.YieldExpression: return true;
|
|
613
|
-
case AST_NODE_TYPES.CallExpression:
|
|
614
|
-
case AST_NODE_TYPES.NewExpression: return parent.callee !== node;
|
|
615
|
-
case AST_NODE_TYPES.SequenceExpression:
|
|
616
|
-
if (parent.expressions[parent.expressions.length - 1] !== node) return false;
|
|
617
|
-
break;
|
|
618
|
-
default: if (parent.type.endsWith("Statement") || parent.type.endsWith("Declaration")) return true;
|
|
619
|
-
}
|
|
620
|
-
node = parent;
|
|
621
|
-
({parent} = parent);
|
|
622
|
-
}
|
|
623
|
-
return false;
|
|
624
|
-
}
|
|
625
|
-
const funcNode = getUpperFunction(id);
|
|
626
|
-
return !!funcNode && isInside(funcNode, rightHandSideNode) && isStorableFunction(funcNode, rightHandSideNode);
|
|
627
|
-
}
|
|
628
|
-
const id = ref.identifier;
|
|
629
|
-
const { parent } = id;
|
|
630
|
-
return ref.isRead() && (parent.type === AST_NODE_TYPES.AssignmentExpression && !LOGICAL_ASSIGNMENT_OPERATORS.has(parent.operator) && isUnusedExpression(parent) && parent.left === id || parent.type === AST_NODE_TYPES.UpdateExpression && isUnusedExpression(parent) || !!rhsNode && isInside(id, rhsNode) && !isInsideOfStorableFunction(id, rhsNode));
|
|
631
|
-
}
|
|
632
|
-
const functionNodes = getFunctionDefinitions(variable);
|
|
633
|
-
const isFunctionDefinition = functionNodes.size > 0;
|
|
634
|
-
const typeDeclNodes = getTypeDeclarations(variable);
|
|
635
|
-
const isTypeDecl = typeDeclNodes.size > 0;
|
|
636
|
-
const moduleDeclNodes = getModuleDeclarations(variable);
|
|
637
|
-
const isModuleDecl = moduleDeclNodes.size > 0;
|
|
638
|
-
const enumDeclNodes = getEnumDeclarations(variable);
|
|
639
|
-
const isEnumDecl = enumDeclNodes.size > 0;
|
|
640
|
-
const isImportedAsType = variable.defs.every(isTypeImport);
|
|
641
|
-
let rhsNode;
|
|
642
|
-
return variable.references.some((ref) => {
|
|
643
|
-
const forItself = isReadForItself(ref, rhsNode);
|
|
644
|
-
rhsNode = getRhsNode(ref, rhsNode);
|
|
645
|
-
return ref.isRead() && !forItself && (isImportedAsType || !referenceContainsTypeQuery(ref.identifier)) && (!isFunctionDefinition || !isSelfReference(ref, functionNodes)) && (!isTypeDecl || !isInsideOneOf(ref, typeDeclNodes)) && (!isModuleDecl || !isSelfReference(ref, moduleDeclNodes)) && (!isEnumDecl || !isSelfReference(ref, enumDeclNodes));
|
|
646
|
-
});
|
|
647
|
-
}
|
|
648
|
-
//#endregion
|
|
649
|
-
//#region src/rules/naming-convention/utils/enums.ts
|
|
650
|
-
const Selector = {
|
|
651
|
-
variable: 1,
|
|
652
|
-
function: 2,
|
|
653
|
-
parameter: 4,
|
|
654
|
-
objectStyleEnum: 8,
|
|
655
|
-
parameterProperty: 16,
|
|
656
|
-
classicAccessor: 32,
|
|
657
|
-
enumMember: 64,
|
|
658
|
-
classMethod: 128,
|
|
659
|
-
objectLiteralMethod: 256,
|
|
660
|
-
typeMethod: 512,
|
|
661
|
-
classProperty: 1024,
|
|
662
|
-
objectLiteralProperty: 2048,
|
|
663
|
-
typeProperty: 4096,
|
|
664
|
-
autoAccessor: 8192,
|
|
665
|
-
class: 16384,
|
|
666
|
-
interface: 32768,
|
|
667
|
-
typeAlias: 65536,
|
|
668
|
-
enum: 131072,
|
|
669
|
-
typeParameter: 262144,
|
|
670
|
-
import: 524288
|
|
671
|
-
};
|
|
672
|
-
const MetaSelector = {
|
|
673
|
-
default: -1,
|
|
674
|
-
variableLike: 15,
|
|
675
|
-
memberLike: 16368,
|
|
676
|
-
typeLike: 507904,
|
|
677
|
-
method: 896,
|
|
678
|
-
property: 7168,
|
|
679
|
-
accessor: 8224
|
|
680
|
-
};
|
|
681
|
-
const Modifier = {
|
|
682
|
-
"const": 1,
|
|
683
|
-
"readonly": 2,
|
|
684
|
-
"static": 4,
|
|
685
|
-
"public": 8,
|
|
686
|
-
"protected": 16,
|
|
687
|
-
"private": 32,
|
|
688
|
-
"#private": 64,
|
|
689
|
-
"abstract": 128,
|
|
690
|
-
"destructured": 256,
|
|
691
|
-
"global": 512,
|
|
692
|
-
"exported": 1024,
|
|
693
|
-
"unused": 2048,
|
|
694
|
-
"requiresQuotes": 4096,
|
|
695
|
-
"override": 8192,
|
|
696
|
-
"async": 16384,
|
|
697
|
-
"default": 32768,
|
|
698
|
-
"namespace": 65536
|
|
699
|
-
};
|
|
700
|
-
const PredefinedFormat = {
|
|
701
|
-
camelCase: 1,
|
|
702
|
-
strictCamelCase: 2,
|
|
703
|
-
PascalCase: 3,
|
|
704
|
-
StrictPascalCase: 4,
|
|
705
|
-
snake_case: 5,
|
|
706
|
-
UPPER_CASE: 6
|
|
707
|
-
};
|
|
708
|
-
const PredefinedFormatValueToKey = Object.fromEntries(Object.entries(PredefinedFormat).map(([key, value]) => [value, key]));
|
|
709
|
-
const TypeModifier = {
|
|
710
|
-
boolean: 131072,
|
|
711
|
-
string: 262144,
|
|
712
|
-
number: 524288,
|
|
713
|
-
function: 1048576,
|
|
714
|
-
array: 2097152
|
|
715
|
-
};
|
|
716
|
-
const TypeModifierValueToKey = Object.fromEntries(Object.entries(TypeModifier).map(([key, value]) => [value, key]));
|
|
717
|
-
const UnderscoreOption = {
|
|
718
|
-
forbid: 1,
|
|
719
|
-
allow: 2,
|
|
720
|
-
require: 3,
|
|
721
|
-
requireDouble: 4,
|
|
722
|
-
allowDouble: 5,
|
|
723
|
-
allowSingleOrDouble: 6
|
|
724
|
-
};
|
|
725
|
-
//#endregion
|
|
726
|
-
//#region src/rules/naming-convention/utils/shared.ts
|
|
727
|
-
function isMetaSelector(selector) {
|
|
728
|
-
return selector in MetaSelector;
|
|
729
|
-
}
|
|
730
|
-
function isMethodOrPropertySelector(selector) {
|
|
731
|
-
return selector === MetaSelector.method || selector === MetaSelector.property;
|
|
732
|
-
}
|
|
733
|
-
function selectorTypeToMessageString(selectorType) {
|
|
734
|
-
const notCamelCase = selectorType.replaceAll(/([A-Z])/g, " $1");
|
|
735
|
-
return notCamelCase.charAt(0).toUpperCase() + notCamelCase.slice(1);
|
|
736
|
-
}
|
|
737
|
-
//#endregion
|
|
738
|
-
//#region src/rules/naming-convention/utils/format.ts
|
|
739
|
-
function isUppercaseChar(char) {
|
|
740
|
-
return char === char.toUpperCase() && char !== char.toLowerCase();
|
|
741
|
-
}
|
|
742
|
-
function hasStrictCamelHumps(name, isUpper) {
|
|
743
|
-
if (name.startsWith("_")) return false;
|
|
744
|
-
for (let index = 1; index < name.length; ++index) {
|
|
745
|
-
const char = name[index];
|
|
746
|
-
if (char === "_") return false;
|
|
747
|
-
if (isUpper === isUppercaseChar(char)) {
|
|
748
|
-
if (isUpper) return false;
|
|
749
|
-
} else isUpper = !isUpper;
|
|
750
|
-
}
|
|
751
|
-
return true;
|
|
752
|
-
}
|
|
753
|
-
function isCamelCase(name) {
|
|
754
|
-
return name.length === 0 || name[0] === name[0]?.toLowerCase() && !name.includes("_");
|
|
755
|
-
}
|
|
756
|
-
function isPascalCase(name) {
|
|
757
|
-
return name.length === 0 || name[0] === name[0]?.toUpperCase() && !name.includes("_");
|
|
758
|
-
}
|
|
759
|
-
/**
|
|
760
|
-
* Check for leading trailing and adjacent underscores.
|
|
761
|
-
* @param name - The name to check.
|
|
762
|
-
* @returns True if the underscores are valid.
|
|
763
|
-
*/
|
|
764
|
-
function validateUnderscores(name) {
|
|
765
|
-
if (name.startsWith("_")) return false;
|
|
766
|
-
let wasUnderscore = false;
|
|
767
|
-
for (let index = 1; index < name.length; ++index) if (name[index] === "_") {
|
|
768
|
-
if (wasUnderscore) return false;
|
|
769
|
-
wasUnderscore = true;
|
|
770
|
-
} else wasUnderscore = false;
|
|
771
|
-
return !wasUnderscore;
|
|
772
|
-
}
|
|
773
|
-
function isSnakeCase(name) {
|
|
774
|
-
return name.length === 0 || name === name.toLowerCase() && validateUnderscores(name);
|
|
775
|
-
}
|
|
776
|
-
function isStrictCamelCase(name) {
|
|
777
|
-
return name.length === 0 || name[0] === name[0]?.toLowerCase() && hasStrictCamelHumps(name, false);
|
|
778
|
-
}
|
|
779
|
-
function isStrictPascalCase(name) {
|
|
780
|
-
return name.length === 0 || name[0] === name[0]?.toUpperCase() && hasStrictCamelHumps(name, true);
|
|
781
|
-
}
|
|
782
|
-
function isUpperCase(name) {
|
|
783
|
-
return name.length === 0 || name === name.toUpperCase() && validateUnderscores(name);
|
|
784
|
-
}
|
|
785
|
-
const FormatCheckersMap = {
|
|
786
|
-
[PredefinedFormat.camelCase]: isCamelCase,
|
|
787
|
-
[PredefinedFormat.PascalCase]: isPascalCase,
|
|
788
|
-
[PredefinedFormat.snake_case]: isSnakeCase,
|
|
789
|
-
[PredefinedFormat.strictCamelCase]: isStrictCamelCase,
|
|
790
|
-
[PredefinedFormat.StrictPascalCase]: isStrictPascalCase,
|
|
791
|
-
[PredefinedFormat.UPPER_CASE]: isUpperCase
|
|
792
|
-
};
|
|
793
|
-
//#endregion
|
|
794
|
-
//#region src/rules/naming-convention/utils/validator.ts
|
|
795
|
-
function createValidator(type, context, allConfigs) {
|
|
796
|
-
const selectorType = Selector[type];
|
|
797
|
-
const configs = allConfigs.filter((configItem) => {
|
|
798
|
-
return (configItem.selector & selectorType) !== 0 || configItem.selector === MetaSelector.default;
|
|
799
|
-
}).sort((a, b) => {
|
|
800
|
-
if (a.selector === b.selector) return b.modifierWeight - a.modifierWeight;
|
|
801
|
-
const aIsMeta = isMetaSelector(a.selector);
|
|
802
|
-
const bIsMeta = isMetaSelector(b.selector);
|
|
803
|
-
if (aIsMeta && !bIsMeta) return 1;
|
|
804
|
-
if (!aIsMeta && bIsMeta) return -1;
|
|
805
|
-
const aIsMethodOrProperty = isMethodOrPropertySelector(a.selector);
|
|
806
|
-
const bIsMethodOrProperty = isMethodOrPropertySelector(b.selector);
|
|
807
|
-
if (aIsMethodOrProperty && !bIsMethodOrProperty) return -1;
|
|
808
|
-
if (!aIsMethodOrProperty && bIsMethodOrProperty) return 1;
|
|
809
|
-
return b.selector - a.selector;
|
|
810
|
-
});
|
|
811
|
-
return (node, modifiers = /* @__PURE__ */ new Set()) => {
|
|
812
|
-
const originalName = node.type === AST_NODE_TYPES.Identifier || node.type === AST_NODE_TYPES.PrivateIdentifier ? node.name : `${node.value}`;
|
|
813
|
-
for (const config of configs) {
|
|
814
|
-
if (config.filter?.regex.test(originalName) !== config.filter?.match) continue;
|
|
815
|
-
if (config.modifiers?.some((modifier) => !modifiers.has(modifier)) === true) continue;
|
|
816
|
-
if (!isCorrectType(node, config, context, selectorType)) continue;
|
|
817
|
-
let name = originalName;
|
|
818
|
-
name = validateUnderscore({
|
|
819
|
-
name,
|
|
820
|
-
config,
|
|
821
|
-
node,
|
|
822
|
-
originalName,
|
|
823
|
-
position: "leading"
|
|
824
|
-
});
|
|
825
|
-
if (name === void 0) return;
|
|
826
|
-
name = validateUnderscore({
|
|
827
|
-
name,
|
|
828
|
-
config,
|
|
829
|
-
node,
|
|
830
|
-
originalName,
|
|
831
|
-
position: "trailing"
|
|
832
|
-
});
|
|
833
|
-
if (name === void 0) return;
|
|
834
|
-
name = validateAffix({
|
|
835
|
-
name,
|
|
836
|
-
config,
|
|
837
|
-
node,
|
|
838
|
-
originalName,
|
|
839
|
-
position: "prefix"
|
|
840
|
-
});
|
|
841
|
-
if (name === void 0) return;
|
|
842
|
-
name = validateAffix({
|
|
843
|
-
name,
|
|
844
|
-
config,
|
|
845
|
-
node,
|
|
846
|
-
originalName,
|
|
847
|
-
position: "suffix"
|
|
848
|
-
});
|
|
849
|
-
if (name === void 0) return;
|
|
850
|
-
if (!validateCustom({
|
|
851
|
-
name,
|
|
852
|
-
config,
|
|
853
|
-
node,
|
|
854
|
-
originalName
|
|
855
|
-
})) return;
|
|
856
|
-
if (!validatePredefinedFormat({
|
|
857
|
-
name,
|
|
858
|
-
config,
|
|
859
|
-
modifiers,
|
|
860
|
-
node,
|
|
861
|
-
originalName
|
|
862
|
-
})) return;
|
|
863
|
-
return;
|
|
864
|
-
}
|
|
865
|
-
};
|
|
866
|
-
function formatReportData({ affixes, count, custom, formats, originalName, position, processedName }) {
|
|
867
|
-
let regexMatch = null;
|
|
868
|
-
if (custom?.match === true) regexMatch = "match";
|
|
869
|
-
else if (custom?.match === false) regexMatch = "not match";
|
|
870
|
-
return {
|
|
871
|
-
name: originalName,
|
|
872
|
-
affixes: affixes?.join(", "),
|
|
873
|
-
count,
|
|
874
|
-
formats: formats?.map((formatItem) => PredefinedFormatValueToKey[formatItem]).join(", "),
|
|
875
|
-
position,
|
|
876
|
-
processedName,
|
|
877
|
-
regex: custom?.regex.toString(),
|
|
878
|
-
regexMatch,
|
|
879
|
-
type: selectorTypeToMessageString(type)
|
|
880
|
-
};
|
|
881
|
-
}
|
|
882
|
-
function validateUnderscore({ name, config, node, originalName, position }) {
|
|
883
|
-
const option = position === "leading" ? config.leadingUnderscore : config.trailingUnderscore;
|
|
884
|
-
if (!option) return name;
|
|
885
|
-
const hasSingleUnderscore = position === "leading" ? () => name.startsWith("_") : () => name.endsWith("_");
|
|
886
|
-
const trimSingleUnderscore = position === "leading" ? () => name.slice(1) : () => name.slice(0, -1);
|
|
887
|
-
const hasDoubleUnderscore = position === "leading" ? () => name.startsWith("__") : () => name.endsWith("__");
|
|
888
|
-
const trimDoubleUnderscore = position === "leading" ? () => name.slice(2) : () => name.slice(0, -2);
|
|
889
|
-
switch (option) {
|
|
890
|
-
case UnderscoreOption.allow:
|
|
891
|
-
if (hasSingleUnderscore()) return trimSingleUnderscore();
|
|
892
|
-
return name;
|
|
893
|
-
case UnderscoreOption.allowDouble:
|
|
894
|
-
if (hasDoubleUnderscore()) return trimDoubleUnderscore();
|
|
895
|
-
return name;
|
|
896
|
-
case UnderscoreOption.allowSingleOrDouble:
|
|
897
|
-
if (hasDoubleUnderscore()) return trimDoubleUnderscore();
|
|
898
|
-
if (hasSingleUnderscore()) return trimSingleUnderscore();
|
|
899
|
-
return name;
|
|
900
|
-
case UnderscoreOption.forbid:
|
|
901
|
-
if (hasSingleUnderscore()) {
|
|
902
|
-
context.report({
|
|
903
|
-
data: formatReportData({
|
|
904
|
-
count: "one",
|
|
905
|
-
originalName,
|
|
906
|
-
position
|
|
907
|
-
}),
|
|
908
|
-
messageId: "unexpectedUnderscore",
|
|
909
|
-
node
|
|
910
|
-
});
|
|
911
|
-
return;
|
|
912
|
-
}
|
|
913
|
-
return name;
|
|
914
|
-
case UnderscoreOption.require:
|
|
915
|
-
if (!hasSingleUnderscore()) {
|
|
916
|
-
context.report({
|
|
917
|
-
data: formatReportData({
|
|
918
|
-
count: "one",
|
|
919
|
-
originalName,
|
|
920
|
-
position
|
|
921
|
-
}),
|
|
922
|
-
messageId: "missingUnderscore",
|
|
923
|
-
node
|
|
924
|
-
});
|
|
925
|
-
return;
|
|
926
|
-
}
|
|
927
|
-
return trimSingleUnderscore();
|
|
928
|
-
case UnderscoreOption.requireDouble:
|
|
929
|
-
if (!hasDoubleUnderscore()) {
|
|
930
|
-
context.report({
|
|
931
|
-
data: formatReportData({
|
|
932
|
-
count: "two",
|
|
933
|
-
originalName,
|
|
934
|
-
position
|
|
935
|
-
}),
|
|
936
|
-
messageId: "missingUnderscore",
|
|
937
|
-
node
|
|
938
|
-
});
|
|
939
|
-
return;
|
|
940
|
-
}
|
|
941
|
-
return trimDoubleUnderscore();
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
function validateAffix({ name, config, node, originalName, position }) {
|
|
945
|
-
const affixes = config[position];
|
|
946
|
-
if (!affixes || affixes.length === 0) return name;
|
|
947
|
-
for (const affix of affixes) {
|
|
948
|
-
const hasAffix = position === "prefix" ? name.startsWith(affix) : name.endsWith(affix);
|
|
949
|
-
const trimAffix = position === "prefix" ? () => name.slice(affix.length) : () => name.slice(0, -affix.length);
|
|
950
|
-
if (hasAffix) return trimAffix();
|
|
951
|
-
}
|
|
952
|
-
context.report({
|
|
953
|
-
data: formatReportData({
|
|
954
|
-
affixes,
|
|
955
|
-
originalName,
|
|
956
|
-
position
|
|
957
|
-
}),
|
|
958
|
-
messageId: "missingAffix",
|
|
959
|
-
node
|
|
960
|
-
});
|
|
961
|
-
}
|
|
962
|
-
function validateCustom({ name, config, node, originalName }) {
|
|
963
|
-
const { custom } = config;
|
|
964
|
-
if (!custom) return true;
|
|
965
|
-
const result = custom.regex.test(name);
|
|
966
|
-
if (custom.match && result) return true;
|
|
967
|
-
if (!custom.match && !result) return true;
|
|
968
|
-
context.report({
|
|
969
|
-
data: formatReportData({
|
|
970
|
-
custom,
|
|
971
|
-
originalName
|
|
972
|
-
}),
|
|
973
|
-
messageId: "satisfyCustom",
|
|
974
|
-
node
|
|
975
|
-
});
|
|
976
|
-
return false;
|
|
977
|
-
}
|
|
978
|
-
function validatePredefinedFormat({ name, config, modifiers, node, originalName }) {
|
|
979
|
-
const formats = config.format;
|
|
980
|
-
if (!formats || formats.length === 0) return true;
|
|
981
|
-
if (!modifiers.has(Modifier.requiresQuotes)) for (const format of formats) {
|
|
982
|
-
const checker = FormatCheckersMap[format];
|
|
983
|
-
if (checker(name)) return true;
|
|
984
|
-
}
|
|
985
|
-
context.report({
|
|
986
|
-
data: formatReportData({
|
|
987
|
-
formats,
|
|
988
|
-
originalName,
|
|
989
|
-
processedName: name
|
|
990
|
-
}),
|
|
991
|
-
messageId: originalName === name ? "doesNotMatchFormat" : "doesNotMatchFormatTrimmed",
|
|
992
|
-
node
|
|
993
|
-
});
|
|
994
|
-
return false;
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
const SelectorsAllowedToHaveTypes = Selector.variable | Selector.parameter | Selector.classProperty | Selector.objectLiteralProperty | Selector.typeProperty | Selector.parameterProperty | Selector.classicAccessor;
|
|
998
|
-
function isAllTypesMatch(type, callback) {
|
|
999
|
-
if (type.isUnion()) return type.types.every((inner) => callback(inner));
|
|
1000
|
-
return callback(type);
|
|
1001
|
-
}
|
|
1002
|
-
function isCorrectType(node, config, context, selector) {
|
|
1003
|
-
if (config.types === void 0) return true;
|
|
1004
|
-
if ((SelectorsAllowedToHaveTypes & selector) === 0) return true;
|
|
1005
|
-
const services = getParserServices(context);
|
|
1006
|
-
const checker = services.program.getTypeChecker();
|
|
1007
|
-
const type = services.getTypeAtLocation(node).getNonNullableType();
|
|
1008
|
-
for (const allowedType of config.types) switch (allowedType) {
|
|
1009
|
-
case TypeModifier.array:
|
|
1010
|
-
if (isAllTypesMatch(type, (inner) => checker.isArrayType(inner) || checker.isTupleType(inner))) return true;
|
|
1011
|
-
break;
|
|
1012
|
-
case TypeModifier.boolean:
|
|
1013
|
-
case TypeModifier.number:
|
|
1014
|
-
case TypeModifier.string:
|
|
1015
|
-
if (checker.typeToString(checker.getWidenedType(checker.getBaseTypeOfLiteralType(type))) === TypeModifierValueToKey[allowedType]) return true;
|
|
1016
|
-
break;
|
|
1017
|
-
case TypeModifier.function:
|
|
1018
|
-
if (isAllTypesMatch(type, (inner) => inner.getCallSignatures().length > 0)) return true;
|
|
1019
|
-
break;
|
|
1020
|
-
}
|
|
1021
|
-
return false;
|
|
1022
|
-
}
|
|
1023
|
-
//#endregion
|
|
1024
|
-
//#region src/rules/naming-convention/utils/parse-options.ts
|
|
1025
|
-
function parseOptions(context) {
|
|
1026
|
-
const normalizedOptions = context.options.flatMap(normalizeOption);
|
|
1027
|
-
return Object.fromEntries(Object.keys(Selector).map((key) => [key, createValidator(key, context, normalizedOptions)]));
|
|
1028
|
-
}
|
|
1029
|
-
function normalizeOption(option) {
|
|
1030
|
-
let weight = 0;
|
|
1031
|
-
if (option.modifiers) for (const modifier of option.modifiers) weight |= Modifier[modifier];
|
|
1032
|
-
if (option.types) for (const type of option.types) weight |= TypeModifier[type];
|
|
1033
|
-
if (option.filter !== void 0) weight |= 1 << 30;
|
|
1034
|
-
const normalizedOption = {
|
|
1035
|
-
custom: option.custom ? {
|
|
1036
|
-
match: option.custom.match,
|
|
1037
|
-
regex: new RegExp(option.custom.regex, "u")
|
|
1038
|
-
} : void 0,
|
|
1039
|
-
filter: option.filter !== void 0 ? typeof option.filter === "string" ? {
|
|
1040
|
-
match: true,
|
|
1041
|
-
regex: new RegExp(option.filter, "u")
|
|
1042
|
-
} : {
|
|
1043
|
-
match: option.filter.match,
|
|
1044
|
-
regex: new RegExp(option.filter.regex, "u")
|
|
1045
|
-
} : void 0,
|
|
1046
|
-
format: option.format ? option.format.map((format) => PredefinedFormat[format]) : void 0,
|
|
1047
|
-
leadingUnderscore: option.leadingUnderscore !== void 0 ? UnderscoreOption[option.leadingUnderscore] : void 0,
|
|
1048
|
-
modifiers: option.modifiers?.map((modifier) => Modifier[modifier]) ?? void 0,
|
|
1049
|
-
modifierWeight: weight,
|
|
1050
|
-
prefix: option.prefix && option.prefix.length > 0 ? option.prefix : void 0,
|
|
1051
|
-
suffix: option.suffix && option.suffix.length > 0 ? option.suffix : void 0,
|
|
1052
|
-
trailingUnderscore: option.trailingUnderscore !== void 0 ? UnderscoreOption[option.trailingUnderscore] : void 0,
|
|
1053
|
-
types: option.types?.map((type) => TypeModifier[type]) ?? void 0
|
|
1054
|
-
};
|
|
1055
|
-
return (Array.isArray(option.selector) ? option.selector : [option.selector]).map((selector) => {
|
|
1056
|
-
return {
|
|
1057
|
-
selector: isMetaSelector(selector) ? MetaSelector[selector] : Selector[selector],
|
|
1058
|
-
...normalizedOption
|
|
1059
|
-
};
|
|
1060
|
-
});
|
|
1061
|
-
}
|
|
1062
|
-
//#endregion
|
|
1063
|
-
//#region src/rules/naming-convention/utils/schema.ts
|
|
1064
|
-
const $DEFS = {
|
|
1065
|
-
formatOptionsConfig: { oneOf: [{
|
|
1066
|
-
additionalItems: false,
|
|
1067
|
-
items: { $ref: "#/$defs/predefinedFormats" },
|
|
1068
|
-
type: "array"
|
|
1069
|
-
}, { type: "null" }] },
|
|
1070
|
-
matchRegexConfig: {
|
|
1071
|
-
additionalProperties: false,
|
|
1072
|
-
properties: {
|
|
1073
|
-
match: { type: "boolean" },
|
|
1074
|
-
regex: { type: "string" }
|
|
1075
|
-
},
|
|
1076
|
-
required: ["match", "regex"],
|
|
1077
|
-
type: "object"
|
|
1078
|
-
},
|
|
1079
|
-
predefinedFormats: {
|
|
1080
|
-
enum: Object.keys(PredefinedFormat),
|
|
1081
|
-
type: "string"
|
|
1082
|
-
},
|
|
1083
|
-
prefixSuffixConfig: {
|
|
1084
|
-
additionalItems: false,
|
|
1085
|
-
items: {
|
|
1086
|
-
minLength: 1,
|
|
1087
|
-
type: "string"
|
|
1088
|
-
},
|
|
1089
|
-
type: "array"
|
|
1090
|
-
},
|
|
1091
|
-
typeModifiers: {
|
|
1092
|
-
enum: Object.keys(TypeModifier),
|
|
1093
|
-
type: "string"
|
|
1094
|
-
},
|
|
1095
|
-
underscoreOptions: {
|
|
1096
|
-
enum: Object.keys(UnderscoreOption),
|
|
1097
|
-
type: "string"
|
|
1098
|
-
}
|
|
1099
|
-
};
|
|
1100
|
-
const UNDERSCORE_SCHEMA = { $ref: "#/$defs/underscoreOptions" };
|
|
1101
|
-
const PREFIX_SUFFIX_SCHEMA = { $ref: "#/$defs/prefixSuffixConfig" };
|
|
1102
|
-
const MATCH_REGEX_SCHEMA = { $ref: "#/$defs/matchRegexConfig" };
|
|
1103
|
-
const FORMAT_OPTIONS_PROPERTIES = {
|
|
1104
|
-
custom: MATCH_REGEX_SCHEMA,
|
|
1105
|
-
failureMessage: { type: "string" },
|
|
1106
|
-
format: { $ref: "#/$defs/formatOptionsConfig" },
|
|
1107
|
-
leadingUnderscore: UNDERSCORE_SCHEMA,
|
|
1108
|
-
prefix: PREFIX_SUFFIX_SCHEMA,
|
|
1109
|
-
suffix: PREFIX_SUFFIX_SCHEMA,
|
|
1110
|
-
trailingUnderscore: UNDERSCORE_SCHEMA
|
|
1111
|
-
};
|
|
1112
|
-
function selectorSchema(selectorString, allowType, modifiers) {
|
|
1113
|
-
const selector = {
|
|
1114
|
-
filter: { oneOf: [{
|
|
1115
|
-
minLength: 1,
|
|
1116
|
-
type: "string"
|
|
1117
|
-
}, MATCH_REGEX_SCHEMA] },
|
|
1118
|
-
selector: {
|
|
1119
|
-
enum: [selectorString],
|
|
1120
|
-
type: "string"
|
|
1121
|
-
}
|
|
1122
|
-
};
|
|
1123
|
-
if (modifiers && modifiers.length > 0) selector["modifiers"] = {
|
|
1124
|
-
additionalItems: false,
|
|
1125
|
-
items: {
|
|
1126
|
-
enum: modifiers,
|
|
1127
|
-
type: "string"
|
|
1128
|
-
},
|
|
1129
|
-
type: "array"
|
|
1130
|
-
};
|
|
1131
|
-
if (allowType) selector["types"] = {
|
|
1132
|
-
additionalItems: false,
|
|
1133
|
-
items: { $ref: "#/$defs/typeModifiers" },
|
|
1134
|
-
type: "array"
|
|
1135
|
-
};
|
|
1136
|
-
return [{
|
|
1137
|
-
additionalProperties: false,
|
|
1138
|
-
description: `Selector '${selectorString}'`,
|
|
1139
|
-
properties: {
|
|
1140
|
-
...FORMAT_OPTIONS_PROPERTIES,
|
|
1141
|
-
...selector
|
|
1142
|
-
},
|
|
1143
|
-
required: ["selector", "format"],
|
|
1144
|
-
type: "object"
|
|
1145
|
-
}];
|
|
1146
|
-
}
|
|
1147
|
-
function selectorsSchema() {
|
|
1148
|
-
return {
|
|
1149
|
-
additionalProperties: false,
|
|
1150
|
-
description: "Multiple selectors in one config",
|
|
1151
|
-
properties: {
|
|
1152
|
-
...FORMAT_OPTIONS_PROPERTIES,
|
|
1153
|
-
filter: { oneOf: [{
|
|
1154
|
-
minLength: 1,
|
|
1155
|
-
type: "string"
|
|
1156
|
-
}, MATCH_REGEX_SCHEMA] },
|
|
1157
|
-
modifiers: {
|
|
1158
|
-
additionalItems: false,
|
|
1159
|
-
items: {
|
|
1160
|
-
enum: Object.keys(Modifier),
|
|
1161
|
-
type: "string"
|
|
1162
|
-
},
|
|
1163
|
-
type: "array"
|
|
1164
|
-
},
|
|
1165
|
-
selector: {
|
|
1166
|
-
additionalItems: false,
|
|
1167
|
-
items: {
|
|
1168
|
-
enum: [...Object.keys(MetaSelector), ...Object.keys(Selector)],
|
|
1169
|
-
type: "string"
|
|
1170
|
-
},
|
|
1171
|
-
type: "array"
|
|
1172
|
-
},
|
|
1173
|
-
types: {
|
|
1174
|
-
additionalItems: false,
|
|
1175
|
-
items: { $ref: "#/$defs/typeModifiers" },
|
|
1176
|
-
type: "array"
|
|
1177
|
-
}
|
|
1178
|
-
},
|
|
1179
|
-
required: ["selector", "format"],
|
|
1180
|
-
type: "object"
|
|
1181
|
-
};
|
|
1182
|
-
}
|
|
1183
|
-
const SCHEMA = {
|
|
1184
|
-
$defs: $DEFS,
|
|
1185
|
-
additionalItems: false,
|
|
1186
|
-
items: { oneOf: [
|
|
1187
|
-
selectorsSchema(),
|
|
1188
|
-
...selectorSchema("default", false, Object.keys(Modifier)),
|
|
1189
|
-
...selectorSchema("variableLike", false, ["unused", "async"]),
|
|
1190
|
-
...selectorSchema("variable", true, [
|
|
1191
|
-
"const",
|
|
1192
|
-
"destructured",
|
|
1193
|
-
"exported",
|
|
1194
|
-
"global",
|
|
1195
|
-
"unused",
|
|
1196
|
-
"async"
|
|
1197
|
-
]),
|
|
1198
|
-
...selectorSchema("function", false, [
|
|
1199
|
-
"exported",
|
|
1200
|
-
"global",
|
|
1201
|
-
"unused",
|
|
1202
|
-
"async"
|
|
1203
|
-
]),
|
|
1204
|
-
...selectorSchema("parameter", true, ["destructured", "unused"]),
|
|
1205
|
-
...selectorSchema("objectStyleEnum", false, [
|
|
1206
|
-
"const",
|
|
1207
|
-
"exported",
|
|
1208
|
-
"global",
|
|
1209
|
-
"unused"
|
|
1210
|
-
]),
|
|
1211
|
-
...selectorSchema("memberLike", false, [
|
|
1212
|
-
"abstract",
|
|
1213
|
-
"private",
|
|
1214
|
-
"#private",
|
|
1215
|
-
"protected",
|
|
1216
|
-
"public",
|
|
1217
|
-
"readonly",
|
|
1218
|
-
"requiresQuotes",
|
|
1219
|
-
"static",
|
|
1220
|
-
"override",
|
|
1221
|
-
"async"
|
|
1222
|
-
]),
|
|
1223
|
-
...selectorSchema("classProperty", true, [
|
|
1224
|
-
"abstract",
|
|
1225
|
-
"private",
|
|
1226
|
-
"#private",
|
|
1227
|
-
"protected",
|
|
1228
|
-
"public",
|
|
1229
|
-
"readonly",
|
|
1230
|
-
"requiresQuotes",
|
|
1231
|
-
"static",
|
|
1232
|
-
"override"
|
|
1233
|
-
]),
|
|
1234
|
-
...selectorSchema("objectLiteralProperty", true, ["public", "requiresQuotes"]),
|
|
1235
|
-
...selectorSchema("typeProperty", true, [
|
|
1236
|
-
"public",
|
|
1237
|
-
"readonly",
|
|
1238
|
-
"requiresQuotes"
|
|
1239
|
-
]),
|
|
1240
|
-
...selectorSchema("parameterProperty", true, [
|
|
1241
|
-
"private",
|
|
1242
|
-
"protected",
|
|
1243
|
-
"public",
|
|
1244
|
-
"readonly"
|
|
1245
|
-
]),
|
|
1246
|
-
...selectorSchema("property", true, [
|
|
1247
|
-
"abstract",
|
|
1248
|
-
"private",
|
|
1249
|
-
"#private",
|
|
1250
|
-
"protected",
|
|
1251
|
-
"public",
|
|
1252
|
-
"readonly",
|
|
1253
|
-
"requiresQuotes",
|
|
1254
|
-
"static",
|
|
1255
|
-
"override",
|
|
1256
|
-
"async"
|
|
1257
|
-
]),
|
|
1258
|
-
...selectorSchema("classMethod", false, [
|
|
1259
|
-
"abstract",
|
|
1260
|
-
"private",
|
|
1261
|
-
"#private",
|
|
1262
|
-
"protected",
|
|
1263
|
-
"public",
|
|
1264
|
-
"requiresQuotes",
|
|
1265
|
-
"static",
|
|
1266
|
-
"override",
|
|
1267
|
-
"async"
|
|
1268
|
-
]),
|
|
1269
|
-
...selectorSchema("objectLiteralMethod", false, [
|
|
1270
|
-
"public",
|
|
1271
|
-
"requiresQuotes",
|
|
1272
|
-
"async"
|
|
1273
|
-
]),
|
|
1274
|
-
...selectorSchema("typeMethod", false, ["public", "requiresQuotes"]),
|
|
1275
|
-
...selectorSchema("method", false, [
|
|
1276
|
-
"abstract",
|
|
1277
|
-
"private",
|
|
1278
|
-
"#private",
|
|
1279
|
-
"protected",
|
|
1280
|
-
"public",
|
|
1281
|
-
"requiresQuotes",
|
|
1282
|
-
"static",
|
|
1283
|
-
"override",
|
|
1284
|
-
"async"
|
|
1285
|
-
]),
|
|
1286
|
-
...selectorSchema("classicAccessor", true, [
|
|
1287
|
-
"abstract",
|
|
1288
|
-
"private",
|
|
1289
|
-
"protected",
|
|
1290
|
-
"public",
|
|
1291
|
-
"requiresQuotes",
|
|
1292
|
-
"static",
|
|
1293
|
-
"override"
|
|
1294
|
-
]),
|
|
1295
|
-
...selectorSchema("autoAccessor", true, [
|
|
1296
|
-
"abstract",
|
|
1297
|
-
"private",
|
|
1298
|
-
"protected",
|
|
1299
|
-
"public",
|
|
1300
|
-
"requiresQuotes",
|
|
1301
|
-
"static",
|
|
1302
|
-
"override"
|
|
1303
|
-
]),
|
|
1304
|
-
...selectorSchema("accessor", true, [
|
|
1305
|
-
"abstract",
|
|
1306
|
-
"private",
|
|
1307
|
-
"protected",
|
|
1308
|
-
"public",
|
|
1309
|
-
"requiresQuotes",
|
|
1310
|
-
"static",
|
|
1311
|
-
"override"
|
|
1312
|
-
]),
|
|
1313
|
-
...selectorSchema("enumMember", false, ["requiresQuotes"]),
|
|
1314
|
-
...selectorSchema("typeLike", false, [
|
|
1315
|
-
"abstract",
|
|
1316
|
-
"exported",
|
|
1317
|
-
"unused"
|
|
1318
|
-
]),
|
|
1319
|
-
...selectorSchema("class", false, [
|
|
1320
|
-
"abstract",
|
|
1321
|
-
"exported",
|
|
1322
|
-
"unused"
|
|
1323
|
-
]),
|
|
1324
|
-
...selectorSchema("interface", false, ["exported", "unused"]),
|
|
1325
|
-
...selectorSchema("typeAlias", false, ["exported", "unused"]),
|
|
1326
|
-
...selectorSchema("enum", false, ["exported", "unused"]),
|
|
1327
|
-
...selectorSchema("typeParameter", false, ["unused"]),
|
|
1328
|
-
...selectorSchema("import", false, ["default", "namespace"])
|
|
1329
|
-
] },
|
|
1330
|
-
type: "array"
|
|
1331
|
-
};
|
|
1332
|
-
//#endregion
|
|
1333
|
-
//#region src/rules/naming-convention/rule.ts
|
|
1334
|
-
const RULE_NAME$7 = "naming-convention";
|
|
1335
|
-
const messages$7 = {
|
|
1336
|
-
doesNotMatchFormat: "{{type}} name `{{name}}` must match one of the following formats: {{formats}}",
|
|
1337
|
-
doesNotMatchFormatTrimmed: "{{type}} name `{{name}}` trimmed as `{{processedName}}` must match one of the following formats: {{formats}}",
|
|
1338
|
-
missingAffix: "{{type}} name `{{name}}` must have one of the following {{position}}es: {{affixes}}",
|
|
1339
|
-
missingUnderscore: "{{type}} name `{{name}}` must have {{count}} {{position}} underscore(s).",
|
|
1340
|
-
satisfyCustom: "{{type}} name `{{name}}` must {{regexMatch}} the RegExp: {{regex}}",
|
|
1341
|
-
unexpectedUnderscore: "{{type}} name `{{name}}` must not have a {{position}} underscore."
|
|
1342
|
-
};
|
|
1343
|
-
const camelCaseNamingConfig = [
|
|
1344
|
-
{
|
|
1345
|
-
format: ["camelCase"],
|
|
1346
|
-
leadingUnderscore: "allow",
|
|
1347
|
-
selector: "default",
|
|
1348
|
-
trailingUnderscore: "allow"
|
|
1349
|
-
},
|
|
1350
|
-
{
|
|
1351
|
-
format: ["camelCase", "PascalCase"],
|
|
1352
|
-
selector: "import"
|
|
1353
|
-
},
|
|
1354
|
-
{
|
|
1355
|
-
format: ["camelCase", "UPPER_CASE"],
|
|
1356
|
-
leadingUnderscore: "allow",
|
|
1357
|
-
selector: "variable",
|
|
1358
|
-
trailingUnderscore: "allow"
|
|
1359
|
-
},
|
|
1360
|
-
{
|
|
1361
|
-
format: ["PascalCase"],
|
|
1362
|
-
selector: "typeLike"
|
|
1363
|
-
}
|
|
1364
|
-
];
|
|
1365
|
-
function create$7(contextWithoutDefaults) {
|
|
1366
|
-
const context = contextWithoutDefaults.options.length > 0 ? contextWithoutDefaults : Object.setPrototypeOf({ options: camelCaseNamingConfig }, contextWithoutDefaults);
|
|
1367
|
-
const validators = parseOptions(context);
|
|
1368
|
-
const compilerOptions = getParserServices(context, true).program?.getCompilerOptions() ?? {};
|
|
1369
|
-
function handleMember(validator, { key }, modifiers) {
|
|
1370
|
-
if (requiresQuoting$1(key, compilerOptions.target)) modifiers.add(Modifier.requiresQuotes);
|
|
1371
|
-
validator(key, modifiers);
|
|
1372
|
-
}
|
|
1373
|
-
function getMemberModifiers(node) {
|
|
1374
|
-
const modifiers = /* @__PURE__ */ new Set();
|
|
1375
|
-
if ("key" in node && node.key.type === AST_NODE_TYPES.PrivateIdentifier) modifiers.add(Modifier["#private"]);
|
|
1376
|
-
else if (node.accessibility) modifiers.add(Modifier[node.accessibility]);
|
|
1377
|
-
else modifiers.add(Modifier.public);
|
|
1378
|
-
if (node.static) modifiers.add(Modifier.static);
|
|
1379
|
-
if ("readonly" in node && node.readonly) modifiers.add(Modifier.readonly);
|
|
1380
|
-
if ("override" in node && node.override) modifiers.add(Modifier.override);
|
|
1381
|
-
if (node.type === AST_NODE_TYPES.TSAbstractPropertyDefinition || node.type === AST_NODE_TYPES.TSAbstractMethodDefinition || node.type === AST_NODE_TYPES.TSAbstractAccessorProperty) modifiers.add(Modifier.abstract);
|
|
1382
|
-
return modifiers;
|
|
1383
|
-
}
|
|
1384
|
-
const { unusedVariables } = collectVariables(context);
|
|
1385
|
-
function isUnused(name, initialScope) {
|
|
1386
|
-
let variable = null;
|
|
1387
|
-
let scope = initialScope;
|
|
1388
|
-
while (scope) {
|
|
1389
|
-
variable = scope.set.get(name) ?? null;
|
|
1390
|
-
if (variable) break;
|
|
1391
|
-
scope = scope.upper;
|
|
1392
|
-
}
|
|
1393
|
-
if (!variable) return false;
|
|
1394
|
-
return unusedVariables.has(variable);
|
|
1395
|
-
}
|
|
1396
|
-
function isDestructured(id) {
|
|
1397
|
-
return id.parent.type === AST_NODE_TYPES.Property && id.parent.shorthand || id.parent.type === AST_NODE_TYPES.AssignmentPattern && id.parent.parent.type === AST_NODE_TYPES.Property && id.parent.parent.shorthand;
|
|
1398
|
-
}
|
|
1399
|
-
function isAsyncMemberOrProperty(propertyOrMemberNode) {
|
|
1400
|
-
return Boolean("value" in propertyOrMemberNode && propertyOrMemberNode.value && "async" in propertyOrMemberNode.value && propertyOrMemberNode.value.async);
|
|
1401
|
-
}
|
|
1402
|
-
function isAsyncVariableIdentifier(id) {
|
|
1403
|
-
return Boolean("async" in id.parent && id.parent.async || "init" in id.parent && id.parent.init && "async" in id.parent.init && id.parent.init.async);
|
|
1404
|
-
}
|
|
1405
|
-
/**
|
|
1406
|
-
* Determines if a VariableDeclarator represents an object-style enum declaration.
|
|
1407
|
-
*
|
|
1408
|
-
* Object-style enums are const assertions applied to object expressions, commonly
|
|
1409
|
-
* used as an alternative to TypeScript enums. Examples:
|
|
1410
|
-
* - `const Colors = { RED: 'red', BLUE: 'blue' } as const`
|
|
1411
|
-
* - `const Status = <const>{ OK: 200, ERROR: 500 }`.
|
|
1412
|
-
*
|
|
1413
|
-
* @param node - The VariableDeclarator AST node to check.
|
|
1414
|
-
* @param parent - The parent VariableDeclaration node.
|
|
1415
|
-
* @returns True if this represents an object-style enum declaration.
|
|
1416
|
-
*/
|
|
1417
|
-
function isObjectStyleEnumDeclaration(node, parent) {
|
|
1418
|
-
if (parent.kind !== "const") return false;
|
|
1419
|
-
if (!node.init) return false;
|
|
1420
|
-
if (node.init.type === AST_NODE_TYPES.TSAsExpression && node.init.typeAnnotation.type === AST_NODE_TYPES.TSTypeReference && node.init.typeAnnotation.typeName.type === AST_NODE_TYPES.Identifier && node.init.typeAnnotation.typeName.name === "const" && node.init.expression.type === AST_NODE_TYPES.ObjectExpression) return true;
|
|
1421
|
-
if (node.init.type === AST_NODE_TYPES.TSTypeAssertion && node.init.typeAnnotation.type === AST_NODE_TYPES.TSTypeReference && node.init.typeAnnotation.typeName.type === AST_NODE_TYPES.Identifier && node.init.typeAnnotation.typeName.name === "const" && node.init.expression.type === AST_NODE_TYPES.ObjectExpression) return true;
|
|
1422
|
-
return false;
|
|
1423
|
-
}
|
|
1424
|
-
/**
|
|
1425
|
-
* Checks if a method name exists in any of the implemented interfaces.
|
|
1426
|
-
*
|
|
1427
|
-
* @param implementsList - List of implemented interfaces.
|
|
1428
|
-
* @param methodName - Name of the method to check.
|
|
1429
|
-
* @param checker - TypeScript type checker.
|
|
1430
|
-
* @param services - Parser services.
|
|
1431
|
-
* @returns True if the method name is found in any interface.
|
|
1432
|
-
*/
|
|
1433
|
-
function checkInterfacesForMethod(implementsList, methodName, checker, services) {
|
|
1434
|
-
for (const implementsClause of implementsList) {
|
|
1435
|
-
const interfaceType = checker.getTypeAtLocation(services.esTreeNodeToTSNodeMap.get(implementsClause));
|
|
1436
|
-
if (!interfaceType.getSymbol()) continue;
|
|
1437
|
-
const interfaceMembers = checker.getPropertiesOfType(interfaceType);
|
|
1438
|
-
for (const member of interfaceMembers) if (member.name === methodName) return true;
|
|
1439
|
-
}
|
|
1440
|
-
return false;
|
|
1441
|
-
}
|
|
1442
|
-
/**
|
|
1443
|
-
* Determines if a class method is implementing an interface method.
|
|
1444
|
-
*
|
|
1445
|
-
* @param node - The class method node to check.
|
|
1446
|
-
* @returns True if the method is implementing an interface method.
|
|
1447
|
-
*/
|
|
1448
|
-
function isImplementingInterfaceMethod(node) {
|
|
1449
|
-
const services = getParserServices(context, true);
|
|
1450
|
-
if (!services.program) return false;
|
|
1451
|
-
const checker = services.program.getTypeChecker();
|
|
1452
|
-
let parentClass = null;
|
|
1453
|
-
let current = node.parent;
|
|
1454
|
-
while (current) {
|
|
1455
|
-
if (current.type === AST_NODE_TYPES.ClassDeclaration || current.type === AST_NODE_TYPES.ClassExpression) {
|
|
1456
|
-
parentClass = current;
|
|
1457
|
-
break;
|
|
1458
|
-
}
|
|
1459
|
-
current = current.parent;
|
|
1460
|
-
}
|
|
1461
|
-
if (!parentClass?.implements || parentClass.implements.length === 0) return false;
|
|
1462
|
-
let methodName = null;
|
|
1463
|
-
if (node.key.type === AST_NODE_TYPES.Identifier) methodName = node.key.name;
|
|
1464
|
-
else if (node.key.type === AST_NODE_TYPES.Literal) methodName = String(node.key.value);
|
|
1465
|
-
if (methodName === null) return false;
|
|
1466
|
-
return checkInterfacesForMethod(parentClass.implements, methodName, checker, services);
|
|
1467
|
-
}
|
|
1468
|
-
const selectors = {
|
|
1469
|
-
":matches(PropertyDefinition, TSAbstractPropertyDefinition)[computed = false][value.type != \"ArrowFunctionExpression\"][value.type != \"FunctionExpression\"][value.type != \"TSEmptyBodyFunctionExpression\"]": {
|
|
1470
|
-
handler: (node, validator) => {
|
|
1471
|
-
handleMember(validator, node, getMemberModifiers(node));
|
|
1472
|
-
},
|
|
1473
|
-
validator: validators.classProperty
|
|
1474
|
-
},
|
|
1475
|
-
":not(ObjectPattern) > Property[computed = false][kind = \"init\"][value.type != \"ArrowFunctionExpression\"][value.type != \"FunctionExpression\"][value.type != \"TSEmptyBodyFunctionExpression\"]": {
|
|
1476
|
-
handler: (node, validator) => {
|
|
1477
|
-
handleMember(validator, node, /* @__PURE__ */ new Set([Modifier.public]));
|
|
1478
|
-
},
|
|
1479
|
-
validator: validators.objectLiteralProperty
|
|
1480
|
-
},
|
|
1481
|
-
[["TSMethodSignature[computed = false]", "TSPropertySignature[computed = false][typeAnnotation.typeAnnotation.type = \"TSFunctionType\"]"].join(", ")]: {
|
|
1482
|
-
handler: (node, validator) => {
|
|
1483
|
-
handleMember(validator, node, /* @__PURE__ */ new Set([Modifier.public]));
|
|
1484
|
-
},
|
|
1485
|
-
validator: validators.typeMethod
|
|
1486
|
-
},
|
|
1487
|
-
[[
|
|
1488
|
-
":matches(PropertyDefinition, TSAbstractPropertyDefinition)[computed = false][value.type = \"ArrowFunctionExpression\"]",
|
|
1489
|
-
":matches(PropertyDefinition, TSAbstractPropertyDefinition)[computed = false][value.type = \"FunctionExpression\"]",
|
|
1490
|
-
":matches(PropertyDefinition, TSAbstractPropertyDefinition)[computed = false][value.type = \"TSEmptyBodyFunctionExpression\"]",
|
|
1491
|
-
":matches(MethodDefinition, TSAbstractMethodDefinition)[computed = false][kind = \"method\"]"
|
|
1492
|
-
].join(", ")]: {
|
|
1493
|
-
handler: (node, validator) => {
|
|
1494
|
-
if (isImplementingInterfaceMethod(node)) return;
|
|
1495
|
-
const modifiers = getMemberModifiers(node);
|
|
1496
|
-
if (isAsyncMemberOrProperty(node)) modifiers.add(Modifier.async);
|
|
1497
|
-
handleMember(validator, node, modifiers);
|
|
1498
|
-
},
|
|
1499
|
-
validator: validators.classMethod
|
|
1500
|
-
},
|
|
1501
|
-
[["MethodDefinition[computed = false]:matches([kind = \"get\"], [kind = \"set\"])", "TSAbstractMethodDefinition[computed = false]:matches([kind=\"get\"], [kind=\"set\"])"].join(", ")]: {
|
|
1502
|
-
handler: (node, validator) => {
|
|
1503
|
-
handleMember(validator, node, getMemberModifiers(node));
|
|
1504
|
-
},
|
|
1505
|
-
validator: validators.classicAccessor
|
|
1506
|
-
},
|
|
1507
|
-
[[
|
|
1508
|
-
"Property[computed = false][kind = \"init\"][value.type = \"ArrowFunctionExpression\"]",
|
|
1509
|
-
"Property[computed = false][kind = \"init\"][value.type = \"FunctionExpression\"]",
|
|
1510
|
-
"Property[computed = false][kind = \"init\"][value.type = \"TSEmptyBodyFunctionExpression\"]"
|
|
1511
|
-
].join(", ")]: {
|
|
1512
|
-
handler: (node, validator) => {
|
|
1513
|
-
const modifiers = /* @__PURE__ */ new Set([Modifier.public]);
|
|
1514
|
-
if (isAsyncMemberOrProperty(node)) modifiers.add(Modifier.async);
|
|
1515
|
-
handleMember(validator, node, modifiers);
|
|
1516
|
-
},
|
|
1517
|
-
validator: validators.objectLiteralMethod
|
|
1518
|
-
},
|
|
1519
|
-
[[AST_NODE_TYPES.AccessorProperty, AST_NODE_TYPES.TSAbstractAccessorProperty].join(", ")]: {
|
|
1520
|
-
handler: (node, validator) => {
|
|
1521
|
-
handleMember(validator, node, getMemberModifiers(node));
|
|
1522
|
-
},
|
|
1523
|
-
validator: validators.autoAccessor
|
|
1524
|
-
},
|
|
1525
|
-
"ClassDeclaration, ClassExpression": {
|
|
1526
|
-
handler: (node, validator) => {
|
|
1527
|
-
const { id, abstract } = node;
|
|
1528
|
-
if (id === null) return;
|
|
1529
|
-
const modifiers = /* @__PURE__ */ new Set();
|
|
1530
|
-
const scope = context.sourceCode.getScope(node).upper;
|
|
1531
|
-
if (abstract) modifiers.add(Modifier.abstract);
|
|
1532
|
-
if (isExported(node, id.name, scope)) modifiers.add(Modifier.exported);
|
|
1533
|
-
if (isUnused(id.name, scope)) modifiers.add(Modifier.unused);
|
|
1534
|
-
validator(id, modifiers);
|
|
1535
|
-
},
|
|
1536
|
-
validator: validators.class
|
|
1537
|
-
},
|
|
1538
|
-
"FunctionDeclaration, TSDeclareFunction, FunctionExpression": {
|
|
1539
|
-
handler: (node, validator) => {
|
|
1540
|
-
if (node.id === null) return;
|
|
1541
|
-
const modifiers = /* @__PURE__ */ new Set();
|
|
1542
|
-
const scope = context.sourceCode.getScope(node).upper;
|
|
1543
|
-
if (isGlobal(scope)) modifiers.add(Modifier.global);
|
|
1544
|
-
if (isExported(node, node.id.name, scope)) modifiers.add(Modifier.exported);
|
|
1545
|
-
if (isUnused(node.id.name, scope)) modifiers.add(Modifier.unused);
|
|
1546
|
-
if (node.async) modifiers.add(Modifier.async);
|
|
1547
|
-
validator(node.id, modifiers);
|
|
1548
|
-
},
|
|
1549
|
-
validator: validators.function
|
|
1550
|
-
},
|
|
1551
|
-
"FunctionDeclaration, TSDeclareFunction, TSEmptyBodyFunctionExpression, FunctionExpression, ArrowFunctionExpression": {
|
|
1552
|
-
handler: (node, validator) => {
|
|
1553
|
-
for (const parameter of node.params) {
|
|
1554
|
-
if (parameter.type === AST_NODE_TYPES.TSParameterProperty) continue;
|
|
1555
|
-
const identifiers = getIdentifiersFromPattern(parameter);
|
|
1556
|
-
for (const index of identifiers) {
|
|
1557
|
-
const modifiers = /* @__PURE__ */ new Set();
|
|
1558
|
-
if (isDestructured(index)) modifiers.add(Modifier.destructured);
|
|
1559
|
-
if (isUnused(index.name, context.sourceCode.getScope(index))) modifiers.add(Modifier.unused);
|
|
1560
|
-
validator(index, modifiers);
|
|
1561
|
-
}
|
|
1562
|
-
}
|
|
1563
|
-
},
|
|
1564
|
-
validator: validators.parameter
|
|
1565
|
-
},
|
|
1566
|
-
"ImportDefaultSpecifier, ImportNamespaceSpecifier, ImportSpecifier": {
|
|
1567
|
-
handler: (node, validator) => {
|
|
1568
|
-
const modifiers = /* @__PURE__ */ new Set();
|
|
1569
|
-
switch (node.type) {
|
|
1570
|
-
case AST_NODE_TYPES.ImportDefaultSpecifier:
|
|
1571
|
-
modifiers.add(Modifier.default);
|
|
1572
|
-
break;
|
|
1573
|
-
case AST_NODE_TYPES.ImportNamespaceSpecifier:
|
|
1574
|
-
modifiers.add(Modifier.namespace);
|
|
1575
|
-
break;
|
|
1576
|
-
case AST_NODE_TYPES.ImportSpecifier:
|
|
1577
|
-
if (node.imported.type === AST_NODE_TYPES.Identifier && node.imported.name !== "default") return;
|
|
1578
|
-
modifiers.add(Modifier.default);
|
|
1579
|
-
break;
|
|
1580
|
-
}
|
|
1581
|
-
validator(node.local, modifiers);
|
|
1582
|
-
},
|
|
1583
|
-
validator: validators.import
|
|
1584
|
-
},
|
|
1585
|
-
"Property[computed = false]:matches([kind = \"get\"], [kind = \"set\"])": {
|
|
1586
|
-
handler: (node, validator) => {
|
|
1587
|
-
handleMember(validator, node, /* @__PURE__ */ new Set([Modifier.public]));
|
|
1588
|
-
},
|
|
1589
|
-
validator: validators.classicAccessor
|
|
1590
|
-
},
|
|
1591
|
-
"TSEnumDeclaration": {
|
|
1592
|
-
handler: (node, validator) => {
|
|
1593
|
-
const modifiers = /* @__PURE__ */ new Set();
|
|
1594
|
-
const scope = context.sourceCode.getScope(node).upper;
|
|
1595
|
-
if (isExported(node, node.id.name, scope)) modifiers.add(Modifier.exported);
|
|
1596
|
-
if (isUnused(node.id.name, scope)) modifiers.add(Modifier.unused);
|
|
1597
|
-
validator(node.id, modifiers);
|
|
1598
|
-
},
|
|
1599
|
-
validator: validators.enum
|
|
1600
|
-
},
|
|
1601
|
-
"TSEnumMember": {
|
|
1602
|
-
handler: ({ id }, validator) => {
|
|
1603
|
-
const modifiers = /* @__PURE__ */ new Set();
|
|
1604
|
-
if (requiresQuoting$1(id, compilerOptions.target)) modifiers.add(Modifier.requiresQuotes);
|
|
1605
|
-
validator(id, modifiers);
|
|
1606
|
-
},
|
|
1607
|
-
validator: validators.enumMember
|
|
1608
|
-
},
|
|
1609
|
-
"TSInterfaceDeclaration": {
|
|
1610
|
-
handler: (node, validator) => {
|
|
1611
|
-
const modifiers = /* @__PURE__ */ new Set();
|
|
1612
|
-
const scope = context.sourceCode.getScope(node);
|
|
1613
|
-
if (isExported(node, node.id.name, scope)) modifiers.add(Modifier.exported);
|
|
1614
|
-
if (isUnused(node.id.name, scope)) modifiers.add(Modifier.unused);
|
|
1615
|
-
validator(node.id, modifiers);
|
|
1616
|
-
},
|
|
1617
|
-
validator: validators.interface
|
|
1618
|
-
},
|
|
1619
|
-
"TSParameterProperty": {
|
|
1620
|
-
handler: (node, validator) => {
|
|
1621
|
-
const modifiers = getMemberModifiers(node);
|
|
1622
|
-
const identifiers = getIdentifiersFromPattern(node.parameter);
|
|
1623
|
-
for (const index of identifiers) validator(index, modifiers);
|
|
1624
|
-
},
|
|
1625
|
-
validator: validators.parameterProperty
|
|
1626
|
-
},
|
|
1627
|
-
"TSPropertySignature[computed = false][typeAnnotation.typeAnnotation.type != \"TSFunctionType\"]": {
|
|
1628
|
-
handler: (node, validator) => {
|
|
1629
|
-
const modifiers = /* @__PURE__ */ new Set([Modifier.public]);
|
|
1630
|
-
if (node.readonly) modifiers.add(Modifier.readonly);
|
|
1631
|
-
handleMember(validator, node, modifiers);
|
|
1632
|
-
},
|
|
1633
|
-
validator: validators.typeProperty
|
|
1634
|
-
},
|
|
1635
|
-
"TSTypeAliasDeclaration": {
|
|
1636
|
-
handler: (node, validator) => {
|
|
1637
|
-
const modifiers = /* @__PURE__ */ new Set();
|
|
1638
|
-
const scope = context.sourceCode.getScope(node);
|
|
1639
|
-
if (isExported(node, node.id.name, scope)) modifiers.add(Modifier.exported);
|
|
1640
|
-
if (isUnused(node.id.name, scope)) modifiers.add(Modifier.unused);
|
|
1641
|
-
validator(node.id, modifiers);
|
|
1642
|
-
},
|
|
1643
|
-
validator: validators.typeAlias
|
|
1644
|
-
},
|
|
1645
|
-
"TSTypeParameterDeclaration > TSTypeParameter": {
|
|
1646
|
-
handler: (node, validator) => {
|
|
1647
|
-
const modifiers = /* @__PURE__ */ new Set();
|
|
1648
|
-
const scope = context.sourceCode.getScope(node);
|
|
1649
|
-
if (isUnused(node.name.name, scope)) modifiers.add(Modifier.unused);
|
|
1650
|
-
validator(node.name, modifiers);
|
|
1651
|
-
},
|
|
1652
|
-
validator: validators.typeParameter
|
|
1653
|
-
},
|
|
1654
|
-
"VariableDeclarator": {
|
|
1655
|
-
handler: (node, validator) => {
|
|
1656
|
-
const identifiers = getIdentifiersFromPattern(node.id);
|
|
1657
|
-
const baseModifiers = /* @__PURE__ */ new Set();
|
|
1658
|
-
const { parent } = node;
|
|
1659
|
-
if (parent.kind === "const") baseModifiers.add(Modifier.const);
|
|
1660
|
-
if (isGlobal(context.sourceCode.getScope(node))) baseModifiers.add(Modifier.global);
|
|
1661
|
-
const isObjectStyleEnum = isObjectStyleEnumDeclaration(node, parent);
|
|
1662
|
-
for (const id of identifiers) {
|
|
1663
|
-
const modifiers = new Set(baseModifiers);
|
|
1664
|
-
if (isDestructured(id)) modifiers.add(Modifier.destructured);
|
|
1665
|
-
const scope = context.sourceCode.getScope(id);
|
|
1666
|
-
if (isExported(parent, id.name, scope)) modifiers.add(Modifier.exported);
|
|
1667
|
-
if (isUnused(id.name, scope)) modifiers.add(Modifier.unused);
|
|
1668
|
-
if (isAsyncVariableIdentifier(id)) modifiers.add(Modifier.async);
|
|
1669
|
-
if (isObjectStyleEnum) validators.objectStyleEnum(id, modifiers);
|
|
1670
|
-
else validator(id, modifiers);
|
|
1671
|
-
}
|
|
1672
|
-
},
|
|
1673
|
-
validator: validators.variable
|
|
1674
|
-
}
|
|
1675
|
-
};
|
|
1676
|
-
return Object.fromEntries(Object.entries(selectors).map(([selector, { handler, validator }]) => {
|
|
1677
|
-
return [selector, (node) => {
|
|
1678
|
-
handler(node, validator);
|
|
1679
|
-
}];
|
|
1680
|
-
}));
|
|
1681
|
-
}
|
|
1682
|
-
const namingConvention = createEslintRule({
|
|
1683
|
-
name: RULE_NAME$7,
|
|
1684
|
-
create: create$7,
|
|
1685
|
-
defaultOptions: camelCaseNamingConfig,
|
|
1686
|
-
meta: {
|
|
1687
|
-
docs: {
|
|
1688
|
-
description: "Enforce naming conventions for everything across a codebase",
|
|
1689
|
-
recommended: true,
|
|
1690
|
-
requiresTypeChecking: true
|
|
1691
|
-
},
|
|
1692
|
-
fixable: void 0,
|
|
1693
|
-
hasSuggestions: false,
|
|
1694
|
-
messages: messages$7,
|
|
1695
|
-
schema: SCHEMA,
|
|
1696
|
-
type: "suggestion"
|
|
1697
|
-
}
|
|
1698
|
-
});
|
|
1699
|
-
function getIdentifiersFromPattern(pattern) {
|
|
1700
|
-
const identifiers = [];
|
|
1701
|
-
new PatternVisitor({}, pattern, (id) => {
|
|
1702
|
-
identifiers.push(id);
|
|
1703
|
-
}).visit(pattern);
|
|
1704
|
-
return identifiers;
|
|
1705
|
-
}
|
|
1706
|
-
function isExported(node, name, scope) {
|
|
1707
|
-
if (node?.parent?.type === AST_NODE_TYPES.ExportDefaultDeclaration || node?.parent?.type === AST_NODE_TYPES.ExportNamedDeclaration) return true;
|
|
1708
|
-
if (scope === null) return false;
|
|
1709
|
-
const variable = scope.set.get(name);
|
|
1710
|
-
if (variable) for (const ref of variable.references) {
|
|
1711
|
-
const refParent = ref.identifier.parent;
|
|
1712
|
-
if (refParent.type === AST_NODE_TYPES.ExportDefaultDeclaration || refParent.type === AST_NODE_TYPES.ExportSpecifier) return true;
|
|
1713
|
-
}
|
|
1714
|
-
return false;
|
|
1715
|
-
}
|
|
1716
|
-
function isGlobal(scope) {
|
|
1717
|
-
if (scope === null) return false;
|
|
1718
|
-
return scope.type === TSESLint.Scope.ScopeType.global || scope.type === TSESLint.Scope.ScopeType.module;
|
|
1719
|
-
}
|
|
1720
|
-
function requiresQuoting$1(node, target) {
|
|
1721
|
-
return requiresQuoting(node.type === AST_NODE_TYPES.Identifier || node.type === AST_NODE_TYPES.PrivateIdentifier ? node.name : `${node.value}`, target);
|
|
1722
|
-
}
|
|
1723
|
-
//#endregion
|
|
1724
|
-
//#region src/utils/nested-expressions.ts
|
|
1725
|
-
/**
|
|
1726
|
-
* Determines whether the given AST subtree contains a call or `new`
|
|
1727
|
-
* expression.
|
|
1728
|
-
*
|
|
1729
|
-
* Replaces `@eslint-react/ast`'s `getNestedCallExpressions` /
|
|
1730
|
-
* `getNestedNewExpressions` (not exposed by `@eslint-react/kit`). The `useMemo`
|
|
1731
|
-
* rule uses this to avoid flagging a factory that performs real computation.
|
|
1732
|
-
* The walk descends into every child node (skipping the `parent` back-link to
|
|
1733
|
-
* avoid cycles), so calls nested in awaits, tagged templates, computed member
|
|
1734
|
-
* expressions, call targets and the like are all detected.
|
|
1735
|
-
*
|
|
1736
|
-
* @param root - The subtree root to inspect (typically a function body).
|
|
1737
|
-
* @returns `true` if a `CallExpression` or `NewExpression` is present.
|
|
1738
|
-
*/
|
|
1739
|
-
function hasNestedCallOrNew(root) {
|
|
1740
|
-
const stack = [root];
|
|
1741
|
-
while (stack.length > 0) {
|
|
1742
|
-
const current = stack.pop();
|
|
1743
|
-
if (current === void 0) break;
|
|
1744
|
-
if (current.type === AST_NODE_TYPES.CallExpression || current.type === AST_NODE_TYPES.NewExpression) return true;
|
|
1745
|
-
pushChildNodes(current, stack);
|
|
1746
|
-
}
|
|
1747
|
-
return false;
|
|
1748
|
-
}
|
|
1749
|
-
/**
|
|
1750
|
-
* Type guard for an AST node value encountered while walking arbitrary
|
|
1751
|
-
* properties of a parent node.
|
|
1752
|
-
*
|
|
1753
|
-
* @param value - The candidate value.
|
|
1754
|
-
* @returns `true` if the value looks like a `TSESTree.Node`.
|
|
1755
|
-
*/
|
|
1756
|
-
function isNode(value) {
|
|
1757
|
-
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
1758
|
-
}
|
|
1759
|
-
/**
|
|
1760
|
-
* Pushes every child node of `node` onto the traversal stack, skipping the
|
|
1761
|
-
* `parent` back-link.
|
|
1762
|
-
*
|
|
1763
|
-
* @param node - The node whose children to enqueue.
|
|
1764
|
-
* @param stack - The traversal stack to push onto.
|
|
1765
|
-
*/
|
|
1766
|
-
function pushChildNodes(node, stack) {
|
|
1767
|
-
for (const key of Object.keys(node)) {
|
|
1768
|
-
if (key === "parent") continue;
|
|
1769
|
-
const value = node[key];
|
|
1770
|
-
if (Array.isArray(value)) {
|
|
1771
|
-
for (const item of value) if (isNode(item)) stack.push(item);
|
|
1772
|
-
} else if (isNode(value)) stack.push(value);
|
|
1773
|
-
}
|
|
1774
|
-
}
|
|
1775
|
-
//#endregion
|
|
1776
|
-
//#region src/utils/resolve.ts
|
|
1777
|
-
/**
|
|
1778
|
-
* Resolves an identifier to the AST node that represents its value.
|
|
1779
|
-
*
|
|
1780
|
-
* This is a focused re-implementation of `@eslint-react/var`'s `resolve`
|
|
1781
|
-
* covering only the cases the unnecessary-hook rules need: variable
|
|
1782
|
-
* initializers and function/class declarations. Every other definition kind
|
|
1783
|
-
* (imports, parameters, catch bindings, ...) resolves to `null`.
|
|
1784
|
-
*
|
|
1785
|
-
* @param sourceCode - Provides the scope used to look up the binding.
|
|
1786
|
-
* @param node - The identifier to resolve.
|
|
1787
|
-
* @returns The resolved value node, or `null` when it cannot be determined.
|
|
1788
|
-
*/
|
|
1789
|
-
function resolve(sourceCode, node) {
|
|
1790
|
-
const variable = findVariable(sourceCode.getScope(node), node);
|
|
1791
|
-
if (variable === null) return null;
|
|
1792
|
-
const definition = variable.defs.at(0);
|
|
1793
|
-
if (definition === void 0) return null;
|
|
1794
|
-
if (definition.type === DefinitionType.ClassName || definition.type === DefinitionType.FunctionName) return definition.node;
|
|
1795
|
-
if (definition.type === DefinitionType.Variable) return definition.node.init;
|
|
1796
|
-
return null;
|
|
1797
|
-
}
|
|
1798
|
-
//#endregion
|
|
1799
|
-
//#region src/utils/unnecessary-hook.ts
|
|
1800
|
-
/**
|
|
1801
|
-
* Builds the `create` for the `no-unnecessary-use-memo` /
|
|
1802
|
-
* `no-unnecessary-use-callback` rules.
|
|
1803
|
-
*
|
|
1804
|
-
* Faithfully ports the rules removed from `eslint-plugin-react-x` (they were
|
|
1805
|
-
* dropped because the React Compiler makes them redundant; the Roblox /
|
|
1806
|
-
* `@rbxts/react` ecosystem has no Compiler). Hook detection is delegated to
|
|
1807
|
-
* `@eslint-react/core` so it matches the upstream semantics (fully-qualified
|
|
1808
|
-
* name resolution across imports, namespaces and `require`).
|
|
1809
|
-
*
|
|
1810
|
-
* @param config - The hook-specific configuration.
|
|
1811
|
-
* @returns A rule `create` function.
|
|
1812
|
-
* @template MessageIds - The rule's message identifiers.
|
|
1813
|
-
*/
|
|
1814
|
-
function createUnnecessaryHookRule({ hook, messageIds }) {
|
|
1815
|
-
const skipComputation = hook === "useMemo";
|
|
1816
|
-
return (context) => {
|
|
1817
|
-
const { sourceCode } = context;
|
|
1818
|
-
const reactContext = context;
|
|
1819
|
-
const detectHookCall = hook === "useMemo" ? core.isUseMemoCall : core.isUseCallbackCall;
|
|
1820
|
-
return { VariableDeclarator(node) {
|
|
1821
|
-
const { id, init } = node;
|
|
1822
|
-
if (id.type !== AST_NODE_TYPES.Identifier || init?.type !== AST_NODE_TYPES.CallExpression || !detectHookCall(reactContext, init)) return;
|
|
1823
|
-
const [variable, ...rest] = sourceCode.getDeclaredVariables(node);
|
|
1824
|
-
if (variable === void 0 || rest.length > 0) return;
|
|
1825
|
-
const insideEffectReport = checkForUsageInsideUseEffect(sourceCode, init, messageIds.insideUseEffect);
|
|
1826
|
-
const component = sourceCode.getScope(init).block;
|
|
1827
|
-
if (!ASTUtils.isFunction(component)) return;
|
|
1828
|
-
const [argument0, argument1] = init.arguments;
|
|
1829
|
-
if (argument0 === void 0 || argument1 === void 0) return;
|
|
1830
|
-
if (skipComputation && ASTUtils.isFunction(argument0) && hasNestedCallOrNew(argument0.body)) {
|
|
1831
|
-
reportIf(context, insideEffectReport);
|
|
1832
|
-
return;
|
|
1833
|
-
}
|
|
1834
|
-
if (!hasEmptyDeps(sourceCode, argument1)) {
|
|
1835
|
-
reportIf(context, insideEffectReport);
|
|
1836
|
-
return;
|
|
1837
|
-
}
|
|
1838
|
-
const factory = resolveFactory(sourceCode, argument0);
|
|
1839
|
-
if (factory === null) return;
|
|
1840
|
-
if (!referencesComponentScope(sourceCode, factory, component)) {
|
|
1841
|
-
context.report({
|
|
1842
|
-
messageId: messageIds.default,
|
|
1843
|
-
node
|
|
1844
|
-
});
|
|
1845
|
-
return;
|
|
1846
|
-
}
|
|
1847
|
-
reportIf(context, insideEffectReport);
|
|
1848
|
-
} };
|
|
1849
|
-
};
|
|
1850
|
-
}
|
|
1851
|
-
/**
|
|
1852
|
-
* Finds the nearest ancestor that is a `useEffect`-like call.
|
|
1853
|
-
*
|
|
1854
|
-
* @param node - The node to search upward from.
|
|
1855
|
-
* @returns The enclosing effect call, or `null` when there is none.
|
|
1856
|
-
*/
|
|
1857
|
-
function findEnclosingEffect(node) {
|
|
1858
|
-
let current = node.parent;
|
|
1859
|
-
while (current !== void 0) {
|
|
1860
|
-
if (core.isUseEffectLikeCall(current)) return current;
|
|
1861
|
-
if (current.type === AST_NODE_TYPES.Program) return null;
|
|
1862
|
-
current = current.parent;
|
|
1863
|
-
}
|
|
1864
|
-
return null;
|
|
1865
|
-
}
|
|
1866
|
-
/**
|
|
1867
|
-
* Reports the "used inside a single useEffect" case when applicable.
|
|
1868
|
-
*
|
|
1869
|
-
* @param sourceCode - Provides declared-variable and text lookups.
|
|
1870
|
-
* @param node - The hook call expression.
|
|
1871
|
-
* @param messageId - The message id to report.
|
|
1872
|
-
* @returns A report descriptor, or `null` when the case does not apply.
|
|
1873
|
-
* @template MessageIds - The rule's message identifiers.
|
|
1874
|
-
*/
|
|
1875
|
-
function checkForUsageInsideUseEffect(sourceCode, node, messageId) {
|
|
1876
|
-
if (!/use\w*Effect/u.test(sourceCode.text)) return null;
|
|
1877
|
-
const { parent } = node;
|
|
1878
|
-
if (parent.type !== AST_NODE_TYPES.VariableDeclarator || parent.id.type !== AST_NODE_TYPES.Identifier) return null;
|
|
1879
|
-
const usages = (sourceCode.getDeclaredVariables(parent).at(0)?.references ?? []).filter((reference) => reference.init !== true);
|
|
1880
|
-
if (usages.length === 0) return null;
|
|
1881
|
-
const effects = /* @__PURE__ */ new Set();
|
|
1882
|
-
for (const usage of usages) {
|
|
1883
|
-
const effect = findEnclosingEffect(usage.identifier);
|
|
1884
|
-
if (effect === null) return null;
|
|
1885
|
-
effects.add(effect);
|
|
1886
|
-
if (effects.size > 1) return null;
|
|
1887
|
-
}
|
|
1888
|
-
return {
|
|
1889
|
-
data: { name: parent.id.name },
|
|
1890
|
-
messageId,
|
|
1891
|
-
node
|
|
1892
|
-
};
|
|
1893
|
-
}
|
|
1894
|
-
/**
|
|
1895
|
-
* Determines whether a dependency argument is an empty array (directly or via a
|
|
1896
|
-
* resolvable identifier).
|
|
1897
|
-
*
|
|
1898
|
-
* @param sourceCode - Provides scope lookup to resolve identifiers.
|
|
1899
|
-
* @param node - The dependency argument node.
|
|
1900
|
-
* @returns `true` if the dependencies are empty.
|
|
1901
|
-
*/
|
|
1902
|
-
function hasEmptyDeps(sourceCode, node) {
|
|
1903
|
-
if (node.type === AST_NODE_TYPES.ArrayExpression) return node.elements.length === 0;
|
|
1904
|
-
if (node.type === AST_NODE_TYPES.Identifier) {
|
|
1905
|
-
const resolved = resolve(sourceCode, node);
|
|
1906
|
-
return resolved?.type === AST_NODE_TYPES.ArrayExpression && resolved.elements.length === 0;
|
|
1907
|
-
}
|
|
1908
|
-
return false;
|
|
1909
|
-
}
|
|
1910
|
-
/**
|
|
1911
|
-
* Collects a scope together with all of its descendant scopes.
|
|
1912
|
-
*
|
|
1913
|
-
* @param scope - The root scope.
|
|
1914
|
-
* @returns The scope and every nested child scope.
|
|
1915
|
-
*/
|
|
1916
|
-
function flattenScopes(scope) {
|
|
1917
|
-
return scope.childScopes.reduce((accumulator, child) => [...accumulator, ...flattenScopes(child)], [scope]);
|
|
1918
|
-
}
|
|
1919
|
-
/**
|
|
1920
|
-
* Determines whether any reference inside the factory resolves to a binding
|
|
1921
|
-
* declared in the component's scope.
|
|
1922
|
-
*
|
|
1923
|
-
* @param sourceCode - Provides scope lookup for the factory.
|
|
1924
|
-
* @param factory - The memoized factory function node.
|
|
1925
|
-
* @param component - The enclosing component function node.
|
|
1926
|
-
* @returns `true` if the factory reads from the component scope.
|
|
1927
|
-
*/
|
|
1928
|
-
function referencesComponentScope(sourceCode, factory, component) {
|
|
1929
|
-
return flattenScopes(sourceCode.getScope(factory)).flatMap((scope) => scope.references).some((reference) => reference.resolved?.scope.block === component);
|
|
1930
|
-
}
|
|
1931
|
-
/**
|
|
1932
|
-
* Reports the descriptor when it is present.
|
|
1933
|
-
*
|
|
1934
|
-
* @param context - The rule context.
|
|
1935
|
-
* @param descriptor - The descriptor to report, or `null` to skip.
|
|
1936
|
-
* @template MessageIds - The rule's message identifiers.
|
|
1937
|
-
*/
|
|
1938
|
-
function reportIf(context, descriptor) {
|
|
1939
|
-
if (descriptor !== null) context.report(descriptor);
|
|
1940
|
-
}
|
|
1941
|
-
/**
|
|
1942
|
-
* Resolves the first argument of the hook call to the underlying factory
|
|
1943
|
-
* function node, unwrapping a curried arrow (`() => () => ...`) and resolving
|
|
1944
|
-
* identifiers to their initializer.
|
|
1945
|
-
*
|
|
1946
|
-
* @param sourceCode - Provides scope lookup to resolve identifiers.
|
|
1947
|
-
* @param node - The first hook argument.
|
|
1948
|
-
* @returns The factory function node, or `null` when it is not a function.
|
|
1949
|
-
*/
|
|
1950
|
-
function resolveFactory(sourceCode, node) {
|
|
1951
|
-
if (node.type === AST_NODE_TYPES.ArrowFunctionExpression) return node.body.type === AST_NODE_TYPES.ArrowFunctionExpression ? node.body : node;
|
|
1952
|
-
if (node.type === AST_NODE_TYPES.FunctionExpression) return node;
|
|
1953
|
-
if (node.type === AST_NODE_TYPES.Identifier) {
|
|
1954
|
-
const resolved = resolve(sourceCode, node);
|
|
1955
|
-
if (resolved?.type === AST_NODE_TYPES.ArrowFunctionExpression || resolved?.type === AST_NODE_TYPES.FunctionExpression) return resolved;
|
|
1956
|
-
}
|
|
1957
|
-
return null;
|
|
1958
|
-
}
|
|
1959
|
-
//#endregion
|
|
1960
|
-
//#region src/rules/no-unnecessary-use-callback/rule.ts
|
|
1961
|
-
const RULE_NAME$6 = "no-unnecessary-use-callback";
|
|
1962
|
-
const MESSAGE_ID_DEFAULT$1 = "default";
|
|
1963
|
-
const MESSAGE_ID_INSIDE_USE_EFFECT$1 = "noUnnecessaryUseCallbackInsideUseEffect";
|
|
1964
|
-
const messages$6 = {
|
|
1965
|
-
[MESSAGE_ID_DEFAULT$1]: "An 'useCallback' with empty deps and no references to the component scope may be unnecessary.",
|
|
1966
|
-
[MESSAGE_ID_INSIDE_USE_EFFECT$1]: "'{{name}}' is only used inside 1 useEffect, which may be unnecessary. You can move the computation into useEffect directly and merge the dependency arrays."
|
|
1967
|
-
};
|
|
1968
|
-
const noUnnecessaryUseCallback = createEslintRule({
|
|
1969
|
-
name: RULE_NAME$6,
|
|
1970
|
-
create: createUnnecessaryHookRule({
|
|
1971
|
-
hook: "useCallback",
|
|
1972
|
-
messageIds: {
|
|
1973
|
-
default: MESSAGE_ID_DEFAULT$1,
|
|
1974
|
-
insideUseEffect: MESSAGE_ID_INSIDE_USE_EFFECT$1
|
|
1975
|
-
}
|
|
1976
|
-
}),
|
|
1977
|
-
defaultOptions: [],
|
|
1978
|
-
meta: {
|
|
1979
|
-
docs: {
|
|
1980
|
-
description: "Disallow unnecessary usage of 'useCallback'",
|
|
1981
|
-
recommended: false,
|
|
1982
|
-
requiresTypeChecking: false
|
|
1983
|
-
},
|
|
1984
|
-
hasSuggestions: false,
|
|
1985
|
-
messages: messages$6,
|
|
1986
|
-
schema: [],
|
|
1987
|
-
type: "suggestion"
|
|
1988
|
-
}
|
|
1989
|
-
});
|
|
1990
|
-
//#endregion
|
|
1991
|
-
//#region src/rules/no-unnecessary-use-memo/rule.ts
|
|
1992
|
-
const RULE_NAME$5 = "no-unnecessary-use-memo";
|
|
1993
|
-
const MESSAGE_ID_DEFAULT = "default";
|
|
1994
|
-
const MESSAGE_ID_INSIDE_USE_EFFECT = "noUnnecessaryUseMemoInsideUseEffect";
|
|
1995
|
-
const messages$5 = {
|
|
1996
|
-
[MESSAGE_ID_DEFAULT]: "An 'useMemo' with empty deps and no references to the component scope may be unnecessary.",
|
|
1997
|
-
[MESSAGE_ID_INSIDE_USE_EFFECT]: "'{{name}}' is only used inside 1 useEffect, which may be unnecessary. You can move the computation into useEffect directly and merge the dependency arrays."
|
|
1998
|
-
};
|
|
1999
|
-
const noUnnecessaryUseMemo = createEslintRule({
|
|
2000
|
-
name: RULE_NAME$5,
|
|
2001
|
-
create: createUnnecessaryHookRule({
|
|
2002
|
-
hook: "useMemo",
|
|
2003
|
-
messageIds: {
|
|
2004
|
-
default: MESSAGE_ID_DEFAULT,
|
|
2005
|
-
insideUseEffect: MESSAGE_ID_INSIDE_USE_EFFECT
|
|
2006
|
-
}
|
|
2007
|
-
}),
|
|
2008
|
-
defaultOptions: [],
|
|
2009
|
-
meta: {
|
|
2010
|
-
docs: {
|
|
2011
|
-
description: "Disallow unnecessary usage of 'useMemo'",
|
|
2012
|
-
recommended: false,
|
|
2013
|
-
requiresTypeChecking: false
|
|
2014
|
-
},
|
|
2015
|
-
hasSuggestions: false,
|
|
2016
|
-
messages: messages$5,
|
|
2017
|
-
schema: [],
|
|
2018
|
-
type: "suggestion"
|
|
2019
|
-
}
|
|
2020
|
-
});
|
|
2021
|
-
//#endregion
|
|
2022
|
-
//#region src/rules/prefer-destructuring-assignment/rule.ts
|
|
2023
|
-
const RULE_NAME$4 = "prefer-destructuring-assignment";
|
|
2024
|
-
const MESSAGE_ID$3 = "default";
|
|
2025
|
-
const messages$4 = { [MESSAGE_ID$3]: "Use destructuring assignment for component props." };
|
|
2026
|
-
/**
|
|
2027
|
-
* Identifiers that cannot be used as a binding name in strict-mode module code.
|
|
2028
|
-
* A property may be accessed with such a name (`props.default`), but a shorthand
|
|
2029
|
-
* destructuring pattern built from it (`{ default }`) would be a syntax error,
|
|
2030
|
-
* so the autofix bails when one is encountered.
|
|
2031
|
-
*/
|
|
2032
|
-
const RESERVED_WORDS = /* @__PURE__ */ new Set([
|
|
2033
|
-
"arguments",
|
|
2034
|
-
"await",
|
|
2035
|
-
"break",
|
|
2036
|
-
"case",
|
|
2037
|
-
"catch",
|
|
2038
|
-
"class",
|
|
2039
|
-
"const",
|
|
2040
|
-
"continue",
|
|
2041
|
-
"debugger",
|
|
2042
|
-
"default",
|
|
2043
|
-
"delete",
|
|
2044
|
-
"do",
|
|
2045
|
-
"else",
|
|
2046
|
-
"enum",
|
|
2047
|
-
"eval",
|
|
2048
|
-
"export",
|
|
2049
|
-
"extends",
|
|
2050
|
-
"false",
|
|
2051
|
-
"finally",
|
|
2052
|
-
"for",
|
|
2053
|
-
"function",
|
|
2054
|
-
"if",
|
|
2055
|
-
"implements",
|
|
2056
|
-
"import",
|
|
2057
|
-
"in",
|
|
2058
|
-
"instanceof",
|
|
2059
|
-
"interface",
|
|
2060
|
-
"let",
|
|
2061
|
-
"new",
|
|
2062
|
-
"null",
|
|
2063
|
-
"package",
|
|
2064
|
-
"private",
|
|
2065
|
-
"protected",
|
|
2066
|
-
"public",
|
|
2067
|
-
"return",
|
|
2068
|
-
"static",
|
|
2069
|
-
"super",
|
|
2070
|
-
"switch",
|
|
2071
|
-
"this",
|
|
2072
|
-
"throw",
|
|
2073
|
-
"true",
|
|
2074
|
-
"try",
|
|
2075
|
-
"typeof",
|
|
2076
|
-
"var",
|
|
2077
|
-
"void",
|
|
2078
|
-
"while",
|
|
2079
|
-
"with",
|
|
2080
|
-
"yield"
|
|
2081
|
-
]);
|
|
2082
|
-
/**
|
|
2083
|
-
* Collects the unique accessed property names, in first-seen order, when every
|
|
2084
|
-
* member reference is a simple non-computed `props.<identifier>` access.
|
|
2085
|
-
*
|
|
2086
|
-
* @param memberReferences - The member accesses to inspect.
|
|
2087
|
-
* @returns The property names, or `null` when any access is not destructurable
|
|
2088
|
-
* (computed access such as `props[key]`, `props` used as a computed key, or a
|
|
2089
|
-
* property whose name is a reserved word that cannot be a binding).
|
|
2090
|
-
*/
|
|
2091
|
-
function collectPropertyNames(memberReferences) {
|
|
2092
|
-
const names = [];
|
|
2093
|
-
for (const { member, reference } of memberReferences) {
|
|
2094
|
-
if (member.object !== reference.identifier || member.computed || member.property.type !== AST_NODE_TYPES.Identifier || RESERVED_WORDS.has(member.property.name)) return null;
|
|
2095
|
-
if (!names.includes(member.property.name)) names.push(member.property.name);
|
|
2096
|
-
}
|
|
2097
|
-
return names;
|
|
2098
|
-
}
|
|
2099
|
-
/**
|
|
2100
|
-
* Determines whether rewriting `props.foo` to `foo` would resolve to a different
|
|
2101
|
-
* binding than the new destructured parameter at any access site.
|
|
2102
|
-
*
|
|
2103
|
-
* A name is unsafe when it is already bound in the component scope (other than by
|
|
2104
|
-
* the props parameter itself) or in any scope nested between an access and the
|
|
2105
|
-
* component, since the rewritten `foo` reference would then resolve to that
|
|
2106
|
-
* binding instead of the destructured prop.
|
|
2107
|
-
*
|
|
2108
|
-
* @param sourceCode - Provides scope lookup for each reference.
|
|
2109
|
-
* @param componentScope - The component function's scope.
|
|
2110
|
-
* @param propsVariable - The props parameter variable (excluded from the check).
|
|
2111
|
-
* @param memberReferences - The member accesses that would be rewritten.
|
|
2112
|
-
* @returns `true` if any rewritten name would collide with an existing binding.
|
|
2113
|
-
*/
|
|
2114
|
-
function wouldShadowExistingBinding(sourceCode, componentScope, propsVariable, memberReferences) {
|
|
2115
|
-
for (const { member, reference } of memberReferences) {
|
|
2116
|
-
const { name } = member.property;
|
|
2117
|
-
let scope = sourceCode.getScope(reference.identifier);
|
|
2118
|
-
while (scope !== null) {
|
|
2119
|
-
if (scope.variables.some((variable) => variable.name === name && variable !== propsVariable)) return true;
|
|
2120
|
-
if (scope === componentScope) break;
|
|
2121
|
-
scope = scope.upper;
|
|
2122
|
-
}
|
|
2123
|
-
}
|
|
2124
|
-
return false;
|
|
2125
|
-
}
|
|
2126
|
-
/**
|
|
2127
|
-
* Reports every `props.<member>` access on a component's props parameter,
|
|
2128
|
-
* attaching an autofix when the parameter can be safely destructured.
|
|
2129
|
-
*
|
|
2130
|
-
* @param context - The rule context.
|
|
2131
|
-
* @param scope - The component function's scope.
|
|
2132
|
-
* @param propsParameter - The props parameter identifier.
|
|
2133
|
-
* @param propertyVariable - The resolved variable for the props parameter.
|
|
2134
|
-
*/
|
|
2135
|
-
function reportComponent(context, scope, propsParameter, propertyVariable) {
|
|
2136
|
-
const memberReferences = [];
|
|
2137
|
-
let hasNonMemberReference = false;
|
|
2138
|
-
for (const reference of propertyVariable.references) {
|
|
2139
|
-
const { parent } = reference.identifier;
|
|
2140
|
-
if (parent.type === AST_NODE_TYPES.MemberExpression) memberReferences.push({
|
|
2141
|
-
member: parent,
|
|
2142
|
-
reference
|
|
2143
|
-
});
|
|
2144
|
-
else hasNonMemberReference = true;
|
|
2145
|
-
}
|
|
2146
|
-
if (memberReferences.length === 0) return;
|
|
2147
|
-
memberReferences.sort((left, right) => left.member.range[0] - right.member.range[0]);
|
|
2148
|
-
const propertyNames = collectPropertyNames(memberReferences);
|
|
2149
|
-
if (!(!hasNonMemberReference && propertyNames !== null && !wouldShadowExistingBinding(context.sourceCode, scope, propertyVariable, memberReferences))) {
|
|
2150
|
-
for (const { member } of memberReferences) context.report({
|
|
2151
|
-
messageId: MESSAGE_ID$3,
|
|
2152
|
-
node: member
|
|
2153
|
-
});
|
|
2154
|
-
return;
|
|
2155
|
-
}
|
|
2156
|
-
const needsParentheses = propsParameter.parent.type === AST_NODE_TYPES.ArrowFunctionExpression && context.sourceCode.getTokenAfter(propsParameter)?.value === "=>";
|
|
2157
|
-
const destructured = `{ ${propertyNames.join(", ")} }`;
|
|
2158
|
-
const pattern = needsParentheses ? `(${destructured})` : destructured;
|
|
2159
|
-
const nameEnd = propsParameter.typeAnnotation?.range[0] ?? propsParameter.range[1];
|
|
2160
|
-
for (const [index, { member }] of memberReferences.entries()) {
|
|
2161
|
-
const propertyName = member.property.name;
|
|
2162
|
-
context.report({
|
|
2163
|
-
fix(fixer) {
|
|
2164
|
-
const replaceAccess = fixer.replaceText(member, propertyName);
|
|
2165
|
-
if (index === 0) return [fixer.replaceTextRange([propsParameter.range[0], nameEnd], pattern), replaceAccess];
|
|
2166
|
-
return replaceAccess;
|
|
2167
|
-
},
|
|
2168
|
-
messageId: MESSAGE_ID$3,
|
|
2169
|
-
node: member
|
|
2170
|
-
});
|
|
2171
|
-
}
|
|
2172
|
-
}
|
|
2173
|
-
/**
|
|
2174
|
-
* Faithfully ports `react-x/prefer-destructuring-assignment`, removed from
|
|
2175
|
-
* `eslint-plugin-react-x` in v5.0.0 (deprecated due to low usage). Component
|
|
2176
|
-
* detection is delegated to `@eslint-react/core` so it matches the upstream
|
|
2177
|
-
* semantics.
|
|
2178
|
-
*
|
|
2179
|
-
* Unlike upstream, this port adds an autofix that rewrites the props parameter
|
|
2180
|
-
* into a destructuring pattern (`(props) => props.id` becomes `({ id }) => id`).
|
|
2181
|
-
* The fix is only offered when it is unambiguously safe; see
|
|
2182
|
-
* {@link collectPropertyNames} and {@link wouldShadowExistingBinding}.
|
|
2183
|
-
*
|
|
2184
|
-
* @param context - The rule context.
|
|
2185
|
-
* @returns The rule listener.
|
|
2186
|
-
*/
|
|
2187
|
-
function create$4(context) {
|
|
2188
|
-
const { api, visitor } = core.getFunctionComponentCollector(context);
|
|
2189
|
-
return {
|
|
2190
|
-
...visitor,
|
|
2191
|
-
"Program:exit": function(program) {
|
|
2192
|
-
for (const component of api.getAllComponents(program)) {
|
|
2193
|
-
if (component.name === null || component.isExportDefaultDeclaration) continue;
|
|
2194
|
-
const [propsParameter] = component.node.params;
|
|
2195
|
-
if (propsParameter?.type !== AST_NODE_TYPES.Identifier) continue;
|
|
2196
|
-
const scope = context.sourceCode.getScope(component.node);
|
|
2197
|
-
const propertyVariable = scope.variables.find((variable) => variable.name === propsParameter.name);
|
|
2198
|
-
if (propertyVariable === void 0) continue;
|
|
2199
|
-
reportComponent(context, scope, propsParameter, propertyVariable);
|
|
2200
|
-
}
|
|
2201
|
-
}
|
|
2202
|
-
};
|
|
2203
|
-
}
|
|
2204
|
-
const preferDestructuringAssignment = createEslintRule({
|
|
2205
|
-
name: RULE_NAME$4,
|
|
2206
|
-
create: create$4,
|
|
2207
|
-
defaultOptions: [],
|
|
2208
|
-
meta: {
|
|
2209
|
-
docs: {
|
|
2210
|
-
description: "Enforce destructuring assignment for component props",
|
|
2211
|
-
recommended: false,
|
|
2212
|
-
requiresTypeChecking: false
|
|
2213
|
-
},
|
|
2214
|
-
fixable: "code",
|
|
2215
|
-
hasSuggestions: false,
|
|
2216
|
-
messages: messages$4,
|
|
2217
|
-
schema: [],
|
|
2218
|
-
type: "problem"
|
|
2219
|
-
}
|
|
2220
|
-
});
|
|
2221
|
-
//#endregion
|
|
2222
|
-
//#region src/rules/prefer-parameter-destructuring/rule.ts
|
|
2223
|
-
const RULE_NAME$3 = "prefer-parameter-destructuring";
|
|
2224
|
-
const MESSAGE_ID$2 = "default";
|
|
2225
|
-
const messages$3 = { [MESSAGE_ID$2]: "Destructure parameter '{{name}}' in the function signature instead of the body." };
|
|
2226
|
-
/**
|
|
2227
|
-
* Collects the binding names introduced by a pattern (or parameter), including
|
|
2228
|
-
* names nested in object/array patterns, defaults, and rest elements.
|
|
2229
|
-
*
|
|
2230
|
-
* @param node - The pattern node to walk.
|
|
2231
|
-
* @param out - Receives every bound name, in source order.
|
|
2232
|
-
*/
|
|
2233
|
-
function collectBoundNames(node, out) {
|
|
2234
|
-
switch (node.type) {
|
|
2235
|
-
case AST_NODE_TYPES.ArrayPattern:
|
|
2236
|
-
for (const element of node.elements) if (element !== null) collectBoundNames(element, out);
|
|
2237
|
-
break;
|
|
2238
|
-
case AST_NODE_TYPES.AssignmentPattern:
|
|
2239
|
-
collectBoundNames(node.left, out);
|
|
2240
|
-
break;
|
|
2241
|
-
case AST_NODE_TYPES.Identifier:
|
|
2242
|
-
out.push(node.name);
|
|
2243
|
-
break;
|
|
2244
|
-
case AST_NODE_TYPES.ObjectPattern:
|
|
2245
|
-
for (const property of node.properties) collectBoundNames(property, out);
|
|
2246
|
-
break;
|
|
2247
|
-
case AST_NODE_TYPES.Property:
|
|
2248
|
-
collectBoundNames(node.value, out);
|
|
2249
|
-
break;
|
|
2250
|
-
case AST_NODE_TYPES.RestElement:
|
|
2251
|
-
collectBoundNames(node.argument, out);
|
|
2252
|
-
break;
|
|
2253
|
-
case AST_NODE_TYPES.TSParameterProperty:
|
|
2254
|
-
collectBoundNames(node.parameter, out);
|
|
2255
|
-
break;
|
|
2256
|
-
default: break;
|
|
2257
|
-
}
|
|
2258
|
-
}
|
|
2259
|
-
/**
|
|
2260
|
-
* Collects every top-level-body object destructuring of the parameter, or
|
|
2261
|
-
* bails when the parameter has any other reference (member access, call
|
|
2262
|
-
* argument, reassignment, a destructure inside a nested block or closure, …) —
|
|
2263
|
-
* those mean the parameter is genuinely used and must stay.
|
|
2264
|
-
*
|
|
2265
|
-
* @param variable - The parameter's resolved scope variable.
|
|
2266
|
-
* @param identifier - The parameter identifier (its default-value write
|
|
2267
|
-
* reference is not a use).
|
|
2268
|
-
* @param body - The function body; only its direct child declarations qualify.
|
|
2269
|
-
* @returns The qualifying destructuring statements in source order, or `null`
|
|
2270
|
-
* when the parameter has a non-destructuring reference.
|
|
2271
|
-
*/
|
|
2272
|
-
function collectDestructureStatements(variable, identifier, body) {
|
|
2273
|
-
const statements = [];
|
|
2274
|
-
for (const reference of variable.references) {
|
|
2275
|
-
const referenceIdentifier = reference.identifier;
|
|
2276
|
-
if (referenceIdentifier === identifier) continue;
|
|
2277
|
-
const { parent } = referenceIdentifier;
|
|
2278
|
-
if (parent.type === AST_NODE_TYPES.VariableDeclarator && parent.init === referenceIdentifier && parent.id.type === AST_NODE_TYPES.ObjectPattern && parent.parent.parent === body) statements.push({
|
|
2279
|
-
declaration: parent.parent,
|
|
2280
|
-
declarator: parent,
|
|
2281
|
-
pattern: parent.id
|
|
2282
|
-
});
|
|
2283
|
-
else return null;
|
|
2284
|
-
}
|
|
2285
|
-
statements.sort((left, right) => left.declarator.range[0] - right.declarator.range[0]);
|
|
2286
|
-
return statements;
|
|
2287
|
-
}
|
|
2288
|
-
/**
|
|
2289
|
-
* Collects the binding names of every parameter other than the target one, so
|
|
2290
|
-
* the fix can avoid creating a duplicate parameter name (a SyntaxError in a
|
|
2291
|
-
* non-simple parameter list).
|
|
2292
|
-
*
|
|
2293
|
-
* @param node - The function whose parameters are inspected.
|
|
2294
|
-
* @param parameter - The parameter being rewritten (excluded).
|
|
2295
|
-
* @returns The other parameters' bound names.
|
|
2296
|
-
*/
|
|
2297
|
-
function collectOtherParameterNames(node, parameter) {
|
|
2298
|
-
const names = [];
|
|
2299
|
-
for (const other of node.params) if (other !== parameter) collectBoundNames(other, names);
|
|
2300
|
-
return new Set(names);
|
|
2301
|
-
}
|
|
2302
|
-
/**
|
|
2303
|
-
* A structural check for AST nodes reached through visitor keys.
|
|
2304
|
-
*
|
|
2305
|
-
* @param value - The child value to test.
|
|
2306
|
-
* @returns `true` when the value is an AST node.
|
|
2307
|
-
*/
|
|
2308
|
-
function isNodeLike(value) {
|
|
2309
|
-
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
2310
|
-
}
|
|
2311
|
-
/**
|
|
2312
|
-
* Checks whether a pattern subtree contains `await` or `yield` (in a computed
|
|
2313
|
-
* key or default value). Parameter initializers may not contain either, so no
|
|
2314
|
-
* signature form exists and the destructuring is not reported.
|
|
2315
|
-
*
|
|
2316
|
-
* @param root - The pattern to walk.
|
|
2317
|
-
* @param visitorKeys - The parser's visitor keys, used to walk without
|
|
2318
|
-
* following `parent` links.
|
|
2319
|
-
* @returns `true` if `await`/`yield` appears anywhere in the pattern.
|
|
2320
|
-
*/
|
|
2321
|
-
function containsAwaitOrYield(root, visitorKeys) {
|
|
2322
|
-
const stack = [root];
|
|
2323
|
-
for (let current = stack.pop(); current !== void 0; current = stack.pop()) {
|
|
2324
|
-
if (current.type === AST_NODE_TYPES.AwaitExpression || current.type === AST_NODE_TYPES.YieldExpression) return true;
|
|
2325
|
-
for (const key of visitorKeys[current.type] ?? []) {
|
|
2326
|
-
const child = current[key];
|
|
2327
|
-
const children = Array.isArray(child) ? child : [child];
|
|
2328
|
-
for (const item of children) if (isNodeLike(item)) stack.push(item);
|
|
2329
|
-
}
|
|
2330
|
-
}
|
|
2331
|
-
return false;
|
|
2332
|
-
}
|
|
2333
|
-
/**
|
|
2334
|
-
* Extracts the identifier a parameter binds, unwrapping a default value
|
|
2335
|
-
* (`obj = {}`); patterns, rest parameters, and TS parameter properties yield
|
|
2336
|
-
* `null`.
|
|
2337
|
-
*
|
|
2338
|
-
* @param parameter - The parameter node.
|
|
2339
|
-
* @returns The parameter's identifier, or `null` when it is not a plain one.
|
|
2340
|
-
*/
|
|
2341
|
-
function getParameterIdentifier(parameter) {
|
|
2342
|
-
if (parameter.type === AST_NODE_TYPES.Identifier) return parameter;
|
|
2343
|
-
if (parameter.type === AST_NODE_TYPES.AssignmentPattern && parameter.left.type === AST_NODE_TYPES.Identifier) return parameter.left;
|
|
2344
|
-
return null;
|
|
2345
|
-
}
|
|
2346
|
-
/**
|
|
2347
|
-
* Determines whether any expression inside the patterns (computed keys,
|
|
2348
|
-
* default values, moved type annotations) references a binding that would be
|
|
2349
|
-
* out of scope — or in its temporal dead zone — at the parameter position:
|
|
2350
|
-
* anything declared in the function body, a parameter at or after the target,
|
|
2351
|
-
* or this function's own `arguments`.
|
|
2352
|
-
*
|
|
2353
|
-
* @param query - The rewrite being planned.
|
|
2354
|
-
* @param patterns - The object patterns being moved.
|
|
2355
|
-
* @returns `true` when moving the patterns would break a reference.
|
|
2356
|
-
*/
|
|
2357
|
-
function hasUnsafePatternReferences({ body, identifier, node, scope }, patterns) {
|
|
2358
|
-
function inPattern(range) {
|
|
2359
|
-
return patterns.some((pattern) => range[0] >= pattern.range[0] && range[1] <= pattern.range[1]);
|
|
2360
|
-
}
|
|
2361
|
-
const stack = [scope];
|
|
2362
|
-
for (let current = stack.pop(); current !== void 0; current = stack.pop()) {
|
|
2363
|
-
stack.push(...current.childScopes);
|
|
2364
|
-
for (const reference of current.references) {
|
|
2365
|
-
if (!inPattern(reference.identifier.range)) continue;
|
|
2366
|
-
const { resolved } = reference;
|
|
2367
|
-
if (resolved === null) continue;
|
|
2368
|
-
if (resolved.defs.length === 0) {
|
|
2369
|
-
const blockRange = resolved.scope.block.range;
|
|
2370
|
-
if (blockRange[0] >= node.range[0] && blockRange[1] <= node.range[1]) return true;
|
|
2371
|
-
continue;
|
|
2372
|
-
}
|
|
2373
|
-
if (resolved.defs.some((definition) => {
|
|
2374
|
-
const { range } = definition.name;
|
|
2375
|
-
if (inPattern(range)) return false;
|
|
2376
|
-
if (range[1] <= node.range[0] || range[0] >= node.range[1]) return false;
|
|
2377
|
-
if (range[0] >= body.range[0]) return true;
|
|
2378
|
-
return range[0] >= identifier.range[0];
|
|
2379
|
-
})) return true;
|
|
2380
|
-
}
|
|
2381
|
-
}
|
|
2382
|
-
return false;
|
|
2383
|
-
}
|
|
2384
|
-
/**
|
|
2385
|
-
* Checks whether a function body opens with a `"use strict"` directive. A
|
|
2386
|
-
* destructured parameter makes the parameter list non-simple, and a function
|
|
2387
|
-
* with a non-simple parameter list may not contain a `"use strict"` directive,
|
|
2388
|
-
* so such functions are skipped entirely.
|
|
2389
|
-
*
|
|
2390
|
-
* @param body - The function body.
|
|
2391
|
-
* @returns `true` if the body has a `"use strict"` directive.
|
|
2392
|
-
*/
|
|
2393
|
-
function hasUseStrictDirective(body) {
|
|
2394
|
-
for (const statement of body.body) {
|
|
2395
|
-
if (statement.type !== AST_NODE_TYPES.ExpressionStatement || typeof statement.directive !== "string") break;
|
|
2396
|
-
if (statement.directive === "use strict") return true;
|
|
2397
|
-
}
|
|
2398
|
-
return false;
|
|
2399
|
-
}
|
|
2400
|
-
/**
|
|
2401
|
-
* Checks whether evaluating the pattern can execute arbitrary code — a default
|
|
2402
|
-
* value or a computed key — as opposed to only performing property reads.
|
|
2403
|
-
*
|
|
2404
|
-
* @param node - The pattern node to walk.
|
|
2405
|
-
* @returns `true` when the pattern contains a default value or computed key.
|
|
2406
|
-
*/
|
|
2407
|
-
function patternExecutesCode(node) {
|
|
2408
|
-
if (node.type === AST_NODE_TYPES.AssignmentPattern) return true;
|
|
2409
|
-
if (node.type === AST_NODE_TYPES.Property) return node.computed || patternExecutesCode(node.value);
|
|
2410
|
-
if (node.type === AST_NODE_TYPES.ObjectPattern) return node.properties.some((property) => patternExecutesCode(property));
|
|
2411
|
-
if (node.type === AST_NODE_TYPES.ArrayPattern) return node.elements.some((element) => element !== null && patternExecutesCode(element));
|
|
2412
|
-
if (node.type === AST_NODE_TYPES.RestElement) return patternExecutesCode(node.argument);
|
|
2413
|
-
return false;
|
|
2414
|
-
}
|
|
2415
|
-
/**
|
|
2416
|
-
* Computes the removal range for a declaration, swallowing the whole line
|
|
2417
|
-
* (indentation and trailing newline) when the declaration is alone on it.
|
|
2418
|
-
*
|
|
2419
|
-
* @param sourceCode - Provides the raw text around the declaration.
|
|
2420
|
-
* @param statement - The declaration to remove.
|
|
2421
|
-
* @returns The range to delete.
|
|
2422
|
-
*/
|
|
2423
|
-
function statementRemovalRange({ text }, statement) {
|
|
2424
|
-
const [start, end] = statement.range;
|
|
2425
|
-
const lineStart = text.lastIndexOf("\n", start - 1) + 1;
|
|
2426
|
-
const newlineIndex = text.indexOf("\n", end);
|
|
2427
|
-
const lineEnd = newlineIndex === -1 ? text.length : newlineIndex + 1;
|
|
2428
|
-
const leadingIsBlank = text.slice(lineStart, start).trim().length === 0;
|
|
2429
|
-
const trailingIsBlank = text.slice(end, newlineIndex === -1 ? text.length : newlineIndex).trim().length === 0;
|
|
2430
|
-
if (leadingIsBlank && trailingIsBlank) return [lineStart, lineEnd];
|
|
2431
|
-
return [start, end];
|
|
2432
|
-
}
|
|
2433
|
-
/**
|
|
2434
|
-
* Builds the autofix, or returns `null` when the rewrite is not unambiguously
|
|
2435
|
-
* safe: unrelated sibling declarators, unmergeable or annotated patterns,
|
|
2436
|
-
* duplicate or colliding binding names, expressions that reference bindings
|
|
2437
|
-
* unavailable at the parameter position, a defused temporal dead zone, or —
|
|
2438
|
-
* with `allowSideEffectReordering: false` — side effects that would be
|
|
2439
|
-
* reordered.
|
|
2440
|
-
*
|
|
2441
|
-
* @param query - The rewrite being planned.
|
|
2442
|
-
* @returns The fix plan, or `null` when only a report should be emitted.
|
|
2443
|
-
*/
|
|
2444
|
-
function planFix(query) {
|
|
2445
|
-
const { allowSideEffectReordering, body, identifier, node, otherParameterNames, sourceCode, statements } = query;
|
|
2446
|
-
const declaratorSet = new Set(statements.map((statement) => statement.declarator));
|
|
2447
|
-
const declarations = [...new Set(statements.map((statement) => statement.declaration))];
|
|
2448
|
-
if (!declarations.every((declaration) => {
|
|
2449
|
-
return declaration.declarations.every((declarator) => declaratorSet.has(declarator));
|
|
2450
|
-
})) return null;
|
|
2451
|
-
const patterns = statements.map((statement) => statement.pattern);
|
|
2452
|
-
if (!allowSideEffectReordering && patterns.some((pattern) => patternExecutesCode(pattern))) {
|
|
2453
|
-
const leadingStatements = new Set(body.body.slice(0, declarations.length));
|
|
2454
|
-
if (!declarations.every((declaration) => leadingStatements.has(declaration))) return null;
|
|
2455
|
-
}
|
|
2456
|
-
if (declarations.some((declaration) => {
|
|
2457
|
-
return sourceCode.getDeclaredVariables(declaration).some((declared) => {
|
|
2458
|
-
return declared.references.some((reference) => reference.identifier.range[0] < declaration.range[0]);
|
|
2459
|
-
});
|
|
2460
|
-
})) return null;
|
|
2461
|
-
if (patterns.some((pattern) => pattern.typeAnnotation !== void 0) && (patterns.length > 1 || identifier.typeAnnotation !== void 0)) return null;
|
|
2462
|
-
if (patterns.length > 1) {
|
|
2463
|
-
if (patterns.some((pattern) => {
|
|
2464
|
-
return pattern.properties.some((property) => property.type === AST_NODE_TYPES.RestElement);
|
|
2465
|
-
})) return null;
|
|
2466
|
-
}
|
|
2467
|
-
const boundNames = [];
|
|
2468
|
-
for (const pattern of patterns) collectBoundNames(pattern, boundNames);
|
|
2469
|
-
if (new Set(boundNames).size !== boundNames.length) return null;
|
|
2470
|
-
if (boundNames.some((name) => otherParameterNames.has(name))) return null;
|
|
2471
|
-
if (hasUnsafePatternReferences(query, patterns)) return null;
|
|
2472
|
-
const [firstPattern] = patterns;
|
|
2473
|
-
let parameterText;
|
|
2474
|
-
if (patterns.length === 1 && firstPattern !== void 0) parameterText = sourceCode.getText(firstPattern);
|
|
2475
|
-
else {
|
|
2476
|
-
const innerTexts = patterns.map((pattern) => sourceCode.getText(pattern).slice(1, -1).trim().replace(/,$/u, "").trim()).filter((text) => text.length > 0);
|
|
2477
|
-
parameterText = innerTexts.length === 0 ? "{}" : `{ ${innerTexts.join(", ")} }`;
|
|
2478
|
-
}
|
|
2479
|
-
if (node.type === AST_NODE_TYPES.ArrowFunctionExpression && sourceCode.getTokenAfter(identifier)?.value === "=>") parameterText = `(${parameterText})`;
|
|
2480
|
-
const parameterRange = [identifier.range[0], identifier.typeAnnotation?.range[0] ?? identifier.range[1]];
|
|
2481
|
-
const removalRanges = declarations.map((declaration) => {
|
|
2482
|
-
return statementRemovalRange(sourceCode, declaration);
|
|
2483
|
-
});
|
|
2484
|
-
return {
|
|
2485
|
-
parameterRange,
|
|
2486
|
-
parameterText,
|
|
2487
|
-
removalRanges
|
|
2488
|
-
};
|
|
2489
|
-
}
|
|
2490
|
-
/**
|
|
2491
|
-
* Reports body destructuring statements of parameters that have no other use,
|
|
2492
|
-
* preferring the pattern in the function signature. The autofix rewrites the
|
|
2493
|
-
* parameter (merging multiple statements into one pattern) and removes the
|
|
2494
|
-
* statements; it is withheld when the rewrite is not unambiguously safe, see
|
|
2495
|
-
* {@link planFix}.
|
|
2496
|
-
*
|
|
2497
|
-
* @param context - The rule context.
|
|
2498
|
-
* @param optionsWithDefault - The resolved rule options.
|
|
2499
|
-
* @returns The rule listener.
|
|
2500
|
-
*/
|
|
2501
|
-
function create$3(context, optionsWithDefault) {
|
|
2502
|
-
const [{ allowSideEffectReordering = true }] = optionsWithDefault;
|
|
2503
|
-
function checkFunction(node) {
|
|
2504
|
-
const { body, params } = node;
|
|
2505
|
-
if (body.type !== AST_NODE_TYPES.BlockStatement || hasUseStrictDirective(body)) return;
|
|
2506
|
-
const scope = context.sourceCode.getScope(node);
|
|
2507
|
-
for (const parameter of params) {
|
|
2508
|
-
const identifier = getParameterIdentifier(parameter);
|
|
2509
|
-
if (identifier === null || identifier.name === "this") continue;
|
|
2510
|
-
const variable = scope.variables.find((candidate) => {
|
|
2511
|
-
return candidate.defs.some((definition) => definition.name === identifier);
|
|
2512
|
-
});
|
|
2513
|
-
if (variable?.defs.length !== 1) continue;
|
|
2514
|
-
const statements = collectDestructureStatements(variable, identifier, body);
|
|
2515
|
-
if (statements === null || statements.length === 0) continue;
|
|
2516
|
-
if (statements.some(({ pattern }) => {
|
|
2517
|
-
return containsAwaitOrYield(pattern, context.sourceCode.visitorKeys);
|
|
2518
|
-
})) continue;
|
|
2519
|
-
const otherParameterNames = collectOtherParameterNames(node, parameter);
|
|
2520
|
-
if (otherParameterNames.has(identifier.name)) continue;
|
|
2521
|
-
const plan = planFix({
|
|
2522
|
-
allowSideEffectReordering,
|
|
2523
|
-
body,
|
|
2524
|
-
identifier,
|
|
2525
|
-
node,
|
|
2526
|
-
otherParameterNames,
|
|
2527
|
-
scope,
|
|
2528
|
-
sourceCode: context.sourceCode,
|
|
2529
|
-
statements
|
|
2530
|
-
});
|
|
2531
|
-
for (const [index, statement] of statements.entries()) context.report({
|
|
2532
|
-
data: { name: identifier.name },
|
|
2533
|
-
fix(fixer) {
|
|
2534
|
-
if (index !== 0 || plan === null) return null;
|
|
2535
|
-
return [fixer.replaceTextRange(plan.parameterRange, plan.parameterText), ...plan.removalRanges.map((range) => fixer.removeRange(range))];
|
|
2536
|
-
},
|
|
2537
|
-
messageId: MESSAGE_ID$2,
|
|
2538
|
-
node: statement.declarator
|
|
2539
|
-
});
|
|
2540
|
-
}
|
|
2541
|
-
}
|
|
2542
|
-
return {
|
|
2543
|
-
ArrowFunctionExpression: checkFunction,
|
|
2544
|
-
FunctionDeclaration: checkFunction,
|
|
2545
|
-
FunctionExpression: checkFunction
|
|
2546
|
-
};
|
|
2547
|
-
}
|
|
2548
|
-
const preferParameterDestructuring = createEslintRule({
|
|
2549
|
-
name: RULE_NAME$3,
|
|
2550
|
-
create: create$3,
|
|
2551
|
-
defaultOptions: [{ allowSideEffectReordering: true }],
|
|
2552
|
-
meta: {
|
|
2553
|
-
defaultOptions: [{ allowSideEffectReordering: true }],
|
|
2554
|
-
docs: {
|
|
2555
|
-
description: "Enforce destructuring parameters in the function signature",
|
|
2556
|
-
recommended: false,
|
|
2557
|
-
requiresTypeChecking: false
|
|
2558
|
-
},
|
|
2559
|
-
fixable: "code",
|
|
2560
|
-
hasSuggestions: false,
|
|
2561
|
-
messages: messages$3,
|
|
2562
|
-
schema: [{
|
|
2563
|
-
additionalProperties: false,
|
|
2564
|
-
properties: { allowSideEffectReordering: {
|
|
2565
|
-
description: "Whether the autofix may hoist pattern defaults and computed keys past earlier statements, reordering their side effects. Set to false to withhold the fix in those cases.",
|
|
2566
|
-
type: "boolean"
|
|
2567
|
-
} },
|
|
2568
|
-
type: "object"
|
|
2569
|
-
}],
|
|
2570
|
-
type: "suggestion"
|
|
2571
|
-
}
|
|
2572
|
-
});
|
|
2573
|
-
//#endregion
|
|
2574
|
-
//#region src/rules/purity/rule.ts
|
|
2575
|
-
const RULE_NAME$2 = "purity";
|
|
2576
|
-
const MESSAGE_ID$1 = "impureCall";
|
|
2577
|
-
/**
|
|
2578
|
-
* Non-deterministic Luau / Roblox calls, written as dotted paths matching how
|
|
2579
|
-
* they appear in roblox-ts source. `new Random()` is normalized to
|
|
2580
|
-
* `"Random.new"` (see the `NewExpression` visitor).
|
|
2581
|
-
*/
|
|
2582
|
-
const DEFAULT_SIGNATURES = [
|
|
2583
|
-
"DateTime.now",
|
|
2584
|
-
"HttpService.GenerateGUID",
|
|
2585
|
-
"Random.new",
|
|
2586
|
-
"Workspace.GetServerTimeNow",
|
|
2587
|
-
"elapsedTime",
|
|
2588
|
-
"math.random",
|
|
2589
|
-
"math.randomseed",
|
|
2590
|
-
"os.clock",
|
|
2591
|
-
"os.date",
|
|
2592
|
-
"os.time",
|
|
2593
|
-
"tick",
|
|
2594
|
-
"time"
|
|
2595
|
-
];
|
|
2596
|
-
/**
|
|
2597
|
-
* Bare Luau globals for which a matching local binding means the reference is
|
|
2598
|
-
* shadowed rather than the ambient global. Service objects are intentionally
|
|
2599
|
-
* excluded because they are impure even when imported from `@rbxts/services`.
|
|
2600
|
-
*/
|
|
2601
|
-
const GUARDED_GLOBALS = /* @__PURE__ */ new Set([
|
|
2602
|
-
"elapsedTime",
|
|
2603
|
-
"math",
|
|
2604
|
-
"os",
|
|
2605
|
-
"tick",
|
|
2606
|
-
"time"
|
|
2607
|
-
]);
|
|
2608
|
-
const messages$2 = { [MESSAGE_ID$1]: "Do not call '{{name}}' during render. Components and hooks must be pure. Move this call into an event handler, effect, or state initializer." };
|
|
2609
|
-
const schema$1 = [{
|
|
2610
|
-
additionalProperties: false,
|
|
2611
|
-
properties: {
|
|
2612
|
-
additionalFunctions: {
|
|
2613
|
-
description: "Extra dotted call signatures to treat as impure (e.g. \"Math.random\").",
|
|
2614
|
-
items: { type: "string" },
|
|
2615
|
-
type: "array",
|
|
2616
|
-
uniqueItems: true
|
|
2617
|
-
},
|
|
2618
|
-
ignore: {
|
|
2619
|
-
description: "Default signatures to exclude (e.g. \"os.date\").",
|
|
2620
|
-
items: { type: "string" },
|
|
2621
|
-
type: "array",
|
|
2622
|
-
uniqueItems: true
|
|
2623
|
-
}
|
|
2624
|
-
},
|
|
2625
|
-
type: "object"
|
|
2626
|
-
}];
|
|
2627
|
-
/**
|
|
2628
|
-
* Strips wrappers that are irrelevant to the callee identity so that
|
|
2629
|
-
* `a.b?.()` and `a.b!()` still match.
|
|
2630
|
-
*
|
|
2631
|
-
* @param node - The node to unwrap.
|
|
2632
|
-
* @returns The unwrapped node.
|
|
2633
|
-
*/
|
|
2634
|
-
function unwrap(node) {
|
|
2635
|
-
let current = node;
|
|
2636
|
-
while (current.type === AST_NODE_TYPES.ChainExpression || current.type === AST_NODE_TYPES.TSNonNullExpression) current = current.expression;
|
|
2637
|
-
return current;
|
|
2638
|
-
}
|
|
2639
|
-
/**
|
|
2640
|
-
* Builds the dotted path for a callee, e.g. `math.random` or `tick`.
|
|
2641
|
-
*
|
|
2642
|
-
* @param callee - The callee expression.
|
|
2643
|
-
* @returns The match, or `null` when the callee cannot be represented as a
|
|
2644
|
-
* simple dotted path (computed access, or an object that is an expression).
|
|
2645
|
-
*/
|
|
2646
|
-
function dottedCalleePath(callee) {
|
|
2647
|
-
let current = unwrap(callee);
|
|
2648
|
-
const parts = [];
|
|
2649
|
-
while (current.type === AST_NODE_TYPES.MemberExpression) {
|
|
2650
|
-
if (current.computed || current.property.type !== AST_NODE_TYPES.Identifier) return null;
|
|
2651
|
-
parts.unshift(current.property.name);
|
|
2652
|
-
current = unwrap(current.object);
|
|
2653
|
-
}
|
|
2654
|
-
if (current.type !== AST_NODE_TYPES.Identifier) return null;
|
|
2655
|
-
parts.unshift(current.name);
|
|
2656
|
-
return {
|
|
2657
|
-
path: parts.join("."),
|
|
2658
|
-
root: current
|
|
2659
|
-
};
|
|
2660
|
-
}
|
|
2661
|
-
/**
|
|
2662
|
-
* Determines whether an identifier resolves to a real user binding (import,
|
|
2663
|
-
* parameter, or local declaration) rather than the ambient Luau global.
|
|
2664
|
-
*
|
|
2665
|
-
* @param sourceCode - Provides scope lookup.
|
|
2666
|
-
* @param node - The identifier node.
|
|
2667
|
-
* @returns `true` when the identifier is a user binding rather than the ambient
|
|
2668
|
-
* global.
|
|
2669
|
-
*/
|
|
2670
|
-
function isUserBinding(sourceCode, node) {
|
|
2671
|
-
let scope = sourceCode.getScope(node);
|
|
2672
|
-
while (scope !== null) {
|
|
2673
|
-
const variable = scope.variables.find((candidate) => candidate.name === node.name);
|
|
2674
|
-
if (variable !== void 0) return variable.defs.length > 0;
|
|
2675
|
-
scope = scope.upper;
|
|
2676
|
-
}
|
|
2677
|
-
return false;
|
|
2678
|
-
}
|
|
2679
|
-
/**
|
|
2680
|
-
* Finds the immediate enclosing function of a node, stopping at the program.
|
|
2681
|
-
*
|
|
2682
|
-
* @param node - The node to search upward from.
|
|
2683
|
-
* @returns The enclosing function, or `null` when the node is at module level.
|
|
2684
|
-
*/
|
|
2685
|
-
function enclosingFunction(node) {
|
|
2686
|
-
let current = node.parent;
|
|
2687
|
-
while (current !== void 0) {
|
|
2688
|
-
if (ASTUtils.isFunction(current)) return current;
|
|
2689
|
-
if (current.type === AST_NODE_TYPES.Program) return null;
|
|
2690
|
-
current = current.parent;
|
|
2691
|
-
}
|
|
2692
|
-
return null;
|
|
2693
|
-
}
|
|
2694
|
-
/**
|
|
2695
|
-
* Determines whether a function executes during render: a component body, a
|
|
2696
|
-
* custom/builtin hook body, or a `useMemo` callback.
|
|
2697
|
-
*
|
|
2698
|
-
* @param reactContext - The context `@eslint-react/core` predicates expect.
|
|
2699
|
-
* @param func - The enclosing function node.
|
|
2700
|
-
* @returns `true` when the function runs during render.
|
|
2701
|
-
*/
|
|
2702
|
-
function isRenderContext(reactContext, func) {
|
|
2703
|
-
if (core.isFunctionComponentDefinition(reactContext, func, core.DEFAULT_COMPONENT_DETECTION_HINT)) return true;
|
|
2704
|
-
if (core.isHookDefinition(func)) return true;
|
|
2705
|
-
const { parent } = func;
|
|
2706
|
-
return parent.type === AST_NODE_TYPES.CallExpression && parent.arguments[0] === func && core.isUseMemoCall(reactContext, parent);
|
|
2707
|
-
}
|
|
2708
|
-
function create$2(context) {
|
|
2709
|
-
const { sourceCode } = context;
|
|
2710
|
-
const reactContext = context;
|
|
2711
|
-
const options = context.options[0] ?? {};
|
|
2712
|
-
const signatures = new Set(DEFAULT_SIGNATURES);
|
|
2713
|
-
for (const signature of options.additionalFunctions ?? []) signatures.add(signature);
|
|
2714
|
-
for (const signature of options.ignore ?? []) signatures.delete(signature);
|
|
2715
|
-
function handle(node, match, path) {
|
|
2716
|
-
if (!signatures.has(path)) return;
|
|
2717
|
-
if (path === "Random.new" && node.arguments.length > 0) return;
|
|
2718
|
-
if (GUARDED_GLOBALS.has(match.root.name) && isUserBinding(sourceCode, match.root)) return;
|
|
2719
|
-
const func = enclosingFunction(node);
|
|
2720
|
-
if (func === null || !isRenderContext(reactContext, func)) return;
|
|
2721
|
-
context.report({
|
|
2722
|
-
data: { name: path },
|
|
2723
|
-
messageId: MESSAGE_ID$1,
|
|
2724
|
-
node
|
|
2725
|
-
});
|
|
2726
|
-
}
|
|
2727
|
-
return {
|
|
2728
|
-
CallExpression(node) {
|
|
2729
|
-
const match = dottedCalleePath(node.callee);
|
|
2730
|
-
if (match !== null) handle(node, match, match.path);
|
|
2731
|
-
},
|
|
2732
|
-
NewExpression(node) {
|
|
2733
|
-
const match = dottedCalleePath(node.callee);
|
|
2734
|
-
if (match !== null) handle(node, match, `${match.path}.new`);
|
|
2735
|
-
}
|
|
2736
|
-
};
|
|
2737
|
-
}
|
|
2738
|
-
const purity = createEslintRule({
|
|
2739
|
-
name: RULE_NAME$2,
|
|
2740
|
-
create: create$2,
|
|
2741
|
-
defaultOptions: [{}],
|
|
2742
|
-
meta: {
|
|
2743
|
-
defaultOptions: [{}],
|
|
2744
|
-
docs: {
|
|
2745
|
-
description: "Disallow impure calls such as `math.random` or `os.clock` during render",
|
|
2746
|
-
recommended: false,
|
|
2747
|
-
requiresTypeChecking: false
|
|
2748
|
-
},
|
|
2749
|
-
hasSuggestions: false,
|
|
2750
|
-
messages: messages$2,
|
|
2751
|
-
schema: schema$1,
|
|
2752
|
-
type: "problem"
|
|
2753
|
-
}
|
|
2754
|
-
});
|
|
2755
|
-
//#endregion
|
|
2756
|
-
//#region src/rules/toml-sort-keys/rule.ts
|
|
2757
|
-
const RULE_NAME$1 = "toml-sort-keys";
|
|
2758
|
-
const MESSAGE_ID = "unsorted";
|
|
2759
|
-
const messages$1 = { [MESSAGE_ID]: "Expected {{target}} to follow the configured order." };
|
|
2760
|
-
const schema = {
|
|
2761
|
-
items: {
|
|
2762
|
-
additionalProperties: false,
|
|
2763
|
-
properties: {
|
|
2764
|
-
order: { oneOf: [{
|
|
2765
|
-
items: { type: "string" },
|
|
2766
|
-
type: "array"
|
|
2767
|
-
}, {
|
|
2768
|
-
additionalProperties: false,
|
|
2769
|
-
properties: {
|
|
2770
|
-
caseSensitive: { type: "boolean" },
|
|
2771
|
-
natural: { type: "boolean" },
|
|
2772
|
-
type: {
|
|
2773
|
-
enum: ["asc", "desc"],
|
|
2774
|
-
type: "string"
|
|
2775
|
-
}
|
|
2776
|
-
},
|
|
2777
|
-
type: "object"
|
|
2778
|
-
}] },
|
|
2779
|
-
pathPattern: { type: "string" }
|
|
2780
|
-
},
|
|
2781
|
-
required: ["order", "pathPattern"],
|
|
2782
|
-
type: "object"
|
|
2783
|
-
},
|
|
2784
|
-
type: "array"
|
|
2785
|
-
};
|
|
2786
|
-
function isTable(entry) {
|
|
2787
|
-
return entry.type === "TOMLTable";
|
|
2788
|
-
}
|
|
2789
|
-
/**
|
|
2790
|
-
* The dotted name of an entry, e.g. `settings`, `settings.node`, `_.path`.
|
|
2791
|
-
*
|
|
2792
|
-
* @param entry - The table header or key-value entry.
|
|
2793
|
-
* @returns The dotted key path as a string.
|
|
2794
|
-
*/
|
|
2795
|
-
function nameOf(entry) {
|
|
2796
|
-
return getStaticTOMLValue(entry.key).join(".");
|
|
2797
|
-
}
|
|
2798
|
-
/**
|
|
2799
|
-
* Builds a rank function from an explicit order list. An entry matches an order
|
|
2800
|
-
* item when it equals it or is a dotted child of it (`settings.node` matches
|
|
2801
|
-
* `settings`), which keeps sub-tables grouped under their parent's slot.
|
|
2802
|
-
*
|
|
2803
|
-
* @param order - The configured names in their desired order.
|
|
2804
|
-
* @returns A function giving a name's rank, or `Infinity` when unlisted.
|
|
2805
|
-
*/
|
|
2806
|
-
function makeRank(order) {
|
|
2807
|
-
return (name) => {
|
|
2808
|
-
for (const [index, item] of order.entries()) if (name === item || name.startsWith(`${item}.`)) return index;
|
|
2809
|
-
return Number.POSITIVE_INFINITY;
|
|
2810
|
-
};
|
|
2811
|
-
}
|
|
2812
|
-
function makeFallback({ caseSensitive = true, natural = false, type = "asc" }) {
|
|
2813
|
-
const sensitivity = caseSensitive ? "variant" : "accent";
|
|
2814
|
-
return (a, b) => {
|
|
2815
|
-
const result = a.localeCompare(b, "en", {
|
|
2816
|
-
numeric: natural,
|
|
2817
|
-
sensitivity
|
|
2818
|
-
});
|
|
2819
|
-
return type === "desc" ? -result : result;
|
|
2820
|
-
};
|
|
2821
|
-
}
|
|
2822
|
-
/**
|
|
2823
|
-
* Builds a comparator for one table body. Explicit-order entries sort first by
|
|
2824
|
-
* their listed position, then unlisted entries fall back to a natural/asc sort
|
|
2825
|
-
* (mirroring `yaml/sort-keys`). The fallback compares keys as written in
|
|
2826
|
-
* source, so quotes participate in the order and quoted keys (e.g.
|
|
2827
|
-
* `"github:owner/repo"`) group before bare keys. At the top level, bare
|
|
2828
|
-
* key-values are always kept before any `[table]` header, since TOML would
|
|
2829
|
-
* otherwise re-scope them.
|
|
2830
|
-
*
|
|
2831
|
-
* @param order - The order configuration for this table's path.
|
|
2832
|
-
* @param isTopLevel - Whether the body is the top-level table.
|
|
2833
|
-
* @param rawName - Returns an entry's key exactly as written in source.
|
|
2834
|
-
* @returns A comparator over two entries.
|
|
2835
|
-
*/
|
|
2836
|
-
function makeComparator(order, isTopLevel, rawName) {
|
|
2837
|
-
const rank = Array.isArray(order) ? makeRank(order) : void 0;
|
|
2838
|
-
const fallback = makeFallback(Array.isArray(order) ? {
|
|
2839
|
-
natural: true,
|
|
2840
|
-
type: "asc"
|
|
2841
|
-
} : order);
|
|
2842
|
-
return (a, b) => {
|
|
2843
|
-
if (isTopLevel) {
|
|
2844
|
-
const aRank = isTable(a) ? 1 : 0;
|
|
2845
|
-
const bRank = isTable(b) ? 1 : 0;
|
|
2846
|
-
if (aRank !== bRank) return aRank - bRank;
|
|
2847
|
-
}
|
|
2848
|
-
if (rank !== void 0) {
|
|
2849
|
-
const aRank = rank(nameOf(a));
|
|
2850
|
-
const bRank = rank(nameOf(b));
|
|
2851
|
-
if (aRank !== bRank) {
|
|
2852
|
-
if (aRank === Number.POSITIVE_INFINITY) return 1;
|
|
2853
|
-
if (bRank === Number.POSITIVE_INFINITY) return -1;
|
|
2854
|
-
return aRank - bRank;
|
|
2855
|
-
}
|
|
2856
|
-
}
|
|
2857
|
-
return fallback(rawName(a), rawName(b));
|
|
2858
|
-
};
|
|
2859
|
-
}
|
|
2860
|
-
function create$1(context) {
|
|
2861
|
-
const { options, sourceCode } = context;
|
|
2862
|
-
if (sourceCode.parserServices.isTOML !== true) return {};
|
|
2863
|
-
const specs = options.map(({ order, pathPattern }) => {
|
|
2864
|
-
return {
|
|
2865
|
-
order,
|
|
2866
|
-
pattern: new RegExp(pathPattern, "u")
|
|
2867
|
-
};
|
|
2868
|
-
});
|
|
2869
|
-
if (specs.length === 0) return {};
|
|
2870
|
-
function resolveOrder(path) {
|
|
2871
|
-
for (const spec of specs) if (spec.pattern.test(path)) return spec.order;
|
|
2872
|
-
}
|
|
2873
|
-
/**
|
|
2874
|
-
* A comment counts as attached to an entry when it sits on its own line
|
|
2875
|
-
* directly above it (no blank line, no trailing code). Such comments travel
|
|
2876
|
-
* with the entry when it moves.
|
|
2877
|
-
*
|
|
2878
|
-
* @param comment - The comment to classify.
|
|
2879
|
-
* @returns Whether the comment stands alone on its line.
|
|
2880
|
-
*/
|
|
2881
|
-
function isOwnLineComment(comment) {
|
|
2882
|
-
const before = sourceCode.getTokenBefore(comment, { includeComments: true });
|
|
2883
|
-
return before === null || before.loc.end.line < comment.loc.start.line;
|
|
2884
|
-
}
|
|
2885
|
-
function leadingStart(entry) {
|
|
2886
|
-
const comments = sourceCode.getCommentsBefore(entry);
|
|
2887
|
-
let start = entry.range[0];
|
|
2888
|
-
let boundaryLine = entry.loc.start.line;
|
|
2889
|
-
for (let index = comments.length - 1; index >= 0; index -= 1) {
|
|
2890
|
-
const comment = comments[index];
|
|
2891
|
-
if (comment === void 0) break;
|
|
2892
|
-
if (comment.loc.end.line !== boundaryLine - 1 || !isOwnLineComment(comment)) break;
|
|
2893
|
-
start = comment.range[0];
|
|
2894
|
-
boundaryLine = comment.loc.start.line;
|
|
2895
|
-
}
|
|
2896
|
-
return start;
|
|
2897
|
-
}
|
|
2898
|
-
function trailingEnd(entry) {
|
|
2899
|
-
const [comment] = sourceCode.getCommentsAfter(entry);
|
|
2900
|
-
if (comment?.loc.start.line === entry.loc.end.line) return comment.range[1];
|
|
2901
|
-
return entry.range[1];
|
|
2902
|
-
}
|
|
2903
|
-
function buildFix(entries, target) {
|
|
2904
|
-
const text = sourceCode.getText();
|
|
2905
|
-
const blocks = entries.map((entry) => {
|
|
2906
|
-
return {
|
|
2907
|
-
end: trailingEnd(entry),
|
|
2908
|
-
entry,
|
|
2909
|
-
start: leadingStart(entry)
|
|
2910
|
-
};
|
|
2911
|
-
});
|
|
2912
|
-
for (let index = 1; index < blocks.length; index += 1) {
|
|
2913
|
-
const previous = blocks[index - 1];
|
|
2914
|
-
const current = blocks[index];
|
|
2915
|
-
if (previous === void 0 || current === void 0) return;
|
|
2916
|
-
if (/\S/u.test(text.slice(previous.end, current.start))) return;
|
|
2917
|
-
}
|
|
2918
|
-
const first = blocks[0];
|
|
2919
|
-
const last = blocks[blocks.length - 1];
|
|
2920
|
-
if (first === void 0 || last === void 0) return;
|
|
2921
|
-
const textByEntry = new Map(blocks.map((block) => [block.entry, text.slice(block.start, block.end)]));
|
|
2922
|
-
const parts = [];
|
|
2923
|
-
for (const [index, entry] of target.entries()) {
|
|
2924
|
-
const previous = target[index - 1];
|
|
2925
|
-
if (previous !== void 0) parts.push(isTable(entry) || isTable(previous) ? "\n\n" : "\n");
|
|
2926
|
-
parts.push(textByEntry.get(entry) ?? "");
|
|
2927
|
-
}
|
|
2928
|
-
const sortedText = parts.join("");
|
|
2929
|
-
return (fixer) => fixer.replaceTextRange([first.start, last.end], sortedText);
|
|
2930
|
-
}
|
|
2931
|
-
function verify(path, body, isTopLevel) {
|
|
2932
|
-
if (body.length < 2) return;
|
|
2933
|
-
const order = resolveOrder(path);
|
|
2934
|
-
if (order === void 0) return;
|
|
2935
|
-
const entries = [...body];
|
|
2936
|
-
const target = [...entries].sort(makeComparator(order, isTopLevel, (entry) => sourceCode.getText(entry.key)));
|
|
2937
|
-
let outOfPlace;
|
|
2938
|
-
for (const [index, entry] of entries.entries()) if (entry !== target[index]) {
|
|
2939
|
-
outOfPlace = entry;
|
|
2940
|
-
break;
|
|
2941
|
-
}
|
|
2942
|
-
if (outOfPlace === void 0) return;
|
|
2943
|
-
context.report({
|
|
2944
|
-
data: { target: path === "" ? "top-level tables" : `keys in "${path}"` },
|
|
2945
|
-
fix: buildFix(entries, target),
|
|
2946
|
-
loc: outOfPlace.key.loc,
|
|
2947
|
-
messageId: MESSAGE_ID
|
|
2948
|
-
});
|
|
2949
|
-
}
|
|
2950
|
-
return {
|
|
2951
|
-
TOMLTable(node) {
|
|
2952
|
-
verify(getStaticTOMLValue(node.key).join("."), node.body, false);
|
|
2953
|
-
},
|
|
2954
|
-
TOMLTopLevelTable(node) {
|
|
2955
|
-
verify("", node.body, true);
|
|
2956
|
-
}
|
|
2957
|
-
};
|
|
2958
|
-
}
|
|
2959
|
-
const tomlSortKeys = createEslintRule({
|
|
2960
|
-
name: RULE_NAME$1,
|
|
2961
|
-
create: create$1,
|
|
2962
|
-
defaultOptions: [],
|
|
2963
|
-
meta: {
|
|
2964
|
-
defaultOptions: [],
|
|
2965
|
-
docs: {
|
|
2966
|
-
description: "Enforce a configured sort order for TOML keys and tables",
|
|
2967
|
-
recommended: false,
|
|
2968
|
-
requiresTypeChecking: false
|
|
2969
|
-
},
|
|
2970
|
-
fixable: "code",
|
|
2971
|
-
hasSuggestions: false,
|
|
2972
|
-
messages: messages$1,
|
|
2973
|
-
schema,
|
|
2974
|
-
type: "layout"
|
|
2975
|
-
}
|
|
2976
|
-
});
|
|
2977
|
-
//#endregion
|
|
2978
|
-
//#region src/rules/yaml-block-key-blank-lines/rule.ts
|
|
2979
|
-
const RULE_NAME = "yaml-block-key-blank-lines";
|
|
2980
|
-
const messages = { blankLine: "Expected {{count}} blank line(s) around this top-level key." };
|
|
2981
|
-
/**
|
|
2982
|
-
* Determines whether a pair value is a block collection (block mapping or block
|
|
2983
|
-
* sequence). Flow collections (`{ ... }` / `[ ... ]`) count as scalars.
|
|
2984
|
-
*
|
|
2985
|
-
* @param value - The value node of a YAML pair.
|
|
2986
|
-
* @returns True if the value is a block mapping or block sequence.
|
|
2987
|
-
*/
|
|
2988
|
-
function isBlock(value) {
|
|
2989
|
-
return value !== null && (value.type === "YAMLMapping" || value.type === "YAMLSequence") && value.style === "block";
|
|
2990
|
-
}
|
|
2991
|
-
function create(context) {
|
|
2992
|
-
const { sourceCode } = context;
|
|
2993
|
-
if (sourceCode.parserServices.isYAML !== true) return {};
|
|
2994
|
-
return { YAMLMapping(yamlNode) {
|
|
2995
|
-
if (yamlNode.parent.type !== "YAMLDocument") return;
|
|
2996
|
-
const { pairs } = yamlNode;
|
|
2997
|
-
for (let index = 1; index < pairs.length; index += 1) {
|
|
2998
|
-
const previous = pairs[index - 1];
|
|
2999
|
-
const current = pairs[index];
|
|
3000
|
-
if (previous === void 0 || current === void 0) continue;
|
|
3001
|
-
const left = sourceCode.getLastToken(previous);
|
|
3002
|
-
const right = sourceCode.getFirstToken(current);
|
|
3003
|
-
if (sourceCode.commentsExistBetween(left, right)) continue;
|
|
3004
|
-
const blanks = right.loc.start.line - left.loc.end.line - 1;
|
|
3005
|
-
const want = isBlock(previous.value) || isBlock(current.value) ? 1 : 0;
|
|
3006
|
-
if (blanks === want) continue;
|
|
3007
|
-
context.report({
|
|
3008
|
-
data: { count: want },
|
|
3009
|
-
fix(fixer) {
|
|
3010
|
-
return fixer.replaceTextRange([left.range[1], right.range[0]], "\n".repeat(want + 1));
|
|
3011
|
-
},
|
|
3012
|
-
loc: (current.key ?? current).loc,
|
|
3013
|
-
messageId: "blankLine"
|
|
3014
|
-
});
|
|
3015
|
-
}
|
|
3016
|
-
} };
|
|
3017
|
-
}
|
|
3018
|
-
const yamlBlockKeyBlankLines = createEslintRule({
|
|
3019
|
-
name: RULE_NAME,
|
|
3020
|
-
create,
|
|
3021
|
-
defaultOptions: [],
|
|
3022
|
-
meta: {
|
|
3023
|
-
docs: {
|
|
3024
|
-
description: "Enforce blank lines around top-level YAML block collection keys",
|
|
3025
|
-
recommended: false,
|
|
3026
|
-
requiresTypeChecking: false
|
|
3027
|
-
},
|
|
3028
|
-
fixable: "whitespace",
|
|
3029
|
-
hasSuggestions: false,
|
|
3030
|
-
messages,
|
|
3031
|
-
schema: [],
|
|
3032
|
-
type: "layout"
|
|
3033
|
-
}
|
|
3034
|
-
});
|
|
3035
|
-
//#endregion
|
|
3036
|
-
//#region src/plugin.ts
|
|
3037
|
-
const PLUGIN_NAME = name.replace(/^eslint-plugin-/, "");
|
|
3038
|
-
/**
|
|
3039
|
-
* Generates a rules record where all plugin rules are set to "error".
|
|
3040
|
-
*
|
|
3041
|
-
* @param pluginName - The plugin identifier used to prefix rule names.
|
|
3042
|
-
* @param rules - The rules record to transform.
|
|
3043
|
-
* @returns A Linter.RulesRecord with all rules enabled.
|
|
3044
|
-
*/
|
|
3045
|
-
function getRules(pluginName, rules) {
|
|
3046
|
-
return Object.fromEntries(Object.keys(rules).map((ruleName) => [`${pluginName}/${ruleName}`, "error"]));
|
|
3047
|
-
}
|
|
3048
|
-
const plugin = {
|
|
3049
|
-
meta: {
|
|
3050
|
-
name: PLUGIN_NAME,
|
|
3051
|
-
version
|
|
3052
|
-
},
|
|
3053
|
-
rules: {
|
|
3054
|
-
"jsx-shorthand-boolean": jsxShorthandBoolean,
|
|
3055
|
-
"jsx-shorthand-fragment": jsxShorthandFragment,
|
|
3056
|
-
"naming-convention": namingConvention,
|
|
3057
|
-
"no-unnecessary-use-callback": noUnnecessaryUseCallback,
|
|
3058
|
-
"no-unnecessary-use-memo": noUnnecessaryUseMemo,
|
|
3059
|
-
"prefer-destructuring-assignment": preferDestructuringAssignment,
|
|
3060
|
-
"prefer-parameter-destructuring": preferParameterDestructuring,
|
|
3061
|
-
"purity": purity,
|
|
3062
|
-
"toml-sort-keys": tomlSortKeys,
|
|
3063
|
-
"yaml-block-key-blank-lines": yamlBlockKeyBlankLines
|
|
3064
|
-
}
|
|
3065
|
-
};
|
|
3066
|
-
getRules(PLUGIN_NAME, plugin.rules);
|
|
3067
|
-
//#endregion
|
|
1
|
+
import { n as plugin, t as PLUGIN_NAME } from "./plugin-DYVtpuio.mjs";
|
|
3068
2
|
//#region src/configs/index.ts
|
|
3069
3
|
const configs = { recommended: {
|
|
3070
4
|
plugins: { [PLUGIN_NAME]: plugin },
|