@pyreon/lint 0.11.5 → 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 +1 -1
  3. package/lib/analysis/index.js.html +1 -1
  4. package/lib/cli.js +214 -1
  5. package/lib/cli.js.map +1 -1
  6. package/lib/index.js +207 -1
  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 +15 -15
  11. package/src/cache.ts +1 -1
  12. package/src/cli.ts +38 -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 -19
  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 -13
  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 +5 -5
  85. package/src/utils/source.ts +2 -2
  86. package/src/watcher.ts +19 -19
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,
@@ -2863,6 +2897,178 @@ function formatCompact(result) {
2863
2897
  return lines.join("\n");
2864
2898
  }
2865
2899
 
2900
+ //#endregion
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);
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
+
2866
3072
  //#endregion
2867
3073
  //#region src/watcher.ts
2868
3074
  function formatOutput(result, format) {
@@ -2936,5 +3142,5 @@ function relintFile(filePath, config, cache, format) {
2936
3142
  }
2937
3143
 
2938
3144
  //#endregion
2939
- 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 };
2940
3146
  //# sourceMappingURL=index.js.map