@rotki/eslint-plugin 1.3.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,11 +38,9 @@ export default [
38
38
 
39
39
  Or configure individual rules:
40
40
 
41
- <!-- eslint-disable perfectionist/sort-imports -->
42
-
43
41
  ```js
44
- import * as jsoncParser from 'jsonc-eslint-parser';
45
42
  import rotkiPlugin from '@rotki/eslint-plugin';
43
+ import * as jsoncParser from 'jsonc-eslint-parser';
46
44
 
47
45
  export default [
48
46
  {
@@ -62,20 +60,10 @@ export default [
62
60
  ];
63
61
  ```
64
62
 
65
- <!-- eslint-enable perfectionist/sort-imports -->
66
-
67
63
  ## Rules
68
64
 
69
- | Rule | Description | Fixable |
70
- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------- | ----------- |
71
- | [consistent-ref-type-annotation](https://rotki.github.io/eslint-plugin/rules/consistent-ref-type-annotation) | Ensures consistent type annotation position for ref, computed assignments | :black_nib: |
72
- | [max-dependencies](https://rotki.github.io/eslint-plugin/rules/max-dependencies) | Enforce a maximum number of dependencies per file | |
73
- | [no-deprecated-classes](https://rotki.github.io/eslint-plugin/rules/no-deprecated-classes) | Disallow deprecated vuetify css classes | :black_nib: |
74
- | [no-deprecated-components](https://rotki.github.io/eslint-plugin/rules/no-deprecated-components) | Disallow deprecated components | :black_nib: |
75
- | [no-deprecated-props](https://rotki.github.io/eslint-plugin/rules/no-deprecated-props) | Replace deprecated props with their replacements | :black_nib: |
76
- | [no-dot-ts-imports](https://rotki.github.io/eslint-plugin/rules/no-dot-ts-imports) | Disallow .ts extension in import statements | :black_nib: |
77
- | [no-legacy-library-import](https://rotki.github.io/eslint-plugin/rules/no-legacy-library-import) | Disallow imports from @rotki/ui-library-compat | :black_nib: |
78
- | [no-unused-i18n-keys](https://rotki.github.io/eslint-plugin/rules/no-unused-i18n-keys) | Disallow unused i18n keys in locale files | :black_nib: |
65
+ See the [full list of available rules](https://rotki.github.io/eslint-plugin/rules/),
66
+ including which are enabled by the `recommended` preset and which are auto-fixable.
79
67
 
80
68
  ## Documentation
81
69
 
package/dist/index.d.mts CHANGED
@@ -27,6 +27,7 @@ type Options$3 = [{
27
27
 
28
28
  type Options$2 = [{
29
29
  autofix?: boolean;
30
+ writablePrefixes?: string[];
30
31
  }];
31
32
 
32
33
  type Options$1 = [{
@@ -57,6 +58,7 @@ declare const plugin: {
57
58
  'no-deprecated-props': PluginRuleModule<[]>;
58
59
  'no-dot-ts-imports': PluginRuleModule<[]>;
59
60
  'no-legacy-library-import': PluginRuleModule<[]>;
61
+ 'no-redundant-flex-row': PluginRuleModule<[]>;
60
62
  'no-unused-i18n-keys': PluginRuleModule<Options$6>;
61
63
  'require-jsdoc-on-composable-options': PluginRuleModule<[]>;
62
64
  };
@@ -82,6 +84,7 @@ declare const _default: {
82
84
  'no-deprecated-props': PluginRuleModule<[]>;
83
85
  'no-dot-ts-imports': PluginRuleModule<[]>;
84
86
  'no-legacy-library-import': PluginRuleModule<[]>;
87
+ 'no-redundant-flex-row': PluginRuleModule<[]>;
85
88
  'no-unused-i18n-keys': PluginRuleModule<Options$6>;
86
89
  'require-jsdoc-on-composable-options': PluginRuleModule<[]>;
87
90
  };
@@ -109,6 +112,7 @@ declare const _default: {
109
112
  'no-deprecated-props': PluginRuleModule<[]>;
110
113
  'no-dot-ts-imports': PluginRuleModule<[]>;
111
114
  'no-legacy-library-import': PluginRuleModule<[]>;
115
+ 'no-redundant-flex-row': PluginRuleModule<[]>;
112
116
  'no-unused-i18n-keys': PluginRuleModule<Options$6>;
113
117
  'require-jsdoc-on-composable-options': PluginRuleModule<[]>;
114
118
  };
@@ -145,6 +149,7 @@ declare const _default: {
145
149
  'no-deprecated-props': PluginRuleModule<[]>;
146
150
  'no-dot-ts-imports': PluginRuleModule<[]>;
147
151
  'no-legacy-library-import': PluginRuleModule<[]>;
152
+ 'no-redundant-flex-row': PluginRuleModule<[]>;
148
153
  'no-unused-i18n-keys': PluginRuleModule<Options$6>;
149
154
  'require-jsdoc-on-composable-options': PluginRuleModule<[]>;
150
155
  };
@@ -181,6 +186,7 @@ declare const _default: {
181
186
  'no-deprecated-props': PluginRuleModule<[]>;
182
187
  'no-dot-ts-imports': PluginRuleModule<[]>;
183
188
  'no-legacy-library-import': PluginRuleModule<[]>;
189
+ 'no-redundant-flex-row': PluginRuleModule<[]>;
184
190
  'no-unused-i18n-keys': PluginRuleModule<Options$6>;
185
191
  'require-jsdoc-on-composable-options': PluginRuleModule<[]>;
186
192
  };
@@ -217,6 +223,7 @@ declare const _default: {
217
223
  'no-deprecated-props': PluginRuleModule<[]>;
218
224
  'no-dot-ts-imports': PluginRuleModule<[]>;
219
225
  'no-legacy-library-import': PluginRuleModule<[]>;
226
+ 'no-redundant-flex-row': PluginRuleModule<[]>;
220
227
  'no-unused-i18n-keys': PluginRuleModule<Options$6>;
221
228
  'require-jsdoc-on-composable-options': PluginRuleModule<[]>;
222
229
  };
@@ -253,6 +260,7 @@ declare const _default: {
253
260
  'no-deprecated-props': PluginRuleModule<[]>;
254
261
  'no-dot-ts-imports': PluginRuleModule<[]>;
255
262
  'no-legacy-library-import': PluginRuleModule<[]>;
263
+ 'no-redundant-flex-row': PluginRuleModule<[]>;
256
264
  'no-unused-i18n-keys': PluginRuleModule<Options$6>;
257
265
  'require-jsdoc-on-composable-options': PluginRuleModule<[]>;
258
266
  };
@@ -289,6 +297,7 @@ declare const _default: {
289
297
  'no-deprecated-props': PluginRuleModule<[]>;
290
298
  'no-dot-ts-imports': PluginRuleModule<[]>;
291
299
  'no-legacy-library-import': PluginRuleModule<[]>;
300
+ 'no-redundant-flex-row': PluginRuleModule<[]>;
292
301
  'no-unused-i18n-keys': PluginRuleModule<Options$6>;
293
302
  'require-jsdoc-on-composable-options': PluginRuleModule<[]>;
294
303
  };
package/dist/index.d.ts CHANGED
@@ -27,6 +27,7 @@ type Options$3 = [{
27
27
 
28
28
  type Options$2 = [{
29
29
  autofix?: boolean;
30
+ writablePrefixes?: string[];
30
31
  }];
31
32
 
32
33
  type Options$1 = [{
@@ -57,6 +58,7 @@ declare const plugin: {
57
58
  'no-deprecated-props': PluginRuleModule<[]>;
58
59
  'no-dot-ts-imports': PluginRuleModule<[]>;
59
60
  'no-legacy-library-import': PluginRuleModule<[]>;
61
+ 'no-redundant-flex-row': PluginRuleModule<[]>;
60
62
  'no-unused-i18n-keys': PluginRuleModule<Options$6>;
61
63
  'require-jsdoc-on-composable-options': PluginRuleModule<[]>;
62
64
  };
@@ -82,6 +84,7 @@ declare const _default: {
82
84
  'no-deprecated-props': PluginRuleModule<[]>;
83
85
  'no-dot-ts-imports': PluginRuleModule<[]>;
84
86
  'no-legacy-library-import': PluginRuleModule<[]>;
87
+ 'no-redundant-flex-row': PluginRuleModule<[]>;
85
88
  'no-unused-i18n-keys': PluginRuleModule<Options$6>;
86
89
  'require-jsdoc-on-composable-options': PluginRuleModule<[]>;
87
90
  };
@@ -109,6 +112,7 @@ declare const _default: {
109
112
  'no-deprecated-props': PluginRuleModule<[]>;
110
113
  'no-dot-ts-imports': PluginRuleModule<[]>;
111
114
  'no-legacy-library-import': PluginRuleModule<[]>;
115
+ 'no-redundant-flex-row': PluginRuleModule<[]>;
112
116
  'no-unused-i18n-keys': PluginRuleModule<Options$6>;
113
117
  'require-jsdoc-on-composable-options': PluginRuleModule<[]>;
114
118
  };
@@ -145,6 +149,7 @@ declare const _default: {
145
149
  'no-deprecated-props': PluginRuleModule<[]>;
146
150
  'no-dot-ts-imports': PluginRuleModule<[]>;
147
151
  'no-legacy-library-import': PluginRuleModule<[]>;
152
+ 'no-redundant-flex-row': PluginRuleModule<[]>;
148
153
  'no-unused-i18n-keys': PluginRuleModule<Options$6>;
149
154
  'require-jsdoc-on-composable-options': PluginRuleModule<[]>;
150
155
  };
@@ -181,6 +186,7 @@ declare const _default: {
181
186
  'no-deprecated-props': PluginRuleModule<[]>;
182
187
  'no-dot-ts-imports': PluginRuleModule<[]>;
183
188
  'no-legacy-library-import': PluginRuleModule<[]>;
189
+ 'no-redundant-flex-row': PluginRuleModule<[]>;
184
190
  'no-unused-i18n-keys': PluginRuleModule<Options$6>;
185
191
  'require-jsdoc-on-composable-options': PluginRuleModule<[]>;
186
192
  };
@@ -217,6 +223,7 @@ declare const _default: {
217
223
  'no-deprecated-props': PluginRuleModule<[]>;
218
224
  'no-dot-ts-imports': PluginRuleModule<[]>;
219
225
  'no-legacy-library-import': PluginRuleModule<[]>;
226
+ 'no-redundant-flex-row': PluginRuleModule<[]>;
220
227
  'no-unused-i18n-keys': PluginRuleModule<Options$6>;
221
228
  'require-jsdoc-on-composable-options': PluginRuleModule<[]>;
222
229
  };
@@ -253,6 +260,7 @@ declare const _default: {
253
260
  'no-deprecated-props': PluginRuleModule<[]>;
254
261
  'no-dot-ts-imports': PluginRuleModule<[]>;
255
262
  'no-legacy-library-import': PluginRuleModule<[]>;
263
+ 'no-redundant-flex-row': PluginRuleModule<[]>;
256
264
  'no-unused-i18n-keys': PluginRuleModule<Options$6>;
257
265
  'require-jsdoc-on-composable-options': PluginRuleModule<[]>;
258
266
  };
@@ -289,6 +297,7 @@ declare const _default: {
289
297
  'no-deprecated-props': PluginRuleModule<[]>;
290
298
  'no-dot-ts-imports': PluginRuleModule<[]>;
291
299
  'no-legacy-library-import': PluginRuleModule<[]>;
300
+ 'no-redundant-flex-row': PluginRuleModule<[]>;
292
301
  'no-unused-i18n-keys': PluginRuleModule<Options$6>;
293
302
  'require-jsdoc-on-composable-options': PluginRuleModule<[]>;
294
303
  };
package/dist/index.mjs CHANGED
@@ -1,5 +1,4 @@
1
1
  import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';
2
- import * as compat from 'eslint-compat-utils';
3
2
  import debugFactory from 'debug';
4
3
  import { extname, resolve } from 'node:path';
5
4
  import { pascalCase, kebabCase } from 'scule';
@@ -7,32 +6,35 @@ import { statSync, readFileSync } from 'node:fs';
7
6
  import { globSync } from 'tinyglobby';
8
7
  import { parse } from 'vue-eslint-parser';
9
8
 
10
- const version = "1.3.1";
9
+ const version = "1.4.0";
11
10
  const pkg = {
12
11
  version: version};
13
12
 
14
13
  function getFilename(context) {
15
- return compat.getFilename(context);
14
+ return context.filename;
16
15
  }
17
16
  function getSourceCode(context) {
18
- return compat.getSourceCode(context);
17
+ return context.sourceCode;
19
18
  }
20
19
 
21
20
  const COMPOSABLE_PATTERN = /^use[A-Z]/;
22
21
  function isComposableName(name) {
23
22
  return COMPOSABLE_PATTERN.test(name);
24
23
  }
24
+ function isComposableNode(node) {
25
+ if (node.type === AST_NODE_TYPES.FunctionDeclaration)
26
+ return !!node.id && isComposableName(node.id.name);
27
+ if (node.type === AST_NODE_TYPES.ArrowFunctionExpression || node.type === AST_NODE_TYPES.FunctionExpression) {
28
+ const parent = node.parent;
29
+ return parent?.type === AST_NODE_TYPES.VariableDeclarator && parent.id.type === AST_NODE_TYPES.Identifier && isComposableName(parent.id.name);
30
+ }
31
+ return false;
32
+ }
25
33
  function getEnclosingComposable(node) {
26
34
  let current = node.parent;
27
35
  while (current) {
28
- if (current.type === AST_NODE_TYPES.FunctionDeclaration && current.id && isComposableName(current.id.name))
36
+ if (isComposableNode(current))
29
37
  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
38
  current = current.parent;
37
39
  }
38
40
  return void 0;
@@ -64,22 +66,18 @@ function createRecommended(plugin, name, flat) {
64
66
  return createConfig(plugin, name, flat, "recommended");
65
67
  }
66
68
 
69
+ function getLiteralValue(node, stringOnly) {
70
+ if (node.value == null)
71
+ return !stringOnly && node.bigint != null ? node.bigint : null;
72
+ if (typeof node.value === "string")
73
+ return node.value;
74
+ return stringOnly ? null : String(node.value);
75
+ }
67
76
  function getStringLiteralValue(node, stringOnly = false) {
68
- if (node.type === "Literal") {
69
- if (node.value == null) {
70
- if (!stringOnly && node.bigint != null)
71
- return node.bigint;
72
- return null;
73
- }
74
- if (typeof node.value === "string")
75
- return node.value;
76
- if (!stringOnly)
77
- return String(node.value);
78
- return null;
79
- }
80
- if (node.type === "TemplateLiteral" && node.expressions.length === 0 && node.quasis.length === 1) {
77
+ if (node.type === "Literal")
78
+ return getLiteralValue(node, stringOnly);
79
+ if (node.type === "TemplateLiteral" && node.expressions.length === 0 && node.quasis.length === 1)
81
80
  return node.quasis[0].value.cooked;
82
- }
83
81
  return null;
84
82
  }
85
83
  function getStaticPropertyName(node) {
@@ -166,7 +164,7 @@ function defineTemplateBodyVisitor(context, templateBodyVisitor, scriptVisitor,
166
164
  );
167
165
  }
168
166
 
169
- const RULE_NAME$f = "composable-input-flexibility";
167
+ const RULE_NAME$g = "composable-input-flexibility";
170
168
  function checkParamForRef(param) {
171
169
  let annotation;
172
170
  if (param.type === AST_NODE_TYPES.Identifier && param.typeAnnotation) {
@@ -247,10 +245,10 @@ const composableInputFlexibility = createEslintRule({
247
245
  ],
248
246
  type: "suggestion"
249
247
  },
250
- name: RULE_NAME$f
248
+ name: RULE_NAME$g
251
249
  });
252
250
 
253
- const RULE_NAME$e = "composable-naming-convention";
251
+ const RULE_NAME$f = "composable-naming-convention";
254
252
  function getComposableFunction(node) {
255
253
  if (node.type === AST_NODE_TYPES.FunctionDeclaration) {
256
254
  if (!node.id || !isComposableName(node.id.name))
@@ -279,6 +277,9 @@ const composableNamingConvention = createEslintRule({
279
277
  checkComposable(node);
280
278
  }
281
279
  };
280
+ function isMismatchedConvention(typeName, prefix, suffix, expected) {
281
+ return !!typeName && typeName.startsWith(prefix) && typeName.endsWith(suffix) && typeName !== expected;
282
+ }
282
283
  function checkComposable(node) {
283
284
  const info = getComposableFunction(node);
284
285
  if (!info)
@@ -288,26 +289,14 @@ const composableNamingConvention = createEslintRule({
288
289
  const expectedReturn = `Use${pascalName}Return`;
289
290
  for (const param of info.params) {
290
291
  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
- }
292
+ const typeName = annotation ? getTypeReferenceName(annotation) : void 0;
293
+ if (isMismatchedConvention(typeName, "Use", "Options", expectedOptions))
294
+ context.report({ data: { expected: expectedOptions, got: typeName }, messageId: "optionsNaming", node: param });
301
295
  }
302
296
  if (info.returnType) {
303
297
  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
- }
298
+ if (isMismatchedConvention(returnTypeName, "Use", "Return", expectedReturn))
299
+ context.report({ data: { expected: expectedReturn, got: returnTypeName }, messageId: "returnNaming", node: info.returnType });
311
300
  }
312
301
  }
313
302
  function getParamTypeAnnotation(param) {
@@ -332,10 +321,10 @@ const composableNamingConvention = createEslintRule({
332
321
  schema: [],
333
322
  type: "suggestion"
334
323
  },
335
- name: RULE_NAME$e
324
+ name: RULE_NAME$f
336
325
  });
337
326
 
338
- const RULE_NAME$d = "composable-no-default-export";
327
+ const RULE_NAME$e = "composable-no-default-export";
339
328
  const composableNoDefaultExport = createEslintRule({
340
329
  create(context) {
341
330
  let hasComposable = false;
@@ -375,10 +364,10 @@ const composableNoDefaultExport = createEslintRule({
375
364
  schema: [],
376
365
  type: "problem"
377
366
  },
378
- name: RULE_NAME$d
367
+ name: RULE_NAME$e
379
368
  });
380
369
 
381
- const RULE_NAME$c = "composable-prefer-shallowref";
370
+ const RULE_NAME$d = "composable-prefer-shallowref";
382
371
  const composablePreferShallowref = createEslintRule({
383
372
  create(context) {
384
373
  const autofix = context.options[0]?.autofix ?? false;
@@ -437,10 +426,10 @@ const composablePreferShallowref = createEslintRule({
437
426
  ],
438
427
  type: "suggestion"
439
428
  },
440
- name: RULE_NAME$c
429
+ name: RULE_NAME$d
441
430
  });
442
431
 
443
- const RULE_NAME$b = "composable-require-cleanup";
432
+ const RULE_NAME$c = "composable-require-cleanup";
444
433
  const SIDE_EFFECT_CALLS = /* @__PURE__ */ new Set(["addEventListener", "setInterval", "setTimeout"]);
445
434
  const SIDE_EFFECT_CONSTRUCTORS = /* @__PURE__ */ new Set(["MutationObserver", "ResizeObserver", "IntersectionObserver"]);
446
435
  const CLEANUP_HOOKS = /* @__PURE__ */ new Set(["onUnmounted", "onBeforeUnmount", "onScopeDispose", "tryOnScopeDispose"]);
@@ -534,16 +523,62 @@ const composableRequireCleanup = createEslintRule({
534
523
  schema: [],
535
524
  type: "problem"
536
525
  },
537
- name: RULE_NAME$b
526
+ name: RULE_NAME$c
538
527
  });
539
528
 
540
- const RULE_NAME$a = "composable-return-readonly";
541
- const REACTIVE_CREATORS = /* @__PURE__ */ new Set(["ref", "shallowRef", "computed"]);
529
+ const RULE_NAME$b = "composable-return-readonly";
530
+ const DEFAULT_WRITABLE_PREFIXES = ["model"];
531
+ const REACTIVE_CREATORS = /* @__PURE__ */ new Set(["ref", "shallowRef"]);
532
+ const READONLY_CREATORS = /* @__PURE__ */ new Set(["computed"]);
533
+ function isReadonlyCall(node) {
534
+ return node.type === AST_NODE_TYPES.CallExpression && node.callee.type === AST_NODE_TYPES.Identifier && node.callee.name === "readonly" && node.arguments.length === 1 && node.arguments[0].type === AST_NODE_TYPES.Identifier;
535
+ }
542
536
  const composableReturnReadonly = createEslintRule({
543
537
  create(context) {
544
538
  const autofix = context.options[0]?.autofix ?? false;
539
+ const writablePrefixes = context.options[0]?.writablePrefixes ?? DEFAULT_WRITABLE_PREFIXES;
545
540
  const source = getSourceCode(context);
546
541
  const reactiveVars = /* @__PURE__ */ new Set();
542
+ const readonlyVars = /* @__PURE__ */ new Set();
543
+ function isWritableByConvention(name) {
544
+ return writablePrefixes.some((prefix) => {
545
+ if (!name.startsWith(prefix))
546
+ return false;
547
+ const next = name[prefix.length];
548
+ return next === void 0 || next >= "A" && next <= "Z";
549
+ });
550
+ }
551
+ function reportWithFixOrSuggest(prop, name, messageId, suggestMessageId, fixFn) {
552
+ context.report({
553
+ data: { name },
554
+ ...autofix ? { fix: fixFn } : { suggest: [{ data: { name }, fix: fixFn, messageId: suggestMessageId }] },
555
+ messageId,
556
+ node: prop
557
+ });
558
+ }
559
+ function checkUnnecessaryReadonly(prop) {
560
+ const valueNode = prop.shorthand ? null : prop.value;
561
+ if (!valueNode || !isReadonlyCall(valueNode))
562
+ return false;
563
+ const innerName = valueNode.arguments[0].name;
564
+ if (!readonlyVars.has(innerName))
565
+ return false;
566
+ reportWithFixOrSuggest(prop, innerName, "unnecessaryReadonly", "suggestRemoveReadonly", (fixer) => fixer.replaceText(valueNode, innerName));
567
+ return true;
568
+ }
569
+ function checkMissingReadonly(prop) {
570
+ if (prop.shorthand && prop.key.type === AST_NODE_TYPES.Identifier && reactiveVars.has(prop.key.name)) {
571
+ const keyName = prop.key.name;
572
+ if (isWritableByConvention(keyName))
573
+ return;
574
+ reportWithFixOrSuggest(prop, keyName, "wrapReadonly", "suggestWrapReadonly", (fixer) => fixer.replaceText(prop, `${keyName}: readonly(${keyName})`));
575
+ } else if (!prop.shorthand && prop.value.type === AST_NODE_TYPES.Identifier && reactiveVars.has(prop.value.name)) {
576
+ const valueName = prop.value.name;
577
+ if (isWritableByConvention(valueName))
578
+ return;
579
+ reportWithFixOrSuggest(prop, valueName, "wrapReadonly", "suggestWrapReadonly", (fixer) => fixer.replaceText(prop.value, `readonly(${prop.value.type === AST_NODE_TYPES.Identifier ? prop.value.name : source.getText(prop.value)})`));
580
+ }
581
+ }
547
582
  return {
548
583
  ReturnStatement: (node) => {
549
584
  if (!getEnclosingComposable(node))
@@ -553,25 +588,8 @@ const composableReturnReadonly = createEslintRule({
553
588
  for (const prop of node.argument.properties) {
554
589
  if (prop.type !== AST_NODE_TYPES.Property)
555
590
  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
- }
591
+ if (!checkUnnecessaryReadonly(prop))
592
+ checkMissingReadonly(prop);
575
593
  }
576
594
  },
577
595
  VariableDeclarator: (node) => {
@@ -579,13 +597,18 @@ const composableReturnReadonly = createEslintRule({
579
597
  return;
580
598
  if (node.id.type !== AST_NODE_TYPES.Identifier)
581
599
  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);
600
+ if (node.init?.type === AST_NODE_TYPES.CallExpression && node.init.callee.type === AST_NODE_TYPES.Identifier) {
601
+ const calleeName = node.init.callee.name;
602
+ if (REACTIVE_CREATORS.has(calleeName)) {
603
+ reactiveVars.add(node.id.name);
604
+ } else if (READONLY_CREATORS.has(calleeName)) {
605
+ readonlyVars.add(node.id.name);
606
+ }
584
607
  }
585
608
  }
586
609
  };
587
610
  },
588
- defaultOptions: [{ autofix: false }],
611
+ defaultOptions: [{ autofix: false, writablePrefixes: DEFAULT_WRITABLE_PREFIXES }],
589
612
  meta: {
590
613
  docs: {
591
614
  description: "Require returned refs from composables to be wrapped with readonly()",
@@ -594,7 +617,9 @@ const composableReturnReadonly = createEslintRule({
594
617
  fixable: "code",
595
618
  hasSuggestions: true,
596
619
  messages: {
620
+ suggestRemoveReadonly: "Remove unnecessary readonly() from '{{ name }}'.",
597
621
  suggestWrapReadonly: "Wrap '{{ name }}' with readonly().",
622
+ unnecessaryReadonly: "Computed ref '{{ name }}' is already readonly and does not need readonly() wrapping.",
598
623
  wrapReadonly: "Returned ref '{{ name }}' should be wrapped with readonly()."
599
624
  },
600
625
  schema: [
@@ -605,6 +630,12 @@ const composableReturnReadonly = createEslintRule({
605
630
  default: false,
606
631
  description: "Enable auto-fix. When disabled, the fix is available as a suggestion.",
607
632
  type: "boolean"
633
+ },
634
+ writablePrefixes: {
635
+ default: DEFAULT_WRITABLE_PREFIXES,
636
+ description: "Returned variables whose name starts with one of these prefixes are exempt from the readonly() requirement (e.g. refs intended for v-model binding).",
637
+ items: { type: "string" },
638
+ type: "array"
608
639
  }
609
640
  },
610
641
  type: "object"
@@ -612,10 +643,10 @@ const composableReturnReadonly = createEslintRule({
612
643
  ],
613
644
  type: "suggestion"
614
645
  },
615
- name: RULE_NAME$a
646
+ name: RULE_NAME$b
616
647
  });
617
648
 
618
- const RULE_NAME$9 = "composable-ssr-safety";
649
+ const RULE_NAME$a = "composable-ssr-safety";
619
650
  const BROWSER_GLOBALS = /* @__PURE__ */ new Set(["window", "document", "navigator"]);
620
651
  const LIFECYCLE_HOOKS = /* @__PURE__ */ new Set(["onMounted", "onBeforeMount"]);
621
652
  function isInsideLifecycleHook(node) {
@@ -642,15 +673,12 @@ function isInsideTypeofCheck(node) {
642
673
  }
643
674
  return false;
644
675
  }
676
+ function isTypeofBrowserGlobal(node) {
677
+ return node.type === AST_NODE_TYPES.UnaryExpression && node.operator === "typeof" && node.argument.type === AST_NODE_TYPES.Identifier && BROWSER_GLOBALS.has(node.argument.name);
678
+ }
645
679
  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
- }
680
+ if (node.type === AST_NODE_TYPES.BinaryExpression)
681
+ return isTypeofBrowserGlobal(node.left) || isTypeofBrowserGlobal(node.right);
654
682
  if (node.type === AST_NODE_TYPES.LogicalExpression)
655
683
  return hasTypeofCheck(node.left) || hasTypeofCheck(node.right);
656
684
  return false;
@@ -706,56 +734,51 @@ const composableSsrSafety = createEslintRule({
706
734
  schema: [],
707
735
  type: "problem"
708
736
  },
709
- name: RULE_NAME$9
737
+ name: RULE_NAME$a
710
738
  });
711
739
 
712
740
  const debug$6 = debugFactory("@rotki/eslint-plugin:consistent-ref-type-annotation");
713
- const RULE_NAME$8 = "consistent-ref-type-annotation";
741
+ const RULE_NAME$9 = "consistent-ref-type-annotation";
714
742
  const FIXABLE_METHODS = /* @__PURE__ */ new Set(["ref", "computed"]);
715
- function checkAssignmentDeclaration(context, source, node, declaration, allowInference) {
716
- let declarationTypeArguments;
743
+ function getFixableCallExpression(declaration) {
717
744
  const init = declaration.init;
718
- if (!(init && init.type === TSESTree.AST_NODE_TYPES.CallExpression))
719
- return;
745
+ if (!init || init.type !== TSESTree.AST_NODE_TYPES.CallExpression)
746
+ return void 0;
720
747
  const callee = init.callee;
721
- if (!(callee && callee.type === TSESTree.AST_NODE_TYPES.Identifier))
722
- return;
723
- if (!FIXABLE_METHODS.has(callee.name))
748
+ if (!callee || callee.type !== TSESTree.AST_NODE_TYPES.Identifier || !FIXABLE_METHODS.has(callee.name))
749
+ return void 0;
750
+ return { callee, init };
751
+ }
752
+ function getDeclarationTypeArguments(declaration) {
753
+ const typeNode = declaration.id.typeAnnotation?.typeAnnotation;
754
+ if (typeNode?.type === TSESTree.AST_NODE_TYPES.TSTypeReference)
755
+ return typeNode.typeArguments;
756
+ return void 0;
757
+ }
758
+ function checkAssignmentDeclaration(context, source, node, declaration, allowInference) {
759
+ const call = getFixableCallExpression(declaration);
760
+ if (!call)
724
761
  return;
762
+ const { callee, init } = call;
725
763
  const name = callee.name;
726
764
  debug$6(`found ${name}, checking type arguments`);
727
- const initializationTypeArguments = init.typeArguments;
765
+ const initTypeArgs = init.typeArguments;
766
+ const declTypeArgs = getDeclarationTypeArguments(declaration);
728
767
  const typeAnnotation = declaration.id.typeAnnotation;
729
- if (typeAnnotation) {
730
- const typeNode = typeAnnotation.typeAnnotation;
731
- if (typeNode && typeNode.type === TSESTree.AST_NODE_TYPES.TSTypeReference)
732
- declarationTypeArguments = typeNode.typeArguments;
733
- }
734
- if (initializationTypeArguments && !declarationTypeArguments)
768
+ if (initTypeArgs && !declTypeArgs)
735
769
  return;
736
770
  debug$6(`generating report for ${name}`);
737
- if (!initializationTypeArguments && !declarationTypeArguments) {
738
- if (allowInference) {
739
- debug$6("type inference is allowed");
740
- } else {
741
- context.report({
742
- data: {
743
- name
744
- },
745
- messageId: "missingType",
746
- node
747
- });
748
- }
771
+ if (!initTypeArgs && !declTypeArgs) {
772
+ if (!allowInference)
773
+ context.report({ data: { name }, messageId: "missingType", node });
749
774
  return;
750
775
  }
751
776
  context.report({
752
- data: {
753
- name
754
- },
777
+ data: { name },
755
778
  fix(fixer) {
756
779
  const fixes = [];
757
- if (!initializationTypeArguments && callee)
758
- fixes.push(fixer.insertTextAfter(callee, source.getText(declarationTypeArguments)));
780
+ if (!initTypeArgs && callee)
781
+ fixes.push(fixer.insertTextAfter(callee, source.getText(declTypeArgs)));
759
782
  if (typeAnnotation)
760
783
  fixes.push(fixer.remove(typeAnnotation));
761
784
  return fixes;
@@ -804,10 +827,10 @@ const consistentRefTypeAnnotation = createEslintRule({
804
827
  ],
805
828
  type: "problem"
806
829
  },
807
- name: RULE_NAME$8
830
+ name: RULE_NAME$9
808
831
  });
809
832
 
810
- const RULE_NAME$7 = "max-dependencies";
833
+ const RULE_NAME$8 = "max-dependencies";
811
834
  const maxDependencies = createEslintRule({
812
835
  create(context, [options]) {
813
836
  let dependencyCount = 0;
@@ -867,10 +890,10 @@ const maxDependencies = createEslintRule({
867
890
  ],
868
891
  type: "suggestion"
869
892
  },
870
- name: RULE_NAME$7
893
+ name: RULE_NAME$8
871
894
  });
872
895
 
873
- const RULE_NAME$6 = "no-deprecated-classes";
896
+ const RULE_NAME$7 = "no-deprecated-classes";
874
897
  const debug$5 = debugFactory("@rotki/eslint-plugin:no-deprecated-classes");
875
898
  const stringReplacements = /* @__PURE__ */ new Map([
876
899
  ["d-block", "block"],
@@ -947,51 +970,62 @@ function reportReplacement(className, replacement, node, context, source, positi
947
970
  messageId: "replacedWith"
948
971
  });
949
972
  }
973
+ function splitClassNames(classNames, reportNode) {
974
+ return classNames.split(/\s+/).map((className) => ({ className, position: classNames.indexOf(className) + 1, reportNode }));
975
+ }
976
+ function* extractFromObject(node) {
977
+ for (const prop of node.properties) {
978
+ if (prop.type !== "Property")
979
+ continue;
980
+ const classNames = getStaticPropertyName(prop);
981
+ if (classNames)
982
+ yield* splitClassNames(classNames, prop.key);
983
+ }
984
+ }
985
+ function* extractFromArray(node) {
986
+ for (const element of node.elements) {
987
+ if (element == null || element.type === "SpreadElement")
988
+ continue;
989
+ yield* extractClassNames(element);
990
+ }
991
+ }
992
+ function* extractFromTemplateLiteral(node) {
993
+ for (const templateElement of node.quasis) {
994
+ const classNames = templateElement.value.cooked;
995
+ if (classNames !== null)
996
+ yield* splitClassNames(classNames, templateElement);
997
+ }
998
+ for (const expr of node.expressions)
999
+ yield* extractClassNames(expr, true);
1000
+ }
1001
+ function* extractFromBinary(node) {
1002
+ if (node.operator !== "+")
1003
+ return;
1004
+ if (node.left.type !== "PrivateIdentifier")
1005
+ yield* extractClassNames(node.left, true);
1006
+ yield* extractClassNames(node.right, true);
1007
+ }
950
1008
  function* extractClassNames(node, textOnly = false) {
951
1009
  if (node.type === "Literal") {
952
- const classNames = `${node.value}`;
953
- yield* classNames.split(/\s+/).map((className) => ({ className, position: classNames.indexOf(className) + 1, reportNode: node }));
1010
+ yield* splitClassNames(`${node.value}`, node);
954
1011
  return;
955
1012
  }
956
1013
  if (node.type === "TemplateLiteral") {
957
- for (const templateElement of node.quasis) {
958
- const classNames = templateElement.value.cooked;
959
- if (classNames === null)
960
- continue;
961
- yield* classNames.split(/\s+/).map((className) => ({ className, position: classNames.indexOf(className) + 1, reportNode: templateElement }));
962
- }
963
- for (const expr of node.expressions)
964
- yield* extractClassNames(expr, true);
1014
+ yield* extractFromTemplateLiteral(node);
965
1015
  return;
966
1016
  }
967
1017
  if (node.type === "BinaryExpression") {
968
- if (node.operator !== "+")
969
- return;
970
- yield* extractClassNames(node.left, true);
971
- yield* extractClassNames(node.right, true);
1018
+ yield* extractFromBinary(node);
972
1019
  return;
973
1020
  }
974
1021
  if (textOnly)
975
1022
  return;
976
1023
  if (node.type === "ObjectExpression") {
977
- for (const prop of node.properties) {
978
- if (prop.type !== "Property")
979
- continue;
980
- const classNames = getStaticPropertyName(prop);
981
- if (!classNames)
982
- continue;
983
- yield* classNames.split(/\s+/).map((className) => ({ className, position: classNames.indexOf(className) + 1, reportNode: prop.key }));
984
- }
1024
+ yield* extractFromObject(node);
985
1025
  return;
986
1026
  }
987
1027
  if (node.type === "ArrayExpression") {
988
- for (const element of node.elements) {
989
- if (element == null)
990
- continue;
991
- if (element.type === "SpreadElement")
992
- continue;
993
- yield* extractClassNames(element);
994
- }
1028
+ yield* extractFromArray(node);
995
1029
  }
996
1030
  if (node.type === "ConditionalExpression") {
997
1031
  yield* extractClassNames(node.consequent);
@@ -1038,11 +1072,11 @@ const noDeprecatedClasses = createEslintRule({
1038
1072
  schema: [],
1039
1073
  type: "problem"
1040
1074
  },
1041
- name: RULE_NAME$6
1075
+ name: RULE_NAME$7
1042
1076
  });
1043
1077
 
1044
1078
  const debug$4 = debugFactory("@rotki/eslint-plugin:no-deprecated-components");
1045
- const RULE_NAME$5 = "no-deprecated-components";
1079
+ const RULE_NAME$6 = "no-deprecated-components";
1046
1080
  const vuetify = {
1047
1081
  VApp: true,
1048
1082
  VAppBar: true,
@@ -1079,7 +1113,7 @@ const skipInLegacy = /* @__PURE__ */ new Set([
1079
1113
  "Fragment"
1080
1114
  ]);
1081
1115
  function hasReplacement$1(tag) {
1082
- return Object.prototype.hasOwnProperty.call(replacements$1, tag);
1116
+ return Object.hasOwn(replacements$1, tag);
1083
1117
  }
1084
1118
  const noDeprecatedComponents = createEslintRule({
1085
1119
  create(context, optionsWithDefault) {
@@ -1147,11 +1181,10 @@ const noDeprecatedComponents = createEslintRule({
1147
1181
  ],
1148
1182
  type: "problem"
1149
1183
  },
1150
- name: RULE_NAME$5
1184
+ name: RULE_NAME$6
1151
1185
  });
1152
1186
 
1153
- const debug$3 = debugFactory("@rotki/eslint-plugin:no-deprecated-props");
1154
- const RULE_NAME$4 = "no-deprecated-props";
1187
+ const RULE_NAME$5 = "no-deprecated-props";
1155
1188
  const replacements = {
1156
1189
  RuiRadio: {
1157
1190
  internalValue: "value"
@@ -1174,37 +1207,34 @@ function getPropName(node) {
1174
1207
  }
1175
1208
  return kebabCase(node.key.rawName);
1176
1209
  }
1210
+ function isNonBindDirective(node) {
1211
+ return node.directive && node.value?.type === "VExpressionContainer" && (node.key.name.name !== "bind" || !node.key.argument);
1212
+ }
1213
+ function getPropNameNode(node) {
1214
+ return node.directive ? node.key.argument : node.key;
1215
+ }
1177
1216
  const noDeprecatedProps = createEslintRule({
1178
1217
  create(context) {
1179
1218
  return defineTemplateBodyVisitor(context, {
1180
1219
  VAttribute(node) {
1181
- if (node.directive && (node.value?.type === "VExpressionContainer" && (node.key.name.name !== "bind" || !node.key.argument)))
1220
+ if (isNonBindDirective(node))
1182
1221
  return;
1183
1222
  const tag = pascalCase(node.parent.parent.rawName);
1184
1223
  if (!hasReplacement(tag))
1185
1224
  return;
1186
- debug$3(`${tag} has replacement properties`);
1187
1225
  const propName = getPropName(node);
1188
- const propNameNode = node.directive ? node.key.argument : node.key;
1189
- if (!propName || !propNameNode) {
1190
- debug$3("could not get prop name and/or node");
1226
+ const propNameNode = getPropNameNode(node);
1227
+ if (!propName || !propNameNode)
1191
1228
  return;
1192
- }
1193
1229
  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
- }
1230
+ if (!match)
1231
+ return;
1232
+ context.report({
1233
+ data: { prop: match.original, replacement: match.replacement },
1234
+ fix: (fixer) => fixer.replaceText(propNameNode, match.replacement),
1235
+ messageId: "replacedWith",
1236
+ node: propNameNode
1237
+ });
1208
1238
  }
1209
1239
  });
1210
1240
  },
@@ -1221,10 +1251,10 @@ const noDeprecatedProps = createEslintRule({
1221
1251
  schema: [],
1222
1252
  type: "problem"
1223
1253
  },
1224
- name: RULE_NAME$4
1254
+ name: RULE_NAME$5
1225
1255
  });
1226
1256
 
1227
- const RULE_NAME$3 = "no-dot-ts-imports";
1257
+ const RULE_NAME$4 = "no-dot-ts-imports";
1228
1258
  const noDotTsImport = createEslintRule({
1229
1259
  create(context) {
1230
1260
  return {
@@ -1259,10 +1289,10 @@ const noDotTsImport = createEslintRule({
1259
1289
  schema: [],
1260
1290
  type: "problem"
1261
1291
  },
1262
- name: RULE_NAME$3
1292
+ name: RULE_NAME$4
1263
1293
  });
1264
1294
 
1265
- const RULE_NAME$2 = "no-legacy-library-import";
1295
+ const RULE_NAME$3 = "no-legacy-library-import";
1266
1296
  const legacyLibrary = "@rotki/ui-library-compat";
1267
1297
  const newLibrary = "@rotki/ui-library";
1268
1298
  const noLegacyLibraryImport = createEslintRule({
@@ -1295,6 +1325,119 @@ const noLegacyLibraryImport = createEslintRule({
1295
1325
  schema: [],
1296
1326
  type: "problem"
1297
1327
  },
1328
+ name: RULE_NAME$3
1329
+ });
1330
+
1331
+ const RULE_NAME$2 = "no-redundant-flex-row";
1332
+ const debug$3 = debugFactory("@rotki/eslint-plugin:no-redundant-flex-row");
1333
+ function stripImportant(token) {
1334
+ return token.replace(/^!/, "").replace(/!$/, "");
1335
+ }
1336
+ function isUnconditional(token) {
1337
+ return !token.includes(":");
1338
+ }
1339
+ function isFlexDisplay(token) {
1340
+ if (!isUnconditional(token))
1341
+ return false;
1342
+ const base = stripImportant(token);
1343
+ return base === "flex" || base === "inline-flex";
1344
+ }
1345
+ function isFlexRow(token) {
1346
+ if (!isUnconditional(token))
1347
+ return false;
1348
+ return stripImportant(token) === "flex-row";
1349
+ }
1350
+ function tokenize(value) {
1351
+ const tokens = [];
1352
+ const matcher = /\S+/g;
1353
+ let match;
1354
+ while ((match = matcher.exec(value)) !== null)
1355
+ tokens.push({ start: match.index, text: match[0] });
1356
+ return tokens;
1357
+ }
1358
+ function removalRange(value, token, contentStart) {
1359
+ let startRel = token.start;
1360
+ let endRel = token.start + token.text.length;
1361
+ if (startRel > 0 && /\s/.test(value[startRel - 1])) {
1362
+ while (startRel > 0 && /\s/.test(value[startRel - 1]))
1363
+ startRel--;
1364
+ } else {
1365
+ while (endRel < value.length && /\s/.test(value[endRel]))
1366
+ endRel++;
1367
+ }
1368
+ return [contentStart + startRel, contentStart + endRel];
1369
+ }
1370
+ function reportRedundant(source, context, sourceCode) {
1371
+ const tokens = tokenize(source.value);
1372
+ if (!tokens.some((token) => isFlexDisplay(token.text)))
1373
+ return;
1374
+ for (const token of tokens) {
1375
+ if (!isFlexRow(token.text))
1376
+ continue;
1377
+ debug$3(`found redundant flex-row token '${token.text}'`);
1378
+ const tokenRange = [
1379
+ source.contentStart + token.start,
1380
+ source.contentStart + token.start + token.text.length
1381
+ ];
1382
+ const removal = removalRange(source.value, token, source.contentStart);
1383
+ context.report({
1384
+ data: { className: token.text },
1385
+ fix(fixer) {
1386
+ return fixer.removeRange(removal);
1387
+ },
1388
+ loc: {
1389
+ end: sourceCode.getLocFromIndex(tokenRange[1]),
1390
+ start: sourceCode.getLocFromIndex(tokenRange[0])
1391
+ },
1392
+ messageId: "redundantFlexRow"
1393
+ });
1394
+ }
1395
+ }
1396
+ function plainStringLists(expression) {
1397
+ if (expression.type === "Literal" && typeof expression.value === "string")
1398
+ return [{ contentStart: expression.range[0] + 1, value: expression.value }];
1399
+ if (expression.type === "TemplateLiteral" && expression.expressions.length === 0 && expression.quasis.length === 1) {
1400
+ const quasi = expression.quasis[0];
1401
+ const cooked = quasi.value.cooked;
1402
+ if (cooked !== null)
1403
+ return [{ contentStart: quasi.range[0] + 1, value: cooked }];
1404
+ }
1405
+ return [];
1406
+ }
1407
+ const noRedundantFlexRow = createEslintRule({
1408
+ create(context) {
1409
+ const sourceCode = getSourceCode(context);
1410
+ return defineTemplateBodyVisitor(context, {
1411
+ 'VAttribute[directive=false][key.name="class"]': function(node) {
1412
+ if (!node.value || !node.value.value || !node.value.range)
1413
+ return;
1414
+ reportRedundant(
1415
+ { contentStart: node.value.range[0] + 1, value: node.value.value },
1416
+ context,
1417
+ sourceCode
1418
+ );
1419
+ },
1420
+ "VAttribute[directive=true][key.name.name='bind'][key.argument.name='class'] > VExpressionContainer.value": function(node) {
1421
+ if (!node.expression)
1422
+ return;
1423
+ for (const source of plainStringLists(node.expression))
1424
+ reportRedundant(source, context, sourceCode);
1425
+ }
1426
+ });
1427
+ },
1428
+ defaultOptions: [],
1429
+ meta: {
1430
+ docs: {
1431
+ description: "disallow redundant `flex-row` since `flex` already defaults to the row direction",
1432
+ recommendation: "recommended"
1433
+ },
1434
+ fixable: "code",
1435
+ messages: {
1436
+ redundantFlexRow: `'{{ className }}' is redundant because 'flex' already lays out in the row direction; remove it, or keep it only behind a conditional variant (e.g. 'sm:flex-row')`
1437
+ },
1438
+ schema: [],
1439
+ type: "suggestion"
1440
+ },
1298
1441
  name: RULE_NAME$2
1299
1442
  });
1300
1443
 
@@ -1311,18 +1454,39 @@ function isI18nCallExpression(node) {
1311
1454
  return I18N_FUNCTION_NAMES.has(callee.name);
1312
1455
  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
1456
  }
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))
1457
+ function getQuasiText(quasi) {
1458
+ if (!quasi || typeof quasi !== "object" || !("value" in quasi))
1321
1459
  return void 0;
1322
- const quasiObj = firstQuasi.value;
1460
+ const quasiObj = quasi.value;
1323
1461
  if (!quasiObj || typeof quasiObj !== "object")
1324
1462
  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;
1463
+ if ("cooked" in quasiObj && typeof quasiObj.cooked === "string")
1464
+ return quasiObj.cooked;
1465
+ if ("raw" in quasiObj && typeof quasiObj.raw === "string")
1466
+ return quasiObj.raw;
1467
+ return void 0;
1468
+ }
1469
+ function getTemplateLiteralText(arg) {
1470
+ const { expressions, quasis } = arg;
1471
+ if (!Array.isArray(quasis) || !Array.isArray(expressions) || quasis.length === 0)
1472
+ return void 0;
1473
+ return getQuasiText(quasis[0]);
1474
+ }
1475
+ function isStaticTemplateLiteral(arg) {
1476
+ return Array.isArray(arg.expressions) && arg.expressions.length === 0 && Array.isArray(arg.quasis) && arg.quasis.length === 1;
1477
+ }
1478
+ function extractKeyFromArg(arg) {
1479
+ if (arg.type === "Literal" && typeof arg.value === "string")
1480
+ return arg.value;
1481
+ if (arg.type === "TemplateLiteral") {
1482
+ const text = getTemplateLiteralText(arg);
1483
+ if (text === void 0)
1484
+ return void 0;
1485
+ if (isStaticTemplateLiteral(arg))
1486
+ return text;
1487
+ return text ? `${text}*` : void 0;
1488
+ }
1489
+ return void 0;
1326
1490
  }
1327
1491
  function extractKeysFromCallExpression(node, keys) {
1328
1492
  if (!isI18nCallExpression(node))
@@ -1333,17 +1497,9 @@ function extractKeysFromCallExpression(node, keys) {
1333
1497
  const arg = args[0];
1334
1498
  if (!isAstNode(arg))
1335
1499
  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
- }
1500
+ const key = extractKeyFromArg(arg);
1501
+ if (key)
1502
+ keys.add(key);
1347
1503
  }
1348
1504
  const TS_AST_CHILD_KEYS = /* @__PURE__ */ new Set([
1349
1505
  "alternate",
@@ -1420,34 +1576,44 @@ function getDirectiveExpression(attr) {
1420
1576
  function extractKeysFromVueTemplate(templateBody, keys) {
1421
1577
  walkVueTemplateNode(templateBody, keys);
1422
1578
  }
1579
+ function collectI18nTKeys(element, keys) {
1580
+ if (element.rawName !== "i18n-t" && element.name !== "i18n-t")
1581
+ return;
1582
+ for (const attr of element.startTag.attributes) {
1583
+ if (isKeypathAttribute(attr))
1584
+ keys.add(attr.value.value);
1585
+ }
1586
+ }
1587
+ function collectAttributeKeys(element, keys) {
1588
+ for (const attr of element.startTag.attributes) {
1589
+ const expr = getDirectiveExpression(attr);
1590
+ if (!expr)
1591
+ continue;
1592
+ if (isVTDirective(attr) && expr.type === "Literal" && typeof expr.value === "string")
1593
+ keys.add(expr.value);
1594
+ walkTsAst(expr, keys);
1595
+ }
1596
+ }
1597
+ function walkChildren(node, keys) {
1598
+ if (!("children" in node) || !Array.isArray(node.children))
1599
+ return;
1600
+ for (const child of node.children) {
1601
+ if (child && typeof child === "object")
1602
+ walkVueTemplateNode(child, keys);
1603
+ }
1604
+ }
1423
1605
  function walkVueTemplateNode(node, keys) {
1424
1606
  if (!node || typeof node !== "object")
1425
1607
  return;
1426
1608
  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);
1609
+ collectI18nTKeys(node, keys);
1610
+ collectAttributeKeys(node, keys);
1611
+ walkChildren(node, keys);
1443
1612
  } else if (isVExpressionContainer(node)) {
1444
1613
  if (node.expression)
1445
1614
  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
- }
1615
+ } else {
1616
+ walkChildren(node, keys);
1451
1617
  }
1452
1618
  }
1453
1619
  function extractKeysFromSfcI18nBlock(content, keys) {
@@ -1501,45 +1667,42 @@ ${content}
1501
1667
  return void 0;
1502
1668
  }
1503
1669
  }
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;
1670
+ function readFileWithMtime(filePath) {
1517
1671
  try {
1518
- content = readFileSync(filePath, "utf-8");
1672
+ const mtimeMs = statSync(filePath).mtimeMs;
1673
+ const content = readFileSync(filePath, "utf-8");
1674
+ return { content, mtimeMs };
1519
1675
  } 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;
1676
+ return void 0;
1527
1677
  }
1528
- const ast = parseSourceFile(content, filePath);
1529
- if (!ast)
1530
- return keys;
1678
+ }
1679
+ function extractKeysFromAst(ast, content, isVue, keys) {
1531
1680
  if (isVue) {
1532
- if (ast.templateBody) {
1681
+ if (ast.templateBody)
1533
1682
  extractKeysFromVueTemplate(ast.templateBody, keys);
1534
- }
1535
1683
  extractKeysFromSfcI18nBlock(content, keys);
1536
1684
  }
1537
1685
  if (ast.body) {
1538
- for (const node of ast.body) {
1686
+ for (const node of ast.body)
1539
1687
  walkTsAst(node, keys);
1540
- }
1541
1688
  }
1542
- fileCache.set(filePath, { keys, mtimeMs });
1689
+ }
1690
+ function collectKeysFromFile(filePath) {
1691
+ const file = readFileWithMtime(filePath);
1692
+ if (!file)
1693
+ return /* @__PURE__ */ new Set();
1694
+ const cached = fileCache.get(filePath);
1695
+ if (cached && cached.mtimeMs === file.mtimeMs)
1696
+ return cached.keys;
1697
+ const keys = /* @__PURE__ */ new Set();
1698
+ const isVue = filePath.endsWith(".vue");
1699
+ const pattern = isVue ? VUE_I18N_PATTERN : I18N_CALL_PATTERN;
1700
+ if (pattern.test(file.content)) {
1701
+ const ast = parseSourceFile(file.content, filePath);
1702
+ if (ast)
1703
+ extractKeysFromAst(ast, file.content, isVue, keys);
1704
+ }
1705
+ fileCache.set(filePath, { keys, mtimeMs: file.mtimeMs });
1543
1706
  return keys;
1544
1707
  }
1545
1708
  function collectAllUsedKeys(srcDir, extensions) {
@@ -1613,21 +1776,22 @@ const debug = debugFactory("@rotki/eslint-plugin:no-unused-i18n-keys");
1613
1776
  function isLocaleFile(filename) {
1614
1777
  return /(?:locales?|i18n|translations?|messages?|lang)\b/i.test(filename);
1615
1778
  }
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;
1779
+ function isProgramWithBody(ast) {
1780
+ return !!ast && typeof ast === "object" && "type" in ast && ast.type === "Program" && "body" in ast && Array.isArray(ast.body) && ast.body.length > 0;
1781
+ }
1782
+ function getProgramFirstBodyType(ast) {
1783
+ if (!isProgramWithBody(ast))
1784
+ return void 0;
1621
1785
  const first = ast.body[0];
1622
- return first !== null && typeof first === "object" && "type" in first && first.type === "JSONExpressionStatement";
1786
+ if (!first || typeof first !== "object" || !("type" in first) || typeof first.type !== "string")
1787
+ return void 0;
1788
+ return first.type;
1789
+ }
1790
+ function isJsonProgram(ast) {
1791
+ return getProgramFirstBodyType(ast) === "JSONExpressionStatement";
1623
1792
  }
1624
1793
  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";
1794
+ return getProgramFirstBodyType(ast) === "YAMLDocument";
1631
1795
  }
1632
1796
  function buildJsonKeyPaths(node, prefix, paths) {
1633
1797
  for (const prop of node.properties) {
@@ -1928,6 +2092,7 @@ const plugin = {
1928
2092
  "no-deprecated-props": noDeprecatedProps,
1929
2093
  "no-dot-ts-imports": noDotTsImport,
1930
2094
  "no-legacy-library-import": noLegacyLibraryImport,
2095
+ "no-redundant-flex-row": noRedundantFlexRow,
1931
2096
  "no-unused-i18n-keys": noUnusedI18nKeys,
1932
2097
  "require-jsdoc-on-composable-options": requireJsdocOnComposableOptions
1933
2098
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rotki/eslint-plugin",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "type": "module",
5
5
  "license": "AGPL-3.0",
6
6
  "bugs": {
@@ -25,43 +25,44 @@
25
25
  },
26
26
  "sideEffects": false,
27
27
  "peerDependencies": {
28
- "eslint": "^9.20.0"
28
+ "eslint": "^9.20.0 || ^10.0.0"
29
29
  },
30
30
  "dependencies": {
31
- "@typescript-eslint/utils": "8.55.0",
31
+ "@typescript-eslint/utils": "8.60.1",
32
32
  "debug": "4.4.3",
33
- "eslint-compat-utils": "0.6.5",
34
- "jsonc-eslint-parser": "2.4.2",
33
+ "jsonc-eslint-parser": "3.1.0",
35
34
  "scule": "1.3.0",
36
- "tinyglobby": "0.2.15",
37
- "vue-eslint-parser": "10.4.0",
35
+ "tinyglobby": "0.2.17",
36
+ "vue-eslint-parser": "10.4.1",
38
37
  "yaml-eslint-parser": "2.0.0"
39
38
  },
40
39
  "devDependencies": {
41
- "@commitlint/cli": "20.4.1",
42
- "@commitlint/config-conventional": "20.4.1",
43
- "@rotki/eslint-config": "4.5.0",
44
- "@rotki/eslint-plugin": "1.2.0",
45
- "@types/debug": "4.1.12",
46
- "@types/node": "24.10.13",
47
- "@typescript-eslint/eslint-plugin": "8.55.0",
48
- "@typescript-eslint/parser": "8.55.0",
49
- "@typescript-eslint/rule-tester": "8.55.0",
50
- "@vitest/coverage-v8": "4.0.18",
51
- "bumpp": "10.4.1",
52
- "eslint": "9.39.2",
40
+ "@commitlint/cli": "21.0.2",
41
+ "@commitlint/config-conventional": "21.0.2",
42
+ "@rotki/eslint-config": "6.1.0",
43
+ "@rotki/eslint-plugin": "1.3.2",
44
+ "@types/debug": "4.1.13",
45
+ "@types/node": "24.13.1",
46
+ "@typescript-eslint/eslint-plugin": "8.60.1",
47
+ "@typescript-eslint/parser": "8.60.1",
48
+ "@typescript-eslint/rule-tester": "8.60.1",
49
+ "@vitest/coverage-v8": "4.1.8",
50
+ "bumpp": "11.1.0",
51
+ "eslint": "10.4.1",
53
52
  "husky": "9.1.7",
54
- "lint-staged": "16.2.7",
53
+ "lint-staged": "17.0.7",
54
+ "oxc-minify": "0.135.0",
55
+ "prettier": "3.8.1",
55
56
  "rimraf": "6.1.3",
56
- "tsx": "4.21.0",
57
- "typescript": "5.9.3",
57
+ "tsx": "4.22.4",
58
+ "typescript": "6.0.3",
58
59
  "unbuild": "3.6.1",
59
- "vitepress": "1.6.4",
60
- "vitest": "4.0.18"
60
+ "vitepress": "2.0.0-alpha.17",
61
+ "vitest": "4.1.8"
61
62
  },
62
63
  "engines": {
63
64
  "node": ">=24 <25",
64
- "pnpm": ">=10 <11"
65
+ "pnpm": ">=10 <12"
65
66
  },
66
67
  "lint-staged": {
67
68
  "*.{js,cjs,ts,vue,yml,json,md}": "eslint"