@stll/oxlint-plugin 0.6.2 → 0.6.3

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.
Files changed (85) hide show
  1. package/dist/bun-test-hygiene.cjs +2 -2
  2. package/dist/bun-test-hygiene.cjs.map +1 -1
  3. package/dist/bun-test-hygiene.d.cts +3 -2
  4. package/dist/bun-test-hygiene.d.mts +3 -2
  5. package/dist/bun-test-hygiene.mjs +2 -2
  6. package/dist/bun-test-hygiene.mjs.map +1 -1
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/no-any-casts.cjs +2 -2
  10. package/dist/no-any-casts.cjs.map +1 -1
  11. package/dist/no-any-casts.d.cts +3 -2
  12. package/dist/no-any-casts.d.mts +3 -2
  13. package/dist/no-any-casts.mjs +2 -2
  14. package/dist/no-any-casts.mjs.map +1 -1
  15. package/dist/no-checkout-writes-in-tests.cjs +2 -2
  16. package/dist/no-checkout-writes-in-tests.cjs.map +1 -1
  17. package/dist/no-checkout-writes-in-tests.d.cts +3 -2
  18. package/dist/no-checkout-writes-in-tests.d.mts +3 -2
  19. package/dist/no-checkout-writes-in-tests.mjs +2 -2
  20. package/dist/no-checkout-writes-in-tests.mjs.map +1 -1
  21. package/dist/no-dangerous-type-assertions.cjs +2 -2
  22. package/dist/no-dangerous-type-assertions.cjs.map +1 -1
  23. package/dist/no-dangerous-type-assertions.d.cts +3 -2
  24. package/dist/no-dangerous-type-assertions.d.mts +3 -2
  25. package/dist/no-dangerous-type-assertions.mjs +2 -2
  26. package/dist/no-dangerous-type-assertions.mjs.map +1 -1
  27. package/dist/no-partial-record-satisfies.cjs +2 -2
  28. package/dist/no-partial-record-satisfies.cjs.map +1 -1
  29. package/dist/no-partial-record-satisfies.d.cts +3 -2
  30. package/dist/no-partial-record-satisfies.d.mts +3 -2
  31. package/dist/no-partial-record-satisfies.mjs +2 -2
  32. package/dist/no-partial-record-satisfies.mjs.map +1 -1
  33. package/dist/no-raw-route-query-client.cjs +2 -2
  34. package/dist/no-raw-route-query-client.cjs.map +1 -1
  35. package/dist/no-raw-route-query-client.d.cts +3 -2
  36. package/dist/no-raw-route-query-client.d.mts +3 -2
  37. package/dist/no-raw-route-query-client.mjs +2 -2
  38. package/dist/no-raw-route-query-client.mjs.map +1 -1
  39. package/dist/no-real-network-in-tests.cjs +2 -2
  40. package/dist/no-real-network-in-tests.cjs.map +1 -1
  41. package/dist/no-real-network-in-tests.d.cts +3 -2
  42. package/dist/no-real-network-in-tests.d.mts +3 -2
  43. package/dist/no-real-network-in-tests.mjs +2 -2
  44. package/dist/no-real-network-in-tests.mjs.map +1 -1
  45. package/dist/no-real-timer-waits-in-tests.cjs +2 -2
  46. package/dist/no-real-timer-waits-in-tests.cjs.map +1 -1
  47. package/dist/no-real-timer-waits-in-tests.d.cts +3 -2
  48. package/dist/no-real-timer-waits-in-tests.d.mts +3 -2
  49. package/dist/no-real-timer-waits-in-tests.mjs +2 -2
  50. package/dist/no-real-timer-waits-in-tests.mjs.map +1 -1
  51. package/dist/no-unsafe-inner-html.cjs +2 -2
  52. package/dist/no-unsafe-inner-html.cjs.map +1 -1
  53. package/dist/no-unsafe-inner-html.d.cts +3 -2
  54. package/dist/no-unsafe-inner-html.d.mts +3 -2
  55. package/dist/no-unsafe-inner-html.mjs +2 -2
  56. package/dist/no-unsafe-inner-html.mjs.map +1 -1
  57. package/dist/no-unseeded-random-in-tests.cjs +2 -2
  58. package/dist/no-unseeded-random-in-tests.cjs.map +1 -1
  59. package/dist/no-unseeded-random-in-tests.d.cts +3 -2
  60. package/dist/no-unseeded-random-in-tests.d.mts +3 -2
  61. package/dist/no-unseeded-random-in-tests.mjs +2 -2
  62. package/dist/no-unseeded-random-in-tests.mjs.map +1 -1
  63. package/dist/no-wall-clock-bound.cjs +2 -2
  64. package/dist/no-wall-clock-bound.cjs.map +1 -1
  65. package/dist/no-wall-clock-bound.d.cts +3 -2
  66. package/dist/no-wall-clock-bound.d.mts +3 -2
  67. package/dist/no-wall-clock-bound.mjs +2 -2
  68. package/dist/no-wall-clock-bound.mjs.map +1 -1
  69. package/dist/no-wall-clock-in-tests.cjs +2 -2
  70. package/dist/no-wall-clock-in-tests.cjs.map +1 -1
  71. package/dist/no-wall-clock-in-tests.d.cts +3 -2
  72. package/dist/no-wall-clock-in-tests.d.mts +3 -2
  73. package/dist/no-wall-clock-in-tests.mjs +2 -2
  74. package/dist/no-wall-clock-in-tests.mjs.map +1 -1
  75. package/dist/require-loader-prefetch.cjs +2 -2
  76. package/dist/require-loader-prefetch.cjs.map +1 -1
  77. package/dist/require-loader-prefetch.d.cts +3 -2
  78. package/dist/require-loader-prefetch.d.mts +3 -2
  79. package/dist/require-loader-prefetch.mjs +2 -2
  80. package/dist/require-loader-prefetch.mjs.map +1 -1
  81. package/dist/test-rule-utils.cjs.map +1 -1
  82. package/dist/test-rule-utils.mjs.map +1 -1
  83. package/dist/utils.cjs.map +1 -1
  84. package/dist/utils.mjs.map +1 -1
  85. package/package.json +3 -4
@@ -1 +1 @@
1
- {"version":3,"file":"no-wall-clock-in-tests.cjs","names":["testOnlyPlugin","createBindingResolver","isNode","propertyName"],"sources":["../src/no-wall-clock-in-tests.ts"],"sourcesContent":["import {\n createBindingResolver,\n isNode,\n propertyName,\n testOnlyPlugin,\n} from \"./test-rule-utils\";\n\nexport default testOnlyPlugin(\n \"no-wall-clock-in-tests\",\n \"noWallClock\",\n \"Use the @stll/test-kit clock instead of reading the wall clock in tests.\",\n (report, getScope) => {\n const resolveBinding = createBindingResolver(getScope);\n const isGlobal = (value: unknown, name: string): boolean => {\n if (!isNode(value)) return false;\n const binding = resolveBinding(value);\n return binding.type === \"global\" && binding.name === name;\n };\n const isClockMember = (value: unknown): boolean => {\n if (!isNode(value)) return false;\n if (value.type === \"Identifier\") {\n const binding = resolveBinding(value);\n if (binding.type === \"global\")\n return (\n binding.name === \"Date.now\" || binding.name === \"performance.now\"\n );\n return binding.type === \"const\" && isClockMember(binding.initializer);\n }\n return (\n value.type === \"MemberExpression\" &&\n propertyName(value.property) === \"now\" &&\n (isGlobal(value.object, \"Date\") ||\n isGlobal(value.object, \"performance\"))\n );\n };\n return {\n CallExpression(node) {\n if (\n isClockMember(node.callee) ||\n (isNode(node.callee) &&\n isGlobal(node.callee, \"Date\") &&\n Array.isArray(node.arguments) &&\n node.arguments.length === 0)\n )\n report(node);\n },\n NewExpression(node) {\n if (\n isNode(node.callee) &&\n isGlobal(node.callee, \"Date\") &&\n Array.isArray(node.arguments) &&\n node.arguments.length === 0\n )\n report(node);\n },\n };\n },\n);\n"],"mappings":";;;;;;AAOA,IAAA,iCAAeA,wBAAAA,eACb,0BACA,eACA,6EACC,QAAQ,aAAa;CACpB,MAAM,iBAAiBC,wBAAAA,sBAAsB,QAAQ;CACrD,MAAM,YAAY,OAAgB,SAA0B;EAC1D,IAAI,CAACC,wBAAAA,OAAO,KAAK,GAAG,OAAO;EAC3B,MAAM,UAAU,eAAe,KAAK;EACpC,OAAO,QAAQ,SAAS,YAAY,QAAQ,SAAS;CACvD;CACA,MAAM,iBAAiB,UAA4B;EACjD,IAAI,CAACA,wBAAAA,OAAO,KAAK,GAAG,OAAO;EAC3B,IAAI,MAAM,SAAS,cAAc;GAC/B,MAAM,UAAU,eAAe,KAAK;GACpC,IAAI,QAAQ,SAAS,UACnB,OACE,QAAQ,SAAS,cAAc,QAAQ,SAAS;GAEpD,OAAO,QAAQ,SAAS,WAAW,cAAc,QAAQ,WAAW;EACtE;EACA,OACE,MAAM,SAAS,sBACfC,wBAAAA,aAAa,MAAM,QAAQ,MAAM,UAChC,SAAS,MAAM,QAAQ,MAAM,KAC5B,SAAS,MAAM,QAAQ,aAAa;CAE1C;CACA,OAAO;EACL,eAAe,MAAM;GACnB,IACE,cAAc,KAAK,MAAM,KACxBD,wBAAAA,OAAO,KAAK,MAAM,KACjB,SAAS,KAAK,QAAQ,MAAM,KAC5B,MAAM,QAAQ,KAAK,SAAS,KAC5B,KAAK,UAAU,WAAW,GAE5B,OAAO,IAAI;EACf;EACA,cAAc,MAAM;GAClB,IACEA,wBAAAA,OAAO,KAAK,MAAM,KAClB,SAAS,KAAK,QAAQ,MAAM,KAC5B,MAAM,QAAQ,KAAK,SAAS,KAC5B,KAAK,UAAU,WAAW,GAE1B,OAAO,IAAI;EACf;CACF;AACF,CACF"}
1
+ {"version":3,"file":"no-wall-clock-in-tests.cjs","names":["testOnlyPlugin","createBindingResolver","isNode","propertyName"],"sources":["../src/no-wall-clock-in-tests.ts"],"sourcesContent":["import type { Plugin } from \"@oxlint/plugins\";\n\nimport {\n createBindingResolver,\n isNode,\n propertyName,\n testOnlyPlugin,\n} from \"./test-rule-utils\";\n\nconst plugin: Plugin = testOnlyPlugin(\n \"no-wall-clock-in-tests\",\n \"noWallClock\",\n \"Use the @stll/test-kit clock instead of reading the wall clock in tests.\",\n (report, getScope) => {\n const resolveBinding = createBindingResolver(getScope);\n const isGlobal = (value: unknown, name: string): boolean => {\n if (!isNode(value)) return false;\n const binding = resolveBinding(value);\n return binding.type === \"global\" && binding.name === name;\n };\n const isClockMember = (value: unknown): boolean => {\n if (!isNode(value)) return false;\n if (value.type === \"Identifier\") {\n const binding = resolveBinding(value);\n if (binding.type === \"global\")\n return (\n binding.name === \"Date.now\" || binding.name === \"performance.now\"\n );\n return binding.type === \"const\" && isClockMember(binding.initializer);\n }\n return (\n value.type === \"MemberExpression\" &&\n propertyName(value.property) === \"now\" &&\n (isGlobal(value.object, \"Date\") ||\n isGlobal(value.object, \"performance\"))\n );\n };\n return {\n CallExpression(node) {\n if (\n isClockMember(node.callee) ||\n (isNode(node.callee) &&\n isGlobal(node.callee, \"Date\") &&\n Array.isArray(node.arguments) &&\n node.arguments.length === 0)\n )\n report(node);\n },\n NewExpression(node) {\n if (\n isNode(node.callee) &&\n isGlobal(node.callee, \"Date\") &&\n Array.isArray(node.arguments) &&\n node.arguments.length === 0\n )\n report(node);\n },\n };\n },\n);\n\nexport default plugin;\n"],"mappings":";;;;;;AASA,MAAM,SAAiBA,wBAAAA,eACrB,0BACA,eACA,6EACC,QAAQ,aAAa;CACpB,MAAM,iBAAiBC,wBAAAA,sBAAsB,QAAQ;CACrD,MAAM,YAAY,OAAgB,SAA0B;EAC1D,IAAI,CAACC,wBAAAA,OAAO,KAAK,GAAG,OAAO;EAC3B,MAAM,UAAU,eAAe,KAAK;EACpC,OAAO,QAAQ,SAAS,YAAY,QAAQ,SAAS;CACvD;CACA,MAAM,iBAAiB,UAA4B;EACjD,IAAI,CAACA,wBAAAA,OAAO,KAAK,GAAG,OAAO;EAC3B,IAAI,MAAM,SAAS,cAAc;GAC/B,MAAM,UAAU,eAAe,KAAK;GACpC,IAAI,QAAQ,SAAS,UACnB,OACE,QAAQ,SAAS,cAAc,QAAQ,SAAS;GAEpD,OAAO,QAAQ,SAAS,WAAW,cAAc,QAAQ,WAAW;EACtE;EACA,OACE,MAAM,SAAS,sBACfC,wBAAAA,aAAa,MAAM,QAAQ,MAAM,UAChC,SAAS,MAAM,QAAQ,MAAM,KAC5B,SAAS,MAAM,QAAQ,aAAa;CAE1C;CACA,OAAO;EACL,eAAe,MAAM;GACnB,IACE,cAAc,KAAK,MAAM,KACxBD,wBAAAA,OAAO,KAAK,MAAM,KACjB,SAAS,KAAK,QAAQ,MAAM,KAC5B,MAAM,QAAQ,KAAK,SAAS,KAC5B,KAAK,UAAU,WAAW,GAE5B,OAAO,IAAI;EACf;EACA,cAAc,MAAM;GAClB,IACEA,wBAAAA,OAAO,KAAK,MAAM,KAClB,SAAS,KAAK,QAAQ,MAAM,KAC5B,MAAM,QAAQ,KAAK,SAAS,KAC5B,KAAK,UAAU,WAAW,GAE1B,OAAO,IAAI;EACf;CACF;AACF,CACF"}
@@ -1,4 +1,5 @@
1
+ import { Plugin } from "@oxlint/plugins";
1
2
  //#region src/no-wall-clock-in-tests.d.ts
2
- declare const _default: import("@oxlint/plugins").Plugin;
3
- export = _default;
3
+ declare const plugin: Plugin;
4
+ export = plugin;
4
5
  //# sourceMappingURL=no-wall-clock-in-tests.d.cts.map
@@ -1,5 +1,6 @@
1
+ import { Plugin } from "@oxlint/plugins";
1
2
  //#region src/no-wall-clock-in-tests.d.ts
2
- declare const _default: import("@oxlint/plugins").Plugin;
3
+ declare const plugin: Plugin;
3
4
  //#endregion
4
- export { _default as default };
5
+ export { plugin as default };
5
6
  //# sourceMappingURL=no-wall-clock-in-tests.d.mts.map
@@ -1,6 +1,6 @@
1
1
  import { a as testOnlyPlugin, i as propertyName, r as isNode, t as createBindingResolver } from "./test-rule-utils.mjs";
2
2
  //#region src/no-wall-clock-in-tests.ts
3
- var no_wall_clock_in_tests_default = testOnlyPlugin("no-wall-clock-in-tests", "noWallClock", "Use the @stll/test-kit clock instead of reading the wall clock in tests.", (report, getScope) => {
3
+ const plugin = testOnlyPlugin("no-wall-clock-in-tests", "noWallClock", "Use the @stll/test-kit clock instead of reading the wall clock in tests.", (report, getScope) => {
4
4
  const resolveBinding = createBindingResolver(getScope);
5
5
  const isGlobal = (value, name) => {
6
6
  if (!isNode(value)) return false;
@@ -26,6 +26,6 @@ var no_wall_clock_in_tests_default = testOnlyPlugin("no-wall-clock-in-tests", "n
26
26
  };
27
27
  });
28
28
  //#endregion
29
- export { no_wall_clock_in_tests_default as default };
29
+ export { plugin as default };
30
30
 
31
31
  //# sourceMappingURL=no-wall-clock-in-tests.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"no-wall-clock-in-tests.mjs","names":[],"sources":["../src/no-wall-clock-in-tests.ts"],"sourcesContent":["import {\n createBindingResolver,\n isNode,\n propertyName,\n testOnlyPlugin,\n} from \"./test-rule-utils\";\n\nexport default testOnlyPlugin(\n \"no-wall-clock-in-tests\",\n \"noWallClock\",\n \"Use the @stll/test-kit clock instead of reading the wall clock in tests.\",\n (report, getScope) => {\n const resolveBinding = createBindingResolver(getScope);\n const isGlobal = (value: unknown, name: string): boolean => {\n if (!isNode(value)) return false;\n const binding = resolveBinding(value);\n return binding.type === \"global\" && binding.name === name;\n };\n const isClockMember = (value: unknown): boolean => {\n if (!isNode(value)) return false;\n if (value.type === \"Identifier\") {\n const binding = resolveBinding(value);\n if (binding.type === \"global\")\n return (\n binding.name === \"Date.now\" || binding.name === \"performance.now\"\n );\n return binding.type === \"const\" && isClockMember(binding.initializer);\n }\n return (\n value.type === \"MemberExpression\" &&\n propertyName(value.property) === \"now\" &&\n (isGlobal(value.object, \"Date\") ||\n isGlobal(value.object, \"performance\"))\n );\n };\n return {\n CallExpression(node) {\n if (\n isClockMember(node.callee) ||\n (isNode(node.callee) &&\n isGlobal(node.callee, \"Date\") &&\n Array.isArray(node.arguments) &&\n node.arguments.length === 0)\n )\n report(node);\n },\n NewExpression(node) {\n if (\n isNode(node.callee) &&\n isGlobal(node.callee, \"Date\") &&\n Array.isArray(node.arguments) &&\n node.arguments.length === 0\n )\n report(node);\n },\n };\n },\n);\n"],"mappings":";;AAOA,IAAA,iCAAe,eACb,0BACA,eACA,6EACC,QAAQ,aAAa;CACpB,MAAM,iBAAiB,sBAAsB,QAAQ;CACrD,MAAM,YAAY,OAAgB,SAA0B;EAC1D,IAAI,CAAC,OAAO,KAAK,GAAG,OAAO;EAC3B,MAAM,UAAU,eAAe,KAAK;EACpC,OAAO,QAAQ,SAAS,YAAY,QAAQ,SAAS;CACvD;CACA,MAAM,iBAAiB,UAA4B;EACjD,IAAI,CAAC,OAAO,KAAK,GAAG,OAAO;EAC3B,IAAI,MAAM,SAAS,cAAc;GAC/B,MAAM,UAAU,eAAe,KAAK;GACpC,IAAI,QAAQ,SAAS,UACnB,OACE,QAAQ,SAAS,cAAc,QAAQ,SAAS;GAEpD,OAAO,QAAQ,SAAS,WAAW,cAAc,QAAQ,WAAW;EACtE;EACA,OACE,MAAM,SAAS,sBACf,aAAa,MAAM,QAAQ,MAAM,UAChC,SAAS,MAAM,QAAQ,MAAM,KAC5B,SAAS,MAAM,QAAQ,aAAa;CAE1C;CACA,OAAO;EACL,eAAe,MAAM;GACnB,IACE,cAAc,KAAK,MAAM,KACxB,OAAO,KAAK,MAAM,KACjB,SAAS,KAAK,QAAQ,MAAM,KAC5B,MAAM,QAAQ,KAAK,SAAS,KAC5B,KAAK,UAAU,WAAW,GAE5B,OAAO,IAAI;EACf;EACA,cAAc,MAAM;GAClB,IACE,OAAO,KAAK,MAAM,KAClB,SAAS,KAAK,QAAQ,MAAM,KAC5B,MAAM,QAAQ,KAAK,SAAS,KAC5B,KAAK,UAAU,WAAW,GAE1B,OAAO,IAAI;EACf;CACF;AACF,CACF"}
1
+ {"version":3,"file":"no-wall-clock-in-tests.mjs","names":[],"sources":["../src/no-wall-clock-in-tests.ts"],"sourcesContent":["import type { Plugin } from \"@oxlint/plugins\";\n\nimport {\n createBindingResolver,\n isNode,\n propertyName,\n testOnlyPlugin,\n} from \"./test-rule-utils\";\n\nconst plugin: Plugin = testOnlyPlugin(\n \"no-wall-clock-in-tests\",\n \"noWallClock\",\n \"Use the @stll/test-kit clock instead of reading the wall clock in tests.\",\n (report, getScope) => {\n const resolveBinding = createBindingResolver(getScope);\n const isGlobal = (value: unknown, name: string): boolean => {\n if (!isNode(value)) return false;\n const binding = resolveBinding(value);\n return binding.type === \"global\" && binding.name === name;\n };\n const isClockMember = (value: unknown): boolean => {\n if (!isNode(value)) return false;\n if (value.type === \"Identifier\") {\n const binding = resolveBinding(value);\n if (binding.type === \"global\")\n return (\n binding.name === \"Date.now\" || binding.name === \"performance.now\"\n );\n return binding.type === \"const\" && isClockMember(binding.initializer);\n }\n return (\n value.type === \"MemberExpression\" &&\n propertyName(value.property) === \"now\" &&\n (isGlobal(value.object, \"Date\") ||\n isGlobal(value.object, \"performance\"))\n );\n };\n return {\n CallExpression(node) {\n if (\n isClockMember(node.callee) ||\n (isNode(node.callee) &&\n isGlobal(node.callee, \"Date\") &&\n Array.isArray(node.arguments) &&\n node.arguments.length === 0)\n )\n report(node);\n },\n NewExpression(node) {\n if (\n isNode(node.callee) &&\n isGlobal(node.callee, \"Date\") &&\n Array.isArray(node.arguments) &&\n node.arguments.length === 0\n )\n report(node);\n },\n };\n },\n);\n\nexport default plugin;\n"],"mappings":";;AASA,MAAM,SAAiB,eACrB,0BACA,eACA,6EACC,QAAQ,aAAa;CACpB,MAAM,iBAAiB,sBAAsB,QAAQ;CACrD,MAAM,YAAY,OAAgB,SAA0B;EAC1D,IAAI,CAAC,OAAO,KAAK,GAAG,OAAO;EAC3B,MAAM,UAAU,eAAe,KAAK;EACpC,OAAO,QAAQ,SAAS,YAAY,QAAQ,SAAS;CACvD;CACA,MAAM,iBAAiB,UAA4B;EACjD,IAAI,CAAC,OAAO,KAAK,GAAG,OAAO;EAC3B,IAAI,MAAM,SAAS,cAAc;GAC/B,MAAM,UAAU,eAAe,KAAK;GACpC,IAAI,QAAQ,SAAS,UACnB,OACE,QAAQ,SAAS,cAAc,QAAQ,SAAS;GAEpD,OAAO,QAAQ,SAAS,WAAW,cAAc,QAAQ,WAAW;EACtE;EACA,OACE,MAAM,SAAS,sBACf,aAAa,MAAM,QAAQ,MAAM,UAChC,SAAS,MAAM,QAAQ,MAAM,KAC5B,SAAS,MAAM,QAAQ,aAAa;CAE1C;CACA,OAAO;EACL,eAAe,MAAM;GACnB,IACE,cAAc,KAAK,MAAM,KACxB,OAAO,KAAK,MAAM,KACjB,SAAS,KAAK,QAAQ,MAAM,KAC5B,MAAM,QAAQ,KAAK,SAAS,KAC5B,KAAK,UAAU,WAAW,GAE5B,OAAO,IAAI;EACf;EACA,cAAc,MAAM;GAClB,IACE,OAAO,KAAK,MAAM,KAClB,SAAS,KAAK,QAAQ,MAAM,KAC5B,MAAM,QAAQ,KAAK,SAAS,KAC5B,KAAK,UAAU,WAAW,GAE1B,OAAO,IAAI;EACf;CACF;AACF,CACF"}
@@ -80,7 +80,7 @@ const suspenseFactoriesInSource = (sourceText) => {
80
80
  }
81
81
  return factories;
82
82
  };
83
- var require_loader_prefetch_default = (0, _oxlint_plugins.eslintCompatPlugin)({
83
+ const plugin = (0, _oxlint_plugins.eslintCompatPlugin)({
84
84
  meta: { name: "require-loader-prefetch" },
85
85
  rules: { "require-loader-prefetch": {
86
86
  meta: {
@@ -183,6 +183,6 @@ var require_loader_prefetch_default = (0, _oxlint_plugins.eslintCompatPlugin)({
183
183
  } }
184
184
  });
185
185
  //#endregion
186
- exports.default = require_loader_prefetch_default;
186
+ exports.default = plugin;
187
187
 
188
188
  //# sourceMappingURL=require-loader-prefetch.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"require-loader-prefetch.cjs","names":["isIdentifier","resolveLiteralArraySpreads","unwrapExpression","getPropertyName","path","readFileSync","eslintCompatPlugin","isAstNode","isStringLiteral","filenameForContext"],"sources":["../src/require-loader-prefetch.ts"],"sourcesContent":["// Require route loaders to prefetch the queries their component suspends on.\n//\n// A route component that calls `useSuspenseQuery(someOptions(...))` without the\n// route loader prefetching that same query forces a render-fetch waterfall: on\n// a cold visit the router resolves the route, the component mounts, suspends,\n// and only THEN starts the fetch — an extra sequential network round on every\n// cold navigation. TanStack Router loaders (`loader:` in `createFileRoute`\n// options) start the fetch during navigation, in parallel with code-split\n// chunk loading, via the helpers in `apps/web/src/lib/react-query.ts`\n// (`ensureRouteQueryData` for critical/blocking data, `prefetchRouteQuery` for\n// non-blocking warmup). With the loader priming the cache, `useSuspenseQuery`\n// in the component consumes a warm cache instead of opening a new round.\n//\n// Single-file AST analysis, scoped to route files (`createFileRoute(...)`):\n// - Flags `useSuspenseQuery(factory(...))` / `useSuspenseQuery(factory)` when\n// the route has no `loader`.\n// - Flags the same when a `loader` exists but never references the factory\n// identifier (any reference inside the loader body counts — typically\n// inside `ensureRouteQueryData` / `prefetchRouteQuery`).\n//\n// Cross-file, one-hop check for colocated route components: the single-file\n// analysis above is blind to a `useSuspenseQuery(factory(...))` call in an\n// imported sibling file. Dash-prefixed directories such as `-components/`,\n// `-hooks/`, and `-queries/` are treated as colocated non-route files.\n//\n// Without a module resolver, confirmed route files with inline options resolve\n// each `@/routes/.../-<dir>/...` import from the linted file's `/routes/`\n// segment. The child file is read once and scanned with a regex for the same\n// two `useSuspenseQuery(...)` shapes the AST check recognizes. Child imports\n// are not followed, and each factory is checked against the route's `loader`.\n//\n// Safe (not flagged):\n// - `useSuspenseQuery` whose argument is a member expression or other shape\n// the rule can't statically attribute to a factory identifier (in-file or\n// in a colocated import).\n// - `useSuspenseQuery` in a non-route file (no `createFileRoute`) — including\n// when that file is itself a route's colocated component and is linted\n// directly rather than through the importing route.\n// - A route whose loader references the factory identifier.\n// - A colocated import that isn't under a dash-prefixed directory, or that\n// can't be read as a sibling `.tsx`/`.ts` file (no fs read is attempted\n// unless the file is already a confirmed route file with such an import).\n//\n// Known limitation: the child-file scan is a raw-text regex, not a second AST\n// parse, so it does not strip comments or string literals. A comment or\n// string in the colocated file that happens to spell out\n// `useSuspenseQuery(identifier(` / `useSuspenseQuery(identifier)` verbatim\n// would false-positive. Judged an acceptable, rare edge case for the\n// bounded/cheap scan this rule relies on; do not write that literal pattern\n// in prose comments inside `-components`/`-hooks`/`-queries` files.\n\nimport { eslintCompatPlugin, type Ranged } from \"@oxlint/plugins\";\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\n\nimport {\n filenameForContext,\n getPropertyName,\n isAstNode,\n isIdentifier,\n isStringLiteral,\n resolveLiteralArraySpreads,\n unwrapExpression,\n} from \"./utils.ts\";\n\nconst CREATE_FILE_ROUTE = \"createFileRoute\";\nconst SUSPENSE_HOOK = \"useSuspenseQuery\";\nconst LOADER_KEY = \"loader\";\n\nconst isCreateFileRouteCall = (callee) =>\n isIdentifier(callee, CREATE_FILE_ROUTE);\n\n// `createFileRoute(\"...\")({ ... })` — the outer call whose callee is itself the\n// `createFileRoute(\"...\")` call. The first argument is the route options object.\nconst isCreateFileRouteOptionsCall = (callee) =>\n callee?.type === \"CallExpression\" && isCreateFileRouteCall(callee.callee);\n\nconst isSuspenseCallee = (callee) => {\n if (isIdentifier(callee, SUSPENSE_HOOK)) {\n return true;\n }\n return (\n callee?.type === \"MemberExpression\" &&\n callee.computed === false &&\n isIdentifier(callee.property, SUSPENSE_HOOK)\n );\n};\n\n// Resolve the factory identifier a `useSuspenseQuery` call depends on:\n// useSuspenseQuery(factory(...)) -> \"factory\"\n// useSuspenseQuery(factory) -> \"factory\"\n// Returns null for any other argument shape (member expressions, object\n// literals, etc.) so the rule never guesses.\nconst suspenseFactoryName = (node) => {\n if (!isSuspenseCallee(node.callee)) {\n return null;\n }\n const { args } = resolveLiteralArraySpreads(node.arguments ?? []);\n const arg = unwrapExpression(args.at(0));\n if (arg?.type === \"CallExpression\" && isIdentifier(arg.callee)) {\n return arg.callee.name;\n }\n if (isIdentifier(arg)) {\n return arg.name;\n }\n return null;\n};\n\nconst isInsideLoader = (node) => {\n let current = node.parent;\n while (current) {\n if (\n current.type === \"Property\" &&\n getPropertyName(current.key) === LOADER_KEY\n ) {\n return true;\n }\n current = current.parent;\n }\n return false;\n};\n\nconst ROUTES_SEGMENT = \"/routes/\";\nconst COLOCATED_DIR_RE = /\\/-[^/]+\\//;\n\n// A colocated, non-route-generating import: under `@/routes/...` and through\n// a dash-prefixed directory (`-components/`, `-hooks/`, `-queries/`, ...).\nconst isColocatedRouteImport = (source) =>\n source.startsWith(\"@/routes/\") && COLOCATED_DIR_RE.test(source);\n\n// Resolve a `@/routes/...` specifier to an absolute path by reusing the\n// currently-linted file's own path up to its `/routes/` segment as the alias\n// root — mirrors the `\"@/*\": [\"./src/*\"]` tsconfig path without a real module\n// resolver. Returns null if the linted file isn't itself under a `routes`\n// directory (shouldn't happen for a confirmed route file, but fails closed).\nconst resolveColocatedImportBase = (filename, source) => {\n // Windows gives `context.filename` with backslashes, which would make\n // `lastIndexOf(ROUTES_SEGMENT)` miss; normalize to forward slashes first\n // (valid path input on Windows, and `join` below normalizes the output).\n const normalizedFilename = filename.replaceAll(\"\\\\\", \"/\");\n const routesIndex = normalizedFilename.lastIndexOf(ROUTES_SEGMENT);\n if (routesIndex === -1) {\n return null;\n }\n const aliasRoot = normalizedFilename.slice(0, routesIndex);\n return path.join(aliasRoot, source.slice(\"@/\".length));\n};\n\n// Bounded, one-hop read: try the two extensions a colocated TS/TSX import can\n// resolve to. No further imports of the child file are followed.\nconst readColocatedFile = (basePath) => {\n for (const extension of [\".tsx\", \".ts\"]) {\n try {\n return readFileSync(`${basePath}${extension}`, \"utf-8\");\n } catch {\n // Try the next extension; if neither exists this is a shape the\n // simplified alias resolution can't handle (e.g. a directory index) —\n // acceptable since this is a best-effort bounded check, not a resolver.\n }\n }\n return null;\n};\n\n// Regex counterpart of `suspenseFactoryName`, scanning raw source text for\n// the same two argument shapes: `useSuspenseQuery(factory(...))` and\n// `useSuspenseQuery(factory)`. The character right after the identifier\n// disambiguates them from a member-expression argument the AST check would\n// also decline to attribute (e.g. `useSuspenseQuery(factory.options())`).\nconst CHILD_SUSPENSE_RE = /useSuspenseQuery\\(\\s*([A-Za-z_$][\\w$]*)\\s*([(),])/g;\n\nconst suspenseFactoriesInSource = (sourceText: string): Set<string> => {\n const factories = new Set<string>();\n for (const match of sourceText.matchAll(CHILD_SUSPENSE_RE)) {\n const factory = match.at(1);\n if (factory !== undefined) {\n factories.add(factory);\n }\n }\n return factories;\n};\n\nexport default eslintCompatPlugin({\n meta: { name: \"require-loader-prefetch\" },\n rules: {\n \"require-loader-prefetch\": {\n meta: {\n type: \"problem\",\n messages: {\n missingLoaderPrefetch:\n \"Route component calls useSuspenseQuery({{factory}}(...)) but the \" +\n \"route defines no `loader`, so this query fetches only after the \" +\n \"component mounts and suspends — an extra sequential network round \" +\n \"on cold navigation. Prefetch the same query options in the route \" +\n \"`loader` via ensureRouteQueryData (blocking, critical data) or \" +\n \"prefetchRouteQuery (non-blocking) from @/lib/react-query, so the \" +\n \"fetch starts during navigation and useSuspenseQuery consumes the \" +\n \"warm cache.\",\n factoryNotPrefetched:\n \"Route component calls useSuspenseQuery({{factory}}(...)) but the \" +\n \"route `loader` never references {{factory}}, so this query still \" +\n \"fetches on mount — an extra sequential network round on cold \" +\n \"navigation. Prefetch {{factory}}(...) in the `loader` via \" +\n \"ensureRouteQueryData (blocking, critical data) or \" +\n \"prefetchRouteQuery (non-blocking) from @/lib/react-query; \" +\n \"useSuspenseQuery in the component then consumes the warm cache.\",\n childMissingLoaderPrefetch:\n \"Route imports {{importPath}}, whose component calls \" +\n \"useSuspenseQuery({{factory}}(...)), but the route defines no \" +\n \"`loader`, so this query fetches only after the child component \" +\n \"mounts and suspends — an extra sequential network round on cold \" +\n \"navigation. Prefetch the same query options in the route \" +\n \"`loader` via ensureRouteQueryData (blocking, critical data) or \" +\n \"prefetchRouteQuery (non-blocking) from @/lib/react-query, so the \" +\n \"fetch starts during navigation and useSuspenseQuery in the child \" +\n \"component consumes the warm cache.\",\n childFactoryNotPrefetched:\n \"Route imports {{importPath}}, whose component calls \" +\n \"useSuspenseQuery({{factory}}(...)), but the route `loader` never \" +\n \"references {{factory}}, so this query still fetches on mount — \" +\n \"an extra sequential network round on cold navigation. Prefetch \" +\n \"{{factory}}(...) in the `loader` via ensureRouteQueryData \" +\n \"(blocking, critical data) or prefetchRouteQuery (non-blocking) \" +\n \"from @/lib/react-query; useSuspenseQuery in the child component \" +\n \"then consumes the warm cache.\",\n },\n },\n createOnce(context) {\n let isRouteFile = false;\n let routeOptions: { properties: unknown } | null = null;\n const suspenseCalls: { factory: string; node: Ranged }[] = [];\n const referencedInLoader = new Set();\n const colocatedImports: { node: Ranged; source: string }[] = [];\n\n return {\n before() {\n isRouteFile = false;\n routeOptions = null;\n suspenseCalls.length = 0;\n referencedInLoader.clear();\n colocatedImports.length = 0;\n },\n CallExpression(node) {\n const callee = node.callee;\n\n if (isCreateFileRouteCall(callee)) {\n isRouteFile = true;\n }\n\n if (isCreateFileRouteOptionsCall(callee)) {\n isRouteFile = true;\n const { args } = resolveLiteralArraySpreads(node.arguments);\n const optionsArg = args.at(0);\n if (\n routeOptions === null &&\n isAstNode(optionsArg) &&\n optionsArg.type === \"ObjectExpression\"\n ) {\n routeOptions = { properties: optionsArg.properties };\n }\n }\n\n const factory = suspenseFactoryName(node);\n if (factory !== null) {\n suspenseCalls.push({ node, factory });\n }\n },\n\n Identifier(node) {\n if (isInsideLoader(node)) {\n referencedInLoader.add(node.name);\n }\n },\n\n ImportDeclaration(node) {\n if (\n isStringLiteral(node.source) &&\n isColocatedRouteImport(node.source.value)\n ) {\n colocatedImports.push({ node, source: node.source.value });\n }\n },\n\n \"Program:exit\"() {\n // Only route files with an inline options object are analyzable;\n // a non-inline options argument can't be statically inspected.\n if (!isRouteFile || routeOptions === null) {\n return;\n }\n\n const properties = Array.isArray(routeOptions.properties)\n ? routeOptions.properties\n : [];\n const hasLoader = properties.some(\n (prop) =>\n prop.type === \"Property\" &&\n getPropertyName(prop.key) === LOADER_KEY,\n );\n\n for (const { node, factory } of suspenseCalls) {\n if (!hasLoader) {\n context.report({\n node,\n messageId: \"missingLoaderPrefetch\",\n data: { factory },\n });\n continue;\n }\n\n if (!referencedInLoader.has(factory)) {\n context.report({\n node,\n messageId: \"factoryNotPrefetched\",\n data: { factory },\n });\n }\n }\n\n if (colocatedImports.length === 0) {\n return;\n }\n\n const filename = filenameForContext(context);\n\n for (const { node, source } of colocatedImports) {\n const basePath = resolveColocatedImportBase(filename, source);\n if (basePath === null) {\n continue;\n }\n\n const childSource = readColocatedFile(basePath);\n if (childSource === null) {\n continue;\n }\n\n for (const factory of suspenseFactoriesInSource(childSource)) {\n if (!hasLoader) {\n context.report({\n node,\n messageId: \"childMissingLoaderPrefetch\",\n data: { factory, importPath: source },\n });\n continue;\n }\n\n if (!referencedInLoader.has(factory)) {\n context.report({\n node,\n messageId: \"childFactoryNotPrefetched\",\n data: { factory, importPath: source },\n });\n }\n }\n }\n },\n };\n },\n },\n },\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"require-loader-prefetch.cjs","names":["isIdentifier","resolveLiteralArraySpreads","unwrapExpression","getPropertyName","path","readFileSync","eslintCompatPlugin","isAstNode","isStringLiteral","filenameForContext"],"sources":["../src/require-loader-prefetch.ts"],"sourcesContent":["// Require route loaders to prefetch the queries their component suspends on.\n//\n// A route component that calls `useSuspenseQuery(someOptions(...))` without the\n// route loader prefetching that same query forces a render-fetch waterfall: on\n// a cold visit the router resolves the route, the component mounts, suspends,\n// and only THEN starts the fetch — an extra sequential network round on every\n// cold navigation. TanStack Router loaders (`loader:` in `createFileRoute`\n// options) start the fetch during navigation, in parallel with code-split\n// chunk loading, via the helpers in `apps/web/src/lib/react-query.ts`\n// (`ensureRouteQueryData` for critical/blocking data, `prefetchRouteQuery` for\n// non-blocking warmup). With the loader priming the cache, `useSuspenseQuery`\n// in the component consumes a warm cache instead of opening a new round.\n//\n// Single-file AST analysis, scoped to route files (`createFileRoute(...)`):\n// - Flags `useSuspenseQuery(factory(...))` / `useSuspenseQuery(factory)` when\n// the route has no `loader`.\n// - Flags the same when a `loader` exists but never references the factory\n// identifier (any reference inside the loader body counts — typically\n// inside `ensureRouteQueryData` / `prefetchRouteQuery`).\n//\n// Cross-file, one-hop check for colocated route components: the single-file\n// analysis above is blind to a `useSuspenseQuery(factory(...))` call in an\n// imported sibling file. Dash-prefixed directories such as `-components/`,\n// `-hooks/`, and `-queries/` are treated as colocated non-route files.\n//\n// Without a module resolver, confirmed route files with inline options resolve\n// each `@/routes/.../-<dir>/...` import from the linted file's `/routes/`\n// segment. The child file is read once and scanned with a regex for the same\n// two `useSuspenseQuery(...)` shapes the AST check recognizes. Child imports\n// are not followed, and each factory is checked against the route's `loader`.\n//\n// Safe (not flagged):\n// - `useSuspenseQuery` whose argument is a member expression or other shape\n// the rule can't statically attribute to a factory identifier (in-file or\n// in a colocated import).\n// - `useSuspenseQuery` in a non-route file (no `createFileRoute`) — including\n// when that file is itself a route's colocated component and is linted\n// directly rather than through the importing route.\n// - A route whose loader references the factory identifier.\n// - A colocated import that isn't under a dash-prefixed directory, or that\n// can't be read as a sibling `.tsx`/`.ts` file (no fs read is attempted\n// unless the file is already a confirmed route file with such an import).\n//\n// Known limitation: the child-file scan is a raw-text regex, not a second AST\n// parse, so it does not strip comments or string literals. A comment or\n// string in the colocated file that happens to spell out\n// `useSuspenseQuery(identifier(` / `useSuspenseQuery(identifier)` verbatim\n// would false-positive. Judged an acceptable, rare edge case for the\n// bounded/cheap scan this rule relies on; do not write that literal pattern\n// in prose comments inside `-components`/`-hooks`/`-queries` files.\n\nimport { eslintCompatPlugin, type Plugin, type Ranged } from \"@oxlint/plugins\";\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\n\nimport {\n filenameForContext,\n getPropertyName,\n isAstNode,\n isIdentifier,\n isStringLiteral,\n resolveLiteralArraySpreads,\n unwrapExpression,\n} from \"./utils.ts\";\n\nconst CREATE_FILE_ROUTE = \"createFileRoute\";\nconst SUSPENSE_HOOK = \"useSuspenseQuery\";\nconst LOADER_KEY = \"loader\";\n\nconst isCreateFileRouteCall = (callee) =>\n isIdentifier(callee, CREATE_FILE_ROUTE);\n\n// `createFileRoute(\"...\")({ ... })` — the outer call whose callee is itself the\n// `createFileRoute(\"...\")` call. The first argument is the route options object.\nconst isCreateFileRouteOptionsCall = (callee) =>\n callee?.type === \"CallExpression\" && isCreateFileRouteCall(callee.callee);\n\nconst isSuspenseCallee = (callee) => {\n if (isIdentifier(callee, SUSPENSE_HOOK)) {\n return true;\n }\n return (\n callee?.type === \"MemberExpression\" &&\n callee.computed === false &&\n isIdentifier(callee.property, SUSPENSE_HOOK)\n );\n};\n\n// Resolve the factory identifier a `useSuspenseQuery` call depends on:\n// useSuspenseQuery(factory(...)) -> \"factory\"\n// useSuspenseQuery(factory) -> \"factory\"\n// Returns null for any other argument shape (member expressions, object\n// literals, etc.) so the rule never guesses.\nconst suspenseFactoryName = (node) => {\n if (!isSuspenseCallee(node.callee)) {\n return null;\n }\n const { args } = resolveLiteralArraySpreads(node.arguments ?? []);\n const arg = unwrapExpression(args.at(0));\n if (arg?.type === \"CallExpression\" && isIdentifier(arg.callee)) {\n return arg.callee.name;\n }\n if (isIdentifier(arg)) {\n return arg.name;\n }\n return null;\n};\n\nconst isInsideLoader = (node) => {\n let current = node.parent;\n while (current) {\n if (\n current.type === \"Property\" &&\n getPropertyName(current.key) === LOADER_KEY\n ) {\n return true;\n }\n current = current.parent;\n }\n return false;\n};\n\nconst ROUTES_SEGMENT = \"/routes/\";\nconst COLOCATED_DIR_RE = /\\/-[^/]+\\//;\n\n// A colocated, non-route-generating import: under `@/routes/...` and through\n// a dash-prefixed directory (`-components/`, `-hooks/`, `-queries/`, ...).\nconst isColocatedRouteImport = (source) =>\n source.startsWith(\"@/routes/\") && COLOCATED_DIR_RE.test(source);\n\n// Resolve a `@/routes/...` specifier to an absolute path by reusing the\n// currently-linted file's own path up to its `/routes/` segment as the alias\n// root — mirrors the `\"@/*\": [\"./src/*\"]` tsconfig path without a real module\n// resolver. Returns null if the linted file isn't itself under a `routes`\n// directory (shouldn't happen for a confirmed route file, but fails closed).\nconst resolveColocatedImportBase = (filename, source) => {\n // Windows gives `context.filename` with backslashes, which would make\n // `lastIndexOf(ROUTES_SEGMENT)` miss; normalize to forward slashes first\n // (valid path input on Windows, and `join` below normalizes the output).\n const normalizedFilename = filename.replaceAll(\"\\\\\", \"/\");\n const routesIndex = normalizedFilename.lastIndexOf(ROUTES_SEGMENT);\n if (routesIndex === -1) {\n return null;\n }\n const aliasRoot = normalizedFilename.slice(0, routesIndex);\n return path.join(aliasRoot, source.slice(\"@/\".length));\n};\n\n// Bounded, one-hop read: try the two extensions a colocated TS/TSX import can\n// resolve to. No further imports of the child file are followed.\nconst readColocatedFile = (basePath) => {\n for (const extension of [\".tsx\", \".ts\"]) {\n try {\n return readFileSync(`${basePath}${extension}`, \"utf-8\");\n } catch {\n // Try the next extension; if neither exists this is a shape the\n // simplified alias resolution can't handle (e.g. a directory index) —\n // acceptable since this is a best-effort bounded check, not a resolver.\n }\n }\n return null;\n};\n\n// Regex counterpart of `suspenseFactoryName`, scanning raw source text for\n// the same two argument shapes: `useSuspenseQuery(factory(...))` and\n// `useSuspenseQuery(factory)`. The character right after the identifier\n// disambiguates them from a member-expression argument the AST check would\n// also decline to attribute (e.g. `useSuspenseQuery(factory.options())`).\nconst CHILD_SUSPENSE_RE = /useSuspenseQuery\\(\\s*([A-Za-z_$][\\w$]*)\\s*([(),])/g;\n\nconst suspenseFactoriesInSource = (sourceText: string): Set<string> => {\n const factories = new Set<string>();\n for (const match of sourceText.matchAll(CHILD_SUSPENSE_RE)) {\n const factory = match.at(1);\n if (factory !== undefined) {\n factories.add(factory);\n }\n }\n return factories;\n};\n\nconst plugin: Plugin = eslintCompatPlugin({\n meta: { name: \"require-loader-prefetch\" },\n rules: {\n \"require-loader-prefetch\": {\n meta: {\n type: \"problem\",\n messages: {\n missingLoaderPrefetch:\n \"Route component calls useSuspenseQuery({{factory}}(...)) but the \" +\n \"route defines no `loader`, so this query fetches only after the \" +\n \"component mounts and suspends — an extra sequential network round \" +\n \"on cold navigation. Prefetch the same query options in the route \" +\n \"`loader` via ensureRouteQueryData (blocking, critical data) or \" +\n \"prefetchRouteQuery (non-blocking) from @/lib/react-query, so the \" +\n \"fetch starts during navigation and useSuspenseQuery consumes the \" +\n \"warm cache.\",\n factoryNotPrefetched:\n \"Route component calls useSuspenseQuery({{factory}}(...)) but the \" +\n \"route `loader` never references {{factory}}, so this query still \" +\n \"fetches on mount — an extra sequential network round on cold \" +\n \"navigation. Prefetch {{factory}}(...) in the `loader` via \" +\n \"ensureRouteQueryData (blocking, critical data) or \" +\n \"prefetchRouteQuery (non-blocking) from @/lib/react-query; \" +\n \"useSuspenseQuery in the component then consumes the warm cache.\",\n childMissingLoaderPrefetch:\n \"Route imports {{importPath}}, whose component calls \" +\n \"useSuspenseQuery({{factory}}(...)), but the route defines no \" +\n \"`loader`, so this query fetches only after the child component \" +\n \"mounts and suspends — an extra sequential network round on cold \" +\n \"navigation. Prefetch the same query options in the route \" +\n \"`loader` via ensureRouteQueryData (blocking, critical data) or \" +\n \"prefetchRouteQuery (non-blocking) from @/lib/react-query, so the \" +\n \"fetch starts during navigation and useSuspenseQuery in the child \" +\n \"component consumes the warm cache.\",\n childFactoryNotPrefetched:\n \"Route imports {{importPath}}, whose component calls \" +\n \"useSuspenseQuery({{factory}}(...)), but the route `loader` never \" +\n \"references {{factory}}, so this query still fetches on mount — \" +\n \"an extra sequential network round on cold navigation. Prefetch \" +\n \"{{factory}}(...) in the `loader` via ensureRouteQueryData \" +\n \"(blocking, critical data) or prefetchRouteQuery (non-blocking) \" +\n \"from @/lib/react-query; useSuspenseQuery in the child component \" +\n \"then consumes the warm cache.\",\n },\n },\n createOnce(context) {\n let isRouteFile = false;\n let routeOptions: { properties: unknown } | null = null;\n const suspenseCalls: { factory: string; node: Ranged }[] = [];\n const referencedInLoader = new Set();\n const colocatedImports: { node: Ranged; source: string }[] = [];\n\n return {\n before() {\n isRouteFile = false;\n routeOptions = null;\n suspenseCalls.length = 0;\n referencedInLoader.clear();\n colocatedImports.length = 0;\n },\n CallExpression(node) {\n const callee = node.callee;\n\n if (isCreateFileRouteCall(callee)) {\n isRouteFile = true;\n }\n\n if (isCreateFileRouteOptionsCall(callee)) {\n isRouteFile = true;\n const { args } = resolveLiteralArraySpreads(node.arguments);\n const optionsArg = args.at(0);\n if (\n routeOptions === null &&\n isAstNode(optionsArg) &&\n optionsArg.type === \"ObjectExpression\"\n ) {\n routeOptions = { properties: optionsArg.properties };\n }\n }\n\n const factory = suspenseFactoryName(node);\n if (factory !== null) {\n suspenseCalls.push({ node, factory });\n }\n },\n\n Identifier(node) {\n if (isInsideLoader(node)) {\n referencedInLoader.add(node.name);\n }\n },\n\n ImportDeclaration(node) {\n if (\n isStringLiteral(node.source) &&\n isColocatedRouteImport(node.source.value)\n ) {\n colocatedImports.push({ node, source: node.source.value });\n }\n },\n\n \"Program:exit\"() {\n // Only route files with an inline options object are analyzable;\n // a non-inline options argument can't be statically inspected.\n if (!isRouteFile || routeOptions === null) {\n return;\n }\n\n const properties = Array.isArray(routeOptions.properties)\n ? routeOptions.properties\n : [];\n const hasLoader = properties.some(\n (prop) =>\n prop.type === \"Property\" &&\n getPropertyName(prop.key) === LOADER_KEY,\n );\n\n for (const { node, factory } of suspenseCalls) {\n if (!hasLoader) {\n context.report({\n node,\n messageId: \"missingLoaderPrefetch\",\n data: { factory },\n });\n continue;\n }\n\n if (!referencedInLoader.has(factory)) {\n context.report({\n node,\n messageId: \"factoryNotPrefetched\",\n data: { factory },\n });\n }\n }\n\n if (colocatedImports.length === 0) {\n return;\n }\n\n const filename = filenameForContext(context);\n\n for (const { node, source } of colocatedImports) {\n const basePath = resolveColocatedImportBase(filename, source);\n if (basePath === null) {\n continue;\n }\n\n const childSource = readColocatedFile(basePath);\n if (childSource === null) {\n continue;\n }\n\n for (const factory of suspenseFactoriesInSource(childSource)) {\n if (!hasLoader) {\n context.report({\n node,\n messageId: \"childMissingLoaderPrefetch\",\n data: { factory, importPath: source },\n });\n continue;\n }\n\n if (!referencedInLoader.has(factory)) {\n context.report({\n node,\n messageId: \"childFactoryNotPrefetched\",\n data: { factory, importPath: source },\n });\n }\n }\n }\n },\n };\n },\n },\n },\n});\n\nexport default plugin;\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
+ import { Plugin } from "@oxlint/plugins";
1
2
  //#region src/require-loader-prefetch.d.ts
2
- declare const _default: import("@oxlint/plugins").Plugin;
3
- export = _default;
3
+ declare const plugin: Plugin;
4
+ export = plugin;
4
5
  //# sourceMappingURL=require-loader-prefetch.d.cts.map
@@ -1,5 +1,6 @@
1
+ import { Plugin } from "@oxlint/plugins";
1
2
  //#region src/require-loader-prefetch.d.ts
2
- declare const _default: import("@oxlint/plugins").Plugin;
3
+ declare const plugin: Plugin;
3
4
  //#endregion
4
- export { _default as default };
5
+ export { plugin as default };
5
6
  //# sourceMappingURL=require-loader-prefetch.d.mts.map
@@ -53,7 +53,7 @@ const suspenseFactoriesInSource = (sourceText) => {
53
53
  }
54
54
  return factories;
55
55
  };
56
- var require_loader_prefetch_default = eslintCompatPlugin({
56
+ const plugin = eslintCompatPlugin({
57
57
  meta: { name: "require-loader-prefetch" },
58
58
  rules: { "require-loader-prefetch": {
59
59
  meta: {
@@ -156,6 +156,6 @@ var require_loader_prefetch_default = eslintCompatPlugin({
156
156
  } }
157
157
  });
158
158
  //#endregion
159
- export { require_loader_prefetch_default as default };
159
+ export { plugin as default };
160
160
 
161
161
  //# sourceMappingURL=require-loader-prefetch.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"require-loader-prefetch.mjs","names":[],"sources":["../src/require-loader-prefetch.ts"],"sourcesContent":["// Require route loaders to prefetch the queries their component suspends on.\n//\n// A route component that calls `useSuspenseQuery(someOptions(...))` without the\n// route loader prefetching that same query forces a render-fetch waterfall: on\n// a cold visit the router resolves the route, the component mounts, suspends,\n// and only THEN starts the fetch — an extra sequential network round on every\n// cold navigation. TanStack Router loaders (`loader:` in `createFileRoute`\n// options) start the fetch during navigation, in parallel with code-split\n// chunk loading, via the helpers in `apps/web/src/lib/react-query.ts`\n// (`ensureRouteQueryData` for critical/blocking data, `prefetchRouteQuery` for\n// non-blocking warmup). With the loader priming the cache, `useSuspenseQuery`\n// in the component consumes a warm cache instead of opening a new round.\n//\n// Single-file AST analysis, scoped to route files (`createFileRoute(...)`):\n// - Flags `useSuspenseQuery(factory(...))` / `useSuspenseQuery(factory)` when\n// the route has no `loader`.\n// - Flags the same when a `loader` exists but never references the factory\n// identifier (any reference inside the loader body counts — typically\n// inside `ensureRouteQueryData` / `prefetchRouteQuery`).\n//\n// Cross-file, one-hop check for colocated route components: the single-file\n// analysis above is blind to a `useSuspenseQuery(factory(...))` call in an\n// imported sibling file. Dash-prefixed directories such as `-components/`,\n// `-hooks/`, and `-queries/` are treated as colocated non-route files.\n//\n// Without a module resolver, confirmed route files with inline options resolve\n// each `@/routes/.../-<dir>/...` import from the linted file's `/routes/`\n// segment. The child file is read once and scanned with a regex for the same\n// two `useSuspenseQuery(...)` shapes the AST check recognizes. Child imports\n// are not followed, and each factory is checked against the route's `loader`.\n//\n// Safe (not flagged):\n// - `useSuspenseQuery` whose argument is a member expression or other shape\n// the rule can't statically attribute to a factory identifier (in-file or\n// in a colocated import).\n// - `useSuspenseQuery` in a non-route file (no `createFileRoute`) — including\n// when that file is itself a route's colocated component and is linted\n// directly rather than through the importing route.\n// - A route whose loader references the factory identifier.\n// - A colocated import that isn't under a dash-prefixed directory, or that\n// can't be read as a sibling `.tsx`/`.ts` file (no fs read is attempted\n// unless the file is already a confirmed route file with such an import).\n//\n// Known limitation: the child-file scan is a raw-text regex, not a second AST\n// parse, so it does not strip comments or string literals. A comment or\n// string in the colocated file that happens to spell out\n// `useSuspenseQuery(identifier(` / `useSuspenseQuery(identifier)` verbatim\n// would false-positive. Judged an acceptable, rare edge case for the\n// bounded/cheap scan this rule relies on; do not write that literal pattern\n// in prose comments inside `-components`/`-hooks`/`-queries` files.\n\nimport { eslintCompatPlugin, type Ranged } from \"@oxlint/plugins\";\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\n\nimport {\n filenameForContext,\n getPropertyName,\n isAstNode,\n isIdentifier,\n isStringLiteral,\n resolveLiteralArraySpreads,\n unwrapExpression,\n} from \"./utils.ts\";\n\nconst CREATE_FILE_ROUTE = \"createFileRoute\";\nconst SUSPENSE_HOOK = \"useSuspenseQuery\";\nconst LOADER_KEY = \"loader\";\n\nconst isCreateFileRouteCall = (callee) =>\n isIdentifier(callee, CREATE_FILE_ROUTE);\n\n// `createFileRoute(\"...\")({ ... })` — the outer call whose callee is itself the\n// `createFileRoute(\"...\")` call. The first argument is the route options object.\nconst isCreateFileRouteOptionsCall = (callee) =>\n callee?.type === \"CallExpression\" && isCreateFileRouteCall(callee.callee);\n\nconst isSuspenseCallee = (callee) => {\n if (isIdentifier(callee, SUSPENSE_HOOK)) {\n return true;\n }\n return (\n callee?.type === \"MemberExpression\" &&\n callee.computed === false &&\n isIdentifier(callee.property, SUSPENSE_HOOK)\n );\n};\n\n// Resolve the factory identifier a `useSuspenseQuery` call depends on:\n// useSuspenseQuery(factory(...)) -> \"factory\"\n// useSuspenseQuery(factory) -> \"factory\"\n// Returns null for any other argument shape (member expressions, object\n// literals, etc.) so the rule never guesses.\nconst suspenseFactoryName = (node) => {\n if (!isSuspenseCallee(node.callee)) {\n return null;\n }\n const { args } = resolveLiteralArraySpreads(node.arguments ?? []);\n const arg = unwrapExpression(args.at(0));\n if (arg?.type === \"CallExpression\" && isIdentifier(arg.callee)) {\n return arg.callee.name;\n }\n if (isIdentifier(arg)) {\n return arg.name;\n }\n return null;\n};\n\nconst isInsideLoader = (node) => {\n let current = node.parent;\n while (current) {\n if (\n current.type === \"Property\" &&\n getPropertyName(current.key) === LOADER_KEY\n ) {\n return true;\n }\n current = current.parent;\n }\n return false;\n};\n\nconst ROUTES_SEGMENT = \"/routes/\";\nconst COLOCATED_DIR_RE = /\\/-[^/]+\\//;\n\n// A colocated, non-route-generating import: under `@/routes/...` and through\n// a dash-prefixed directory (`-components/`, `-hooks/`, `-queries/`, ...).\nconst isColocatedRouteImport = (source) =>\n source.startsWith(\"@/routes/\") && COLOCATED_DIR_RE.test(source);\n\n// Resolve a `@/routes/...` specifier to an absolute path by reusing the\n// currently-linted file's own path up to its `/routes/` segment as the alias\n// root — mirrors the `\"@/*\": [\"./src/*\"]` tsconfig path without a real module\n// resolver. Returns null if the linted file isn't itself under a `routes`\n// directory (shouldn't happen for a confirmed route file, but fails closed).\nconst resolveColocatedImportBase = (filename, source) => {\n // Windows gives `context.filename` with backslashes, which would make\n // `lastIndexOf(ROUTES_SEGMENT)` miss; normalize to forward slashes first\n // (valid path input on Windows, and `join` below normalizes the output).\n const normalizedFilename = filename.replaceAll(\"\\\\\", \"/\");\n const routesIndex = normalizedFilename.lastIndexOf(ROUTES_SEGMENT);\n if (routesIndex === -1) {\n return null;\n }\n const aliasRoot = normalizedFilename.slice(0, routesIndex);\n return path.join(aliasRoot, source.slice(\"@/\".length));\n};\n\n// Bounded, one-hop read: try the two extensions a colocated TS/TSX import can\n// resolve to. No further imports of the child file are followed.\nconst readColocatedFile = (basePath) => {\n for (const extension of [\".tsx\", \".ts\"]) {\n try {\n return readFileSync(`${basePath}${extension}`, \"utf-8\");\n } catch {\n // Try the next extension; if neither exists this is a shape the\n // simplified alias resolution can't handle (e.g. a directory index) —\n // acceptable since this is a best-effort bounded check, not a resolver.\n }\n }\n return null;\n};\n\n// Regex counterpart of `suspenseFactoryName`, scanning raw source text for\n// the same two argument shapes: `useSuspenseQuery(factory(...))` and\n// `useSuspenseQuery(factory)`. The character right after the identifier\n// disambiguates them from a member-expression argument the AST check would\n// also decline to attribute (e.g. `useSuspenseQuery(factory.options())`).\nconst CHILD_SUSPENSE_RE = /useSuspenseQuery\\(\\s*([A-Za-z_$][\\w$]*)\\s*([(),])/g;\n\nconst suspenseFactoriesInSource = (sourceText: string): Set<string> => {\n const factories = new Set<string>();\n for (const match of sourceText.matchAll(CHILD_SUSPENSE_RE)) {\n const factory = match.at(1);\n if (factory !== undefined) {\n factories.add(factory);\n }\n }\n return factories;\n};\n\nexport default eslintCompatPlugin({\n meta: { name: \"require-loader-prefetch\" },\n rules: {\n \"require-loader-prefetch\": {\n meta: {\n type: \"problem\",\n messages: {\n missingLoaderPrefetch:\n \"Route component calls useSuspenseQuery({{factory}}(...)) but the \" +\n \"route defines no `loader`, so this query fetches only after the \" +\n \"component mounts and suspends — an extra sequential network round \" +\n \"on cold navigation. Prefetch the same query options in the route \" +\n \"`loader` via ensureRouteQueryData (blocking, critical data) or \" +\n \"prefetchRouteQuery (non-blocking) from @/lib/react-query, so the \" +\n \"fetch starts during navigation and useSuspenseQuery consumes the \" +\n \"warm cache.\",\n factoryNotPrefetched:\n \"Route component calls useSuspenseQuery({{factory}}(...)) but the \" +\n \"route `loader` never references {{factory}}, so this query still \" +\n \"fetches on mount — an extra sequential network round on cold \" +\n \"navigation. Prefetch {{factory}}(...) in the `loader` via \" +\n \"ensureRouteQueryData (blocking, critical data) or \" +\n \"prefetchRouteQuery (non-blocking) from @/lib/react-query; \" +\n \"useSuspenseQuery in the component then consumes the warm cache.\",\n childMissingLoaderPrefetch:\n \"Route imports {{importPath}}, whose component calls \" +\n \"useSuspenseQuery({{factory}}(...)), but the route defines no \" +\n \"`loader`, so this query fetches only after the child component \" +\n \"mounts and suspends — an extra sequential network round on cold \" +\n \"navigation. Prefetch the same query options in the route \" +\n \"`loader` via ensureRouteQueryData (blocking, critical data) or \" +\n \"prefetchRouteQuery (non-blocking) from @/lib/react-query, so the \" +\n \"fetch starts during navigation and useSuspenseQuery in the child \" +\n \"component consumes the warm cache.\",\n childFactoryNotPrefetched:\n \"Route imports {{importPath}}, whose component calls \" +\n \"useSuspenseQuery({{factory}}(...)), but the route `loader` never \" +\n \"references {{factory}}, so this query still fetches on mount — \" +\n \"an extra sequential network round on cold navigation. Prefetch \" +\n \"{{factory}}(...) in the `loader` via ensureRouteQueryData \" +\n \"(blocking, critical data) or prefetchRouteQuery (non-blocking) \" +\n \"from @/lib/react-query; useSuspenseQuery in the child component \" +\n \"then consumes the warm cache.\",\n },\n },\n createOnce(context) {\n let isRouteFile = false;\n let routeOptions: { properties: unknown } | null = null;\n const suspenseCalls: { factory: string; node: Ranged }[] = [];\n const referencedInLoader = new Set();\n const colocatedImports: { node: Ranged; source: string }[] = [];\n\n return {\n before() {\n isRouteFile = false;\n routeOptions = null;\n suspenseCalls.length = 0;\n referencedInLoader.clear();\n colocatedImports.length = 0;\n },\n CallExpression(node) {\n const callee = node.callee;\n\n if (isCreateFileRouteCall(callee)) {\n isRouteFile = true;\n }\n\n if (isCreateFileRouteOptionsCall(callee)) {\n isRouteFile = true;\n const { args } = resolveLiteralArraySpreads(node.arguments);\n const optionsArg = args.at(0);\n if (\n routeOptions === null &&\n isAstNode(optionsArg) &&\n optionsArg.type === \"ObjectExpression\"\n ) {\n routeOptions = { properties: optionsArg.properties };\n }\n }\n\n const factory = suspenseFactoryName(node);\n if (factory !== null) {\n suspenseCalls.push({ node, factory });\n }\n },\n\n Identifier(node) {\n if (isInsideLoader(node)) {\n referencedInLoader.add(node.name);\n }\n },\n\n ImportDeclaration(node) {\n if (\n isStringLiteral(node.source) &&\n isColocatedRouteImport(node.source.value)\n ) {\n colocatedImports.push({ node, source: node.source.value });\n }\n },\n\n \"Program:exit\"() {\n // Only route files with an inline options object are analyzable;\n // a non-inline options argument can't be statically inspected.\n if (!isRouteFile || routeOptions === null) {\n return;\n }\n\n const properties = Array.isArray(routeOptions.properties)\n ? routeOptions.properties\n : [];\n const hasLoader = properties.some(\n (prop) =>\n prop.type === \"Property\" &&\n getPropertyName(prop.key) === LOADER_KEY,\n );\n\n for (const { node, factory } of suspenseCalls) {\n if (!hasLoader) {\n context.report({\n node,\n messageId: \"missingLoaderPrefetch\",\n data: { factory },\n });\n continue;\n }\n\n if (!referencedInLoader.has(factory)) {\n context.report({\n node,\n messageId: \"factoryNotPrefetched\",\n data: { factory },\n });\n }\n }\n\n if (colocatedImports.length === 0) {\n return;\n }\n\n const filename = filenameForContext(context);\n\n for (const { node, source } of colocatedImports) {\n const basePath = resolveColocatedImportBase(filename, source);\n if (basePath === null) {\n continue;\n }\n\n const childSource = readColocatedFile(basePath);\n if (childSource === null) {\n continue;\n }\n\n for (const factory of suspenseFactoriesInSource(childSource)) {\n if (!hasLoader) {\n context.report({\n node,\n messageId: \"childMissingLoaderPrefetch\",\n data: { factory, importPath: source },\n });\n continue;\n }\n\n if (!referencedInLoader.has(factory)) {\n context.report({\n node,\n messageId: \"childFactoryNotPrefetched\",\n data: { factory, importPath: source },\n });\n }\n }\n }\n },\n };\n },\n },\n },\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"require-loader-prefetch.mjs","names":[],"sources":["../src/require-loader-prefetch.ts"],"sourcesContent":["// Require route loaders to prefetch the queries their component suspends on.\n//\n// A route component that calls `useSuspenseQuery(someOptions(...))` without the\n// route loader prefetching that same query forces a render-fetch waterfall: on\n// a cold visit the router resolves the route, the component mounts, suspends,\n// and only THEN starts the fetch — an extra sequential network round on every\n// cold navigation. TanStack Router loaders (`loader:` in `createFileRoute`\n// options) start the fetch during navigation, in parallel with code-split\n// chunk loading, via the helpers in `apps/web/src/lib/react-query.ts`\n// (`ensureRouteQueryData` for critical/blocking data, `prefetchRouteQuery` for\n// non-blocking warmup). With the loader priming the cache, `useSuspenseQuery`\n// in the component consumes a warm cache instead of opening a new round.\n//\n// Single-file AST analysis, scoped to route files (`createFileRoute(...)`):\n// - Flags `useSuspenseQuery(factory(...))` / `useSuspenseQuery(factory)` when\n// the route has no `loader`.\n// - Flags the same when a `loader` exists but never references the factory\n// identifier (any reference inside the loader body counts — typically\n// inside `ensureRouteQueryData` / `prefetchRouteQuery`).\n//\n// Cross-file, one-hop check for colocated route components: the single-file\n// analysis above is blind to a `useSuspenseQuery(factory(...))` call in an\n// imported sibling file. Dash-prefixed directories such as `-components/`,\n// `-hooks/`, and `-queries/` are treated as colocated non-route files.\n//\n// Without a module resolver, confirmed route files with inline options resolve\n// each `@/routes/.../-<dir>/...` import from the linted file's `/routes/`\n// segment. The child file is read once and scanned with a regex for the same\n// two `useSuspenseQuery(...)` shapes the AST check recognizes. Child imports\n// are not followed, and each factory is checked against the route's `loader`.\n//\n// Safe (not flagged):\n// - `useSuspenseQuery` whose argument is a member expression or other shape\n// the rule can't statically attribute to a factory identifier (in-file or\n// in a colocated import).\n// - `useSuspenseQuery` in a non-route file (no `createFileRoute`) — including\n// when that file is itself a route's colocated component and is linted\n// directly rather than through the importing route.\n// - A route whose loader references the factory identifier.\n// - A colocated import that isn't under a dash-prefixed directory, or that\n// can't be read as a sibling `.tsx`/`.ts` file (no fs read is attempted\n// unless the file is already a confirmed route file with such an import).\n//\n// Known limitation: the child-file scan is a raw-text regex, not a second AST\n// parse, so it does not strip comments or string literals. A comment or\n// string in the colocated file that happens to spell out\n// `useSuspenseQuery(identifier(` / `useSuspenseQuery(identifier)` verbatim\n// would false-positive. Judged an acceptable, rare edge case for the\n// bounded/cheap scan this rule relies on; do not write that literal pattern\n// in prose comments inside `-components`/`-hooks`/`-queries` files.\n\nimport { eslintCompatPlugin, type Plugin, type Ranged } from \"@oxlint/plugins\";\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\n\nimport {\n filenameForContext,\n getPropertyName,\n isAstNode,\n isIdentifier,\n isStringLiteral,\n resolveLiteralArraySpreads,\n unwrapExpression,\n} from \"./utils.ts\";\n\nconst CREATE_FILE_ROUTE = \"createFileRoute\";\nconst SUSPENSE_HOOK = \"useSuspenseQuery\";\nconst LOADER_KEY = \"loader\";\n\nconst isCreateFileRouteCall = (callee) =>\n isIdentifier(callee, CREATE_FILE_ROUTE);\n\n// `createFileRoute(\"...\")({ ... })` — the outer call whose callee is itself the\n// `createFileRoute(\"...\")` call. The first argument is the route options object.\nconst isCreateFileRouteOptionsCall = (callee) =>\n callee?.type === \"CallExpression\" && isCreateFileRouteCall(callee.callee);\n\nconst isSuspenseCallee = (callee) => {\n if (isIdentifier(callee, SUSPENSE_HOOK)) {\n return true;\n }\n return (\n callee?.type === \"MemberExpression\" &&\n callee.computed === false &&\n isIdentifier(callee.property, SUSPENSE_HOOK)\n );\n};\n\n// Resolve the factory identifier a `useSuspenseQuery` call depends on:\n// useSuspenseQuery(factory(...)) -> \"factory\"\n// useSuspenseQuery(factory) -> \"factory\"\n// Returns null for any other argument shape (member expressions, object\n// literals, etc.) so the rule never guesses.\nconst suspenseFactoryName = (node) => {\n if (!isSuspenseCallee(node.callee)) {\n return null;\n }\n const { args } = resolveLiteralArraySpreads(node.arguments ?? []);\n const arg = unwrapExpression(args.at(0));\n if (arg?.type === \"CallExpression\" && isIdentifier(arg.callee)) {\n return arg.callee.name;\n }\n if (isIdentifier(arg)) {\n return arg.name;\n }\n return null;\n};\n\nconst isInsideLoader = (node) => {\n let current = node.parent;\n while (current) {\n if (\n current.type === \"Property\" &&\n getPropertyName(current.key) === LOADER_KEY\n ) {\n return true;\n }\n current = current.parent;\n }\n return false;\n};\n\nconst ROUTES_SEGMENT = \"/routes/\";\nconst COLOCATED_DIR_RE = /\\/-[^/]+\\//;\n\n// A colocated, non-route-generating import: under `@/routes/...` and through\n// a dash-prefixed directory (`-components/`, `-hooks/`, `-queries/`, ...).\nconst isColocatedRouteImport = (source) =>\n source.startsWith(\"@/routes/\") && COLOCATED_DIR_RE.test(source);\n\n// Resolve a `@/routes/...` specifier to an absolute path by reusing the\n// currently-linted file's own path up to its `/routes/` segment as the alias\n// root — mirrors the `\"@/*\": [\"./src/*\"]` tsconfig path without a real module\n// resolver. Returns null if the linted file isn't itself under a `routes`\n// directory (shouldn't happen for a confirmed route file, but fails closed).\nconst resolveColocatedImportBase = (filename, source) => {\n // Windows gives `context.filename` with backslashes, which would make\n // `lastIndexOf(ROUTES_SEGMENT)` miss; normalize to forward slashes first\n // (valid path input on Windows, and `join` below normalizes the output).\n const normalizedFilename = filename.replaceAll(\"\\\\\", \"/\");\n const routesIndex = normalizedFilename.lastIndexOf(ROUTES_SEGMENT);\n if (routesIndex === -1) {\n return null;\n }\n const aliasRoot = normalizedFilename.slice(0, routesIndex);\n return path.join(aliasRoot, source.slice(\"@/\".length));\n};\n\n// Bounded, one-hop read: try the two extensions a colocated TS/TSX import can\n// resolve to. No further imports of the child file are followed.\nconst readColocatedFile = (basePath) => {\n for (const extension of [\".tsx\", \".ts\"]) {\n try {\n return readFileSync(`${basePath}${extension}`, \"utf-8\");\n } catch {\n // Try the next extension; if neither exists this is a shape the\n // simplified alias resolution can't handle (e.g. a directory index) —\n // acceptable since this is a best-effort bounded check, not a resolver.\n }\n }\n return null;\n};\n\n// Regex counterpart of `suspenseFactoryName`, scanning raw source text for\n// the same two argument shapes: `useSuspenseQuery(factory(...))` and\n// `useSuspenseQuery(factory)`. The character right after the identifier\n// disambiguates them from a member-expression argument the AST check would\n// also decline to attribute (e.g. `useSuspenseQuery(factory.options())`).\nconst CHILD_SUSPENSE_RE = /useSuspenseQuery\\(\\s*([A-Za-z_$][\\w$]*)\\s*([(),])/g;\n\nconst suspenseFactoriesInSource = (sourceText: string): Set<string> => {\n const factories = new Set<string>();\n for (const match of sourceText.matchAll(CHILD_SUSPENSE_RE)) {\n const factory = match.at(1);\n if (factory !== undefined) {\n factories.add(factory);\n }\n }\n return factories;\n};\n\nconst plugin: Plugin = eslintCompatPlugin({\n meta: { name: \"require-loader-prefetch\" },\n rules: {\n \"require-loader-prefetch\": {\n meta: {\n type: \"problem\",\n messages: {\n missingLoaderPrefetch:\n \"Route component calls useSuspenseQuery({{factory}}(...)) but the \" +\n \"route defines no `loader`, so this query fetches only after the \" +\n \"component mounts and suspends — an extra sequential network round \" +\n \"on cold navigation. Prefetch the same query options in the route \" +\n \"`loader` via ensureRouteQueryData (blocking, critical data) or \" +\n \"prefetchRouteQuery (non-blocking) from @/lib/react-query, so the \" +\n \"fetch starts during navigation and useSuspenseQuery consumes the \" +\n \"warm cache.\",\n factoryNotPrefetched:\n \"Route component calls useSuspenseQuery({{factory}}(...)) but the \" +\n \"route `loader` never references {{factory}}, so this query still \" +\n \"fetches on mount — an extra sequential network round on cold \" +\n \"navigation. Prefetch {{factory}}(...) in the `loader` via \" +\n \"ensureRouteQueryData (blocking, critical data) or \" +\n \"prefetchRouteQuery (non-blocking) from @/lib/react-query; \" +\n \"useSuspenseQuery in the component then consumes the warm cache.\",\n childMissingLoaderPrefetch:\n \"Route imports {{importPath}}, whose component calls \" +\n \"useSuspenseQuery({{factory}}(...)), but the route defines no \" +\n \"`loader`, so this query fetches only after the child component \" +\n \"mounts and suspends — an extra sequential network round on cold \" +\n \"navigation. Prefetch the same query options in the route \" +\n \"`loader` via ensureRouteQueryData (blocking, critical data) or \" +\n \"prefetchRouteQuery (non-blocking) from @/lib/react-query, so the \" +\n \"fetch starts during navigation and useSuspenseQuery in the child \" +\n \"component consumes the warm cache.\",\n childFactoryNotPrefetched:\n \"Route imports {{importPath}}, whose component calls \" +\n \"useSuspenseQuery({{factory}}(...)), but the route `loader` never \" +\n \"references {{factory}}, so this query still fetches on mount — \" +\n \"an extra sequential network round on cold navigation. Prefetch \" +\n \"{{factory}}(...) in the `loader` via ensureRouteQueryData \" +\n \"(blocking, critical data) or prefetchRouteQuery (non-blocking) \" +\n \"from @/lib/react-query; useSuspenseQuery in the child component \" +\n \"then consumes the warm cache.\",\n },\n },\n createOnce(context) {\n let isRouteFile = false;\n let routeOptions: { properties: unknown } | null = null;\n const suspenseCalls: { factory: string; node: Ranged }[] = [];\n const referencedInLoader = new Set();\n const colocatedImports: { node: Ranged; source: string }[] = [];\n\n return {\n before() {\n isRouteFile = false;\n routeOptions = null;\n suspenseCalls.length = 0;\n referencedInLoader.clear();\n colocatedImports.length = 0;\n },\n CallExpression(node) {\n const callee = node.callee;\n\n if (isCreateFileRouteCall(callee)) {\n isRouteFile = true;\n }\n\n if (isCreateFileRouteOptionsCall(callee)) {\n isRouteFile = true;\n const { args } = resolveLiteralArraySpreads(node.arguments);\n const optionsArg = args.at(0);\n if (\n routeOptions === null &&\n isAstNode(optionsArg) &&\n optionsArg.type === \"ObjectExpression\"\n ) {\n routeOptions = { properties: optionsArg.properties };\n }\n }\n\n const factory = suspenseFactoryName(node);\n if (factory !== null) {\n suspenseCalls.push({ node, factory });\n }\n },\n\n Identifier(node) {\n if (isInsideLoader(node)) {\n referencedInLoader.add(node.name);\n }\n },\n\n ImportDeclaration(node) {\n if (\n isStringLiteral(node.source) &&\n isColocatedRouteImport(node.source.value)\n ) {\n colocatedImports.push({ node, source: node.source.value });\n }\n },\n\n \"Program:exit\"() {\n // Only route files with an inline options object are analyzable;\n // a non-inline options argument can't be statically inspected.\n if (!isRouteFile || routeOptions === null) {\n return;\n }\n\n const properties = Array.isArray(routeOptions.properties)\n ? routeOptions.properties\n : [];\n const hasLoader = properties.some(\n (prop) =>\n prop.type === \"Property\" &&\n getPropertyName(prop.key) === LOADER_KEY,\n );\n\n for (const { node, factory } of suspenseCalls) {\n if (!hasLoader) {\n context.report({\n node,\n messageId: \"missingLoaderPrefetch\",\n data: { factory },\n });\n continue;\n }\n\n if (!referencedInLoader.has(factory)) {\n context.report({\n node,\n messageId: \"factoryNotPrefetched\",\n data: { factory },\n });\n }\n }\n\n if (colocatedImports.length === 0) {\n return;\n }\n\n const filename = filenameForContext(context);\n\n for (const { node, source } of colocatedImports) {\n const basePath = resolveColocatedImportBase(filename, source);\n if (basePath === null) {\n continue;\n }\n\n const childSource = readColocatedFile(basePath);\n if (childSource === null) {\n continue;\n }\n\n for (const factory of suspenseFactoriesInSource(childSource)) {\n if (!hasLoader) {\n context.report({\n node,\n messageId: \"childMissingLoaderPrefetch\",\n data: { factory, importPath: source },\n });\n continue;\n }\n\n if (!referencedInLoader.has(factory)) {\n context.report({\n node,\n messageId: \"childFactoryNotPrefetched\",\n data: { factory, importPath: source },\n });\n }\n }\n }\n },\n };\n },\n },\n },\n});\n\nexport default plugin;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"test-rule-utils.cjs","names":["filenameForContext","eslintCompatPlugin"],"sources":["../src/test-rule-utils.ts"],"sourcesContent":["import type { ESTree, Scope } from \"@oxlint/plugins\";\nimport { eslintCompatPlugin } from \"@oxlint/plugins\";\n\nimport { filenameForContext } from \"./utils\";\n\nexport type Node = {\n type: string;\n range: [number, number];\n arguments?: unknown;\n callee?: unknown;\n computed?: unknown;\n expressions?: unknown;\n id?: unknown;\n imported?: unknown;\n init?: unknown;\n elements?: unknown;\n operator?: unknown;\n key?: unknown;\n label?: unknown;\n kind?: unknown;\n left?: unknown;\n local?: unknown;\n meta?: unknown;\n name?: unknown;\n object?: unknown;\n parent?: unknown;\n property?: unknown;\n quasis?: unknown;\n right?: unknown;\n body?: unknown;\n source?: unknown;\n specifiers?: unknown;\n value?: unknown;\n};\n\nexport const isNode = (value: unknown): value is Node =>\n typeof value === \"object\" &&\n value !== null &&\n typeof Reflect.get(value, \"type\") === \"string\";\n\n/** `Node` is a loose view of oxlint's AST objects; every one of them carries its source span. */\nconst isScopeNode = (value: Node): value is Node & ESTree.Node =>\n typeof Reflect.get(value, \"start\") === \"number\" &&\n typeof Reflect.get(value, \"end\") === \"number\";\n\nexport const identifierName = (value: unknown): string | null =>\n isNode(value) && value.type === \"Identifier\" && typeof value.name === \"string\"\n ? value.name\n : null;\n\nexport const propertyName = (value: unknown): string | null => {\n if (!isNode(value)) return null;\n if (value.type === \"Identifier\" && typeof value.name === \"string\")\n return value.name;\n if (value.type === \"Literal\" && typeof value.value === \"string\")\n return value.value;\n return null;\n};\n\nexport const memberName = (value: unknown): string | null => {\n if (!isNode(value) || value.type !== \"MemberExpression\") return null;\n const object = identifierName(value.object);\n const property = propertyName(value.property);\n return object !== null && property !== null ? `${object}.${property}` : null;\n};\n\nexport type Binding =\n | { type: \"global\"; name: string }\n | { type: \"import\"; module: string; imported: string }\n | { type: \"const\"; initializer: Node }\n | { type: \"unknown\" };\n\nconst unknownBinding = { type: \"unknown\" } as const;\nconst maximumBindingDepth = 32;\n\nexport const createBindingResolver = (getScope: (node: Node) => Scope) => {\n const resolve = (\n node: Node,\n visited: ReadonlySet<object>,\n depth: number,\n ): Binding => {\n const name = identifierName(node);\n if (name === null || depth >= maximumBindingDepth || visited.has(node))\n return unknownBinding;\n\n const nextVisited = new Set(visited);\n nextVisited.add(node);\n let scope: Scope | null = getScope(node);\n while (scope !== null) {\n const variable = scope.set.get(name);\n if (variable === undefined) {\n scope = scope.upper;\n continue;\n }\n const definition = variable.defs.at(0);\n if (definition === undefined) return { type: \"global\", name };\n if (definition.type === \"ImportBinding\") {\n const specifier = definition.node as unknown;\n if (!isNode(specifier) || !isNode(specifier.parent))\n return unknownBinding;\n const declaration = specifier.parent;\n if (\n declaration.type !== \"ImportDeclaration\" ||\n !isNode(declaration.source) ||\n typeof declaration.source.value !== \"string\"\n )\n return unknownBinding;\n let imported = propertyName(specifier.imported);\n if (specifier.type === \"ImportDefaultSpecifier\") imported = \"default\";\n if (specifier.type === \"ImportNamespaceSpecifier\") imported = \"*\";\n return imported === null\n ? unknownBinding\n : { type: \"import\", module: declaration.source.value, imported };\n }\n if (definition.type !== \"Variable\") return unknownBinding;\n const declarator = definition.node as unknown;\n if (\n !isNode(declarator) ||\n declarator.type !== \"VariableDeclarator\" ||\n !isNode(declarator.parent) ||\n declarator.parent.type !== \"VariableDeclaration\" ||\n declarator.parent.kind !== \"const\" ||\n !isNode(declarator.init)\n )\n return unknownBinding;\n if (identifierName(declarator.id) !== null) {\n if (declarator.init.type !== \"Identifier\")\n return { type: \"const\", initializer: declarator.init };\n return resolve(declarator.init, nextVisited, depth + 1);\n }\n if (!isNode(declarator.id) || declarator.id.type !== \"ObjectPattern\")\n return unknownBinding;\n const properties = Reflect.get(declarator.id, \"properties\");\n if (!Array.isArray(properties)) return unknownBinding;\n const property = properties.find(\n (candidate) =>\n isNode(candidate) &&\n candidate.type === \"Property\" &&\n candidate.computed !== true &&\n identifierName(candidate.value) === name,\n );\n if (!isNode(property)) return unknownBinding;\n const member = propertyName(property.key);\n if (member === null || declarator.init.type !== \"Identifier\")\n return unknownBinding;\n const source = resolve(declarator.init, nextVisited, depth + 1);\n if (source.type === \"import\")\n return { type: \"import\", module: source.module, imported: member };\n if (source.type !== \"global\") return unknownBinding;\n if (\n source.name === \"globalThis\" ||\n source.name === \"window\" ||\n source.name === \"self\"\n )\n return { type: \"global\", name: member };\n return { type: \"global\", name: `${source.name}.${member}` };\n }\n return { type: \"global\", name };\n };\n\n return (node: Node): Binding => resolve(node, new Set(), 0);\n};\n\nexport const isTestFile = (context: object): boolean => {\n const filename = filenameForContext(context);\n return /(?:^|\\/)(?:__tests__\\/.*|[^/]+\\.(?:test|spec)\\.[^/]+)$/u.test(\n filename,\n );\n};\n\nexport const testOnlyPlugin = (\n name: string,\n messageId: string,\n message: string,\n makeVisitors: (\n report: (node: Node) => void,\n getScope: (node: Node) => Scope,\n ) => Record<string, (node: Node) => void>,\n) =>\n eslintCompatPlugin({\n meta: { name },\n rules: {\n [name]: {\n meta: { type: \"problem\", messages: { [messageId]: message } },\n create(context) {\n if (!isTestFile(context)) return {};\n return makeVisitors(\n (node) => context.report({ node, messageId }),\n (node) => {\n if (!isScopeNode(node))\n throw new Error(\n `Expected an oxlint AST node, got ${node.type}`,\n );\n return context.sourceCode.getScope(node);\n },\n );\n },\n },\n },\n });\n"],"mappings":"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"}
1
+ {"version":3,"file":"test-rule-utils.cjs","names":["filenameForContext","eslintCompatPlugin"],"sources":["../src/test-rule-utils.ts"],"sourcesContent":["import type { ESTree, Plugin, Scope } from \"@oxlint/plugins\";\nimport { eslintCompatPlugin } from \"@oxlint/plugins\";\n\nimport { filenameForContext } from \"./utils\";\n\nexport type Node = {\n type: string;\n range: [number, number];\n arguments?: unknown;\n callee?: unknown;\n computed?: unknown;\n expressions?: unknown;\n id?: unknown;\n imported?: unknown;\n init?: unknown;\n elements?: unknown;\n operator?: unknown;\n key?: unknown;\n label?: unknown;\n kind?: unknown;\n left?: unknown;\n local?: unknown;\n meta?: unknown;\n name?: unknown;\n object?: unknown;\n parent?: unknown;\n property?: unknown;\n quasis?: unknown;\n right?: unknown;\n body?: unknown;\n source?: unknown;\n specifiers?: unknown;\n value?: unknown;\n};\n\nexport const isNode = (value: unknown): value is Node =>\n typeof value === \"object\" &&\n value !== null &&\n typeof Reflect.get(value, \"type\") === \"string\";\n\n/** `Node` is a loose view of oxlint's AST objects; every one of them carries its source span. */\nconst isScopeNode = (value: Node): value is Node & ESTree.Node =>\n typeof Reflect.get(value, \"start\") === \"number\" &&\n typeof Reflect.get(value, \"end\") === \"number\";\n\nexport const identifierName = (value: unknown): string | null =>\n isNode(value) && value.type === \"Identifier\" && typeof value.name === \"string\"\n ? value.name\n : null;\n\nexport const propertyName = (value: unknown): string | null => {\n if (!isNode(value)) return null;\n if (value.type === \"Identifier\" && typeof value.name === \"string\")\n return value.name;\n if (value.type === \"Literal\" && typeof value.value === \"string\")\n return value.value;\n return null;\n};\n\nexport const memberName = (value: unknown): string | null => {\n if (!isNode(value) || value.type !== \"MemberExpression\") return null;\n const object = identifierName(value.object);\n const property = propertyName(value.property);\n return object !== null && property !== null ? `${object}.${property}` : null;\n};\n\nexport type Binding =\n | { type: \"global\"; name: string }\n | { type: \"import\"; module: string; imported: string }\n | { type: \"const\"; initializer: Node }\n | { type: \"unknown\" };\n\nconst unknownBinding = { type: \"unknown\" } as const;\nconst maximumBindingDepth = 32;\n\nexport const createBindingResolver = (getScope: (node: Node) => Scope) => {\n const resolve = (\n node: Node,\n visited: ReadonlySet<object>,\n depth: number,\n ): Binding => {\n const name = identifierName(node);\n if (name === null || depth >= maximumBindingDepth || visited.has(node))\n return unknownBinding;\n\n const nextVisited = new Set(visited);\n nextVisited.add(node);\n let scope: Scope | null = getScope(node);\n while (scope !== null) {\n const variable = scope.set.get(name);\n if (variable === undefined) {\n scope = scope.upper;\n continue;\n }\n const definition = variable.defs.at(0);\n if (definition === undefined) return { type: \"global\", name };\n if (definition.type === \"ImportBinding\") {\n const specifier = definition.node as unknown;\n if (!isNode(specifier) || !isNode(specifier.parent))\n return unknownBinding;\n const declaration = specifier.parent;\n if (\n declaration.type !== \"ImportDeclaration\" ||\n !isNode(declaration.source) ||\n typeof declaration.source.value !== \"string\"\n )\n return unknownBinding;\n let imported = propertyName(specifier.imported);\n if (specifier.type === \"ImportDefaultSpecifier\") imported = \"default\";\n if (specifier.type === \"ImportNamespaceSpecifier\") imported = \"*\";\n return imported === null\n ? unknownBinding\n : { type: \"import\", module: declaration.source.value, imported };\n }\n if (definition.type !== \"Variable\") return unknownBinding;\n const declarator = definition.node as unknown;\n if (\n !isNode(declarator) ||\n declarator.type !== \"VariableDeclarator\" ||\n !isNode(declarator.parent) ||\n declarator.parent.type !== \"VariableDeclaration\" ||\n declarator.parent.kind !== \"const\" ||\n !isNode(declarator.init)\n )\n return unknownBinding;\n if (identifierName(declarator.id) !== null) {\n if (declarator.init.type !== \"Identifier\")\n return { type: \"const\", initializer: declarator.init };\n return resolve(declarator.init, nextVisited, depth + 1);\n }\n if (!isNode(declarator.id) || declarator.id.type !== \"ObjectPattern\")\n return unknownBinding;\n const properties = Reflect.get(declarator.id, \"properties\");\n if (!Array.isArray(properties)) return unknownBinding;\n const property = properties.find(\n (candidate) =>\n isNode(candidate) &&\n candidate.type === \"Property\" &&\n candidate.computed !== true &&\n identifierName(candidate.value) === name,\n );\n if (!isNode(property)) return unknownBinding;\n const member = propertyName(property.key);\n if (member === null || declarator.init.type !== \"Identifier\")\n return unknownBinding;\n const source = resolve(declarator.init, nextVisited, depth + 1);\n if (source.type === \"import\")\n return { type: \"import\", module: source.module, imported: member };\n if (source.type !== \"global\") return unknownBinding;\n if (\n source.name === \"globalThis\" ||\n source.name === \"window\" ||\n source.name === \"self\"\n )\n return { type: \"global\", name: member };\n return { type: \"global\", name: `${source.name}.${member}` };\n }\n return { type: \"global\", name };\n };\n\n return (node: Node): Binding => resolve(node, new Set(), 0);\n};\n\nexport const isTestFile = (context: object): boolean => {\n const filename = filenameForContext(context);\n return /(?:^|\\/)(?:__tests__\\/.*|[^/]+\\.(?:test|spec)\\.[^/]+)$/u.test(\n filename,\n );\n};\n\nexport const testOnlyPlugin = (\n name: string,\n messageId: string,\n message: string,\n makeVisitors: (\n report: (node: Node) => void,\n getScope: (node: Node) => Scope,\n ) => Record<string, (node: Node) => void>,\n): Plugin =>\n eslintCompatPlugin({\n meta: { name },\n rules: {\n [name]: {\n meta: { type: \"problem\", messages: { [messageId]: message } },\n create(context) {\n if (!isTestFile(context)) return {};\n return makeVisitors(\n (node) => context.report({ node, messageId }),\n (node) => {\n if (!isScopeNode(node))\n throw new Error(\n `Expected an oxlint AST node, got ${node.type}`,\n );\n return context.sourceCode.getScope(node);\n },\n );\n },\n },\n },\n });\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"test-rule-utils.mjs","names":[],"sources":["../src/test-rule-utils.ts"],"sourcesContent":["import type { ESTree, Scope } from \"@oxlint/plugins\";\nimport { eslintCompatPlugin } from \"@oxlint/plugins\";\n\nimport { filenameForContext } from \"./utils\";\n\nexport type Node = {\n type: string;\n range: [number, number];\n arguments?: unknown;\n callee?: unknown;\n computed?: unknown;\n expressions?: unknown;\n id?: unknown;\n imported?: unknown;\n init?: unknown;\n elements?: unknown;\n operator?: unknown;\n key?: unknown;\n label?: unknown;\n kind?: unknown;\n left?: unknown;\n local?: unknown;\n meta?: unknown;\n name?: unknown;\n object?: unknown;\n parent?: unknown;\n property?: unknown;\n quasis?: unknown;\n right?: unknown;\n body?: unknown;\n source?: unknown;\n specifiers?: unknown;\n value?: unknown;\n};\n\nexport const isNode = (value: unknown): value is Node =>\n typeof value === \"object\" &&\n value !== null &&\n typeof Reflect.get(value, \"type\") === \"string\";\n\n/** `Node` is a loose view of oxlint's AST objects; every one of them carries its source span. */\nconst isScopeNode = (value: Node): value is Node & ESTree.Node =>\n typeof Reflect.get(value, \"start\") === \"number\" &&\n typeof Reflect.get(value, \"end\") === \"number\";\n\nexport const identifierName = (value: unknown): string | null =>\n isNode(value) && value.type === \"Identifier\" && typeof value.name === \"string\"\n ? value.name\n : null;\n\nexport const propertyName = (value: unknown): string | null => {\n if (!isNode(value)) return null;\n if (value.type === \"Identifier\" && typeof value.name === \"string\")\n return value.name;\n if (value.type === \"Literal\" && typeof value.value === \"string\")\n return value.value;\n return null;\n};\n\nexport const memberName = (value: unknown): string | null => {\n if (!isNode(value) || value.type !== \"MemberExpression\") return null;\n const object = identifierName(value.object);\n const property = propertyName(value.property);\n return object !== null && property !== null ? `${object}.${property}` : null;\n};\n\nexport type Binding =\n | { type: \"global\"; name: string }\n | { type: \"import\"; module: string; imported: string }\n | { type: \"const\"; initializer: Node }\n | { type: \"unknown\" };\n\nconst unknownBinding = { type: \"unknown\" } as const;\nconst maximumBindingDepth = 32;\n\nexport const createBindingResolver = (getScope: (node: Node) => Scope) => {\n const resolve = (\n node: Node,\n visited: ReadonlySet<object>,\n depth: number,\n ): Binding => {\n const name = identifierName(node);\n if (name === null || depth >= maximumBindingDepth || visited.has(node))\n return unknownBinding;\n\n const nextVisited = new Set(visited);\n nextVisited.add(node);\n let scope: Scope | null = getScope(node);\n while (scope !== null) {\n const variable = scope.set.get(name);\n if (variable === undefined) {\n scope = scope.upper;\n continue;\n }\n const definition = variable.defs.at(0);\n if (definition === undefined) return { type: \"global\", name };\n if (definition.type === \"ImportBinding\") {\n const specifier = definition.node as unknown;\n if (!isNode(specifier) || !isNode(specifier.parent))\n return unknownBinding;\n const declaration = specifier.parent;\n if (\n declaration.type !== \"ImportDeclaration\" ||\n !isNode(declaration.source) ||\n typeof declaration.source.value !== \"string\"\n )\n return unknownBinding;\n let imported = propertyName(specifier.imported);\n if (specifier.type === \"ImportDefaultSpecifier\") imported = \"default\";\n if (specifier.type === \"ImportNamespaceSpecifier\") imported = \"*\";\n return imported === null\n ? unknownBinding\n : { type: \"import\", module: declaration.source.value, imported };\n }\n if (definition.type !== \"Variable\") return unknownBinding;\n const declarator = definition.node as unknown;\n if (\n !isNode(declarator) ||\n declarator.type !== \"VariableDeclarator\" ||\n !isNode(declarator.parent) ||\n declarator.parent.type !== \"VariableDeclaration\" ||\n declarator.parent.kind !== \"const\" ||\n !isNode(declarator.init)\n )\n return unknownBinding;\n if (identifierName(declarator.id) !== null) {\n if (declarator.init.type !== \"Identifier\")\n return { type: \"const\", initializer: declarator.init };\n return resolve(declarator.init, nextVisited, depth + 1);\n }\n if (!isNode(declarator.id) || declarator.id.type !== \"ObjectPattern\")\n return unknownBinding;\n const properties = Reflect.get(declarator.id, \"properties\");\n if (!Array.isArray(properties)) return unknownBinding;\n const property = properties.find(\n (candidate) =>\n isNode(candidate) &&\n candidate.type === \"Property\" &&\n candidate.computed !== true &&\n identifierName(candidate.value) === name,\n );\n if (!isNode(property)) return unknownBinding;\n const member = propertyName(property.key);\n if (member === null || declarator.init.type !== \"Identifier\")\n return unknownBinding;\n const source = resolve(declarator.init, nextVisited, depth + 1);\n if (source.type === \"import\")\n return { type: \"import\", module: source.module, imported: member };\n if (source.type !== \"global\") return unknownBinding;\n if (\n source.name === \"globalThis\" ||\n source.name === \"window\" ||\n source.name === \"self\"\n )\n return { type: \"global\", name: member };\n return { type: \"global\", name: `${source.name}.${member}` };\n }\n return { type: \"global\", name };\n };\n\n return (node: Node): Binding => resolve(node, new Set(), 0);\n};\n\nexport const isTestFile = (context: object): boolean => {\n const filename = filenameForContext(context);\n return /(?:^|\\/)(?:__tests__\\/.*|[^/]+\\.(?:test|spec)\\.[^/]+)$/u.test(\n filename,\n );\n};\n\nexport const testOnlyPlugin = (\n name: string,\n messageId: string,\n message: string,\n makeVisitors: (\n report: (node: Node) => void,\n getScope: (node: Node) => Scope,\n ) => Record<string, (node: Node) => void>,\n) =>\n eslintCompatPlugin({\n meta: { name },\n rules: {\n [name]: {\n meta: { type: \"problem\", messages: { [messageId]: message } },\n create(context) {\n if (!isTestFile(context)) return {};\n return makeVisitors(\n (node) => context.report({ node, messageId }),\n (node) => {\n if (!isScopeNode(node))\n throw new Error(\n `Expected an oxlint AST node, got ${node.type}`,\n );\n return context.sourceCode.getScope(node);\n },\n );\n },\n },\n },\n });\n"],"mappings":"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"}
1
+ {"version":3,"file":"test-rule-utils.mjs","names":[],"sources":["../src/test-rule-utils.ts"],"sourcesContent":["import type { ESTree, Plugin, Scope } from \"@oxlint/plugins\";\nimport { eslintCompatPlugin } from \"@oxlint/plugins\";\n\nimport { filenameForContext } from \"./utils\";\n\nexport type Node = {\n type: string;\n range: [number, number];\n arguments?: unknown;\n callee?: unknown;\n computed?: unknown;\n expressions?: unknown;\n id?: unknown;\n imported?: unknown;\n init?: unknown;\n elements?: unknown;\n operator?: unknown;\n key?: unknown;\n label?: unknown;\n kind?: unknown;\n left?: unknown;\n local?: unknown;\n meta?: unknown;\n name?: unknown;\n object?: unknown;\n parent?: unknown;\n property?: unknown;\n quasis?: unknown;\n right?: unknown;\n body?: unknown;\n source?: unknown;\n specifiers?: unknown;\n value?: unknown;\n};\n\nexport const isNode = (value: unknown): value is Node =>\n typeof value === \"object\" &&\n value !== null &&\n typeof Reflect.get(value, \"type\") === \"string\";\n\n/** `Node` is a loose view of oxlint's AST objects; every one of them carries its source span. */\nconst isScopeNode = (value: Node): value is Node & ESTree.Node =>\n typeof Reflect.get(value, \"start\") === \"number\" &&\n typeof Reflect.get(value, \"end\") === \"number\";\n\nexport const identifierName = (value: unknown): string | null =>\n isNode(value) && value.type === \"Identifier\" && typeof value.name === \"string\"\n ? value.name\n : null;\n\nexport const propertyName = (value: unknown): string | null => {\n if (!isNode(value)) return null;\n if (value.type === \"Identifier\" && typeof value.name === \"string\")\n return value.name;\n if (value.type === \"Literal\" && typeof value.value === \"string\")\n return value.value;\n return null;\n};\n\nexport const memberName = (value: unknown): string | null => {\n if (!isNode(value) || value.type !== \"MemberExpression\") return null;\n const object = identifierName(value.object);\n const property = propertyName(value.property);\n return object !== null && property !== null ? `${object}.${property}` : null;\n};\n\nexport type Binding =\n | { type: \"global\"; name: string }\n | { type: \"import\"; module: string; imported: string }\n | { type: \"const\"; initializer: Node }\n | { type: \"unknown\" };\n\nconst unknownBinding = { type: \"unknown\" } as const;\nconst maximumBindingDepth = 32;\n\nexport const createBindingResolver = (getScope: (node: Node) => Scope) => {\n const resolve = (\n node: Node,\n visited: ReadonlySet<object>,\n depth: number,\n ): Binding => {\n const name = identifierName(node);\n if (name === null || depth >= maximumBindingDepth || visited.has(node))\n return unknownBinding;\n\n const nextVisited = new Set(visited);\n nextVisited.add(node);\n let scope: Scope | null = getScope(node);\n while (scope !== null) {\n const variable = scope.set.get(name);\n if (variable === undefined) {\n scope = scope.upper;\n continue;\n }\n const definition = variable.defs.at(0);\n if (definition === undefined) return { type: \"global\", name };\n if (definition.type === \"ImportBinding\") {\n const specifier = definition.node as unknown;\n if (!isNode(specifier) || !isNode(specifier.parent))\n return unknownBinding;\n const declaration = specifier.parent;\n if (\n declaration.type !== \"ImportDeclaration\" ||\n !isNode(declaration.source) ||\n typeof declaration.source.value !== \"string\"\n )\n return unknownBinding;\n let imported = propertyName(specifier.imported);\n if (specifier.type === \"ImportDefaultSpecifier\") imported = \"default\";\n if (specifier.type === \"ImportNamespaceSpecifier\") imported = \"*\";\n return imported === null\n ? unknownBinding\n : { type: \"import\", module: declaration.source.value, imported };\n }\n if (definition.type !== \"Variable\") return unknownBinding;\n const declarator = definition.node as unknown;\n if (\n !isNode(declarator) ||\n declarator.type !== \"VariableDeclarator\" ||\n !isNode(declarator.parent) ||\n declarator.parent.type !== \"VariableDeclaration\" ||\n declarator.parent.kind !== \"const\" ||\n !isNode(declarator.init)\n )\n return unknownBinding;\n if (identifierName(declarator.id) !== null) {\n if (declarator.init.type !== \"Identifier\")\n return { type: \"const\", initializer: declarator.init };\n return resolve(declarator.init, nextVisited, depth + 1);\n }\n if (!isNode(declarator.id) || declarator.id.type !== \"ObjectPattern\")\n return unknownBinding;\n const properties = Reflect.get(declarator.id, \"properties\");\n if (!Array.isArray(properties)) return unknownBinding;\n const property = properties.find(\n (candidate) =>\n isNode(candidate) &&\n candidate.type === \"Property\" &&\n candidate.computed !== true &&\n identifierName(candidate.value) === name,\n );\n if (!isNode(property)) return unknownBinding;\n const member = propertyName(property.key);\n if (member === null || declarator.init.type !== \"Identifier\")\n return unknownBinding;\n const source = resolve(declarator.init, nextVisited, depth + 1);\n if (source.type === \"import\")\n return { type: \"import\", module: source.module, imported: member };\n if (source.type !== \"global\") return unknownBinding;\n if (\n source.name === \"globalThis\" ||\n source.name === \"window\" ||\n source.name === \"self\"\n )\n return { type: \"global\", name: member };\n return { type: \"global\", name: `${source.name}.${member}` };\n }\n return { type: \"global\", name };\n };\n\n return (node: Node): Binding => resolve(node, new Set(), 0);\n};\n\nexport const isTestFile = (context: object): boolean => {\n const filename = filenameForContext(context);\n return /(?:^|\\/)(?:__tests__\\/.*|[^/]+\\.(?:test|spec)\\.[^/]+)$/u.test(\n filename,\n );\n};\n\nexport const testOnlyPlugin = (\n name: string,\n messageId: string,\n message: string,\n makeVisitors: (\n report: (node: Node) => void,\n getScope: (node: Node) => Scope,\n ) => Record<string, (node: Node) => void>,\n): Plugin =>\n eslintCompatPlugin({\n meta: { name },\n rules: {\n [name]: {\n meta: { type: \"problem\", messages: { [messageId]: message } },\n create(context) {\n if (!isTestFile(context)) return {};\n return makeVisitors(\n (node) => context.report({ node, messageId }),\n (node) => {\n if (!isScopeNode(node))\n throw new Error(\n `Expected an oxlint AST node, got ${node.type}`,\n );\n return context.sourceCode.getScope(node);\n },\n );\n },\n },\n },\n });\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"utils.cjs","names":[],"sources":["../src/utils.ts"],"sourcesContent":["// Shared AST helpers for the rules in this folder.\n//\n// Oxlint plugin AST nodes are passed in untyped. Each helper narrows from\n// `unknown` so rule files can call them without per-call type ceremony or\n// shared type-import boilerplate.\n\nimport type { ESTree, Ranged, Scope, Variable } from \"@oxlint/plugins\";\n\nexport type AstNode = Ranged & { type: string } & Record<string, unknown>;\n\ntype FilenameContext = {\n filename?: string;\n getFilename?: () => string;\n};\n\nexport const filenameForContext = (context: FilenameContext): string =>\n (context.filename ?? context.getFilename?.() ?? \"\").replaceAll(\"\\\\\", \"/\");\n\nexport const isAstNode = (node: unknown): node is AstNode =>\n typeof node === \"object\" &&\n node !== null &&\n \"type\" in node &&\n typeof (node as { type: unknown }).type === \"string\" &&\n \"range\" in node;\n\nexport const isIdentifier = (\n node: unknown,\n name?: string,\n): node is AstNode & { type: \"Identifier\"; name: string } => {\n if (!isAstNode(node) || node.type !== \"Identifier\") {\n return false;\n }\n if (typeof node.name !== \"string\") {\n return false;\n }\n return name === undefined || node.name === name;\n};\n\nexport const isStringLiteral = (\n node: unknown,\n): node is AstNode & { type: \"Literal\"; value: string } =>\n isAstNode(node) && node.type === \"Literal\" && typeof node.value === \"string\";\n\n// Resolve the static name of a Property or MemberExpression key:\n// Identifier.name or string-Literal.value. Returns null for computed keys\n// driven by a non-literal expression.\nexport const getPropertyName = (node: unknown): string | null => {\n if (isIdentifier(node)) {\n return node.name;\n }\n if (isStringLiteral(node)) {\n return node.value;\n }\n return null;\n};\n\nexport const staticStringValue = (node: unknown): string | null => {\n const expression = unwrapExpression(node);\n if (expression === null) return null;\n node = expression;\n if (isStringLiteral(node)) return node.value;\n if (\n !isAstNode(node) ||\n node.type !== \"TemplateLiteral\" ||\n !Array.isArray(node.expressions) ||\n node.expressions.length !== 0 ||\n !Array.isArray(node.quasis)\n )\n return null;\n const quasi = node.quasis.at(0);\n if (!isAstNode(quasi)) return null;\n const value = quasi.value;\n const cooked =\n typeof value === \"object\" && value !== null\n ? Reflect.get(value, \"cooked\")\n : undefined;\n return typeof cooked === \"string\" ? cooked : null;\n};\n\nexport const memberPropertyName = (node: unknown): string | null => {\n if (!isAstNode(node) || node.type !== \"MemberExpression\") {\n return null;\n }\n if (node.computed !== true) {\n return getPropertyName(node.property);\n }\n return staticStringValue(node.property);\n};\n\nexport const invokedCallee = (node: unknown): AstNode | null => {\n if (!isAstNode(node) || node.type !== \"CallExpression\") {\n return null;\n }\n const callee = unwrapExpression(node.callee);\n if (!isAstNode(callee)) {\n return null;\n }\n if (\n callee.type === \"SequenceExpression\" &&\n Array.isArray(callee.expressions)\n ) {\n return unwrapExpression(callee.expressions.at(-1));\n }\n if (\n callee.type === \"MemberExpression\" &&\n (memberPropertyName(callee) === \"call\" ||\n memberPropertyName(callee) === \"apply\")\n ) {\n return unwrapExpression(callee.object);\n }\n return callee;\n};\n\n// Match `<object>.<property>` member access where both halves are\n// Identifiers and the access is not computed.\nexport const isMemberAccess = (\n node: unknown,\n object: string,\n property: string,\n): boolean =>\n isAstNode(node) &&\n node.type === \"MemberExpression\" &&\n node.computed === false &&\n isIdentifier(node.object, object) &&\n isIdentifier(node.property, property);\n\n// Match `CallExpression` whose callee is an Identifier with the given name.\nexport const isCallTo = (node: unknown, name: string): boolean =>\n isAstNode(node) &&\n node.type === \"CallExpression\" &&\n isIdentifier(node.callee, name);\n\n// Resolve the dot-notation name of a callee: an Identifier, or a\n// non-computed MemberExpression chain (e.g. `t.String`, `Schema.is`,\n// `process.stderr.write`). Returns null when the chain is computed\n// or the property name itself can't be resolved.\n//\n// If the chain is rooted at a non-Identifier expression (e.g. `foo().bar`),\n// returns the bare property name (\"bar\") rather than null. Callers that\n// match the result against a fixed allowlist must consider whether they\n// need to distinguish `foo().createSafeHandler` from `createSafeHandler`.\nexport const getCalleeName = (callee: unknown): string | null => {\n if (isIdentifier(callee)) {\n return callee.name;\n }\n if (!isAstNode(callee) || callee.type !== \"MemberExpression\") {\n return null;\n }\n if (callee.computed !== false) {\n return null;\n }\n const objectName = getCalleeName(callee.object);\n const propertyName = getPropertyName(callee.property);\n if (propertyName === null) {\n return null;\n }\n return objectName === null ? propertyName : `${objectName}.${propertyName}`;\n};\n\n// Parser wrappers with no value transformation of their own.\nexport const EXPRESSION_WRAPPER_TYPES = [\n \"ParenthesizedExpression\",\n \"ChainExpression\",\n \"TSNonNullExpression\",\n \"TSAsExpression\",\n \"TSSatisfiesExpression\",\n \"TSTypeAssertion\",\n \"TSInstantiationExpression\",\n] as const satisfies readonly ESTree.Node[\"type\"][];\n\n// Keep the parser's other expression-bearing contexts intact.\nexport const unwrapExpression = (node: unknown): AstNode | null => {\n while (isAstNode(node)) {\n const { type } = node;\n if (!EXPRESSION_WRAPPER_TYPES.some((wrapper) => wrapper === type))\n return node;\n node = node.expression;\n }\n return null;\n};\n\nexport type LiteralArgumentResolution =\n | { status: \"known\"; args: unknown[] }\n | { status: \"opaque\"; args: unknown[]; spread: unknown; tail: unknown[] };\n\n// Expand literal array spreads and retain unresolved spread nodes in sequence.\n// Positional readers use only `args`, the prefix before the first barrier;\n// source inspectors can still inspect later entries in `tail`.\nexport const resolveLiteralArraySpreads = (\n args: unknown[],\n): LiteralArgumentResolution => {\n const expanded: unknown[] = [];\n const append = (argument: unknown): void => {\n if (!isAstNode(argument) || argument.type !== \"SpreadElement\") {\n expanded.push(argument);\n return;\n }\n const expression = unwrapExpression(argument.argument);\n if (\n expression?.type !== \"ArrayExpression\" ||\n !Array.isArray(expression.elements)\n ) {\n expanded.push(argument);\n return;\n }\n for (const element of expression.elements) append(element);\n };\n for (const argument of args) append(argument);\n const barrierIndex = expanded.findIndex(\n (argument) => isAstNode(argument) && argument.type === \"SpreadElement\",\n );\n if (barrierIndex === -1) return { status: \"known\", args: expanded };\n const spread = expanded.at(barrierIndex);\n return {\n status: \"opaque\",\n args: expanded.slice(0, barrierIndex),\n spread,\n tail: expanded.slice(barrierIndex + 1),\n };\n};\n\n// Resolve an ImportSpecifier's imported binding name (Identifier.name or\n// string-Literal.value). Returns null when the specifier shape is unexpected.\nexport const getImportedName = (specifier: unknown): string | null => {\n if (!isAstNode(specifier) || specifier.type !== \"ImportSpecifier\") {\n return null;\n }\n const imported = specifier.imported;\n if (isIdentifier(imported)) {\n return imported.name;\n }\n if (isStringLiteral(imported)) {\n return imported.value;\n }\n return null;\n};\n\n// Resolve an ImportSpecifier's local binding name. This differs from\n// getImportedName for aliased imports such as `import { source as local }`.\nexport const getImportLocalName = (specifier: unknown): string | null => {\n if (!isAstNode(specifier) || specifier.type !== \"ImportSpecifier\") {\n return null;\n }\n return isIdentifier(specifier.local) ? specifier.local.name : null;\n};\n\n// An Identifier the parser emitted as a value reference (it carries a range,\n// which synthetic name nodes do not).\nexport const isIdentifierReference = (\n node: unknown,\n): node is ESTree.IdentifierReference =>\n isIdentifier(node) && Array.isArray(node.range);\n\nexport type ScopeContext = {\n sourceCode: { getScope: (node: ESTree.Node) => Scope };\n};\n\nconst resolveVariable = (\n context: ScopeContext,\n identifier: ESTree.IdentifierReference,\n): Variable | null => {\n let scope: Scope | null = context.sourceCode.getScope(identifier);\n while (scope !== null) {\n const variable = scope.set.get(identifier.name);\n if (variable !== undefined) {\n return variable;\n }\n scope = scope.upper;\n }\n return null;\n};\n\nconst isSingleAssignment = (variable: Variable): boolean =>\n variable.references.every(\n (reference) => !reference.isWrite() || reference.init,\n );\n\ntype ImportedBinding = { source: string; imported: string };\nconst NAMESPACE_IMPORT = \"*\";\n\nconst memberOfNamespace = (\n base: ImportedBinding | null,\n property: string | null,\n): ImportedBinding | null =>\n base?.imported === NAMESPACE_IMPORT && property !== null\n ? { source: base.source, imported: property }\n : null;\n\nconst bindingFromVariable = (\n context: ScopeContext,\n variable: Variable,\n seen: Set<unknown>,\n): ImportedBinding | null => {\n const definition = variable.defs.at(0);\n if (variable.defs.length !== 1 || definition === undefined) {\n return null;\n }\n const node: unknown = definition.node;\n if (definition.type === \"ImportBinding\") {\n const declaration: unknown = definition.parent;\n if (\n !isAstNode(node) ||\n node.importKind === \"type\" ||\n !isAstNode(declaration) ||\n declaration.type !== \"ImportDeclaration\" ||\n declaration.importKind === \"type\" ||\n !isStringLiteral(declaration.source)\n ) {\n return null;\n }\n const source = declaration.source.value;\n if (node.type === \"ImportNamespaceSpecifier\") {\n return { source, imported: NAMESPACE_IMPORT };\n }\n if (node.type === \"ImportDefaultSpecifier\") {\n return { source, imported: \"default\" };\n }\n const imported = getImportedName(node);\n return imported === null ? null : { source, imported };\n }\n if (\n definition.type !== \"Variable\" ||\n !isAstNode(node) ||\n node.type !== \"VariableDeclarator\" ||\n !isSingleAssignment(variable) ||\n !isIdentifier(node.id)\n ) {\n return null;\n }\n return resolveImportedExpressionForEnforcement(context, node.init, seen);\n};\n\n// The module export an expression evaluates to, or null for a local,\n// parameter, call result, or unresolved global.\nexport const resolveImportedExpressionForEnforcement = (\n context: ScopeContext,\n node: unknown,\n seen = new Set<unknown>(),\n): ImportedBinding | null => {\n const expression = unwrapExpression(node);\n if (!isAstNode(expression) || seen.has(expression)) {\n return null;\n }\n seen.add(expression);\n if (isIdentifierReference(expression)) {\n const variable = resolveVariable(context, expression);\n return variable === null\n ? null\n : bindingFromVariable(context, variable, seen);\n }\n if (expression.type === \"MemberExpression\") {\n const base = resolveImportedExpressionForEnforcement(\n context,\n expression.object,\n seen,\n );\n return memberOfNamespace(base, memberPropertyName(expression));\n }\n return null;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"utils.cjs","names":[],"sources":["../src/utils.ts"],"sourcesContent":["// Shared AST helpers for the rules in this folder.\n//\n// Oxlint plugin AST nodes are passed in untyped. Each helper narrows from\n// `unknown` so rule files can call them without per-call type ceremony or\n// shared type-import boilerplate.\n\nimport type { ESTree, Ranged, Scope, Variable } from \"@oxlint/plugins\";\n\nexport type AstNode = Ranged & { type: string } & Record<string, unknown>;\n\ntype FilenameContext = {\n filename?: string;\n getFilename?: () => string;\n};\n\nexport const filenameForContext = (context: FilenameContext): string =>\n (context.filename ?? context.getFilename?.() ?? \"\").replaceAll(\"\\\\\", \"/\");\n\nexport const isAstNode = (node: unknown): node is AstNode =>\n typeof node === \"object\" &&\n node !== null &&\n \"type\" in node &&\n typeof (node as { type: unknown }).type === \"string\" &&\n \"range\" in node;\n\nexport const isIdentifier = (\n node: unknown,\n name?: string,\n): node is AstNode & { type: \"Identifier\"; name: string } => {\n if (!isAstNode(node) || node.type !== \"Identifier\") {\n return false;\n }\n if (typeof node.name !== \"string\") {\n return false;\n }\n return name === undefined || node.name === name;\n};\n\nexport const isStringLiteral = (\n node: unknown,\n): node is AstNode & { type: \"Literal\"; value: string } =>\n isAstNode(node) && node.type === \"Literal\" && typeof node.value === \"string\";\n\n// Resolve the static name of a Property or MemberExpression key:\n// Identifier.name or string-Literal.value. Returns null for computed keys\n// driven by a non-literal expression.\nexport const getPropertyName = (node: unknown): string | null => {\n if (isIdentifier(node)) {\n return node.name;\n }\n if (isStringLiteral(node)) {\n return node.value;\n }\n return null;\n};\n\nexport const staticStringValue = (node: unknown): string | null => {\n const expression = unwrapExpression(node);\n if (expression === null) return null;\n node = expression;\n if (isStringLiteral(node)) return node.value;\n if (\n !isAstNode(node) ||\n node.type !== \"TemplateLiteral\" ||\n !Array.isArray(node.expressions) ||\n node.expressions.length !== 0 ||\n !Array.isArray(node.quasis)\n )\n return null;\n const quasi = node.quasis.at(0);\n if (!isAstNode(quasi)) return null;\n const value = quasi.value;\n const cooked =\n typeof value === \"object\" && value !== null\n ? Reflect.get(value, \"cooked\")\n : undefined;\n return typeof cooked === \"string\" ? cooked : null;\n};\n\nexport const memberPropertyName = (node: unknown): string | null => {\n if (!isAstNode(node) || node.type !== \"MemberExpression\") {\n return null;\n }\n if (node.computed !== true) {\n return getPropertyName(node.property);\n }\n return staticStringValue(node.property);\n};\n\nexport const invokedCallee = (node: unknown): AstNode | null => {\n if (!isAstNode(node) || node.type !== \"CallExpression\") {\n return null;\n }\n const callee = unwrapExpression(node.callee);\n if (!isAstNode(callee)) {\n return null;\n }\n if (\n callee.type === \"SequenceExpression\" &&\n Array.isArray(callee.expressions)\n ) {\n return unwrapExpression(callee.expressions.at(-1));\n }\n if (\n callee.type === \"MemberExpression\" &&\n (memberPropertyName(callee) === \"call\" ||\n memberPropertyName(callee) === \"apply\")\n ) {\n return unwrapExpression(callee.object);\n }\n return callee;\n};\n\n// Match `<object>.<property>` member access where both halves are\n// Identifiers and the access is not computed.\nexport const isMemberAccess = (\n node: unknown,\n object: string,\n property: string,\n): boolean =>\n isAstNode(node) &&\n node.type === \"MemberExpression\" &&\n node.computed === false &&\n isIdentifier(node.object, object) &&\n isIdentifier(node.property, property);\n\n// Match `CallExpression` whose callee is an Identifier with the given name.\nexport const isCallTo = (node: unknown, name: string): boolean =>\n isAstNode(node) &&\n node.type === \"CallExpression\" &&\n isIdentifier(node.callee, name);\n\n// Resolve the dot-notation name of a callee: an Identifier, or a\n// non-computed MemberExpression chain (e.g. `t.String`, `Schema.is`,\n// `process.stderr.write`). Returns null when the chain is computed\n// or the property name itself can't be resolved.\n//\n// If the chain is rooted at a non-Identifier expression (e.g. `foo().bar`),\n// returns the bare property name (\"bar\") rather than null. Callers that\n// match the result against a fixed allowlist must consider whether they\n// need to distinguish `foo().createSafeHandler` from `createSafeHandler`.\nexport const getCalleeName = (callee: unknown): string | null => {\n if (isIdentifier(callee)) {\n return callee.name;\n }\n if (!isAstNode(callee) || callee.type !== \"MemberExpression\") {\n return null;\n }\n if (callee.computed !== false) {\n return null;\n }\n const objectName = getCalleeName(callee.object);\n const propertyName = getPropertyName(callee.property);\n if (propertyName === null) {\n return null;\n }\n return objectName === null ? propertyName : `${objectName}.${propertyName}`;\n};\n\n// Parser wrappers with no value transformation of their own.\nexport const EXPRESSION_WRAPPER_TYPES = [\n \"ParenthesizedExpression\",\n \"ChainExpression\",\n \"TSNonNullExpression\",\n \"TSAsExpression\",\n \"TSSatisfiesExpression\",\n \"TSTypeAssertion\",\n \"TSInstantiationExpression\",\n] as const;\n\n// Keep the parser's other expression-bearing contexts intact.\nexport const unwrapExpression = (node: unknown): AstNode | null => {\n while (isAstNode(node)) {\n const { type } = node;\n if (\n !(EXPRESSION_WRAPPER_TYPES satisfies readonly ESTree.Node[\"type\"][]).some(\n (wrapper) => wrapper === type,\n )\n )\n return node;\n node = node.expression;\n }\n return null;\n};\n\nexport type LiteralArgumentResolution =\n | { status: \"known\"; args: unknown[] }\n | { status: \"opaque\"; args: unknown[]; spread: unknown; tail: unknown[] };\n\n// Expand literal array spreads and retain unresolved spread nodes in sequence.\n// Positional readers use only `args`, the prefix before the first barrier;\n// source inspectors can still inspect later entries in `tail`.\nexport const resolveLiteralArraySpreads = (\n args: unknown[],\n): LiteralArgumentResolution => {\n const expanded: unknown[] = [];\n const append = (argument: unknown): void => {\n if (!isAstNode(argument) || argument.type !== \"SpreadElement\") {\n expanded.push(argument);\n return;\n }\n const expression = unwrapExpression(argument.argument);\n if (\n expression?.type !== \"ArrayExpression\" ||\n !Array.isArray(expression.elements)\n ) {\n expanded.push(argument);\n return;\n }\n for (const element of expression.elements) append(element);\n };\n for (const argument of args) append(argument);\n const barrierIndex = expanded.findIndex(\n (argument) => isAstNode(argument) && argument.type === \"SpreadElement\",\n );\n if (barrierIndex === -1) return { status: \"known\", args: expanded };\n const spread = expanded.at(barrierIndex);\n return {\n status: \"opaque\",\n args: expanded.slice(0, barrierIndex),\n spread,\n tail: expanded.slice(barrierIndex + 1),\n };\n};\n\n// Resolve an ImportSpecifier's imported binding name (Identifier.name or\n// string-Literal.value). Returns null when the specifier shape is unexpected.\nexport const getImportedName = (specifier: unknown): string | null => {\n if (!isAstNode(specifier) || specifier.type !== \"ImportSpecifier\") {\n return null;\n }\n const imported = specifier.imported;\n if (isIdentifier(imported)) {\n return imported.name;\n }\n if (isStringLiteral(imported)) {\n return imported.value;\n }\n return null;\n};\n\n// Resolve an ImportSpecifier's local binding name. This differs from\n// getImportedName for aliased imports such as `import { source as local }`.\nexport const getImportLocalName = (specifier: unknown): string | null => {\n if (!isAstNode(specifier) || specifier.type !== \"ImportSpecifier\") {\n return null;\n }\n return isIdentifier(specifier.local) ? specifier.local.name : null;\n};\n\n// An Identifier the parser emitted as a value reference (it carries a range,\n// which synthetic name nodes do not).\nexport const isIdentifierReference = (\n node: unknown,\n): node is ESTree.IdentifierReference =>\n isIdentifier(node) && Array.isArray(node.range);\n\nexport type ScopeContext = {\n sourceCode: { getScope: (node: ESTree.Node) => Scope };\n};\n\nconst resolveVariable = (\n context: ScopeContext,\n identifier: ESTree.IdentifierReference,\n): Variable | null => {\n let scope: Scope | null = context.sourceCode.getScope(identifier);\n while (scope !== null) {\n const variable = scope.set.get(identifier.name);\n if (variable !== undefined) {\n return variable;\n }\n scope = scope.upper;\n }\n return null;\n};\n\nconst isSingleAssignment = (variable: Variable): boolean =>\n variable.references.every(\n (reference) => !reference.isWrite() || reference.init,\n );\n\ntype ImportedBinding = { source: string; imported: string };\nconst NAMESPACE_IMPORT = \"*\";\n\nconst memberOfNamespace = (\n base: ImportedBinding | null,\n property: string | null,\n): ImportedBinding | null =>\n base?.imported === NAMESPACE_IMPORT && property !== null\n ? { source: base.source, imported: property }\n : null;\n\nconst bindingFromVariable = (\n context: ScopeContext,\n variable: Variable,\n seen: Set<unknown>,\n): ImportedBinding | null => {\n const definition = variable.defs.at(0);\n if (variable.defs.length !== 1 || definition === undefined) {\n return null;\n }\n const node: unknown = definition.node;\n if (definition.type === \"ImportBinding\") {\n const declaration: unknown = definition.parent;\n if (\n !isAstNode(node) ||\n node.importKind === \"type\" ||\n !isAstNode(declaration) ||\n declaration.type !== \"ImportDeclaration\" ||\n declaration.importKind === \"type\" ||\n !isStringLiteral(declaration.source)\n ) {\n return null;\n }\n const source = declaration.source.value;\n if (node.type === \"ImportNamespaceSpecifier\") {\n return { source, imported: NAMESPACE_IMPORT };\n }\n if (node.type === \"ImportDefaultSpecifier\") {\n return { source, imported: \"default\" };\n }\n const imported = getImportedName(node);\n return imported === null ? null : { source, imported };\n }\n if (\n definition.type !== \"Variable\" ||\n !isAstNode(node) ||\n node.type !== \"VariableDeclarator\" ||\n !isSingleAssignment(variable) ||\n !isIdentifier(node.id)\n ) {\n return null;\n }\n return resolveImportedExpressionForEnforcement(context, node.init, seen);\n};\n\n// The module export an expression evaluates to, or null for a local,\n// parameter, call result, or unresolved global.\nexport const resolveImportedExpressionForEnforcement = (\n context: ScopeContext,\n node: unknown,\n seen: Set<unknown> = new Set(),\n): ImportedBinding | null => {\n const expression = unwrapExpression(node);\n if (!isAstNode(expression) || seen.has(expression)) {\n return null;\n }\n seen.add(expression);\n if (isIdentifierReference(expression)) {\n const variable = resolveVariable(context, expression);\n return variable === null\n ? null\n : bindingFromVariable(context, variable, seen);\n }\n if (expression.type === \"MemberExpression\") {\n const base = resolveImportedExpressionForEnforcement(\n context,\n expression.object,\n seen,\n );\n return memberOfNamespace(base, memberPropertyName(expression));\n }\n return null;\n};\n"],"mappings":"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"}