@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.
- package/README.md +91 -91
- package/lib/analysis/cli.js.html +5406 -0
- package/lib/analysis/index.js.html +1 -1
- package/lib/cli.js +3290 -0
- package/lib/cli.js.map +1 -0
- package/lib/index.js +220 -29
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +30 -5
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +19 -19
- package/src/cache.ts +1 -1
- package/src/cli.ts +39 -28
- package/src/config/ignore.ts +23 -23
- package/src/config/loader.ts +8 -8
- package/src/config/presets.ts +11 -11
- package/src/index.ts +14 -12
- package/src/lint.ts +19 -25
- package/src/lsp/index.ts +225 -0
- package/src/reporter.ts +17 -17
- package/src/rules/accessibility/dialog-a11y.ts +10 -10
- package/src/rules/accessibility/overlay-a11y.ts +11 -11
- package/src/rules/accessibility/toast-a11y.ts +11 -11
- package/src/rules/architecture/dev-guard-warnings.ts +19 -19
- package/src/rules/architecture/no-circular-import.ts +16 -16
- package/src/rules/architecture/no-cross-layer-import.ts +35 -35
- package/src/rules/architecture/no-deep-import.ts +7 -7
- package/src/rules/architecture/no-error-without-prefix.ts +20 -20
- package/src/rules/form/no-submit-without-validation.ts +13 -13
- package/src/rules/form/no-unregistered-field.ts +12 -12
- package/src/rules/form/prefer-field-array.ts +11 -11
- package/src/rules/hooks/no-raw-addeventlistener.ts +9 -9
- package/src/rules/hooks/no-raw-localstorage.ts +11 -11
- package/src/rules/hooks/no-raw-setinterval.ts +11 -11
- package/src/rules/index.ts +60 -57
- package/src/rules/jsx/no-and-conditional.ts +8 -8
- package/src/rules/jsx/no-children-access.ts +12 -12
- package/src/rules/jsx/no-classname.ts +10 -10
- package/src/rules/jsx/no-htmlfor.ts +10 -10
- package/src/rules/jsx/no-index-as-by.ts +17 -17
- package/src/rules/jsx/no-map-in-jsx.ts +9 -9
- package/src/rules/jsx/no-missing-for-by.ts +9 -9
- package/src/rules/jsx/no-onchange.ts +12 -12
- package/src/rules/jsx/no-props-destructure.ts +11 -11
- package/src/rules/jsx/no-ternary-conditional.ts +8 -8
- package/src/rules/jsx/use-by-not-key.ts +12 -12
- package/src/rules/lifecycle/no-dom-in-setup.ts +18 -18
- package/src/rules/lifecycle/no-effect-in-mount.ts +11 -11
- package/src/rules/lifecycle/no-missing-cleanup.ts +19 -19
- package/src/rules/lifecycle/no-mount-in-effect.ts +11 -11
- package/src/rules/performance/no-eager-import.ts +7 -7
- package/src/rules/performance/no-effect-in-for.ts +10 -10
- package/src/rules/performance/no-large-for-without-by.ts +9 -9
- package/src/rules/performance/prefer-show-over-display.ts +16 -16
- package/src/rules/reactivity/no-bare-signal-in-jsx.ts +10 -10
- package/src/rules/reactivity/no-context-destructure.ts +45 -0
- package/src/rules/reactivity/no-effect-assignment.ts +16 -16
- package/src/rules/reactivity/no-nested-effect.ts +10 -10
- package/src/rules/reactivity/no-peek-in-tracked.ts +10 -10
- package/src/rules/reactivity/no-signal-in-loop.ts +13 -13
- package/src/rules/reactivity/no-signal-leak.ts +9 -9
- package/src/rules/reactivity/no-unbatched-updates.ts +12 -12
- package/src/rules/reactivity/prefer-computed.ts +13 -13
- package/src/rules/router/index.ts +4 -4
- package/src/rules/router/no-href-navigation.ts +14 -14
- package/src/rules/router/no-imperative-navigate-in-render.ts +19 -19
- package/src/rules/router/no-missing-fallback.ts +16 -16
- package/src/rules/router/prefer-use-is-active.ts +11 -11
- package/src/rules/ssr/no-mismatch-risk.ts +11 -11
- package/src/rules/ssr/no-window-in-ssr.ts +22 -22
- package/src/rules/ssr/prefer-request-context.ts +14 -14
- package/src/rules/store/no-duplicate-store-id.ts +9 -9
- package/src/rules/store/no-mutate-store-state.ts +11 -11
- package/src/rules/store/no-store-outside-provider.ts +15 -15
- package/src/rules/styling/no-dynamic-styled.ts +13 -13
- package/src/rules/styling/no-inline-style-object.ts +10 -10
- package/src/rules/styling/no-theme-outside-provider.ts +11 -11
- package/src/rules/styling/prefer-cx.ts +12 -12
- package/src/runner.ts +13 -14
- package/src/tests/lsp.test.ts +88 -0
- package/src/tests/runner.test.ts +325 -325
- package/src/types.ts +15 -15
- package/src/utils/ast.ts +50 -50
- package/src/utils/imports.ts +53 -53
- package/src/utils/index.ts +12 -3
- package/src/utils/source.ts +2 -2
- 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/
|
|
2529
|
-
|
|
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
|
|
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
|
|
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/
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
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
|