@studiometa/eslint-plugin-js-toolkit 3.9.0 → 4.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +2061 -1537
- package/dist/index.js.map +1 -7
- package/package.json +7 -7
- package/dist/rules/async-lifecycle-methods.d.ts +0 -7
- package/dist/rules/components-pascal-case.d.ts +0 -8
- package/dist/rules/emits-kebab-case.d.ts +0 -8
- package/dist/rules/emits-multi-word.d.ts +0 -8
- package/dist/rules/index.d.ts +0 -30
- package/dist/rules/no-create-app.d.ts +0 -7
- package/dist/rules/no-deep-utils-import.d.ts +0 -7
- package/dist/rules/no-deprecated-properties.d.ts +0 -7
- package/dist/rules/no-dispatch-event.d.ts +0 -7
- package/dist/rules/no-dollar-prefix.d.ts +0 -8
- package/dist/rules/no-event-listener-methods.d.ts +0 -7
- package/dist/rules/no-internal-barrel-import.d.ts +0 -7
- package/dist/rules/no-manual-intersection-observer.d.ts +0 -7
- package/dist/rules/no-manual-mutation-observer.d.ts +0 -7
- package/dist/rules/no-redundant-with-mount-when-in-view.d.ts +0 -7
- package/dist/rules/no-shadow-dom.d.ts +0 -7
- package/dist/rules/on-global-handler-prefix.d.ts +0 -7
- package/dist/rules/on-handler-naming.d.ts +0 -7
- package/dist/rules/options-camel-case.d.ts +0 -8
- package/dist/rules/prefer-destructured-lookups.d.ts +0 -8
- package/dist/rules/prefer-ref-over-query-selector.d.ts +0 -7
- package/dist/rules/refs-camel-case.d.ts +0 -8
- package/dist/rules/refs-no-bracket-access.d.ts +0 -7
- package/dist/rules/refs-plural-multiple.d.ts +0 -8
- package/dist/rules/require-children-declared-in-config.d.ts +0 -7
- package/dist/rules/require-config-name-pascal-case.d.ts +0 -8
- package/dist/rules/require-config.d.ts +0 -8
- package/dist/rules/require-destroyed-cleanup.d.ts +0 -12
- package/dist/rules/require-emit-declared-in-config.d.ts +0 -7
- package/dist/rules/require-options-declared-in-config.d.ts +0 -7
- package/dist/rules/require-refs-declared-in-config.d.ts +0 -7
- package/dist/utils/ast.d.ts +0 -51
- package/dist/utils/rule-tester.d.ts +0 -6
package/dist/index.js
CHANGED
|
@@ -1,1631 +1,2155 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
function eslintCompatPlugin(plugin2) {
|
|
4
|
-
if (typeof plugin2 != "object" || !plugin2) throw Error("Plugin must be an object");
|
|
5
|
-
let { rules: rules2 } = plugin2;
|
|
6
|
-
if (typeof rules2 != "object" || !rules2) throw Error("Plugin must have an object as `rules` property");
|
|
7
|
-
let afterHooksState = new AfterHooksState();
|
|
8
|
-
for (let ruleName in rules2) Object.hasOwn(rules2, ruleName) && convertRule(rules2[ruleName], afterHooksState);
|
|
9
|
-
return plugin2;
|
|
10
|
-
}
|
|
11
|
-
var AfterHooksState = class {
|
|
12
|
-
resetFunctions = [];
|
|
13
|
-
pendingStates = [];
|
|
14
|
-
pendingCount = 0;
|
|
15
|
-
lintFinishedCount = 0;
|
|
16
|
-
resetIsScheduled = false;
|
|
17
|
-
sourceCode = null;
|
|
18
|
-
resetMicrotask = this.resetMicrotaskImpl.bind(this);
|
|
19
|
-
/**
|
|
20
|
-
* Register a function to run `after` hook for a rule, and reset state.
|
|
21
|
-
* @param reset - Function to run `after` hook and reset state
|
|
22
|
-
* @returns Index of rule
|
|
23
|
-
*/
|
|
24
|
-
registerResetFunction(reset) {
|
|
25
|
-
let { pendingStates } = this, index = pendingStates.length;
|
|
26
|
-
return pendingStates.push(0), this.resetFunctions.push(reset), index;
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Register that a rule with `after` hook has completed linting a file.
|
|
30
|
-
* Called by `onCodePathEnd` CFG event handler which is added to visitor for rules with `after` hooks.
|
|
31
|
-
*
|
|
32
|
-
* If all rules with an `after` hook which needs to be run have completed linting the file, run all `after` hooks.
|
|
33
|
-
*/
|
|
34
|
-
ruleFinished() {
|
|
35
|
-
this.lintFinishedCount++, this.lintFinishedCount === this.pendingCount && this.reset(false);
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Call all reset functions where corresponding entry in `pendingStates` is `AFTER_HOOK_PENDING`.
|
|
39
|
-
* Should only be called when some `after` hooks are pending.
|
|
40
|
-
*
|
|
41
|
-
* @param ignoreErrors - `true` to catch and silently ignore any errors which occur in `after` hooks.
|
|
42
|
-
* `false` to throw them,
|
|
43
|
-
* @throws {unknown} If `ignoreErrors` is `false` and an error occurs in any `after` hooks.
|
|
44
|
-
*/
|
|
45
|
-
reset(ignoreErrors) {
|
|
46
|
-
this.pendingCount;
|
|
47
|
-
let { resetFunctions, pendingStates } = this, hooksLen = pendingStates.length, hasError = false, error;
|
|
48
|
-
for (let i = 0; i < hooksLen; i++) if (pendingStates[i] !== 0) {
|
|
49
|
-
pendingStates[i] = 0;
|
|
50
|
-
try {
|
|
51
|
-
resetFunctions[i]();
|
|
52
|
-
} catch (e) {
|
|
53
|
-
hasError === false && (hasError = true, error = e);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
if (this.pendingCount = 0, this.lintFinishedCount = 0, this.sourceCode = null, hasError === true && ignoreErrors === false) throw error;
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Schedule a microtask to run `reset` functions.
|
|
60
|
-
*/
|
|
61
|
-
scheduleReset() {
|
|
62
|
-
queueMicrotask(this.resetMicrotask), this.resetIsScheduled = true;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Function which is scheduled as the cleanup microtask.
|
|
66
|
-
* `scheduleReset` uses `resetMicrotask` which is this method bound to `this`.
|
|
67
|
-
*/
|
|
68
|
-
resetMicrotaskImpl() {
|
|
69
|
-
this.resetIsScheduled = false, this.pendingCount !== 0 && this.reset(true);
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
function convertRule(rule, afterHooksState) {
|
|
73
|
-
if (typeof rule != "object" || !rule) throw Error("Rule must be an object");
|
|
74
|
-
if ("create" in rule) return;
|
|
75
|
-
let context = null, visitor, beforeHook, setupAfterHook;
|
|
76
|
-
rule.create = (eslintContext) => {
|
|
77
|
-
context === null && ({ context, visitor, beforeHook, setupAfterHook } = createContextAndVisitor(rule, afterHooksState));
|
|
78
|
-
let eslintFileContext = Object.getPrototypeOf(eslintContext);
|
|
79
|
-
if (setupAfterHook !== null) {
|
|
80
|
-
let { sourceCode } = eslintFileContext;
|
|
81
|
-
afterHooksState.sourceCode !== sourceCode && (afterHooksState.sourceCode = sourceCode, afterHooksState.pendingCount !== 0 && afterHooksState.reset(true));
|
|
82
|
-
}
|
|
83
|
-
return Object.defineProperties(context, {
|
|
84
|
-
id: { value: eslintContext.id },
|
|
85
|
-
options: { value: eslintContext.options },
|
|
86
|
-
report: { value: eslintContext.report }
|
|
87
|
-
}), Object.setPrototypeOf(context, eslintFileContext), beforeHook !== null && beforeHook() === false ? EMPTY_VISITOR : (setupAfterHook !== null && (setupAfterHook(eslintFileContext.sourceCode.ast), afterHooksState.resetIsScheduled === false && afterHooksState.scheduleReset()), visitor);
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
var FILE_CONTEXT = Object.freeze({
|
|
91
|
-
get filename() {
|
|
92
|
-
throw Error("Cannot access `context.filename` in `createOnce`");
|
|
93
|
-
},
|
|
94
|
-
getFilename() {
|
|
95
|
-
throw Error("Cannot call `context.getFilename` in `createOnce`");
|
|
96
|
-
},
|
|
97
|
-
get physicalFilename() {
|
|
98
|
-
throw Error("Cannot access `context.physicalFilename` in `createOnce`");
|
|
99
|
-
},
|
|
100
|
-
getPhysicalFilename() {
|
|
101
|
-
throw Error("Cannot call `context.getPhysicalFilename` in `createOnce`");
|
|
102
|
-
},
|
|
103
|
-
get cwd() {
|
|
104
|
-
throw Error("Cannot access `context.cwd` in `createOnce`");
|
|
105
|
-
},
|
|
106
|
-
getCwd() {
|
|
107
|
-
throw Error("Cannot call `context.getCwd` in `createOnce`");
|
|
108
|
-
},
|
|
109
|
-
get sourceCode() {
|
|
110
|
-
throw Error("Cannot access `context.sourceCode` in `createOnce`");
|
|
111
|
-
},
|
|
112
|
-
getSourceCode() {
|
|
113
|
-
throw Error("Cannot call `context.getSourceCode` in `createOnce`");
|
|
114
|
-
},
|
|
115
|
-
get languageOptions() {
|
|
116
|
-
throw Error("Cannot access `context.languageOptions` in `createOnce`");
|
|
117
|
-
},
|
|
118
|
-
get settings() {
|
|
119
|
-
throw Error("Cannot access `context.settings` in `createOnce`");
|
|
120
|
-
},
|
|
121
|
-
extend(extension) {
|
|
122
|
-
return Object.freeze(Object.assign(Object.create(this), extension));
|
|
123
|
-
},
|
|
124
|
-
get parserOptions() {
|
|
125
|
-
throw Error("Cannot access `context.parserOptions` in `createOnce`");
|
|
126
|
-
},
|
|
127
|
-
get parserPath() {
|
|
128
|
-
throw Error("Cannot access `context.parserPath` in `createOnce`");
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
function createContextAndVisitor(rule, afterHooksState) {
|
|
132
|
-
let { createOnce } = rule;
|
|
133
|
-
if (createOnce == null) throw Error("Rules must define either a `create` or `createOnce` method");
|
|
134
|
-
if (typeof createOnce != "function") throw Error("Rule `createOnce` property must be a function");
|
|
135
|
-
let context = Object.create(FILE_CONTEXT, {
|
|
136
|
-
id: {
|
|
137
|
-
value: null,
|
|
138
|
-
enumerable: true,
|
|
139
|
-
configurable: true
|
|
140
|
-
},
|
|
141
|
-
options: {
|
|
142
|
-
value: null,
|
|
143
|
-
enumerable: true,
|
|
144
|
-
configurable: true
|
|
145
|
-
},
|
|
146
|
-
report: {
|
|
147
|
-
value() {
|
|
148
|
-
throw Error("Cannot report errors in `createOnce`");
|
|
149
|
-
},
|
|
150
|
-
enumerable: true,
|
|
151
|
-
configurable: true
|
|
152
|
-
}
|
|
153
|
-
}), { before: beforeHook, after: afterHook, ...visitor } = createOnce.call(rule, context);
|
|
154
|
-
if (beforeHook === void 0) beforeHook = null;
|
|
155
|
-
else if (beforeHook !== null && typeof beforeHook != "function") throw Error("`before` property of visitor must be a function if defined");
|
|
156
|
-
let setupAfterHook = null;
|
|
157
|
-
if (afterHook != null) {
|
|
158
|
-
if (typeof afterHook != "function") throw Error("`after` property of visitor must be a function if defined");
|
|
159
|
-
let program = null, ruleIndex = afterHooksState.registerResetFunction(() => {
|
|
160
|
-
program = null, afterHook();
|
|
161
|
-
});
|
|
162
|
-
setupAfterHook = (ast) => {
|
|
163
|
-
program = ast, afterHooksState.pendingStates[ruleIndex] = 1, afterHooksState.pendingCount++;
|
|
164
|
-
};
|
|
165
|
-
let onCodePathEnd = visitor.onCodePathEnd;
|
|
166
|
-
visitor.onCodePathEnd = onCodePathEnd == null ? function(_codePath, node) {
|
|
167
|
-
node === program && afterHooksState.ruleFinished();
|
|
168
|
-
} : function(codePath, node) {
|
|
169
|
-
onCodePathEnd.call(this, codePath, node), node === program && afterHooksState.ruleFinished();
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
return {
|
|
173
|
-
context,
|
|
174
|
-
visitor,
|
|
175
|
-
beforeHook,
|
|
176
|
-
setupAfterHook
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// src/utils/ast.ts
|
|
1
|
+
import { eslintCompatPlugin } from "@oxlint/plugins";
|
|
2
|
+
//#region src/utils/ast.ts
|
|
181
3
|
function createRule(rule) {
|
|
182
|
-
|
|
4
|
+
return rule;
|
|
183
5
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
6
|
+
const LIFECYCLE_METHODS = /* @__PURE__ */ new Set([
|
|
7
|
+
"mounted",
|
|
8
|
+
"destroyed",
|
|
9
|
+
"updated",
|
|
10
|
+
"terminated",
|
|
11
|
+
"ticked",
|
|
12
|
+
"scrolled",
|
|
13
|
+
"resized",
|
|
14
|
+
"moved",
|
|
15
|
+
"loaded",
|
|
16
|
+
"keyed"
|
|
195
17
|
]);
|
|
196
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Returns true if a class declaration extends an identifier imported from
|
|
20
|
+
* @studiometa/js-toolkit, or extends a bare identifier named `Base`.
|
|
21
|
+
*
|
|
22
|
+
* This is a heuristic — we cannot do full type resolution in a lint rule.
|
|
23
|
+
*/
|
|
197
24
|
function isBaseSubclass(node, context) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const sourceCode = context.sourceCode ?? context.getSourceCode?.();
|
|
212
|
-
const ast = sourceCode?.ast;
|
|
213
|
-
if (!ast) {
|
|
214
|
-
return false;
|
|
215
|
-
}
|
|
216
|
-
for (const node2 of ast.body) {
|
|
217
|
-
if (node2.type !== "ImportDeclaration") continue;
|
|
218
|
-
if (node2.source.value !== TOOLKIT_PACKAGE) continue;
|
|
219
|
-
for (const specifier of node2.specifiers) {
|
|
220
|
-
if (specifier.type === "ImportSpecifier" && specifier.local.name === superName) {
|
|
221
|
-
return true;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
return false;
|
|
25
|
+
if (node.type !== "ClassDeclaration" && node.type !== "ClassExpression") return false;
|
|
26
|
+
if (!node.superClass) return false;
|
|
27
|
+
const superName = node.superClass.type === "Identifier" ? node.superClass.name : null;
|
|
28
|
+
if (!superName) return false;
|
|
29
|
+
if (superName === "Base") return true;
|
|
30
|
+
const ast = (context.sourceCode ?? context.getSourceCode?.())?.ast;
|
|
31
|
+
if (!ast) return false;
|
|
32
|
+
for (const node of ast.body) {
|
|
33
|
+
if (node.type !== "ImportDeclaration") continue;
|
|
34
|
+
if (node.source.value !== "@studiometa/js-toolkit") continue;
|
|
35
|
+
for (const specifier of node.specifiers) if (specifier.type === "ImportSpecifier" && specifier.local.name === superName) return true;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
226
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Returns true if the string is camelCase (starts lowercase, no separators).
|
|
41
|
+
*/
|
|
227
42
|
function isCamelCase(value) {
|
|
228
|
-
|
|
43
|
+
return /^[a-z][a-zA-Z0-9]*$/.test(value);
|
|
229
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Returns true if the string is PascalCase.
|
|
47
|
+
*/
|
|
230
48
|
function isPascalCase(value) {
|
|
231
|
-
|
|
49
|
+
return /^[A-Z][a-zA-Z0-9]*$/.test(value);
|
|
232
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Converts a kebab-case, snake_case or PascalCase string to camelCase.
|
|
53
|
+
*/
|
|
233
54
|
function toCamelCase(value) {
|
|
234
|
-
|
|
55
|
+
return value.replace(/[-_](.)/g, (_, c) => c.toUpperCase()).replace(/^[A-Z]/, (c) => c.toLowerCase());
|
|
235
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Converts a kebab-case, snake_case or camelCase string to PascalCase.
|
|
59
|
+
*/
|
|
236
60
|
function toPascalCase(value) {
|
|
237
|
-
|
|
61
|
+
return value.replace(/[-_](.)/g, (_, c) => c.toUpperCase()).replace(/^[a-z]/, (c) => c.toUpperCase());
|
|
238
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Walks up ancestor nodes to find the nearest class declaration/expression.
|
|
65
|
+
*/
|
|
239
66
|
function findEnclosingClass(ancestors) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
return null;
|
|
67
|
+
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
68
|
+
const node = ancestors[i];
|
|
69
|
+
if (node.type === "ClassDeclaration" || node.type === "ClassExpression") return node;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
247
72
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Returns the ancestors of a node, whichever accessor the runtime exposes.
|
|
75
|
+
*/
|
|
76
|
+
function getAncestors(node, context) {
|
|
77
|
+
return context.getAncestors ? context.getAncestors() : context.sourceCode?.getAncestors?.(node) ?? [];
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Returns true for the node types that open a new function scope.
|
|
81
|
+
*/
|
|
82
|
+
function isFunctionNode(node) {
|
|
83
|
+
return node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression";
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Walks every descendant node, depth first, calling `visit` on each.
|
|
87
|
+
*
|
|
88
|
+
* Returning `false` from `visit` skips the subtree of that node. Used by rules
|
|
89
|
+
* which have to analyse a node the visitor will not hand them again, such as
|
|
90
|
+
* the body of a method resolved from its call site.
|
|
91
|
+
*/
|
|
92
|
+
function walk(node, visit) {
|
|
93
|
+
if (!node || typeof node.type !== "string") return;
|
|
94
|
+
if (visit(node) === false) return;
|
|
95
|
+
for (const key of Object.keys(node)) {
|
|
96
|
+
if (key === "parent") continue;
|
|
97
|
+
const value = node[key];
|
|
98
|
+
if (Array.isArray(value)) {
|
|
99
|
+
for (const item of value) if (item && typeof item.type === "string") walk(item, visit);
|
|
100
|
+
} else if (value && typeof value.type === "string") walk(value, visit);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Returns the name of a class member key, for identifier and literal keys alike.
|
|
105
|
+
*/
|
|
106
|
+
function getKeyName(node) {
|
|
107
|
+
const key = node?.key;
|
|
108
|
+
if (!key) return null;
|
|
109
|
+
if (key.type === "Identifier") return key.name;
|
|
110
|
+
if (key.type === "Literal" && typeof key.value === "string") return key.value;
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Returns true if a class member carries one of the named decorators, written
|
|
115
|
+
* either bare (`@write`) or called (`@write()`).
|
|
116
|
+
*/
|
|
117
|
+
function hasDecorator(node, names) {
|
|
118
|
+
for (const decorator of node?.decorators ?? []) {
|
|
119
|
+
const expression = decorator.expression;
|
|
120
|
+
if (!expression) continue;
|
|
121
|
+
if (expression.type === "Identifier" && names.has(expression.name)) return true;
|
|
122
|
+
if (expression.type === "CallExpression" && expression.callee?.type === "Identifier" && names.has(expression.callee.name)) return true;
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Unwraps the type-argument application a v4 component writes on its
|
|
128
|
+
* superclass, as in `extends withRaf(Base)<Props>`.
|
|
129
|
+
*/
|
|
130
|
+
function unwrapTypeArguments(node) {
|
|
131
|
+
return node?.type === "TSInstantiationExpression" ? node.expression ?? node : node;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The instance surface only a v4 component has.
|
|
135
|
+
*
|
|
136
|
+
* Reaching for one of these off `this` is the signal that a class extending
|
|
137
|
+
* something unresolvable is a component rather than an ordinary subclass. The
|
|
138
|
+
* lifecycle and service hook names are deliberately **not** in this set: they
|
|
139
|
+
* are the very thing a rule like `no-write-in-read-phase` keys on, so counting
|
|
140
|
+
* them as proof would make the test circular and let `class Store extends
|
|
141
|
+
* Error { ticked() {} }` pass for a component.
|
|
142
|
+
*/
|
|
143
|
+
const COMPONENT_MEMBERS = /* @__PURE__ */ new Set([
|
|
144
|
+
"$el",
|
|
145
|
+
"$refs",
|
|
146
|
+
"$options",
|
|
147
|
+
"$id",
|
|
148
|
+
"$config",
|
|
149
|
+
"$isMounted",
|
|
150
|
+
"$emit",
|
|
151
|
+
"$on",
|
|
152
|
+
"$off",
|
|
153
|
+
"$query",
|
|
154
|
+
"$closest",
|
|
155
|
+
"$watchChildren",
|
|
156
|
+
"$provide",
|
|
157
|
+
"$inject",
|
|
158
|
+
"$injectSync",
|
|
159
|
+
"$read",
|
|
160
|
+
"$write",
|
|
161
|
+
"$mount",
|
|
162
|
+
"$unmount",
|
|
163
|
+
"$destroy",
|
|
164
|
+
"$services"
|
|
165
|
+
]);
|
|
166
|
+
/**
|
|
167
|
+
* Returns true if the class body reaches for the framework surface off `this`.
|
|
168
|
+
*/
|
|
169
|
+
function usesComponentSurface(node) {
|
|
170
|
+
let found = false;
|
|
171
|
+
walk(node.body, (inner) => {
|
|
172
|
+
if (found) return false;
|
|
173
|
+
if (inner !== node.body && (inner.type === "ClassDeclaration" || inner.type === "ClassExpression")) return false;
|
|
174
|
+
if (inner.type === "MemberExpression" && !inner.computed && inner.object?.type === "ThisExpression" && COMPONENT_MEMBERS.has(inner.property?.name)) {
|
|
175
|
+
found = true;
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
return found;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Returns true if a class looks like a v4 component.
|
|
183
|
+
*
|
|
184
|
+
* A component always extends something — `Base`, a `with<Service>(…)` mixin,
|
|
185
|
+
* or a local abstract component — so a superclass is necessary. It is nowhere
|
|
186
|
+
* near sufficient: `class Store extends Error` would read as a component, and
|
|
187
|
+
* `prefer-instance-scheduler` would then tell it to call a `this.$read()` it
|
|
188
|
+
* does not have, which is wrong advice rather than mere noise.
|
|
189
|
+
*
|
|
190
|
+
* Restricting to a list of known bases is not the answer either. `extends
|
|
191
|
+
* AbstractCarouselChild`, `extends Indexable` and `extends AbstractPrefetch`
|
|
192
|
+
* are the common shape in `@studiometa/ui`, and a single-file linter cannot
|
|
193
|
+
* follow the chain to `Base`.
|
|
194
|
+
*
|
|
195
|
+
* So a superclass **plus** a second signal in the same class body: a
|
|
196
|
+
* `@component({ … })` decorator, a `static config`, or use of the framework
|
|
197
|
+
* surface off `this`. That keeps a subclass of a local abstract component —
|
|
198
|
+
* which reads `this.$el` or emits — while rejecting a class that merely
|
|
199
|
+
* extends something.
|
|
200
|
+
*
|
|
201
|
+
* Requiring the superclass is what keeps `Base` itself out. It declares
|
|
202
|
+
* `static config` and defines the whole surface, but it extends nothing, and
|
|
203
|
+
* the class defining `$read()` is not a class that should be told to call it.
|
|
204
|
+
*
|
|
205
|
+
* A component using none of the three anywhere in its body is missed, which
|
|
206
|
+
* is the right way to be wrong: a false negative rather than advice that does
|
|
207
|
+
* not apply.
|
|
208
|
+
*/
|
|
209
|
+
function isComponentClass(node) {
|
|
210
|
+
if (node?.type !== "ClassDeclaration" && node?.type !== "ClassExpression") return false;
|
|
211
|
+
if (!unwrapTypeArguments(node.superClass)) return false;
|
|
212
|
+
return hasDecorator(node, /* @__PURE__ */ new Set(["component"])) || Boolean(findStaticConfig(node)) || usesComponentSurface(node);
|
|
213
|
+
}
|
|
214
|
+
/** The object literal passed to a class's `@component({ … })` decorator. */
|
|
215
|
+
function findDecoratorConfig(node) {
|
|
216
|
+
for (const decorator of node?.decorators ?? []) {
|
|
217
|
+
const expression = decorator.expression;
|
|
218
|
+
if (expression?.type !== "CallExpression") continue;
|
|
219
|
+
if (expression.callee?.type !== "Identifier") continue;
|
|
220
|
+
if (expression.callee.name !== "component") continue;
|
|
221
|
+
const argument = expression.arguments?.[0];
|
|
222
|
+
if (argument?.type === "ObjectExpression") return argument;
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
/** The `static config = { … }` object of a class. */
|
|
227
|
+
function findStaticConfig(node) {
|
|
228
|
+
for (const member of node?.body?.body ?? []) {
|
|
229
|
+
if (member.type !== "PropertyDefinition") continue;
|
|
230
|
+
if (member.static !== true) continue;
|
|
231
|
+
if (member.key?.name !== "config") continue;
|
|
232
|
+
if (member.value?.type === "ObjectExpression") return member.value;
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Finds the `static config = { … }` object of a class, or the object literal
|
|
238
|
+
* passed to its `@component({ … })` decorator. v4 accepts both spellings.
|
|
239
|
+
*/
|
|
240
|
+
function findComponentConfig(node) {
|
|
241
|
+
return findDecoratorConfig(node) ?? findStaticConfig(node);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Returns the value of a named property of an object literal.
|
|
245
|
+
*/
|
|
246
|
+
function getProperty(node, name) {
|
|
247
|
+
for (const property of node?.properties ?? []) {
|
|
248
|
+
if (property.type !== "Property") continue;
|
|
249
|
+
if (getKeyName(property) === name) return property.value ?? null;
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
//#endregion
|
|
254
|
+
//#region src/rules/require-config.ts
|
|
255
|
+
const requireConfig = createRule({
|
|
256
|
+
meta: {
|
|
257
|
+
type: "problem",
|
|
258
|
+
docs: { description: "Require a static config property on classes extending Base" },
|
|
259
|
+
messages: {
|
|
260
|
+
missingConfig: "Classes extending Base must define a static config property.",
|
|
261
|
+
missingName: "The static config must include a name property."
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
createOnce(context) {
|
|
265
|
+
return {
|
|
266
|
+
ClassDeclaration(node) {
|
|
267
|
+
check$9(node, context);
|
|
268
|
+
},
|
|
269
|
+
ClassExpression(node) {
|
|
270
|
+
check$9(node, context);
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
}
|
|
271
274
|
});
|
|
272
|
-
function check(node, context) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
context.report({ node: configProp, messageId: "missingName" });
|
|
289
|
-
}
|
|
275
|
+
function check$9(node, context) {
|
|
276
|
+
if (!isBaseSubclass(node, context)) return;
|
|
277
|
+
const configProp = (node.body?.body ?? []).find((member) => member.type === "PropertyDefinition" && member.static === true && member.key?.name === "config");
|
|
278
|
+
if (!configProp) {
|
|
279
|
+
context.report({
|
|
280
|
+
node,
|
|
281
|
+
messageId: "missingConfig"
|
|
282
|
+
});
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const value = configProp.value;
|
|
286
|
+
if (!value || value.type !== "ObjectExpression") return;
|
|
287
|
+
if (!value.properties?.some((prop) => prop.type === "Property" && prop.key?.name === "name" && prop.value?.value)) context.report({
|
|
288
|
+
node: configProp,
|
|
289
|
+
messageId: "missingName"
|
|
290
|
+
});
|
|
290
291
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
check2(node, context);
|
|
311
|
-
}
|
|
312
|
-
};
|
|
313
|
-
}
|
|
292
|
+
//#endregion
|
|
293
|
+
//#region src/rules/require-config-name-pascal-case.ts
|
|
294
|
+
const requireConfigNamePascalCase = createRule({
|
|
295
|
+
meta: {
|
|
296
|
+
type: "problem",
|
|
297
|
+
fixable: "code",
|
|
298
|
+
docs: { description: "Require config.name to be PascalCase on classes extending Base" },
|
|
299
|
+
messages: { notPascalCase: "config.name \"{{name}}\" must be PascalCase." }
|
|
300
|
+
},
|
|
301
|
+
createOnce(context) {
|
|
302
|
+
return {
|
|
303
|
+
ClassDeclaration(node) {
|
|
304
|
+
check$8(node, context);
|
|
305
|
+
},
|
|
306
|
+
ClassExpression(node) {
|
|
307
|
+
check$8(node, context);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
}
|
|
314
311
|
});
|
|
315
|
-
function
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
context.report({
|
|
330
|
-
node: nameProp.value,
|
|
331
|
-
messageId: "notPascalCase",
|
|
332
|
-
data: { name: nameValue },
|
|
333
|
-
fix: (fixer) => fixer.replaceText(nameProp.value, `'${toPascalCase(nameValue)}'`)
|
|
334
|
-
});
|
|
335
|
-
}
|
|
312
|
+
function check$8(node, context) {
|
|
313
|
+
if (!isBaseSubclass(node, context)) return;
|
|
314
|
+
const configProp = (node.body?.body ?? []).find((member) => member.type === "PropertyDefinition" && member.static === true && member.key?.name === "config");
|
|
315
|
+
if (!configProp?.value || configProp.value.type !== "ObjectExpression") return;
|
|
316
|
+
const nameProp = configProp.value.properties?.find((prop) => prop.type === "Property" && prop.key?.name === "name");
|
|
317
|
+
if (!nameProp) return;
|
|
318
|
+
const nameValue = nameProp.value?.value;
|
|
319
|
+
if (typeof nameValue !== "string") return;
|
|
320
|
+
if (!isPascalCase(nameValue)) context.report({
|
|
321
|
+
node: nameProp.value,
|
|
322
|
+
messageId: "notPascalCase",
|
|
323
|
+
data: { name: nameValue },
|
|
324
|
+
fix: (fixer) => fixer.replaceText(nameProp.value, `'${toPascalCase(nameValue)}'`)
|
|
325
|
+
});
|
|
336
326
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
check3(node, context);
|
|
357
|
-
}
|
|
358
|
-
};
|
|
359
|
-
}
|
|
327
|
+
//#endregion
|
|
328
|
+
//#region src/rules/refs-camel-case.ts
|
|
329
|
+
const refsCamelCase = createRule({
|
|
330
|
+
meta: {
|
|
331
|
+
type: "problem",
|
|
332
|
+
fixable: "code",
|
|
333
|
+
docs: { description: "Require ref names in static config to be camelCase" },
|
|
334
|
+
messages: { notCamelCase: "Ref name \"{{name}}\" must be camelCase." }
|
|
335
|
+
},
|
|
336
|
+
createOnce(context) {
|
|
337
|
+
return {
|
|
338
|
+
ClassDeclaration(node) {
|
|
339
|
+
check$7(node, context);
|
|
340
|
+
},
|
|
341
|
+
ClassExpression(node) {
|
|
342
|
+
check$7(node, context);
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
}
|
|
360
346
|
});
|
|
361
|
-
function
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
data: { name },
|
|
383
|
-
fix: (fixer) => fixer.replaceText(element, `'${fixed}'`)
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
}
|
|
347
|
+
function check$7(node, context) {
|
|
348
|
+
if (!isBaseSubclass(node, context)) return;
|
|
349
|
+
const configProp = (node.body?.body ?? []).find((member) => member.type === "PropertyDefinition" && member.static === true && member.key?.name === "config");
|
|
350
|
+
if (!configProp?.value || configProp.value.type !== "ObjectExpression") return;
|
|
351
|
+
const refsProp = configProp.value.properties?.find((prop) => prop.type === "Property" && prop.key?.name === "refs");
|
|
352
|
+
if (!refsProp?.value || refsProp.value.type !== "ArrayExpression") return;
|
|
353
|
+
for (const element of refsProp.value.elements ?? []) {
|
|
354
|
+
if (!element || element.type !== "Literal") continue;
|
|
355
|
+
const raw = element.value;
|
|
356
|
+
if (typeof raw !== "string") continue;
|
|
357
|
+
const name = raw.endsWith("[]") ? raw.slice(0, -2) : raw;
|
|
358
|
+
if (!isCamelCase(name)) {
|
|
359
|
+
const fixed = toCamelCase(name) + (raw.endsWith("[]") ? "[]" : "");
|
|
360
|
+
context.report({
|
|
361
|
+
node: element,
|
|
362
|
+
messageId: "notCamelCase",
|
|
363
|
+
data: { name },
|
|
364
|
+
fix: (fixer) => fixer.replaceText(element, `'${fixed}'`)
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
387
368
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
check4(node, context);
|
|
408
|
-
}
|
|
409
|
-
};
|
|
410
|
-
}
|
|
369
|
+
//#endregion
|
|
370
|
+
//#region src/rules/options-camel-case.ts
|
|
371
|
+
const optionsCamelCase = createRule({
|
|
372
|
+
meta: {
|
|
373
|
+
type: "problem",
|
|
374
|
+
fixable: "code",
|
|
375
|
+
docs: { description: "Require option keys in static config to be camelCase" },
|
|
376
|
+
messages: { notCamelCase: "Option key \"{{name}}\" must be camelCase." }
|
|
377
|
+
},
|
|
378
|
+
createOnce(context) {
|
|
379
|
+
return {
|
|
380
|
+
ClassDeclaration(node) {
|
|
381
|
+
check$6(node, context);
|
|
382
|
+
},
|
|
383
|
+
ClassExpression(node) {
|
|
384
|
+
check$6(node, context);
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
}
|
|
411
388
|
});
|
|
412
|
-
function
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
data: { name },
|
|
434
|
-
fix: (fixer) => fixer.replaceText(prop.key, fixedText)
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
}
|
|
389
|
+
function check$6(node, context) {
|
|
390
|
+
if (!isBaseSubclass(node, context)) return;
|
|
391
|
+
const configProp = (node.body?.body ?? []).find((member) => member.type === "PropertyDefinition" && member.static === true && member.key?.name === "config");
|
|
392
|
+
if (!configProp?.value || configProp.value.type !== "ObjectExpression") return;
|
|
393
|
+
const optionsProp = configProp.value.properties?.find((prop) => prop.type === "Property" && prop.key?.name === "options");
|
|
394
|
+
if (!optionsProp?.value || optionsProp.value.type !== "ObjectExpression") return;
|
|
395
|
+
for (const prop of optionsProp.value.properties ?? []) {
|
|
396
|
+
if (prop.type !== "Property") continue;
|
|
397
|
+
const name = prop.key?.name ?? prop.key?.value;
|
|
398
|
+
if (typeof name !== "string") continue;
|
|
399
|
+
if (!isCamelCase(name)) {
|
|
400
|
+
const fixed = toCamelCase(name);
|
|
401
|
+
const fixedText = prop.key.type === "Literal" ? `'${fixed}'` : fixed;
|
|
402
|
+
context.report({
|
|
403
|
+
node: prop.key,
|
|
404
|
+
messageId: "notCamelCase",
|
|
405
|
+
data: { name },
|
|
406
|
+
fix: (fixer) => fixer.replaceText(prop.key, fixedText)
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
438
410
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
messageId: "notAsync",
|
|
464
|
-
data: { name },
|
|
465
|
-
fix: (fixer) => fixer.insertTextBefore(node.key, "async ")
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
};
|
|
469
|
-
}
|
|
411
|
+
//#endregion
|
|
412
|
+
//#region src/rules/async-lifecycle-methods.ts
|
|
413
|
+
const asyncLifecycleMethods = createRule({
|
|
414
|
+
meta: {
|
|
415
|
+
type: "problem",
|
|
416
|
+
fixable: "code",
|
|
417
|
+
docs: { description: "Require lifecycle methods on Base subclasses to be async" },
|
|
418
|
+
messages: { notAsync: "Lifecycle method \"{{name}}\" must be declared as async." }
|
|
419
|
+
},
|
|
420
|
+
createOnce(context) {
|
|
421
|
+
return { MethodDefinition(node) {
|
|
422
|
+
const name = node.key?.name;
|
|
423
|
+
if (!name || !LIFECYCLE_METHODS.has(name)) return;
|
|
424
|
+
if (node.value?.async) return;
|
|
425
|
+
const enclosingClass = findEnclosingClass(context.getAncestors ? context.getAncestors() : context.sourceCode?.getAncestors?.(node) ?? []);
|
|
426
|
+
if (!enclosingClass || !isBaseSubclass(enclosingClass, context)) return;
|
|
427
|
+
context.report({
|
|
428
|
+
node,
|
|
429
|
+
messageId: "notAsync",
|
|
430
|
+
data: { name },
|
|
431
|
+
fix: (fixer) => fixer.insertTextBefore(node.key, "async ")
|
|
432
|
+
});
|
|
433
|
+
} };
|
|
434
|
+
}
|
|
470
435
|
});
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
}
|
|
496
|
-
};
|
|
497
|
-
}
|
|
436
|
+
//#endregion
|
|
437
|
+
//#region src/rules/on-handler-naming.ts
|
|
438
|
+
const ON_HANDLER_RE = /^on[A-Z][a-zA-Z0-9]*$/;
|
|
439
|
+
const onHandlerNaming = createRule({
|
|
440
|
+
meta: {
|
|
441
|
+
type: "problem",
|
|
442
|
+
docs: { description: "Require event handler methods to follow the onXxxYyy naming convention" },
|
|
443
|
+
messages: { invalidName: "Event handler \"{{name}}\" must follow the onXxxYyy camelCase convention (e.g. onClickButton)." }
|
|
444
|
+
},
|
|
445
|
+
createOnce(context) {
|
|
446
|
+
return { MethodDefinition(node) {
|
|
447
|
+
if (node.kind === "get" || node.kind === "set") return;
|
|
448
|
+
const name = node.key?.name;
|
|
449
|
+
if (!name || !/^on[A-Z]/.test(name)) return;
|
|
450
|
+
if (ON_HANDLER_RE.test(name)) return;
|
|
451
|
+
const enclosingClass = findEnclosingClass(context.getAncestors ? context.getAncestors() : context.sourceCode?.getAncestors?.(node) ?? []);
|
|
452
|
+
if (!enclosingClass || !isBaseSubclass(enclosingClass, context)) return;
|
|
453
|
+
context.report({
|
|
454
|
+
node,
|
|
455
|
+
messageId: "invalidName",
|
|
456
|
+
data: { name }
|
|
457
|
+
});
|
|
458
|
+
} };
|
|
459
|
+
}
|
|
498
460
|
});
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
});
|
|
529
|
-
}
|
|
530
|
-
};
|
|
531
|
-
}
|
|
461
|
+
//#endregion
|
|
462
|
+
//#region src/rules/on-global-handler-prefix.ts
|
|
463
|
+
const GLOBAL_ONLY_EVENTS = /* @__PURE__ */ new Set(["Resize"]);
|
|
464
|
+
const onGlobalHandlerPrefix = createRule({
|
|
465
|
+
meta: {
|
|
466
|
+
type: "suggestion",
|
|
467
|
+
docs: { description: "Require global event handlers (resize, scroll, etc.) to be prefixed with onWindow or onDocument" },
|
|
468
|
+
messages: { missingPrefix: "\"{{name}}\" looks like a global event handler. Use \"onWindow{{event}}\" or \"onDocument{{event}}\" to bind to window/document events." }
|
|
469
|
+
},
|
|
470
|
+
createOnce(context) {
|
|
471
|
+
return { MethodDefinition(node) {
|
|
472
|
+
const name = node.key?.name;
|
|
473
|
+
if (!name || !name.startsWith("on")) return;
|
|
474
|
+
const suffix = name.slice(2);
|
|
475
|
+
const event = GLOBAL_ONLY_EVENTS.has(suffix) ? suffix : null;
|
|
476
|
+
if (!event) return;
|
|
477
|
+
if (name.startsWith("onWindow") || name.startsWith("onDocument")) return;
|
|
478
|
+
const enclosingClass = findEnclosingClass(context.getAncestors ? context.getAncestors() : context.sourceCode?.getAncestors?.(node) ?? []);
|
|
479
|
+
if (!enclosingClass || !isBaseSubclass(enclosingClass, context)) return;
|
|
480
|
+
context.report({
|
|
481
|
+
node,
|
|
482
|
+
messageId: "missingPrefix",
|
|
483
|
+
data: {
|
|
484
|
+
name,
|
|
485
|
+
event
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
} };
|
|
489
|
+
}
|
|
532
490
|
});
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
491
|
+
//#endregion
|
|
492
|
+
//#region src/rules/no-deprecated-properties.ts
|
|
493
|
+
/** Deprecated in v3, still present at runtime. */
|
|
494
|
+
const V3_DEPRECATED = /* @__PURE__ */ new Map([
|
|
495
|
+
["$parent", "$closest()"],
|
|
496
|
+
["$root", "$closest()"],
|
|
497
|
+
["$children", "$query()"]
|
|
539
498
|
]);
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
499
|
+
/** Removed in v4. Reaching for any of these is a runtime error, not a warning. */
|
|
500
|
+
const V4_REMOVED = /* @__PURE__ */ new Map([
|
|
501
|
+
["$parent", "$closest()"],
|
|
502
|
+
["$root", "$closest()"],
|
|
503
|
+
["$children", "$watchChildren()"],
|
|
504
|
+
["$update", "nothing — $refs and $options read the DOM on every access"],
|
|
505
|
+
["$log", "console.log()"],
|
|
506
|
+
["$terminate", "$unmount()"]
|
|
507
|
+
]);
|
|
508
|
+
/** Methods v4 no longer calls. Defining one is dead code. */
|
|
509
|
+
const V4_REMOVED_METHODS = /* @__PURE__ */ new Map([["updated", "$watchChildren(), or an option<Name>Changed() hook"], ["terminated", "unmounted()"]]);
|
|
510
|
+
/** `$services` survives, but its two switches do not. */
|
|
511
|
+
const V4_REMOVED_SERVICE_METHODS = /* @__PURE__ */ new Map([["enable", "this.$services.<hook>.start()"], ["disable", "this.$services.<hook>.stop()"]]);
|
|
512
|
+
const noDeprecatedProperties = createRule({
|
|
513
|
+
meta: {
|
|
514
|
+
type: "problem",
|
|
515
|
+
docs: { description: "Disallow deprecated Base properties, and the members v4 removed" },
|
|
516
|
+
schema: [{
|
|
517
|
+
type: "object",
|
|
518
|
+
properties: { version: { enum: ["v3", "v4"] } },
|
|
519
|
+
additionalProperties: false
|
|
520
|
+
}],
|
|
521
|
+
messages: {
|
|
522
|
+
deprecated: "\"{{name}}\" is deprecated. Use {{replacement}} instead.",
|
|
523
|
+
removed: "\"{{name}}\" is removed in v4. Use {{replacement}} instead.",
|
|
524
|
+
removedMethod: "\"{{name}}()\" is removed in v4, so this method is never called. Use {{replacement}} instead.",
|
|
525
|
+
removedConfig: "`config.emits` is removed in v4. Declare the events in the `$emits` type of the component instead — nothing of it stays in the bundle."
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
createOnce(context) {
|
|
529
|
+
/**
|
|
530
|
+
* `createOnce` runs once for the whole run, while `context` tracks the file
|
|
531
|
+
* being linted. Options are therefore read per file, at report time.
|
|
532
|
+
*/
|
|
533
|
+
const isV4Mode = () => context.options?.[0]?.version === "v4";
|
|
534
|
+
/**
|
|
535
|
+
* v3 resolves a component class by its `Base` superclass; v4 also writes
|
|
536
|
+
* `extends with<Service>(…)` and `@component({ … })`.
|
|
537
|
+
*/
|
|
538
|
+
function isComponent(node) {
|
|
539
|
+
return isV4Mode() ? isComponentClass(node) : isBaseSubclass(node, context);
|
|
540
|
+
}
|
|
541
|
+
function enclosingComponent(node) {
|
|
542
|
+
const enclosing = findEnclosingClass(getAncestors(node, context));
|
|
543
|
+
return enclosing && isComponent(enclosing) ? enclosing : null;
|
|
544
|
+
}
|
|
545
|
+
return {
|
|
546
|
+
MemberExpression(node) {
|
|
547
|
+
const name = node.property?.name;
|
|
548
|
+
if (!name) return;
|
|
549
|
+
const isV4 = isV4Mode();
|
|
550
|
+
if (isV4 && V4_REMOVED_SERVICE_METHODS.has(name) && node.object?.type === "MemberExpression" && node.object.object?.type === "ThisExpression" && node.object.property?.name === "$services") {
|
|
551
|
+
if (!enclosingComponent(node)) return;
|
|
552
|
+
context.report({
|
|
553
|
+
node,
|
|
554
|
+
messageId: "removed",
|
|
555
|
+
data: {
|
|
556
|
+
name: `$services.${name}`,
|
|
557
|
+
replacement: V4_REMOVED_SERVICE_METHODS.get(name)
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
const members = isV4 ? V4_REMOVED : V3_DEPRECATED;
|
|
563
|
+
if (!members.has(name)) return;
|
|
564
|
+
if (node.object?.type !== "ThisExpression") return;
|
|
565
|
+
if (!enclosingComponent(node)) return;
|
|
566
|
+
context.report({
|
|
567
|
+
node,
|
|
568
|
+
messageId: isV4 ? "removed" : "deprecated",
|
|
569
|
+
data: {
|
|
570
|
+
name,
|
|
571
|
+
replacement: members.get(name)
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
},
|
|
575
|
+
MethodDefinition(node) {
|
|
576
|
+
if (!isV4Mode()) return;
|
|
577
|
+
const name = getKeyName(node);
|
|
578
|
+
if (!name || !V4_REMOVED_METHODS.has(name)) return;
|
|
579
|
+
if (node.static === true) return;
|
|
580
|
+
if (!enclosingComponent(node)) return;
|
|
581
|
+
context.report({
|
|
582
|
+
node: node.key ?? node,
|
|
583
|
+
messageId: "removedMethod",
|
|
584
|
+
data: {
|
|
585
|
+
name,
|
|
586
|
+
replacement: V4_REMOVED_METHODS.get(name)
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
},
|
|
590
|
+
ClassDeclaration(node) {
|
|
591
|
+
checkConfig(node);
|
|
592
|
+
},
|
|
593
|
+
ClassExpression(node) {
|
|
594
|
+
checkConfig(node);
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
function checkConfig(node) {
|
|
598
|
+
if (!isV4Mode() || !isComponent(node)) return;
|
|
599
|
+
const emits = getProperty(findComponentConfig(node), "emits");
|
|
600
|
+
if (!emits) return;
|
|
601
|
+
context.report({
|
|
602
|
+
node: emits,
|
|
603
|
+
messageId: "removedConfig"
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
}
|
|
568
607
|
});
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
);
|
|
590
|
-
if (!isDispatchEvent) return;
|
|
591
|
-
const ancestors = context.getAncestors ? context.getAncestors() : context.sourceCode?.getAncestors?.(node) ?? [];
|
|
592
|
-
const enclosingClass = findEnclosingClass(ancestors);
|
|
593
|
-
if (!enclosingClass || !isBaseSubclass(enclosingClass, context)) return;
|
|
594
|
-
context.report({ node, messageId: "useEmit" });
|
|
595
|
-
}
|
|
596
|
-
};
|
|
597
|
-
}
|
|
608
|
+
//#endregion
|
|
609
|
+
//#region src/rules/no-dispatch-event.ts
|
|
610
|
+
const noDispatchEvent = createRule({
|
|
611
|
+
meta: {
|
|
612
|
+
type: "suggestion",
|
|
613
|
+
docs: { description: "Disallow dispatchEvent() in Base subclasses — use this.$emit() instead" },
|
|
614
|
+
messages: { useEmit: "Use this.$emit() instead of dispatchEvent() to emit events in a Base component." }
|
|
615
|
+
},
|
|
616
|
+
createOnce(context) {
|
|
617
|
+
return { CallExpression(node) {
|
|
618
|
+
const callee = node.callee;
|
|
619
|
+
if (!(callee.type === "MemberExpression" && callee.object?.type === "ThisExpression" && callee.property?.name === "dispatchEvent" || callee.type === "MemberExpression" && callee.property?.name === "dispatchEvent")) return;
|
|
620
|
+
const enclosingClass = findEnclosingClass(context.getAncestors ? context.getAncestors() : context.sourceCode?.getAncestors?.(node) ?? []);
|
|
621
|
+
if (!enclosingClass || !isBaseSubclass(enclosingClass, context)) return;
|
|
622
|
+
context.report({
|
|
623
|
+
node,
|
|
624
|
+
messageId: "useEmit"
|
|
625
|
+
});
|
|
626
|
+
} };
|
|
627
|
+
}
|
|
598
628
|
});
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
const enclosingClass = findEnclosingClass(ancestors);
|
|
620
|
-
if (!enclosingClass || !isBaseSubclass(enclosingClass, context)) return;
|
|
621
|
-
context.report({ node, messageId: "noShadow" });
|
|
622
|
-
}
|
|
623
|
-
};
|
|
624
|
-
}
|
|
629
|
+
//#endregion
|
|
630
|
+
//#region src/rules/no-shadow-dom.ts
|
|
631
|
+
const noShadowDom = createRule({
|
|
632
|
+
meta: {
|
|
633
|
+
type: "problem",
|
|
634
|
+
docs: { description: "Disallow attachShadow() in Base subclasses — js-toolkit uses Light DOM only" },
|
|
635
|
+
messages: { noShadow: "Do not use attachShadow() in a Base component. @studiometa/js-toolkit uses Light DOM only." }
|
|
636
|
+
},
|
|
637
|
+
createOnce(context) {
|
|
638
|
+
return { CallExpression(node) {
|
|
639
|
+
const callee = node.callee;
|
|
640
|
+
if (callee.type !== "MemberExpression" || callee.property?.name !== "attachShadow") return;
|
|
641
|
+
const enclosingClass = findEnclosingClass(context.getAncestors ? context.getAncestors() : context.sourceCode?.getAncestors?.(node) ?? []);
|
|
642
|
+
if (!enclosingClass || !isBaseSubclass(enclosingClass, context)) return;
|
|
643
|
+
context.report({
|
|
644
|
+
node,
|
|
645
|
+
messageId: "noShadow"
|
|
646
|
+
});
|
|
647
|
+
} };
|
|
648
|
+
}
|
|
625
649
|
});
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
context.report({ node, messageId: "useRegister" });
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
};
|
|
647
|
-
}
|
|
650
|
+
//#endregion
|
|
651
|
+
//#region src/rules/no-create-app.ts
|
|
652
|
+
const noCreateApp = createRule({
|
|
653
|
+
meta: {
|
|
654
|
+
type: "suggestion",
|
|
655
|
+
docs: { description: "Disallow createApp() — use registerComponent() instead" },
|
|
656
|
+
messages: { useRegister: "createApp() is deprecated. Use registerComponent() from @studiometa/js-toolkit instead." }
|
|
657
|
+
},
|
|
658
|
+
createOnce(context) {
|
|
659
|
+
return { CallExpression(node) {
|
|
660
|
+
const callee = node.callee;
|
|
661
|
+
if (callee.type === "Identifier" && callee.name === "createApp") context.report({
|
|
662
|
+
node,
|
|
663
|
+
messageId: "useRegister"
|
|
664
|
+
});
|
|
665
|
+
} };
|
|
666
|
+
}
|
|
648
667
|
});
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
}
|
|
679
|
-
};
|
|
680
|
-
}
|
|
668
|
+
//#endregion
|
|
669
|
+
//#region src/rules/no-event-listener-methods.ts
|
|
670
|
+
const FORBIDDEN = /* @__PURE__ */ new Set(["addEventListener", "removeEventListener"]);
|
|
671
|
+
const noEventListenerMethods = createRule({
|
|
672
|
+
meta: {
|
|
673
|
+
type: "problem",
|
|
674
|
+
docs: { description: "Disallow addEventListener/removeEventListener in Base subclasses — use on* methods instead" },
|
|
675
|
+
messages: { useOnMethod: "\"{{method}}()\" should not be used in a Base component. Define an \"on<Target><Event>()\" method instead — the framework handles binding and cleanup automatically." }
|
|
676
|
+
},
|
|
677
|
+
createOnce(context) {
|
|
678
|
+
return { CallExpression(node) {
|
|
679
|
+
const callee = node.callee;
|
|
680
|
+
if (callee.type !== "MemberExpression") return;
|
|
681
|
+
const method = callee.property?.name;
|
|
682
|
+
if (!method || !FORBIDDEN.has(method)) return;
|
|
683
|
+
let root = callee.object;
|
|
684
|
+
while (root?.type === "MemberExpression") root = root.object;
|
|
685
|
+
const isThis = root?.type === "ThisExpression";
|
|
686
|
+
const isGlobal = root?.type === "Identifier" && (root.name === "document" || root.name === "window");
|
|
687
|
+
if (!isThis && !isGlobal) return;
|
|
688
|
+
const enclosingClass = findEnclosingClass(context.getAncestors ? context.getAncestors() : context.sourceCode?.getAncestors?.(node) ?? []);
|
|
689
|
+
if (!enclosingClass || !isBaseSubclass(enclosingClass, context)) return;
|
|
690
|
+
context.report({
|
|
691
|
+
node,
|
|
692
|
+
messageId: "useOnMethod",
|
|
693
|
+
data: { method }
|
|
694
|
+
});
|
|
695
|
+
} };
|
|
696
|
+
}
|
|
681
697
|
});
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
check5(node, context);
|
|
701
|
-
}
|
|
702
|
-
};
|
|
703
|
-
}
|
|
698
|
+
//#endregion
|
|
699
|
+
//#region src/rules/refs-plural-multiple.ts
|
|
700
|
+
const refsPluralMultiple = createRule({
|
|
701
|
+
meta: {
|
|
702
|
+
type: "problem",
|
|
703
|
+
docs: { description: "Require multiple-ref names (using [] suffix) to be pluralized" },
|
|
704
|
+
messages: { notPlural: "Multiple ref \"{{name}}\" must be pluralized (e.g. \"{{name}}s[]\")." }
|
|
705
|
+
},
|
|
706
|
+
createOnce(context) {
|
|
707
|
+
return {
|
|
708
|
+
ClassDeclaration(node) {
|
|
709
|
+
check$5(node, context);
|
|
710
|
+
},
|
|
711
|
+
ClassExpression(node) {
|
|
712
|
+
check$5(node, context);
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
}
|
|
704
716
|
});
|
|
705
|
-
function
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
context.report({
|
|
724
|
-
node: element,
|
|
725
|
-
messageId: "notPlural",
|
|
726
|
-
data: { name }
|
|
727
|
-
});
|
|
728
|
-
}
|
|
729
|
-
}
|
|
717
|
+
function check$5(node, context) {
|
|
718
|
+
if (!isBaseSubclass(node, context)) return;
|
|
719
|
+
const configProp = (node.body?.body ?? []).find((member) => member.type === "PropertyDefinition" && member.static === true && member.key?.name === "config");
|
|
720
|
+
if (!configProp?.value || configProp.value.type !== "ObjectExpression") return;
|
|
721
|
+
const refsProp = configProp.value.properties?.find((prop) => prop.type === "Property" && prop.key?.name === "refs");
|
|
722
|
+
if (!refsProp?.value || refsProp.value.type !== "ArrayExpression") return;
|
|
723
|
+
for (const element of refsProp.value.elements ?? []) {
|
|
724
|
+
if (!element || element.type !== "Literal") continue;
|
|
725
|
+
const raw = element.value;
|
|
726
|
+
if (typeof raw !== "string") continue;
|
|
727
|
+
if (!raw.endsWith("[]")) continue;
|
|
728
|
+
const name = raw.slice(0, -2);
|
|
729
|
+
if (!name.endsWith("s")) context.report({
|
|
730
|
+
node: element,
|
|
731
|
+
messageId: "notPlural",
|
|
732
|
+
data: { name }
|
|
733
|
+
});
|
|
734
|
+
}
|
|
730
735
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
736
|
+
//#endregion
|
|
737
|
+
//#region src/rules/no-deep-utils-import.ts
|
|
738
|
+
const UTILS_PREFIX = "@studiometa/js-toolkit/utils/";
|
|
739
|
+
/**
|
|
740
|
+
* A published per-symbol subpath: exactly one segment after `utils/`, and no dot in it.
|
|
741
|
+
*
|
|
742
|
+
* Since v3.9.0 every util is exported at `@studiometa/js-toolkit/utils/<name>`. Those subpaths are
|
|
743
|
+
* public API and the cheapest way to import a util — they resolve to the module declaring the symbol,
|
|
744
|
+
* so a consumer served by a CDN downloads that module instead of the whole `utils` barrel. Anything
|
|
745
|
+
* deeper (`utils/math/damp.js`) reaches into the package layout, which is not part of the API and
|
|
746
|
+
* changes without notice.
|
|
747
|
+
*
|
|
748
|
+
* Every key of the `exports` map is a symbol name, so none of them contains a dot; rejecting the dot
|
|
749
|
+
* outright keeps a file path out (`utils/damp.ts`, `utils/foo.css`) whatever its extension.
|
|
750
|
+
*/
|
|
751
|
+
const PUBLIC_SUBPATH_RE = /^[^/.]+$/;
|
|
752
|
+
const noDeepUtilsImport = createRule({
|
|
753
|
+
meta: {
|
|
754
|
+
type: "problem",
|
|
755
|
+
docs: { description: "Disallow importing internal module paths from @studiometa/js-toolkit/utils — use the public per-symbol subpath" },
|
|
756
|
+
messages: { noDeepImport: "Import from \"{{source}}\" reaches into the package layout. Use the public subpath \"@studiometa/js-toolkit/utils/<name>\" for a single util, or \"@studiometa/js-toolkit/utils\" for several." }
|
|
757
|
+
},
|
|
758
|
+
createOnce(context) {
|
|
759
|
+
return { ImportDeclaration(node) {
|
|
760
|
+
const source = node.source.value;
|
|
761
|
+
if (!source.startsWith(UTILS_PREFIX)) return;
|
|
762
|
+
if (PUBLIC_SUBPATH_RE.test(source.slice(29))) return;
|
|
763
|
+
context.report({
|
|
764
|
+
node,
|
|
765
|
+
messageId: "noDeepImport",
|
|
766
|
+
data: { source }
|
|
767
|
+
});
|
|
768
|
+
} };
|
|
769
|
+
}
|
|
755
770
|
});
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
}
|
|
771
|
+
//#endregion
|
|
772
|
+
//#region src/rules/no-internal-barrel-import.ts
|
|
773
|
+
/** A relative import of a barrel: `'./utils/index.js'`, `'../Base/index.js'`. */
|
|
774
|
+
const RELATIVE_BARREL_RE = /^\.{1,2}\/.*\/index\.js$/;
|
|
775
|
+
const noInternalBarrelImport = createRule({
|
|
776
|
+
meta: {
|
|
777
|
+
type: "problem",
|
|
778
|
+
docs: { description: "Disallow importing an internal barrel inside the package — import the declaring module instead" },
|
|
779
|
+
messages: { noBarrelImport: "Import from \"{{source}}\" pulls in every symbol the barrel names. Import the module declaring the symbol instead, so a consumer served by a CDN only downloads what it uses." }
|
|
780
|
+
},
|
|
781
|
+
createOnce(context) {
|
|
782
|
+
return { ImportDeclaration(node) {
|
|
783
|
+
const source = node.source.value;
|
|
784
|
+
if (!RELATIVE_BARREL_RE.test(source)) return;
|
|
785
|
+
context.report({
|
|
786
|
+
node,
|
|
787
|
+
messageId: "noBarrelImport",
|
|
788
|
+
data: { source }
|
|
789
|
+
});
|
|
790
|
+
} };
|
|
791
|
+
}
|
|
778
792
|
});
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
function isThisRefs(node) {
|
|
782
|
-
|
|
793
|
+
//#endregion
|
|
794
|
+
//#region src/rules/refs-no-bracket-access.ts
|
|
795
|
+
function isThisRefs$1(node) {
|
|
796
|
+
return node.type === "MemberExpression" && node.object?.type === "ThisExpression" && node.property?.name === "$refs";
|
|
783
797
|
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
}
|
|
815
|
-
});
|
|
816
|
-
}
|
|
817
|
-
};
|
|
818
|
-
}
|
|
798
|
+
const refsNoBracketAccess = createRule({
|
|
799
|
+
meta: {
|
|
800
|
+
type: "problem",
|
|
801
|
+
fixable: "code",
|
|
802
|
+
docs: { description: "Disallow bracket access with [] suffix on this.$refs" },
|
|
803
|
+
messages: { noBracketAccess: "Use \"this.$refs.{{fixed}}\" instead of \"this.$refs['{{raw}}']\"." }
|
|
804
|
+
},
|
|
805
|
+
createOnce(context) {
|
|
806
|
+
return { MemberExpression(node) {
|
|
807
|
+
if (!isThisRefs$1(node.object)) return;
|
|
808
|
+
if (!node.computed) return;
|
|
809
|
+
const prop = node.property;
|
|
810
|
+
if (prop.type !== "Literal" || typeof prop.value !== "string") return;
|
|
811
|
+
const raw = prop.value;
|
|
812
|
+
if (!raw.endsWith("[]")) return;
|
|
813
|
+
const fixed = toCamelCase(raw.slice(0, -2));
|
|
814
|
+
context.report({
|
|
815
|
+
node,
|
|
816
|
+
messageId: "noBracketAccess",
|
|
817
|
+
data: {
|
|
818
|
+
raw,
|
|
819
|
+
fixed
|
|
820
|
+
},
|
|
821
|
+
fix(fixer) {
|
|
822
|
+
const objText = (context.sourceCode ?? context.getSourceCode?.()).getText(node.object);
|
|
823
|
+
return fixer.replaceText(node, `${objText}.${fixed}`);
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
} };
|
|
827
|
+
}
|
|
819
828
|
});
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
return {
|
|
834
|
-
"ClassDeclaration, ClassExpression"(node) {
|
|
835
|
-
check6(node, context);
|
|
836
|
-
}
|
|
837
|
-
};
|
|
838
|
-
}
|
|
829
|
+
//#endregion
|
|
830
|
+
//#region src/rules/no-redundant-with-mount-when-in-view.ts
|
|
831
|
+
const noRedundantWithMountWhenInView = createRule({
|
|
832
|
+
meta: {
|
|
833
|
+
type: "suggestion",
|
|
834
|
+
docs: { description: "Disallow wrapping withMountWhenInView inside withScrolledInView (redundant)" },
|
|
835
|
+
messages: { redundant: "withScrolledInView already includes withMountWhenInView internally. Remove the inner withMountWhenInView call." }
|
|
836
|
+
},
|
|
837
|
+
createOnce(context) {
|
|
838
|
+
return { "ClassDeclaration, ClassExpression"(node) {
|
|
839
|
+
check$4(node, context);
|
|
840
|
+
} };
|
|
841
|
+
}
|
|
839
842
|
});
|
|
840
|
-
function
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
843
|
+
function check$4(node, context) {
|
|
844
|
+
const superClass = node.superClass;
|
|
845
|
+
if (!superClass || superClass.type !== "CallExpression") return;
|
|
846
|
+
if (superClass.callee?.name !== "withScrolledInView") return;
|
|
847
|
+
const arg = superClass.arguments?.[0];
|
|
848
|
+
if (!arg || arg.type !== "CallExpression") return;
|
|
849
|
+
if (arg.callee?.name !== "withMountWhenInView") return;
|
|
850
|
+
context.report({
|
|
851
|
+
node: superClass,
|
|
852
|
+
messageId: "redundant"
|
|
853
|
+
});
|
|
848
854
|
}
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
855
|
+
//#endregion
|
|
856
|
+
//#region src/rules/prefer-ref-over-query-selector.ts
|
|
857
|
+
const QUERY_METHODS = /* @__PURE__ */ new Set(["querySelector", "querySelectorAll"]);
|
|
852
858
|
function isThisEl(node) {
|
|
853
|
-
|
|
859
|
+
return node.type === "MemberExpression" && node.object?.type === "ThisExpression" && node.property?.name === "$el";
|
|
854
860
|
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
}
|
|
878
|
-
};
|
|
879
|
-
}
|
|
861
|
+
const preferRefOverQuerySelector = createRule({
|
|
862
|
+
meta: {
|
|
863
|
+
type: "suggestion",
|
|
864
|
+
docs: { description: "Prefer this.$refs over this.$el.querySelector() in Base subclasses" },
|
|
865
|
+
messages: { preferRef: "Avoid \"this.$el.{{method}}()\". Declare a ref in static config and use \"this.$refs\" instead." }
|
|
866
|
+
},
|
|
867
|
+
createOnce(context) {
|
|
868
|
+
return { CallExpression(node) {
|
|
869
|
+
const callee = node.callee;
|
|
870
|
+
if (callee?.type !== "MemberExpression") return;
|
|
871
|
+
if (!isThisEl(callee.object)) return;
|
|
872
|
+
const method = callee.property?.name;
|
|
873
|
+
if (!QUERY_METHODS.has(method)) return;
|
|
874
|
+
const cls = findEnclosingClass(context.getAncestors?.() ?? context.sourceCode?.getAncestors?.(node) ?? []);
|
|
875
|
+
if (!cls || !isBaseSubclass(cls, context)) return;
|
|
876
|
+
context.report({
|
|
877
|
+
node,
|
|
878
|
+
messageId: "preferRef",
|
|
879
|
+
data: { method }
|
|
880
|
+
});
|
|
881
|
+
} };
|
|
882
|
+
}
|
|
880
883
|
});
|
|
881
|
-
|
|
882
|
-
|
|
884
|
+
//#endregion
|
|
885
|
+
//#region src/rules/require-refs-declared-in-config.ts
|
|
883
886
|
function collectDeclaredRefs(classNode) {
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
const raw = el.value;
|
|
897
|
-
const stripped = raw.endsWith("[]") ? raw.slice(0, -2) : raw;
|
|
898
|
-
names.add(toCamelCase(stripped));
|
|
899
|
-
}
|
|
900
|
-
return names;
|
|
887
|
+
const configProp = (classNode.body?.body ?? []).find((m) => m.type === "PropertyDefinition" && m.static === true && m.key?.name === "config");
|
|
888
|
+
if (!configProp?.value || configProp.value.type !== "ObjectExpression") return /* @__PURE__ */ new Set();
|
|
889
|
+
const refsProp = configProp.value.properties?.find((p) => p.type === "Property" && p.key?.name === "refs");
|
|
890
|
+
if (!refsProp?.value || refsProp.value.type !== "ArrayExpression") return /* @__PURE__ */ new Set();
|
|
891
|
+
const names = /* @__PURE__ */ new Set();
|
|
892
|
+
for (const el of refsProp.value.elements ?? []) {
|
|
893
|
+
if (!el || el.type !== "Literal" || typeof el.value !== "string") continue;
|
|
894
|
+
const raw = el.value;
|
|
895
|
+
const stripped = raw.endsWith("[]") ? raw.slice(0, -2) : raw;
|
|
896
|
+
names.add(toCamelCase(stripped));
|
|
897
|
+
}
|
|
898
|
+
return names;
|
|
901
899
|
}
|
|
902
|
-
function
|
|
903
|
-
|
|
900
|
+
function isThisRefs(node) {
|
|
901
|
+
return node.type === "MemberExpression" && node.object?.type === "ThisExpression" && node.property?.name === "$refs";
|
|
904
902
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
context.report({
|
|
933
|
-
node,
|
|
934
|
-
messageId: "undeclared",
|
|
935
|
-
data: { name: refName }
|
|
936
|
-
});
|
|
937
|
-
}
|
|
938
|
-
});
|
|
939
|
-
}
|
|
940
|
-
};
|
|
941
|
-
}
|
|
903
|
+
const requireRefsDeclaredInConfig = createRule({
|
|
904
|
+
meta: {
|
|
905
|
+
type: "problem",
|
|
906
|
+
docs: { description: "Require all this.$refs accesses to be declared in static config.refs" },
|
|
907
|
+
messages: { undeclared: "Ref \"{{name}}\" is not declared in static config.refs." }
|
|
908
|
+
},
|
|
909
|
+
createOnce(context) {
|
|
910
|
+
return { "ClassDeclaration, ClassExpression"(classNode) {
|
|
911
|
+
if (!isBaseSubclass(classNode, context)) return;
|
|
912
|
+
const declared = collectDeclaredRefs(classNode);
|
|
913
|
+
walkNode$3(classNode.body, (node) => {
|
|
914
|
+
if (node.type !== "MemberExpression") return;
|
|
915
|
+
if (!isThisRefs(node.object)) return;
|
|
916
|
+
let refName = null;
|
|
917
|
+
if (!node.computed && node.property?.type === "Identifier") refName = node.property.name;
|
|
918
|
+
else if (node.computed && node.property?.type === "Literal" && typeof node.property.value === "string") {
|
|
919
|
+
const raw = node.property.value;
|
|
920
|
+
refName = toCamelCase(raw.endsWith("[]") ? raw.slice(0, -2) : raw);
|
|
921
|
+
}
|
|
922
|
+
if (refName && !declared.has(refName)) context.report({
|
|
923
|
+
node,
|
|
924
|
+
messageId: "undeclared",
|
|
925
|
+
data: { name: refName }
|
|
926
|
+
});
|
|
927
|
+
});
|
|
928
|
+
} };
|
|
929
|
+
}
|
|
942
930
|
});
|
|
943
|
-
function walkNode(node, visit) {
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
walkNode(child, visit);
|
|
953
|
-
}
|
|
954
|
-
}
|
|
931
|
+
function walkNode$3(node, visit) {
|
|
932
|
+
if (!node || typeof node !== "object") return;
|
|
933
|
+
visit(node);
|
|
934
|
+
for (const key of Object.keys(node)) {
|
|
935
|
+
if (key === "parent") continue;
|
|
936
|
+
const child = node[key];
|
|
937
|
+
if (Array.isArray(child)) for (const item of child) walkNode$3(item, visit);
|
|
938
|
+
else if (child && typeof child === "object" && child.type) walkNode$3(child, visit);
|
|
939
|
+
}
|
|
955
940
|
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
context.report({ node, messageId: "noManual" });
|
|
976
|
-
}
|
|
977
|
-
};
|
|
978
|
-
}
|
|
941
|
+
//#endregion
|
|
942
|
+
//#region src/rules/no-manual-intersection-observer.ts
|
|
943
|
+
const noManualIntersectionObserver = createRule({
|
|
944
|
+
meta: {
|
|
945
|
+
type: "suggestion",
|
|
946
|
+
docs: { description: "Disallow manual IntersectionObserver inside Base subclasses; use withIntersectionObserver or withMountWhenInView instead" },
|
|
947
|
+
messages: { noManual: "Avoid manual \"new IntersectionObserver()\". Use \"withIntersectionObserver\" or \"withMountWhenInView\" decorators instead." }
|
|
948
|
+
},
|
|
949
|
+
createOnce(context) {
|
|
950
|
+
return { NewExpression(node) {
|
|
951
|
+
if (node.callee?.name !== "IntersectionObserver") return;
|
|
952
|
+
const cls = findEnclosingClass(context.getAncestors?.() ?? context.sourceCode?.getAncestors?.(node) ?? []);
|
|
953
|
+
if (!cls || !isBaseSubclass(cls, context)) return;
|
|
954
|
+
context.report({
|
|
955
|
+
node,
|
|
956
|
+
messageId: "noManual"
|
|
957
|
+
});
|
|
958
|
+
} };
|
|
959
|
+
}
|
|
979
960
|
});
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
context.report({ node, messageId: "noManual" });
|
|
1000
|
-
}
|
|
1001
|
-
};
|
|
1002
|
-
}
|
|
961
|
+
//#endregion
|
|
962
|
+
//#region src/rules/no-manual-mutation-observer.ts
|
|
963
|
+
const noManualMutationObserver = createRule({
|
|
964
|
+
meta: {
|
|
965
|
+
type: "suggestion",
|
|
966
|
+
docs: { description: "Disallow manual MutationObserver inside Base subclasses; use withMutation decorator instead" },
|
|
967
|
+
messages: { noManual: "Avoid manual \"new MutationObserver()\". Use the \"withMutation\" decorator instead." }
|
|
968
|
+
},
|
|
969
|
+
createOnce(context) {
|
|
970
|
+
return { NewExpression(node) {
|
|
971
|
+
if (node.callee?.name !== "MutationObserver") return;
|
|
972
|
+
const cls = findEnclosingClass(context.getAncestors?.() ?? context.sourceCode?.getAncestors?.(node) ?? []);
|
|
973
|
+
if (!cls || !isBaseSubclass(cls, context)) return;
|
|
974
|
+
context.report({
|
|
975
|
+
node,
|
|
976
|
+
messageId: "noManual"
|
|
977
|
+
});
|
|
978
|
+
} };
|
|
979
|
+
}
|
|
1003
980
|
});
|
|
1004
|
-
|
|
1005
|
-
|
|
981
|
+
//#endregion
|
|
982
|
+
//#region src/rules/emits-kebab-case.ts
|
|
983
|
+
/**
|
|
984
|
+
* Returns true if the string is kebab-case:
|
|
985
|
+
* all lowercase letters, digits and hyphens, no underscores or uppercase.
|
|
986
|
+
*/
|
|
1006
987
|
function isKebabCase(value) {
|
|
1007
|
-
|
|
988
|
+
return /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(value);
|
|
1008
989
|
}
|
|
990
|
+
/**
|
|
991
|
+
* Converts a camelCase, PascalCase or snake_case string to kebab-case.
|
|
992
|
+
*/
|
|
1009
993
|
function toKebabCase(value) {
|
|
1010
|
-
|
|
994
|
+
return value.replace(/([A-Z])/g, (_, c) => `-${c.toLowerCase()}`).replace(/_/g, "-").replace(/^-/, "").toLowerCase();
|
|
1011
995
|
}
|
|
1012
|
-
function
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
data: { name },
|
|
1033
|
-
fix: (fixer) => fixer.replaceText(element, `'${fixed}'`)
|
|
1034
|
-
});
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
996
|
+
function check$3(node, context) {
|
|
997
|
+
if (!isBaseSubclass(node, context)) return;
|
|
998
|
+
const configProp = (node.body?.body ?? []).find((member) => member.type === "PropertyDefinition" && member.static === true && member.key?.name === "config");
|
|
999
|
+
if (!configProp?.value || configProp.value.type !== "ObjectExpression") return;
|
|
1000
|
+
const emitsProp = configProp.value.properties?.find((prop) => prop.type === "Property" && prop.key?.name === "emits");
|
|
1001
|
+
if (!emitsProp?.value || emitsProp.value.type !== "ArrayExpression") return;
|
|
1002
|
+
for (const element of emitsProp.value.elements ?? []) {
|
|
1003
|
+
if (!element || element.type !== "Literal") continue;
|
|
1004
|
+
const name = element.value;
|
|
1005
|
+
if (typeof name !== "string") continue;
|
|
1006
|
+
if (!isKebabCase(name)) {
|
|
1007
|
+
const fixed = toKebabCase(name);
|
|
1008
|
+
context.report({
|
|
1009
|
+
node: element,
|
|
1010
|
+
messageId: "notKebabCase",
|
|
1011
|
+
data: { name },
|
|
1012
|
+
fix: (fixer) => fixer.replaceText(element, `'${fixed}'`)
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1037
1016
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
check7(node, context);
|
|
1056
|
-
}
|
|
1057
|
-
};
|
|
1058
|
-
}
|
|
1017
|
+
const emitsKebabCase = createRule({
|
|
1018
|
+
meta: {
|
|
1019
|
+
type: "problem",
|
|
1020
|
+
fixable: "code",
|
|
1021
|
+
docs: { description: "Require emit names in static config to be kebab-case" },
|
|
1022
|
+
messages: { notKebabCase: "Emit name \"{{name}}\" must be kebab-case (e.g. \"content-change\")." }
|
|
1023
|
+
},
|
|
1024
|
+
createOnce(context) {
|
|
1025
|
+
return {
|
|
1026
|
+
ClassDeclaration(node) {
|
|
1027
|
+
check$3(node, context);
|
|
1028
|
+
},
|
|
1029
|
+
ClassExpression(node) {
|
|
1030
|
+
check$3(node, context);
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1059
1034
|
});
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
function
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
context.report({
|
|
1079
|
-
node: element,
|
|
1080
|
-
messageId: "singleWord",
|
|
1081
|
-
data: { name }
|
|
1082
|
-
});
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1035
|
+
//#endregion
|
|
1036
|
+
//#region src/rules/emits-multi-word.ts
|
|
1037
|
+
function check$2(node, context) {
|
|
1038
|
+
if (!isBaseSubclass(node, context)) return;
|
|
1039
|
+
const configProp = (node.body?.body ?? []).find((member) => member.type === "PropertyDefinition" && member.static === true && member.key?.name === "config");
|
|
1040
|
+
if (!configProp?.value || configProp.value.type !== "ObjectExpression") return;
|
|
1041
|
+
const emitsProp = configProp.value.properties?.find((prop) => prop.type === "Property" && prop.key?.name === "emits");
|
|
1042
|
+
if (!emitsProp?.value || emitsProp.value.type !== "ArrayExpression") return;
|
|
1043
|
+
for (const element of emitsProp.value.elements ?? []) {
|
|
1044
|
+
if (!element || element.type !== "Literal") continue;
|
|
1045
|
+
const name = element.value;
|
|
1046
|
+
if (typeof name !== "string") continue;
|
|
1047
|
+
if (!name.includes("-")) context.report({
|
|
1048
|
+
node: element,
|
|
1049
|
+
messageId: "singleWord",
|
|
1050
|
+
data: { name }
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1085
1053
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
check8(node, context);
|
|
1103
|
-
}
|
|
1104
|
-
};
|
|
1105
|
-
}
|
|
1054
|
+
const emitsMultiWord = createRule({
|
|
1055
|
+
meta: {
|
|
1056
|
+
type: "problem",
|
|
1057
|
+
docs: { description: "Require emit names in static config to be multi-word to avoid collision with native DOM events" },
|
|
1058
|
+
messages: { singleWord: "Emit name \"{{name}}\" must be multi-word (e.g. \"item-click\" instead of \"click\") to avoid collisions with native DOM events." }
|
|
1059
|
+
},
|
|
1060
|
+
createOnce(context) {
|
|
1061
|
+
return {
|
|
1062
|
+
ClassDeclaration(node) {
|
|
1063
|
+
check$2(node, context);
|
|
1064
|
+
},
|
|
1065
|
+
ClassExpression(node) {
|
|
1066
|
+
check$2(node, context);
|
|
1067
|
+
}
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1106
1070
|
});
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
function
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
data: { name },
|
|
1131
|
-
fix: (fixer) => fixer.replaceText(prop.key, fixedText)
|
|
1132
|
-
});
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1071
|
+
//#endregion
|
|
1072
|
+
//#region src/rules/components-pascal-case.ts
|
|
1073
|
+
function check$1(node, context) {
|
|
1074
|
+
if (!isBaseSubclass(node, context)) return;
|
|
1075
|
+
const configProp = (node.body?.body ?? []).find((member) => member.type === "PropertyDefinition" && member.static === true && member.key?.name === "config");
|
|
1076
|
+
if (!configProp?.value || configProp.value.type !== "ObjectExpression") return;
|
|
1077
|
+
const componentsProp = configProp.value.properties?.find((prop) => prop.type === "Property" && prop.key?.name === "components");
|
|
1078
|
+
if (!componentsProp?.value || componentsProp.value.type !== "ObjectExpression") return;
|
|
1079
|
+
for (const prop of componentsProp.value.properties ?? []) {
|
|
1080
|
+
if (prop.type !== "Property") continue;
|
|
1081
|
+
const name = prop.key?.name ?? prop.key?.value;
|
|
1082
|
+
if (typeof name !== "string") continue;
|
|
1083
|
+
if (!isPascalCase(name)) {
|
|
1084
|
+
const fixed = toPascalCase(name);
|
|
1085
|
+
const fixedText = prop.key.type === "Literal" ? `'${fixed}'` : fixed;
|
|
1086
|
+
context.report({
|
|
1087
|
+
node: prop.key,
|
|
1088
|
+
messageId: "notPascalCase",
|
|
1089
|
+
data: { name },
|
|
1090
|
+
fix: (fixer) => fixer.replaceText(prop.key, fixedText)
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1135
1094
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
check9(node, context);
|
|
1154
|
-
}
|
|
1155
|
-
};
|
|
1156
|
-
}
|
|
1095
|
+
const componentsPascalCase = createRule({
|
|
1096
|
+
meta: {
|
|
1097
|
+
type: "problem",
|
|
1098
|
+
fixable: "code",
|
|
1099
|
+
docs: { description: "Require component keys in static config to be PascalCase" },
|
|
1100
|
+
messages: { notPascalCase: "Component key \"{{name}}\" must be PascalCase (e.g. \"MyComponent\")." }
|
|
1101
|
+
},
|
|
1102
|
+
createOnce(context) {
|
|
1103
|
+
return {
|
|
1104
|
+
ClassDeclaration(node) {
|
|
1105
|
+
check$1(node, context);
|
|
1106
|
+
},
|
|
1107
|
+
ClassExpression(node) {
|
|
1108
|
+
check$1(node, context);
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1157
1112
|
});
|
|
1158
|
-
|
|
1159
|
-
|
|
1113
|
+
//#endregion
|
|
1114
|
+
//#region src/rules/require-emit-declared-in-config.ts
|
|
1160
1115
|
function collectDeclaredEmits(classNode) {
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
);
|
|
1169
|
-
if (!emitsProp?.value || emitsProp.value.type !== "ArrayExpression") return /* @__PURE__ */ new Set();
|
|
1170
|
-
const names = /* @__PURE__ */ new Set();
|
|
1171
|
-
for (const el of emitsProp.value.elements ?? []) {
|
|
1172
|
-
if (el?.type === "Literal" && typeof el.value === "string") {
|
|
1173
|
-
names.add(el.value);
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
return names;
|
|
1116
|
+
const configProp = (classNode.body?.body ?? []).find((m) => m.type === "PropertyDefinition" && m.static === true && m.key?.name === "config");
|
|
1117
|
+
if (!configProp?.value || configProp.value.type !== "ObjectExpression") return /* @__PURE__ */ new Set();
|
|
1118
|
+
const emitsProp = configProp.value.properties?.find((p) => p.type === "Property" && p.key?.name === "emits");
|
|
1119
|
+
if (!emitsProp?.value || emitsProp.value.type !== "ArrayExpression") return /* @__PURE__ */ new Set();
|
|
1120
|
+
const names = /* @__PURE__ */ new Set();
|
|
1121
|
+
for (const el of emitsProp.value.elements ?? []) if (el?.type === "Literal" && typeof el.value === "string") names.add(el.value);
|
|
1122
|
+
return names;
|
|
1177
1123
|
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Returns true if `node` looks like `this.$emit`.
|
|
1126
|
+
*/
|
|
1178
1127
|
function isThisEmit(node) {
|
|
1179
|
-
|
|
1128
|
+
return node.type === "MemberExpression" && node.object?.type === "ThisExpression" && node.property?.name === "$emit";
|
|
1180
1129
|
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
messageId: "undeclared",
|
|
1207
|
-
data: { name }
|
|
1208
|
-
});
|
|
1209
|
-
}
|
|
1210
|
-
});
|
|
1211
|
-
}
|
|
1212
|
-
};
|
|
1213
|
-
}
|
|
1130
|
+
const requireEmitDeclaredInConfig = createRule({
|
|
1131
|
+
meta: {
|
|
1132
|
+
type: "problem",
|
|
1133
|
+
docs: { description: "Require all this.$emit() calls to use event names declared in static config.emits" },
|
|
1134
|
+
messages: { undeclared: "Emit \"{{name}}\" is not declared in static config.emits." }
|
|
1135
|
+
},
|
|
1136
|
+
createOnce(context) {
|
|
1137
|
+
return { "ClassDeclaration, ClassExpression"(classNode) {
|
|
1138
|
+
if (!isBaseSubclass(classNode, context)) return;
|
|
1139
|
+
const declared = collectDeclaredEmits(classNode);
|
|
1140
|
+
walkNode$2(classNode.body, (node) => {
|
|
1141
|
+
if (node.type !== "CallExpression") return;
|
|
1142
|
+
if (!isThisEmit(node.callee)) return;
|
|
1143
|
+
const firstArg = node.arguments?.[0];
|
|
1144
|
+
if (!firstArg || firstArg.type !== "Literal") return;
|
|
1145
|
+
const name = firstArg.value;
|
|
1146
|
+
if (typeof name !== "string") return;
|
|
1147
|
+
if (!declared.has(name)) context.report({
|
|
1148
|
+
node: firstArg,
|
|
1149
|
+
messageId: "undeclared",
|
|
1150
|
+
data: { name }
|
|
1151
|
+
});
|
|
1152
|
+
});
|
|
1153
|
+
} };
|
|
1154
|
+
}
|
|
1214
1155
|
});
|
|
1215
|
-
function
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
walkNode2(child, visit);
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1156
|
+
function walkNode$2(node, visit) {
|
|
1157
|
+
if (!node || typeof node !== "object") return;
|
|
1158
|
+
visit(node);
|
|
1159
|
+
for (const key of Object.keys(node)) {
|
|
1160
|
+
if (key === "parent") continue;
|
|
1161
|
+
const child = node[key];
|
|
1162
|
+
if (Array.isArray(child)) for (const item of child) walkNode$2(item, visit);
|
|
1163
|
+
else if (child && typeof child === "object" && child.type) walkNode$2(child, visit);
|
|
1164
|
+
}
|
|
1227
1165
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1166
|
+
//#endregion
|
|
1167
|
+
//#region src/rules/require-children-declared-in-config.ts
|
|
1230
1168
|
function collectDeclaredComponents(classNode) {
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
if (prop.type !== "Property") continue;
|
|
1243
|
-
const name = prop.key?.name ?? prop.key?.value;
|
|
1244
|
-
if (typeof name === "string") {
|
|
1245
|
-
names.add(name);
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
return names;
|
|
1169
|
+
const configProp = (classNode.body?.body ?? []).find((m) => m.type === "PropertyDefinition" && m.static === true && m.key?.name === "config");
|
|
1170
|
+
if (!configProp?.value || configProp.value.type !== "ObjectExpression") return /* @__PURE__ */ new Set();
|
|
1171
|
+
const componentsProp = configProp.value.properties?.find((p) => p.type === "Property" && p.key?.name === "components");
|
|
1172
|
+
if (!componentsProp?.value || componentsProp.value.type !== "ObjectExpression") return /* @__PURE__ */ new Set();
|
|
1173
|
+
const names = /* @__PURE__ */ new Set();
|
|
1174
|
+
for (const prop of componentsProp.value.properties ?? []) {
|
|
1175
|
+
if (prop.type !== "Property") continue;
|
|
1176
|
+
const name = prop.key?.name ?? prop.key?.value;
|
|
1177
|
+
if (typeof name === "string") names.add(name);
|
|
1178
|
+
}
|
|
1179
|
+
return names;
|
|
1249
1180
|
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Returns true if `node` looks like `this.$children`.
|
|
1183
|
+
*/
|
|
1250
1184
|
function isThisChildren(node) {
|
|
1251
|
-
|
|
1185
|
+
return node.type === "MemberExpression" && node.object?.type === "ThisExpression" && node.property?.name === "$children";
|
|
1252
1186
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
if (name && !declared.has(name)) {
|
|
1278
|
-
context.report({
|
|
1279
|
-
node,
|
|
1280
|
-
messageId: "undeclared",
|
|
1281
|
-
data: { name }
|
|
1282
|
-
});
|
|
1283
|
-
}
|
|
1284
|
-
});
|
|
1285
|
-
}
|
|
1286
|
-
};
|
|
1287
|
-
}
|
|
1187
|
+
const requireChildrenDeclaredInConfig = createRule({
|
|
1188
|
+
meta: {
|
|
1189
|
+
type: "problem",
|
|
1190
|
+
docs: { description: "Require all this.$children accesses to use component names declared in static config.components" },
|
|
1191
|
+
messages: { undeclared: "Component \"{{name}}\" is not declared in static config.components." }
|
|
1192
|
+
},
|
|
1193
|
+
createOnce(context) {
|
|
1194
|
+
return { "ClassDeclaration, ClassExpression"(classNode) {
|
|
1195
|
+
if (!isBaseSubclass(classNode, context)) return;
|
|
1196
|
+
const declared = collectDeclaredComponents(classNode);
|
|
1197
|
+
walkNode$1(classNode.body, (node) => {
|
|
1198
|
+
if (node.type !== "MemberExpression") return;
|
|
1199
|
+
if (!isThisChildren(node.object)) return;
|
|
1200
|
+
let name = null;
|
|
1201
|
+
if (!node.computed && node.property?.type === "Identifier") name = node.property.name;
|
|
1202
|
+
else if (node.computed && node.property?.type === "Literal" && typeof node.property.value === "string") name = node.property.value;
|
|
1203
|
+
if (name && !declared.has(name)) context.report({
|
|
1204
|
+
node,
|
|
1205
|
+
messageId: "undeclared",
|
|
1206
|
+
data: { name }
|
|
1207
|
+
});
|
|
1208
|
+
});
|
|
1209
|
+
} };
|
|
1210
|
+
}
|
|
1288
1211
|
});
|
|
1289
|
-
function
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
walkNode3(child, visit);
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1212
|
+
function walkNode$1(node, visit) {
|
|
1213
|
+
if (!node || typeof node !== "object") return;
|
|
1214
|
+
visit(node);
|
|
1215
|
+
for (const key of Object.keys(node)) {
|
|
1216
|
+
if (key === "parent") continue;
|
|
1217
|
+
const child = node[key];
|
|
1218
|
+
if (Array.isArray(child)) for (const item of child) walkNode$1(item, visit);
|
|
1219
|
+
else if (child && typeof child === "object" && child.type) walkNode$1(child, visit);
|
|
1220
|
+
}
|
|
1301
1221
|
}
|
|
1302
|
-
|
|
1303
|
-
|
|
1222
|
+
//#endregion
|
|
1223
|
+
//#region src/rules/require-options-declared-in-config.ts
|
|
1304
1224
|
function collectDeclaredOptions(classNode) {
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
if (prop.type !== "Property") continue;
|
|
1317
|
-
const name = prop.key?.name ?? prop.key?.value;
|
|
1318
|
-
if (typeof name === "string") {
|
|
1319
|
-
names.add(toCamelCase(name));
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
return names;
|
|
1225
|
+
const configProp = (classNode.body?.body ?? []).find((m) => m.type === "PropertyDefinition" && m.static === true && m.key?.name === "config");
|
|
1226
|
+
if (!configProp?.value || configProp.value.type !== "ObjectExpression") return /* @__PURE__ */ new Set();
|
|
1227
|
+
const optionsProp = configProp.value.properties?.find((p) => p.type === "Property" && p.key?.name === "options");
|
|
1228
|
+
if (!optionsProp?.value || optionsProp.value.type !== "ObjectExpression") return /* @__PURE__ */ new Set();
|
|
1229
|
+
const names = /* @__PURE__ */ new Set();
|
|
1230
|
+
for (const prop of optionsProp.value.properties ?? []) {
|
|
1231
|
+
if (prop.type !== "Property") continue;
|
|
1232
|
+
const name = prop.key?.name ?? prop.key?.value;
|
|
1233
|
+
if (typeof name === "string") names.add(toCamelCase(name));
|
|
1234
|
+
}
|
|
1235
|
+
return names;
|
|
1323
1236
|
}
|
|
1237
|
+
/**
|
|
1238
|
+
* Returns true if `node` looks like `this.$options`.
|
|
1239
|
+
*/
|
|
1324
1240
|
function isThisOptions(node) {
|
|
1325
|
-
|
|
1241
|
+
return node.type === "MemberExpression" && node.object?.type === "ThisExpression" && node.property?.name === "$options";
|
|
1326
1242
|
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
if (name && !declared.has(name)) {
|
|
1352
|
-
context.report({
|
|
1353
|
-
node,
|
|
1354
|
-
messageId: "undeclared",
|
|
1355
|
-
data: { name }
|
|
1356
|
-
});
|
|
1357
|
-
}
|
|
1358
|
-
});
|
|
1359
|
-
}
|
|
1360
|
-
};
|
|
1361
|
-
}
|
|
1243
|
+
const requireOptionsDeclaredInConfig = createRule({
|
|
1244
|
+
meta: {
|
|
1245
|
+
type: "problem",
|
|
1246
|
+
docs: { description: "Require all this.$options accesses to use option names declared in static config.options" },
|
|
1247
|
+
messages: { undeclared: "Option \"{{name}}\" is not declared in static config.options." }
|
|
1248
|
+
},
|
|
1249
|
+
createOnce(context) {
|
|
1250
|
+
return { "ClassDeclaration, ClassExpression"(classNode) {
|
|
1251
|
+
if (!isBaseSubclass(classNode, context)) return;
|
|
1252
|
+
const declared = collectDeclaredOptions(classNode);
|
|
1253
|
+
walkNode(classNode.body, (node) => {
|
|
1254
|
+
if (node.type !== "MemberExpression") return;
|
|
1255
|
+
if (!isThisOptions(node.object)) return;
|
|
1256
|
+
let name = null;
|
|
1257
|
+
if (!node.computed && node.property?.type === "Identifier") name = node.property.name;
|
|
1258
|
+
else if (node.computed && node.property?.type === "Literal" && typeof node.property.value === "string") name = toCamelCase(node.property.value);
|
|
1259
|
+
if (name && !declared.has(name)) context.report({
|
|
1260
|
+
node,
|
|
1261
|
+
messageId: "undeclared",
|
|
1262
|
+
data: { name }
|
|
1263
|
+
});
|
|
1264
|
+
});
|
|
1265
|
+
} };
|
|
1266
|
+
}
|
|
1362
1267
|
});
|
|
1363
|
-
function
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
walkNode4(child, visit);
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1268
|
+
function walkNode(node, visit) {
|
|
1269
|
+
if (!node || typeof node !== "object") return;
|
|
1270
|
+
visit(node);
|
|
1271
|
+
for (const key of Object.keys(node)) {
|
|
1272
|
+
if (key === "parent") continue;
|
|
1273
|
+
const child = node[key];
|
|
1274
|
+
if (Array.isArray(child)) for (const item of child) walkNode(item, visit);
|
|
1275
|
+
else if (child && typeof child === "object" && child.type) walkNode(child, visit);
|
|
1276
|
+
}
|
|
1375
1277
|
}
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1278
|
+
//#endregion
|
|
1279
|
+
//#region src/rules/prefer-destructured-lookups.ts
|
|
1280
|
+
const TRACKED_PROPERTIES = /* @__PURE__ */ new Set([
|
|
1281
|
+
"$refs",
|
|
1282
|
+
"$options",
|
|
1283
|
+
"$children"
|
|
1284
|
+
]);
|
|
1379
1285
|
function isTrackedLookup(node) {
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1286
|
+
if (node.type === "MemberExpression" && node.object?.type === "MemberExpression" && node.object.object?.type === "ThisExpression" && node.object.property?.type === "Identifier" && TRACKED_PROPERTIES.has(node.object.property.name) && node.property?.type === "Identifier" && !node.computed) return {
|
|
1287
|
+
prop: node.object.property.name,
|
|
1288
|
+
name: node.property.name
|
|
1289
|
+
};
|
|
1290
|
+
return null;
|
|
1384
1291
|
}
|
|
1385
1292
|
function findEnclosingMethod(ancestors) {
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1293
|
+
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
1294
|
+
const node = ancestors[i];
|
|
1295
|
+
if (node.type === "MethodDefinition") return node;
|
|
1296
|
+
}
|
|
1297
|
+
return null;
|
|
1391
1298
|
}
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1299
|
+
const preferDestructuredLookups = createRule({
|
|
1300
|
+
meta: {
|
|
1301
|
+
type: "suggestion",
|
|
1302
|
+
docs: { description: "Prefer destructuring this.$refs, this.$options and this.$children into local variables when accessed multiple times in the same method" },
|
|
1303
|
+
messages: { preferDestructured: "\"this.{{prop}}.{{name}}\" is accessed {{count}} times. Destructure it into a local variable to avoid repeated lookups." }
|
|
1304
|
+
},
|
|
1305
|
+
createOnce(context) {
|
|
1306
|
+
const methodLookups = /* @__PURE__ */ new Map();
|
|
1307
|
+
return {
|
|
1308
|
+
MemberExpression(node) {
|
|
1309
|
+
const match = isTrackedLookup(node);
|
|
1310
|
+
if (!match) return;
|
|
1311
|
+
const ancestors = context.getAncestors?.() ?? context.sourceCode?.getAncestors?.(node) ?? [];
|
|
1312
|
+
const method = findEnclosingMethod(ancestors);
|
|
1313
|
+
if (!method) return;
|
|
1314
|
+
const cls = findEnclosingClass(ancestors);
|
|
1315
|
+
if (!cls || !isBaseSubclass(cls, context)) return;
|
|
1316
|
+
if (!methodLookups.has(method)) methodLookups.set(method, /* @__PURE__ */ new Map());
|
|
1317
|
+
const map = methodLookups.get(method);
|
|
1318
|
+
const key = `${match.prop}.${match.name}`;
|
|
1319
|
+
const list = map.get(key) ?? [];
|
|
1320
|
+
list.push(node);
|
|
1321
|
+
map.set(key, list);
|
|
1322
|
+
},
|
|
1323
|
+
"MethodDefinition:exit"(node) {
|
|
1324
|
+
const map = methodLookups.get(node);
|
|
1325
|
+
if (!map) return;
|
|
1326
|
+
methodLookups.delete(node);
|
|
1327
|
+
for (const [key, nodes] of map) {
|
|
1328
|
+
if (nodes.length < 2) continue;
|
|
1329
|
+
const [prop, name] = key.split(".");
|
|
1330
|
+
context.report({
|
|
1331
|
+
node: nodes[0],
|
|
1332
|
+
messageId: "preferDestructured",
|
|
1333
|
+
data: {
|
|
1334
|
+
prop,
|
|
1335
|
+
name,
|
|
1336
|
+
count: String(nodes.length)
|
|
1337
|
+
}
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1436
1343
|
});
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
check10(node, context);
|
|
1456
|
-
}
|
|
1457
|
-
};
|
|
1458
|
-
}
|
|
1344
|
+
//#endregion
|
|
1345
|
+
//#region src/rules/no-dollar-prefix.ts
|
|
1346
|
+
const noDollarPrefix = createRule({
|
|
1347
|
+
meta: {
|
|
1348
|
+
type: "problem",
|
|
1349
|
+
docs: { description: "Disallow user-defined methods and properties prefixed with \"$\" in Base subclasses — \"$\" is reserved for js-toolkit framework members" },
|
|
1350
|
+
messages: { noDollarPrefix: "\"${{name}}\" uses the \"$\" prefix, which is reserved for js-toolkit framework members. Rename it without the \"$\" prefix." }
|
|
1351
|
+
},
|
|
1352
|
+
createOnce(context) {
|
|
1353
|
+
return {
|
|
1354
|
+
ClassDeclaration(node) {
|
|
1355
|
+
check(node, context);
|
|
1356
|
+
},
|
|
1357
|
+
ClassExpression(node) {
|
|
1358
|
+
check(node, context);
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1459
1362
|
});
|
|
1460
|
-
function
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1363
|
+
function check(node, context) {
|
|
1364
|
+
if (!isBaseSubclass(node, context)) return;
|
|
1365
|
+
for (const member of node.body?.body ?? []) {
|
|
1366
|
+
if (member.static) continue;
|
|
1367
|
+
if (member.type !== "MethodDefinition" && member.type !== "PropertyDefinition") continue;
|
|
1368
|
+
const name = member.key?.name ?? "";
|
|
1369
|
+
if (name.startsWith("$")) context.report({
|
|
1370
|
+
node: member.key,
|
|
1371
|
+
messageId: "noDollarPrefix",
|
|
1372
|
+
data: { name: name.slice(1) }
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1474
1375
|
}
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1376
|
+
//#endregion
|
|
1377
|
+
//#region src/rules/require-destroyed-cleanup.ts
|
|
1378
|
+
const SETUP_CALLS = /* @__PURE__ */ new Set([
|
|
1379
|
+
"setTimeout",
|
|
1380
|
+
"setInterval",
|
|
1381
|
+
"requestAnimationFrame"
|
|
1382
|
+
]);
|
|
1383
|
+
const requireDestroyedCleanup = createRule({
|
|
1384
|
+
meta: {
|
|
1385
|
+
type: "suggestion",
|
|
1386
|
+
docs: { description: "Require a destroyed() method in Base subclasses that use setTimeout, setInterval or requestAnimationFrame" },
|
|
1387
|
+
messages: { requireDestroyed: "This component uses {{calls}} but has no destroyed() method. Add destroyed() to clear timers and avoid memory leaks." }
|
|
1388
|
+
},
|
|
1389
|
+
createOnce(context) {
|
|
1390
|
+
const classTimers = /* @__PURE__ */ new Map();
|
|
1391
|
+
const classHasDestroyed = /* @__PURE__ */ new Map();
|
|
1392
|
+
return {
|
|
1393
|
+
ClassDeclaration(node) {
|
|
1394
|
+
classTimers.set(node, /* @__PURE__ */ new Set());
|
|
1395
|
+
classHasDestroyed.set(node, false);
|
|
1396
|
+
},
|
|
1397
|
+
ClassExpression(node) {
|
|
1398
|
+
classTimers.set(node, /* @__PURE__ */ new Set());
|
|
1399
|
+
classHasDestroyed.set(node, false);
|
|
1400
|
+
},
|
|
1401
|
+
MethodDefinition(node) {
|
|
1402
|
+
if (node.key?.name === "destroyed") {
|
|
1403
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode?.();
|
|
1404
|
+
const ancestors = context.getAncestors?.() ?? sourceCode?.getAncestors?.(node) ?? [];
|
|
1405
|
+
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
1406
|
+
const a = ancestors[i];
|
|
1407
|
+
if (a.type === "ClassDeclaration" || a.type === "ClassExpression") {
|
|
1408
|
+
classHasDestroyed.set(a, true);
|
|
1409
|
+
break;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
},
|
|
1414
|
+
CallExpression(node) {
|
|
1415
|
+
if (node.callee?.type !== "Identifier") return;
|
|
1416
|
+
if (!SETUP_CALLS.has(node.callee.name)) return;
|
|
1417
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode?.();
|
|
1418
|
+
const ancestors = context.getAncestors?.() ?? sourceCode?.getAncestors?.(node) ?? [];
|
|
1419
|
+
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
1420
|
+
const a = ancestors[i];
|
|
1421
|
+
if (a.type === "ClassDeclaration" || a.type === "ClassExpression") {
|
|
1422
|
+
if (!isBaseSubclass(a, context)) break;
|
|
1423
|
+
classTimers.get(a)?.add(node.callee.name);
|
|
1424
|
+
break;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
},
|
|
1428
|
+
"ClassDeclaration:exit"(node) {
|
|
1429
|
+
reportIfNeeded(node, context, classTimers, classHasDestroyed);
|
|
1430
|
+
},
|
|
1431
|
+
"ClassExpression:exit"(node) {
|
|
1432
|
+
reportIfNeeded(node, context, classTimers, classHasDestroyed);
|
|
1433
|
+
}
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1535
1436
|
});
|
|
1536
1437
|
function reportIfNeeded(node, context, classTimers, classHasDestroyed) {
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1438
|
+
const timers = classTimers.get(node);
|
|
1439
|
+
const hasDestroyed = classHasDestroyed.get(node);
|
|
1440
|
+
classTimers.delete(node);
|
|
1441
|
+
classHasDestroyed.delete(node);
|
|
1442
|
+
if (!timers || timers.size === 0) return;
|
|
1443
|
+
if (hasDestroyed) return;
|
|
1444
|
+
context.report({
|
|
1445
|
+
node: node.id ?? node,
|
|
1446
|
+
messageId: "requireDestroyed",
|
|
1447
|
+
data: { calls: [...timers].join(", ") }
|
|
1448
|
+
});
|
|
1548
1449
|
}
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1450
|
+
//#endregion
|
|
1451
|
+
//#region src/rules/no-write-in-read-phase.ts
|
|
1452
|
+
/**
|
|
1453
|
+
* Hooks whose service fans its subscribers out inside `defaultScheduler.read()`.
|
|
1454
|
+
*
|
|
1455
|
+
* `raf.ts`, `scroll.ts` and `scroll-progress.ts` all coalesce into the read
|
|
1456
|
+
* phase. The other services — resize, pointer, key, mutation, in-view — emit
|
|
1457
|
+
* straight from their listener or observer, in no phase at all, so their hooks
|
|
1458
|
+
* are not read-phase contexts and are deliberately absent.
|
|
1459
|
+
*/
|
|
1460
|
+
const READ_PHASE_HOOKS = /* @__PURE__ */ new Set([
|
|
1461
|
+
"ticked",
|
|
1462
|
+
"scrolled",
|
|
1463
|
+
"scrolledInView"
|
|
1464
|
+
]);
|
|
1465
|
+
/** Services whose `subscribe()` callback runs in the read phase. */
|
|
1466
|
+
const READ_PHASE_SERVICES = /* @__PURE__ */ new Set([
|
|
1467
|
+
"useRaf",
|
|
1468
|
+
"useScroll",
|
|
1469
|
+
"useWindowScroll",
|
|
1470
|
+
"useScrollProgress"
|
|
1471
|
+
]);
|
|
1472
|
+
/** A method carrying either of these already names its own phase. */
|
|
1473
|
+
const PHASE_DECORATORS = /* @__PURE__ */ new Set(["read", "write"]);
|
|
1474
|
+
/** Properties whose assignment mutates the DOM. */
|
|
1475
|
+
const WRITE_PROPERTIES = /* @__PURE__ */ new Set([
|
|
1476
|
+
"textContent",
|
|
1477
|
+
"innerHTML",
|
|
1478
|
+
"className",
|
|
1479
|
+
"scrollTop",
|
|
1480
|
+
"scrollLeft",
|
|
1481
|
+
"value"
|
|
1482
|
+
]);
|
|
1483
|
+
/** Methods which mutate the DOM. */
|
|
1484
|
+
const WRITE_METHODS = /* @__PURE__ */ new Set([
|
|
1485
|
+
"setProperty",
|
|
1486
|
+
"setAttribute",
|
|
1487
|
+
"removeAttribute",
|
|
1488
|
+
"toggleAttribute",
|
|
1489
|
+
"append",
|
|
1490
|
+
"prepend",
|
|
1491
|
+
"remove",
|
|
1492
|
+
"replaceChildren",
|
|
1493
|
+
"insertAdjacentHTML",
|
|
1494
|
+
"scrollTo",
|
|
1495
|
+
"scrollIntoView",
|
|
1496
|
+
"animate"
|
|
1497
|
+
]);
|
|
1498
|
+
/** Globals a component may reach the DOM through. */
|
|
1499
|
+
const DOM_GLOBALS = /* @__PURE__ */ new Set(["document", "window"]);
|
|
1500
|
+
/**
|
|
1501
|
+
* Splits a member expression into the expression it starts from and the chain
|
|
1502
|
+
* of property names read off it. A computed property contributes `null`.
|
|
1503
|
+
*/
|
|
1504
|
+
function memberPath(node) {
|
|
1505
|
+
const path = [];
|
|
1506
|
+
let current = node;
|
|
1507
|
+
while (current?.type === "MemberExpression") {
|
|
1508
|
+
path.unshift(current.computed ? null : current.property?.name ?? null);
|
|
1509
|
+
current = current.object;
|
|
1510
|
+
}
|
|
1511
|
+
return {
|
|
1512
|
+
base: current ?? null,
|
|
1513
|
+
path
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
/**
|
|
1517
|
+
* Returns true if an expression reaches the DOM from one of the roots a v4
|
|
1518
|
+
* component owns: `this.$el`, `this.$refs.*`, `document`, `window`, or a local
|
|
1519
|
+
* initialised from one of those.
|
|
1520
|
+
*
|
|
1521
|
+
* Anchoring the receiver is what makes a type-free rule accurate enough to
|
|
1522
|
+
* turn on: without it, every `foo.value = 1` in the file would be a candidate.
|
|
1523
|
+
*/
|
|
1524
|
+
function isAnchored(node, aliases) {
|
|
1525
|
+
if (!node) return false;
|
|
1526
|
+
if (node.type === "CallExpression") return isAnchored(node.callee, aliases);
|
|
1527
|
+
const { base, path } = memberPath(node);
|
|
1528
|
+
if (!base) return false;
|
|
1529
|
+
if (base.type === "ThisExpression") return path[0] === "$el" || path[0] === "$refs";
|
|
1530
|
+
if (base.type === "Identifier") return DOM_GLOBALS.has(base.name) || aliases.has(base.name);
|
|
1531
|
+
return false;
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Names the DOM write an assignment performs, or null if it is not one.
|
|
1535
|
+
*/
|
|
1536
|
+
function assignmentWrite(node, aliases) {
|
|
1537
|
+
const left = node.left;
|
|
1538
|
+
if (left?.type !== "MemberExpression") return null;
|
|
1539
|
+
if (!isAnchored(left, aliases)) return null;
|
|
1540
|
+
const { path } = memberPath(left);
|
|
1541
|
+
const last = path[path.length - 1];
|
|
1542
|
+
if (path.includes("style")) return "style";
|
|
1543
|
+
if (typeof last === "string" && WRITE_PROPERTIES.has(last)) return last;
|
|
1544
|
+
return null;
|
|
1545
|
+
}
|
|
1546
|
+
/**
|
|
1547
|
+
* Names the DOM write a call performs, or null if it is not one.
|
|
1548
|
+
*/
|
|
1549
|
+
function callWrite(node, aliases) {
|
|
1550
|
+
const callee = node.callee;
|
|
1551
|
+
if (callee?.type !== "MemberExpression") return null;
|
|
1552
|
+
if (!isAnchored(callee, aliases)) return null;
|
|
1553
|
+
const { path } = memberPath(callee);
|
|
1554
|
+
const last = path[path.length - 1];
|
|
1555
|
+
if (path.includes("classList")) return "classList";
|
|
1556
|
+
if (typeof last === "string" && WRITE_METHODS.has(last)) return `${last}()`;
|
|
1557
|
+
return null;
|
|
1558
|
+
}
|
|
1559
|
+
/**
|
|
1560
|
+
* Collects, in one pass over the file, the names a member path may be anchored
|
|
1561
|
+
* to and the names holding a `smoothTo()` value.
|
|
1562
|
+
*
|
|
1563
|
+
* A pre-pass rather than incremental collection, so that a field declared after
|
|
1564
|
+
* the method using it is still resolved.
|
|
1565
|
+
*/
|
|
1566
|
+
function collectNames(program) {
|
|
1567
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
1568
|
+
const smooth = /* @__PURE__ */ new Set();
|
|
1569
|
+
const candidates = [];
|
|
1570
|
+
walk(program, (node) => {
|
|
1571
|
+
let name = null;
|
|
1572
|
+
let init = null;
|
|
1573
|
+
if (node.type === "VariableDeclarator" && node.id?.type === "Identifier") {
|
|
1574
|
+
name = node.id.name;
|
|
1575
|
+
init = node.init;
|
|
1576
|
+
} else if (node.type === "PropertyDefinition") {
|
|
1577
|
+
name = getKeyName(node);
|
|
1578
|
+
init = node.value;
|
|
1579
|
+
}
|
|
1580
|
+
if (!name || !init) return;
|
|
1581
|
+
if (init.type === "CallExpression" && init.callee?.type === "Identifier") {
|
|
1582
|
+
if (init.callee.name === "smoothTo") smooth.add(name);
|
|
1583
|
+
}
|
|
1584
|
+
candidates.push({
|
|
1585
|
+
name,
|
|
1586
|
+
init
|
|
1587
|
+
});
|
|
1588
|
+
});
|
|
1589
|
+
let changed = true;
|
|
1590
|
+
while (changed) {
|
|
1591
|
+
changed = false;
|
|
1592
|
+
for (const { name, init } of candidates) {
|
|
1593
|
+
if (aliases.has(name)) continue;
|
|
1594
|
+
if (isAnchored(init, aliases)) {
|
|
1595
|
+
aliases.add(name);
|
|
1596
|
+
changed = true;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
return {
|
|
1601
|
+
aliases,
|
|
1602
|
+
smooth
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Returns true if `object` is something whose `subscribe()` callback runs in
|
|
1607
|
+
* the read phase.
|
|
1608
|
+
*/
|
|
1609
|
+
function isReadPhaseSubscribable(object, smooth) {
|
|
1610
|
+
if (!object) return false;
|
|
1611
|
+
if (object.type === "CallExpression" && object.callee?.type === "Identifier") return READ_PHASE_SERVICES.has(object.callee.name);
|
|
1612
|
+
if (object.type === "MemberExpression" && object.object?.type === "ThisExpression" && !object.computed) return smooth.has(object.property?.name);
|
|
1613
|
+
if (object.type === "Identifier") return smooth.has(object.name);
|
|
1614
|
+
return false;
|
|
1615
|
+
}
|
|
1616
|
+
/** Returns true for `this.$read(fn)` and `defaultScheduler.read(fn)`. */
|
|
1617
|
+
function isReadScheduling(callee) {
|
|
1618
|
+
const name = callee?.property?.name;
|
|
1619
|
+
if (name === "$read" && callee.object?.type === "ThisExpression") return true;
|
|
1620
|
+
return name === "read" && callee.object?.type === "Identifier" && callee.object.name === "defaultScheduler";
|
|
1621
|
+
}
|
|
1622
|
+
const noWriteInReadPhase = createRule({
|
|
1623
|
+
meta: {
|
|
1624
|
+
type: "problem",
|
|
1625
|
+
docs: { description: "Disallow DOM writes in a read-phase callback — return a render function instead" },
|
|
1626
|
+
messages: {
|
|
1627
|
+
writeInReadPhase: "This writes the DOM ({{write}}) in the read phase, which forces layout for every component measuring in the same frame. Return a render function, or move the write into this.$write().",
|
|
1628
|
+
writeInHelper: "This writes the DOM ({{write}}) in the read phase, through {{helper}}(). Return a render function, or mark {{helper}}() with @write."
|
|
1629
|
+
}
|
|
1630
|
+
},
|
|
1631
|
+
createOnce(context) {
|
|
1632
|
+
let aliases = /* @__PURE__ */ new Set();
|
|
1633
|
+
let smooth = /* @__PURE__ */ new Set();
|
|
1634
|
+
/** One frame per function scope. Nesting resets the armed flag by design. */
|
|
1635
|
+
const scopes = [];
|
|
1636
|
+
const classes = [];
|
|
1637
|
+
const reported = /* @__PURE__ */ new Set();
|
|
1638
|
+
const isArmed = () => scopes[scopes.length - 1] === true;
|
|
1639
|
+
/**
|
|
1640
|
+
* Decides whether entering this function enters a read-phase context.
|
|
1641
|
+
*/
|
|
1642
|
+
function isReadPhaseEntry(node) {
|
|
1643
|
+
const ancestors = getAncestors(node, context);
|
|
1644
|
+
const parent = ancestors[ancestors.length - 1];
|
|
1645
|
+
if (!parent) return false;
|
|
1646
|
+
if (parent.type === "MethodDefinition" && parent.value === node) {
|
|
1647
|
+
const name = getKeyName(parent);
|
|
1648
|
+
if (!name || !READ_PHASE_HOOKS.has(name)) return false;
|
|
1649
|
+
const enclosing = findEnclosingClass(ancestors);
|
|
1650
|
+
return Boolean(enclosing && isComponentClass(enclosing));
|
|
1651
|
+
}
|
|
1652
|
+
if (parent.type !== "CallExpression") return false;
|
|
1653
|
+
const callee = parent.callee;
|
|
1654
|
+
if (callee?.type !== "MemberExpression") return false;
|
|
1655
|
+
if (isReadScheduling(callee) && parent.arguments?.includes(node)) return true;
|
|
1656
|
+
if (callee.property?.name === "subscribe" && parent.arguments?.[0] === node) return isReadPhaseSubscribable(callee.object, smooth);
|
|
1657
|
+
return false;
|
|
1658
|
+
}
|
|
1659
|
+
/**
|
|
1660
|
+
* Resolves `this.<name>(…)` one level and reports the writes of that method.
|
|
1661
|
+
*
|
|
1662
|
+
* One level only, and never through a method naming its own phase with
|
|
1663
|
+
* `@read` or `@write` — which is how `Cursor`'s `@write render()` stays
|
|
1664
|
+
* correct while still being called from a frame subscriber.
|
|
1665
|
+
*/
|
|
1666
|
+
function descend(node) {
|
|
1667
|
+
const callee = node.callee;
|
|
1668
|
+
if (callee?.type !== "MemberExpression") return;
|
|
1669
|
+
if (callee.computed || callee.object?.type !== "ThisExpression") return;
|
|
1670
|
+
const name = callee.property?.name;
|
|
1671
|
+
if (typeof name !== "string" || name.startsWith("$")) return;
|
|
1672
|
+
const enclosing = classes[classes.length - 1];
|
|
1673
|
+
if (!enclosing) return;
|
|
1674
|
+
const method = (enclosing.body?.body ?? []).find((member) => member.type === "MethodDefinition" && member.kind === "method" && getKeyName(member) === name);
|
|
1675
|
+
if (!method || hasDecorator(method, PHASE_DECORATORS)) return;
|
|
1676
|
+
walk(method.value?.body, (inner) => {
|
|
1677
|
+
if (isFunctionNode(inner)) return false;
|
|
1678
|
+
const write = inner.type === "AssignmentExpression" ? assignmentWrite(inner, aliases) : inner.type === "CallExpression" ? callWrite(inner, aliases) : null;
|
|
1679
|
+
if (write && !reported.has(inner)) {
|
|
1680
|
+
reported.add(inner);
|
|
1681
|
+
context.report({
|
|
1682
|
+
node: inner,
|
|
1683
|
+
messageId: "writeInHelper",
|
|
1684
|
+
data: {
|
|
1685
|
+
write,
|
|
1686
|
+
helper: name
|
|
1687
|
+
}
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
function report(node, write) {
|
|
1693
|
+
if (reported.has(node)) return;
|
|
1694
|
+
reported.add(node);
|
|
1695
|
+
context.report({
|
|
1696
|
+
node,
|
|
1697
|
+
messageId: "writeInReadPhase",
|
|
1698
|
+
data: { write }
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
return {
|
|
1702
|
+
Program(node) {
|
|
1703
|
+
const collected = collectNames(node);
|
|
1704
|
+
aliases = collected.aliases;
|
|
1705
|
+
smooth = collected.smooth;
|
|
1706
|
+
scopes.length = 0;
|
|
1707
|
+
classes.length = 0;
|
|
1708
|
+
reported.clear();
|
|
1709
|
+
},
|
|
1710
|
+
ClassDeclaration(node) {
|
|
1711
|
+
classes.push(node);
|
|
1712
|
+
},
|
|
1713
|
+
"ClassDeclaration:exit"() {
|
|
1714
|
+
classes.pop();
|
|
1715
|
+
},
|
|
1716
|
+
ClassExpression(node) {
|
|
1717
|
+
classes.push(node);
|
|
1718
|
+
},
|
|
1719
|
+
"ClassExpression:exit"() {
|
|
1720
|
+
classes.pop();
|
|
1721
|
+
},
|
|
1722
|
+
FunctionDeclaration(node) {
|
|
1723
|
+
scopes.push(isReadPhaseEntry(node));
|
|
1724
|
+
},
|
|
1725
|
+
"FunctionDeclaration:exit"() {
|
|
1726
|
+
scopes.pop();
|
|
1727
|
+
},
|
|
1728
|
+
FunctionExpression(node) {
|
|
1729
|
+
scopes.push(isReadPhaseEntry(node));
|
|
1730
|
+
},
|
|
1731
|
+
"FunctionExpression:exit"() {
|
|
1732
|
+
scopes.pop();
|
|
1733
|
+
},
|
|
1734
|
+
ArrowFunctionExpression(node) {
|
|
1735
|
+
scopes.push(isReadPhaseEntry(node));
|
|
1736
|
+
},
|
|
1737
|
+
"ArrowFunctionExpression:exit"() {
|
|
1738
|
+
scopes.pop();
|
|
1739
|
+
},
|
|
1740
|
+
AssignmentExpression(node) {
|
|
1741
|
+
if (!isArmed()) return;
|
|
1742
|
+
const write = assignmentWrite(node, aliases);
|
|
1743
|
+
if (write) report(node, write);
|
|
1744
|
+
},
|
|
1745
|
+
CallExpression(node) {
|
|
1746
|
+
if (!isArmed()) return;
|
|
1747
|
+
const write = callWrite(node, aliases);
|
|
1748
|
+
if (write) {
|
|
1749
|
+
report(node, write);
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
descend(node);
|
|
1753
|
+
}
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
});
|
|
1757
|
+
//#endregion
|
|
1758
|
+
//#region src/rules/no-options-assignment.ts
|
|
1759
|
+
/**
|
|
1760
|
+
* Returns true if an expression is `this.$options`, or a path reaching into it.
|
|
1761
|
+
*/
|
|
1762
|
+
function isOptionsPath(node) {
|
|
1763
|
+
let current = node;
|
|
1764
|
+
while (current?.type === "MemberExpression") {
|
|
1765
|
+
if (current.object?.type === "ThisExpression" && !current.computed && current.property?.name === "$options") return true;
|
|
1766
|
+
current = current.object;
|
|
1767
|
+
}
|
|
1768
|
+
return false;
|
|
1769
|
+
}
|
|
1770
|
+
const noOptionsAssignment = createRule({
|
|
1771
|
+
meta: {
|
|
1772
|
+
type: "problem",
|
|
1773
|
+
docs: { description: "Disallow writing to this.$options — it is a read-only view of the attributes" },
|
|
1774
|
+
messages: {
|
|
1775
|
+
assignment: "`$options` is a read-only view of the element attributes: every property is a getter with no setter, so this throws at runtime. Write the attribute instead, with setAttribute().",
|
|
1776
|
+
viewAssignment: "`$options` is defined non-writable, so replacing the view throws at runtime — and everything else in the framework would go on reading the original. Write the attributes instead, with setAttribute().",
|
|
1777
|
+
objectAssign: "Object.assign() onto `$options` writes properties which are getters with no setter, so this throws at runtime. Write the attributes instead, with setAttribute()."
|
|
1778
|
+
}
|
|
1779
|
+
},
|
|
1780
|
+
createOnce(context) {
|
|
1781
|
+
function inComponent(node) {
|
|
1782
|
+
const enclosing = findEnclosingClass(getAncestors(node, context));
|
|
1783
|
+
return Boolean(enclosing && isComponentClass(enclosing));
|
|
1784
|
+
}
|
|
1785
|
+
return {
|
|
1786
|
+
AssignmentExpression(node) {
|
|
1787
|
+
const left = node.left;
|
|
1788
|
+
if (left?.type !== "MemberExpression") return;
|
|
1789
|
+
if (!isOptionsPath(left)) return;
|
|
1790
|
+
if (!inComponent(node)) return;
|
|
1791
|
+
const isWholeView = left.object?.type === "ThisExpression" && left.property?.name === "$options";
|
|
1792
|
+
context.report({
|
|
1793
|
+
node,
|
|
1794
|
+
messageId: isWholeView ? "viewAssignment" : "assignment"
|
|
1795
|
+
});
|
|
1796
|
+
},
|
|
1797
|
+
CallExpression(node) {
|
|
1798
|
+
const callee = node.callee;
|
|
1799
|
+
if (callee?.type !== "MemberExpression") return;
|
|
1800
|
+
if (callee.object?.type !== "Identifier" || callee.object.name !== "Object") return;
|
|
1801
|
+
if (callee.property?.name !== "assign") return;
|
|
1802
|
+
const target = node.arguments?.[0];
|
|
1803
|
+
if (!target || target.type !== "MemberExpression") return;
|
|
1804
|
+
if (!isOptionsPath(target)) return;
|
|
1805
|
+
if (!inComponent(node)) return;
|
|
1806
|
+
context.report({
|
|
1807
|
+
node,
|
|
1808
|
+
messageId: "objectAssign"
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1814
|
+
//#endregion
|
|
1815
|
+
//#region src/rules/prefer-instance-scheduler.ts
|
|
1816
|
+
/** The phases a component owns an instance-scoped equivalent of. */
|
|
1817
|
+
const PHASES = /* @__PURE__ */ new Map([["read", "$read"], ["write", "$write"]]);
|
|
1818
|
+
const preferInstanceScheduler = createRule({
|
|
1819
|
+
meta: {
|
|
1820
|
+
type: "suggestion",
|
|
1821
|
+
docs: { description: "Prefer this.$read()/this.$write() over defaultScheduler inside a component" },
|
|
1822
|
+
messages: { preferInstance: "Use `this.{{instance}}()` rather than `defaultScheduler.{{phase}}()` inside a component. The instance form ties the task to this component, so destroying it cancels the task instead of running it against a detached element." }
|
|
1823
|
+
},
|
|
1824
|
+
createOnce(context) {
|
|
1825
|
+
return { CallExpression(node) {
|
|
1826
|
+
const callee = node.callee;
|
|
1827
|
+
if (callee?.type !== "MemberExpression" || callee.computed) return;
|
|
1828
|
+
if (callee.object?.type !== "Identifier" || callee.object.name !== "defaultScheduler") return;
|
|
1829
|
+
const phase = callee.property?.name;
|
|
1830
|
+
const instance = PHASES.get(phase);
|
|
1831
|
+
if (!instance) return;
|
|
1832
|
+
const enclosing = findEnclosingClass(getAncestors(node, context));
|
|
1833
|
+
if (!enclosing || !isComponentClass(enclosing)) return;
|
|
1834
|
+
context.report({
|
|
1835
|
+
node,
|
|
1836
|
+
messageId: "preferInstance",
|
|
1837
|
+
data: {
|
|
1838
|
+
phase,
|
|
1839
|
+
instance
|
|
1840
|
+
}
|
|
1841
|
+
});
|
|
1842
|
+
} };
|
|
1843
|
+
}
|
|
1844
|
+
});
|
|
1845
|
+
//#endregion
|
|
1846
|
+
//#region src/rules/option-default-factory.ts
|
|
1847
|
+
const optionDefaultFactory = createRule({
|
|
1848
|
+
meta: {
|
|
1849
|
+
type: "problem",
|
|
1850
|
+
docs: { description: "Require a factory for an option default which is an object or an array" },
|
|
1851
|
+
messages: { literalDefault: "Option \"{{name}}\" declares a literal {{kind}} default, which every instance of this component then shares — one instance mutating it mutates it for all of them. Declare it as a factory: `default: () => ({{example}})`." }
|
|
1852
|
+
},
|
|
1853
|
+
createOnce(context) {
|
|
1854
|
+
function check(node) {
|
|
1855
|
+
const options = getProperty(findComponentConfig(node), "options");
|
|
1856
|
+
if (options?.type !== "ObjectExpression") return;
|
|
1857
|
+
for (const property of options.properties ?? []) {
|
|
1858
|
+
if (property.type !== "Property") continue;
|
|
1859
|
+
const name = getKeyName(property);
|
|
1860
|
+
if (!name || property.value?.type !== "ObjectExpression") continue;
|
|
1861
|
+
const fallback = getProperty(property.value, "default");
|
|
1862
|
+
if (!fallback) continue;
|
|
1863
|
+
if (fallback.type === "ObjectExpression") context.report({
|
|
1864
|
+
node: fallback,
|
|
1865
|
+
messageId: "literalDefault",
|
|
1866
|
+
data: {
|
|
1867
|
+
name,
|
|
1868
|
+
kind: "object",
|
|
1869
|
+
example: "{}"
|
|
1870
|
+
}
|
|
1871
|
+
});
|
|
1872
|
+
else if (fallback.type === "ArrayExpression") context.report({
|
|
1873
|
+
node: fallback,
|
|
1874
|
+
messageId: "literalDefault",
|
|
1875
|
+
data: {
|
|
1876
|
+
name,
|
|
1877
|
+
kind: "array",
|
|
1878
|
+
example: "[]"
|
|
1879
|
+
}
|
|
1880
|
+
});
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
return {
|
|
1884
|
+
ClassDeclaration: check,
|
|
1885
|
+
ClassExpression: check
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
});
|
|
1889
|
+
//#endregion
|
|
1890
|
+
//#region src/rules/no-conflicting-negated-option.ts
|
|
1891
|
+
/**
|
|
1892
|
+
* Returns true if an option declaration can hold a boolean, in any of the
|
|
1893
|
+
* spellings v4 accepts: the shorthand `Boolean`, a union `[Boolean, String]`,
|
|
1894
|
+
* or the long form `{ type: Boolean }`.
|
|
1895
|
+
*/
|
|
1896
|
+
function isBooleanOption(value) {
|
|
1897
|
+
if (!value) return false;
|
|
1898
|
+
if (value.type === "Identifier") return value.name === "Boolean";
|
|
1899
|
+
if (value.type === "ArrayExpression") return (value.elements ?? []).some((element) => element?.type === "Identifier" && element.name === "Boolean");
|
|
1900
|
+
if (value.type === "ObjectExpression") return isBooleanOption(getProperty(value, "type"));
|
|
1901
|
+
return false;
|
|
1902
|
+
}
|
|
1903
|
+
const noConflictingNegatedOption = createRule({
|
|
1904
|
+
meta: {
|
|
1905
|
+
type: "problem",
|
|
1906
|
+
docs: { description: "Disallow declaring both a boolean option and its negated spelling" },
|
|
1907
|
+
messages: { conflict: "Option \"{{negated}}\" claims `data-option-no-{{attribute}}`, which is also how the boolean option \"{{name}}\" is turned off. One attribute would mean two things. Rename one of them." }
|
|
1908
|
+
},
|
|
1909
|
+
createOnce(context) {
|
|
1910
|
+
function check(node) {
|
|
1911
|
+
const options = getProperty(findComponentConfig(node), "options");
|
|
1912
|
+
if (options?.type !== "ObjectExpression") return;
|
|
1913
|
+
const declared = /* @__PURE__ */ new Map();
|
|
1914
|
+
for (const property of options.properties ?? []) {
|
|
1915
|
+
if (property.type !== "Property") continue;
|
|
1916
|
+
const name = getKeyName(property);
|
|
1917
|
+
if (name) declared.set(name, property);
|
|
1918
|
+
}
|
|
1919
|
+
for (const [name, property] of declared) {
|
|
1920
|
+
if (!isBooleanOption(property.value)) continue;
|
|
1921
|
+
const negated = `no${toPascalCase(name)}`;
|
|
1922
|
+
const conflicting = declared.get(negated);
|
|
1923
|
+
if (!conflicting) continue;
|
|
1924
|
+
context.report({
|
|
1925
|
+
node: conflicting.key ?? conflicting,
|
|
1926
|
+
messageId: "conflict",
|
|
1927
|
+
data: {
|
|
1928
|
+
name,
|
|
1929
|
+
negated,
|
|
1930
|
+
attribute: name.replaceAll(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()
|
|
1931
|
+
}
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
return {
|
|
1936
|
+
ClassDeclaration: check,
|
|
1937
|
+
ClassExpression: check
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
});
|
|
1941
|
+
//#endregion
|
|
1942
|
+
//#region src/rules/no-destroy-lifecycle.ts
|
|
1943
|
+
/**
|
|
1944
|
+
* v4 renamed `$destroy()` to `$unmount()` and `destroyed()` to `unmounted()`.
|
|
1945
|
+
*
|
|
1946
|
+
* There is no destroyed state in v4: `#isMounted` is one boolean with two
|
|
1947
|
+
* values, and the method documented itself as "the reversible inverse of
|
|
1948
|
+
* `$mount()`". `$mount()`/`$unmount()` and `mounted()`/`unmounted()` are
|
|
1949
|
+
* symmetric pairs where `$mount()`/`$destroy()` was not, which is the same
|
|
1950
|
+
* rename Vue 3 made to `beforeDestroy`/`destroyed`.
|
|
1951
|
+
*
|
|
1952
|
+
* ## Why this is its own rule and not an entry in `no-deprecated-properties`
|
|
1953
|
+
*
|
|
1954
|
+
* Three reasons, and the first is the one that decides it.
|
|
1955
|
+
*
|
|
1956
|
+
* **`no-deprecated-properties` only flags a member expression whose object is
|
|
1957
|
+
* `this`.** That guard is deliberate — `$parent` and `$root` are ordinary
|
|
1958
|
+
* enough words that flagging `someLibrary.$parent` would be noise. But most
|
|
1959
|
+
* `$destroy()` calls are `instance.$destroy()`: from a registry, from a test,
|
|
1960
|
+
* from application code holding an instance. Folding the rename in would
|
|
1961
|
+
* therefore miss the common case, and lifting the guard for one entry would
|
|
1962
|
+
* lift it for every entry, which is exactly the false positive that guard
|
|
1963
|
+
* exists to prevent.
|
|
1964
|
+
*
|
|
1965
|
+
* **`no-deprecated-properties` is not fixable, and should not become so.**
|
|
1966
|
+
* `$parent` → `$closest()` and `$children` → `$watchChildren()` are not safe
|
|
1967
|
+
* textual rewrites; they change the shape of the call and often the logic
|
|
1968
|
+
* around it. A rename is the one case where a fixer is exactly right, and a
|
|
1969
|
+
* rule that is fixable for one of its eleven reports is worse than two rules.
|
|
1970
|
+
*
|
|
1971
|
+
* **The two rules say different things.** `no-deprecated-properties` reports
|
|
1972
|
+
* v3 names that v4 dropped. `$destroy` was never a v3-only name: it is v4's
|
|
1973
|
+
* own, renamed while v4 is unreleased. This rule's whole lifetime is that
|
|
1974
|
+
* rename window, and it is meant to be deleted, not grown.
|
|
1975
|
+
*
|
|
1976
|
+
* ## What is flagged, and what is not
|
|
1977
|
+
*
|
|
1978
|
+
* The two halves are deliberately asymmetric, because the two names are.
|
|
1979
|
+
*
|
|
1980
|
+
* `$destroy` is flagged on **any** receiver — `this.$destroy()`,
|
|
1981
|
+
* `instance.$destroy()`, `super.$destroy()` — and on a `$destroy()` method
|
|
1982
|
+
* definition, which is the shape a service mixin overriding the framework's
|
|
1983
|
+
* teardown writes. The `$` prefix is the toolkit's own namespace, and the rule
|
|
1984
|
+
* ships in `configs.v4` only, which a project turns on when it is on v4. That
|
|
1985
|
+
* opt-in is the guard against rewriting an unrelated library's API.
|
|
1986
|
+
*
|
|
1987
|
+
* `destroyed` has no such prefix — `emitter.destroyed`, `record.destroyed`
|
|
1988
|
+
* and a `destroyed` boolean are all ordinary code — so that half is narrowed
|
|
1989
|
+
* to the two places where it can only mean the hook: a non-static
|
|
1990
|
+
* `destroyed()` method definition in a class that reads as a v4 component,
|
|
1991
|
+
* and a `super.destroyed()` call inside one. A plain `foo.destroyed()` call is
|
|
1992
|
+
* left alone.
|
|
1993
|
+
*
|
|
1994
|
+
* Computed access (`instance['$destroy']()`) is not flagged. It is rare, and a
|
|
1995
|
+
* fixer would have to guess the quote style of a string it did not write.
|
|
1996
|
+
*/
|
|
1997
|
+
const noDestroyLifecycle = createRule({
|
|
1998
|
+
meta: {
|
|
1999
|
+
type: "problem",
|
|
2000
|
+
fixable: "code",
|
|
2001
|
+
docs: { description: "Require the v4 names `$unmount()` and `unmounted()` over `$destroy()` and `destroyed()`" },
|
|
2002
|
+
messages: {
|
|
2003
|
+
renamedMethod: "`$destroy()` is named `$unmount()` in v4.",
|
|
2004
|
+
renamedHook: "The `destroyed()` hook is named `unmounted()` in v4."
|
|
2005
|
+
}
|
|
2006
|
+
},
|
|
2007
|
+
createOnce(context) {
|
|
2008
|
+
function inComponent(node) {
|
|
2009
|
+
const enclosing = findEnclosingClass(getAncestors(node, context));
|
|
2010
|
+
return Boolean(enclosing) && isComponentClass(enclosing);
|
|
2011
|
+
}
|
|
2012
|
+
function rename(target, messageId, to) {
|
|
2013
|
+
context.report({
|
|
2014
|
+
node: target,
|
|
2015
|
+
messageId,
|
|
2016
|
+
fix: (fixer) => fixer.replaceText(target, to)
|
|
2017
|
+
});
|
|
2018
|
+
}
|
|
2019
|
+
return {
|
|
2020
|
+
MemberExpression(node) {
|
|
2021
|
+
if (node.computed) return;
|
|
2022
|
+
const name = node.property?.name;
|
|
2023
|
+
if (name === "$destroy") {
|
|
2024
|
+
rename(node.property, "renamedMethod", "$unmount");
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
if (name === "destroyed" && node.object?.type === "Super" && inComponent(node)) rename(node.property, "renamedHook", "unmounted");
|
|
2028
|
+
},
|
|
2029
|
+
MethodDefinition(node) {
|
|
2030
|
+
if (node.computed || !node.key) return;
|
|
2031
|
+
const name = getKeyName(node);
|
|
2032
|
+
if (name === "$destroy") {
|
|
2033
|
+
rename(node.key, "renamedMethod", "$unmount");
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
2036
|
+
if (name !== "destroyed" || node.static === true) return;
|
|
2037
|
+
if (!inComponent(node)) return;
|
|
2038
|
+
rename(node.key, "renamedHook", "unmounted");
|
|
2039
|
+
}
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
});
|
|
2043
|
+
//#endregion
|
|
2044
|
+
//#region src/index.ts
|
|
2045
|
+
const PLUGIN_NAME = "js-toolkit";
|
|
2046
|
+
const rules = {
|
|
2047
|
+
"require-config": requireConfig,
|
|
2048
|
+
"require-config-name-pascal-case": requireConfigNamePascalCase,
|
|
2049
|
+
"refs-camel-case": refsCamelCase,
|
|
2050
|
+
"refs-plural-multiple": refsPluralMultiple,
|
|
2051
|
+
"options-camel-case": optionsCamelCase,
|
|
2052
|
+
"async-lifecycle-methods": asyncLifecycleMethods,
|
|
2053
|
+
"on-handler-naming": onHandlerNaming,
|
|
2054
|
+
"on-global-handler-prefix": onGlobalHandlerPrefix,
|
|
2055
|
+
"no-deprecated-properties": noDeprecatedProperties,
|
|
2056
|
+
"no-dispatch-event": noDispatchEvent,
|
|
2057
|
+
"no-shadow-dom": noShadowDom,
|
|
2058
|
+
"no-create-app": noCreateApp,
|
|
2059
|
+
"no-event-listener-methods": noEventListenerMethods,
|
|
2060
|
+
"no-deep-utils-import": noDeepUtilsImport,
|
|
2061
|
+
"no-internal-barrel-import": noInternalBarrelImport,
|
|
2062
|
+
"refs-no-bracket-access": refsNoBracketAccess,
|
|
2063
|
+
"no-redundant-with-mount-when-in-view": noRedundantWithMountWhenInView,
|
|
2064
|
+
"prefer-ref-over-query-selector": preferRefOverQuerySelector,
|
|
2065
|
+
"require-refs-declared-in-config": requireRefsDeclaredInConfig,
|
|
2066
|
+
"no-manual-intersection-observer": noManualIntersectionObserver,
|
|
2067
|
+
"no-manual-mutation-observer": noManualMutationObserver,
|
|
2068
|
+
"emits-kebab-case": emitsKebabCase,
|
|
2069
|
+
"emits-multi-word": emitsMultiWord,
|
|
2070
|
+
"components-pascal-case": componentsPascalCase,
|
|
2071
|
+
"require-emit-declared-in-config": requireEmitDeclaredInConfig,
|
|
2072
|
+
"require-children-declared-in-config": requireChildrenDeclaredInConfig,
|
|
2073
|
+
"require-options-declared-in-config": requireOptionsDeclaredInConfig,
|
|
2074
|
+
"prefer-destructured-lookups": preferDestructuredLookups,
|
|
2075
|
+
"no-dollar-prefix": noDollarPrefix,
|
|
2076
|
+
"require-destroyed-cleanup": requireDestroyedCleanup,
|
|
2077
|
+
"no-write-in-read-phase": noWriteInReadPhase,
|
|
2078
|
+
"no-options-assignment": noOptionsAssignment,
|
|
2079
|
+
"prefer-instance-scheduler": preferInstanceScheduler,
|
|
2080
|
+
"option-default-factory": optionDefaultFactory,
|
|
2081
|
+
"no-conflicting-negated-option": noConflictingNegatedOption,
|
|
2082
|
+
"no-destroy-lifecycle": noDestroyLifecycle
|
|
1583
2083
|
};
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
2084
|
+
const recommendedRules = {
|
|
2085
|
+
[`${PLUGIN_NAME}/require-config`]: "error",
|
|
2086
|
+
[`${PLUGIN_NAME}/require-config-name-pascal-case`]: "error",
|
|
2087
|
+
[`${PLUGIN_NAME}/refs-camel-case`]: "error",
|
|
2088
|
+
[`${PLUGIN_NAME}/refs-plural-multiple`]: "error",
|
|
2089
|
+
[`${PLUGIN_NAME}/options-camel-case`]: "error",
|
|
2090
|
+
[`${PLUGIN_NAME}/async-lifecycle-methods`]: "error",
|
|
2091
|
+
[`${PLUGIN_NAME}/on-handler-naming`]: "error",
|
|
2092
|
+
[`${PLUGIN_NAME}/on-global-handler-prefix`]: "warn",
|
|
2093
|
+
[`${PLUGIN_NAME}/no-deprecated-properties`]: "warn",
|
|
2094
|
+
[`${PLUGIN_NAME}/no-dispatch-event`]: "warn",
|
|
2095
|
+
[`${PLUGIN_NAME}/no-shadow-dom`]: "error",
|
|
2096
|
+
[`${PLUGIN_NAME}/no-create-app`]: "warn",
|
|
2097
|
+
[`${PLUGIN_NAME}/no-event-listener-methods`]: "error",
|
|
2098
|
+
[`${PLUGIN_NAME}/no-deep-utils-import`]: "error",
|
|
2099
|
+
[`${PLUGIN_NAME}/refs-no-bracket-access`]: "error",
|
|
2100
|
+
[`${PLUGIN_NAME}/no-redundant-with-mount-when-in-view`]: "warn",
|
|
2101
|
+
[`${PLUGIN_NAME}/prefer-ref-over-query-selector`]: "warn",
|
|
2102
|
+
[`${PLUGIN_NAME}/require-refs-declared-in-config`]: "error",
|
|
2103
|
+
[`${PLUGIN_NAME}/no-manual-intersection-observer`]: "warn",
|
|
2104
|
+
[`${PLUGIN_NAME}/no-manual-mutation-observer`]: "warn",
|
|
2105
|
+
[`${PLUGIN_NAME}/emits-kebab-case`]: "error",
|
|
2106
|
+
[`${PLUGIN_NAME}/emits-multi-word`]: "error",
|
|
2107
|
+
[`${PLUGIN_NAME}/components-pascal-case`]: "error",
|
|
2108
|
+
[`${PLUGIN_NAME}/require-emit-declared-in-config`]: "error",
|
|
2109
|
+
[`${PLUGIN_NAME}/require-children-declared-in-config`]: "error",
|
|
2110
|
+
[`${PLUGIN_NAME}/require-options-declared-in-config`]: "error",
|
|
2111
|
+
[`${PLUGIN_NAME}/prefer-destructured-lookups`]: "warn",
|
|
2112
|
+
[`${PLUGIN_NAME}/no-dollar-prefix`]: "error",
|
|
2113
|
+
[`${PLUGIN_NAME}/require-destroyed-cleanup`]: "warn"
|
|
1614
2114
|
};
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
2115
|
+
/**
|
|
2116
|
+
* v4 rules.
|
|
2117
|
+
*
|
|
2118
|
+
* v4 is not v3 with a bigger version number, so this is a separate set rather
|
|
2119
|
+
* than an extension of `recommended`. Rules policing something v4 removed —
|
|
2120
|
+
* `config.emits`, `$children`, the v3 decorator names — are absent, and
|
|
2121
|
+
* `async-lifecycle-methods` is absent because a v4 frame hook returns the
|
|
2122
|
+
* function which writes rather than a promise.
|
|
2123
|
+
*/
|
|
2124
|
+
const v4Rules = {
|
|
2125
|
+
[`${PLUGIN_NAME}/no-write-in-read-phase`]: "error",
|
|
2126
|
+
[`${PLUGIN_NAME}/no-options-assignment`]: "error",
|
|
2127
|
+
[`${PLUGIN_NAME}/prefer-instance-scheduler`]: "warn",
|
|
2128
|
+
[`${PLUGIN_NAME}/option-default-factory`]: "error",
|
|
2129
|
+
[`${PLUGIN_NAME}/no-conflicting-negated-option`]: "error",
|
|
2130
|
+
[`${PLUGIN_NAME}/no-destroy-lifecycle`]: "error",
|
|
2131
|
+
[`${PLUGIN_NAME}/no-deprecated-properties`]: ["error", { version: "v4" }],
|
|
2132
|
+
[`${PLUGIN_NAME}/no-dispatch-event`]: "warn",
|
|
2133
|
+
[`${PLUGIN_NAME}/no-shadow-dom`]: "error",
|
|
2134
|
+
[`${PLUGIN_NAME}/no-event-listener-methods`]: "error",
|
|
2135
|
+
[`${PLUGIN_NAME}/no-manual-mutation-observer`]: "warn",
|
|
2136
|
+
[`${PLUGIN_NAME}/refs-no-bracket-access`]: "error",
|
|
2137
|
+
[`${PLUGIN_NAME}/prefer-ref-over-query-selector`]: "warn"
|
|
2138
|
+
};
|
|
2139
|
+
const base = eslintCompatPlugin({
|
|
2140
|
+
meta: { name: "@studiometa/eslint-plugin-js-toolkit" },
|
|
2141
|
+
rules
|
|
1620
2142
|
});
|
|
1621
|
-
|
|
2143
|
+
const plugin = Object.assign(base, { configs: {} });
|
|
1622
2144
|
plugin.configs["recommended"] = {
|
|
1623
|
-
|
|
1624
|
-
|
|
2145
|
+
plugins: { [PLUGIN_NAME]: plugin },
|
|
2146
|
+
rules: recommendedRules
|
|
1625
2147
|
};
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
plugin as jsToolkit
|
|
2148
|
+
plugin.configs["v4"] = {
|
|
2149
|
+
plugins: { [PLUGIN_NAME]: plugin },
|
|
2150
|
+
rules: v4Rules
|
|
1630
2151
|
};
|
|
1631
|
-
//#
|
|
2152
|
+
//#endregion
|
|
2153
|
+
export { plugin as default, plugin as jsToolkit };
|
|
2154
|
+
|
|
2155
|
+
//# sourceMappingURL=index.js.map
|