@geoql/doctor-rule-core 1.2.2 → 1.3.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/dist/index.js +343 -49
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -133,7 +133,7 @@ const noExplicitImportsOfAutoImported = defineRule({ create(context) {
|
|
|
133
133
|
} });
|
|
134
134
|
//#endregion
|
|
135
135
|
//#region src/rules/nuxt/ai-slop/no-fetch-in-setup.ts
|
|
136
|
-
const MESSAGE$
|
|
136
|
+
const MESSAGE$31 = `Bare $fetch/fetch at the top level of <script setup> runs on both server and client without SSR deduplication. Use useFetch for automatic request deduplication and SSR hydration. See https://nuxt.com/docs/4.x/guide/data-fetching`;
|
|
137
137
|
function isFetchCall(node) {
|
|
138
138
|
if (!node || node.type !== "CallExpression") return false;
|
|
139
139
|
const callee = node.callee;
|
|
@@ -170,7 +170,7 @@ const noFetchInSetup = defineRule({ create(context) {
|
|
|
170
170
|
if (node["parent"]?.type === "AwaitExpression") return;
|
|
171
171
|
context.report({
|
|
172
172
|
node,
|
|
173
|
-
message: MESSAGE$
|
|
173
|
+
message: MESSAGE$31
|
|
174
174
|
});
|
|
175
175
|
},
|
|
176
176
|
AwaitExpression(node) {
|
|
@@ -178,7 +178,7 @@ const noFetchInSetup = defineRule({ create(context) {
|
|
|
178
178
|
if (!isFetchCall(node.argument)) return;
|
|
179
179
|
context.report({
|
|
180
180
|
node,
|
|
181
|
-
message: MESSAGE$
|
|
181
|
+
message: MESSAGE$31
|
|
182
182
|
});
|
|
183
183
|
}
|
|
184
184
|
};
|
|
@@ -190,7 +190,7 @@ const LEGACY_PROPS = /* @__PURE__ */ new Set([
|
|
|
190
190
|
"server",
|
|
191
191
|
"browser"
|
|
192
192
|
]);
|
|
193
|
-
const MESSAGE$
|
|
193
|
+
const MESSAGE$30 = `Use import.meta.client / import.meta.server / import.meta.browser instead of process.client / process.server / process.browser (Nuxt 4). See https://nuxt.com/docs/4.x/guide/concepts/auto-imports`;
|
|
194
194
|
const noProcessClientServer = defineRule({
|
|
195
195
|
create(context) {
|
|
196
196
|
return { MemberExpression(node) {
|
|
@@ -202,7 +202,7 @@ const noProcessClientServer = defineRule({
|
|
|
202
202
|
if (!LEGACY_PROPS.has(property.name)) return;
|
|
203
203
|
context.report({
|
|
204
204
|
node,
|
|
205
|
-
message: MESSAGE$
|
|
205
|
+
message: MESSAGE$30
|
|
206
206
|
});
|
|
207
207
|
} };
|
|
208
208
|
},
|
|
@@ -219,7 +219,7 @@ const noProcessClientServer = defineRule({
|
|
|
219
219
|
});
|
|
220
220
|
//#endregion
|
|
221
221
|
//#region src/rules/nuxt/ai-slop/no-useState-for-server-data.ts
|
|
222
|
-
const MESSAGE$
|
|
222
|
+
const MESSAGE$29 = `useState with a fetch/await initializer suggests useFetch or useAsyncData for automatic SSR hydration and request deduplication. See https://nuxt.com/docs/4.x/guide/data-fetching`;
|
|
223
223
|
function containsFetchOrAwait(node, visited) {
|
|
224
224
|
if (!node || visited.has(node)) return false;
|
|
225
225
|
visited.add(node);
|
|
@@ -256,13 +256,13 @@ const noUseStateForServerData = defineRule({ create(context) {
|
|
|
256
256
|
if (!containsFetchOrAwait(initFn.body, /* @__PURE__ */ new Set())) return;
|
|
257
257
|
context.report({
|
|
258
258
|
node,
|
|
259
|
-
message: MESSAGE$
|
|
259
|
+
message: MESSAGE$29
|
|
260
260
|
});
|
|
261
261
|
} };
|
|
262
262
|
} });
|
|
263
263
|
//#endregion
|
|
264
264
|
//#region src/rules/nuxt/data-fetching/useAsyncData-key-required-in-loop.ts
|
|
265
|
-
const MESSAGE$
|
|
265
|
+
const MESSAGE$28 = `useAsyncData/useFetch without an explicit string key inside a loop causes duplicate requests and cache fragmentation. Pass a unique string key as the first argument. See https://nuxt.com/docs/4.x/guide/data-fetching`;
|
|
266
266
|
const DATA_FETCHERS = /* @__PURE__ */ new Set(["useAsyncData", "useFetch"]);
|
|
267
267
|
function isMapCall(node) {
|
|
268
268
|
const callee = node.callee;
|
|
@@ -315,7 +315,7 @@ const useAsyncDataKeyRequiredInLoop = defineRule({ create(context) {
|
|
|
315
315
|
if (firstArg.type === "Literal" && typeof firstArg.value === "string") return;
|
|
316
316
|
context.report({
|
|
317
317
|
node,
|
|
318
|
-
message: MESSAGE$
|
|
318
|
+
message: MESSAGE$28
|
|
319
319
|
});
|
|
320
320
|
},
|
|
321
321
|
"CallExpression:exit"(node) {
|
|
@@ -325,7 +325,7 @@ const useAsyncDataKeyRequiredInLoop = defineRule({ create(context) {
|
|
|
325
325
|
} });
|
|
326
326
|
//#endregion
|
|
327
327
|
//#region src/rules/nuxt/security/no-user-input-in-fetch-url.ts
|
|
328
|
-
const MESSAGE$
|
|
328
|
+
const MESSAGE$27 = `Building a useFetch/$fetch URL from user input (route.query/route.params) lets an attacker control the request target — an SSRF risk during SSR. Use a fixed path and pass user input via the query/body options instead. See https://owasp.org/www-community/attacks/Server_Side_Request_Forgery`;
|
|
329
329
|
const FETCH_CALLEES = /* @__PURE__ */ new Set([
|
|
330
330
|
"useFetch",
|
|
331
331
|
"useLazyFetch",
|
|
@@ -363,14 +363,14 @@ const noUserInputInFetchUrl = defineRule({ create(context) {
|
|
|
363
363
|
const urlArg = node.arguments[0];
|
|
364
364
|
if (urlArgIsTainted(urlArg)) context.report({
|
|
365
365
|
node,
|
|
366
|
-
message: MESSAGE$
|
|
366
|
+
message: MESSAGE$27
|
|
367
367
|
});
|
|
368
368
|
} };
|
|
369
369
|
} });
|
|
370
370
|
//#endregion
|
|
371
371
|
//#region src/rules/nuxt/hydration/clientOnly-for-browser-apis.ts
|
|
372
|
-
const MESSAGE$
|
|
373
|
-
const BROWSER_GLOBALS = /* @__PURE__ */ new Set([
|
|
372
|
+
const MESSAGE$26 = `Accessing window./document./navigator./localStorage./sessionStorage. without an import.meta.client or process.client guard causes SSR failures. Wrap with if (import.meta.client) { ... } or move to onMounted. See https://nuxt.com/docs/4.x/guide/components`;
|
|
373
|
+
const BROWSER_GLOBALS$1 = /* @__PURE__ */ new Set([
|
|
374
374
|
"window",
|
|
375
375
|
"document",
|
|
376
376
|
"navigator",
|
|
@@ -437,17 +437,78 @@ const clientOnlyForBrowserApis = defineRule({ create(context) {
|
|
|
437
437
|
if (isGuarded) return;
|
|
438
438
|
const obj = node.object;
|
|
439
439
|
if (obj?.type !== "Identifier") return;
|
|
440
|
-
if (!BROWSER_GLOBALS.has(obj.name)) return;
|
|
440
|
+
if (!BROWSER_GLOBALS$1.has(obj.name)) return;
|
|
441
441
|
context.report({
|
|
442
442
|
node,
|
|
443
|
-
message: MESSAGE$
|
|
443
|
+
message: MESSAGE$26
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
} });
|
|
448
|
+
//#endregion
|
|
449
|
+
//#region src/rules/nuxt/hydration/no-browser-global-in-computed.ts
|
|
450
|
+
const MESSAGE$25 = `Reading a browser global inside a computed getter runs during SSR (computeds evaluate on the server render) and throws or hydration-mismatches. Guard with import.meta.client or derive the value in onMounted. See https://nuxt.com/docs/4.x/getting-started/data-fetching`;
|
|
451
|
+
const BROWSER_GLOBALS = /* @__PURE__ */ new Set([
|
|
452
|
+
"window",
|
|
453
|
+
"document",
|
|
454
|
+
"navigator",
|
|
455
|
+
"localStorage",
|
|
456
|
+
"sessionStorage"
|
|
457
|
+
]);
|
|
458
|
+
function calleeIdentifierName$3(node) {
|
|
459
|
+
const callee = node.callee;
|
|
460
|
+
if (callee?.type === "Identifier") return callee.name;
|
|
461
|
+
}
|
|
462
|
+
function isImportMetaClient(node) {
|
|
463
|
+
if (node.type !== "MemberExpression") return false;
|
|
464
|
+
if (node.object?.type !== "MetaProperty") return false;
|
|
465
|
+
const property = node.property;
|
|
466
|
+
return property?.type === "Identifier" && property.name === "client";
|
|
467
|
+
}
|
|
468
|
+
const noBrowserGlobalInComputed = defineRule({ create(context) {
|
|
469
|
+
const computedStack = [];
|
|
470
|
+
return {
|
|
471
|
+
CallExpression(node) {
|
|
472
|
+
if (calleeIdentifierName$3(node) === "computed") computedStack.push({
|
|
473
|
+
sourceCall: node,
|
|
474
|
+
guardDepth: 0
|
|
475
|
+
});
|
|
476
|
+
},
|
|
477
|
+
"CallExpression:exit"(node) {
|
|
478
|
+
if (computedStack[computedStack.length - 1]?.sourceCall === node) computedStack.pop();
|
|
479
|
+
},
|
|
480
|
+
IfStatement(node) {
|
|
481
|
+
const top = computedStack[computedStack.length - 1];
|
|
482
|
+
if (top && isImportMetaClient(node.test)) top.guardDepth += 1;
|
|
483
|
+
},
|
|
484
|
+
"IfStatement:exit"(node) {
|
|
485
|
+
const top = computedStack[computedStack.length - 1];
|
|
486
|
+
if (top && isImportMetaClient(node.test)) top.guardDepth -= 1;
|
|
487
|
+
},
|
|
488
|
+
ConditionalExpression(node) {
|
|
489
|
+
const top = computedStack[computedStack.length - 1];
|
|
490
|
+
if (top && isImportMetaClient(node.test)) top.guardDepth += 1;
|
|
491
|
+
},
|
|
492
|
+
"ConditionalExpression:exit"(node) {
|
|
493
|
+
const top = computedStack[computedStack.length - 1];
|
|
494
|
+
if (top && isImportMetaClient(node.test)) top.guardDepth -= 1;
|
|
495
|
+
},
|
|
496
|
+
MemberExpression(node) {
|
|
497
|
+
const top = computedStack[computedStack.length - 1];
|
|
498
|
+
if (!top || top.guardDepth > 0) return;
|
|
499
|
+
const object = node.object;
|
|
500
|
+
if (object?.type !== "Identifier") return;
|
|
501
|
+
if (!BROWSER_GLOBALS.has(object.name)) return;
|
|
502
|
+
context.report({
|
|
503
|
+
node,
|
|
504
|
+
message: MESSAGE$25
|
|
444
505
|
});
|
|
445
506
|
}
|
|
446
507
|
};
|
|
447
508
|
} });
|
|
448
509
|
//#endregion
|
|
449
510
|
//#region src/rules/nuxt/hydration/no-document-in-setup.ts
|
|
450
|
-
const MESSAGE$
|
|
511
|
+
const MESSAGE$24 = `Accessing document/window/navigator/localStorage at the top level of <script setup> causes a server-side crash (SSR). These browser globals are undefined on the server. Move access inside onMounted or guard with import.meta.client. See https://nuxt.com/docs/4.x/guide/components`;
|
|
451
512
|
const SSR_UNSAFE_GLOBALS = /* @__PURE__ */ new Set([
|
|
452
513
|
"document",
|
|
453
514
|
"window",
|
|
@@ -527,14 +588,14 @@ const noDocumentInSetup = defineRule({ create(context) {
|
|
|
527
588
|
if (isGuardedByImportMetaClient(node)) return;
|
|
528
589
|
context.report({
|
|
529
590
|
node,
|
|
530
|
-
message: MESSAGE$
|
|
591
|
+
message: MESSAGE$24
|
|
531
592
|
});
|
|
532
593
|
}
|
|
533
594
|
};
|
|
534
595
|
} });
|
|
535
596
|
//#endregion
|
|
536
597
|
//#region src/rules/nuxt/server-routes/createError-on-failure.ts
|
|
537
|
-
const MESSAGE$
|
|
598
|
+
const MESSAGE$23 = `throw new Error() leaks the call stack and exposes internal details. Use throw createError() from h3 for proper HTTP errors with no stack leak. See https://h3.unjs.io/guide/throw`;
|
|
538
599
|
const createErrorOnFailure = defineRule({ create(context) {
|
|
539
600
|
let handlerDepth = 0;
|
|
540
601
|
return {
|
|
@@ -559,14 +620,14 @@ const createErrorOnFailure = defineRule({ create(context) {
|
|
|
559
620
|
if (ctor.name !== "Error") return;
|
|
560
621
|
context.report({
|
|
561
622
|
node,
|
|
562
|
-
message: MESSAGE$
|
|
623
|
+
message: MESSAGE$23
|
|
563
624
|
});
|
|
564
625
|
}
|
|
565
626
|
};
|
|
566
627
|
} });
|
|
567
628
|
//#endregion
|
|
568
629
|
//#region src/rules/nuxt/server-routes/defineEventHandler-typed.ts
|
|
569
|
-
const MESSAGE$
|
|
630
|
+
const MESSAGE$22 = `defineEventHandler handler param event lacks a type annotation. Add a generic: defineEventHandler<H3Event>(...) or type the event parameter explicitly. See https://h3.unjs.io/guide/typed`;
|
|
570
631
|
function isFunction$2(node) {
|
|
571
632
|
return node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression";
|
|
572
633
|
}
|
|
@@ -585,13 +646,13 @@ const defineEventHandlerTyped = defineRule({ create(context) {
|
|
|
585
646
|
if (params[0].typeAnnotation) return;
|
|
586
647
|
context.report({
|
|
587
648
|
node,
|
|
588
|
-
message: MESSAGE$
|
|
649
|
+
message: MESSAGE$22
|
|
589
650
|
});
|
|
590
651
|
} };
|
|
591
652
|
} });
|
|
592
653
|
//#endregion
|
|
593
654
|
//#region src/rules/nuxt/server-routes/validate-body-with-h3-v2.ts
|
|
594
|
-
const MESSAGE$
|
|
655
|
+
const MESSAGE$21 = `readBody() is the legacy H3 reader. In Nuxt 4 with h3 v2, use readValidatedBody to parse and validate the request body in one step. See https://nuxt.com/docs/4.x/guide/data-fetching`;
|
|
595
656
|
const validateBodyWithH3V2 = defineRule({ create(context) {
|
|
596
657
|
return { CallExpression(node) {
|
|
597
658
|
const callee = node.callee;
|
|
@@ -599,7 +660,7 @@ const validateBodyWithH3V2 = defineRule({ create(context) {
|
|
|
599
660
|
if (callee.name !== "readBody") return;
|
|
600
661
|
context.report({
|
|
601
662
|
node,
|
|
602
|
-
message: MESSAGE$
|
|
663
|
+
message: MESSAGE$21
|
|
603
664
|
});
|
|
604
665
|
} };
|
|
605
666
|
} });
|
|
@@ -624,12 +685,13 @@ const NUXT_RULES = [
|
|
|
624
685
|
coreRule$1("server-routes/validate-body-with-h3-v2", "server-routes", "warn", true, defineRule(validateBodyWithH3V2)),
|
|
625
686
|
coreRule$1("server-routes/createError-on-failure", "server-routes", "warn", true, defineRule(createErrorOnFailure)),
|
|
626
687
|
coreRule$1("hydration/no-document-in-setup", "hydration", "error", true, defineRule(noDocumentInSetup)),
|
|
688
|
+
coreRule$1("hydration/no-browser-global-in-computed", "hydration", "error", true, defineRule(noBrowserGlobalInComputed)),
|
|
627
689
|
coreRule$1("hydration/clientOnly-for-browser-apis", "hydration", "error", true, defineRule(clientOnlyForBrowserApis)),
|
|
628
690
|
coreRule$1("security/no-user-input-in-fetch-url", "security", "warn", true, defineRule(noUserInputInFetchUrl))
|
|
629
691
|
];
|
|
630
692
|
//#endregion
|
|
631
693
|
//#region src/rules/vue/ai-slop/no-destructure-props-without-toRefs.ts
|
|
632
|
-
const MESSAGE$
|
|
694
|
+
const MESSAGE$20 = `Destructuring props loses reactivity. Wrap with toRefs(...) to keep refs. See https://vuejs.org/guide/components/props.html#reactive-props-destructure`;
|
|
633
695
|
function calleeName$6(node) {
|
|
634
696
|
const callee = node.callee;
|
|
635
697
|
if (callee?.type === "Identifier") return callee.name;
|
|
@@ -685,7 +747,7 @@ const noDestructurePropsWithoutToRefs = defineRule({ create(context) {
|
|
|
685
747
|
if (isToRefsCall$1(init)) return;
|
|
686
748
|
if ((isDefinePropsCall(init) || init.type === "Identifier" && propsSources.has(init.name)) && isOneTimeContext()) context.report({
|
|
687
749
|
node,
|
|
688
|
-
message: MESSAGE$
|
|
750
|
+
message: MESSAGE$20
|
|
689
751
|
});
|
|
690
752
|
},
|
|
691
753
|
Property(node) {
|
|
@@ -700,7 +762,7 @@ const noDestructurePropsWithoutToRefs = defineRule({ create(context) {
|
|
|
700
762
|
} });
|
|
701
763
|
//#endregion
|
|
702
764
|
//#region src/rules/vue/ai-slop/no-destructure-reactive-without-toRefs.ts
|
|
703
|
-
const MESSAGE$
|
|
765
|
+
const MESSAGE$19 = `Destructuring reactive state loses reactivity. Wrap with toRefs(...) or read single keys via toRef(state, 'key'). See https://vuejs.org/guide/essentials/reactivity-fundamentals.html#destructuring-reactive-state`;
|
|
704
766
|
const REACTIVE_FACTORIES = /* @__PURE__ */ new Set(["reactive", "shallowReactive"]);
|
|
705
767
|
function calleeName$5(node) {
|
|
706
768
|
const callee = node.callee;
|
|
@@ -732,7 +794,7 @@ const noDestructureReactiveWithoutToRefs = defineRule({ create(context) {
|
|
|
732
794
|
if (isToRefsCall(init)) return;
|
|
733
795
|
if (isReactiveCall(init) || init.type === "Identifier" && reactiveSources.has(init.name)) context.report({
|
|
734
796
|
node,
|
|
735
|
-
message: MESSAGE$
|
|
797
|
+
message: MESSAGE$19
|
|
736
798
|
});
|
|
737
799
|
} };
|
|
738
800
|
} });
|
|
@@ -801,7 +863,7 @@ const noImportsFromVueWhenAutoImported = defineRule({ create(context) {
|
|
|
801
863
|
} });
|
|
802
864
|
//#endregion
|
|
803
865
|
//#region src/rules/vue/ai-slop/no-non-null-assertion-on-ref-value.ts
|
|
804
|
-
const MESSAGE$
|
|
866
|
+
const MESSAGE$18 = `Non-null assertion on a ref's .value hides nullability. Add an explicit guard ('if (!r.value) return') or use optional chaining ('r.value?.x'). See https://vuejs.org/guide/typescript/composition-api.html#typing-ref`;
|
|
805
867
|
const REF_FACTORIES = /* @__PURE__ */ new Set([
|
|
806
868
|
"ref",
|
|
807
869
|
"shallowRef",
|
|
@@ -838,14 +900,14 @@ const noNonNullAssertionOnRefValue = defineRule({ create(context) {
|
|
|
838
900
|
if (!refSources.has(object.name)) return;
|
|
839
901
|
context.report({
|
|
840
902
|
node,
|
|
841
|
-
message: MESSAGE$
|
|
903
|
+
message: MESSAGE$18
|
|
842
904
|
});
|
|
843
905
|
}
|
|
844
906
|
};
|
|
845
907
|
} });
|
|
846
908
|
//#endregion
|
|
847
909
|
//#region src/rules/vue/composition/defineProps-typed.ts
|
|
848
|
-
const MESSAGE$
|
|
910
|
+
const MESSAGE$17 = `This defineProps() call declares props with a runtime object, which discards static type information. Use the type-only generic form (defineProps<{ ... }>()) so props are fully typed. See https://vuejs.org/guide/typescript/composition-api.html#typing-component-props`;
|
|
849
911
|
function calleeName$3(node) {
|
|
850
912
|
const callee = node.callee;
|
|
851
913
|
if (callee?.type === "Identifier") return callee.name;
|
|
@@ -855,13 +917,13 @@ const definePropsTyped = defineRule({ create(context) {
|
|
|
855
917
|
if (calleeName$3(node) !== "defineProps") return;
|
|
856
918
|
if (node.arguments[0]?.type === "ObjectExpression") context.report({
|
|
857
919
|
node,
|
|
858
|
-
message: MESSAGE$
|
|
920
|
+
message: MESSAGE$17
|
|
859
921
|
});
|
|
860
922
|
} };
|
|
861
923
|
} });
|
|
862
924
|
//#endregion
|
|
863
925
|
//#region src/rules/vue/composition/no-pinia-store-in-setup.ts
|
|
864
|
-
const MESSAGE$
|
|
926
|
+
const MESSAGE$16 = `This calls defineStore() inside a function (setup, a composable, or a component body), so a brand-new store definition is created on every call instead of one shared singleton. Call defineStore() once at module scope and call the returned useXxxStore() inside setup. See https://pinia.vuejs.org/core-concepts/#defining-a-store`;
|
|
865
927
|
const noPiniaStoreInSetup = defineRule({ create(context) {
|
|
866
928
|
let functionDepth = 0;
|
|
867
929
|
const enter = () => {
|
|
@@ -876,7 +938,7 @@ const noPiniaStoreInSetup = defineRule({ create(context) {
|
|
|
876
938
|
if (callee?.type !== "Identifier" || callee.name !== "defineStore") return;
|
|
877
939
|
if (functionDepth > 0) context.report({
|
|
878
940
|
node,
|
|
879
|
-
message: MESSAGE$
|
|
941
|
+
message: MESSAGE$16
|
|
880
942
|
});
|
|
881
943
|
},
|
|
882
944
|
FunctionDeclaration: enter,
|
|
@@ -888,8 +950,68 @@ const noPiniaStoreInSetup = defineRule({ create(context) {
|
|
|
888
950
|
};
|
|
889
951
|
} });
|
|
890
952
|
//#endregion
|
|
953
|
+
//#region src/rules/vue/composition/no-prop-callback-in-setup.ts
|
|
954
|
+
const MESSAGE$15 = `Calling a parent callback prop during setup or inside a computed getter runs a side effect on the render path — it fires on every re-evaluation and during SSR. Move the call into an event handler, watcher, or lifecycle hook. See https://vuejs.org/guide/components/events.html`;
|
|
955
|
+
const CALLBACK_NAME = /^on[A-Z]/;
|
|
956
|
+
function calleeIdentifierName$2(node) {
|
|
957
|
+
const callee = node.callee;
|
|
958
|
+
if (callee?.type === "Identifier") return callee.name;
|
|
959
|
+
}
|
|
960
|
+
function isComputedGetter(fn) {
|
|
961
|
+
const parent = fn.parent;
|
|
962
|
+
if (parent?.type !== "CallExpression") return false;
|
|
963
|
+
return calleeIdentifierName$2(parent) === "computed";
|
|
964
|
+
}
|
|
965
|
+
function definePropsBinding(node) {
|
|
966
|
+
let call = node.init;
|
|
967
|
+
if (call?.type === "CallExpression" && calleeIdentifierName$2(call) === "withDefaults") call = call.arguments[0];
|
|
968
|
+
if (call?.type !== "CallExpression" || calleeIdentifierName$2(call) !== "defineProps") return;
|
|
969
|
+
const id = node.id;
|
|
970
|
+
if (id?.type !== "Identifier") return void 0;
|
|
971
|
+
return id.name;
|
|
972
|
+
}
|
|
973
|
+
const noPropCallbackInSetup = defineRule({ create(context) {
|
|
974
|
+
const propsNames = /* @__PURE__ */ new Set();
|
|
975
|
+
const functionStack = [];
|
|
976
|
+
const enterFunction = (node) => {
|
|
977
|
+
functionStack.push({
|
|
978
|
+
node,
|
|
979
|
+
isComputedGetter: isComputedGetter(node)
|
|
980
|
+
});
|
|
981
|
+
};
|
|
982
|
+
const exitFunction = () => {
|
|
983
|
+
functionStack.pop();
|
|
984
|
+
};
|
|
985
|
+
return {
|
|
986
|
+
VariableDeclarator(node) {
|
|
987
|
+
const name = definePropsBinding(node);
|
|
988
|
+
if (name !== void 0) propsNames.add(name);
|
|
989
|
+
},
|
|
990
|
+
FunctionDeclaration: enterFunction,
|
|
991
|
+
"FunctionDeclaration:exit": exitFunction,
|
|
992
|
+
FunctionExpression: enterFunction,
|
|
993
|
+
"FunctionExpression:exit": exitFunction,
|
|
994
|
+
ArrowFunctionExpression: enterFunction,
|
|
995
|
+
"ArrowFunctionExpression:exit": exitFunction,
|
|
996
|
+
CallExpression(node) {
|
|
997
|
+
const callee = node.callee;
|
|
998
|
+
if (callee?.type !== "MemberExpression" || callee.computed === true) return;
|
|
999
|
+
const object = callee.object;
|
|
1000
|
+
const property = callee.property;
|
|
1001
|
+
if (object?.type !== "Identifier" || property?.type !== "Identifier") return;
|
|
1002
|
+
if (!propsNames.has(object.name)) return;
|
|
1003
|
+
if (!CALLBACK_NAME.test(property.name)) return;
|
|
1004
|
+
const enclosing = functionStack[functionStack.length - 1];
|
|
1005
|
+
if (enclosing === void 0 || enclosing.isComputedGetter) context.report({
|
|
1006
|
+
node,
|
|
1007
|
+
message: MESSAGE$15
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
} });
|
|
1012
|
+
//#endregion
|
|
891
1013
|
//#region src/rules/vue/composition/prefer-script-setup-for-new-files.ts
|
|
892
|
-
const MESSAGE$
|
|
1014
|
+
const MESSAGE$14 = `This component nests composition-API logic inside an options-object setup(). Use a <script setup> block instead — it removes the setup() boilerplate and exposes bindings to the template directly. See https://vuejs.org/api/sfc-script-setup.html`;
|
|
893
1015
|
function isFunctionValue(node) {
|
|
894
1016
|
return node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression";
|
|
895
1017
|
}
|
|
@@ -906,13 +1028,13 @@ const preferScriptSetupForNewFiles = defineRule({ create(context) {
|
|
|
906
1028
|
if (declaration?.type !== "ObjectExpression") return;
|
|
907
1029
|
if (declaration.properties.some(isSetupProperty)) context.report({
|
|
908
1030
|
node,
|
|
909
|
-
message: MESSAGE$
|
|
1031
|
+
message: MESSAGE$14
|
|
910
1032
|
});
|
|
911
1033
|
} };
|
|
912
1034
|
} });
|
|
913
1035
|
//#endregion
|
|
914
1036
|
//#region src/rules/vue/performance/prefer-defineAsyncComponent-on-route.ts
|
|
915
|
-
const MESSAGE$
|
|
1037
|
+
const MESSAGE$13 = `This route record's \`component:\` field references the imported component directly, which forces a synchronous import of the route bundle. Use \`() => import('./Foo.vue')\` or \`defineAsyncComponent(() => import('./Foo.vue'))\` to lazy-load the route. See https://vuejs.org/guide/components/async.html`;
|
|
916
1038
|
function keyName(key) {
|
|
917
1039
|
return key.type === "Identifier" ? key.name : key.value;
|
|
918
1040
|
}
|
|
@@ -922,13 +1044,13 @@ const preferDefineAsyncComponentOnRoute = defineRule({ create(context) {
|
|
|
922
1044
|
if (keyName(node.key) !== "component") return;
|
|
923
1045
|
if (node.value.type === "Identifier") context.report({
|
|
924
1046
|
node,
|
|
925
|
-
message: MESSAGE$
|
|
1047
|
+
message: MESSAGE$13
|
|
926
1048
|
});
|
|
927
1049
|
} };
|
|
928
1050
|
} });
|
|
929
1051
|
//#endregion
|
|
930
1052
|
//#region src/rules/vue/performance/prefer-module-scope-pure-function.ts
|
|
931
|
-
const MESSAGE$
|
|
1053
|
+
const MESSAGE$12 = `This helper closes over nothing reactive (only its parameters, imports, and globals), yet it is re-created on every call of the enclosing function. Hoist it to module scope so a single function reference is shared across all component instances. See https://vuejs.org/guide/best-practices/performance.html`;
|
|
932
1054
|
const GLOBALS = /* @__PURE__ */ new Set([
|
|
933
1055
|
"console",
|
|
934
1056
|
"Math",
|
|
@@ -1056,7 +1178,7 @@ const preferModuleScopePureFunction = defineRule({ create(context) {
|
|
|
1056
1178
|
if (functionDepth < 1) return;
|
|
1057
1179
|
if (isHoistable(fn, imports)) context.report({
|
|
1058
1180
|
node: fn,
|
|
1059
|
-
message: MESSAGE$
|
|
1181
|
+
message: MESSAGE$12
|
|
1060
1182
|
});
|
|
1061
1183
|
};
|
|
1062
1184
|
return {
|
|
@@ -1074,7 +1196,7 @@ const preferModuleScopePureFunction = defineRule({ create(context) {
|
|
|
1074
1196
|
if (init?.type === "ArrowFunctionExpression" || init?.type === "FunctionExpression") {
|
|
1075
1197
|
if (isHoistable(init, imports)) context.report({
|
|
1076
1198
|
node: init,
|
|
1077
|
-
message: MESSAGE$
|
|
1199
|
+
message: MESSAGE$12
|
|
1078
1200
|
});
|
|
1079
1201
|
}
|
|
1080
1202
|
}
|
|
@@ -1094,7 +1216,7 @@ const preferModuleScopePureFunction = defineRule({ create(context) {
|
|
|
1094
1216
|
} });
|
|
1095
1217
|
//#endregion
|
|
1096
1218
|
//#region src/rules/vue/performance/prefer-module-scope-static-value.ts
|
|
1097
|
-
const MESSAGE$
|
|
1219
|
+
const MESSAGE$11 = `This static array or object literal is declared inside a function, so a fresh copy is allocated on every call (including every component instantiation). Hoist it to module scope so the single frozen reference is shared. See https://vuejs.org/guide/best-practices/performance.html`;
|
|
1098
1220
|
const LITERAL_TYPES = /* @__PURE__ */ new Set([
|
|
1099
1221
|
"Literal",
|
|
1100
1222
|
"StringLiteral",
|
|
@@ -1159,14 +1281,14 @@ const preferModuleScopeStaticValue = defineRule({ create(context) {
|
|
|
1159
1281
|
if (!isStaticLiteral(init)) return;
|
|
1160
1282
|
context.report({
|
|
1161
1283
|
node: init,
|
|
1162
|
-
message: MESSAGE$
|
|
1284
|
+
message: MESSAGE$11
|
|
1163
1285
|
});
|
|
1164
1286
|
}
|
|
1165
1287
|
};
|
|
1166
1288
|
} });
|
|
1167
1289
|
//#endregion
|
|
1168
1290
|
//#region src/rules/vue/performance/prefer-stable-empty-fallback.ts
|
|
1169
|
-
const MESSAGE$
|
|
1291
|
+
const MESSAGE$10 = `This computed falls back to a fresh empty array or object literal, so it returns a brand-new reference whenever the source is nullish. Downstream watchers, child props, and v-memo see an identity change every recompute. Hoist a single module-scope EMPTY constant and fall back to that. See https://vuejs.org/guide/best-practices/performance.html`;
|
|
1170
1292
|
function isEmptyLiteral(node) {
|
|
1171
1293
|
if (node?.type === "ArrayExpression") return node.elements.length === 0;
|
|
1172
1294
|
if (node?.type === "ObjectExpression") return node.properties.length === 0;
|
|
@@ -1196,13 +1318,114 @@ const preferStableEmptyFallback = defineRule({ create(context) {
|
|
|
1196
1318
|
const returned = getterBody(node);
|
|
1197
1319
|
if (returned && fallsBackToEmptyLiteral(returned)) context.report({
|
|
1198
1320
|
node: returned,
|
|
1199
|
-
message: MESSAGE$
|
|
1321
|
+
message: MESSAGE$10
|
|
1200
1322
|
});
|
|
1201
1323
|
} };
|
|
1202
1324
|
} });
|
|
1203
1325
|
//#endregion
|
|
1326
|
+
//#region src/rules/vue/reactivity/effect-listener-cleanup-mismatch.ts
|
|
1327
|
+
const MESSAGE$9 = `This cleanup cannot detach the listener it pairs with: removeEventListener only removes when the handler reference AND capture flag match addEventListener exactly. Use the same named handler (never an inline arrow) and the same capture flag. See https://vuejs.org/guide/essentials/watchers.html#side-effect-cleanup`;
|
|
1328
|
+
const EFFECT_CALLS = /* @__PURE__ */ new Set([
|
|
1329
|
+
"watch",
|
|
1330
|
+
"watchEffect",
|
|
1331
|
+
"onMounted",
|
|
1332
|
+
"onBeforeUnmount",
|
|
1333
|
+
"onUnmounted"
|
|
1334
|
+
]);
|
|
1335
|
+
function calleeIdentifierName$1(node) {
|
|
1336
|
+
const callee = node.callee;
|
|
1337
|
+
if (callee?.type === "Identifier") return callee.name;
|
|
1338
|
+
}
|
|
1339
|
+
function calledMethodName$1(node) {
|
|
1340
|
+
const callee = node.callee;
|
|
1341
|
+
if (callee?.type === "MemberExpression" && callee.computed !== true) return callee.property.name;
|
|
1342
|
+
}
|
|
1343
|
+
function literalString(node) {
|
|
1344
|
+
if (node?.type === "Literal" && typeof node.value === "string") return node.value;
|
|
1345
|
+
}
|
|
1346
|
+
function handlerKey(node) {
|
|
1347
|
+
if (node?.type === "Identifier") return node.name;
|
|
1348
|
+
if (node?.type === "MemberExpression" && node.computed !== true) {
|
|
1349
|
+
const object = handlerKey(node.object);
|
|
1350
|
+
if (object === void 0) return void 0;
|
|
1351
|
+
return `${object}.${node.property.name}`;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
function captureKey(node) {
|
|
1355
|
+
if (node === void 0) return "false";
|
|
1356
|
+
if (node.type === "Literal") return String(node.value);
|
|
1357
|
+
if (node.type === "ObjectExpression") {
|
|
1358
|
+
const props = node.properties;
|
|
1359
|
+
for (const prop of props) {
|
|
1360
|
+
const key = prop.key;
|
|
1361
|
+
if (key?.type === "Identifier" && key.name === "capture") return String(prop.value?.value ?? "unknown");
|
|
1362
|
+
}
|
|
1363
|
+
return "false";
|
|
1364
|
+
}
|
|
1365
|
+
return "unknown";
|
|
1366
|
+
}
|
|
1367
|
+
function toRecord(node) {
|
|
1368
|
+
const args = node.arguments;
|
|
1369
|
+
const event = literalString(args[0]);
|
|
1370
|
+
if (event === void 0) return void 0;
|
|
1371
|
+
return {
|
|
1372
|
+
event,
|
|
1373
|
+
handlerKey: handlerKey(args[1]),
|
|
1374
|
+
capture: captureKey(args[2]),
|
|
1375
|
+
node
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
function reportMismatches(context, frame) {
|
|
1379
|
+
for (const removed of frame.removed) {
|
|
1380
|
+
const pairable = frame.added.filter((a) => a.event === removed.event);
|
|
1381
|
+
if (pairable.length === 0) continue;
|
|
1382
|
+
if (!pairable.some((a) => a.handlerKey !== void 0 && a.handlerKey === removed.handlerKey && a.capture === removed.capture)) context.report({
|
|
1383
|
+
node: removed.node,
|
|
1384
|
+
message: MESSAGE$9
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
const effectListenerCleanupMismatch = defineRule({ create(context) {
|
|
1389
|
+
const stack = [];
|
|
1390
|
+
return {
|
|
1391
|
+
CallExpression(node) {
|
|
1392
|
+
const name = calleeIdentifierName$1(node);
|
|
1393
|
+
if (name !== void 0 && EFFECT_CALLS.has(name)) {
|
|
1394
|
+
stack.push({
|
|
1395
|
+
sourceCall: node,
|
|
1396
|
+
added: [],
|
|
1397
|
+
removed: []
|
|
1398
|
+
});
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
if (stack.length === 0) return;
|
|
1402
|
+
const method = calledMethodName$1(node);
|
|
1403
|
+
if (method === "addEventListener") {
|
|
1404
|
+
const record = toRecord(node);
|
|
1405
|
+
if (record) stack[stack.length - 1].added.push(record);
|
|
1406
|
+
} else if (method === "removeEventListener") {
|
|
1407
|
+
const record = toRecord(node);
|
|
1408
|
+
if (record) stack[stack.length - 1].removed.push(record);
|
|
1409
|
+
}
|
|
1410
|
+
},
|
|
1411
|
+
"CallExpression:exit"(node) {
|
|
1412
|
+
if (stack.length === 0) return;
|
|
1413
|
+
const top = stack[stack.length - 1];
|
|
1414
|
+
if (top.sourceCall !== node) return;
|
|
1415
|
+
stack.pop();
|
|
1416
|
+
const parent = stack[stack.length - 1];
|
|
1417
|
+
if (parent) {
|
|
1418
|
+
parent.added.push(...top.added);
|
|
1419
|
+
parent.removed.push(...top.removed);
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
reportMismatches(context, top);
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
} });
|
|
1426
|
+
//#endregion
|
|
1204
1427
|
//#region src/rules/vue/reactivity/no-fresh-deps-in-watch.ts
|
|
1205
|
-
const MESSAGE$
|
|
1428
|
+
const MESSAGE$8 = `This watch getter returns a freshly-constructed array or object literal, so Vue compares it by reference (Object.is) and the watcher re-fires on every reactive tick — even when nothing changed. Use the multi-source array form watch([a, b], cb) for several sources, or watch a primitive/ref directly. See https://vuejs.org/guide/essentials/watchers.html#watch-source-types`;
|
|
1206
1429
|
function isFunction$1(node) {
|
|
1207
1430
|
return node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression";
|
|
1208
1431
|
}
|
|
@@ -1229,11 +1452,74 @@ const noFreshDepsInWatch = defineRule({ create(context) {
|
|
|
1229
1452
|
if (!source || !isFunction$1(source)) return;
|
|
1230
1453
|
if (getterReturnsFreshLiteral(source)) context.report({
|
|
1231
1454
|
node: source,
|
|
1232
|
-
message: MESSAGE$
|
|
1455
|
+
message: MESSAGE$8
|
|
1233
1456
|
});
|
|
1234
1457
|
} };
|
|
1235
1458
|
} });
|
|
1236
1459
|
//#endregion
|
|
1460
|
+
//#region src/rules/vue/reactivity/no-stale-timer-ref.ts
|
|
1461
|
+
const MESSAGE$7 = `clearTimeout/clearInterval on a ref leaves the stale timer id in .value, so any "timer pending" check on that ref keeps passing after the timer is gone. Reset the ref (e.g. timer.value = null) after clearing. See https://vuejs.org/guide/essentials/reactivity-fundamentals`;
|
|
1462
|
+
const CLEAR_CALLS = /* @__PURE__ */ new Set(["clearTimeout", "clearInterval"]);
|
|
1463
|
+
/** Matches `<identifier>.value` and returns the identifier name. */
|
|
1464
|
+
function refValueTarget(node) {
|
|
1465
|
+
if (node?.type !== "MemberExpression" || node.computed === true) return;
|
|
1466
|
+
const object = node.object;
|
|
1467
|
+
const property = node.property;
|
|
1468
|
+
if (object?.type !== "Identifier" || property?.type !== "Identifier") return;
|
|
1469
|
+
if (property.name !== "value") return void 0;
|
|
1470
|
+
return object.name;
|
|
1471
|
+
}
|
|
1472
|
+
function flush(context, frame) {
|
|
1473
|
+
for (const item of frame.pending) context.report({
|
|
1474
|
+
node: item.node,
|
|
1475
|
+
message: MESSAGE$7
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
const noStaleTimerRef = defineRule({ create(context) {
|
|
1479
|
+
const frames = [{
|
|
1480
|
+
owner: null,
|
|
1481
|
+
pending: []
|
|
1482
|
+
}];
|
|
1483
|
+
const top = () => frames[frames.length - 1];
|
|
1484
|
+
const enterFunction = (node) => {
|
|
1485
|
+
frames.push({
|
|
1486
|
+
owner: node,
|
|
1487
|
+
pending: []
|
|
1488
|
+
});
|
|
1489
|
+
};
|
|
1490
|
+
const exitFunction = () => {
|
|
1491
|
+
flush(context, frames.pop());
|
|
1492
|
+
};
|
|
1493
|
+
return {
|
|
1494
|
+
FunctionDeclaration: enterFunction,
|
|
1495
|
+
"FunctionDeclaration:exit": exitFunction,
|
|
1496
|
+
FunctionExpression: enterFunction,
|
|
1497
|
+
"FunctionExpression:exit": exitFunction,
|
|
1498
|
+
ArrowFunctionExpression: enterFunction,
|
|
1499
|
+
"ArrowFunctionExpression:exit": exitFunction,
|
|
1500
|
+
CallExpression(node) {
|
|
1501
|
+
const callee = node.callee;
|
|
1502
|
+
if (callee?.type !== "Identifier" || !CLEAR_CALLS.has(callee.name)) return;
|
|
1503
|
+
const args = node.arguments;
|
|
1504
|
+
const refName = refValueTarget(args[0]);
|
|
1505
|
+
if (refName === void 0) return;
|
|
1506
|
+
top().pending.push({
|
|
1507
|
+
refName,
|
|
1508
|
+
node
|
|
1509
|
+
});
|
|
1510
|
+
},
|
|
1511
|
+
AssignmentExpression(node) {
|
|
1512
|
+
const refName = refValueTarget(node.left);
|
|
1513
|
+
if (refName === void 0) return;
|
|
1514
|
+
const frame = top();
|
|
1515
|
+
frame.pending = frame.pending.filter((p) => p.refName !== refName);
|
|
1516
|
+
},
|
|
1517
|
+
"Program:exit"() {
|
|
1518
|
+
flush(context, frames[0]);
|
|
1519
|
+
}
|
|
1520
|
+
};
|
|
1521
|
+
} });
|
|
1522
|
+
//#endregion
|
|
1237
1523
|
//#region src/rules/vue/reactivity/prefer-readonly-for-injected.ts
|
|
1238
1524
|
const MESSAGE$6 = `Mutating an injected value from a consumer breaks one-way data flow and makes the source of changes hard to trace. Wrap the provided value in readonly() at the provide site and expose a dedicated mutator instead. See https://vuejs.org/guide/components/provide-inject.html#working-with-reactivity`;
|
|
1239
1525
|
const MUTATING_METHODS = /* @__PURE__ */ new Set([
|
|
@@ -1331,10 +1617,15 @@ const REGISTER_CALLS = /* @__PURE__ */ new Set([
|
|
|
1331
1617
|
"setInterval",
|
|
1332
1618
|
"setTimeout"
|
|
1333
1619
|
]);
|
|
1334
|
-
const
|
|
1620
|
+
const RETAINED_RESOURCE_CONSTRUCTORS = /* @__PURE__ */ new Set([
|
|
1335
1621
|
"IntersectionObserver",
|
|
1336
1622
|
"MutationObserver",
|
|
1337
|
-
"ResizeObserver"
|
|
1623
|
+
"ResizeObserver",
|
|
1624
|
+
"PerformanceObserver",
|
|
1625
|
+
"WebSocket",
|
|
1626
|
+
"EventSource",
|
|
1627
|
+
"BroadcastChannel",
|
|
1628
|
+
"RTCPeerConnection"
|
|
1338
1629
|
]);
|
|
1339
1630
|
const CLEANUP_CALLS = /* @__PURE__ */ new Set(["onCleanup", "onWatcherCleanup"]);
|
|
1340
1631
|
function calleeIdentifierName(node) {
|
|
@@ -1375,7 +1666,7 @@ const watchWithoutCleanup = defineRule({ create(context) {
|
|
|
1375
1666
|
NewExpression(node) {
|
|
1376
1667
|
if (watchStack.length === 0) return;
|
|
1377
1668
|
const callee = node.callee;
|
|
1378
|
-
if (callee?.type === "Identifier" &&
|
|
1669
|
+
if (callee?.type === "Identifier" && RETAINED_RESOURCE_CONSTRUCTORS.has(callee.name)) watchStack[watchStack.length - 1].hasRegistration = true;
|
|
1379
1670
|
},
|
|
1380
1671
|
ReturnStatement(node) {
|
|
1381
1672
|
if (watchStack.length === 0) return;
|
|
@@ -1569,9 +1860,12 @@ const VUE_RULES = [
|
|
|
1569
1860
|
coreRule("reactivity/prefer-shallowRef-for-large-data", "reactivity", "info", false, defineRule(preferShallowRefForLargeData)),
|
|
1570
1861
|
coreRule("reactivity/prefer-readonly-for-injected", "reactivity", "info", false, defineRule(preferReadonlyForInjected)),
|
|
1571
1862
|
coreRule("reactivity/no-fresh-deps-in-watch", "reactivity", "warn", true, defineRule(noFreshDepsInWatch)),
|
|
1863
|
+
coreRule("reactivity/no-stale-timer-ref", "reactivity", "warn", true, defineRule(noStaleTimerRef)),
|
|
1864
|
+
coreRule("reactivity/effect-listener-cleanup-mismatch", "reactivity", "warn", true, defineRule(effectListenerCleanupMismatch)),
|
|
1572
1865
|
coreRule("composition/prefer-script-setup-for-new-files", "composition", "warn", true, defineRule(preferScriptSetupForNewFiles)),
|
|
1573
1866
|
coreRule("composition/defineProps-typed", "composition", "warn", true, defineRule(definePropsTyped)),
|
|
1574
1867
|
coreRule("composition/no-pinia-store-in-setup", "composition", "warn", true, defineRule(noPiniaStoreInSetup)),
|
|
1868
|
+
coreRule("composition/no-prop-callback-in-setup", "composition", "warn", true, defineRule(noPropCallbackInSetup)),
|
|
1575
1869
|
coreRule("performance/prefer-defineAsyncComponent-on-route", "performance", "info", false, defineRule(preferDefineAsyncComponentOnRoute)),
|
|
1576
1870
|
coreRule("performance/prefer-module-scope-static-value", "performance", "info", false, defineRule(preferModuleScopeStaticValue)),
|
|
1577
1871
|
coreRule("performance/prefer-module-scope-pure-function", "performance", "info", false, defineRule(preferModuleScopePureFunction)),
|