@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.
- package/README.md +91 -91
- package/lib/analysis/cli.js.html +1 -1
- package/lib/analysis/index.js.html +1 -1
- package/lib/cli.js +214 -1
- package/lib/cli.js.map +1 -1
- package/lib/index.js +207 -1
- 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 +15 -15
- package/src/cache.ts +1 -1
- package/src/cli.ts +38 -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 -19
- 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 -13
- 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 +5 -5
- package/src/utils/source.ts +2 -2
- 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
|