@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,30 +1,30 @@
1
- import type { Rule, VisitorCallbacks } from "../../types"
2
- import { getJSXAttribute, getSpan, hasJSXAttribute } from "../../utils/ast"
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getJSXAttribute, getSpan, hasJSXAttribute } from '../../utils/ast'
3
3
 
4
4
  export const useByNotKey: Rule = {
5
5
  meta: {
6
- id: "pyreon/use-by-not-key",
7
- category: "jsx",
6
+ id: 'pyreon/use-by-not-key',
7
+ category: 'jsx',
8
8
  description:
9
- "Use `by` prop on <For> instead of `key` — JSX reserves `key` for VNode reconciliation.",
10
- severity: "error",
9
+ 'Use `by` prop on <For> instead of `key` — JSX reserves `key` for VNode reconciliation.',
10
+ severity: 'error',
11
11
  fixable: true,
12
12
  },
13
13
  create(context) {
14
14
  const callbacks: VisitorCallbacks = {
15
15
  JSXOpeningElement(node: any) {
16
- const tagName = node.name?.type === "JSXIdentifier" ? node.name.name : null
17
- if (tagName !== "For") return
18
- const keyAttr = getJSXAttribute(node, "key")
16
+ const tagName = node.name?.type === 'JSXIdentifier' ? node.name.name : null
17
+ if (tagName !== 'For') return
18
+ const keyAttr = getJSXAttribute(node, 'key')
19
19
  if (!keyAttr) return
20
- if (hasJSXAttribute(node, "by")) return // already has by
20
+ if (hasJSXAttribute(node, 'by')) return // already has by
21
21
 
22
22
  const attrSpan = getSpan(keyAttr.name)
23
23
  context.report({
24
24
  message:
25
- "Use `by` prop on `<For>` instead of `key` — JSX reserves `key` for VNode reconciliation.",
25
+ 'Use `by` prop on `<For>` instead of `key` — JSX reserves `key` for VNode reconciliation.',
26
26
  span: getSpan(keyAttr),
27
- fix: { span: attrSpan, replacement: "by" },
27
+ fix: { span: attrSpan, replacement: 'by' },
28
28
  })
29
29
  },
30
30
  }
@@ -1,27 +1,27 @@
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
  const DOM_METHODS = new Set([
5
- "querySelector",
6
- "querySelectorAll",
7
- "getElementById",
8
- "getElementsByClassName",
9
- "getElementsByTagName",
5
+ 'querySelector',
6
+ 'querySelectorAll',
7
+ 'getElementById',
8
+ 'getElementsByClassName',
9
+ 'getElementsByTagName',
10
10
  ])
11
11
 
12
12
  export const noDomInSetup: Rule = {
13
13
  meta: {
14
- id: "pyreon/no-dom-in-setup",
15
- category: "lifecycle",
16
- description: "Warn when DOM query methods are used outside onMount or effect.",
17
- severity: "warn",
14
+ id: 'pyreon/no-dom-in-setup',
15
+ category: 'lifecycle',
16
+ description: 'Warn when DOM query methods are used outside onMount or effect.',
17
+ severity: 'warn',
18
18
  fixable: false,
19
19
  },
20
20
  create(context) {
21
21
  let safeDepth = 0 // inside onMount or effect
22
22
  const callbacks: VisitorCallbacks = {
23
23
  CallExpression(node: any) {
24
- if (isCallTo(node, "onMount") || isCallTo(node, "effect")) {
24
+ if (isCallTo(node, 'onMount') || isCallTo(node, 'effect')) {
25
25
  safeDepth++
26
26
  }
27
27
 
@@ -30,10 +30,10 @@ export const noDomInSetup: Rule = {
30
30
  // Check for document.querySelector() etc.
31
31
  const callee = node.callee
32
32
  if (
33
- callee?.type === "MemberExpression" &&
34
- callee.object?.type === "Identifier" &&
35
- callee.object.name === "document" &&
36
- callee.property?.type === "Identifier" &&
33
+ callee?.type === 'MemberExpression' &&
34
+ callee.object?.type === 'Identifier' &&
35
+ callee.object.name === 'document' &&
36
+ callee.property?.type === 'Identifier' &&
37
37
  DOM_METHODS.has(callee.property.name)
38
38
  ) {
39
39
  context.report({
@@ -42,8 +42,8 @@ export const noDomInSetup: Rule = {
42
42
  })
43
43
  }
44
44
  },
45
- "CallExpression:exit"(node: any) {
46
- if (isCallTo(node, "onMount") || isCallTo(node, "effect")) {
45
+ 'CallExpression:exit'(node: any) {
46
+ if (isCallTo(node, 'onMount') || isCallTo(node, 'effect')) {
47
47
  safeDepth--
48
48
  }
49
49
  },
@@ -1,32 +1,32 @@
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 noEffectInMount: Rule = {
5
5
  meta: {
6
- id: "pyreon/no-effect-in-mount",
7
- category: "lifecycle",
6
+ id: 'pyreon/no-effect-in-mount',
7
+ category: 'lifecycle',
8
8
  description:
9
- "Inform when effect() is created inside onMount — effects are typically created at setup time.",
10
- severity: "info",
9
+ 'Inform when effect() is created inside onMount — effects are typically created at setup time.',
10
+ severity: 'info',
11
11
  fixable: false,
12
12
  },
13
13
  create(context) {
14
14
  let mountDepth = 0
15
15
  const callbacks: VisitorCallbacks = {
16
16
  CallExpression(node: any) {
17
- if (isCallTo(node, "onMount")) {
17
+ if (isCallTo(node, 'onMount')) {
18
18
  mountDepth++
19
19
  }
20
- if (mountDepth > 0 && isCallTo(node, "effect")) {
20
+ if (mountDepth > 0 && isCallTo(node, 'effect')) {
21
21
  context.report({
22
22
  message:
23
- "`effect()` inside `onMount` — effects are typically created at component setup time, not inside lifecycle hooks.",
23
+ '`effect()` inside `onMount` — effects are typically created at component setup time, not inside lifecycle hooks.',
24
24
  span: getSpan(node),
25
25
  })
26
26
  }
27
27
  },
28
- "CallExpression:exit"(node: any) {
29
- if (isCallTo(node, "onMount")) {
28
+ 'CallExpression:exit'(node: any) {
29
+ if (isCallTo(node, 'onMount')) {
30
30
  mountDepth--
31
31
  }
32
32
  },
@@ -1,63 +1,63 @@
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
- const NEEDS_CLEANUP = new Set(["setInterval", "addEventListener"])
4
+ const NEEDS_CLEANUP = new Set(['setInterval', 'addEventListener'])
5
5
 
6
6
  export const noMissingCleanup: Rule = {
7
7
  meta: {
8
- id: "pyreon/no-missing-cleanup",
9
- category: "lifecycle",
8
+ id: 'pyreon/no-missing-cleanup',
9
+ category: 'lifecycle',
10
10
  description:
11
- "Warn when onMount uses setInterval/addEventListener without returning a cleanup function.",
12
- severity: "warn",
11
+ 'Warn when onMount uses setInterval/addEventListener without returning a cleanup function.',
12
+ severity: 'warn',
13
13
  fixable: false,
14
14
  },
15
15
  create(context) {
16
16
  const callbacks: VisitorCallbacks = {
17
17
  CallExpression(node: any) {
18
- if (!isCallTo(node, "onMount")) return
18
+ if (!isCallTo(node, 'onMount')) return
19
19
  const args = node.arguments
20
20
  if (!args || args.length === 0) return
21
21
 
22
22
  const fn = args[0]
23
23
  if (!fn) return
24
- if (fn.type !== "ArrowFunctionExpression" && fn.type !== "FunctionExpression") return
24
+ if (fn.type !== 'ArrowFunctionExpression' && fn.type !== 'FunctionExpression') return
25
25
 
26
26
  const body = fn.body
27
27
  if (!body) return
28
28
 
29
29
  // Only check block bodies
30
- if (body.type !== "BlockStatement") return
30
+ if (body.type !== 'BlockStatement') return
31
31
 
32
32
  let hasCleanupTarget = false
33
33
  let hasReturn = false
34
34
 
35
35
  function walk(n: any) {
36
36
  if (!n) return
37
- if (n.type === "CallExpression") {
37
+ if (n.type === 'CallExpression') {
38
38
  const callee = n.callee
39
- if (callee?.type === "Identifier" && NEEDS_CLEANUP.has(callee.name)) {
39
+ if (callee?.type === 'Identifier' && NEEDS_CLEANUP.has(callee.name)) {
40
40
  hasCleanupTarget = true
41
41
  }
42
42
  if (
43
- callee?.type === "MemberExpression" &&
44
- callee.property?.type === "Identifier" &&
43
+ callee?.type === 'MemberExpression' &&
44
+ callee.property?.type === 'Identifier' &&
45
45
  NEEDS_CLEANUP.has(callee.property.name)
46
46
  ) {
47
47
  hasCleanupTarget = true
48
48
  }
49
49
  }
50
- if (n.type === "ReturnStatement" && n.argument) {
50
+ if (n.type === 'ReturnStatement' && n.argument) {
51
51
  hasReturn = true
52
52
  }
53
53
  for (const key of Object.keys(n)) {
54
54
  const child = n[key]
55
- if (child && typeof child === "object") {
55
+ if (child && typeof child === 'object') {
56
56
  if (Array.isArray(child)) {
57
57
  for (const item of child) {
58
- if (item && typeof item.type === "string") walk(item)
58
+ if (item && typeof item.type === 'string') walk(item)
59
59
  }
60
- } else if (typeof child.type === "string") {
60
+ } else if (typeof child.type === 'string') {
61
61
  walk(child)
62
62
  }
63
63
  }
@@ -69,7 +69,7 @@ export const noMissingCleanup: Rule = {
69
69
  if (hasCleanupTarget && !hasReturn) {
70
70
  context.report({
71
71
  message:
72
- "`onMount` uses `setInterval`/`addEventListener` without returning a cleanup function — this will cause a memory leak.",
72
+ '`onMount` uses `setInterval`/`addEventListener` without returning a cleanup function — this will cause a memory leak.',
73
73
  span: getSpan(node),
74
74
  })
75
75
  }
@@ -1,31 +1,31 @@
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 noMountInEffect: Rule = {
5
5
  meta: {
6
- id: "pyreon/no-mount-in-effect",
7
- category: "lifecycle",
8
- description: "Warn when onMount is called inside effect().",
9
- severity: "warn",
6
+ id: 'pyreon/no-mount-in-effect',
7
+ category: 'lifecycle',
8
+ description: 'Warn when onMount is called inside 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")) {
16
+ if (isCallTo(node, 'effect')) {
17
17
  effectDepth++
18
18
  }
19
- if (effectDepth > 0 && isCallTo(node, "onMount")) {
19
+ if (effectDepth > 0 && isCallTo(node, 'onMount')) {
20
20
  context.report({
21
21
  message:
22
- "`onMount` inside `effect()` — `onMount` runs once on mount, not on every effect re-run.",
22
+ '`onMount` inside `effect()` — `onMount` runs once on mount, not on every effect re-run.',
23
23
  span: getSpan(node),
24
24
  })
25
25
  }
26
26
  },
27
- "CallExpression:exit"(node: any) {
28
- if (isCallTo(node, "effect")) {
27
+ 'CallExpression:exit'(node: any) {
28
+ if (isCallTo(node, 'effect')) {
29
29
  effectDepth--
30
30
  }
31
31
  },
@@ -1,13 +1,13 @@
1
- import type { Rule, VisitorCallbacks } from "../../types"
2
- import { getSpan } from "../../utils/ast"
3
- import { HEAVY_PACKAGES } from "../../utils/imports"
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getSpan } from '../../utils/ast'
3
+ import { HEAVY_PACKAGES } from '../../utils/imports'
4
4
 
5
5
  export const noEagerImport: Rule = {
6
6
  meta: {
7
- id: "pyreon/no-eager-import",
8
- category: "performance",
9
- description: "Suggest lazy-loading heavy Pyreon packages (charts, code, document, flow).",
10
- severity: "info",
7
+ id: 'pyreon/no-eager-import',
8
+ category: 'performance',
9
+ description: 'Suggest lazy-loading heavy Pyreon packages (charts, code, document, flow).',
10
+ severity: 'info',
11
11
  fixable: false,
12
12
  },
13
13
  create(context) {
@@ -1,13 +1,13 @@
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 noEffectInFor: Rule = {
5
5
  meta: {
6
- id: "pyreon/no-effect-in-for",
7
- category: "performance",
6
+ id: 'pyreon/no-effect-in-for',
7
+ category: 'performance',
8
8
  description:
9
- "Warn when effect() is created inside <For> — creates effects per item on every reconciliation.",
10
- severity: "warn",
9
+ 'Warn when effect() is created inside <For> — creates effects per item on every reconciliation.',
10
+ severity: 'warn',
11
11
  fixable: false,
12
12
  },
13
13
  create(context) {
@@ -15,22 +15,22 @@ export const noEffectInFor: Rule = {
15
15
  const callbacks: VisitorCallbacks = {
16
16
  JSXOpeningElement(node: any) {
17
17
  const name = node.name
18
- if (name?.type === "JSXIdentifier" && name.name === "For") {
18
+ if (name?.type === 'JSXIdentifier' && name.name === 'For') {
19
19
  forJsxDepth++
20
20
  }
21
21
  },
22
22
  JSXClosingElement(node: any) {
23
23
  const name = node.name
24
- if (name?.type === "JSXIdentifier" && name.name === "For") {
24
+ if (name?.type === 'JSXIdentifier' && name.name === 'For') {
25
25
  forJsxDepth--
26
26
  }
27
27
  },
28
28
  CallExpression(node: any) {
29
29
  if (forJsxDepth === 0) return
30
- if (isCallTo(node, "effect")) {
30
+ if (isCallTo(node, 'effect')) {
31
31
  context.report({
32
32
  message:
33
- "`effect()` inside `<For>` — this creates a new effect for every item on each reconciliation. Lift the effect outside.",
33
+ '`effect()` inside `<For>` — this creates a new effect for every item on each reconciliation. Lift the effect outside.',
34
34
  span: getSpan(node),
35
35
  })
36
36
  }
@@ -1,24 +1,24 @@
1
- import type { Rule, VisitorCallbacks } from "../../types"
2
- import { getSpan, hasJSXAttribute } from "../../utils/ast"
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getSpan, hasJSXAttribute } from '../../utils/ast'
3
3
 
4
4
  export const noLargeForWithoutBy: Rule = {
5
5
  meta: {
6
- id: "pyreon/no-large-for-without-by",
7
- category: "performance",
6
+ id: 'pyreon/no-large-for-without-by',
7
+ category: 'performance',
8
8
  description:
9
- "Error when <For> is used without a `by` prop — critical for reconciliation performance.",
10
- severity: "error",
9
+ 'Error when <For> is used without a `by` prop — critical for reconciliation performance.',
10
+ severity: 'error',
11
11
  fixable: false,
12
12
  },
13
13
  create(context) {
14
14
  const callbacks: VisitorCallbacks = {
15
15
  JSXOpeningElement(node: any) {
16
16
  const name = node.name
17
- if (!name || name.type !== "JSXIdentifier" || name.name !== "For") return
18
- if (hasJSXAttribute(node, "by")) return
17
+ if (!name || name.type !== 'JSXIdentifier' || name.name !== 'For') return
18
+ if (hasJSXAttribute(node, 'by')) return
19
19
  context.report({
20
20
  message:
21
- "`<For>` without `by` prop — provide a key function for efficient reconciliation.",
21
+ '`<For>` without `by` prop — provide a key function for efficient reconciliation.',
22
22
  span: getSpan(node),
23
23
  })
24
24
  },
@@ -1,40 +1,40 @@
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 preferShowOverDisplay: Rule = {
5
5
  meta: {
6
- id: "pyreon/prefer-show-over-display",
7
- category: "performance",
8
- description: "Suggest <Show> over conditional `display` style property in JSX.",
9
- severity: "info",
6
+ id: 'pyreon/prefer-show-over-display',
7
+ category: 'performance',
8
+ description: 'Suggest <Show> over conditional `display` style property in JSX.',
9
+ severity: 'info',
10
10
  fixable: false,
11
11
  },
12
12
  create(context) {
13
13
  const callbacks: VisitorCallbacks = {
14
14
  JSXAttribute(node: any) {
15
- if (node.name?.type !== "JSXIdentifier" || node.name.name !== "style") return
15
+ if (node.name?.type !== 'JSXIdentifier' || node.name.name !== 'style') return
16
16
  const value = node.value
17
- if (!value || value.type !== "JSXExpressionContainer") return
17
+ if (!value || value.type !== 'JSXExpressionContainer') return
18
18
  const expr = value.expression
19
- if (!expr || expr.type !== "ObjectExpression") return
19
+ if (!expr || expr.type !== 'ObjectExpression') return
20
20
 
21
21
  for (const prop of expr.properties ?? []) {
22
- if (prop.type !== "Property") continue
22
+ if (prop.type !== 'Property') continue
23
23
  const key = prop.key
24
24
  if (!key) continue
25
25
  const propName =
26
- key.type === "Identifier" ? key.name : key.type === "Literal" ? key.value : null
27
- if (propName === "display") {
26
+ key.type === 'Identifier' ? key.name : key.type === 'Literal' ? key.value : null
27
+ if (propName === 'display') {
28
28
  // Check if the value is conditional
29
29
  const val = prop.value
30
30
  if (
31
- val?.type === "ConditionalExpression" ||
32
- val?.type === "LogicalExpression" ||
33
- val?.type === "CallExpression"
31
+ val?.type === 'ConditionalExpression' ||
32
+ val?.type === 'LogicalExpression' ||
33
+ val?.type === 'CallExpression'
34
34
  ) {
35
35
  context.report({
36
36
  message:
37
- "Conditional `display` style — consider using `<Show>` for conditional rendering instead of toggling CSS display.",
37
+ 'Conditional `display` style — consider using `<Show>` for conditional rendering instead of toggling CSS display.',
38
38
  span: getSpan(prop),
39
39
  })
40
40
  }
@@ -1,15 +1,15 @@
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
  const SKIP_PREFIXES = /^(use|get|is|has|[A-Z])/
5
5
 
6
6
  export const noBareSignalInJsx: Rule = {
7
7
  meta: {
8
- id: "pyreon/no-bare-signal-in-jsx",
9
- category: "reactivity",
8
+ id: 'pyreon/no-bare-signal-in-jsx',
9
+ category: 'reactivity',
10
10
  description:
11
- "Disallow bare signal calls in JSX text positions. Wrap in `() =>` for reactivity.",
12
- severity: "error",
11
+ 'Disallow bare signal calls in JSX text positions. Wrap in `() =>` for reactivity.',
12
+ severity: 'error',
13
13
  fixable: true,
14
14
  },
15
15
  create(context) {
@@ -18,21 +18,21 @@ export const noBareSignalInJsx: Rule = {
18
18
  JSXElement() {
19
19
  jsxDepth++
20
20
  },
21
- "JSXElement:exit"() {
21
+ 'JSXElement:exit'() {
22
22
  jsxDepth--
23
23
  },
24
24
  JSXFragment() {
25
25
  jsxDepth++
26
26
  },
27
- "JSXFragment:exit"() {
27
+ 'JSXFragment:exit'() {
28
28
  jsxDepth--
29
29
  },
30
30
  JSXExpressionContainer(node: any) {
31
31
  if (jsxDepth === 0) return
32
32
  const expr = node.expression
33
- if (!expr || expr.type !== "CallExpression") return
33
+ if (!expr || expr.type !== 'CallExpression') return
34
34
  const callee = expr.callee
35
- if (!callee || callee.type !== "Identifier") return
35
+ if (!callee || callee.type !== 'Identifier') return
36
36
 
37
37
  const name: string = callee.name
38
38
  if (SKIP_PREFIXES.test(name)) return
@@ -0,0 +1,45 @@
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getSpan } from '../../utils/ast'
3
+
4
+ /**
5
+ * Detects destructuring the return value of useContext().
6
+ *
7
+ * `const { mode } = useContext(ctx)` loses reactivity when the context
8
+ * provides getter properties. The value is captured once at setup time.
9
+ *
10
+ * Correct: `const ctx = useContext(Ctx)` then read `ctx.mode` lazily.
11
+ */
12
+ export const noContextDestructure: Rule = {
13
+ meta: {
14
+ id: 'pyreon/no-context-destructure',
15
+ category: 'reactivity',
16
+ description:
17
+ 'Disallow destructuring useContext() — it breaks reactivity when context provides getters.',
18
+ severity: 'warn',
19
+ fixable: false,
20
+ },
21
+ create(context) {
22
+ const callbacks: VisitorCallbacks = {
23
+ VariableDeclarator(node: any) {
24
+ // Match: const { x } = useContext(...)
25
+ const id = node.id
26
+ const init = node.init
27
+ if (!id || !init) return
28
+ if (id.type !== 'ObjectPattern') return
29
+ if (
30
+ init.type !== 'CallExpression' ||
31
+ init.callee?.type !== 'Identifier' ||
32
+ init.callee.name !== 'useContext'
33
+ )
34
+ return
35
+
36
+ context.report({
37
+ message:
38
+ 'Destructuring useContext() captures values once — reactive getters lose reactivity. Keep the object reference: `const ctx = useContext(Ctx)` and access `ctx.mode` lazily.',
39
+ span: getSpan(id),
40
+ })
41
+ },
42
+ }
43
+ return callbacks
44
+ },
45
+ }
@@ -1,27 +1,27 @@
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
  function isUpdateCall(node: any): boolean {
5
5
  return (
6
- node.type === "CallExpression" &&
7
- node.callee?.type === "MemberExpression" &&
8
- node.callee.property?.type === "Identifier" &&
9
- node.callee.property.name === "update"
6
+ node.type === 'CallExpression' &&
7
+ node.callee?.type === 'MemberExpression' &&
8
+ node.callee.property?.type === 'Identifier' &&
9
+ node.callee.property.name === 'update'
10
10
  )
11
11
  }
12
12
 
13
13
  export const noEffectAssignment: Rule = {
14
14
  meta: {
15
- id: "pyreon/no-effect-assignment",
16
- category: "reactivity",
17
- description: "Warn when an effect only contains a single .update() call.",
18
- severity: "warn",
15
+ id: 'pyreon/no-effect-assignment',
16
+ category: 'reactivity',
17
+ description: 'Warn when an effect only contains a single .update() call.',
18
+ severity: 'warn',
19
19
  fixable: false,
20
20
  },
21
21
  create(context) {
22
22
  const callbacks: VisitorCallbacks = {
23
23
  CallExpression(node: any) {
24
- if (!isCallTo(node, "effect")) return
24
+ if (!isCallTo(node, 'effect')) return
25
25
  const args = node.arguments
26
26
  if (!args || args.length === 0) return
27
27
 
@@ -29,7 +29,7 @@ export const noEffectAssignment: Rule = {
29
29
  if (!fn) return
30
30
 
31
31
  let body: any = null
32
- if (fn.type === "ArrowFunctionExpression" || fn.type === "FunctionExpression") {
32
+ if (fn.type === 'ArrowFunctionExpression' || fn.type === 'FunctionExpression') {
33
33
  body = fn.body
34
34
  }
35
35
  if (!body) return
@@ -38,21 +38,21 @@ export const noEffectAssignment: Rule = {
38
38
  if (isUpdateCall(body)) {
39
39
  context.report({
40
40
  message:
41
- "Effect contains a single `.update()` — consider using `computed()` for derived values.",
41
+ 'Effect contains a single `.update()` — consider using `computed()` for derived values.',
42
42
  span: getSpan(node),
43
43
  })
44
44
  return
45
45
  }
46
46
 
47
47
  // Block body with single statement
48
- if (body.type === "BlockStatement") {
48
+ if (body.type === 'BlockStatement') {
49
49
  const stmts = body.body
50
50
  if (stmts && stmts.length === 1) {
51
51
  const stmt = stmts[0]
52
- if (stmt.type === "ExpressionStatement" && isUpdateCall(stmt.expression)) {
52
+ if (stmt.type === 'ExpressionStatement' && isUpdateCall(stmt.expression)) {
53
53
  context.report({
54
54
  message:
55
- "Effect contains a single `.update()` — consider using `computed()` for derived values.",
55
+ 'Effect contains a single `.update()` — consider using `computed()` for derived values.',
56
56
  span: getSpan(node),
57
57
  })
58
58
  }