@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.
Files changed (86) hide show
  1. package/README.md +91 -91
  2. package/lib/analysis/cli.js.html +5406 -0
  3. package/lib/analysis/index.js.html +1 -1
  4. package/lib/cli.js +3290 -0
  5. package/lib/cli.js.map +1 -0
  6. package/lib/index.js +220 -29
  7. package/lib/index.js.map +1 -1
  8. package/lib/types/index.d.ts +30 -5
  9. package/lib/types/index.d.ts.map +1 -1
  10. package/package.json +19 -19
  11. package/src/cache.ts +1 -1
  12. package/src/cli.ts +39 -28
  13. package/src/config/ignore.ts +23 -23
  14. package/src/config/loader.ts +8 -8
  15. package/src/config/presets.ts +11 -11
  16. package/src/index.ts +14 -12
  17. package/src/lint.ts +19 -25
  18. package/src/lsp/index.ts +225 -0
  19. package/src/reporter.ts +17 -17
  20. package/src/rules/accessibility/dialog-a11y.ts +10 -10
  21. package/src/rules/accessibility/overlay-a11y.ts +11 -11
  22. package/src/rules/accessibility/toast-a11y.ts +11 -11
  23. package/src/rules/architecture/dev-guard-warnings.ts +19 -19
  24. package/src/rules/architecture/no-circular-import.ts +16 -16
  25. package/src/rules/architecture/no-cross-layer-import.ts +35 -35
  26. package/src/rules/architecture/no-deep-import.ts +7 -7
  27. package/src/rules/architecture/no-error-without-prefix.ts +20 -20
  28. package/src/rules/form/no-submit-without-validation.ts +13 -13
  29. package/src/rules/form/no-unregistered-field.ts +12 -12
  30. package/src/rules/form/prefer-field-array.ts +11 -11
  31. package/src/rules/hooks/no-raw-addeventlistener.ts +9 -9
  32. package/src/rules/hooks/no-raw-localstorage.ts +11 -11
  33. package/src/rules/hooks/no-raw-setinterval.ts +11 -11
  34. package/src/rules/index.ts +60 -57
  35. package/src/rules/jsx/no-and-conditional.ts +8 -8
  36. package/src/rules/jsx/no-children-access.ts +12 -12
  37. package/src/rules/jsx/no-classname.ts +10 -10
  38. package/src/rules/jsx/no-htmlfor.ts +10 -10
  39. package/src/rules/jsx/no-index-as-by.ts +17 -17
  40. package/src/rules/jsx/no-map-in-jsx.ts +9 -9
  41. package/src/rules/jsx/no-missing-for-by.ts +9 -9
  42. package/src/rules/jsx/no-onchange.ts +12 -12
  43. package/src/rules/jsx/no-props-destructure.ts +11 -11
  44. package/src/rules/jsx/no-ternary-conditional.ts +8 -8
  45. package/src/rules/jsx/use-by-not-key.ts +12 -12
  46. package/src/rules/lifecycle/no-dom-in-setup.ts +18 -18
  47. package/src/rules/lifecycle/no-effect-in-mount.ts +11 -11
  48. package/src/rules/lifecycle/no-missing-cleanup.ts +19 -19
  49. package/src/rules/lifecycle/no-mount-in-effect.ts +11 -11
  50. package/src/rules/performance/no-eager-import.ts +7 -7
  51. package/src/rules/performance/no-effect-in-for.ts +10 -10
  52. package/src/rules/performance/no-large-for-without-by.ts +9 -9
  53. package/src/rules/performance/prefer-show-over-display.ts +16 -16
  54. package/src/rules/reactivity/no-bare-signal-in-jsx.ts +10 -10
  55. package/src/rules/reactivity/no-context-destructure.ts +45 -0
  56. package/src/rules/reactivity/no-effect-assignment.ts +16 -16
  57. package/src/rules/reactivity/no-nested-effect.ts +10 -10
  58. package/src/rules/reactivity/no-peek-in-tracked.ts +10 -10
  59. package/src/rules/reactivity/no-signal-in-loop.ts +13 -13
  60. package/src/rules/reactivity/no-signal-leak.ts +9 -9
  61. package/src/rules/reactivity/no-unbatched-updates.ts +12 -12
  62. package/src/rules/reactivity/prefer-computed.ts +13 -13
  63. package/src/rules/router/index.ts +4 -4
  64. package/src/rules/router/no-href-navigation.ts +14 -14
  65. package/src/rules/router/no-imperative-navigate-in-render.ts +19 -19
  66. package/src/rules/router/no-missing-fallback.ts +16 -16
  67. package/src/rules/router/prefer-use-is-active.ts +11 -11
  68. package/src/rules/ssr/no-mismatch-risk.ts +11 -11
  69. package/src/rules/ssr/no-window-in-ssr.ts +22 -22
  70. package/src/rules/ssr/prefer-request-context.ts +14 -14
  71. package/src/rules/store/no-duplicate-store-id.ts +9 -9
  72. package/src/rules/store/no-mutate-store-state.ts +11 -11
  73. package/src/rules/store/no-store-outside-provider.ts +15 -15
  74. package/src/rules/styling/no-dynamic-styled.ts +13 -13
  75. package/src/rules/styling/no-inline-style-object.ts +10 -10
  76. package/src/rules/styling/no-theme-outside-provider.ts +11 -11
  77. package/src/rules/styling/prefer-cx.ts +12 -12
  78. package/src/runner.ts +13 -14
  79. package/src/tests/lsp.test.ts +88 -0
  80. package/src/tests/runner.test.ts +325 -325
  81. package/src/types.ts +15 -15
  82. package/src/utils/ast.ts +50 -50
  83. package/src/utils/imports.ts +53 -53
  84. package/src/utils/index.ts +12 -3
  85. package/src/utils/source.ts +2 -2
  86. package/src/watcher.ts +19 -25
@@ -1,29 +1,29 @@
1
- import type { Rule, VisitorCallbacks } from "../../types"
2
- import { getSpan, isCallTo } from "../../utils/ast"
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getSpan, isCallTo } from '../../utils/ast'
3
3
 
4
4
  export const noNestedEffect: Rule = {
5
5
  meta: {
6
- id: "pyreon/no-nested-effect",
7
- category: "reactivity",
8
- description: "Warn against nesting effect() inside another effect().",
9
- severity: "warn",
6
+ id: 'pyreon/no-nested-effect',
7
+ category: 'reactivity',
8
+ description: 'Warn against nesting effect() inside another effect().',
9
+ severity: 'warn',
10
10
  fixable: false,
11
11
  },
12
12
  create(context) {
13
13
  let effectDepth = 0
14
14
  const callbacks: VisitorCallbacks = {
15
15
  CallExpression(node: any) {
16
- if (!isCallTo(node, "effect")) return
16
+ if (!isCallTo(node, 'effect')) return
17
17
  if (effectDepth > 0) {
18
18
  context.report({
19
- message: "Nested `effect()` — consider using `computed()` for derived values instead.",
19
+ message: 'Nested `effect()` — consider using `computed()` for derived values instead.',
20
20
  span: getSpan(node),
21
21
  })
22
22
  }
23
23
  effectDepth++
24
24
  },
25
- "CallExpression:exit"(node: any) {
26
- if (isCallTo(node, "effect")) {
25
+ 'CallExpression:exit'(node: any) {
26
+ if (isCallTo(node, 'effect')) {
27
27
  effectDepth--
28
28
  }
29
29
  },
@@ -1,31 +1,31 @@
1
- import type { Rule, VisitorCallbacks } from "../../types"
2
- import { getSpan, isCallTo, isPeekCall } from "../../utils/ast"
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getSpan, isCallTo, isPeekCall } from '../../utils/ast'
3
3
 
4
4
  export const noPeekInTracked: Rule = {
5
5
  meta: {
6
- id: "pyreon/no-peek-in-tracked",
7
- category: "reactivity",
8
- description: "Disallow .peek() inside effect() or computed() — it bypasses tracking.",
9
- severity: "error",
6
+ id: 'pyreon/no-peek-in-tracked',
7
+ category: 'reactivity',
8
+ description: 'Disallow .peek() inside effect() or computed() — it bypasses tracking.',
9
+ severity: 'error',
10
10
  fixable: false,
11
11
  },
12
12
  create(context) {
13
13
  let trackedDepth = 0
14
14
  const callbacks: VisitorCallbacks = {
15
15
  CallExpression(node: any) {
16
- if (isCallTo(node, "effect") || isCallTo(node, "computed")) {
16
+ if (isCallTo(node, 'effect') || isCallTo(node, 'computed')) {
17
17
  trackedDepth++
18
18
  }
19
19
  if (trackedDepth > 0 && isPeekCall(node)) {
20
20
  context.report({
21
21
  message:
22
- "`.peek()` inside a tracked scope (effect/computed) bypasses dependency tracking — use a normal signal read instead.",
22
+ '`.peek()` inside a tracked scope (effect/computed) bypasses dependency tracking — use a normal signal read instead.',
23
23
  span: getSpan(node),
24
24
  })
25
25
  }
26
26
  },
27
- "CallExpression:exit"(node: any) {
28
- if (isCallTo(node, "effect") || isCallTo(node, "computed")) {
27
+ 'CallExpression:exit'(node: any) {
28
+ if (isCallTo(node, 'effect') || isCallTo(node, 'computed')) {
29
29
  trackedDepth--
30
30
  }
31
31
  },
@@ -1,12 +1,12 @@
1
- import type { Rule, VisitorCallbacks } from "../../types"
2
- import { getSpan } from "../../utils/ast"
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getSpan } from '../../utils/ast'
3
3
 
4
4
  export const noSignalInLoop: Rule = {
5
5
  meta: {
6
- id: "pyreon/no-signal-in-loop",
7
- category: "reactivity",
8
- description: "Disallow creating signals or computeds inside loops.",
9
- severity: "error",
6
+ id: 'pyreon/no-signal-in-loop',
7
+ category: 'reactivity',
8
+ description: 'Disallow creating signals or computeds inside loops.',
9
+ severity: 'error',
10
10
  fixable: false,
11
11
  },
12
12
  create(context) {
@@ -15,38 +15,38 @@ export const noSignalInLoop: Rule = {
15
15
  ForStatement() {
16
16
  loopDepth++
17
17
  },
18
- "ForStatement:exit"() {
18
+ 'ForStatement:exit'() {
19
19
  loopDepth--
20
20
  },
21
21
  ForInStatement() {
22
22
  loopDepth++
23
23
  },
24
- "ForInStatement:exit"() {
24
+ 'ForInStatement:exit'() {
25
25
  loopDepth--
26
26
  },
27
27
  ForOfStatement() {
28
28
  loopDepth++
29
29
  },
30
- "ForOfStatement:exit"() {
30
+ 'ForOfStatement:exit'() {
31
31
  loopDepth--
32
32
  },
33
33
  WhileStatement() {
34
34
  loopDepth++
35
35
  },
36
- "WhileStatement:exit"() {
36
+ 'WhileStatement:exit'() {
37
37
  loopDepth--
38
38
  },
39
39
  DoWhileStatement() {
40
40
  loopDepth++
41
41
  },
42
- "DoWhileStatement:exit"() {
42
+ 'DoWhileStatement:exit'() {
43
43
  loopDepth--
44
44
  },
45
45
  CallExpression(node: any) {
46
46
  if (loopDepth === 0) return
47
47
  const callee = node.callee
48
- if (!callee || callee.type !== "Identifier") return
49
- if (callee.name === "signal" || callee.name === "computed") {
48
+ if (!callee || callee.type !== 'Identifier') return
49
+ if (callee.name === 'signal' || callee.name === 'computed') {
50
50
  context.report({
51
51
  message: `\`${callee.name}()\` inside a loop — signals should be created once at component setup, not on every iteration.`,
52
52
  span: getSpan(node),
@@ -1,12 +1,12 @@
1
- import type { Rule, VisitorCallbacks } from "../../types"
2
- import { getSpan, isCallTo } from "../../utils/ast"
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getSpan, isCallTo } from '../../utils/ast'
3
3
 
4
4
  export const noSignalLeak: Rule = {
5
5
  meta: {
6
- id: "pyreon/no-signal-leak",
7
- category: "reactivity",
8
- description: "Warn about unused signal declarations (potential leaks).",
9
- severity: "warn",
6
+ id: 'pyreon/no-signal-leak',
7
+ category: 'reactivity',
8
+ description: 'Warn about unused signal declarations (potential leaks).',
9
+ severity: 'warn',
10
10
  fixable: false,
11
11
  },
12
12
  create(context) {
@@ -19,9 +19,9 @@ export const noSignalLeak: Rule = {
19
19
  const callbacks: VisitorCallbacks = {
20
20
  VariableDeclarator(node: any) {
21
21
  const init = node.init
22
- if (!init || !isCallTo(init, "signal")) return
22
+ if (!init || !isCallTo(init, 'signal')) return
23
23
  const id = node.id
24
- if (!id || id.type !== "Identifier") return
24
+ if (!id || id.type !== 'Identifier') return
25
25
  signalDecls.set(id.name, {
26
26
  span: getSpan(node),
27
27
  declStart: id.start as number,
@@ -39,7 +39,7 @@ export const noSignalLeak: Rule = {
39
39
  ])
40
40
  }
41
41
  },
42
- "Program:exit"() {
42
+ 'Program:exit'() {
43
43
  for (const [name, { span, declStart, declEnd }] of signalDecls) {
44
44
  const occurrences = identifierOccurrences.get(name) ?? []
45
45
  // Filter out the declaration identifier itself
@@ -1,5 +1,5 @@
1
- import type { Rule, VisitorCallbacks } from "../../types"
2
- import { getSpan, isCallTo, isSetCall } from "../../utils/ast"
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getSpan, isCallTo, isSetCall } from '../../utils/ast'
3
3
 
4
4
  interface ScopeInfo {
5
5
  setCalls: Array<{ span: { start: number; end: number } }>
@@ -10,10 +10,10 @@ interface ScopeInfo {
10
10
 
11
11
  export const noUnbatchedUpdates: Rule = {
12
12
  meta: {
13
- id: "pyreon/no-unbatched-updates",
14
- category: "reactivity",
15
- description: "Warn when 3+ .set() calls occur in the same function without batch().",
16
- severity: "warn",
13
+ id: 'pyreon/no-unbatched-updates',
14
+ category: 'reactivity',
15
+ description: 'Warn when 3+ .set() calls occur in the same function without batch().',
16
+ severity: 'warn',
17
17
  fixable: false,
18
18
  },
19
19
  create(context) {
@@ -39,24 +39,24 @@ export const noUnbatchedUpdates: Rule = {
39
39
  FunctionDeclaration(node: any) {
40
40
  enterScope(node)
41
41
  },
42
- "FunctionDeclaration:exit"() {
42
+ 'FunctionDeclaration:exit'() {
43
43
  exitScope()
44
44
  },
45
45
  FunctionExpression(node: any) {
46
46
  enterScope(node)
47
47
  },
48
- "FunctionExpression:exit"() {
48
+ 'FunctionExpression:exit'() {
49
49
  exitScope()
50
50
  },
51
51
  ArrowFunctionExpression(node: any) {
52
52
  enterScope(node)
53
53
  },
54
- "ArrowFunctionExpression:exit"() {
54
+ 'ArrowFunctionExpression:exit'() {
55
55
  exitScope()
56
56
  },
57
57
  CallExpression(node: any) {
58
58
  const currentScope = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : undefined
59
- if (isCallTo(node, "batch")) {
59
+ if (isCallTo(node, 'batch')) {
60
60
  batchDepth++
61
61
  if (currentScope) {
62
62
  currentScope.hasBatch = true
@@ -66,8 +66,8 @@ export const noUnbatchedUpdates: Rule = {
66
66
  currentScope.setCalls.push({ span: getSpan(node) })
67
67
  }
68
68
  },
69
- "CallExpression:exit"(node: any) {
70
- if (isCallTo(node, "batch")) {
69
+ 'CallExpression:exit'(node: any) {
70
+ if (isCallTo(node, 'batch')) {
71
71
  batchDepth--
72
72
  }
73
73
  },
@@ -1,18 +1,18 @@
1
- import type { Rule, VisitorCallbacks } from "../../types"
2
- import { getSpan, isCallTo, isSetCall } from "../../utils/ast"
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getSpan, isCallTo, isSetCall } from '../../utils/ast'
3
3
 
4
4
  export const preferComputed: Rule = {
5
5
  meta: {
6
- id: "pyreon/prefer-computed",
7
- category: "reactivity",
8
- description: "Suggest computed() when an effect only contains a single .set() call.",
9
- severity: "warn",
6
+ id: 'pyreon/prefer-computed',
7
+ category: 'reactivity',
8
+ description: 'Suggest computed() when an effect only contains a single .set() call.',
9
+ severity: 'warn',
10
10
  fixable: false,
11
11
  },
12
12
  create(context) {
13
13
  const callbacks: VisitorCallbacks = {
14
14
  CallExpression(node: any) {
15
- if (!isCallTo(node, "effect")) return
15
+ if (!isCallTo(node, 'effect')) return
16
16
  const args = node.arguments
17
17
  if (!args || args.length === 0) return
18
18
 
@@ -20,30 +20,30 @@ export const preferComputed: Rule = {
20
20
  if (!fn) return
21
21
 
22
22
  let body: any = null
23
- if (fn.type === "ArrowFunctionExpression" || fn.type === "FunctionExpression") {
23
+ if (fn.type === 'ArrowFunctionExpression' || fn.type === 'FunctionExpression') {
24
24
  body = fn.body
25
25
  }
26
26
  if (!body) return
27
27
 
28
28
  // Arrow with expression body: effect(() => x.set(y))
29
- if (body.type === "CallExpression" && isSetCall(body)) {
29
+ if (body.type === 'CallExpression' && isSetCall(body)) {
30
30
  context.report({
31
31
  message:
32
- "Effect contains a single `.set()` — consider using `computed()` instead for derived values.",
32
+ 'Effect contains a single `.set()` — consider using `computed()` instead for derived values.',
33
33
  span: getSpan(node),
34
34
  })
35
35
  return
36
36
  }
37
37
 
38
38
  // Block body with single statement: effect(() => { x.set(y) })
39
- if (body.type === "BlockStatement") {
39
+ if (body.type === 'BlockStatement') {
40
40
  const stmts = body.body
41
41
  if (stmts && stmts.length === 1) {
42
42
  const stmt = stmts[0]
43
- if (stmt.type === "ExpressionStatement" && isSetCall(stmt.expression)) {
43
+ if (stmt.type === 'ExpressionStatement' && isSetCall(stmt.expression)) {
44
44
  context.report({
45
45
  message:
46
- "Effect contains a single `.set()` — consider using `computed()` instead for derived values.",
46
+ 'Effect contains a single `.set()` — consider using `computed()` instead for derived values.',
47
47
  span: getSpan(node),
48
48
  })
49
49
  }
@@ -1,4 +1,4 @@
1
- export { noHrefNavigation } from "./no-href-navigation"
2
- export { noImperativeNavigateInRender } from "./no-imperative-navigate-in-render"
3
- export { noMissingFallback } from "./no-missing-fallback"
4
- export { preferUseIsActive } from "./prefer-use-is-active"
1
+ export { noHrefNavigation } from './no-href-navigation'
2
+ export { noImperativeNavigateInRender } from './no-imperative-navigate-in-render'
3
+ export { noMissingFallback } from './no-missing-fallback'
4
+ export { preferUseIsActive } from './prefer-use-is-active'
@@ -1,16 +1,16 @@
1
- import type { Rule, VisitorCallbacks } from "../../types"
2
- import { getJSXAttribute, getSpan } from "../../utils/ast"
3
- import { extractImportInfo } from "../../utils/imports"
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getJSXAttribute, getSpan } from '../../utils/ast'
3
+ import { extractImportInfo } from '../../utils/imports'
4
4
 
5
- const EXTERNAL_PREFIXES = ["http://", "https://", "mailto:", "tel:"]
5
+ const EXTERNAL_PREFIXES = ['http://', 'https://', 'mailto:', 'tel:']
6
6
 
7
7
  export const noHrefNavigation: Rule = {
8
8
  meta: {
9
- id: "pyreon/no-href-navigation",
10
- category: "router",
9
+ id: 'pyreon/no-href-navigation',
10
+ category: 'router',
11
11
  description:
12
- "Warn when `<a href>` is used in files that import @pyreon/router — use `<Link>` instead.",
13
- severity: "warn",
12
+ 'Warn when `<a href>` is used in files that import @pyreon/router — use `<Link>` instead.',
13
+ severity: 'warn',
14
14
  fixable: false,
15
15
  },
16
16
  create(context) {
@@ -19,29 +19,29 @@ export const noHrefNavigation: Rule = {
19
19
  const callbacks: VisitorCallbacks = {
20
20
  ImportDeclaration(node: any) {
21
21
  const info = extractImportInfo(node)
22
- if (info && info.source === "@pyreon/router") {
22
+ if (info && info.source === '@pyreon/router') {
23
23
  importsRouter = true
24
24
  }
25
25
  },
26
26
  JSXOpeningElement(node: any) {
27
27
  if (!importsRouter) return
28
28
  const name = node.name
29
- if (!name || name.type !== "JSXIdentifier" || name.name !== "a") return
29
+ if (!name || name.type !== 'JSXIdentifier' || name.name !== 'a') return
30
30
 
31
- const hrefAttr = getJSXAttribute(node, "href")
31
+ const hrefAttr = getJSXAttribute(node, 'href')
32
32
  if (!hrefAttr) return
33
33
 
34
34
  // Get the href value
35
35
  const value = hrefAttr.value
36
- if (value?.type === "Literal" && typeof value.value === "string") {
36
+ if (value?.type === 'Literal' && typeof value.value === 'string') {
37
37
  const href: string = value.value
38
38
  // Skip external URLs and anchor links
39
- if (href.startsWith("#") || EXTERNAL_PREFIXES.some((p) => href.startsWith(p))) return
39
+ if (href.startsWith('#') || EXTERNAL_PREFIXES.some((p) => href.startsWith(p))) return
40
40
  }
41
41
 
42
42
  context.report({
43
43
  message:
44
- "`<a href>` in a router file — use `<Link>` or `<RouterLink>` for client-side navigation.",
44
+ '`<a href>` in a router file — use `<Link>` or `<RouterLink>` for client-side navigation.',
45
45
  span: getSpan(node),
46
46
  })
47
47
  },
@@ -1,13 +1,13 @@
1
- import type { Rule, VisitorCallbacks } from "../../types"
2
- import { getSpan, isCallTo, isMemberCallTo } from "../../utils/ast"
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getSpan, isCallTo, isMemberCallTo } from '../../utils/ast'
3
3
 
4
4
  export const noImperativeNavigateInRender: Rule = {
5
5
  meta: {
6
- id: "pyreon/no-imperative-navigate-in-render",
7
- category: "router",
6
+ id: 'pyreon/no-imperative-navigate-in-render',
7
+ category: 'router',
8
8
  description:
9
- "Error when navigate() or router.push() is called at the top level of a component — causes infinite render loops.",
10
- severity: "error",
9
+ 'Error when navigate() or router.push() is called at the top level of a component — causes infinite render loops.',
10
+ severity: 'error',
11
11
  fixable: false,
12
12
  },
13
13
  create(context) {
@@ -20,27 +20,27 @@ export const noImperativeNavigateInRender: Rule = {
20
20
 
21
21
  const callbacks: VisitorCallbacks = {
22
22
  FunctionDeclaration(node: any) {
23
- const name: string = node.id?.name ?? ""
23
+ const name: string = node.id?.name ?? ''
24
24
  if (/^[A-Z]/.test(name)) {
25
25
  componentBodyDepth++
26
26
  }
27
27
  },
28
- "FunctionDeclaration:exit"(node: any) {
29
- const name: string = node.id?.name ?? ""
28
+ 'FunctionDeclaration:exit'(node: any) {
29
+ const name: string = node.id?.name ?? ''
30
30
  if (/^[A-Z]/.test(name)) {
31
31
  componentBodyDepth--
32
32
  }
33
33
  },
34
34
  // For arrow functions, we use VariableDeclarator to detect component assignment
35
35
  VariableDeclarator(node: any) {
36
- const name: string = node.id?.name ?? ""
37
- if (/^[A-Z]/.test(name) && node.init?.type === "ArrowFunctionExpression") {
36
+ const name: string = node.id?.name ?? ''
37
+ if (/^[A-Z]/.test(name) && node.init?.type === 'ArrowFunctionExpression') {
38
38
  componentBodyDepth++
39
39
  }
40
40
  },
41
- "VariableDeclarator:exit"(node: any) {
42
- const name: string = node.id?.name ?? ""
43
- if (/^[A-Z]/.test(name) && node.init?.type === "ArrowFunctionExpression") {
41
+ 'VariableDeclarator:exit'(node: any) {
42
+ const name: string = node.id?.name ?? ''
43
+ if (/^[A-Z]/.test(name) && node.init?.type === 'ArrowFunctionExpression') {
44
44
  componentBodyDepth--
45
45
  }
46
46
  },
@@ -56,15 +56,15 @@ export const noImperativeNavigateInRender: Rule = {
56
56
  // Only report if we're in a component body and NOT inside a safe callback
57
57
  if (safeDepth > 0) return
58
58
 
59
- if (isCallTo(node, "navigate") || isMemberCallTo(node, "router", "push")) {
59
+ if (isCallTo(node, 'navigate') || isMemberCallTo(node, 'router', 'push')) {
60
60
  context.report({
61
61
  message:
62
- "Imperative navigation at the top level of a component — this runs on every render and causes infinite loops. Move inside `onMount`, `effect`, or an event handler.",
62
+ 'Imperative navigation at the top level of a component — this runs on every render and causes infinite loops. Move inside `onMount`, `effect`, or an event handler.',
63
63
  span: getSpan(node),
64
64
  })
65
65
  }
66
66
  },
67
- "CallExpression:exit"(node: any) {
67
+ 'CallExpression:exit'(node: any) {
68
68
  if (componentBodyDepth <= 0) return
69
69
  if (isSafeWrapperCall(node)) {
70
70
  safeDepth--
@@ -77,7 +77,7 @@ export const noImperativeNavigateInRender: Rule = {
77
77
 
78
78
  function isSafeWrapperCall(node: any): boolean {
79
79
  const callee = node.callee
80
- if (!callee || callee.type !== "Identifier") return false
80
+ if (!callee || callee.type !== 'Identifier') return false
81
81
  const name: string = callee.name
82
- return name === "onMount" || name === "effect" || name === "onUnmount"
82
+ return name === 'onMount' || name === 'effect' || name === 'onUnmount'
83
83
  }
@@ -1,27 +1,27 @@
1
- import type { Rule, VisitorCallbacks } from "../../types"
2
- import { getSpan } from "../../utils/ast"
3
- import { extractImportInfo } from "../../utils/imports"
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getSpan } from '../../utils/ast'
3
+ import { extractImportInfo } from '../../utils/imports'
4
4
 
5
5
  function isCatchAllPath(value: string): boolean {
6
- return value === "*" || value.endsWith("*")
6
+ return value === '*' || value.endsWith('*')
7
7
  }
8
8
 
9
9
  function getPathValue(prop: any): string | null {
10
10
  const key = prop.key
11
11
  if (!key) return null
12
- const keyName = key.type === "Identifier" ? key.name : null
13
- if (keyName !== "path") return null
12
+ const keyName = key.type === 'Identifier' ? key.name : null
13
+ if (keyName !== 'path') return null
14
14
  const val = prop.value
15
- if (val?.type === "Literal" && typeof val.value === "string") {
15
+ if (val?.type === 'Literal' && typeof val.value === 'string') {
16
16
  return val.value
17
17
  }
18
18
  return null
19
19
  }
20
20
 
21
21
  function hasPathProperty(obj: any): boolean {
22
- if (!obj || obj.type !== "ObjectExpression") return false
22
+ if (!obj || obj.type !== 'ObjectExpression') return false
23
23
  for (const prop of obj.properties ?? []) {
24
- if (prop.type !== "Property") continue
24
+ if (prop.type !== 'Property') continue
25
25
  if (getPathValue(prop) !== null) return true
26
26
  }
27
27
  return false
@@ -29,9 +29,9 @@ function hasPathProperty(obj: any): boolean {
29
29
 
30
30
  function hasCatchAllRoute(elements: any[]): boolean {
31
31
  for (const elem of elements) {
32
- if (!elem || elem.type !== "ObjectExpression") continue
32
+ if (!elem || elem.type !== 'ObjectExpression') continue
33
33
  for (const prop of elem.properties ?? []) {
34
- if (prop.type !== "Property") continue
34
+ if (prop.type !== 'Property') continue
35
35
  const pathVal = getPathValue(prop)
36
36
  if (pathVal !== null && isCatchAllPath(pathVal)) return true
37
37
  }
@@ -41,11 +41,11 @@ function hasCatchAllRoute(elements: any[]): boolean {
41
41
 
42
42
  export const noMissingFallback: Rule = {
43
43
  meta: {
44
- id: "pyreon/no-missing-fallback",
45
- category: "router",
44
+ id: 'pyreon/no-missing-fallback',
45
+ category: 'router',
46
46
  description:
47
47
  'Warn when route config has no catch-all route (`path: "*"` or `path: "/:rest*"`).',
48
- severity: "warn",
48
+ severity: 'warn',
49
49
  fixable: false,
50
50
  },
51
51
  create(context) {
@@ -56,7 +56,7 @@ export const noMissingFallback: Rule = {
56
56
  const callbacks: VisitorCallbacks = {
57
57
  ImportDeclaration(node: any) {
58
58
  const info = extractImportInfo(node)
59
- if (info && info.source === "@pyreon/router") {
59
+ if (info && info.source === '@pyreon/router') {
60
60
  importsRouter = true
61
61
  }
62
62
  },
@@ -73,7 +73,7 @@ export const noMissingFallback: Rule = {
73
73
  foundCatchAll = true
74
74
  }
75
75
  },
76
- "Program:exit"() {
76
+ 'Program:exit'() {
77
77
  if (!importsRouter || !routeArraySpan || foundCatchAll) return
78
78
  context.report({
79
79
  message:
@@ -1,25 +1,25 @@
1
- import type { Rule, VisitorCallbacks } from "../../types"
2
- import { getSpan } from "../../utils/ast"
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getSpan } from '../../utils/ast'
3
3
 
4
4
  export const preferUseIsActive: Rule = {
5
5
  meta: {
6
- id: "pyreon/prefer-use-is-active",
7
- category: "router",
6
+ id: 'pyreon/prefer-use-is-active',
7
+ category: 'router',
8
8
  description:
9
9
  'Suggest useIsActive() instead of `location.pathname === "/foo"` or `route.path === "/foo"` patterns.',
10
- severity: "info",
10
+ severity: 'info',
11
11
  fixable: false,
12
12
  },
13
13
  create(context) {
14
14
  const callbacks: VisitorCallbacks = {
15
15
  BinaryExpression(node: any) {
16
- if (node.operator !== "===" && node.operator !== "==") return
16
+ if (node.operator !== '===' && node.operator !== '==') return
17
17
 
18
18
  // Check both sides for location.pathname or route.path
19
19
  if (isPathComparison(node.left) || isPathComparison(node.right)) {
20
20
  context.report({
21
21
  message:
22
- "Manual path comparison — use `useIsActive()` for reactive route matching with segment-aware prefix matching.",
22
+ 'Manual path comparison — use `useIsActive()` for reactive route matching with segment-aware prefix matching.',
23
23
  span: getSpan(node),
24
24
  })
25
25
  }
@@ -30,16 +30,16 @@ export const preferUseIsActive: Rule = {
30
30
  }
31
31
 
32
32
  function isPathComparison(node: any): boolean {
33
- if (!node || node.type !== "MemberExpression") return false
33
+ if (!node || node.type !== 'MemberExpression') return false
34
34
  const obj = node.object
35
35
  const prop = node.property
36
- if (!obj || !prop || prop.type !== "Identifier") return false
36
+ if (!obj || !prop || prop.type !== 'Identifier') return false
37
37
 
38
38
  // location.pathname
39
- if (obj.type === "Identifier" && obj.name === "location" && prop.name === "pathname") return true
39
+ if (obj.type === 'Identifier' && obj.name === 'location' && prop.name === 'pathname') return true
40
40
 
41
41
  // route.path
42
- if (obj.type === "Identifier" && obj.name === "route" && prop.name === "path") return true
42
+ if (obj.type === 'Identifier' && obj.name === 'route' && prop.name === 'path') return true
43
43
 
44
44
  return false
45
45
  }