@pyreon/lint 0.12.0 → 0.12.2

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/index.js CHANGED
@@ -1745,6 +1745,48 @@ const noSignalInLoop = {
1745
1745
  }
1746
1746
  };
1747
1747
 
1748
+ //#endregion
1749
+ //#region src/rules/reactivity/no-signal-in-props.ts
1750
+ function isComponentTag(name) {
1751
+ return name.length > 0 && name[0] === name[0]?.toUpperCase() && name[0] !== name[0]?.toLowerCase();
1752
+ }
1753
+ /**
1754
+ * Warn when a known signal/computed is called in a component prop position.
1755
+ * Component props are evaluated once at mount — signal reads are NOT reactive
1756
+ * unless the compiler wraps them with _rp(). The compiler handles this
1757
+ * automatically, but this rule catches manual h() calls and educates developers.
1758
+ */
1759
+ const noSignalInProps = {
1760
+ meta: {
1761
+ id: "pyreon/no-signal-in-props",
1762
+ category: "reactivity",
1763
+ description: "Signal call in component prop — value captured once unless compiler wraps it. Use props.x pattern for reactivity.",
1764
+ severity: "warn",
1765
+ fixable: false
1766
+ },
1767
+ create(context) {
1768
+ return { JSXExpressionContainer(node) {
1769
+ const expr = node.expression;
1770
+ if (!expr || expr.type !== "CallExpression") return;
1771
+ const callee = expr.callee;
1772
+ if (!callee || callee.type !== "Identifier") return;
1773
+ const source = context.getSourceText();
1774
+ let i = node.start - 1;
1775
+ while (i >= 0 && source[i] !== "<" && source[i] !== ">") i--;
1776
+ if (i < 0 || source[i] !== "<") return;
1777
+ const tagStart = i + 1;
1778
+ let tagEnd = tagStart;
1779
+ while (tagEnd < source.length && /[\w.]/.test(source[tagEnd] ?? "")) tagEnd++;
1780
+ const tagName = source.slice(tagStart, tagEnd);
1781
+ if (!tagName || !isComponentTag(tagName)) return;
1782
+ context.report({
1783
+ message: `Signal call in <${tagName}> prop — use props.x pattern inside the component for reactive access.`,
1784
+ span: getSpan(expr)
1785
+ });
1786
+ } };
1787
+ }
1788
+ };
1789
+
1748
1790
  //#endregion
1749
1791
  //#region src/rules/reactivity/no-signal-leak.ts
1750
1792
  const noSignalLeak = {
@@ -2463,6 +2505,7 @@ const allRules = [
2463
2505
  noBareSignalInJsx,
2464
2506
  noContextDestructure,
2465
2507
  noSignalInLoop,
2508
+ noSignalInProps,
2466
2509
  noNestedEffect,
2467
2510
  noPeekInTracked,
2468
2511
  noUnbatchedUpdates,
@@ -2704,10 +2747,19 @@ function lintFile(filePath, sourceText, rules, config, cache) {
2704
2747
  allCallbacks.push(rule.create(ctx));
2705
2748
  }
2706
2749
  new Visitor(mergeCallbacks(allCallbacks)).visit(program);
2707
- diagnostics.sort((a, b) => a.span.start - b.span.start);
2750
+ const lines = sourceText.split("\n");
2751
+ const filtered = diagnostics.filter((d) => {
2752
+ const prevLineIdx = d.loc.line - 2;
2753
+ if (prevLineIdx < 0) return true;
2754
+ const prevLine = lines[prevLineIdx]?.trim();
2755
+ if (!prevLine?.startsWith("// pyreon-lint-ignore")) return true;
2756
+ const rest = prevLine.slice(21).trim();
2757
+ return rest.length > 0 && rest !== d.ruleId;
2758
+ });
2759
+ filtered.sort((a, b) => a.span.start - b.span.start);
2708
2760
  return {
2709
2761
  filePath,
2710
- diagnostics
2762
+ diagnostics: filtered
2711
2763
  };
2712
2764
  }
2713
2765
  /**