eslint-plugin-harlanzw 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.mjs +539 -104
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -1,9 +1,10 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
+ import process from 'node:process';
3
4
  import { TextSourceCodeBase, ConfigCommentParser, Directive, VisitNodeStep } from '@eslint/plugin-kit';
4
5
  import { AST_NODE_TYPES } from '@typescript-eslint/utils';
5
6
 
6
- const version = "0.3.0";
7
+ const version = "0.4.0";
7
8
 
8
9
  const STRENGTH_PATTERNS = {
9
10
  strong: ["never", "must", "always", "under no circumstances", "absolutely", "required", "mandatory", "forbidden", "prohibited"],
@@ -1465,8 +1466,10 @@ const hasDocs = [
1465
1466
  "link-require-descriptive-text",
1466
1467
  "link-require-href",
1467
1468
  "link-trailing-slash",
1469
+ "nuxt-no-random",
1468
1470
  "nuxt-no-redundant-import-meta",
1469
1471
  "nuxt-no-side-effects-in-setup",
1472
+ "nuxt-no-unsafe-date",
1470
1473
  "nuxt-prefer-navigate-to-over-router-push-replace",
1471
1474
  "nuxt-prefer-nuxt-link-over-router-link",
1472
1475
  "use-composables-must-use-reactivity",
@@ -1633,6 +1636,57 @@ const VUE_REACTIVITY_APIS = /* @__PURE__ */ new Set([
1633
1636
  "getCurrentScope",
1634
1637
  "onScopeDispose"
1635
1638
  ]);
1639
+ const VUEUSE_REACTIVITY_APIS = /* @__PURE__ */ new Set([
1640
+ // Watch variants
1641
+ "whenever",
1642
+ "watchArray",
1643
+ "watchAtMost",
1644
+ "watchDebounced",
1645
+ "watchDeep",
1646
+ "watchIgnorable",
1647
+ "watchImmediate",
1648
+ "watchOnce",
1649
+ "watchPausable",
1650
+ "watchThrottled",
1651
+ "watchTriggerable",
1652
+ "watchWithFilter",
1653
+ "debouncedWatch",
1654
+ "throttledWatch",
1655
+ "until",
1656
+ // Computed variants
1657
+ "computedAsync",
1658
+ "computedEager",
1659
+ "computedInject",
1660
+ "computedWithControl",
1661
+ // Reactive utilities
1662
+ "reactiveComputed",
1663
+ "reactiveOmit",
1664
+ "reactivePick",
1665
+ "toReactive",
1666
+ // Ref utilities
1667
+ "controlledRef",
1668
+ "debouncedRef",
1669
+ "throttledRef",
1670
+ "refAutoReset",
1671
+ "refDebounced",
1672
+ "refDefault",
1673
+ "refThrottled",
1674
+ "refWithControl",
1675
+ "extendRef",
1676
+ "syncRef",
1677
+ "syncRefs",
1678
+ "templateRef",
1679
+ // State management
1680
+ "createGlobalState",
1681
+ "createInjectionState",
1682
+ "createSharedComposable",
1683
+ // Event/lifecycle hooks
1684
+ "onClickOutside",
1685
+ "onKeyStroke",
1686
+ "onLongPress",
1687
+ "onStartTyping",
1688
+ "createEventHook"
1689
+ ]);
1636
1690
  const SIDE_EFFECT_FUNCTIONS = /* @__PURE__ */ new Set([
1637
1691
  // Timer functions
1638
1692
  "setTimeout",
@@ -1744,10 +1798,99 @@ function trackVueImports(node, vueImports) {
1744
1798
  });
1745
1799
  }
1746
1800
  }
1801
+ function trackNonVueImports(node, nonVueImports) {
1802
+ if (node.source.value !== "vue") {
1803
+ for (const spec of node.specifiers) {
1804
+ if (spec.type === "ImportSpecifier" && spec.imported.type === "Identifier") {
1805
+ nonVueImports.add(spec.imported.name);
1806
+ }
1807
+ }
1808
+ }
1809
+ }
1810
+ function createReactivityChecker(vueImports, nonVueImports) {
1811
+ function isAutoImportedReactivityCall(node) {
1812
+ if (node.callee.type === "Identifier") {
1813
+ const name = node.callee.name;
1814
+ return (VUE_REACTIVITY_APIS.has(name) || VUEUSE_REACTIVITY_APIS.has(name)) && !nonVueImports.has(name);
1815
+ }
1816
+ return false;
1817
+ }
1818
+ function isReactiveLifecycleCall(node) {
1819
+ return node.callee.type === "Identifier" && /^tryOn[A-Z]/.test(node.callee.name);
1820
+ }
1821
+ function hasReactivityInExpression(expr) {
1822
+ if (!expr)
1823
+ return false;
1824
+ switch (expr.type) {
1825
+ case "CallExpression":
1826
+ if (isReactivityCall(expr, vueImports) || isAutoImportedReactivityCall(expr) || isComposableCall(expr) || isReactiveLifecycleCall(expr))
1827
+ return true;
1828
+ return expr.arguments.some((arg) => hasReactivityInArg(arg));
1829
+ case "NewExpression":
1830
+ return expr.arguments.some((arg) => hasReactivityInArg(arg));
1831
+ case "MemberExpression":
1832
+ return hasReactivityInExpression(expr.object);
1833
+ case "AssignmentExpression":
1834
+ return hasReactivityInExpression(expr.right);
1835
+ case "ObjectExpression":
1836
+ return expr.properties.some((prop) => prop.type === "Property" && hasReactivityInExpression(prop.value));
1837
+ case "ArrayExpression":
1838
+ return expr.elements.some((elem) => hasReactivityInExpression(elem));
1839
+ case "AwaitExpression":
1840
+ return hasReactivityInExpression(expr.argument);
1841
+ case "ConditionalExpression":
1842
+ return hasReactivityInExpression(expr.consequent) || hasReactivityInExpression(expr.alternate);
1843
+ case "LogicalExpression":
1844
+ return hasReactivityInExpression(expr.left) || hasReactivityInExpression(expr.right);
1845
+ default:
1846
+ return false;
1847
+ }
1848
+ }
1849
+ function hasReactivityInArg(arg) {
1850
+ if (arg.type === "ArrowFunctionExpression" || arg.type === "FunctionExpression") {
1851
+ if (arg.body.type === "BlockStatement")
1852
+ return arg.body.body.some((stmt) => hasReactivityInStatement(stmt));
1853
+ return hasReactivityInExpression(arg.body);
1854
+ }
1855
+ if (arg.type === "SpreadElement")
1856
+ return hasReactivityInExpression(arg.argument);
1857
+ return hasReactivityInExpression(arg);
1858
+ }
1859
+ function hasReactivityInStatement(stmt) {
1860
+ if (!stmt)
1861
+ return false;
1862
+ switch (stmt.type) {
1863
+ case "ExpressionStatement":
1864
+ return hasReactivityInExpression(stmt.expression);
1865
+ case "VariableDeclaration":
1866
+ return stmt.declarations.some((decl) => hasReactivityInExpression(decl.init));
1867
+ case "ReturnStatement":
1868
+ return hasReactivityInExpression(stmt.argument);
1869
+ case "BlockStatement":
1870
+ return stmt.body.some((s) => hasReactivityInStatement(s));
1871
+ case "IfStatement":
1872
+ return hasReactivityInStatement(stmt.consequent) || (stmt.alternate ? hasReactivityInStatement(stmt.alternate) : false);
1873
+ case "WhileStatement":
1874
+ case "DoWhileStatement":
1875
+ return hasReactivityInStatement(stmt.body);
1876
+ case "ForStatement":
1877
+ case "ForInStatement":
1878
+ case "ForOfStatement":
1879
+ return hasReactivityInStatement(stmt.body);
1880
+ case "TryStatement":
1881
+ return hasReactivityInStatement(stmt.block) || (stmt.handler ? hasReactivityInStatement(stmt.handler.body) : false) || (stmt.finalizer ? hasReactivityInStatement(stmt.finalizer) : false);
1882
+ case "SwitchStatement":
1883
+ return stmt.cases.some((switchCase) => switchCase.consequent.some((s) => hasReactivityInStatement(s)));
1884
+ default:
1885
+ return false;
1886
+ }
1887
+ }
1888
+ return { hasReactivityInStatement, hasReactivityInExpression };
1889
+ }
1747
1890
 
1748
- const RULE_NAME$i = "link-ascii-only";
1891
+ const RULE_NAME$l = "link-ascii-only";
1749
1892
  const linkAsciiOnly = createEslintRule({
1750
- name: RULE_NAME$i,
1893
+ name: RULE_NAME$l,
1751
1894
  meta: {
1752
1895
  type: "suggestion",
1753
1896
  docs: {
@@ -1819,9 +1962,9 @@ const linkAsciiOnly = createEslintRule({
1819
1962
  }
1820
1963
  });
1821
1964
 
1822
- const RULE_NAME$h = "link-lowercase";
1965
+ const RULE_NAME$k = "link-lowercase";
1823
1966
  const linkLowercase = createEslintRule({
1824
- name: RULE_NAME$h,
1967
+ name: RULE_NAME$k,
1825
1968
  meta: {
1826
1969
  type: "suggestion",
1827
1970
  docs: {
@@ -1897,7 +2040,7 @@ const linkLowercase = createEslintRule({
1897
2040
  }
1898
2041
  });
1899
2042
 
1900
- const RULE_NAME$g = "link-no-double-slashes";
2043
+ const RULE_NAME$j = "link-no-double-slashes";
1901
2044
  function fixDoubleSlashesInUrl(url) {
1902
2045
  if (url.startsWith("//") || url.includes("://"))
1903
2046
  return url;
@@ -1917,7 +2060,7 @@ function fixDoubleSlashesInUrl(url) {
1917
2060
  return `${path.replace(/\/+/g, "/")}${search}${hash}`;
1918
2061
  }
1919
2062
  const linkNoDoubleSlashes = createEslintRule({
1920
- name: RULE_NAME$g,
2063
+ name: RULE_NAME$j,
1921
2064
  meta: {
1922
2065
  type: "problem",
1923
2066
  docs: {
@@ -1993,9 +2136,9 @@ const linkNoDoubleSlashes = createEslintRule({
1993
2136
  }
1994
2137
  });
1995
2138
 
1996
- const RULE_NAME$f = "link-no-underscores";
2139
+ const RULE_NAME$i = "link-no-underscores";
1997
2140
  const linkNoUnderscores = createEslintRule({
1998
- name: RULE_NAME$f,
2141
+ name: RULE_NAME$i,
1999
2142
  meta: {
2000
2143
  type: "suggestion",
2001
2144
  docs: {
@@ -2069,9 +2212,9 @@ const linkNoUnderscores = createEslintRule({
2069
2212
  }
2070
2213
  });
2071
2214
 
2072
- const RULE_NAME$e = "link-no-whitespace";
2215
+ const RULE_NAME$h = "link-no-whitespace";
2073
2216
  const linkNoWhitespace = createEslintRule({
2074
- name: RULE_NAME$e,
2217
+ name: RULE_NAME$h,
2075
2218
  meta: {
2076
2219
  type: "suggestion",
2077
2220
  docs: {
@@ -2145,7 +2288,7 @@ const linkNoWhitespace = createEslintRule({
2145
2288
  }
2146
2289
  });
2147
2290
 
2148
- const RULE_NAME$d = "link-require-descriptive-text";
2291
+ const RULE_NAME$g = "link-require-descriptive-text";
2149
2292
  const BAD_LINK_TEXTS = /* @__PURE__ */ new Set([
2150
2293
  "click here",
2151
2294
  "click this",
@@ -2191,7 +2334,7 @@ function getVueLinkUrl(node) {
2191
2334
  return null;
2192
2335
  }
2193
2336
  const linkRequireDescriptiveText = createEslintRule({
2194
- name: RULE_NAME$d,
2337
+ name: RULE_NAME$g,
2195
2338
  meta: {
2196
2339
  type: "suggestion",
2197
2340
  docs: {
@@ -2267,9 +2410,9 @@ const linkRequireDescriptiveText = createEslintRule({
2267
2410
  }
2268
2411
  });
2269
2412
 
2270
- const RULE_NAME$c = "link-require-href";
2413
+ const RULE_NAME$f = "link-require-href";
2271
2414
  const linkRequireHref = createEslintRule({
2272
- name: RULE_NAME$c,
2415
+ name: RULE_NAME$f,
2273
2416
  meta: {
2274
2417
  type: "problem",
2275
2418
  docs: {
@@ -2334,12 +2477,12 @@ const linkRequireHref = createEslintRule({
2334
2477
  }
2335
2478
  });
2336
2479
 
2337
- const RULE_NAME$b = "link-trailing-slash";
2480
+ const RULE_NAME$e = "link-trailing-slash";
2338
2481
  function shouldSkipUrl(url) {
2339
2482
  return url.startsWith("#") || url.includes(":") || url === "/" || url === "";
2340
2483
  }
2341
2484
  const linkTrailingSlash = createEslintRule({
2342
- name: RULE_NAME$b,
2485
+ name: RULE_NAME$e,
2343
2486
  meta: {
2344
2487
  type: "suggestion",
2345
2488
  docs: {
@@ -2454,9 +2597,9 @@ const linkTrailingSlash = createEslintRule({
2454
2597
  }
2455
2598
  });
2456
2599
 
2457
- const RULE_NAME$a = "nuxt-await-navigate-to";
2600
+ const RULE_NAME$d = "nuxt-await-navigate-to";
2458
2601
  const nuxtAwaitNavigateTo = createEslintRule({
2459
- name: RULE_NAME$a,
2602
+ name: RULE_NAME$d,
2460
2603
  meta: {
2461
2604
  type: "problem",
2462
2605
  docs: {
@@ -2505,9 +2648,188 @@ const nuxtAwaitNavigateTo = createEslintRule({
2505
2648
  }
2506
2649
  });
2507
2650
 
2508
- const RULE_NAME$9 = "nuxt-no-redundant-import-meta";
2651
+ const CLIENT_LIFECYCLE_HOOKS = /* @__PURE__ */ new Set([
2652
+ "onMounted",
2653
+ "onBeforeMount",
2654
+ "onUpdated",
2655
+ "onBeforeUpdate",
2656
+ "onActivated",
2657
+ "onDeactivated"
2658
+ ]);
2659
+ const DEFERRED_CALLBACK_FUNCTIONS = /* @__PURE__ */ new Set([
2660
+ "watch"
2661
+ ]);
2662
+ const SERVER_HANDLER_FUNCTIONS = /* @__PURE__ */ new Set([
2663
+ "defineEventHandler",
2664
+ "defineCachedEventHandler",
2665
+ "defineNitroPlugin",
2666
+ "defineTask"
2667
+ ]);
2668
+ function isNamedFunction(funcNode) {
2669
+ if (funcNode.type === "FunctionDeclaration")
2670
+ return true;
2671
+ const parent = funcNode.parent;
2672
+ if (!parent)
2673
+ return false;
2674
+ if (parent.type === "VariableDeclarator")
2675
+ return true;
2676
+ if (parent.type === "Property") {
2677
+ if (parent.key.type === "Identifier" && parent.key.name === "setup" && parent.parent?.type === "ObjectExpression" && parent.parent.parent?.type === "CallExpression" && parent.parent.parent.callee.type === "Identifier" && parent.parent.parent.callee.name === "defineComponent") {
2678
+ return false;
2679
+ }
2680
+ return true;
2681
+ }
2682
+ if (parent.type === "MethodDefinition")
2683
+ return true;
2684
+ if (parent.type === "AssignmentExpression" && parent.left.type === "Identifier")
2685
+ return true;
2686
+ return false;
2687
+ }
2688
+ function executedDuringSetup(node) {
2689
+ let current = node.parent;
2690
+ while (current) {
2691
+ if (current.type === "ArrowFunctionExpression" || current.type === "FunctionExpression" || current.type === "FunctionDeclaration") {
2692
+ if ((current.type === "ArrowFunctionExpression" || current.type === "FunctionExpression") && current.parent?.type === "CallExpression" && current.parent.callee.type === "Identifier") {
2693
+ const calleeName = current.parent.callee.name;
2694
+ if (CLIENT_LIFECYCLE_HOOKS.has(calleeName))
2695
+ return false;
2696
+ if (SERVER_HANDLER_FUNCTIONS.has(calleeName))
2697
+ return false;
2698
+ if (DEFERRED_CALLBACK_FUNCTIONS.has(calleeName)) {
2699
+ const args = current.parent.arguments;
2700
+ const argIndex = args.indexOf(current);
2701
+ if (argIndex > 0)
2702
+ return false;
2703
+ }
2704
+ }
2705
+ if (isNamedFunction(current))
2706
+ return false;
2707
+ }
2708
+ if (current.type === "Program")
2709
+ return true;
2710
+ current = current.parent;
2711
+ }
2712
+ return false;
2713
+ }
2714
+ function isClientGuardTest(test) {
2715
+ if (test.type === "MemberExpression" && test.object.type === "MetaProperty" && test.property.type === "Identifier" && test.property.name === "client") {
2716
+ return true;
2717
+ }
2718
+ if (test.type === "MemberExpression" && test.object.type === "Identifier" && test.object.name === "process" && test.property.type === "Identifier" && test.property.name === "client") {
2719
+ return true;
2720
+ }
2721
+ if (test.type === "BinaryExpression" && test.left.type === "UnaryExpression" && test.left.operator === "typeof" && test.left.argument.type === "Identifier" && test.left.argument.name === "window") {
2722
+ return true;
2723
+ }
2724
+ return false;
2725
+ }
2726
+ function isInsideClientGuard(node) {
2727
+ let parent = node.parent;
2728
+ while (parent) {
2729
+ if (parent.type === "IfStatement" && isClientGuardTest(parent.test))
2730
+ return isDescendantOfConsequent(node, parent);
2731
+ if (parent.type === "ConditionalExpression" && isClientGuardTest(parent.test)) {
2732
+ return isInConsequentBranch(node, parent);
2733
+ }
2734
+ parent = parent.parent;
2735
+ }
2736
+ return false;
2737
+ }
2738
+ function isDescendantOfConsequent(node, ifStmt) {
2739
+ let current = node;
2740
+ while (current && current !== ifStmt) {
2741
+ if (current === ifStmt.consequent)
2742
+ return true;
2743
+ current = current.parent;
2744
+ }
2745
+ return false;
2746
+ }
2747
+ function isInConsequentBranch(node, ternary) {
2748
+ let current = node;
2749
+ while (current && current !== ternary) {
2750
+ if (current === ternary.consequent)
2751
+ return true;
2752
+ if (current === ternary.alternate)
2753
+ return false;
2754
+ current = current.parent;
2755
+ }
2756
+ return false;
2757
+ }
2758
+
2759
+ const RULE_NAME$c = "nuxt-no-random";
2760
+ function isMathRandomCall(node) {
2761
+ return node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && node.callee.object.name === "Math" && node.callee.property.type === "Identifier" && node.callee.property.name === "random";
2762
+ }
2763
+ function isCryptoRandomCall(node) {
2764
+ if (node.callee.type !== "MemberExpression" || node.callee.property.type !== "Identifier")
2765
+ return false;
2766
+ const method = node.callee.property.name;
2767
+ if (method !== "randomUUID" && method !== "getRandomValues")
2768
+ return false;
2769
+ const obj = node.callee.object;
2770
+ if (obj.type === "Identifier" && obj.name === "crypto")
2771
+ return true;
2772
+ if (obj.type === "MemberExpression" && obj.object.type === "Identifier" && (obj.object.name === "globalThis" || obj.object.name === "window") && obj.property.type === "Identifier" && obj.property.name === "crypto") {
2773
+ return true;
2774
+ }
2775
+ return false;
2776
+ }
2777
+ function reportRandom(context, node, template) {
2778
+ if (isMathRandomCall(node)) {
2779
+ context.report({ node, messageId: template ? "noMathRandomTemplate" : "noMathRandom" });
2780
+ } else if (isCryptoRandomCall(node)) {
2781
+ const method = node.callee.property;
2782
+ context.report({
2783
+ node,
2784
+ messageId: template ? "noCryptoRandomTemplate" : "noCryptoRandom",
2785
+ data: { method: `crypto.${method.name}` }
2786
+ });
2787
+ }
2788
+ }
2789
+ const nuxtNoRandom = createEslintRule({
2790
+ name: RULE_NAME$c,
2791
+ meta: {
2792
+ type: "problem",
2793
+ docs: {
2794
+ description: "disallow Math.random() and crypto random APIs in SSR-rendered code to prevent hydration mismatches"
2795
+ },
2796
+ schema: [],
2797
+ messages: {
2798
+ noMathRandom: "Math.random() produces different values on server and client, causing hydration mismatches. Move to onMounted() or guard with `if (import.meta.client)`.",
2799
+ noCryptoRandom: "{{method}}() produces different values on server and client, causing hydration mismatches. Move to onMounted() or guard with `if (import.meta.client)`.",
2800
+ noMathRandomTemplate: "Math.random() in templates produces different values on server and client, causing hydration mismatches. Move to a computed property backed by onMounted().",
2801
+ noCryptoRandomTemplate: "{{method}}() in templates produces different values on server and client, causing hydration mismatches. Move to a computed property backed by onMounted()."
2802
+ }
2803
+ },
2804
+ defaultOptions: [],
2805
+ create: (context) => {
2806
+ function checkScript(node) {
2807
+ if (!isMathRandomCall(node) && !isCryptoRandomCall(node))
2808
+ return;
2809
+ if (!executedDuringSetup(node))
2810
+ return;
2811
+ if (isInsideClientGuard(node))
2812
+ return;
2813
+ reportRandom(context, node, false);
2814
+ }
2815
+ if (isVueParser(context)) {
2816
+ return defineTemplateBodyVisitor(context, {
2817
+ // Template expressions — always execute during render
2818
+ CallExpression(node) {
2819
+ if (isMathRandomCall(node) || isCryptoRandomCall(node))
2820
+ reportRandom(context, node, true);
2821
+ }
2822
+ }, {
2823
+ CallExpression: checkScript
2824
+ });
2825
+ }
2826
+ return { CallExpression: checkScript };
2827
+ }
2828
+ });
2829
+
2830
+ const RULE_NAME$b = "nuxt-no-redundant-import-meta";
2509
2831
  const nuxtNoRedundantImportMeta = createEslintRule({
2510
- name: RULE_NAME$9,
2832
+ name: RULE_NAME$b,
2511
2833
  meta: {
2512
2834
  type: "problem",
2513
2835
  docs: {
@@ -2544,7 +2866,7 @@ const nuxtNoRedundantImportMeta = createEslintRule({
2544
2866
  }
2545
2867
  });
2546
2868
 
2547
- const RULE_NAME$8 = "nuxt-no-side-effects-in-async-data-handler";
2869
+ const RULE_NAME$a = "nuxt-no-side-effects-in-async-data-handler";
2548
2870
  const SIDE_EFFECT_PATTERNS = /* @__PURE__ */ new Set([
2549
2871
  // Store/State mutations
2550
2872
  "$patch",
@@ -2652,7 +2974,7 @@ function findSideEffectsInFunction(functionNode) {
2652
2974
  return sideEffects;
2653
2975
  }
2654
2976
  const nuxtNoSideEffectsInAsyncDataHandler = createEslintRule({
2655
- name: RULE_NAME$8,
2977
+ name: RULE_NAME$a,
2656
2978
  meta: {
2657
2979
  type: "problem",
2658
2980
  docs: {
@@ -2743,9 +3065,9 @@ ${indent}${callOnceBlock}`)
2743
3065
  }
2744
3066
  });
2745
3067
 
2746
- const RULE_NAME$7 = "nuxt-no-side-effects-in-setup";
3068
+ const RULE_NAME$9 = "nuxt-no-side-effects-in-setup";
2747
3069
  const nuxtNoSideEffectsInSetup = createEslintRule({
2748
- name: RULE_NAME$7,
3070
+ name: RULE_NAME$9,
2749
3071
  meta: {
2750
3072
  type: "problem",
2751
3073
  docs: {
@@ -2838,9 +3160,82 @@ ${indent}})`;
2838
3160
  }
2839
3161
  });
2840
3162
 
2841
- const RULE_NAME$6 = "nuxt-prefer-navigate-to-over-router-push-replace";
3163
+ const RULE_NAME$8 = "nuxt-no-unsafe-date";
3164
+ function isDateNowCall(node) {
3165
+ return node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && node.callee.object.name === "Date" && node.callee.property.type === "Identifier" && node.callee.property.name === "now";
3166
+ }
3167
+ function isDateFunctionCall(node) {
3168
+ return node.callee.type === "Identifier" && node.callee.name === "Date" && node.parent?.type !== "NewExpression";
3169
+ }
3170
+ function isNewDateCall(node) {
3171
+ return node.callee.type === "Identifier" && node.callee.name === "Date" && node.arguments.length === 0;
3172
+ }
3173
+ const nuxtNoUnsafeDate = createEslintRule({
3174
+ name: RULE_NAME$8,
3175
+ meta: {
3176
+ type: "problem",
3177
+ docs: {
3178
+ description: "disallow Date.now() and new Date() in SSR-rendered code to prevent hydration mismatches"
3179
+ },
3180
+ schema: [],
3181
+ messages: {
3182
+ noDateNow: "Date.now() returns different timestamps on server and client, causing hydration mismatches. Use the <NuxtTime> component or move to onMounted().",
3183
+ noNewDate: "new Date() returns different timestamps on server and client, causing hydration mismatches. Use the <NuxtTime> component or move to onMounted().",
3184
+ noDateCall: "Date() returns the current time as a string, which differs on server and client. Use the <NuxtTime> component or move to onMounted().",
3185
+ noDateNowTemplate: "Date.now() in templates returns different timestamps on server and client. Use the <NuxtTime> component instead.",
3186
+ noNewDateTemplate: "new Date() in templates returns different timestamps on server and client. Use the <NuxtTime> component instead.",
3187
+ noDateCallTemplate: "Date() in templates returns the current time, which differs on server and client. Use the <NuxtTime> component instead."
3188
+ }
3189
+ },
3190
+ defaultOptions: [],
3191
+ create: (context) => {
3192
+ function checkCallScript(node) {
3193
+ const isNow = isDateNowCall(node);
3194
+ const isCall = !isNow && isDateFunctionCall(node);
3195
+ if (!isNow && !isCall)
3196
+ return;
3197
+ if (!executedDuringSetup(node))
3198
+ return;
3199
+ if (isInsideClientGuard(node))
3200
+ return;
3201
+ context.report({ node, messageId: isNow ? "noDateNow" : "noDateCall" });
3202
+ }
3203
+ function checkNewScript(node) {
3204
+ if (!isNewDateCall(node))
3205
+ return;
3206
+ if (!executedDuringSetup(node))
3207
+ return;
3208
+ if (isInsideClientGuard(node))
3209
+ return;
3210
+ context.report({ node, messageId: "noNewDate" });
3211
+ }
3212
+ if (isVueParser(context)) {
3213
+ return defineTemplateBodyVisitor(context, {
3214
+ CallExpression(node) {
3215
+ if (isDateNowCall(node))
3216
+ context.report({ node, messageId: "noDateNowTemplate" });
3217
+ else if (isDateFunctionCall(node))
3218
+ context.report({ node, messageId: "noDateCallTemplate" });
3219
+ },
3220
+ NewExpression(node) {
3221
+ if (isNewDateCall(node))
3222
+ context.report({ node, messageId: "noNewDateTemplate" });
3223
+ }
3224
+ }, {
3225
+ CallExpression: checkCallScript,
3226
+ NewExpression: checkNewScript
3227
+ });
3228
+ }
3229
+ return {
3230
+ CallExpression: checkCallScript,
3231
+ NewExpression: checkNewScript
3232
+ };
3233
+ }
3234
+ });
3235
+
3236
+ const RULE_NAME$7 = "nuxt-prefer-navigate-to-over-router-push-replace";
2842
3237
  const nuxtPreferNavigateToOverRouterPushReplace = createEslintRule({
2843
- name: RULE_NAME$6,
3238
+ name: RULE_NAME$7,
2844
3239
  meta: {
2845
3240
  type: "suggestion",
2846
3241
  docs: {
@@ -2903,9 +3298,9 @@ const nuxtPreferNavigateToOverRouterPushReplace = createEslintRule({
2903
3298
  }
2904
3299
  });
2905
3300
 
2906
- const RULE_NAME$5 = "nuxt-prefer-nuxt-link-over-router-link";
3301
+ const RULE_NAME$6 = "nuxt-prefer-nuxt-link-over-router-link";
2907
3302
  const nuxtPreferNuxtLinkOverRouterLink = createEslintRule({
2908
- name: RULE_NAME$5,
3303
+ name: RULE_NAME$6,
2909
3304
  meta: {
2910
3305
  type: "suggestion",
2911
3306
  docs: {
@@ -2976,9 +3371,9 @@ const nuxtPreferNuxtLinkOverRouterLink = createEslintRule({
2976
3371
  }
2977
3372
  });
2978
3373
 
2979
- const RULE_NAME$4 = "vue-no-faux-composables";
3374
+ const RULE_NAME$5 = "vue-no-faux-composables";
2980
3375
  const vueNoFauxComposables = createEslintRule({
2981
- name: RULE_NAME$4,
3376
+ name: RULE_NAME$5,
2982
3377
  meta: {
2983
3378
  type: "problem",
2984
3379
  docs: {
@@ -2994,64 +3389,16 @@ const vueNoFauxComposables = createEslintRule({
2994
3389
  const vueImports = /* @__PURE__ */ new Set();
2995
3390
  const nonVueImports = /* @__PURE__ */ new Set();
2996
3391
  const composableFunctions = /* @__PURE__ */ new Map();
2997
- function isAutoImportedReactivityCall(node) {
2998
- if (node.callee.type === "Identifier") {
2999
- const name = node.callee.name;
3000
- return VUE_REACTIVITY_APIS.has(name) && !nonVueImports.has(name);
3001
- }
3002
- return false;
3003
- }
3004
- function hasReactivityInStatement(stmt) {
3005
- if (!stmt)
3006
- return false;
3007
- switch (stmt.type) {
3008
- case "ExpressionStatement":
3009
- return hasReactivityInExpression(stmt.expression);
3010
- case "VariableDeclaration":
3011
- return stmt.declarations.some((decl) => hasReactivityInExpression(decl.init));
3012
- case "ReturnStatement":
3013
- return hasReactivityInExpression(stmt.argument);
3014
- case "BlockStatement":
3015
- return stmt.body.some((s) => hasReactivityInStatement(s));
3016
- case "IfStatement":
3017
- return hasReactivityInStatement(stmt.consequent) || (stmt.alternate ? hasReactivityInStatement(stmt.alternate) : false);
3018
- case "WhileStatement":
3019
- case "DoWhileStatement":
3020
- return hasReactivityInStatement(stmt.body);
3021
- case "ForStatement":
3022
- case "ForInStatement":
3023
- case "ForOfStatement":
3024
- return hasReactivityInStatement(stmt.body);
3025
- case "TryStatement":
3026
- return hasReactivityInStatement(stmt.block) || (stmt.handler ? hasReactivityInStatement(stmt.handler.body) : false) || (stmt.finalizer ? hasReactivityInStatement(stmt.finalizer) : false);
3027
- case "SwitchStatement":
3028
- return stmt.cases.some((switchCase) => switchCase.consequent.some((s) => hasReactivityInStatement(s)));
3029
- default:
3030
- return false;
3031
- }
3032
- }
3033
- function hasReactivityInExpression(expr) {
3034
- if (!expr)
3035
- return false;
3036
- switch (expr.type) {
3037
- case "CallExpression":
3038
- if (isReactivityCall(expr, vueImports) || isAutoImportedReactivityCall(expr) || isComposableCall(expr))
3039
- return true;
3040
- return false;
3041
- case "ObjectExpression":
3042
- return expr.properties.some((prop) => prop.type === "Property" && hasReactivityInExpression(prop.value));
3043
- case "ArrayExpression":
3044
- return expr.elements.some((elem) => hasReactivityInExpression(elem));
3045
- case "AwaitExpression":
3046
- return hasReactivityInExpression(expr.argument);
3047
- default:
3048
- return false;
3049
- }
3050
- }
3392
+ const { hasReactivityInStatement, hasReactivityInExpression } = createReactivityChecker(vueImports, nonVueImports);
3051
3393
  function checkFunctionForReactivity(functionNode, functionName) {
3052
- if (!functionNode.body || functionNode.body.type !== "BlockStatement")
3394
+ if (!functionNode.body)
3053
3395
  return;
3054
- const hasReactivity = functionNode.body.body.some((stmt) => hasReactivityInStatement(stmt));
3396
+ let hasReactivity;
3397
+ if (functionNode.body.type === "BlockStatement") {
3398
+ hasReactivity = functionNode.body.body.some((stmt) => hasReactivityInStatement(stmt));
3399
+ } else {
3400
+ hasReactivity = hasReactivityInExpression(functionNode.body);
3401
+ }
3055
3402
  if (!hasReactivity) {
3056
3403
  context.report({
3057
3404
  node: functionNode,
@@ -3068,13 +3415,7 @@ const vueNoFauxComposables = createEslintRule({
3068
3415
  },
3069
3416
  ImportDeclaration(node) {
3070
3417
  trackVueImports(node, vueImports);
3071
- if (node.source.value !== "vue") {
3072
- for (const spec of node.specifiers) {
3073
- if (spec.type === "ImportSpecifier" && spec.imported.type === "Identifier") {
3074
- nonVueImports.add(spec.imported.name);
3075
- }
3076
- }
3077
- }
3418
+ trackNonVueImports(node, nonVueImports);
3078
3419
  },
3079
3420
  "Program:exit": function() {
3080
3421
  for (const [name, functionNode] of composableFunctions)
@@ -3098,9 +3439,9 @@ const vueNoFauxComposables = createEslintRule({
3098
3439
  }
3099
3440
  });
3100
3441
 
3101
- const RULE_NAME$3 = "vue-no-nested-reactivity";
3442
+ const RULE_NAME$4 = "vue-no-nested-reactivity";
3102
3443
  const vueNoNestedReactivity = createEslintRule({
3103
- name: RULE_NAME$3,
3444
+ name: RULE_NAME$4,
3104
3445
  meta: {
3105
3446
  type: "problem",
3106
3447
  docs: {
@@ -3345,9 +3686,9 @@ const vueNoNestedReactivity = createEslintRule({
3345
3686
  }
3346
3687
  });
3347
3688
 
3348
- const RULE_NAME$2 = "vue-no-passing-refs-as-props";
3689
+ const RULE_NAME$3 = "vue-no-passing-refs-as-props";
3349
3690
  const vueNoPassingRefsAsProps = createEslintRule({
3350
- name: RULE_NAME$2,
3691
+ name: RULE_NAME$3,
3351
3692
  meta: {
3352
3693
  type: "problem",
3353
3694
  docs: {
@@ -3454,9 +3795,9 @@ const vueNoReactiveDestructuring = createEslintRule({
3454
3795
  }
3455
3796
  });
3456
3797
 
3457
- const RULE_NAME$1 = "vue-no-ref-access-in-templates";
3798
+ const RULE_NAME$2 = "vue-no-ref-access-in-templates";
3458
3799
  const vueNoRefAccessInTemplates = createEslintRule({
3459
- name: RULE_NAME$1,
3800
+ name: RULE_NAME$2,
3460
3801
  meta: {
3461
3802
  type: "suggestion",
3462
3803
  docs: {
@@ -3566,9 +3907,9 @@ const vueNoRefAccessInTemplates = createEslintRule({
3566
3907
  }
3567
3908
  });
3568
3909
 
3569
- const RULE_NAME = "vue-no-torefs-on-props";
3910
+ const RULE_NAME$1 = "vue-no-torefs-on-props";
3570
3911
  const vueNoTorefsOnProps = createEslintRule({
3571
- name: RULE_NAME,
3912
+ name: RULE_NAME$1,
3572
3913
  meta: {
3573
3914
  type: "suggestion",
3574
3915
  docs: {
@@ -3629,6 +3970,94 @@ const vueNoTorefsOnProps = createEslintRule({
3629
3970
  }
3630
3971
  });
3631
3972
 
3973
+ const RULE_NAME = "vue-require-composable-prefix";
3974
+ const vueRequireComposablePrefix = createEslintRule({
3975
+ name: RULE_NAME,
3976
+ meta: {
3977
+ type: "suggestion",
3978
+ docs: {
3979
+ description: "enforce use* prefix for functions that use Vue reactivity"
3980
+ },
3981
+ hasSuggestions: true,
3982
+ schema: [],
3983
+ messages: {
3984
+ requirePrefix: 'Function "{{name}}" uses Vue reactivity \u2014 consider renaming to "use{{Name}}"'
3985
+ }
3986
+ },
3987
+ defaultOptions: [],
3988
+ create: (context) => {
3989
+ const vueImports = /* @__PURE__ */ new Set();
3990
+ const nonVueImports = /* @__PURE__ */ new Set();
3991
+ const candidateFunctions = /* @__PURE__ */ new Map();
3992
+ const { hasReactivityInStatement, hasReactivityInExpression } = createReactivityChecker(vueImports, nonVueImports);
3993
+ function isExcludedName(name) {
3994
+ return /^define[A-Z]/.test(name) || name === "setup";
3995
+ }
3996
+ function checkFunctionForReactivity(functionNode, idNode, functionName) {
3997
+ if (!functionNode.body)
3998
+ return;
3999
+ let hasReactivity;
4000
+ if (functionNode.body.type === "BlockStatement") {
4001
+ hasReactivity = functionNode.body.body.some((stmt) => hasReactivityInStatement(stmt));
4002
+ } else {
4003
+ hasReactivity = hasReactivityInExpression(functionNode.body);
4004
+ }
4005
+ if (hasReactivity) {
4006
+ const capitalizedName = `${functionName.charAt(0).toUpperCase()}${functionName.slice(1)}`;
4007
+ const suggestedName = `use${capitalizedName}`;
4008
+ context.report({
4009
+ node: idNode,
4010
+ messageId: "requirePrefix",
4011
+ data: { name: functionName, Name: capitalizedName },
4012
+ suggest: [
4013
+ {
4014
+ messageId: "requirePrefix",
4015
+ data: { name: functionName, Name: capitalizedName },
4016
+ fix(fixer) {
4017
+ return fixer.replaceText(idNode, suggestedName);
4018
+ }
4019
+ }
4020
+ ]
4021
+ });
4022
+ }
4023
+ }
4024
+ return {
4025
+ Program() {
4026
+ vueImports.clear();
4027
+ nonVueImports.clear();
4028
+ candidateFunctions.clear();
4029
+ },
4030
+ ImportDeclaration(node) {
4031
+ trackVueImports(node, vueImports);
4032
+ trackNonVueImports(node, nonVueImports);
4033
+ },
4034
+ "Program:exit": function() {
4035
+ for (const [name, { node, idNode }] of candidateFunctions)
4036
+ checkFunctionForReactivity(node, idNode, name);
4037
+ },
4038
+ FunctionDeclaration(node) {
4039
+ if (!node.id || isComposableName(node.id.name) || isExcludedName(node.id.name))
4040
+ return;
4041
+ if (node.parent.type === "Program" || node.parent.type === "ExportNamedDeclaration")
4042
+ candidateFunctions.set(node.id.name, { node, idNode: node.id });
4043
+ },
4044
+ VariableDeclarator(node) {
4045
+ if (node.id.type !== "Identifier" || isComposableName(node.id.name) || isExcludedName(node.id.name)) {
4046
+ return;
4047
+ }
4048
+ if (node.init?.type !== "FunctionExpression" && node.init?.type !== "ArrowFunctionExpression")
4049
+ return;
4050
+ const varDecl = node.parent;
4051
+ if (varDecl?.type !== "VariableDeclaration")
4052
+ return;
4053
+ if (varDecl.parent?.type !== "Program" && varDecl.parent?.type !== "ExportNamedDeclaration")
4054
+ return;
4055
+ candidateFunctions.set(node.id.name, { node: node.init, idNode: node.id });
4056
+ }
4057
+ };
4058
+ }
4059
+ });
4060
+
3632
4061
  const plugin = {
3633
4062
  meta: {
3634
4063
  name: "harlanzw",
@@ -3648,9 +4077,11 @@ const plugin = {
3648
4077
  "link-require-href": linkRequireHref,
3649
4078
  "link-trailing-slash": linkTrailingSlash,
3650
4079
  "nuxt-await-navigate-to": nuxtAwaitNavigateTo,
4080
+ "nuxt-no-random": nuxtNoRandom,
3651
4081
  "nuxt-no-redundant-import-meta": nuxtNoRedundantImportMeta,
3652
4082
  "nuxt-no-side-effects-in-async-data-handler": nuxtNoSideEffectsInAsyncDataHandler,
3653
4083
  "nuxt-no-side-effects-in-setup": nuxtNoSideEffectsInSetup,
4084
+ "nuxt-no-unsafe-date": nuxtNoUnsafeDate,
3654
4085
  "nuxt-prefer-navigate-to-over-router-push-replace": nuxtPreferNavigateToOverRouterPushReplace,
3655
4086
  "nuxt-prefer-nuxt-link-over-router-link": nuxtPreferNuxtLinkOverRouterLink,
3656
4087
  "prompt-ambiguous-quantifier": promptAmbiguousQuantifier,
@@ -3679,7 +4110,8 @@ const plugin = {
3679
4110
  "vue-no-passing-refs-as-props": vueNoPassingRefsAsProps,
3680
4111
  "vue-no-reactive-destructuring": vueNoReactiveDestructuring,
3681
4112
  "vue-no-ref-access-in-templates": vueNoRefAccessInTemplates,
3682
- "vue-no-torefs-on-props": vueNoTorefsOnProps
4113
+ "vue-no-torefs-on-props": vueNoTorefsOnProps,
4114
+ "vue-require-composable-prefix": vueRequireComposablePrefix
3683
4115
  },
3684
4116
  configs: {}
3685
4117
  };
@@ -3776,9 +4208,11 @@ plugin.configs.nuxt = [
3776
4208
  plugins: { harlanzw: plugin },
3777
4209
  rules: {
3778
4210
  "harlanzw/nuxt-await-navigate-to": "error",
4211
+ "harlanzw/nuxt-no-random": "error",
3779
4212
  "harlanzw/nuxt-no-redundant-import-meta": "error",
3780
4213
  "harlanzw/nuxt-no-side-effects-in-async-data-handler": "error",
3781
4214
  "harlanzw/nuxt-no-side-effects-in-setup": "error",
4215
+ "harlanzw/nuxt-no-unsafe-date": "warn",
3782
4216
  "harlanzw/nuxt-prefer-navigate-to-over-router-push-replace": "warn",
3783
4217
  "harlanzw/nuxt-prefer-nuxt-link-over-router-link": "warn"
3784
4218
  }
@@ -3795,7 +4229,8 @@ plugin.configs.vue = [
3795
4229
  "harlanzw/vue-no-passing-refs-as-props": "error",
3796
4230
  "harlanzw/vue-no-reactive-destructuring": "error",
3797
4231
  "harlanzw/vue-no-ref-access-in-templates": "warn",
3798
- "harlanzw/vue-no-torefs-on-props": "warn"
4232
+ "harlanzw/vue-no-torefs-on-props": "warn",
4233
+ "harlanzw/vue-require-composable-prefix": "warn"
3799
4234
  }
3800
4235
  }
3801
4236
  ];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "eslint-plugin-harlanzw",
3
3
  "type": "module",
4
- "version": "0.3.0",
4
+ "version": "0.4.0",
5
5
  "description": "Harlan's opinionated ESLint rules",
6
6
  "author": "Harlan Wilton <harlan@harlanzw.com>",
7
7
  "license": "MIT",