@pyreon/lint 0.11.4 → 0.11.6

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 (86) hide show
  1. package/README.md +91 -91
  2. package/lib/analysis/cli.js.html +5406 -0
  3. package/lib/analysis/index.js.html +1 -1
  4. package/lib/cli.js +3290 -0
  5. package/lib/cli.js.map +1 -0
  6. package/lib/index.js +220 -29
  7. package/lib/index.js.map +1 -1
  8. package/lib/types/index.d.ts +30 -5
  9. package/lib/types/index.d.ts.map +1 -1
  10. package/package.json +19 -19
  11. package/src/cache.ts +1 -1
  12. package/src/cli.ts +39 -28
  13. package/src/config/ignore.ts +23 -23
  14. package/src/config/loader.ts +8 -8
  15. package/src/config/presets.ts +11 -11
  16. package/src/index.ts +14 -12
  17. package/src/lint.ts +19 -25
  18. package/src/lsp/index.ts +225 -0
  19. package/src/reporter.ts +17 -17
  20. package/src/rules/accessibility/dialog-a11y.ts +10 -10
  21. package/src/rules/accessibility/overlay-a11y.ts +11 -11
  22. package/src/rules/accessibility/toast-a11y.ts +11 -11
  23. package/src/rules/architecture/dev-guard-warnings.ts +19 -19
  24. package/src/rules/architecture/no-circular-import.ts +16 -16
  25. package/src/rules/architecture/no-cross-layer-import.ts +35 -35
  26. package/src/rules/architecture/no-deep-import.ts +7 -7
  27. package/src/rules/architecture/no-error-without-prefix.ts +20 -20
  28. package/src/rules/form/no-submit-without-validation.ts +13 -13
  29. package/src/rules/form/no-unregistered-field.ts +12 -12
  30. package/src/rules/form/prefer-field-array.ts +11 -11
  31. package/src/rules/hooks/no-raw-addeventlistener.ts +9 -9
  32. package/src/rules/hooks/no-raw-localstorage.ts +11 -11
  33. package/src/rules/hooks/no-raw-setinterval.ts +11 -11
  34. package/src/rules/index.ts +60 -57
  35. package/src/rules/jsx/no-and-conditional.ts +8 -8
  36. package/src/rules/jsx/no-children-access.ts +12 -12
  37. package/src/rules/jsx/no-classname.ts +10 -10
  38. package/src/rules/jsx/no-htmlfor.ts +10 -10
  39. package/src/rules/jsx/no-index-as-by.ts +17 -17
  40. package/src/rules/jsx/no-map-in-jsx.ts +9 -9
  41. package/src/rules/jsx/no-missing-for-by.ts +9 -9
  42. package/src/rules/jsx/no-onchange.ts +12 -12
  43. package/src/rules/jsx/no-props-destructure.ts +11 -11
  44. package/src/rules/jsx/no-ternary-conditional.ts +8 -8
  45. package/src/rules/jsx/use-by-not-key.ts +12 -12
  46. package/src/rules/lifecycle/no-dom-in-setup.ts +18 -18
  47. package/src/rules/lifecycle/no-effect-in-mount.ts +11 -11
  48. package/src/rules/lifecycle/no-missing-cleanup.ts +19 -19
  49. package/src/rules/lifecycle/no-mount-in-effect.ts +11 -11
  50. package/src/rules/performance/no-eager-import.ts +7 -7
  51. package/src/rules/performance/no-effect-in-for.ts +10 -10
  52. package/src/rules/performance/no-large-for-without-by.ts +9 -9
  53. package/src/rules/performance/prefer-show-over-display.ts +16 -16
  54. package/src/rules/reactivity/no-bare-signal-in-jsx.ts +10 -10
  55. package/src/rules/reactivity/no-context-destructure.ts +45 -0
  56. package/src/rules/reactivity/no-effect-assignment.ts +16 -16
  57. package/src/rules/reactivity/no-nested-effect.ts +10 -10
  58. package/src/rules/reactivity/no-peek-in-tracked.ts +10 -10
  59. package/src/rules/reactivity/no-signal-in-loop.ts +13 -13
  60. package/src/rules/reactivity/no-signal-leak.ts +9 -9
  61. package/src/rules/reactivity/no-unbatched-updates.ts +12 -12
  62. package/src/rules/reactivity/prefer-computed.ts +13 -13
  63. package/src/rules/router/index.ts +4 -4
  64. package/src/rules/router/no-href-navigation.ts +14 -14
  65. package/src/rules/router/no-imperative-navigate-in-render.ts +19 -19
  66. package/src/rules/router/no-missing-fallback.ts +16 -16
  67. package/src/rules/router/prefer-use-is-active.ts +11 -11
  68. package/src/rules/ssr/no-mismatch-risk.ts +11 -11
  69. package/src/rules/ssr/no-window-in-ssr.ts +22 -22
  70. package/src/rules/ssr/prefer-request-context.ts +14 -14
  71. package/src/rules/store/no-duplicate-store-id.ts +9 -9
  72. package/src/rules/store/no-mutate-store-state.ts +11 -11
  73. package/src/rules/store/no-store-outside-provider.ts +15 -15
  74. package/src/rules/styling/no-dynamic-styled.ts +13 -13
  75. package/src/rules/styling/no-inline-style-object.ts +10 -10
  76. package/src/rules/styling/no-theme-outside-provider.ts +11 -11
  77. package/src/rules/styling/prefer-cx.ts +12 -12
  78. package/src/runner.ts +13 -14
  79. package/src/tests/lsp.test.ts +88 -0
  80. package/src/tests/runner.test.ts +325 -325
  81. package/src/types.ts +15 -15
  82. package/src/utils/ast.ts +50 -50
  83. package/src/utils/imports.ts +53 -53
  84. package/src/utils/index.ts +12 -3
  85. package/src/utils/source.ts +2 -2
  86. package/src/watcher.ts +19 -25
package/lib/index.js CHANGED
@@ -1524,6 +1524,39 @@ const noBareSignalInJsx = {
1524
1524
  }
1525
1525
  };
1526
1526
 
1527
+ //#endregion
1528
+ //#region src/rules/reactivity/no-context-destructure.ts
1529
+ /**
1530
+ * Detects destructuring the return value of useContext().
1531
+ *
1532
+ * `const { mode } = useContext(ctx)` loses reactivity when the context
1533
+ * provides getter properties. The value is captured once at setup time.
1534
+ *
1535
+ * Correct: `const ctx = useContext(Ctx)` then read `ctx.mode` lazily.
1536
+ */
1537
+ const noContextDestructure = {
1538
+ meta: {
1539
+ id: "pyreon/no-context-destructure",
1540
+ category: "reactivity",
1541
+ description: "Disallow destructuring useContext() — it breaks reactivity when context provides getters.",
1542
+ severity: "warn",
1543
+ fixable: false
1544
+ },
1545
+ create(context) {
1546
+ return { VariableDeclarator(node) {
1547
+ const id = node.id;
1548
+ const init = node.init;
1549
+ if (!id || !init) return;
1550
+ if (id.type !== "ObjectPattern") return;
1551
+ if (init.type !== "CallExpression" || init.callee?.type !== "Identifier" || init.callee.name !== "useContext") return;
1552
+ context.report({
1553
+ message: "Destructuring useContext() captures values once — reactive getters lose reactivity. Keep the object reference: `const ctx = useContext(Ctx)` and access `ctx.mode` lazily.",
1554
+ span: getSpan(id)
1555
+ });
1556
+ } };
1557
+ }
1558
+ };
1559
+
1527
1560
  //#endregion
1528
1561
  //#region src/rules/reactivity/no-effect-assignment.ts
1529
1562
  function isUpdateCall(node) {
@@ -2395,6 +2428,7 @@ const preferCx = {
2395
2428
  //#region src/rules/index.ts
2396
2429
  const allRules = [
2397
2430
  noBareSignalInJsx,
2431
+ noContextDestructure,
2398
2432
  noSignalInLoop,
2399
2433
  noNestedEffect,
2400
2434
  noPeekInTracked,
@@ -2525,8 +2559,9 @@ var LineIndex = class {
2525
2559
  };
2526
2560
 
2527
2561
  //#endregion
2528
- //#region src/runner.ts
2529
- const JS_EXTENSIONS$2 = new Set([
2562
+ //#region src/utils/index.ts
2563
+ /** Supported JS/TS file extensions for linting. */
2564
+ const JS_EXTENSIONS = new Set([
2530
2565
  ".ts",
2531
2566
  ".tsx",
2532
2567
  ".js",
@@ -2534,6 +2569,14 @@ const JS_EXTENSIONS$2 = new Set([
2534
2569
  ".mts",
2535
2570
  ".mjs"
2536
2571
  ]);
2572
+ /** Check if a file path has a supported JS/TS extension. */
2573
+ function hasJsExtension(filePath) {
2574
+ const ext = filePath.slice(filePath.lastIndexOf("."));
2575
+ return JS_EXTENSIONS.has(ext);
2576
+ }
2577
+
2578
+ //#endregion
2579
+ //#region src/runner.ts
2537
2580
  function getExtension(filePath) {
2538
2581
  const lastDot = filePath.lastIndexOf(".");
2539
2582
  return lastDot === -1 ? "" : filePath.slice(lastDot);
@@ -2591,7 +2634,7 @@ function mergeCallbacks(allCallbacks) {
2591
2634
  */
2592
2635
  function lintFile(filePath, sourceText, rules, config, cache) {
2593
2636
  const ext = getExtension(filePath);
2594
- if (!JS_EXTENSIONS$2.has(ext)) return {
2637
+ if (!JS_EXTENSIONS.has(ext)) return {
2595
2638
  filePath,
2596
2639
  diagnostics: []
2597
2640
  };
@@ -2658,21 +2701,9 @@ function applyFixes(sourceText, diagnostics) {
2658
2701
 
2659
2702
  //#endregion
2660
2703
  //#region src/lint.ts
2661
- const JS_EXTENSIONS$1 = new Set([
2662
- ".ts",
2663
- ".tsx",
2664
- ".js",
2665
- ".jsx",
2666
- ".mts",
2667
- ".mjs"
2668
- ]);
2669
2704
  function isHiddenOrVendor(entry) {
2670
2705
  return entry.startsWith(".") || entry === "node_modules" || entry === "lib" || entry === "dist";
2671
2706
  }
2672
- function hasJsExtension$1(filePath) {
2673
- const ext = filePath.slice(filePath.lastIndexOf("."));
2674
- return JS_EXTENSIONS$1.has(ext);
2675
- }
2676
2707
  function matchesPatterns(filePath, include, exclude) {
2677
2708
  if (exclude) {
2678
2709
  for (const pattern of exclude) if (filePath.includes(pattern)) return false;
@@ -2705,7 +2736,7 @@ function processEntry(full, files, isIgnored, include, exclude) {
2705
2736
  return;
2706
2737
  }
2707
2738
  if (stat.isDirectory()) walkDirectory(full, files, isIgnored, include, exclude);
2708
- else if (stat.isFile() && hasJsExtension$1(full) && matchesPatterns(full, include, exclude)) files.push(full);
2739
+ else if (stat.isFile() && hasJsExtension(full) && matchesPatterns(full, include, exclude)) files.push(full);
2709
2740
  }
2710
2741
  function collectFiles(dir, isIgnored, include, exclude) {
2711
2742
  const files = [];
@@ -2867,19 +2898,179 @@ function formatCompact(result) {
2867
2898
  }
2868
2899
 
2869
2900
  //#endregion
2870
- //#region src/watcher.ts
2871
- const JS_EXTENSIONS = new Set([
2872
- ".ts",
2873
- ".tsx",
2874
- ".js",
2875
- ".jsx",
2876
- ".mts",
2877
- ".mjs"
2878
- ]);
2879
- function hasJsExtension(filePath) {
2880
- const ext = filePath.slice(filePath.lastIndexOf("."));
2881
- return JS_EXTENSIONS.has(ext);
2901
+ //#region src/lsp/index.ts
2902
+ /**
2903
+ * Minimal LSP server for @pyreon/lint.
2904
+ *
2905
+ * Provides real-time Pyreon-specific diagnostics in editors that support
2906
+ * the Language Server Protocol (VS Code, Neovim, etc.).
2907
+ *
2908
+ * Usage: pyreon-lint --lsp
2909
+ *
2910
+ * The server communicates via JSON-RPC over stdin/stdout following the
2911
+ * LSP specification (https://microsoft.github.io/language-server-protocol/).
2912
+ *
2913
+ * Supported capabilities:
2914
+ * - textDocument/didOpen — lint on open
2915
+ * - textDocument/didSave — lint on save
2916
+ * - textDocument/didChange — lint on change (debounced)
2917
+ *
2918
+ * @module
2919
+ */
2920
+ const cache = new AstCache();
2921
+ const config = getPreset("recommended");
2922
+ function toLspDiagnostics(diagnostics) {
2923
+ return diagnostics.map((d) => ({
2924
+ range: {
2925
+ start: {
2926
+ line: d.loc.line - 1,
2927
+ character: d.loc.column - 1
2928
+ },
2929
+ end: {
2930
+ line: d.loc.line - 1,
2931
+ character: d.loc.column - 1 + (d.span.end - d.span.start)
2932
+ }
2933
+ },
2934
+ severity: d.severity === "error" ? 1 : d.severity === "warn" ? 2 : 3,
2935
+ source: "pyreon-lint",
2936
+ message: d.message,
2937
+ code: d.ruleId
2938
+ }));
2939
+ }
2940
+ function lintDocument(uri, text) {
2941
+ try {
2942
+ return toLspDiagnostics(lintFile(uri.replace("file://", ""), text, allRules, config, cache).diagnostics);
2943
+ } catch {
2944
+ return [];
2945
+ }
2946
+ }
2947
+ const DEBOUNCE_MS = 150;
2948
+ const debounceTimers = /* @__PURE__ */ new Map();
2949
+ function debounceLint(uri, text) {
2950
+ const existing = debounceTimers.get(uri);
2951
+ if (existing) clearTimeout(existing);
2952
+ debounceTimers.set(uri, setTimeout(() => {
2953
+ debounceTimers.delete(uri);
2954
+ sendNotification("textDocument/publishDiagnostics", {
2955
+ uri,
2956
+ diagnostics: lintDocument(uri, text)
2957
+ });
2958
+ }, DEBOUNCE_MS));
2959
+ }
2960
+ const openDocuments = /* @__PURE__ */ new Map();
2961
+ function handleMessage(msg) {
2962
+ if (msg.method === "initialize") return {
2963
+ jsonrpc: "2.0",
2964
+ id: msg.id,
2965
+ result: {
2966
+ capabilities: {
2967
+ textDocumentSync: 1,
2968
+ diagnosticProvider: {
2969
+ interFileDependencies: false,
2970
+ workspaceDiagnostics: false
2971
+ }
2972
+ },
2973
+ serverInfo: {
2974
+ name: "pyreon-lint",
2975
+ version: "0.11.5"
2976
+ }
2977
+ }
2978
+ };
2979
+ if (msg.method === "initialized") return null;
2980
+ if (msg.method === "textDocument/didOpen") {
2981
+ const { uri, text } = msg.params.textDocument;
2982
+ openDocuments.set(uri, text);
2983
+ sendNotification("textDocument/publishDiagnostics", {
2984
+ uri,
2985
+ diagnostics: lintDocument(uri, text)
2986
+ });
2987
+ return null;
2988
+ }
2989
+ if (msg.method === "textDocument/didChange") {
2990
+ const uri = msg.params.textDocument.uri;
2991
+ const text = msg.params.contentChanges[0]?.text;
2992
+ if (text != null) {
2993
+ openDocuments.set(uri, text);
2994
+ debounceLint(uri, text);
2995
+ }
2996
+ return null;
2997
+ }
2998
+ if (msg.method === "textDocument/didSave") {
2999
+ const uri = msg.params.textDocument.uri;
3000
+ const text = openDocuments.get(uri);
3001
+ if (text) sendNotification("textDocument/publishDiagnostics", {
3002
+ uri,
3003
+ diagnostics: lintDocument(uri, text)
3004
+ });
3005
+ return null;
3006
+ }
3007
+ if (msg.method === "textDocument/didClose") {
3008
+ const uri = msg.params.textDocument.uri;
3009
+ openDocuments.delete(uri);
3010
+ sendNotification("textDocument/publishDiagnostics", {
3011
+ uri,
3012
+ diagnostics: []
3013
+ });
3014
+ return null;
3015
+ }
3016
+ if (msg.method === "shutdown") return {
3017
+ jsonrpc: "2.0",
3018
+ id: msg.id,
3019
+ result: null
3020
+ };
3021
+ if (msg.method === "exit") process.exit(0);
3022
+ if (msg.id != null) return {
3023
+ jsonrpc: "2.0",
3024
+ id: msg.id,
3025
+ result: null
3026
+ };
3027
+ return null;
3028
+ }
3029
+ function sendMessage(msg) {
3030
+ const body = JSON.stringify(msg);
3031
+ const header = `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n`;
3032
+ process.stdout.write(header + body);
2882
3033
  }
3034
+ function sendNotification(method, params) {
3035
+ sendMessage({
3036
+ jsonrpc: "2.0",
3037
+ method,
3038
+ params
3039
+ });
3040
+ }
3041
+ /**
3042
+ * Start the LSP server. Reads JSON-RPC messages from stdin,
3043
+ * processes them, and writes responses to stdout.
3044
+ */
3045
+ function startLspServer() {
3046
+ let buffer = "";
3047
+ process.stdin.setEncoding("utf-8");
3048
+ process.stdin.on("data", (chunk) => {
3049
+ buffer += chunk;
3050
+ while (true) {
3051
+ const headerEnd = buffer.indexOf("\r\n\r\n");
3052
+ if (headerEnd === -1) break;
3053
+ const match = buffer.slice(0, headerEnd).match(/Content-Length:\s*(\d+)/i);
3054
+ if (!match) {
3055
+ buffer = buffer.slice(headerEnd + 4);
3056
+ continue;
3057
+ }
3058
+ const contentLength = Number.parseInt(match[1], 10);
3059
+ const bodyStart = headerEnd + 4;
3060
+ if (buffer.length < bodyStart + contentLength) break;
3061
+ const body = buffer.slice(bodyStart, bodyStart + contentLength);
3062
+ buffer = buffer.slice(bodyStart + contentLength);
3063
+ try {
3064
+ const response = handleMessage(JSON.parse(body));
3065
+ if (response) sendMessage(response);
3066
+ } catch {}
3067
+ }
3068
+ });
3069
+ process.stderr.write("[pyreon-lint] LSP server started\n");
3070
+ }
3071
+
3072
+ //#endregion
3073
+ //#region src/watcher.ts
2883
3074
  function formatOutput(result, format) {
2884
3075
  if (format === "json") return formatJSON(result);
2885
3076
  if (format === "compact") return formatCompact(result);
@@ -2951,5 +3142,5 @@ function relintFile(filePath, config, cache, format) {
2951
3142
  }
2952
3143
 
2953
3144
  //#endregion
2954
- export { AstCache, LineIndex, allRules, applyFixes, createIgnoreFilter, extractImportInfo, formatCompact, formatJSON, formatText, getLocalName, getPreset, importsName, isPyreonImport, isPyreonPackage, lint, lintFile, listRules, loadConfig, loadConfigFromPath, watchAndLint };
3145
+ export { AstCache, LineIndex, allRules, applyFixes, createIgnoreFilter, extractImportInfo, formatCompact, formatJSON, formatText, getLocalName, getPreset, importsName, isPyreonImport, isPyreonPackage, lint, lintFile, listRules, loadConfig, loadConfigFromPath, startLspServer, watchAndLint };
2955
3146
  //# sourceMappingURL=index.js.map