@pyreon/lint 0.11.5 → 0.11.7

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 +1 -1
  3. package/lib/analysis/index.js.html +1 -1
  4. package/lib/cli.js +214 -1
  5. package/lib/cli.js.map +1 -1
  6. package/lib/index.js +207 -1
  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 +15 -15
  11. package/src/cache.ts +1 -1
  12. package/src/cli.ts +38 -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 -19
  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 -13
  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 +5 -5
  85. package/src/utils/source.ts +2 -2
  86. package/src/watcher.ts +19 -19
@@ -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
  }
@@ -1,13 +1,13 @@
1
- import type { Rule, VisitorCallbacks } from "../../types"
2
- import { getSpan, isMemberCallTo } from "../../utils/ast"
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getSpan, isMemberCallTo } from '../../utils/ast'
3
3
 
4
4
  export const noMismatchRisk: Rule = {
5
5
  meta: {
6
- id: "pyreon/no-mismatch-risk",
7
- category: "ssr",
6
+ id: 'pyreon/no-mismatch-risk',
7
+ category: 'ssr',
8
8
  description:
9
- "Warn about non-deterministic calls (Date.now, Math.random, crypto.randomUUID) in JSX context that cause hydration mismatches.",
10
- severity: "warn",
9
+ 'Warn about non-deterministic calls (Date.now, Math.random, crypto.randomUUID) in JSX context that cause hydration mismatches.',
10
+ severity: 'warn',
11
11
  fixable: false,
12
12
  },
13
13
  create(context) {
@@ -16,22 +16,22 @@ export const noMismatchRisk: Rule = {
16
16
  JSXElement() {
17
17
  jsxDepth++
18
18
  },
19
- "JSXElement:exit"() {
19
+ 'JSXElement:exit'() {
20
20
  jsxDepth--
21
21
  },
22
22
  JSXFragment() {
23
23
  jsxDepth++
24
24
  },
25
- "JSXFragment:exit"() {
25
+ 'JSXFragment:exit'() {
26
26
  jsxDepth--
27
27
  },
28
28
  CallExpression(node: any) {
29
29
  if (jsxDepth === 0) return
30
30
 
31
31
  if (
32
- isMemberCallTo(node, "Date", "now") ||
33
- isMemberCallTo(node, "Math", "random") ||
34
- isMemberCallTo(node, "crypto", "randomUUID")
32
+ isMemberCallTo(node, 'Date', 'now') ||
33
+ isMemberCallTo(node, 'Math', 'random') ||
34
+ isMemberCallTo(node, 'crypto', 'randomUUID')
35
35
  ) {
36
36
  const callee = node.callee
37
37
  const name = `${callee.object.name}.${callee.property.name}`
@@ -1,13 +1,13 @@
1
- import type { Rule, VisitorCallbacks } from "../../types"
2
- import { getSpan, isCallTo } from "../../utils/ast"
3
- import { BROWSER_GLOBALS } from "../../utils/imports"
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getSpan, isCallTo } from '../../utils/ast'
3
+ import { BROWSER_GLOBALS } from '../../utils/imports'
4
4
 
5
5
  export const noWindowInSsr: Rule = {
6
6
  meta: {
7
- id: "pyreon/no-window-in-ssr",
8
- category: "ssr",
9
- description: "Disallow browser globals outside onMount/effect/typeof guards — they break SSR.",
10
- severity: "error",
7
+ id: 'pyreon/no-window-in-ssr',
8
+ category: 'ssr',
9
+ description: 'Disallow browser globals outside onMount/effect/typeof guards — they break SSR.',
10
+ severity: 'error',
11
11
  fixable: false,
12
12
  },
13
13
  create(context) {
@@ -16,12 +16,12 @@ export const noWindowInSsr: Rule = {
16
16
 
17
17
  const callbacks: VisitorCallbacks = {
18
18
  CallExpression(node: any) {
19
- if (isCallTo(node, "onMount") || isCallTo(node, "effect")) {
19
+ if (isCallTo(node, 'onMount') || isCallTo(node, 'effect')) {
20
20
  safeDepth++
21
21
  }
22
22
  },
23
- "CallExpression:exit"(node: any) {
24
- if (isCallTo(node, "onMount") || isCallTo(node, "effect")) {
23
+ 'CallExpression:exit'(node: any) {
24
+ if (isCallTo(node, 'onMount') || isCallTo(node, 'effect')) {
25
25
  safeDepth--
26
26
  }
27
27
  },
@@ -29,19 +29,19 @@ export const noWindowInSsr: Rule = {
29
29
  // typeof window !== "undefined"
30
30
  const test = node.test
31
31
  if (
32
- test?.type === "BinaryExpression" &&
33
- test.left?.type === "UnaryExpression" &&
34
- test.left.operator === "typeof"
32
+ test?.type === 'BinaryExpression' &&
33
+ test.left?.type === 'UnaryExpression' &&
34
+ test.left.operator === 'typeof'
35
35
  ) {
36
36
  typeofGuardDepth++
37
37
  }
38
38
  },
39
- "IfStatement:exit"(node: any) {
39
+ 'IfStatement:exit'(node: any) {
40
40
  const test = node.test
41
41
  if (
42
- test?.type === "BinaryExpression" &&
43
- test.left?.type === "UnaryExpression" &&
44
- test.left.operator === "typeof"
42
+ test?.type === 'BinaryExpression' &&
43
+ test.left?.type === 'UnaryExpression' &&
44
+ test.left.operator === 'typeof'
45
45
  ) {
46
46
  typeofGuardDepth--
47
47
  }
@@ -51,18 +51,18 @@ export const noWindowInSsr: Rule = {
51
51
  if (!BROWSER_GLOBALS.has(node.name)) return
52
52
 
53
53
  // Skip typeof expressions: typeof window
54
- if (parent?.type === "UnaryExpression" && parent.operator === "typeof") return
54
+ if (parent?.type === 'UnaryExpression' && parent.operator === 'typeof') return
55
55
 
56
56
  // Skip import specifiers
57
57
  if (
58
- parent?.type === "ImportSpecifier" ||
59
- parent?.type === "ImportDefaultSpecifier" ||
60
- parent?.type === "ImportNamespaceSpecifier"
58
+ parent?.type === 'ImportSpecifier' ||
59
+ parent?.type === 'ImportDefaultSpecifier' ||
60
+ parent?.type === 'ImportNamespaceSpecifier'
61
61
  )
62
62
  return
63
63
 
64
64
  // Skip property access on member expressions (only flag when used as the object)
65
- if (parent?.type === "MemberExpression" && parent.property === node && !parent.computed)
65
+ if (parent?.type === 'MemberExpression' && parent.property === node && !parent.computed)
66
66
  return
67
67
 
68
68
  context.report({
@@ -1,22 +1,22 @@
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 preferRequestContext: Rule = {
5
5
  meta: {
6
- id: "pyreon/prefer-request-context",
7
- category: "ssr",
6
+ id: 'pyreon/prefer-request-context',
7
+ category: 'ssr',
8
8
  description:
9
- "Warn about module-level signal()/createStore() in server files — use request context instead.",
10
- severity: "warn",
9
+ 'Warn about module-level signal()/createStore() in server files — use request context instead.',
10
+ severity: 'warn',
11
11
  fixable: false,
12
12
  },
13
13
  create(context) {
14
14
  const filePath = context.getFilePath()
15
15
  const isServerFile =
16
- filePath.includes("server") ||
17
- filePath.includes(".server.") ||
18
- filePath.endsWith("server.ts") ||
19
- filePath.endsWith("server.tsx")
16
+ filePath.includes('server') ||
17
+ filePath.includes('.server.') ||
18
+ filePath.endsWith('server.ts') ||
19
+ filePath.endsWith('server.tsx')
20
20
 
21
21
  if (!isServerFile) return {}
22
22
 
@@ -25,24 +25,24 @@ export const preferRequestContext: Rule = {
25
25
  FunctionDeclaration() {
26
26
  functionDepth++
27
27
  },
28
- "FunctionDeclaration:exit"() {
28
+ 'FunctionDeclaration:exit'() {
29
29
  functionDepth--
30
30
  },
31
31
  FunctionExpression() {
32
32
  functionDepth++
33
33
  },
34
- "FunctionExpression:exit"() {
34
+ 'FunctionExpression:exit'() {
35
35
  functionDepth--
36
36
  },
37
37
  ArrowFunctionExpression() {
38
38
  functionDepth++
39
39
  },
40
- "ArrowFunctionExpression:exit"() {
40
+ 'ArrowFunctionExpression:exit'() {
41
41
  functionDepth--
42
42
  },
43
43
  CallExpression(node: any) {
44
44
  if (functionDepth > 0) return // only flag module-level calls
45
- if (isCallTo(node, "signal") || isCallTo(node, "createStore")) {
45
+ if (isCallTo(node, 'signal') || isCallTo(node, 'createStore')) {
46
46
  const name = node.callee.name
47
47
  context.report({
48
48
  message: `Module-level \`${name}()\` in a server file — this state is shared across all requests. Use \`runWithRequestContext()\` for per-request isolation.`,
@@ -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 noDuplicateStoreId: Rule = {
5
5
  meta: {
6
- id: "pyreon/no-duplicate-store-id",
7
- category: "store",
8
- description: "Disallow duplicate defineStore() IDs in the same file.",
9
- severity: "error",
6
+ id: 'pyreon/no-duplicate-store-id',
7
+ category: 'store',
8
+ description: 'Disallow duplicate defineStore() IDs in the same file.',
9
+ severity: 'error',
10
10
  fixable: false,
11
11
  },
12
12
  create(context) {
@@ -14,7 +14,7 @@ export const noDuplicateStoreId: Rule = {
14
14
 
15
15
  const callbacks: VisitorCallbacks = {
16
16
  CallExpression(node: any) {
17
- if (!isCallTo(node, "defineStore")) return
17
+ if (!isCallTo(node, 'defineStore')) return
18
18
  const args = node.arguments
19
19
  if (!args || args.length === 0) return
20
20
 
@@ -22,11 +22,11 @@ export const noDuplicateStoreId: Rule = {
22
22
  if (!firstArg) return
23
23
 
24
24
  let id: string | null = null
25
- if (firstArg.type === "Literal" || firstArg.type === "StringLiteral") {
25
+ if (firstArg.type === 'Literal' || firstArg.type === 'StringLiteral') {
26
26
  id = firstArg.value as string
27
27
  }
28
28
 
29
- if (typeof id !== "string") return
29
+ if (typeof id !== 'string') return
30
30
 
31
31
  if (storeIds.has(id)) {
32
32
  context.report({
@@ -1,30 +1,30 @@
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 noMutateStoreState: Rule = {
5
5
  meta: {
6
- id: "pyreon/no-mutate-store-state",
7
- category: "store",
8
- description: "Warn when directly calling .set() on store signals — use store actions instead.",
9
- severity: "warn",
6
+ id: 'pyreon/no-mutate-store-state',
7
+ category: 'store',
8
+ description: 'Warn when directly calling .set() on store signals — use store actions instead.',
9
+ severity: 'warn',
10
10
  fixable: false,
11
11
  },
12
12
  create(context) {
13
13
  const callbacks: VisitorCallbacks = {
14
14
  CallExpression(node: any) {
15
15
  const callee = node.callee
16
- if (!callee || callee.type !== "MemberExpression") return
17
- if (callee.property?.type !== "Identifier" || callee.property.name !== "set") return
16
+ if (!callee || callee.type !== 'MemberExpression') return
17
+ if (callee.property?.type !== 'Identifier' || callee.property.name !== 'set') return
18
18
 
19
19
  // Check for store.signal.set() pattern — member.member.set()
20
20
  const obj = callee.object
21
- if (!obj || obj.type !== "MemberExpression") return
21
+ if (!obj || obj.type !== 'MemberExpression') return
22
22
  const outerObj = obj.object
23
- if (!outerObj || outerObj.type !== "Identifier") return
23
+ if (!outerObj || outerObj.type !== 'Identifier') return
24
24
 
25
25
  const name: string = outerObj.name
26
26
  // Heuristic: if the outer object name contains "store" (case-insensitive)
27
- if (name.toLowerCase().includes("store")) {
27
+ if (name.toLowerCase().includes('store')) {
28
28
  context.report({
29
29
  message: `Direct \`.set()\` on store state \`${name}\` — use store actions to mutate state for better traceability.`,
30
30
  span: getSpan(node),