@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 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$27 = `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`;
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$27
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$27
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$26 = `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`;
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$26
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$25 = `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`;
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$25
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$24 = `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`;
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$24
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$23 = `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`;
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$23
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$22 = `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 = /* @__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$22
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$21 = `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`;
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$21
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$20 = `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`;
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$20
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$19 = `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`;
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$19
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$18 = `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`;
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$18
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$17 = `Destructuring props loses reactivity. Wrap with toRefs(...) to keep refs. See https://vuejs.org/guide/components/props.html#reactive-props-destructure`;
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$17
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$16 = `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`;
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$16
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$15 = `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`;
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$15
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$14 = `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`;
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$14
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$13 = `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`;
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$13
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$12 = `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`;
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$12
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$11 = `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`;
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$11
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$10 = `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`;
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$10
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$10
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$9 = `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`;
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$9
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$8 = `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`;
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$8
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$7 = `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`;
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$7
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 OBSERVERS = /* @__PURE__ */ new Set([
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" && OBSERVERS.has(callee.name)) watchStack[watchStack.length - 1].hasRegistration = true;
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)),