@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.
@@ -1 +1 @@
1
- {"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/types.ts","../../../src/utils/source.ts","../../../src/cache.ts","../../../src/config/ignore.ts","../../../src/config/loader.ts","../../../src/config/presets.ts","../../../src/lint.ts","../../../src/reporter.ts","../../../src/lsp/index.ts","../../../src/rules/index.ts","../../../src/runner.ts","../../../src/utils/imports.ts","../../../src/watcher.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/types.ts","../../../src/utils/source.ts","../../../src/cache.ts","../../../src/config/ignore.ts","../../../src/config/loader.ts","../../../src/config/presets.ts","../../../src/lint.ts","../../../src/reporter.ts","../../../src/lsp/index.ts","../../../src/rules/index.ts","../../../src/runner.ts","../../../src/utils/imports.ts","../../../src/watcher.ts"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/lint",
3
- "version": "0.11.10",
3
+ "version": "0.12.1",
4
4
  "description": "Pyreon-specific linter — 56 rules for signals, JSX, SSR, performance, router, and architecture",
5
5
  "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/lint#readme",
6
6
  "bugs": {
@@ -46,6 +46,7 @@ import { noEffectAssignment } from './reactivity/no-effect-assignment'
46
46
  import { noNestedEffect } from './reactivity/no-nested-effect'
47
47
  import { noPeekInTracked } from './reactivity/no-peek-in-tracked'
48
48
  import { noSignalInLoop } from './reactivity/no-signal-in-loop'
49
+ import { noSignalInProps } from './reactivity/no-signal-in-props'
49
50
  import { noSignalLeak } from './reactivity/no-signal-leak'
50
51
  import { noUnbatchedUpdates } from './reactivity/no-unbatched-updates'
51
52
  import { preferComputed } from './reactivity/prefer-computed'
@@ -69,10 +70,11 @@ import { noThemeOutsideProvider } from './styling/no-theme-outside-provider'
69
70
  import { preferCx } from './styling/prefer-cx'
70
71
 
71
72
  export const allRules: Rule[] = [
72
- // Reactivity (9)
73
+ // Reactivity (10)
73
74
  noBareSignalInJsx,
74
75
  noContextDestructure,
75
76
  noSignalInLoop,
77
+ noSignalInProps,
76
78
  noNestedEffect,
77
79
  noPeekInTracked,
78
80
  noUnbatchedUpdates,
@@ -187,6 +189,7 @@ export {
187
189
  noRawLocalStorage,
188
190
  noRawSetInterval,
189
191
  noSignalInLoop,
192
+ noSignalInProps,
190
193
  noSignalLeak,
191
194
  // Store
192
195
  noStoreOutsideProvider,
@@ -3,11 +3,9 @@ import { getSpan, isDestructuring } from '../../utils/ast'
3
3
 
4
4
  function containsJSXReturn(node: any): boolean {
5
5
  if (!node) return false
6
- // Arrow with expression body returning JSX
7
6
  if (node.type === 'JSXElement' || node.type === 'JSXFragment') return true
8
7
  if (node.type === 'ParenthesizedExpression') return containsJSXReturn(node.expression)
9
8
 
10
- // Block body — look for return statements with JSX
11
9
  if (node.type === 'BlockStatement') {
12
10
  for (const stmt of node.body ?? []) {
13
11
  if (stmt.type === 'ReturnStatement' && containsJSXReturn(stmt.argument)) {
@@ -18,46 +16,89 @@ function containsJSXReturn(node: any): boolean {
18
16
  return false
19
17
  }
20
18
 
19
+ /**
20
+ * Extract destructured property names from an ObjectPattern.
21
+ * Returns names for the fix suggestion.
22
+ */
23
+ function getDestructuredNames(pattern: any): string[] {
24
+ if (pattern.type !== 'ObjectPattern') return []
25
+ const names: string[] = []
26
+ for (const prop of pattern.properties ?? []) {
27
+ if (prop.type === 'ObjectProperty' && prop.key?.type === 'Identifier') {
28
+ names.push(prop.key.name)
29
+ }
30
+ }
31
+ return names
32
+ }
33
+
21
34
  export const noPropsDestructure: Rule = {
22
35
  meta: {
23
36
  id: 'pyreon/no-props-destructure',
24
37
  category: 'jsx',
25
38
  description:
26
- 'Disallow destructuring props in component functions — it breaks signal reactivity.',
39
+ 'Disallow destructuring props in component functions — breaks reactive prop tracking. Use props.x or splitProps().',
27
40
  severity: 'error',
28
41
  fixable: false,
29
42
  },
30
43
  create(context) {
44
+ let functionDepth = 0
45
+
31
46
  const callbacks: VisitorCallbacks = {
32
47
  ArrowFunctionExpression(node: any) {
33
- checkFunction(node, context)
48
+ functionDepth++
49
+ checkFunction(node, context, functionDepth)
50
+ },
51
+ 'ArrowFunctionExpression:exit'() {
52
+ functionDepth--
34
53
  },
35
54
  FunctionDeclaration(node: any) {
36
- checkFunction(node, context)
55
+ functionDepth++
56
+ checkFunction(node, context, functionDepth)
57
+ },
58
+ 'FunctionDeclaration:exit'() {
59
+ functionDepth--
37
60
  },
38
61
  FunctionExpression(node: any) {
39
- checkFunction(node, context)
62
+ functionDepth++
63
+ checkFunction(node, context, functionDepth)
64
+ },
65
+ 'FunctionExpression:exit'() {
66
+ functionDepth--
40
67
  },
41
68
  }
42
69
  return callbacks
43
70
  },
44
71
  }
45
72
 
46
- function checkFunction(node: any, context: any) {
73
+ function checkFunction(node: any, context: any, depth: number) {
47
74
  const params = node.params
48
75
  if (!params || params.length === 0) return
49
76
 
50
77
  const firstParam = params[0]
51
78
  if (!isDestructuring(firstParam)) return
52
79
 
53
- // Check if this function returns JSX
80
+ // Skip HOC inner functions (depth > 1)
81
+ if (depth > 1) return
82
+
54
83
  const body = node.body
55
84
  if (!body) return
56
85
 
57
86
  if (containsJSXReturn(body)) {
87
+ const names = getDestructuredNames(firstParam)
88
+ const hasRest = (firstParam.properties ?? []).some((p: any) => p.type === 'RestElement')
89
+
90
+ let suggestion = 'Use `props.x` pattern for reactive prop access.'
91
+ if (names.length > 0) {
92
+ const propsAccess = names.map((n) => `props.${n}`).join(', ')
93
+ suggestion = `Use \`props\` parameter and access as ${propsAccess}.`
94
+ if (hasRest) {
95
+ suggestion += ` For rest props, use \`splitProps(props, [${names.map((n) => `'${n}'`).join(', ')}])\`.`
96
+ }
97
+ }
98
+
58
99
  context.report({
59
100
  message:
60
- 'Destructured props in a component function — this breaks signal reactivity. Use `props.x` or `splitProps()` instead.',
101
+ `Destructured props in component function — breaks reactive prop tracking. ${suggestion}`,
61
102
  span: getSpan(firstParam),
62
103
  })
63
104
  }
@@ -0,0 +1,53 @@
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getSpan } from '../../utils/ast'
3
+
4
+ function isComponentTag(name: string): boolean {
5
+ return name.length > 0 && name[0] === name[0]?.toUpperCase() && name[0] !== name[0]?.toLowerCase()
6
+ }
7
+
8
+ /**
9
+ * Warn when a known signal/computed is called in a component prop position.
10
+ * Component props are evaluated once at mount — signal reads are NOT reactive
11
+ * unless the compiler wraps them with _rp(). The compiler handles this
12
+ * automatically, but this rule catches manual h() calls and educates developers.
13
+ */
14
+ export const noSignalInProps: Rule = {
15
+ meta: {
16
+ id: 'pyreon/no-signal-in-props',
17
+ category: 'reactivity',
18
+ description:
19
+ 'Signal call in component prop — value captured once unless compiler wraps it. Use props.x pattern for reactivity.',
20
+ severity: 'warn',
21
+ fixable: false,
22
+ },
23
+ create(context) {
24
+ const callbacks: VisitorCallbacks = {
25
+ JSXExpressionContainer(node: any) {
26
+ const expr = node.expression
27
+ if (!expr || expr.type !== 'CallExpression') return
28
+ const callee = expr.callee
29
+ if (!callee || callee.type !== 'Identifier') return
30
+
31
+ const source = context.getSourceText()
32
+ const start = node.start as number
33
+
34
+ let i = start - 1
35
+ while (i >= 0 && source[i] !== '<' && source[i] !== '>') i--
36
+ if (i < 0 || source[i] !== '<') return
37
+
38
+ const tagStart = i + 1
39
+ let tagEnd = tagStart
40
+ while (tagEnd < source.length && /[\w.]/.test(source[tagEnd] ?? '')) tagEnd++
41
+ const tagName = source.slice(tagStart, tagEnd)
42
+
43
+ if (!tagName || !isComponentTag(tagName)) return
44
+
45
+ context.report({
46
+ message: `Signal call in <${tagName}> prop — use props.x pattern inside the component for reactive access.`,
47
+ span: getSpan(expr),
48
+ })
49
+ },
50
+ }
51
+ return callbacks
52
+ },
53
+ }
package/src/runner.ts CHANGED
@@ -138,8 +138,21 @@ export function lintFile(
138
138
  const visitor = new Visitor(mergeCallbacks(allCallbacks))
139
139
  visitor.visit(program)
140
140
 
141
- diagnostics.sort((a, b) => a.span.start - b.span.start)
142
- return { filePath, diagnostics }
141
+ // Filter suppressed diagnostics:
142
+ // // pyreon-lint-ignore — suppress all on next line
143
+ // // pyreon-lint-ignore rule-name — suppress specific rule on next line
144
+ const lines = sourceText.split('\n')
145
+ const filtered = diagnostics.filter((d) => {
146
+ const prevLineIdx = d.loc.line - 2
147
+ if (prevLineIdx < 0) return true
148
+ const prevLine = lines[prevLineIdx]?.trim()
149
+ if (!prevLine?.startsWith('// pyreon-lint-ignore')) return true
150
+ const rest = prevLine.slice('// pyreon-lint-ignore'.length).trim()
151
+ return rest.length > 0 && rest !== d.ruleId
152
+ })
153
+
154
+ filtered.sort((a, b) => a.span.start - b.span.start)
155
+ return { filePath, diagnostics: filtered }
143
156
  }
144
157
 
145
158
  /**
@@ -37,7 +37,7 @@ function lintWith(ruleId: string, source: string, filePath?: string) {
37
37
 
38
38
  describe('Rule metadata', () => {
39
39
  it('should have 56 rules', () => {
40
- expect(allRules.length).toBe(56)
40
+ expect(allRules.length).toBe(57)
41
41
  })
42
42
 
43
43
  it('should have unique rule IDs', () => {
@@ -77,7 +77,7 @@ describe('Rule metadata', () => {
77
77
  for (const rule of allRules) {
78
78
  counts[rule.meta.category] = (counts[rule.meta.category] ?? 0) + 1
79
79
  }
80
- expect(counts.reactivity).toBe(9)
80
+ expect(counts.reactivity).toBe(10)
81
81
  expect(counts.jsx).toBe(11)
82
82
  expect(counts.lifecycle).toBe(4)
83
83
  expect(counts.performance).toBe(4)
@@ -1013,7 +1013,7 @@ describe('Ignore filter', () => {
1013
1013
  describe('Presets', () => {
1014
1014
  it('recommended should include all rules', () => {
1015
1015
  const config = getPreset('recommended')
1016
- expect(Object.keys(config.rules).length).toBe(56)
1016
+ expect(Object.keys(config.rules).length).toBe(57)
1017
1017
  })
1018
1018
 
1019
1019
  it('strict should promote all warns to errors', () => {