@rotki/eslint-plugin 1.2.0 → 1.3.1
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 +98 -1
- package/dist/index.d.mts +215 -15
- package/dist/index.d.ts +215 -15
- package/dist/index.mjs +1282 -87
- package/package.json +23 -23
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
-
import createDebug from 'debug';
|
|
1
|
+
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';
|
|
3
2
|
import * as compat from 'eslint-compat-utils';
|
|
4
|
-
import
|
|
3
|
+
import debugFactory from 'debug';
|
|
4
|
+
import { extname, resolve } from 'node:path';
|
|
5
5
|
import { pascalCase, kebabCase } from 'scule';
|
|
6
|
+
import { statSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { globSync } from 'tinyglobby';
|
|
8
|
+
import { parse } from 'vue-eslint-parser';
|
|
6
9
|
|
|
7
|
-
const version = "1.
|
|
10
|
+
const version = "1.3.1";
|
|
8
11
|
const pkg = {
|
|
9
12
|
version: version};
|
|
10
13
|
|
|
@@ -15,8 +18,34 @@ function getSourceCode(context) {
|
|
|
15
18
|
return compat.getSourceCode(context);
|
|
16
19
|
}
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
const COMPOSABLE_PATTERN = /^use[A-Z]/;
|
|
22
|
+
function isComposableName(name) {
|
|
23
|
+
return COMPOSABLE_PATTERN.test(name);
|
|
24
|
+
}
|
|
25
|
+
function getEnclosingComposable(node) {
|
|
26
|
+
let current = node.parent;
|
|
27
|
+
while (current) {
|
|
28
|
+
if (current.type === AST_NODE_TYPES.FunctionDeclaration && current.id && isComposableName(current.id.name))
|
|
29
|
+
return current;
|
|
30
|
+
if (current.type === AST_NODE_TYPES.ArrowFunctionExpression || current.type === AST_NODE_TYPES.FunctionExpression) {
|
|
31
|
+
const parent = current.parent;
|
|
32
|
+
if (parent?.type === AST_NODE_TYPES.VariableDeclarator && parent.id.type === AST_NODE_TYPES.Identifier && isComposableName(parent.id.name)) {
|
|
33
|
+
return current;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
current = current.parent;
|
|
37
|
+
}
|
|
38
|
+
return void 0;
|
|
39
|
+
}
|
|
40
|
+
function isInsideComposable(node) {
|
|
41
|
+
return getEnclosingComposable(node) !== void 0;
|
|
42
|
+
}
|
|
43
|
+
function composableNameToPascal(name) {
|
|
44
|
+
return name.slice(3);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function createConfig(plugin, name, flat, category) {
|
|
48
|
+
const rules = Object.fromEntries(Object.entries(plugin.rules).filter(([_key, rule]) => rule.meta.docs?.recommendation === category && !rule.meta.deprecated).map(([key]) => [`${name}/${key}`, 2]));
|
|
20
49
|
if (flat) {
|
|
21
50
|
return {
|
|
22
51
|
plugins: {
|
|
@@ -31,6 +60,9 @@ function createRecommended(plugin, name, flat) {
|
|
|
31
60
|
};
|
|
32
61
|
}
|
|
33
62
|
}
|
|
63
|
+
function createRecommended(plugin, name, flat) {
|
|
64
|
+
return createConfig(plugin, name, flat, "recommended");
|
|
65
|
+
}
|
|
34
66
|
|
|
35
67
|
function getStringLiteralValue(node, stringOnly = false) {
|
|
36
68
|
if (node.type === "Literal") {
|
|
@@ -134,12 +166,553 @@ function defineTemplateBodyVisitor(context, templateBodyVisitor, scriptVisitor,
|
|
|
134
166
|
);
|
|
135
167
|
}
|
|
136
168
|
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
169
|
+
const RULE_NAME$f = "composable-input-flexibility";
|
|
170
|
+
function checkParamForRef(param) {
|
|
171
|
+
let annotation;
|
|
172
|
+
if (param.type === AST_NODE_TYPES.Identifier && param.typeAnnotation) {
|
|
173
|
+
annotation = param.typeAnnotation.typeAnnotation;
|
|
174
|
+
} else if (param.type === AST_NODE_TYPES.AssignmentPattern && param.left.type === AST_NODE_TYPES.Identifier && param.left.typeAnnotation) {
|
|
175
|
+
annotation = param.left.typeAnnotation.typeAnnotation;
|
|
176
|
+
}
|
|
177
|
+
if (annotation?.type === AST_NODE_TYPES.TSTypeReference && annotation.typeName.type === AST_NODE_TYPES.Identifier && annotation.typeName.name === "Ref") {
|
|
178
|
+
return annotation;
|
|
179
|
+
}
|
|
180
|
+
return void 0;
|
|
181
|
+
}
|
|
182
|
+
const composableInputFlexibility = createEslintRule({
|
|
183
|
+
create(context) {
|
|
184
|
+
const autofix = context.options[0]?.autofix ?? false;
|
|
185
|
+
return {
|
|
186
|
+
FunctionDeclaration: (node) => {
|
|
187
|
+
if (!node.id || !isComposableName(node.id.name))
|
|
188
|
+
return;
|
|
189
|
+
checkParams(node.params);
|
|
190
|
+
},
|
|
191
|
+
VariableDeclarator: (node) => {
|
|
192
|
+
if (node.id.type !== AST_NODE_TYPES.Identifier || !isComposableName(node.id.name))
|
|
193
|
+
return;
|
|
194
|
+
if (node.init?.type === AST_NODE_TYPES.ArrowFunctionExpression || node.init?.type === AST_NODE_TYPES.FunctionExpression) {
|
|
195
|
+
checkParams(node.init.params);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
function checkParams(params) {
|
|
200
|
+
for (const param of params) {
|
|
201
|
+
const refType = checkParamForRef(param);
|
|
202
|
+
if (refType) {
|
|
203
|
+
context.report({
|
|
204
|
+
...autofix ? {
|
|
205
|
+
fix(fixer) {
|
|
206
|
+
return fixer.replaceText(refType.typeName, "MaybeRefOrGetter");
|
|
207
|
+
}
|
|
208
|
+
} : {
|
|
209
|
+
suggest: [{
|
|
210
|
+
fix(fixer) {
|
|
211
|
+
return fixer.replaceText(refType.typeName, "MaybeRefOrGetter");
|
|
212
|
+
},
|
|
213
|
+
messageId: "suggestMaybeRefOrGetter"
|
|
214
|
+
}]
|
|
215
|
+
},
|
|
216
|
+
messageId: "preferMaybeRefOrGetter",
|
|
217
|
+
node: refType
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
defaultOptions: [{ autofix: false }],
|
|
224
|
+
meta: {
|
|
225
|
+
docs: {
|
|
226
|
+
description: "Prefer MaybeRefOrGetter over Ref for composable parameters",
|
|
227
|
+
recommendation: "stylistic"
|
|
228
|
+
},
|
|
229
|
+
fixable: "code",
|
|
230
|
+
hasSuggestions: true,
|
|
231
|
+
messages: {
|
|
232
|
+
preferMaybeRefOrGetter: "Use MaybeRefOrGetter<T> instead of Ref<T> for composable parameters to increase input flexibility.",
|
|
233
|
+
suggestMaybeRefOrGetter: "Replace Ref<T> with MaybeRefOrGetter<T>."
|
|
234
|
+
},
|
|
235
|
+
schema: [
|
|
236
|
+
{
|
|
237
|
+
additionalProperties: false,
|
|
238
|
+
properties: {
|
|
239
|
+
autofix: {
|
|
240
|
+
default: false,
|
|
241
|
+
description: "Enable auto-fix. When disabled, the fix is available as a suggestion.",
|
|
242
|
+
type: "boolean"
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
type: "object"
|
|
246
|
+
}
|
|
247
|
+
],
|
|
248
|
+
type: "suggestion"
|
|
249
|
+
},
|
|
250
|
+
name: RULE_NAME$f
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const RULE_NAME$e = "composable-naming-convention";
|
|
254
|
+
function getComposableFunction(node) {
|
|
255
|
+
if (node.type === AST_NODE_TYPES.FunctionDeclaration) {
|
|
256
|
+
if (!node.id || !isComposableName(node.id.name))
|
|
257
|
+
return void 0;
|
|
258
|
+
return { fn: node, name: node.id.name, params: node.params, returnType: node.returnType };
|
|
259
|
+
}
|
|
260
|
+
if (node.type === AST_NODE_TYPES.VariableDeclarator && node.id.type === AST_NODE_TYPES.Identifier && isComposableName(node.id.name) && node.init && (node.init.type === AST_NODE_TYPES.ArrowFunctionExpression || node.init.type === AST_NODE_TYPES.FunctionExpression)) {
|
|
261
|
+
const fn = node.init;
|
|
262
|
+
return { fn, name: node.id.name, params: fn.params, returnType: fn.returnType };
|
|
263
|
+
}
|
|
264
|
+
return void 0;
|
|
265
|
+
}
|
|
266
|
+
function getTypeReferenceName(annotation) {
|
|
267
|
+
if (annotation.type === AST_NODE_TYPES.TSTypeReference && annotation.typeName.type === AST_NODE_TYPES.Identifier) {
|
|
268
|
+
return annotation.typeName.name;
|
|
269
|
+
}
|
|
270
|
+
return void 0;
|
|
271
|
+
}
|
|
272
|
+
const composableNamingConvention = createEslintRule({
|
|
273
|
+
create(context) {
|
|
274
|
+
return {
|
|
275
|
+
FunctionDeclaration: (node) => {
|
|
276
|
+
checkComposable(node);
|
|
277
|
+
},
|
|
278
|
+
VariableDeclarator: (node) => {
|
|
279
|
+
checkComposable(node);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
function checkComposable(node) {
|
|
283
|
+
const info = getComposableFunction(node);
|
|
284
|
+
if (!info)
|
|
285
|
+
return;
|
|
286
|
+
const pascalName = composableNameToPascal(info.name);
|
|
287
|
+
const expectedOptions = `Use${pascalName}Options`;
|
|
288
|
+
const expectedReturn = `Use${pascalName}Return`;
|
|
289
|
+
for (const param of info.params) {
|
|
290
|
+
const annotation = getParamTypeAnnotation(param);
|
|
291
|
+
if (!annotation)
|
|
292
|
+
continue;
|
|
293
|
+
const typeName = getTypeReferenceName(annotation);
|
|
294
|
+
if (typeName && typeName.startsWith("Use") && typeName.endsWith("Options") && typeName !== expectedOptions) {
|
|
295
|
+
context.report({
|
|
296
|
+
data: { expected: expectedOptions, got: typeName },
|
|
297
|
+
messageId: "optionsNaming",
|
|
298
|
+
node: param
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (info.returnType) {
|
|
303
|
+
const returnTypeName = getTypeReferenceName(info.returnType.typeAnnotation);
|
|
304
|
+
if (returnTypeName && returnTypeName.startsWith("Use") && returnTypeName.endsWith("Return") && returnTypeName !== expectedReturn) {
|
|
305
|
+
context.report({
|
|
306
|
+
data: { expected: expectedReturn, got: returnTypeName },
|
|
307
|
+
messageId: "returnNaming",
|
|
308
|
+
node: info.returnType
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function getParamTypeAnnotation(param) {
|
|
314
|
+
if (param.type === AST_NODE_TYPES.Identifier && param.typeAnnotation)
|
|
315
|
+
return param.typeAnnotation.typeAnnotation;
|
|
316
|
+
if (param.type === AST_NODE_TYPES.AssignmentPattern && param.left.type === AST_NODE_TYPES.Identifier && param.left.typeAnnotation) {
|
|
317
|
+
return param.left.typeAnnotation.typeAnnotation;
|
|
318
|
+
}
|
|
319
|
+
return void 0;
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
defaultOptions: [],
|
|
323
|
+
meta: {
|
|
324
|
+
docs: {
|
|
325
|
+
description: "Enforce consistent naming for composable options and return types",
|
|
326
|
+
recommendation: "stylistic"
|
|
327
|
+
},
|
|
328
|
+
messages: {
|
|
329
|
+
optionsNaming: "Options type should be named '{{ expected }}' instead of '{{ got }}'.",
|
|
330
|
+
returnNaming: "Return type should be named '{{ expected }}' instead of '{{ got }}'."
|
|
331
|
+
},
|
|
332
|
+
schema: [],
|
|
333
|
+
type: "suggestion"
|
|
334
|
+
},
|
|
335
|
+
name: RULE_NAME$e
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const RULE_NAME$d = "composable-no-default-export";
|
|
339
|
+
const composableNoDefaultExport = createEslintRule({
|
|
340
|
+
create(context) {
|
|
341
|
+
let hasComposable = false;
|
|
342
|
+
let defaultExportNode = null;
|
|
343
|
+
return {
|
|
344
|
+
"ExportDefaultDeclaration": (node) => {
|
|
345
|
+
defaultExportNode = node;
|
|
346
|
+
},
|
|
347
|
+
"FunctionDeclaration": (node) => {
|
|
348
|
+
if (node.id && isComposableName(node.id.name))
|
|
349
|
+
hasComposable = true;
|
|
350
|
+
},
|
|
351
|
+
"Program:exit": () => {
|
|
352
|
+
if (hasComposable && defaultExportNode) {
|
|
353
|
+
context.report({
|
|
354
|
+
messageId: "noDefaultExport",
|
|
355
|
+
node: defaultExportNode
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
"VariableDeclarator": (node) => {
|
|
360
|
+
if (node.id.type === AST_NODE_TYPES.Identifier && isComposableName(node.id.name) && node.init && (node.init.type === AST_NODE_TYPES.ArrowFunctionExpression || node.init.type === AST_NODE_TYPES.FunctionExpression)) {
|
|
361
|
+
hasComposable = true;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
},
|
|
366
|
+
defaultOptions: [],
|
|
367
|
+
meta: {
|
|
368
|
+
docs: {
|
|
369
|
+
description: "Forbid default exports in files containing composables",
|
|
370
|
+
recommendation: "strict"
|
|
371
|
+
},
|
|
372
|
+
messages: {
|
|
373
|
+
noDefaultExport: "Files containing composables should use named exports instead of default exports."
|
|
374
|
+
},
|
|
375
|
+
schema: [],
|
|
376
|
+
type: "problem"
|
|
377
|
+
},
|
|
378
|
+
name: RULE_NAME$d
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const RULE_NAME$c = "composable-prefer-shallowref";
|
|
382
|
+
const composablePreferShallowref = createEslintRule({
|
|
383
|
+
create(context) {
|
|
384
|
+
const autofix = context.options[0]?.autofix ?? false;
|
|
385
|
+
return {
|
|
386
|
+
CallExpression(node) {
|
|
387
|
+
if (node.callee.type !== AST_NODE_TYPES.Identifier || node.callee.name !== "ref")
|
|
388
|
+
return;
|
|
389
|
+
if (!isInsideComposable(node))
|
|
390
|
+
return;
|
|
391
|
+
const arg = node.arguments[0];
|
|
392
|
+
if (!arg || arg.type !== AST_NODE_TYPES.Literal)
|
|
393
|
+
return;
|
|
394
|
+
context.report({
|
|
395
|
+
...autofix ? {
|
|
396
|
+
fix(fixer) {
|
|
397
|
+
return fixer.replaceText(node.callee, "shallowRef");
|
|
398
|
+
}
|
|
399
|
+
} : {
|
|
400
|
+
suggest: [{
|
|
401
|
+
fix(fixer) {
|
|
402
|
+
return fixer.replaceText(node.callee, "shallowRef");
|
|
403
|
+
},
|
|
404
|
+
messageId: "suggestShallowRef"
|
|
405
|
+
}]
|
|
406
|
+
},
|
|
407
|
+
messageId: "preferShallowRef",
|
|
408
|
+
node
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
},
|
|
413
|
+
defaultOptions: [{ autofix: false }],
|
|
414
|
+
meta: {
|
|
415
|
+
docs: {
|
|
416
|
+
description: "Prefer shallowRef() over ref() for primitive values in composables",
|
|
417
|
+
recommendation: "strict"
|
|
418
|
+
},
|
|
419
|
+
fixable: "code",
|
|
420
|
+
hasSuggestions: true,
|
|
421
|
+
messages: {
|
|
422
|
+
preferShallowRef: "Use shallowRef() instead of ref() for primitive values in composables.",
|
|
423
|
+
suggestShallowRef: "Replace ref() with shallowRef()."
|
|
424
|
+
},
|
|
425
|
+
schema: [
|
|
426
|
+
{
|
|
427
|
+
additionalProperties: false,
|
|
428
|
+
properties: {
|
|
429
|
+
autofix: {
|
|
430
|
+
default: false,
|
|
431
|
+
description: "Enable auto-fix. When disabled, the fix is available as a suggestion.",
|
|
432
|
+
type: "boolean"
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
type: "object"
|
|
436
|
+
}
|
|
437
|
+
],
|
|
438
|
+
type: "suggestion"
|
|
439
|
+
},
|
|
440
|
+
name: RULE_NAME$c
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
const RULE_NAME$b = "composable-require-cleanup";
|
|
444
|
+
const SIDE_EFFECT_CALLS = /* @__PURE__ */ new Set(["addEventListener", "setInterval", "setTimeout"]);
|
|
445
|
+
const SIDE_EFFECT_CONSTRUCTORS = /* @__PURE__ */ new Set(["MutationObserver", "ResizeObserver", "IntersectionObserver"]);
|
|
446
|
+
const CLEANUP_HOOKS = /* @__PURE__ */ new Set(["onUnmounted", "onBeforeUnmount", "onScopeDispose", "tryOnScopeDispose"]);
|
|
447
|
+
const composableRequireCleanup = createEslintRule({
|
|
448
|
+
create(context) {
|
|
449
|
+
const scopeStack = [];
|
|
450
|
+
function enterComposable() {
|
|
451
|
+
scopeStack.push({ hasCleanup: false, sideEffects: [] });
|
|
452
|
+
}
|
|
453
|
+
function exitComposable() {
|
|
454
|
+
const scope = scopeStack.pop();
|
|
455
|
+
if (scope && scope.sideEffects.length > 0 && !scope.hasCleanup) {
|
|
456
|
+
for (const node of scope.sideEffects) {
|
|
457
|
+
context.report({
|
|
458
|
+
messageId: "requireCleanup",
|
|
459
|
+
node
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
function currentScope() {
|
|
465
|
+
return scopeStack.at(-1);
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
"ArrowFunctionExpression": (node) => {
|
|
469
|
+
const parent = node.parent;
|
|
470
|
+
if (parent?.type === AST_NODE_TYPES.VariableDeclarator && parent.id.type === AST_NODE_TYPES.Identifier && isComposableName(parent.id.name)) {
|
|
471
|
+
enterComposable();
|
|
472
|
+
}
|
|
473
|
+
},
|
|
474
|
+
"ArrowFunctionExpression:exit": (node) => {
|
|
475
|
+
const parent = node.parent;
|
|
476
|
+
if (parent?.type === AST_NODE_TYPES.VariableDeclarator && parent.id.type === AST_NODE_TYPES.Identifier && isComposableName(parent.id.name)) {
|
|
477
|
+
exitComposable();
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
"CallExpression": (node) => {
|
|
481
|
+
const scope = currentScope();
|
|
482
|
+
if (!scope)
|
|
483
|
+
return;
|
|
484
|
+
if (node.callee.type === AST_NODE_TYPES.Identifier && CLEANUP_HOOKS.has(node.callee.name)) {
|
|
485
|
+
scope.hasCleanup = true;
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (node.callee.type === AST_NODE_TYPES.MemberExpression && node.callee.property.type === AST_NODE_TYPES.Identifier && SIDE_EFFECT_CALLS.has(node.callee.property.name)) {
|
|
489
|
+
scope.sideEffects.push(node);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (node.callee.type === AST_NODE_TYPES.Identifier && SIDE_EFFECT_CALLS.has(node.callee.name)) {
|
|
493
|
+
scope.sideEffects.push(node);
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
"FunctionDeclaration": (node) => {
|
|
497
|
+
if (node.id && isComposableName(node.id.name))
|
|
498
|
+
enterComposable();
|
|
499
|
+
},
|
|
500
|
+
"FunctionDeclaration:exit": (node) => {
|
|
501
|
+
if (node.id && isComposableName(node.id.name))
|
|
502
|
+
exitComposable();
|
|
503
|
+
},
|
|
504
|
+
"FunctionExpression": (node) => {
|
|
505
|
+
const parent = node.parent;
|
|
506
|
+
if (parent?.type === AST_NODE_TYPES.VariableDeclarator && parent.id.type === AST_NODE_TYPES.Identifier && isComposableName(parent.id.name)) {
|
|
507
|
+
enterComposable();
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
"FunctionExpression:exit": (node) => {
|
|
511
|
+
const parent = node.parent;
|
|
512
|
+
if (parent?.type === AST_NODE_TYPES.VariableDeclarator && parent.id.type === AST_NODE_TYPES.Identifier && isComposableName(parent.id.name)) {
|
|
513
|
+
exitComposable();
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
"NewExpression": (node) => {
|
|
517
|
+
const scope = currentScope();
|
|
518
|
+
if (!scope)
|
|
519
|
+
return;
|
|
520
|
+
if (node.callee.type === AST_NODE_TYPES.Identifier && SIDE_EFFECT_CONSTRUCTORS.has(node.callee.name))
|
|
521
|
+
scope.sideEffects.push(node);
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
},
|
|
525
|
+
defaultOptions: [],
|
|
526
|
+
meta: {
|
|
527
|
+
docs: {
|
|
528
|
+
description: "Require cleanup hooks when composables use side effects",
|
|
529
|
+
recommendation: "strict"
|
|
530
|
+
},
|
|
531
|
+
messages: {
|
|
532
|
+
requireCleanup: "Side effect requires a cleanup hook (onUnmounted, onBeforeUnmount, or onScopeDispose)."
|
|
533
|
+
},
|
|
534
|
+
schema: [],
|
|
535
|
+
type: "problem"
|
|
536
|
+
},
|
|
537
|
+
name: RULE_NAME$b
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
const RULE_NAME$a = "composable-return-readonly";
|
|
541
|
+
const REACTIVE_CREATORS = /* @__PURE__ */ new Set(["ref", "shallowRef", "computed"]);
|
|
542
|
+
const composableReturnReadonly = createEslintRule({
|
|
543
|
+
create(context) {
|
|
544
|
+
const autofix = context.options[0]?.autofix ?? false;
|
|
545
|
+
const source = getSourceCode(context);
|
|
546
|
+
const reactiveVars = /* @__PURE__ */ new Set();
|
|
547
|
+
return {
|
|
548
|
+
ReturnStatement: (node) => {
|
|
549
|
+
if (!getEnclosingComposable(node))
|
|
550
|
+
return;
|
|
551
|
+
if (!node.argument || node.argument.type !== AST_NODE_TYPES.ObjectExpression)
|
|
552
|
+
return;
|
|
553
|
+
for (const prop of node.argument.properties) {
|
|
554
|
+
if (prop.type !== AST_NODE_TYPES.Property)
|
|
555
|
+
continue;
|
|
556
|
+
if (prop.shorthand && prop.key.type === AST_NODE_TYPES.Identifier && reactiveVars.has(prop.key.name)) {
|
|
557
|
+
const keyName = prop.key.name;
|
|
558
|
+
const fixFn = (fixer) => fixer.replaceText(prop, `${keyName}: readonly(${keyName})`);
|
|
559
|
+
context.report({
|
|
560
|
+
data: { name: keyName },
|
|
561
|
+
...autofix ? { fix: fixFn } : { suggest: [{ data: { name: keyName }, fix: fixFn, messageId: "suggestWrapReadonly" }] },
|
|
562
|
+
messageId: "wrapReadonly",
|
|
563
|
+
node: prop
|
|
564
|
+
});
|
|
565
|
+
} else if (!prop.shorthand && prop.value.type === AST_NODE_TYPES.Identifier && reactiveVars.has(prop.value.name)) {
|
|
566
|
+
const valueName = prop.value.name;
|
|
567
|
+
const fixFn = (fixer) => fixer.replaceText(prop.value, `readonly(${prop.value.type === AST_NODE_TYPES.Identifier ? prop.value.name : source.getText(prop.value)})`);
|
|
568
|
+
context.report({
|
|
569
|
+
data: { name: valueName },
|
|
570
|
+
...autofix ? { fix: fixFn } : { suggest: [{ data: { name: valueName }, fix: fixFn, messageId: "suggestWrapReadonly" }] },
|
|
571
|
+
messageId: "wrapReadonly",
|
|
572
|
+
node: prop
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
VariableDeclarator: (node) => {
|
|
578
|
+
if (!getEnclosingComposable(node))
|
|
579
|
+
return;
|
|
580
|
+
if (node.id.type !== AST_NODE_TYPES.Identifier)
|
|
581
|
+
return;
|
|
582
|
+
if (node.init?.type === AST_NODE_TYPES.CallExpression && node.init.callee.type === AST_NODE_TYPES.Identifier && REACTIVE_CREATORS.has(node.init.callee.name)) {
|
|
583
|
+
reactiveVars.add(node.id.name);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
},
|
|
588
|
+
defaultOptions: [{ autofix: false }],
|
|
589
|
+
meta: {
|
|
590
|
+
docs: {
|
|
591
|
+
description: "Require returned refs from composables to be wrapped with readonly()",
|
|
592
|
+
recommendation: "strict"
|
|
593
|
+
},
|
|
594
|
+
fixable: "code",
|
|
595
|
+
hasSuggestions: true,
|
|
596
|
+
messages: {
|
|
597
|
+
suggestWrapReadonly: "Wrap '{{ name }}' with readonly().",
|
|
598
|
+
wrapReadonly: "Returned ref '{{ name }}' should be wrapped with readonly()."
|
|
599
|
+
},
|
|
600
|
+
schema: [
|
|
601
|
+
{
|
|
602
|
+
additionalProperties: false,
|
|
603
|
+
properties: {
|
|
604
|
+
autofix: {
|
|
605
|
+
default: false,
|
|
606
|
+
description: "Enable auto-fix. When disabled, the fix is available as a suggestion.",
|
|
607
|
+
type: "boolean"
|
|
608
|
+
}
|
|
609
|
+
},
|
|
610
|
+
type: "object"
|
|
611
|
+
}
|
|
612
|
+
],
|
|
613
|
+
type: "suggestion"
|
|
614
|
+
},
|
|
615
|
+
name: RULE_NAME$a
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
const RULE_NAME$9 = "composable-ssr-safety";
|
|
619
|
+
const BROWSER_GLOBALS = /* @__PURE__ */ new Set(["window", "document", "navigator"]);
|
|
620
|
+
const LIFECYCLE_HOOKS = /* @__PURE__ */ new Set(["onMounted", "onBeforeMount"]);
|
|
621
|
+
function isInsideLifecycleHook(node) {
|
|
622
|
+
let current = node.parent;
|
|
623
|
+
while (current) {
|
|
624
|
+
if ((current.type === AST_NODE_TYPES.ArrowFunctionExpression || current.type === AST_NODE_TYPES.FunctionExpression) && current.parent?.type === AST_NODE_TYPES.CallExpression && current.parent.callee.type === AST_NODE_TYPES.Identifier && LIFECYCLE_HOOKS.has(current.parent.callee.name)) {
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
627
|
+
current = current.parent;
|
|
628
|
+
}
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
function isInsideTypeofCheck(node) {
|
|
632
|
+
let current = node.parent;
|
|
633
|
+
while (current) {
|
|
634
|
+
if (current.type === AST_NODE_TYPES.UnaryExpression && current.operator === "typeof")
|
|
635
|
+
return true;
|
|
636
|
+
if (current.type === AST_NODE_TYPES.IfStatement) {
|
|
637
|
+
const test = current.test;
|
|
638
|
+
if (hasTypeofCheck(test))
|
|
639
|
+
return true;
|
|
640
|
+
}
|
|
641
|
+
current = current.parent;
|
|
642
|
+
}
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
function hasTypeofCheck(node) {
|
|
646
|
+
if (node.type === AST_NODE_TYPES.BinaryExpression) {
|
|
647
|
+
if (node.left.type === AST_NODE_TYPES.UnaryExpression && node.left.operator === "typeof" && node.left.argument.type === AST_NODE_TYPES.Identifier && BROWSER_GLOBALS.has(node.left.argument.name)) {
|
|
648
|
+
return true;
|
|
649
|
+
}
|
|
650
|
+
if (node.right.type === AST_NODE_TYPES.UnaryExpression && node.right.operator === "typeof" && node.right.argument.type === AST_NODE_TYPES.Identifier && BROWSER_GLOBALS.has(node.right.argument.name)) {
|
|
651
|
+
return true;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
if (node.type === AST_NODE_TYPES.LogicalExpression)
|
|
655
|
+
return hasTypeofCheck(node.left) || hasTypeofCheck(node.right);
|
|
656
|
+
return false;
|
|
657
|
+
}
|
|
658
|
+
function isGuardedByIfTypeofOrLifecycle(node) {
|
|
659
|
+
if (isInsideLifecycleHook(node))
|
|
660
|
+
return true;
|
|
661
|
+
let current = node.parent;
|
|
662
|
+
while (current) {
|
|
663
|
+
if (current.type === AST_NODE_TYPES.IfStatement && hasTypeofCheck(current.test)) {
|
|
664
|
+
return true;
|
|
665
|
+
}
|
|
666
|
+
if (current.type === AST_NODE_TYPES.ConditionalExpression && hasTypeofCheck(current.test))
|
|
667
|
+
return true;
|
|
668
|
+
if (current.type === AST_NODE_TYPES.LogicalExpression && current.operator === "&&" && hasTypeofCheck(current.left)) {
|
|
669
|
+
return true;
|
|
670
|
+
}
|
|
671
|
+
current = current.parent;
|
|
672
|
+
}
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
const composableSsrSafety = createEslintRule({
|
|
676
|
+
create(context) {
|
|
677
|
+
return {
|
|
678
|
+
MemberExpression(node) {
|
|
679
|
+
if (node.object.type !== AST_NODE_TYPES.Identifier)
|
|
680
|
+
return;
|
|
681
|
+
if (!BROWSER_GLOBALS.has(node.object.name))
|
|
682
|
+
return;
|
|
683
|
+
if (!getEnclosingComposable(node))
|
|
684
|
+
return;
|
|
685
|
+
if (isInsideTypeofCheck(node))
|
|
686
|
+
return;
|
|
687
|
+
if (isGuardedByIfTypeofOrLifecycle(node))
|
|
688
|
+
return;
|
|
689
|
+
context.report({
|
|
690
|
+
data: { name: node.object.name },
|
|
691
|
+
messageId: "ssrUnsafe",
|
|
692
|
+
node
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
},
|
|
697
|
+
defaultOptions: [],
|
|
698
|
+
meta: {
|
|
699
|
+
docs: {
|
|
700
|
+
description: "Require browser global access in composables to be SSR-safe",
|
|
701
|
+
recommendation: "strict"
|
|
702
|
+
},
|
|
703
|
+
messages: {
|
|
704
|
+
ssrUnsafe: "Access to '{{ name }}' in a composable must be guarded with a typeof check or placed inside onMounted/onBeforeMount."
|
|
705
|
+
},
|
|
706
|
+
schema: [],
|
|
707
|
+
type: "problem"
|
|
708
|
+
},
|
|
709
|
+
name: RULE_NAME$9
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
const debug$6 = debugFactory("@rotki/eslint-plugin:consistent-ref-type-annotation");
|
|
713
|
+
const RULE_NAME$8 = "consistent-ref-type-annotation";
|
|
714
|
+
const FIXABLE_METHODS = /* @__PURE__ */ new Set(["ref", "computed"]);
|
|
715
|
+
function checkAssignmentDeclaration(context, source, node, declaration, allowInference) {
|
|
143
716
|
let declarationTypeArguments;
|
|
144
717
|
const init = declaration.init;
|
|
145
718
|
if (!(init && init.type === TSESTree.AST_NODE_TYPES.CallExpression))
|
|
@@ -147,10 +720,10 @@ function checkAssignmentDeclaration(context, node, declaration, options) {
|
|
|
147
720
|
const callee = init.callee;
|
|
148
721
|
if (!(callee && callee.type === TSESTree.AST_NODE_TYPES.Identifier))
|
|
149
722
|
return;
|
|
150
|
-
if (!
|
|
723
|
+
if (!FIXABLE_METHODS.has(callee.name))
|
|
151
724
|
return;
|
|
152
725
|
const name = callee.name;
|
|
153
|
-
debug$
|
|
726
|
+
debug$6(`found ${name}, checking type arguments`);
|
|
154
727
|
const initializationTypeArguments = init.typeArguments;
|
|
155
728
|
const typeAnnotation = declaration.id.typeAnnotation;
|
|
156
729
|
if (typeAnnotation) {
|
|
@@ -160,10 +733,10 @@ function checkAssignmentDeclaration(context, node, declaration, options) {
|
|
|
160
733
|
}
|
|
161
734
|
if (initializationTypeArguments && !declarationTypeArguments)
|
|
162
735
|
return;
|
|
163
|
-
debug$
|
|
736
|
+
debug$6(`generating report for ${name}`);
|
|
164
737
|
if (!initializationTypeArguments && !declarationTypeArguments) {
|
|
165
738
|
if (allowInference) {
|
|
166
|
-
debug$
|
|
739
|
+
debug$6("type inference is allowed");
|
|
167
740
|
} else {
|
|
168
741
|
context.report({
|
|
169
742
|
data: {
|
|
@@ -195,13 +768,14 @@ const consistentRefTypeAnnotation = createEslintRule({
|
|
|
195
768
|
create(context, optionsWithDefault) {
|
|
196
769
|
const options = optionsWithDefault[0] || {};
|
|
197
770
|
const allowInference = options.allowInference;
|
|
771
|
+
const source = getSourceCode(context);
|
|
198
772
|
return {
|
|
199
773
|
VariableDeclaration: (node) => {
|
|
200
774
|
const declarations = node.declarations;
|
|
201
775
|
for (const declaration of declarations) {
|
|
202
776
|
if (declaration.type !== TSESTree.AST_NODE_TYPES.VariableDeclarator)
|
|
203
777
|
continue;
|
|
204
|
-
checkAssignmentDeclaration(context, node, declaration,
|
|
778
|
+
checkAssignmentDeclaration(context, source, node, declaration, allowInference);
|
|
205
779
|
}
|
|
206
780
|
}
|
|
207
781
|
};
|
|
@@ -230,10 +804,10 @@ const consistentRefTypeAnnotation = createEslintRule({
|
|
|
230
804
|
],
|
|
231
805
|
type: "problem"
|
|
232
806
|
},
|
|
233
|
-
name: RULE_NAME$
|
|
807
|
+
name: RULE_NAME$8
|
|
234
808
|
});
|
|
235
809
|
|
|
236
|
-
const RULE_NAME$
|
|
810
|
+
const RULE_NAME$7 = "max-dependencies";
|
|
237
811
|
const maxDependencies = createEslintRule({
|
|
238
812
|
create(context, [options]) {
|
|
239
813
|
let dependencyCount = 0;
|
|
@@ -293,12 +867,12 @@ const maxDependencies = createEslintRule({
|
|
|
293
867
|
],
|
|
294
868
|
type: "suggestion"
|
|
295
869
|
},
|
|
296
|
-
name: RULE_NAME$
|
|
870
|
+
name: RULE_NAME$7
|
|
297
871
|
});
|
|
298
872
|
|
|
299
|
-
const RULE_NAME$
|
|
300
|
-
const debug$
|
|
301
|
-
const
|
|
873
|
+
const RULE_NAME$6 = "no-deprecated-classes";
|
|
874
|
+
const debug$5 = debugFactory("@rotki/eslint-plugin:no-deprecated-classes");
|
|
875
|
+
const stringReplacements = /* @__PURE__ */ new Map([
|
|
302
876
|
["d-block", "block"],
|
|
303
877
|
["d-flex", "flex"],
|
|
304
878
|
["flex-column", "flex-col"],
|
|
@@ -306,6 +880,11 @@ const replacements$2 = [
|
|
|
306
880
|
["flex-grow-0", "grow-0"],
|
|
307
881
|
["flex-shrink-1", "shrink"],
|
|
308
882
|
["flex-shrink-0", "shrink-0"],
|
|
883
|
+
["text--secondary", "text-rui-text-secondary"],
|
|
884
|
+
["white--text", "text-white"],
|
|
885
|
+
["primary--text", "text-rui-primary"]
|
|
886
|
+
]);
|
|
887
|
+
const regexReplacements = [
|
|
309
888
|
[
|
|
310
889
|
/^align-(start|end|center|baseline|stretch)$/,
|
|
311
890
|
([align]) => `items-${align}`
|
|
@@ -320,9 +899,6 @@ const replacements$2 = [
|
|
|
320
899
|
([weight]) => `font-${weight}`
|
|
321
900
|
],
|
|
322
901
|
[/^text-(capitalize|uppercase|lowercase)$/, ([casing]) => casing],
|
|
323
|
-
["text--secondary", "text-rui-text-secondary"],
|
|
324
|
-
["white--text", "text-white"],
|
|
325
|
-
["primary--text", "text-rui-primary"],
|
|
326
902
|
[
|
|
327
903
|
/^([mp])([abelr-txy]?)-n(\d)$/,
|
|
328
904
|
([type, position, size]) => `-${type}${position === "a" ? "" : position}-${size}`
|
|
@@ -332,22 +908,14 @@ const replacements$2 = [
|
|
|
332
908
|
([type, size]) => `${type}-${size}`
|
|
333
909
|
]
|
|
334
910
|
];
|
|
335
|
-
function isString(replacement) {
|
|
336
|
-
return typeof replacement[0] === "string";
|
|
337
|
-
}
|
|
338
|
-
function isRegex(replacement) {
|
|
339
|
-
return replacement[0] instanceof RegExp;
|
|
340
|
-
}
|
|
341
911
|
function findReplacement(className) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
return replace(matches);
|
|
350
|
-
}
|
|
912
|
+
const direct = stringReplacements.get(className);
|
|
913
|
+
if (direct !== void 0)
|
|
914
|
+
return direct;
|
|
915
|
+
for (const [regex, replace] of regexReplacements) {
|
|
916
|
+
const matches = regex.exec(className);
|
|
917
|
+
if (matches)
|
|
918
|
+
return replace(matches.slice(1));
|
|
351
919
|
}
|
|
352
920
|
return void 0;
|
|
353
921
|
}
|
|
@@ -356,9 +924,8 @@ function getRange(node) {
|
|
|
356
924
|
return node.value.range;
|
|
357
925
|
return node.range;
|
|
358
926
|
}
|
|
359
|
-
function reportReplacement(className, replacement, node, context, position = 1) {
|
|
360
|
-
debug$
|
|
361
|
-
const source = getSourceCode(context);
|
|
927
|
+
function reportReplacement(className, replacement, node, context, source, position = 1) {
|
|
928
|
+
debug$5(`found replacement ${replacement} for ${className}`);
|
|
362
929
|
const initialRange = getRange(node);
|
|
363
930
|
const range = [
|
|
364
931
|
initialRange[0] + position,
|
|
@@ -433,6 +1000,7 @@ function* extractClassNames(node, textOnly = false) {
|
|
|
433
1000
|
}
|
|
434
1001
|
const noDeprecatedClasses = createEslintRule({
|
|
435
1002
|
create(context) {
|
|
1003
|
+
const source = getSourceCode(context);
|
|
436
1004
|
return defineTemplateBodyVisitor(context, {
|
|
437
1005
|
'VAttribute[directive=false][key.name="class"]': function(node) {
|
|
438
1006
|
if (!node.value || !node.value.value)
|
|
@@ -442,7 +1010,7 @@ const noDeprecatedClasses = createEslintRule({
|
|
|
442
1010
|
const position = node.value.value.indexOf(className) + 1;
|
|
443
1011
|
if (!replacement)
|
|
444
1012
|
continue;
|
|
445
|
-
reportReplacement(className, replacement, node, context, position);
|
|
1013
|
+
reportReplacement(className, replacement, node, context, source, position);
|
|
446
1014
|
}
|
|
447
1015
|
},
|
|
448
1016
|
"VAttribute[directive=true][key.name.name='bind'][key.argument.name='class'] > VExpressionContainer.value": function(node) {
|
|
@@ -452,7 +1020,7 @@ const noDeprecatedClasses = createEslintRule({
|
|
|
452
1020
|
const replacement = findReplacement(className);
|
|
453
1021
|
if (!replacement)
|
|
454
1022
|
continue;
|
|
455
|
-
reportReplacement(className, replacement, reportNode, context, position);
|
|
1023
|
+
reportReplacement(className, replacement, reportNode, context, source, position);
|
|
456
1024
|
}
|
|
457
1025
|
}
|
|
458
1026
|
});
|
|
@@ -470,11 +1038,11 @@ const noDeprecatedClasses = createEslintRule({
|
|
|
470
1038
|
schema: [],
|
|
471
1039
|
type: "problem"
|
|
472
1040
|
},
|
|
473
|
-
name: RULE_NAME$
|
|
1041
|
+
name: RULE_NAME$6
|
|
474
1042
|
});
|
|
475
1043
|
|
|
476
|
-
const debug$
|
|
477
|
-
const RULE_NAME$
|
|
1044
|
+
const debug$4 = debugFactory("@rotki/eslint-plugin:no-deprecated-components");
|
|
1045
|
+
const RULE_NAME$5 = "no-deprecated-components";
|
|
478
1046
|
const vuetify = {
|
|
479
1047
|
VApp: true,
|
|
480
1048
|
VAppBar: true,
|
|
@@ -507,9 +1075,9 @@ const replacements$1 = {
|
|
|
507
1075
|
Fragment: false,
|
|
508
1076
|
...vuetify
|
|
509
1077
|
};
|
|
510
|
-
const skipInLegacy = [
|
|
1078
|
+
const skipInLegacy = /* @__PURE__ */ new Set([
|
|
511
1079
|
"Fragment"
|
|
512
|
-
];
|
|
1080
|
+
]);
|
|
513
1081
|
function hasReplacement$1(tag) {
|
|
514
1082
|
return Object.prototype.hasOwnProperty.call(replacements$1, tag);
|
|
515
1083
|
}
|
|
@@ -517,17 +1085,17 @@ const noDeprecatedComponents = createEslintRule({
|
|
|
517
1085
|
create(context, optionsWithDefault) {
|
|
518
1086
|
const options = optionsWithDefault[0] || {};
|
|
519
1087
|
const legacy = options.legacy;
|
|
1088
|
+
const sourceCode = getSourceCode(context);
|
|
1089
|
+
if (sourceCode?.parserServices && !("getTemplateBodyTokenStore" in sourceCode.parserServices))
|
|
1090
|
+
throw new Error("cannot find getTemplateBodyTokenStore in parserServices");
|
|
520
1091
|
return defineTemplateBodyVisitor(context, {
|
|
521
1092
|
VElement(element) {
|
|
522
1093
|
const tag = pascalCase(element.rawName);
|
|
523
|
-
const sourceCode = getSourceCode(context);
|
|
524
|
-
if (sourceCode?.parserServices && !("getTemplateBodyTokenStore" in sourceCode.parserServices))
|
|
525
|
-
throw new Error("cannot find getTemplateBodyTokenStore in parserServices");
|
|
526
1094
|
if (!hasReplacement$1(tag))
|
|
527
1095
|
return;
|
|
528
1096
|
const replacement = replacements$1[tag];
|
|
529
|
-
if (replacement || legacy && skipInLegacy.
|
|
530
|
-
debug$
|
|
1097
|
+
if (replacement || legacy && skipInLegacy.has(tag)) {
|
|
1098
|
+
debug$4(`${tag} has been deprecated`);
|
|
531
1099
|
context.report({
|
|
532
1100
|
data: {
|
|
533
1101
|
name: tag
|
|
@@ -536,7 +1104,7 @@ const noDeprecatedComponents = createEslintRule({
|
|
|
536
1104
|
node: element
|
|
537
1105
|
});
|
|
538
1106
|
} else {
|
|
539
|
-
debug$
|
|
1107
|
+
debug$4(`${tag} has will be removed`);
|
|
540
1108
|
context.report({
|
|
541
1109
|
data: {
|
|
542
1110
|
name: tag
|
|
@@ -579,18 +1147,24 @@ const noDeprecatedComponents = createEslintRule({
|
|
|
579
1147
|
],
|
|
580
1148
|
type: "problem"
|
|
581
1149
|
},
|
|
582
|
-
name: RULE_NAME$
|
|
1150
|
+
name: RULE_NAME$5
|
|
583
1151
|
});
|
|
584
1152
|
|
|
585
|
-
const debug =
|
|
586
|
-
const RULE_NAME$
|
|
1153
|
+
const debug$3 = debugFactory("@rotki/eslint-plugin:no-deprecated-props");
|
|
1154
|
+
const RULE_NAME$4 = "no-deprecated-props";
|
|
587
1155
|
const replacements = {
|
|
588
1156
|
RuiRadio: {
|
|
589
1157
|
internalValue: "value"
|
|
590
1158
|
}
|
|
591
1159
|
};
|
|
1160
|
+
const replacementMaps = new Map(
|
|
1161
|
+
Object.entries(replacements).map(([tag, props]) => [
|
|
1162
|
+
tag,
|
|
1163
|
+
new Map(Object.entries(props).map(([prop, repl]) => [kebabCase(prop), { original: prop, replacement: repl }]))
|
|
1164
|
+
])
|
|
1165
|
+
);
|
|
592
1166
|
function hasReplacement(tag) {
|
|
593
|
-
return
|
|
1167
|
+
return replacementMaps.has(tag);
|
|
594
1168
|
}
|
|
595
1169
|
function getPropName(node) {
|
|
596
1170
|
if (node.directive) {
|
|
@@ -609,29 +1183,28 @@ const noDeprecatedProps = createEslintRule({
|
|
|
609
1183
|
const tag = pascalCase(node.parent.parent.rawName);
|
|
610
1184
|
if (!hasReplacement(tag))
|
|
611
1185
|
return;
|
|
612
|
-
debug(`${tag} has replacement properties`);
|
|
1186
|
+
debug$3(`${tag} has replacement properties`);
|
|
613
1187
|
const propName = getPropName(node);
|
|
614
1188
|
const propNameNode = node.directive ? node.key.argument : node.key;
|
|
615
1189
|
if (!propName || !propNameNode) {
|
|
616
|
-
debug("could not get prop name and/or node");
|
|
1190
|
+
debug$3("could not get prop name and/or node");
|
|
617
1191
|
return;
|
|
618
1192
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
});
|
|
1193
|
+
const match = replacementMaps.get(tag)?.get(propName);
|
|
1194
|
+
if (match) {
|
|
1195
|
+
debug$3(`preparing a replacement for ${tag}:${propName} -> ${match.replacement}`);
|
|
1196
|
+
context.report({
|
|
1197
|
+
data: {
|
|
1198
|
+
prop: match.original,
|
|
1199
|
+
replacement: match.replacement
|
|
1200
|
+
},
|
|
1201
|
+
fix(fixer) {
|
|
1202
|
+
return fixer.replaceText(propNameNode, match.replacement);
|
|
1203
|
+
},
|
|
1204
|
+
messageId: "replacedWith",
|
|
1205
|
+
node: propNameNode
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
635
1208
|
}
|
|
636
1209
|
});
|
|
637
1210
|
},
|
|
@@ -648,10 +1221,10 @@ const noDeprecatedProps = createEslintRule({
|
|
|
648
1221
|
schema: [],
|
|
649
1222
|
type: "problem"
|
|
650
1223
|
},
|
|
651
|
-
name: RULE_NAME$
|
|
1224
|
+
name: RULE_NAME$4
|
|
652
1225
|
});
|
|
653
1226
|
|
|
654
|
-
const RULE_NAME$
|
|
1227
|
+
const RULE_NAME$3 = "no-dot-ts-imports";
|
|
655
1228
|
const noDotTsImport = createEslintRule({
|
|
656
1229
|
create(context) {
|
|
657
1230
|
return {
|
|
@@ -659,8 +1232,7 @@ const noDotTsImport = createEslintRule({
|
|
|
659
1232
|
const importDeclaration = node.source.value;
|
|
660
1233
|
if (!importDeclaration.endsWith(".ts"))
|
|
661
1234
|
return;
|
|
662
|
-
const
|
|
663
|
-
const replacement = importDeclaration.substring(0, lastIndexOfExtension);
|
|
1235
|
+
const replacement = importDeclaration.slice(0, -3);
|
|
664
1236
|
context.report({
|
|
665
1237
|
data: {
|
|
666
1238
|
import: importDeclaration
|
|
@@ -687,10 +1259,10 @@ const noDotTsImport = createEslintRule({
|
|
|
687
1259
|
schema: [],
|
|
688
1260
|
type: "problem"
|
|
689
1261
|
},
|
|
690
|
-
name: RULE_NAME$
|
|
1262
|
+
name: RULE_NAME$3
|
|
691
1263
|
});
|
|
692
1264
|
|
|
693
|
-
const RULE_NAME = "no-legacy-library-import";
|
|
1265
|
+
const RULE_NAME$2 = "no-legacy-library-import";
|
|
694
1266
|
const legacyLibrary = "@rotki/ui-library-compat";
|
|
695
1267
|
const newLibrary = "@rotki/ui-library";
|
|
696
1268
|
const noLegacyLibraryImport = createEslintRule({
|
|
@@ -723,6 +1295,616 @@ const noLegacyLibraryImport = createEslintRule({
|
|
|
723
1295
|
schema: [],
|
|
724
1296
|
type: "problem"
|
|
725
1297
|
},
|
|
1298
|
+
name: RULE_NAME$2
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
const I18N_FUNCTION_NAMES = /* @__PURE__ */ new Set(["t", "te", "tc", "$t", "$te", "$tc"]);
|
|
1302
|
+
const I18N_MEMBER_NAMES = /* @__PURE__ */ new Set(["$t", "$te", "$tc"]);
|
|
1303
|
+
function isAstNode(value) {
|
|
1304
|
+
return value !== null && typeof value === "object" && "type" in value && typeof value.type === "string";
|
|
1305
|
+
}
|
|
1306
|
+
function isI18nCallExpression(node) {
|
|
1307
|
+
const callee = node.callee;
|
|
1308
|
+
if (!isAstNode(callee))
|
|
1309
|
+
return false;
|
|
1310
|
+
if (callee.type === "Identifier" && typeof callee.name === "string")
|
|
1311
|
+
return I18N_FUNCTION_NAMES.has(callee.name);
|
|
1312
|
+
return callee.type === "MemberExpression" && !callee.computed && isAstNode(callee.property) && callee.property.type === "Identifier" && typeof callee.property.name === "string" && I18N_MEMBER_NAMES.has(callee.property.name);
|
|
1313
|
+
}
|
|
1314
|
+
function getTemplateLiteralText(arg) {
|
|
1315
|
+
const quasis = arg.quasis;
|
|
1316
|
+
const expressions = arg.expressions;
|
|
1317
|
+
if (!Array.isArray(quasis) || !Array.isArray(expressions) || quasis.length === 0)
|
|
1318
|
+
return void 0;
|
|
1319
|
+
const firstQuasi = quasis[0];
|
|
1320
|
+
if (!firstQuasi || typeof firstQuasi !== "object" || !("value" in firstQuasi))
|
|
1321
|
+
return void 0;
|
|
1322
|
+
const quasiObj = firstQuasi.value;
|
|
1323
|
+
if (!quasiObj || typeof quasiObj !== "object")
|
|
1324
|
+
return void 0;
|
|
1325
|
+
return "cooked" in quasiObj && typeof quasiObj.cooked === "string" ? quasiObj.cooked : "raw" in quasiObj && typeof quasiObj.raw === "string" ? quasiObj.raw : void 0;
|
|
1326
|
+
}
|
|
1327
|
+
function extractKeysFromCallExpression(node, keys) {
|
|
1328
|
+
if (!isI18nCallExpression(node))
|
|
1329
|
+
return;
|
|
1330
|
+
const args = node.arguments;
|
|
1331
|
+
if (!Array.isArray(args) || args.length === 0)
|
|
1332
|
+
return;
|
|
1333
|
+
const arg = args[0];
|
|
1334
|
+
if (!isAstNode(arg))
|
|
1335
|
+
return;
|
|
1336
|
+
if (arg.type === "Literal" && typeof arg.value === "string") {
|
|
1337
|
+
keys.add(arg.value);
|
|
1338
|
+
} else if (arg.type === "TemplateLiteral") {
|
|
1339
|
+
const text = getTemplateLiteralText(arg);
|
|
1340
|
+
if (text === void 0)
|
|
1341
|
+
return;
|
|
1342
|
+
if (arg.expressions && Array.isArray(arg.expressions) && arg.expressions.length === 0 && Array.isArray(arg.quasis) && arg.quasis.length === 1)
|
|
1343
|
+
keys.add(text);
|
|
1344
|
+
else if (text)
|
|
1345
|
+
keys.add(`${text}*`);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
const TS_AST_CHILD_KEYS = /* @__PURE__ */ new Set([
|
|
1349
|
+
"alternate",
|
|
1350
|
+
"argument",
|
|
1351
|
+
"arguments",
|
|
1352
|
+
"block",
|
|
1353
|
+
"body",
|
|
1354
|
+
"callee",
|
|
1355
|
+
"cases",
|
|
1356
|
+
"consequent",
|
|
1357
|
+
"declaration",
|
|
1358
|
+
"declarations",
|
|
1359
|
+
"discriminant",
|
|
1360
|
+
"elements",
|
|
1361
|
+
"expression",
|
|
1362
|
+
"expressions",
|
|
1363
|
+
"handler",
|
|
1364
|
+
"init",
|
|
1365
|
+
"left",
|
|
1366
|
+
"object",
|
|
1367
|
+
"params",
|
|
1368
|
+
"properties",
|
|
1369
|
+
"property",
|
|
1370
|
+
"right",
|
|
1371
|
+
"source",
|
|
1372
|
+
"specifiers",
|
|
1373
|
+
"tag",
|
|
1374
|
+
"test",
|
|
1375
|
+
"update",
|
|
1376
|
+
"value"
|
|
1377
|
+
]);
|
|
1378
|
+
function walkTsAst(node, keys) {
|
|
1379
|
+
if (!isAstNode(node))
|
|
1380
|
+
return;
|
|
1381
|
+
if (node.type === "CallExpression") {
|
|
1382
|
+
extractKeysFromCallExpression(node, keys);
|
|
1383
|
+
}
|
|
1384
|
+
for (const key of TS_AST_CHILD_KEYS) {
|
|
1385
|
+
if (!(key in node))
|
|
1386
|
+
continue;
|
|
1387
|
+
const child = node[key];
|
|
1388
|
+
if (!child || typeof child !== "object")
|
|
1389
|
+
continue;
|
|
1390
|
+
if (Array.isArray(child)) {
|
|
1391
|
+
for (const item of child) {
|
|
1392
|
+
walkTsAst(item, keys);
|
|
1393
|
+
}
|
|
1394
|
+
} else {
|
|
1395
|
+
walkTsAst(child, keys);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
const debug$2 = debugFactory("@rotki/eslint-plugin:i18n-vue-template");
|
|
1401
|
+
function isVElement(node) {
|
|
1402
|
+
return "type" in node && node.type === "VElement";
|
|
1403
|
+
}
|
|
1404
|
+
function isVExpressionContainer(node) {
|
|
1405
|
+
return "type" in node && node.type === "VExpressionContainer";
|
|
1406
|
+
}
|
|
1407
|
+
function isKeypathAttribute(attr) {
|
|
1408
|
+
return attr.type === "VAttribute" && !attr.directive && (attr.key.name === "keypath" || attr.key.name === "path") && attr.value !== null && attr.value.type === "VLiteral";
|
|
1409
|
+
}
|
|
1410
|
+
function isVTDirective(attr) {
|
|
1411
|
+
return attr.type === "VAttribute" && attr.directive && attr.key.name.type === "VIdentifier" && attr.key.name.rawName === "t";
|
|
1412
|
+
}
|
|
1413
|
+
function getDirectiveExpression(attr) {
|
|
1414
|
+
if (attr.type !== "VAttribute" || !attr.directive)
|
|
1415
|
+
return null;
|
|
1416
|
+
if (!attr.value || attr.value.type !== "VExpressionContainer")
|
|
1417
|
+
return null;
|
|
1418
|
+
return attr.value.expression;
|
|
1419
|
+
}
|
|
1420
|
+
function extractKeysFromVueTemplate(templateBody, keys) {
|
|
1421
|
+
walkVueTemplateNode(templateBody, keys);
|
|
1422
|
+
}
|
|
1423
|
+
function walkVueTemplateNode(node, keys) {
|
|
1424
|
+
if (!node || typeof node !== "object")
|
|
1425
|
+
return;
|
|
1426
|
+
if (isVElement(node)) {
|
|
1427
|
+
if (node.rawName === "i18n-t" || node.name === "i18n-t") {
|
|
1428
|
+
for (const attr of node.startTag.attributes) {
|
|
1429
|
+
if (isKeypathAttribute(attr))
|
|
1430
|
+
keys.add(attr.value.value);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
for (const attr of node.startTag.attributes) {
|
|
1434
|
+
const expr = getDirectiveExpression(attr);
|
|
1435
|
+
if (!expr)
|
|
1436
|
+
continue;
|
|
1437
|
+
if (isVTDirective(attr) && expr.type === "Literal" && typeof expr.value === "string")
|
|
1438
|
+
keys.add(expr.value);
|
|
1439
|
+
walkTsAst(expr, keys);
|
|
1440
|
+
}
|
|
1441
|
+
for (const child of node.children)
|
|
1442
|
+
walkVueTemplateNode(child, keys);
|
|
1443
|
+
} else if (isVExpressionContainer(node)) {
|
|
1444
|
+
if (node.expression)
|
|
1445
|
+
walkTsAst(node.expression, keys);
|
|
1446
|
+
} else if ("children" in node && Array.isArray(node.children)) {
|
|
1447
|
+
for (const child of node.children) {
|
|
1448
|
+
if (child && typeof child === "object")
|
|
1449
|
+
walkVueTemplateNode(child, keys);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
function extractKeysFromSfcI18nBlock(content, keys) {
|
|
1454
|
+
const i18nBlockRegex = /<i18n(?:\s[^>]*)?>([^]*?)<\/i18n>/g;
|
|
1455
|
+
let match = i18nBlockRegex.exec(content);
|
|
1456
|
+
while (match !== null) {
|
|
1457
|
+
const blockContent = match[1].trim();
|
|
1458
|
+
if (blockContent) {
|
|
1459
|
+
try {
|
|
1460
|
+
const parsed = JSON.parse(blockContent);
|
|
1461
|
+
collectJsonKeys(parsed, "", keys);
|
|
1462
|
+
} catch (error) {
|
|
1463
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1464
|
+
debug$2("Failed to parse <i18n> block as JSON: %s", message);
|
|
1465
|
+
console.warn(`[@rotki/eslint-plugin] Failed to parse <i18n> block as JSON. If you are using JSON5, it is not supported. Error: ${message}`);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
match = i18nBlockRegex.exec(content);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
function collectJsonKeys(obj, prefix, keys) {
|
|
1472
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj))
|
|
1473
|
+
return;
|
|
1474
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1475
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
1476
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
1477
|
+
collectJsonKeys(value, fullKey, keys);
|
|
1478
|
+
} else {
|
|
1479
|
+
keys.add(fullKey);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
const debug$1 = debugFactory("@rotki/eslint-plugin:i18n-key-collector");
|
|
1485
|
+
const fileCache = /* @__PURE__ */ new Map();
|
|
1486
|
+
let cachedUsedKeys;
|
|
1487
|
+
const I18N_CALL_PATTERN = /\bt\s*\(|\bte\s*\(|\btc\s*\(|\$t\s*\(|\$te\s*\(|\$tc\s*\(/;
|
|
1488
|
+
const VUE_I18N_PATTERN = /\bt\s*\(|\bte\s*\(|\btc\s*\(|\$t\s*\(|\$te\s*\(|\$tc\s*\(|<i18n[\s>-]|v-t\b/;
|
|
1489
|
+
function parseSourceFile(content, filePath) {
|
|
1490
|
+
const isVue = filePath.endsWith(".vue");
|
|
1491
|
+
const source = isVue ? content : `<script lang="ts">
|
|
1492
|
+
${content}
|
|
1493
|
+
<\/script>`;
|
|
1494
|
+
try {
|
|
1495
|
+
return parse(source, {
|
|
1496
|
+
parser: "@typescript-eslint/parser",
|
|
1497
|
+
sourceType: "module"
|
|
1498
|
+
});
|
|
1499
|
+
} catch (error) {
|
|
1500
|
+
debug$1(`Failed to parse file ${filePath}: ${String(error)}`);
|
|
1501
|
+
return void 0;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
function collectKeysFromFile(filePath) {
|
|
1505
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1506
|
+
let mtimeMs;
|
|
1507
|
+
try {
|
|
1508
|
+
mtimeMs = statSync(filePath).mtimeMs;
|
|
1509
|
+
} catch {
|
|
1510
|
+
return keys;
|
|
1511
|
+
}
|
|
1512
|
+
const cached = fileCache.get(filePath);
|
|
1513
|
+
if (cached && cached.mtimeMs === mtimeMs) {
|
|
1514
|
+
return cached.keys;
|
|
1515
|
+
}
|
|
1516
|
+
let content;
|
|
1517
|
+
try {
|
|
1518
|
+
content = readFileSync(filePath, "utf-8");
|
|
1519
|
+
} catch {
|
|
1520
|
+
return keys;
|
|
1521
|
+
}
|
|
1522
|
+
const isVue = filePath.endsWith(".vue");
|
|
1523
|
+
const pattern = isVue ? VUE_I18N_PATTERN : I18N_CALL_PATTERN;
|
|
1524
|
+
if (!pattern.test(content)) {
|
|
1525
|
+
fileCache.set(filePath, { keys, mtimeMs });
|
|
1526
|
+
return keys;
|
|
1527
|
+
}
|
|
1528
|
+
const ast = parseSourceFile(content, filePath);
|
|
1529
|
+
if (!ast)
|
|
1530
|
+
return keys;
|
|
1531
|
+
if (isVue) {
|
|
1532
|
+
if (ast.templateBody) {
|
|
1533
|
+
extractKeysFromVueTemplate(ast.templateBody, keys);
|
|
1534
|
+
}
|
|
1535
|
+
extractKeysFromSfcI18nBlock(content, keys);
|
|
1536
|
+
}
|
|
1537
|
+
if (ast.body) {
|
|
1538
|
+
for (const node of ast.body) {
|
|
1539
|
+
walkTsAst(node, keys);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
fileCache.set(filePath, { keys, mtimeMs });
|
|
1543
|
+
return keys;
|
|
1544
|
+
}
|
|
1545
|
+
function collectAllUsedKeys(srcDir, extensions) {
|
|
1546
|
+
const resolvedSrc = resolve(srcDir);
|
|
1547
|
+
if (cachedUsedKeys && cachedUsedKeys.srcDir === resolvedSrc && cachedUsedKeys.extensions.join(",") === extensions.join(",")) {
|
|
1548
|
+
return cachedUsedKeys.keys;
|
|
1549
|
+
}
|
|
1550
|
+
const patterns = extensions.map((ext) => `**/*${ext}`);
|
|
1551
|
+
const files = globSync(patterns, { absolute: true, cwd: resolvedSrc });
|
|
1552
|
+
debug$1(`Found ${files.length} source files in ${resolvedSrc}`);
|
|
1553
|
+
const allKeys = /* @__PURE__ */ new Set();
|
|
1554
|
+
for (const file of files) {
|
|
1555
|
+
const keys = collectKeysFromFile(file);
|
|
1556
|
+
for (const key of keys) {
|
|
1557
|
+
allKeys.add(key);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
cachedUsedKeys = { extensions, keys: allKeys, srcDir: resolvedSrc };
|
|
1561
|
+
return allKeys;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
function extractLinkedKeys(value) {
|
|
1565
|
+
const linkedKeyRegex = /@(?:\.\w+)?:(?:{([^}]+)}|(\w+(?:\.\w+)*))/g;
|
|
1566
|
+
const keys = [];
|
|
1567
|
+
let match = linkedKeyRegex.exec(value);
|
|
1568
|
+
while (match !== null) {
|
|
1569
|
+
const key = match[1] || match[2];
|
|
1570
|
+
if (key)
|
|
1571
|
+
keys.push(key);
|
|
1572
|
+
match = linkedKeyRegex.exec(value);
|
|
1573
|
+
}
|
|
1574
|
+
return keys;
|
|
1575
|
+
}
|
|
1576
|
+
function prepareUsedKeys(usedKeys, ignorePatterns) {
|
|
1577
|
+
const directKeys = /* @__PURE__ */ new Set();
|
|
1578
|
+
const wildcardPrefixes = [];
|
|
1579
|
+
for (const key of usedKeys) {
|
|
1580
|
+
if (key.endsWith("*")) {
|
|
1581
|
+
wildcardPrefixes.push(key.slice(0, -1));
|
|
1582
|
+
} else {
|
|
1583
|
+
directKeys.add(key);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
const ignoreRegexps = ignorePatterns.map(
|
|
1587
|
+
(pattern) => new RegExp(`^${pattern.replace(/[$()+?[\\\]^{|}]/g, "\\$&").replace(/\./g, "\\.").replace(/\*/g, ".*")}$`)
|
|
1588
|
+
);
|
|
1589
|
+
return { directKeys, ignoreRegexps, wildcardPrefixes };
|
|
1590
|
+
}
|
|
1591
|
+
function isKeyUsed(keyPath, prepared) {
|
|
1592
|
+
if (prepared.directKeys.has(keyPath))
|
|
1593
|
+
return true;
|
|
1594
|
+
let dotIndex = keyPath.indexOf(".");
|
|
1595
|
+
while (dotIndex !== -1) {
|
|
1596
|
+
if (prepared.directKeys.has(keyPath.slice(0, dotIndex)))
|
|
1597
|
+
return true;
|
|
1598
|
+
dotIndex = keyPath.indexOf(".", dotIndex + 1);
|
|
1599
|
+
}
|
|
1600
|
+
for (const prefix of prepared.wildcardPrefixes) {
|
|
1601
|
+
if (keyPath.startsWith(prefix))
|
|
1602
|
+
return true;
|
|
1603
|
+
}
|
|
1604
|
+
for (const regex of prepared.ignoreRegexps) {
|
|
1605
|
+
if (regex.test(keyPath))
|
|
1606
|
+
return true;
|
|
1607
|
+
}
|
|
1608
|
+
return false;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
const RULE_NAME$1 = "no-unused-i18n-keys";
|
|
1612
|
+
const debug = debugFactory("@rotki/eslint-plugin:no-unused-i18n-keys");
|
|
1613
|
+
function isLocaleFile(filename) {
|
|
1614
|
+
return /(?:locales?|i18n|translations?|messages?|lang)\b/i.test(filename);
|
|
1615
|
+
}
|
|
1616
|
+
function isJsonProgram(ast) {
|
|
1617
|
+
if (!ast || typeof ast !== "object" || !("type" in ast) || ast.type !== "Program")
|
|
1618
|
+
return false;
|
|
1619
|
+
if (!("body" in ast) || !Array.isArray(ast.body) || ast.body.length === 0)
|
|
1620
|
+
return false;
|
|
1621
|
+
const first = ast.body[0];
|
|
1622
|
+
return first !== null && typeof first === "object" && "type" in first && first.type === "JSONExpressionStatement";
|
|
1623
|
+
}
|
|
1624
|
+
function isYamlProgram(ast) {
|
|
1625
|
+
if (!ast || typeof ast !== "object" || !("type" in ast) || ast.type !== "Program")
|
|
1626
|
+
return false;
|
|
1627
|
+
if (!("body" in ast) || !Array.isArray(ast.body) || ast.body.length === 0)
|
|
1628
|
+
return false;
|
|
1629
|
+
const first = ast.body[0];
|
|
1630
|
+
return first !== null && typeof first === "object" && "type" in first && first.type === "YAMLDocument";
|
|
1631
|
+
}
|
|
1632
|
+
function buildJsonKeyPaths(node, prefix, paths) {
|
|
1633
|
+
for (const prop of node.properties) {
|
|
1634
|
+
const keyName = prop.key.type === "JSONLiteral" ? String(prop.key.value) : prop.key.name;
|
|
1635
|
+
const fullKey = prefix ? `${prefix}.${keyName}` : keyName;
|
|
1636
|
+
if (prop.value.type === "JSONObjectExpression") {
|
|
1637
|
+
buildJsonKeyPaths(prop.value, fullKey, paths);
|
|
1638
|
+
} else {
|
|
1639
|
+
paths.push({ key: fullKey, node: prop });
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
function collectJsonLinkedKeys(node, linkedKeys) {
|
|
1644
|
+
for (const prop of node.properties) {
|
|
1645
|
+
if (prop.value.type === "JSONLiteral" && typeof prop.value.value === "string") {
|
|
1646
|
+
for (const key of extractLinkedKeys(prop.value.value)) {
|
|
1647
|
+
linkedKeys.add(key);
|
|
1648
|
+
}
|
|
1649
|
+
} else if (prop.value.type === "JSONObjectExpression") {
|
|
1650
|
+
collectJsonLinkedKeys(prop.value, linkedKeys);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
function getYamlScalarValue(node) {
|
|
1655
|
+
if (!node)
|
|
1656
|
+
return void 0;
|
|
1657
|
+
if (node.type === "YAMLWithMeta")
|
|
1658
|
+
return getYamlScalarValue(node.value);
|
|
1659
|
+
if (node.type === "YAMLScalar" && typeof node.value === "string")
|
|
1660
|
+
return node.value;
|
|
1661
|
+
return void 0;
|
|
1662
|
+
}
|
|
1663
|
+
function getYamlKeyName(node) {
|
|
1664
|
+
if (!node)
|
|
1665
|
+
return void 0;
|
|
1666
|
+
if (node.type === "YAMLWithMeta")
|
|
1667
|
+
return getYamlKeyName(node.value);
|
|
1668
|
+
if (node.type === "YAMLScalar")
|
|
1669
|
+
return String(node.value);
|
|
1670
|
+
return void 0;
|
|
1671
|
+
}
|
|
1672
|
+
function isYamlMapping(node) {
|
|
1673
|
+
if (!node)
|
|
1674
|
+
return false;
|
|
1675
|
+
if (node.type === "YAMLMapping")
|
|
1676
|
+
return true;
|
|
1677
|
+
if (node.type === "YAMLWithMeta" && node.value?.type === "YAMLMapping")
|
|
1678
|
+
return true;
|
|
1679
|
+
return false;
|
|
1680
|
+
}
|
|
1681
|
+
function getYamlMapping(node) {
|
|
1682
|
+
if (!node)
|
|
1683
|
+
return null;
|
|
1684
|
+
if (node.type === "YAMLMapping")
|
|
1685
|
+
return node;
|
|
1686
|
+
if (node.type === "YAMLWithMeta" && node.value?.type === "YAMLMapping")
|
|
1687
|
+
return node.value;
|
|
1688
|
+
return null;
|
|
1689
|
+
}
|
|
1690
|
+
function buildYamlKeyPaths(node, prefix, paths) {
|
|
1691
|
+
for (const pair of node.pairs) {
|
|
1692
|
+
const keyName = getYamlKeyName(pair.key);
|
|
1693
|
+
if (keyName === void 0)
|
|
1694
|
+
continue;
|
|
1695
|
+
const fullKey = prefix ? `${prefix}.${keyName}` : keyName;
|
|
1696
|
+
if (isYamlMapping(pair.value)) {
|
|
1697
|
+
const mapping = getYamlMapping(pair.value);
|
|
1698
|
+
if (mapping) {
|
|
1699
|
+
buildYamlKeyPaths(mapping, fullKey, paths);
|
|
1700
|
+
}
|
|
1701
|
+
} else {
|
|
1702
|
+
paths.push({ key: fullKey, node: pair });
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
function collectYamlLinkedKeys(node, linkedKeys) {
|
|
1707
|
+
for (const pair of node.pairs) {
|
|
1708
|
+
const value = getYamlScalarValue(pair.value);
|
|
1709
|
+
if (value) {
|
|
1710
|
+
for (const key of extractLinkedKeys(value)) {
|
|
1711
|
+
linkedKeys.add(key);
|
|
1712
|
+
}
|
|
1713
|
+
} else {
|
|
1714
|
+
const mapping = getYamlMapping(pair.value);
|
|
1715
|
+
if (mapping) {
|
|
1716
|
+
collectYamlLinkedKeys(mapping, linkedKeys);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
function removeJsonProperty(prop) {
|
|
1722
|
+
const parent = prop.parent;
|
|
1723
|
+
const index = parent.properties.indexOf(prop);
|
|
1724
|
+
const isLast = index === parent.properties.length - 1;
|
|
1725
|
+
const isOnly = parent.properties.length === 1;
|
|
1726
|
+
let start = prop.range[0];
|
|
1727
|
+
let end = prop.range[1];
|
|
1728
|
+
if (isOnly) {
|
|
1729
|
+
return { end, start };
|
|
1730
|
+
}
|
|
1731
|
+
if (isLast) {
|
|
1732
|
+
const prev = parent.properties[index - 1];
|
|
1733
|
+
start = prev.range[1];
|
|
1734
|
+
} else {
|
|
1735
|
+
end = parent.properties[index + 1].range[0];
|
|
1736
|
+
}
|
|
1737
|
+
return { end, start };
|
|
1738
|
+
}
|
|
1739
|
+
function removeYamlPair(pair) {
|
|
1740
|
+
const parent = pair.parent;
|
|
1741
|
+
const index = parent.pairs.indexOf(pair);
|
|
1742
|
+
const isLast = index === parent.pairs.length - 1;
|
|
1743
|
+
const isOnly = parent.pairs.length === 1;
|
|
1744
|
+
let start = pair.range[0];
|
|
1745
|
+
let end = pair.range[1];
|
|
1746
|
+
if (isOnly) {
|
|
1747
|
+
return { end, start };
|
|
1748
|
+
}
|
|
1749
|
+
if (isLast) {
|
|
1750
|
+
const prev = parent.pairs[index - 1];
|
|
1751
|
+
start = prev.range[1];
|
|
1752
|
+
} else {
|
|
1753
|
+
end = parent.pairs[index + 1].range[0];
|
|
1754
|
+
}
|
|
1755
|
+
return { end, start };
|
|
1756
|
+
}
|
|
1757
|
+
const noUnusedI18nKeys = createEslintRule({
|
|
1758
|
+
create(context, optionsWithDefault) {
|
|
1759
|
+
const options = optionsWithDefault[0];
|
|
1760
|
+
const filename = getFilename(context);
|
|
1761
|
+
if (!isLocaleFile(filename)) {
|
|
1762
|
+
return {};
|
|
1763
|
+
}
|
|
1764
|
+
const sourceCode = getSourceCode(context);
|
|
1765
|
+
const ast = sourceCode.ast;
|
|
1766
|
+
if (isJsonProgram(ast)) {
|
|
1767
|
+
debug(`Processing JSON locale file: ${filename}`);
|
|
1768
|
+
const rootExpr = ast.body[0].expression;
|
|
1769
|
+
if (rootExpr.type !== "JSONObjectExpression")
|
|
1770
|
+
return {};
|
|
1771
|
+
const usedKeys = collectAllUsedKeys(options.src, options.extensions);
|
|
1772
|
+
const linkedKeys = /* @__PURE__ */ new Set();
|
|
1773
|
+
collectJsonLinkedKeys(rootExpr, linkedKeys);
|
|
1774
|
+
const allUsedKeys = /* @__PURE__ */ new Set([...usedKeys, ...linkedKeys]);
|
|
1775
|
+
const prepared = prepareUsedKeys(allUsedKeys, options.ignoreKeys);
|
|
1776
|
+
const paths = [];
|
|
1777
|
+
buildJsonKeyPaths(rootExpr, "", paths);
|
|
1778
|
+
return {
|
|
1779
|
+
"Program:exit": function() {
|
|
1780
|
+
for (const { key, node } of paths) {
|
|
1781
|
+
if (!isKeyUsed(key, prepared)) {
|
|
1782
|
+
const loc = node.loc;
|
|
1783
|
+
context.report({
|
|
1784
|
+
data: { key },
|
|
1785
|
+
fix(fixer) {
|
|
1786
|
+
const { end, start } = removeJsonProperty(node);
|
|
1787
|
+
return fixer.removeRange([start, end]);
|
|
1788
|
+
},
|
|
1789
|
+
loc,
|
|
1790
|
+
messageId: "unused"
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
if (isYamlProgram(ast)) {
|
|
1798
|
+
debug(`Processing YAML locale file: ${filename}`);
|
|
1799
|
+
const doc = ast.body[0];
|
|
1800
|
+
const content = doc.content;
|
|
1801
|
+
const mapping = getYamlMapping(content);
|
|
1802
|
+
if (!mapping)
|
|
1803
|
+
return {};
|
|
1804
|
+
const usedKeys = collectAllUsedKeys(options.src, options.extensions);
|
|
1805
|
+
const linkedKeys = /* @__PURE__ */ new Set();
|
|
1806
|
+
collectYamlLinkedKeys(mapping, linkedKeys);
|
|
1807
|
+
const allUsedKeys = /* @__PURE__ */ new Set([...usedKeys, ...linkedKeys]);
|
|
1808
|
+
const prepared = prepareUsedKeys(allUsedKeys, options.ignoreKeys);
|
|
1809
|
+
const paths = [];
|
|
1810
|
+
buildYamlKeyPaths(mapping, "", paths);
|
|
1811
|
+
return {
|
|
1812
|
+
"Program:exit": function() {
|
|
1813
|
+
for (const { key, node } of paths) {
|
|
1814
|
+
if (!isKeyUsed(key, prepared)) {
|
|
1815
|
+
const loc = node.loc;
|
|
1816
|
+
context.report({
|
|
1817
|
+
data: { key },
|
|
1818
|
+
fix(fixer) {
|
|
1819
|
+
const { end, start } = removeYamlPair(node);
|
|
1820
|
+
return fixer.removeRange([start, end]);
|
|
1821
|
+
},
|
|
1822
|
+
loc,
|
|
1823
|
+
messageId: "unused"
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
return {};
|
|
1831
|
+
},
|
|
1832
|
+
defaultOptions: [{
|
|
1833
|
+
extensions: [".vue", ".ts"],
|
|
1834
|
+
ignoreKeys: [],
|
|
1835
|
+
src: "src"
|
|
1836
|
+
}],
|
|
1837
|
+
meta: {
|
|
1838
|
+
docs: {
|
|
1839
|
+
description: "disallow unused i18n keys in locale files",
|
|
1840
|
+
recommendation: "recommended"
|
|
1841
|
+
},
|
|
1842
|
+
fixable: "code",
|
|
1843
|
+
messages: {
|
|
1844
|
+
unused: `The i18n key '{{ key }}' is unused`
|
|
1845
|
+
},
|
|
1846
|
+
schema: [
|
|
1847
|
+
{
|
|
1848
|
+
additionalProperties: false,
|
|
1849
|
+
properties: {
|
|
1850
|
+
extensions: {
|
|
1851
|
+
items: { type: "string" },
|
|
1852
|
+
type: "array"
|
|
1853
|
+
},
|
|
1854
|
+
ignoreKeys: {
|
|
1855
|
+
items: { type: "string" },
|
|
1856
|
+
type: "array"
|
|
1857
|
+
},
|
|
1858
|
+
src: {
|
|
1859
|
+
type: "string"
|
|
1860
|
+
}
|
|
1861
|
+
},
|
|
1862
|
+
type: "object"
|
|
1863
|
+
}
|
|
1864
|
+
],
|
|
1865
|
+
type: "suggestion"
|
|
1866
|
+
},
|
|
1867
|
+
name: RULE_NAME$1
|
|
1868
|
+
});
|
|
1869
|
+
|
|
1870
|
+
const RULE_NAME = "require-jsdoc-on-composable-options";
|
|
1871
|
+
const OPTIONS_PATTERN = /^Use\w+Options$/;
|
|
1872
|
+
const requireJsdocOnComposableOptions = createEslintRule({
|
|
1873
|
+
create(context) {
|
|
1874
|
+
const source = getSourceCode(context);
|
|
1875
|
+
return {
|
|
1876
|
+
TSInterfaceDeclaration(node) {
|
|
1877
|
+
if (!OPTIONS_PATTERN.test(node.id.name))
|
|
1878
|
+
return;
|
|
1879
|
+
for (const member of node.body.body) {
|
|
1880
|
+
if (member.type !== AST_NODE_TYPES.TSPropertySignature)
|
|
1881
|
+
continue;
|
|
1882
|
+
const comments = source.getCommentsBefore(member);
|
|
1883
|
+
const hasJsdoc = comments.some((comment) => comment.type === "Block" && comment.value.startsWith("*"));
|
|
1884
|
+
if (!hasJsdoc) {
|
|
1885
|
+
const propertyName = member.key.type === AST_NODE_TYPES.Identifier ? member.key.name : source.getText(member.key);
|
|
1886
|
+
context.report({
|
|
1887
|
+
data: { interfaceName: node.id.name, property: propertyName },
|
|
1888
|
+
messageId: "missingJsdoc",
|
|
1889
|
+
node: member
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
};
|
|
1895
|
+
},
|
|
1896
|
+
defaultOptions: [],
|
|
1897
|
+
meta: {
|
|
1898
|
+
docs: {
|
|
1899
|
+
description: "Require JSDoc comments on composable options interface properties",
|
|
1900
|
+
recommendation: "stylistic"
|
|
1901
|
+
},
|
|
1902
|
+
messages: {
|
|
1903
|
+
missingJsdoc: "Property '{{ property }}' in '{{ interfaceName }}' should have a JSDoc comment."
|
|
1904
|
+
},
|
|
1905
|
+
schema: [],
|
|
1906
|
+
type: "suggestion"
|
|
1907
|
+
},
|
|
726
1908
|
name: RULE_NAME
|
|
727
1909
|
});
|
|
728
1910
|
|
|
@@ -732,19 +1914,32 @@ const plugin = {
|
|
|
732
1914
|
version: pkg.version
|
|
733
1915
|
},
|
|
734
1916
|
rules: {
|
|
1917
|
+
"composable-input-flexibility": composableInputFlexibility,
|
|
1918
|
+
"composable-naming-convention": composableNamingConvention,
|
|
1919
|
+
"composable-no-default-export": composableNoDefaultExport,
|
|
1920
|
+
"composable-prefer-shallowref": composablePreferShallowref,
|
|
1921
|
+
"composable-require-cleanup": composableRequireCleanup,
|
|
1922
|
+
"composable-return-readonly": composableReturnReadonly,
|
|
1923
|
+
"composable-ssr-safety": composableSsrSafety,
|
|
735
1924
|
"consistent-ref-type-annotation": consistentRefTypeAnnotation,
|
|
736
1925
|
"max-dependencies": maxDependencies,
|
|
737
1926
|
"no-deprecated-classes": noDeprecatedClasses,
|
|
738
1927
|
"no-deprecated-components": noDeprecatedComponents,
|
|
739
1928
|
"no-deprecated-props": noDeprecatedProps,
|
|
740
1929
|
"no-dot-ts-imports": noDotTsImport,
|
|
741
|
-
"no-legacy-library-import": noLegacyLibraryImport
|
|
1930
|
+
"no-legacy-library-import": noLegacyLibraryImport,
|
|
1931
|
+
"no-unused-i18n-keys": noUnusedI18nKeys,
|
|
1932
|
+
"require-jsdoc-on-composable-options": requireJsdocOnComposableOptions
|
|
742
1933
|
}
|
|
743
1934
|
};
|
|
744
1935
|
|
|
745
1936
|
const configs = {
|
|
746
1937
|
"recommended": createRecommended(plugin, "@rotki", false),
|
|
747
|
-
"recommended-flat": createRecommended(plugin, "@rotki", true)
|
|
1938
|
+
"recommended-flat": createRecommended(plugin, "@rotki", true),
|
|
1939
|
+
"strict": createConfig(plugin, "@rotki", false, "strict"),
|
|
1940
|
+
"strict-flat": createConfig(plugin, "@rotki", true, "strict"),
|
|
1941
|
+
"stylistic": createConfig(plugin, "@rotki", false, "stylistic"),
|
|
1942
|
+
"stylistic-flat": createConfig(plugin, "@rotki", true, "stylistic")
|
|
748
1943
|
};
|
|
749
1944
|
|
|
750
1945
|
const index = Object.assign(plugin, { configs });
|