@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/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 { extname } from 'node:path';
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.2.0";
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
- function createRecommended(plugin, name, flat) {
19
- const rules = Object.fromEntries(Object.entries(plugin.rules).filter(([_key, rule]) => rule.meta.recommended === "recommended" && !rule.meta.deprecated).map(([key]) => [`${name}/${key}`, 2]));
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 debug$3 = createDebug("@rotki/eslint-plugin:consistent-ref-type-annotation");
138
- const RULE_NAME$6 = "consistent-ref-type-annotation";
139
- const FIXABLE_METHODS = ["ref", "computed"];
140
- function checkAssignmentDeclaration(context, node, declaration, options) {
141
- const source = getSourceCode(context);
142
- const { allowInference } = options;
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 (!Array.prototype.includes.call(FIXABLE_METHODS, callee.name))
723
+ if (!FIXABLE_METHODS.has(callee.name))
151
724
  return;
152
725
  const name = callee.name;
153
- debug$3(`found ${name}, checking type arguments`);
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$3(`generating report for ${name}`);
736
+ debug$6(`generating report for ${name}`);
164
737
  if (!initializationTypeArguments && !declarationTypeArguments) {
165
738
  if (allowInference) {
166
- debug$3("type inference is allowed");
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, { allowInference });
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$6
807
+ name: RULE_NAME$8
234
808
  });
235
809
 
236
- const RULE_NAME$5 = "max-dependencies";
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$5
870
+ name: RULE_NAME$7
297
871
  });
298
872
 
299
- const RULE_NAME$4 = "no-deprecated-classes";
300
- const debug$2 = createDebug("@rotki/eslint-plugin:no-deprecated-classes");
301
- const replacements$2 = [
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
- for (const replacement of replacements$2) {
343
- if (isString(replacement) && replacement[0] === className)
344
- return replacement[1];
345
- if (isRegex(replacement)) {
346
- const matches = (replacement[0].exec(className) || []).slice(1);
347
- const replace = replacement[1];
348
- if (matches.length > 0 && typeof replace === "function")
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$2(`found replacement ${replacement} for ${className}`);
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$4
1041
+ name: RULE_NAME$6
474
1042
  });
475
1043
 
476
- const debug$1 = createDebug("@rotki/eslint-plugin:no-deprecated-components");
477
- const RULE_NAME$3 = "no-deprecated-components";
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.includes(tag)) {
530
- debug$1(`${tag} has been deprecated`);
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$1(`${tag} has will be removed`);
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$3
1150
+ name: RULE_NAME$5
583
1151
  });
584
1152
 
585
- const debug = createDebug("@rotki/eslint-plugin:no-deprecated-props");
586
- const RULE_NAME$2 = "no-deprecated-props";
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 Object.prototype.hasOwnProperty.call(replacements, tag);
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
- Object.entries(replacements[tag]).forEach(([prop, replacement]) => {
620
- if (kebabCase(prop) === propName) {
621
- debug(`preparing a replacement for ${tag}:${propName} -> ${replacement}`);
622
- context.report({
623
- data: {
624
- prop,
625
- replacement
626
- },
627
- fix(fixer) {
628
- return fixer.replaceText(propNameNode, replacement);
629
- },
630
- messageId: "replacedWith",
631
- node: propNameNode
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$2
1224
+ name: RULE_NAME$4
652
1225
  });
653
1226
 
654
- const RULE_NAME$1 = "no-dot-ts-imports";
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 lastIndexOfExtension = importDeclaration.lastIndexOf(".ts");
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$1
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 });