@pyreon/lint 0.11.10 → 0.12.1
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/lib/analysis/cli.js.html +1 -1
- package/lib/analysis/index.js.html +1 -1
- package/lib/cli.js +96 -11
- package/lib/cli.js.map +1 -1
- package/lib/index.js +96 -11
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/rules/index.ts +4 -1
- package/src/rules/jsx/no-props-destructure.ts +50 -9
- package/src/rules/reactivity/no-signal-in-props.ts +53 -0
- package/src/runner.ts +15 -2
- package/src/tests/runner.test.ts +3 -3
package/lib/cli.js
CHANGED
|
@@ -1118,39 +1118,72 @@ function containsJSXReturn(node) {
|
|
|
1118
1118
|
}
|
|
1119
1119
|
return false;
|
|
1120
1120
|
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Extract destructured property names from an ObjectPattern.
|
|
1123
|
+
* Returns names for the fix suggestion.
|
|
1124
|
+
*/
|
|
1125
|
+
function getDestructuredNames(pattern) {
|
|
1126
|
+
if (pattern.type !== "ObjectPattern") return [];
|
|
1127
|
+
const names = [];
|
|
1128
|
+
for (const prop of pattern.properties ?? []) if (prop.type === "ObjectProperty" && prop.key?.type === "Identifier") names.push(prop.key.name);
|
|
1129
|
+
return names;
|
|
1130
|
+
}
|
|
1121
1131
|
const noPropsDestructure = {
|
|
1122
1132
|
meta: {
|
|
1123
1133
|
id: "pyreon/no-props-destructure",
|
|
1124
1134
|
category: "jsx",
|
|
1125
|
-
description: "Disallow destructuring props in component functions —
|
|
1135
|
+
description: "Disallow destructuring props in component functions — breaks reactive prop tracking. Use props.x or splitProps().",
|
|
1126
1136
|
severity: "error",
|
|
1127
1137
|
fixable: false
|
|
1128
1138
|
},
|
|
1129
1139
|
create(context) {
|
|
1140
|
+
let functionDepth = 0;
|
|
1130
1141
|
return {
|
|
1131
1142
|
ArrowFunctionExpression(node) {
|
|
1132
|
-
|
|
1143
|
+
functionDepth++;
|
|
1144
|
+
checkFunction(node, context, functionDepth);
|
|
1145
|
+
},
|
|
1146
|
+
"ArrowFunctionExpression:exit"() {
|
|
1147
|
+
functionDepth--;
|
|
1133
1148
|
},
|
|
1134
1149
|
FunctionDeclaration(node) {
|
|
1135
|
-
|
|
1150
|
+
functionDepth++;
|
|
1151
|
+
checkFunction(node, context, functionDepth);
|
|
1152
|
+
},
|
|
1153
|
+
"FunctionDeclaration:exit"() {
|
|
1154
|
+
functionDepth--;
|
|
1136
1155
|
},
|
|
1137
1156
|
FunctionExpression(node) {
|
|
1138
|
-
|
|
1157
|
+
functionDepth++;
|
|
1158
|
+
checkFunction(node, context, functionDepth);
|
|
1159
|
+
},
|
|
1160
|
+
"FunctionExpression:exit"() {
|
|
1161
|
+
functionDepth--;
|
|
1139
1162
|
}
|
|
1140
1163
|
};
|
|
1141
1164
|
}
|
|
1142
1165
|
};
|
|
1143
|
-
function checkFunction(node, context) {
|
|
1166
|
+
function checkFunction(node, context, depth) {
|
|
1144
1167
|
const params = node.params;
|
|
1145
1168
|
if (!params || params.length === 0) return;
|
|
1146
1169
|
const firstParam = params[0];
|
|
1147
1170
|
if (!isDestructuring(firstParam)) return;
|
|
1171
|
+
if (depth > 1) return;
|
|
1148
1172
|
const body = node.body;
|
|
1149
1173
|
if (!body) return;
|
|
1150
|
-
if (containsJSXReturn(body))
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1174
|
+
if (containsJSXReturn(body)) {
|
|
1175
|
+
const names = getDestructuredNames(firstParam);
|
|
1176
|
+
const hasRest = (firstParam.properties ?? []).some((p) => p.type === "RestElement");
|
|
1177
|
+
let suggestion = "Use `props.x` pattern for reactive prop access.";
|
|
1178
|
+
if (names.length > 0) {
|
|
1179
|
+
suggestion = `Use \`props\` parameter and access as ${names.map((n) => `props.${n}`).join(", ")}.`;
|
|
1180
|
+
if (hasRest) suggestion += ` For rest props, use \`splitProps(props, [${names.map((n) => `'${n}'`).join(", ")}])\`.`;
|
|
1181
|
+
}
|
|
1182
|
+
context.report({
|
|
1183
|
+
message: `Destructured props in component function — breaks reactive prop tracking. ${suggestion}`,
|
|
1184
|
+
span: getSpan(firstParam)
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1154
1187
|
}
|
|
1155
1188
|
|
|
1156
1189
|
//#endregion
|
|
@@ -1700,6 +1733,48 @@ const noSignalInLoop = {
|
|
|
1700
1733
|
}
|
|
1701
1734
|
};
|
|
1702
1735
|
|
|
1736
|
+
//#endregion
|
|
1737
|
+
//#region src/rules/reactivity/no-signal-in-props.ts
|
|
1738
|
+
function isComponentTag(name) {
|
|
1739
|
+
return name.length > 0 && name[0] === name[0]?.toUpperCase() && name[0] !== name[0]?.toLowerCase();
|
|
1740
|
+
}
|
|
1741
|
+
/**
|
|
1742
|
+
* Warn when a known signal/computed is called in a component prop position.
|
|
1743
|
+
* Component props are evaluated once at mount — signal reads are NOT reactive
|
|
1744
|
+
* unless the compiler wraps them with _rp(). The compiler handles this
|
|
1745
|
+
* automatically, but this rule catches manual h() calls and educates developers.
|
|
1746
|
+
*/
|
|
1747
|
+
const noSignalInProps = {
|
|
1748
|
+
meta: {
|
|
1749
|
+
id: "pyreon/no-signal-in-props",
|
|
1750
|
+
category: "reactivity",
|
|
1751
|
+
description: "Signal call in component prop — value captured once unless compiler wraps it. Use props.x pattern for reactivity.",
|
|
1752
|
+
severity: "warn",
|
|
1753
|
+
fixable: false
|
|
1754
|
+
},
|
|
1755
|
+
create(context) {
|
|
1756
|
+
return { JSXExpressionContainer(node) {
|
|
1757
|
+
const expr = node.expression;
|
|
1758
|
+
if (!expr || expr.type !== "CallExpression") return;
|
|
1759
|
+
const callee = expr.callee;
|
|
1760
|
+
if (!callee || callee.type !== "Identifier") return;
|
|
1761
|
+
const source = context.getSourceText();
|
|
1762
|
+
let i = node.start - 1;
|
|
1763
|
+
while (i >= 0 && source[i] !== "<" && source[i] !== ">") i--;
|
|
1764
|
+
if (i < 0 || source[i] !== "<") return;
|
|
1765
|
+
const tagStart = i + 1;
|
|
1766
|
+
let tagEnd = tagStart;
|
|
1767
|
+
while (tagEnd < source.length && /[\w.]/.test(source[tagEnd] ?? "")) tagEnd++;
|
|
1768
|
+
const tagName = source.slice(tagStart, tagEnd);
|
|
1769
|
+
if (!tagName || !isComponentTag(tagName)) return;
|
|
1770
|
+
context.report({
|
|
1771
|
+
message: `Signal call in <${tagName}> prop — use props.x pattern inside the component for reactive access.`,
|
|
1772
|
+
span: getSpan(expr)
|
|
1773
|
+
});
|
|
1774
|
+
} };
|
|
1775
|
+
}
|
|
1776
|
+
};
|
|
1777
|
+
|
|
1703
1778
|
//#endregion
|
|
1704
1779
|
//#region src/rules/reactivity/no-signal-leak.ts
|
|
1705
1780
|
const noSignalLeak = {
|
|
@@ -2418,6 +2493,7 @@ const allRules = [
|
|
|
2418
2493
|
noBareSignalInJsx,
|
|
2419
2494
|
noContextDestructure,
|
|
2420
2495
|
noSignalInLoop,
|
|
2496
|
+
noSignalInProps,
|
|
2421
2497
|
noNestedEffect,
|
|
2422
2498
|
noPeekInTracked,
|
|
2423
2499
|
noUnbatchedUpdates,
|
|
@@ -2659,10 +2735,19 @@ function lintFile(filePath, sourceText, rules, config, cache) {
|
|
|
2659
2735
|
allCallbacks.push(rule.create(ctx));
|
|
2660
2736
|
}
|
|
2661
2737
|
new Visitor(mergeCallbacks(allCallbacks)).visit(program);
|
|
2662
|
-
|
|
2738
|
+
const lines = sourceText.split("\n");
|
|
2739
|
+
const filtered = diagnostics.filter((d) => {
|
|
2740
|
+
const prevLineIdx = d.loc.line - 2;
|
|
2741
|
+
if (prevLineIdx < 0) return true;
|
|
2742
|
+
const prevLine = lines[prevLineIdx]?.trim();
|
|
2743
|
+
if (!prevLine?.startsWith("// pyreon-lint-ignore")) return true;
|
|
2744
|
+
const rest = prevLine.slice(21).trim();
|
|
2745
|
+
return rest.length > 0 && rest !== d.ruleId;
|
|
2746
|
+
});
|
|
2747
|
+
filtered.sort((a, b) => a.span.start - b.span.start);
|
|
2663
2748
|
return {
|
|
2664
2749
|
filePath,
|
|
2665
|
-
diagnostics
|
|
2750
|
+
diagnostics: filtered
|
|
2666
2751
|
};
|
|
2667
2752
|
}
|
|
2668
2753
|
/**
|