@pyreon/lint 0.11.0

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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +214 -0
  3. package/lib/analysis/index.js.html +5406 -0
  4. package/lib/index.js +2955 -0
  5. package/lib/index.js.map +1 -0
  6. package/lib/types/index.d.ts +260 -0
  7. package/lib/types/index.d.ts.map +1 -0
  8. package/package.json +56 -0
  9. package/src/cache.ts +51 -0
  10. package/src/cli.ts +199 -0
  11. package/src/config/ignore.ts +159 -0
  12. package/src/config/loader.ts +72 -0
  13. package/src/config/presets.ts +62 -0
  14. package/src/index.ts +40 -0
  15. package/src/lint.ts +226 -0
  16. package/src/reporter.ts +85 -0
  17. package/src/rules/accessibility/dialog-a11y.ts +32 -0
  18. package/src/rules/accessibility/overlay-a11y.ts +33 -0
  19. package/src/rules/accessibility/toast-a11y.ts +38 -0
  20. package/src/rules/architecture/dev-guard-warnings.ts +57 -0
  21. package/src/rules/architecture/no-circular-import.ts +59 -0
  22. package/src/rules/architecture/no-cross-layer-import.ts +75 -0
  23. package/src/rules/architecture/no-deep-import.ts +32 -0
  24. package/src/rules/architecture/no-error-without-prefix.ts +75 -0
  25. package/src/rules/form/no-submit-without-validation.ts +45 -0
  26. package/src/rules/form/no-unregistered-field.ts +45 -0
  27. package/src/rules/form/prefer-field-array.ts +41 -0
  28. package/src/rules/hooks/no-raw-addeventlistener.ts +28 -0
  29. package/src/rules/hooks/no-raw-localstorage.ts +35 -0
  30. package/src/rules/hooks/no-raw-setinterval.ts +41 -0
  31. package/src/rules/index.ts +208 -0
  32. package/src/rules/jsx/no-and-conditional.ts +32 -0
  33. package/src/rules/jsx/no-children-access.ts +44 -0
  34. package/src/rules/jsx/no-classname.ts +27 -0
  35. package/src/rules/jsx/no-htmlfor.ts +27 -0
  36. package/src/rules/jsx/no-index-as-by.ts +70 -0
  37. package/src/rules/jsx/no-map-in-jsx.ts +43 -0
  38. package/src/rules/jsx/no-missing-for-by.ts +27 -0
  39. package/src/rules/jsx/no-onchange.ts +46 -0
  40. package/src/rules/jsx/no-props-destructure.ts +64 -0
  41. package/src/rules/jsx/no-ternary-conditional.ts +32 -0
  42. package/src/rules/jsx/use-by-not-key.ts +33 -0
  43. package/src/rules/lifecycle/no-dom-in-setup.ts +53 -0
  44. package/src/rules/lifecycle/no-effect-in-mount.ts +36 -0
  45. package/src/rules/lifecycle/no-missing-cleanup.ts +80 -0
  46. package/src/rules/lifecycle/no-mount-in-effect.ts +35 -0
  47. package/src/rules/performance/no-eager-import.ts +28 -0
  48. package/src/rules/performance/no-effect-in-for.ts +41 -0
  49. package/src/rules/performance/no-large-for-without-by.ts +28 -0
  50. package/src/rules/performance/prefer-show-over-display.ts +47 -0
  51. package/src/rules/reactivity/no-bare-signal-in-jsx.ts +56 -0
  52. package/src/rules/reactivity/no-effect-assignment.ts +65 -0
  53. package/src/rules/reactivity/no-nested-effect.ts +33 -0
  54. package/src/rules/reactivity/no-peek-in-tracked.ts +35 -0
  55. package/src/rules/reactivity/no-signal-in-loop.ts +59 -0
  56. package/src/rules/reactivity/no-signal-leak.ts +58 -0
  57. package/src/rules/reactivity/no-unbatched-updates.ts +77 -0
  58. package/src/rules/reactivity/prefer-computed.ts +56 -0
  59. package/src/rules/router/index.ts +4 -0
  60. package/src/rules/router/no-href-navigation.ts +51 -0
  61. package/src/rules/router/no-imperative-navigate-in-render.ts +83 -0
  62. package/src/rules/router/no-missing-fallback.ts +87 -0
  63. package/src/rules/router/prefer-use-is-active.ts +45 -0
  64. package/src/rules/ssr/no-mismatch-risk.ts +47 -0
  65. package/src/rules/ssr/no-window-in-ssr.ts +76 -0
  66. package/src/rules/ssr/prefer-request-context.ts +56 -0
  67. package/src/rules/store/no-duplicate-store-id.ts +43 -0
  68. package/src/rules/store/no-mutate-store-state.ts +37 -0
  69. package/src/rules/store/no-store-outside-provider.ts +59 -0
  70. package/src/rules/styling/no-dynamic-styled.ts +60 -0
  71. package/src/rules/styling/no-inline-style-object.ts +30 -0
  72. package/src/rules/styling/no-theme-outside-provider.ts +45 -0
  73. package/src/rules/styling/prefer-cx.ts +44 -0
  74. package/src/runner.ts +170 -0
  75. package/src/tests/runner.test.ts +1043 -0
  76. package/src/types.ts +125 -0
  77. package/src/utils/ast.ts +192 -0
  78. package/src/utils/imports.ts +122 -0
  79. package/src/utils/index.ts +39 -0
  80. package/src/utils/source.ts +36 -0
  81. package/src/watcher.ts +118 -0
@@ -0,0 +1,208 @@
1
+ import type { Rule } from "../types"
2
+ import { dialogA11y } from "./accessibility/dialog-a11y"
3
+ import { overlayA11y } from "./accessibility/overlay-a11y"
4
+ // Accessibility
5
+ import { toastA11y } from "./accessibility/toast-a11y"
6
+ import { devGuardWarnings } from "./architecture/dev-guard-warnings"
7
+ // Architecture
8
+ import { noCircularImport } from "./architecture/no-circular-import"
9
+ import { noCrossLayerImport } from "./architecture/no-cross-layer-import"
10
+ import { noDeepImport } from "./architecture/no-deep-import"
11
+ import { noErrorWithoutPrefix } from "./architecture/no-error-without-prefix"
12
+ import { noSubmitWithoutValidation } from "./form/no-submit-without-validation"
13
+ // Form
14
+ import { noUnregisteredField } from "./form/no-unregistered-field"
15
+ import { preferFieldArray } from "./form/prefer-field-array"
16
+ // Hooks
17
+ import { noRawAddEventListener } from "./hooks/no-raw-addeventlistener"
18
+ import { noRawLocalStorage } from "./hooks/no-raw-localstorage"
19
+ import { noRawSetInterval } from "./hooks/no-raw-setinterval"
20
+ import { noAndConditional } from "./jsx/no-and-conditional"
21
+ import { noChildrenAccess } from "./jsx/no-children-access"
22
+ import { noClassName } from "./jsx/no-classname"
23
+ import { noHtmlFor } from "./jsx/no-htmlfor"
24
+ import { noIndexAsBy } from "./jsx/no-index-as-by"
25
+ // JSX
26
+ import { noMapInJsx } from "./jsx/no-map-in-jsx"
27
+ import { noMissingForBy } from "./jsx/no-missing-for-by"
28
+ import { noOnChange } from "./jsx/no-onchange"
29
+ import { noPropsDestructure } from "./jsx/no-props-destructure"
30
+ import { noTernaryConditional } from "./jsx/no-ternary-conditional"
31
+ import { useByNotKey } from "./jsx/use-by-not-key"
32
+ import { noDomInSetup } from "./lifecycle/no-dom-in-setup"
33
+ import { noEffectInMount } from "./lifecycle/no-effect-in-mount"
34
+ // Lifecycle
35
+ import { noMissingCleanup } from "./lifecycle/no-missing-cleanup"
36
+ import { noMountInEffect } from "./lifecycle/no-mount-in-effect"
37
+ import { noEagerImport } from "./performance/no-eager-import"
38
+ import { noEffectInFor } from "./performance/no-effect-in-for"
39
+ // Performance
40
+ import { noLargeForWithoutBy } from "./performance/no-large-for-without-by"
41
+ import { preferShowOverDisplay } from "./performance/prefer-show-over-display"
42
+ // Reactivity
43
+ import { noBareSignalInJsx } from "./reactivity/no-bare-signal-in-jsx"
44
+ import { noEffectAssignment } from "./reactivity/no-effect-assignment"
45
+ import { noNestedEffect } from "./reactivity/no-nested-effect"
46
+ import { noPeekInTracked } from "./reactivity/no-peek-in-tracked"
47
+ import { noSignalInLoop } from "./reactivity/no-signal-in-loop"
48
+ import { noSignalLeak } from "./reactivity/no-signal-leak"
49
+ import { noUnbatchedUpdates } from "./reactivity/no-unbatched-updates"
50
+ import { preferComputed } from "./reactivity/prefer-computed"
51
+ // Router
52
+ import { noHrefNavigation } from "./router/no-href-navigation"
53
+ import { noImperativeNavigateInRender } from "./router/no-imperative-navigate-in-render"
54
+ import { noMissingFallback } from "./router/no-missing-fallback"
55
+ import { preferUseIsActive } from "./router/prefer-use-is-active"
56
+ import { noMismatchRisk } from "./ssr/no-mismatch-risk"
57
+ // SSR
58
+ import { noWindowInSsr } from "./ssr/no-window-in-ssr"
59
+ import { preferRequestContext } from "./ssr/prefer-request-context"
60
+ import { noDuplicateStoreId } from "./store/no-duplicate-store-id"
61
+ import { noMutateStoreState } from "./store/no-mutate-store-state"
62
+ // Store
63
+ import { noStoreOutsideProvider } from "./store/no-store-outside-provider"
64
+ import { noDynamicStyled } from "./styling/no-dynamic-styled"
65
+ // Styling
66
+ import { noInlineStyleObject } from "./styling/no-inline-style-object"
67
+ import { noThemeOutsideProvider } from "./styling/no-theme-outside-provider"
68
+ import { preferCx } from "./styling/prefer-cx"
69
+
70
+ export const allRules: Rule[] = [
71
+ // Reactivity (8)
72
+ noBareSignalInJsx,
73
+ noSignalInLoop,
74
+ noNestedEffect,
75
+ noPeekInTracked,
76
+ noUnbatchedUpdates,
77
+ preferComputed,
78
+ noEffectAssignment,
79
+ noSignalLeak,
80
+ // JSX (11)
81
+ noMapInJsx,
82
+ useByNotKey,
83
+ noClassName,
84
+ noHtmlFor,
85
+ noOnChange,
86
+ noTernaryConditional,
87
+ noAndConditional,
88
+ noIndexAsBy,
89
+ noMissingForBy,
90
+ noPropsDestructure,
91
+ noChildrenAccess,
92
+ // Lifecycle (4)
93
+ noMissingCleanup,
94
+ noMountInEffect,
95
+ noEffectInMount,
96
+ noDomInSetup,
97
+ // Performance (4)
98
+ noLargeForWithoutBy,
99
+ noEffectInFor,
100
+ noEagerImport,
101
+ preferShowOverDisplay,
102
+ // SSR (3)
103
+ noWindowInSsr,
104
+ noMismatchRisk,
105
+ preferRequestContext,
106
+ // Architecture (5)
107
+ noCircularImport,
108
+ noDeepImport,
109
+ noCrossLayerImport,
110
+ devGuardWarnings,
111
+ noErrorWithoutPrefix,
112
+ // Store (3)
113
+ noStoreOutsideProvider,
114
+ noMutateStoreState,
115
+ noDuplicateStoreId,
116
+ // Form (3)
117
+ noUnregisteredField,
118
+ noSubmitWithoutValidation,
119
+ preferFieldArray,
120
+ // Styling (4)
121
+ noInlineStyleObject,
122
+ noDynamicStyled,
123
+ preferCx,
124
+ noThemeOutsideProvider,
125
+ // Hooks (3)
126
+ noRawAddEventListener,
127
+ noRawSetInterval,
128
+ noRawLocalStorage,
129
+ // Accessibility (3)
130
+ toastA11y,
131
+ dialogA11y,
132
+ overlayA11y,
133
+ // Router (4)
134
+ noHrefNavigation,
135
+ noImperativeNavigateInRender,
136
+ noMissingFallback,
137
+ preferUseIsActive,
138
+ ]
139
+
140
+ // Re-export all rules individually
141
+ export {
142
+ devGuardWarnings,
143
+ dialogA11y,
144
+ noAndConditional,
145
+ // Reactivity
146
+ noBareSignalInJsx,
147
+ noChildrenAccess,
148
+ // Architecture
149
+ noCircularImport,
150
+ noClassName,
151
+ noCrossLayerImport,
152
+ noDeepImport,
153
+ noDomInSetup,
154
+ noDuplicateStoreId,
155
+ noDynamicStyled,
156
+ noEagerImport,
157
+ noEffectAssignment,
158
+ noEffectInFor,
159
+ noEffectInMount,
160
+ noErrorWithoutPrefix,
161
+ noHrefNavigation,
162
+ noHtmlFor,
163
+ noImperativeNavigateInRender,
164
+ noIndexAsBy,
165
+ // Styling
166
+ noInlineStyleObject,
167
+ // Performance
168
+ noLargeForWithoutBy,
169
+ // JSX
170
+ noMapInJsx,
171
+ noMismatchRisk,
172
+ // Lifecycle
173
+ noMissingCleanup,
174
+ noMissingFallback,
175
+ noMissingForBy,
176
+ noMountInEffect,
177
+ noMutateStoreState,
178
+ noNestedEffect,
179
+ noOnChange,
180
+ noPeekInTracked,
181
+ noPropsDestructure,
182
+ // Hooks
183
+ noRawAddEventListener,
184
+ noRawLocalStorage,
185
+ noRawSetInterval,
186
+ noSignalInLoop,
187
+ noSignalLeak,
188
+ // Store
189
+ noStoreOutsideProvider,
190
+ noSubmitWithoutValidation,
191
+ noTernaryConditional,
192
+ noThemeOutsideProvider,
193
+ noUnbatchedUpdates,
194
+ // Form
195
+ noUnregisteredField,
196
+ // SSR
197
+ noWindowInSsr,
198
+ overlayA11y,
199
+ preferComputed,
200
+ preferCx,
201
+ preferFieldArray,
202
+ preferRequestContext,
203
+ preferShowOverDisplay,
204
+ preferUseIsActive,
205
+ // Accessibility
206
+ toastA11y,
207
+ useByNotKey,
208
+ }
@@ -0,0 +1,32 @@
1
+ import type { Rule, VisitorCallbacks } from "../../types"
2
+ import { getSpan, isLogicalAndWithJSX } from "../../utils/ast"
3
+
4
+ export const noAndConditional: Rule = {
5
+ meta: {
6
+ id: "pyreon/no-and-conditional",
7
+ category: "jsx",
8
+ description: "Prefer <Show> over `&&` with JSX in expression containers.",
9
+ severity: "warn",
10
+ fixable: false,
11
+ },
12
+ create(context) {
13
+ let jsxExpressionDepth = 0
14
+ const callbacks: VisitorCallbacks = {
15
+ JSXExpressionContainer() {
16
+ jsxExpressionDepth++
17
+ },
18
+ "JSXExpressionContainer:exit"() {
19
+ jsxExpressionDepth--
20
+ },
21
+ LogicalExpression(node: any) {
22
+ if (jsxExpressionDepth === 0) return
23
+ if (!isLogicalAndWithJSX(node)) return
24
+ context.report({
25
+ message: "`&&` with JSX — use `<Show>` for conditional rendering.",
26
+ span: getSpan(node),
27
+ })
28
+ },
29
+ }
30
+ return callbacks
31
+ },
32
+ }
@@ -0,0 +1,44 @@
1
+ import type { Rule, VisitorCallbacks } from "../../types"
2
+ import { getSpan } from "../../utils/ast"
3
+ import { extractImportInfo, type ImportInfo } from "../../utils/imports"
4
+
5
+ export const noChildrenAccess: Rule = {
6
+ meta: {
7
+ id: "pyreon/no-children-access",
8
+ category: "jsx",
9
+ description: "Inform about direct props.children access in renderer files.",
10
+ severity: "info",
11
+ fixable: false,
12
+ },
13
+ create(context) {
14
+ const imports: ImportInfo[] = []
15
+ let isRendererFile = false
16
+
17
+ const callbacks: VisitorCallbacks = {
18
+ ImportDeclaration(node: any) {
19
+ const info = extractImportInfo(node)
20
+ if (info) {
21
+ imports.push(info)
22
+ if (info.source === "@pyreon/runtime-server" || info.source === "@pyreon/runtime-dom") {
23
+ isRendererFile = true
24
+ }
25
+ }
26
+ },
27
+ MemberExpression(node: any) {
28
+ if (!isRendererFile) return
29
+ if (
30
+ node.object?.type === "Identifier" &&
31
+ node.property?.type === "Identifier" &&
32
+ node.property.name === "children"
33
+ ) {
34
+ context.report({
35
+ message:
36
+ "Direct `props.children` access in a renderer file — children are already merged via `mergeChildrenIntoProps`.",
37
+ span: getSpan(node),
38
+ })
39
+ }
40
+ },
41
+ }
42
+ return callbacks
43
+ },
44
+ }
@@ -0,0 +1,27 @@
1
+ import type { Rule, VisitorCallbacks } from "../../types"
2
+ import { getSpan } from "../../utils/ast"
3
+
4
+ export const noClassName: Rule = {
5
+ meta: {
6
+ id: "pyreon/no-classname",
7
+ category: "jsx",
8
+ description: "Use `class` instead of `className` — Pyreon uses standard HTML attributes.",
9
+ severity: "error",
10
+ fixable: true,
11
+ },
12
+ create(context) {
13
+ const callbacks: VisitorCallbacks = {
14
+ JSXAttribute(node: any) {
15
+ if (node.name?.type !== "JSXIdentifier") return
16
+ if (node.name.name !== "className") return
17
+ const nameSpan = getSpan(node.name)
18
+ context.report({
19
+ message: "Use `class` instead of `className` — Pyreon uses standard HTML attributes.",
20
+ span: getSpan(node),
21
+ fix: { span: nameSpan, replacement: "class" },
22
+ })
23
+ },
24
+ }
25
+ return callbacks
26
+ },
27
+ }
@@ -0,0 +1,27 @@
1
+ import type { Rule, VisitorCallbacks } from "../../types"
2
+ import { getSpan } from "../../utils/ast"
3
+
4
+ export const noHtmlFor: Rule = {
5
+ meta: {
6
+ id: "pyreon/no-htmlfor",
7
+ category: "jsx",
8
+ description: "Use `for` instead of `htmlFor` — Pyreon uses standard HTML attributes.",
9
+ severity: "error",
10
+ fixable: true,
11
+ },
12
+ create(context) {
13
+ const callbacks: VisitorCallbacks = {
14
+ JSXAttribute(node: any) {
15
+ if (node.name?.type !== "JSXIdentifier") return
16
+ if (node.name.name !== "htmlFor") return
17
+ const nameSpan = getSpan(node.name)
18
+ context.report({
19
+ message: "Use `for` instead of `htmlFor` — Pyreon uses standard HTML attributes.",
20
+ span: getSpan(node),
21
+ fix: { span: nameSpan, replacement: "for" },
22
+ })
23
+ },
24
+ }
25
+ return callbacks
26
+ },
27
+ }
@@ -0,0 +1,70 @@
1
+ import type { Rule, VisitorCallbacks } from "../../types"
2
+ import { getJSXAttribute, getSpan } from "../../utils/ast"
3
+
4
+ export const noIndexAsBy: Rule = {
5
+ meta: {
6
+ id: "pyreon/no-index-as-by",
7
+ category: "jsx",
8
+ description: "Disallow using index as `by` prop on <For> — use a unique key instead.",
9
+ severity: "warn",
10
+ fixable: false,
11
+ },
12
+ create(context) {
13
+ const callbacks: VisitorCallbacks = {
14
+ JSXOpeningElement(node: any) {
15
+ const name = node.name
16
+ if (!name || name.type !== "JSXIdentifier" || name.name !== "For") return
17
+
18
+ const byAttr = getJSXAttribute(node, "by")
19
+ if (!byAttr) return
20
+
21
+ const value = byAttr.value
22
+ if (!value || value.type !== "JSXExpressionContainer") return
23
+
24
+ const expr = value.expression
25
+ if (!expr) return
26
+
27
+ // Detect: by={(_, i) => i} or by={(item, index) => index}
28
+ if (expr.type === "ArrowFunctionExpression" || expr.type === "FunctionExpression") {
29
+ const params = expr.params
30
+ if (!params || params.length < 2) return
31
+
32
+ const secondParam = params[1]
33
+ if (!secondParam || secondParam.type !== "Identifier") return
34
+
35
+ const indexName = secondParam.name
36
+ const body = expr.body
37
+
38
+ // Arrow expression body: (_, i) => i
39
+ if (body?.type === "Identifier" && body.name === indexName) {
40
+ context.report({
41
+ message:
42
+ "Using index as `by` prop on `<For>` — use a unique key from the data instead.",
43
+ span: getSpan(byAttr),
44
+ })
45
+ }
46
+
47
+ // Block body: (_, i) => { return i }
48
+ if (body?.type === "BlockStatement") {
49
+ const stmts = body.body
50
+ if (stmts?.length === 1) {
51
+ const stmt = stmts[0]
52
+ if (
53
+ stmt.type === "ReturnStatement" &&
54
+ stmt.argument?.type === "Identifier" &&
55
+ stmt.argument.name === indexName
56
+ ) {
57
+ context.report({
58
+ message:
59
+ "Using index as `by` prop on `<For>` — use a unique key from the data instead.",
60
+ span: getSpan(byAttr),
61
+ })
62
+ }
63
+ }
64
+ }
65
+ }
66
+ },
67
+ }
68
+ return callbacks
69
+ },
70
+ }
@@ -0,0 +1,43 @@
1
+ import type { Rule, VisitorCallbacks } from "../../types"
2
+ import { getSpan, isArrayMapCall } from "../../utils/ast"
3
+
4
+ export const noMapInJsx: Rule = {
5
+ meta: {
6
+ id: "pyreon/no-map-in-jsx",
7
+ category: "jsx",
8
+ description: "Prefer <For> over .map() inside JSX for reactive list rendering.",
9
+ severity: "warn",
10
+ fixable: false,
11
+ },
12
+ create(context) {
13
+ let jsxDepth = 0
14
+ const callbacks: VisitorCallbacks = {
15
+ JSXElement() {
16
+ jsxDepth++
17
+ },
18
+ "JSXElement:exit"() {
19
+ jsxDepth--
20
+ },
21
+ JSXFragment() {
22
+ jsxDepth++
23
+ },
24
+ "JSXFragment:exit"() {
25
+ jsxDepth--
26
+ },
27
+ CallExpression(node: any) {
28
+ if (jsxDepth === 0) return
29
+ if (!isArrayMapCall(node)) return
30
+ // Check callback contains JSX
31
+ const args = node.arguments
32
+ if (!args || args.length === 0) return
33
+ const callback = args[0]
34
+ if (!callback) return
35
+ context.report({
36
+ message: "`.map()` in JSX — use `<For>` for reactive list rendering instead.",
37
+ span: getSpan(node),
38
+ })
39
+ },
40
+ }
41
+ return callbacks
42
+ },
43
+ }
@@ -0,0 +1,27 @@
1
+ import type { Rule, VisitorCallbacks } from "../../types"
2
+ import { getSpan, hasJSXAttribute } from "../../utils/ast"
3
+
4
+ export const noMissingForBy: Rule = {
5
+ meta: {
6
+ id: "pyreon/no-missing-for-by",
7
+ category: "jsx",
8
+ description: "Warn when <For> is used without a `by` prop.",
9
+ severity: "warn",
10
+ fixable: false,
11
+ },
12
+ create(context) {
13
+ const callbacks: VisitorCallbacks = {
14
+ JSXOpeningElement(node: any) {
15
+ const name = node.name
16
+ if (!name || name.type !== "JSXIdentifier" || name.name !== "For") return
17
+ if (hasJSXAttribute(node, "by")) return
18
+ context.report({
19
+ message:
20
+ "`<For>` without `by` prop — provide a key function for efficient reconciliation.",
21
+ span: getSpan(node),
22
+ })
23
+ },
24
+ }
25
+ return callbacks
26
+ },
27
+ }
@@ -0,0 +1,46 @@
1
+ import type { Rule, VisitorCallbacks } from "../../types"
2
+ import { getSpan } from "../../utils/ast"
3
+
4
+ const INPUT_TAGS = new Set(["input", "textarea", "select"])
5
+
6
+ export const noOnChange: Rule = {
7
+ meta: {
8
+ id: "pyreon/no-onchange",
9
+ category: "jsx",
10
+ description:
11
+ "Prefer `onInput` over `onChange` on input elements for keypress-by-keypress updates.",
12
+ severity: "warn",
13
+ fixable: true,
14
+ },
15
+ create(context) {
16
+ let currentTag: string | null = null
17
+ const callbacks: VisitorCallbacks = {
18
+ JSXOpeningElement(node: any) {
19
+ const name = node.name
20
+ if (name?.type === "JSXIdentifier" && INPUT_TAGS.has(name.name)) {
21
+ currentTag = name.name
22
+ } else {
23
+ currentTag = null
24
+ }
25
+
26
+ if (!currentTag) return
27
+ const attrs = node.attributes ?? []
28
+ for (const attr of attrs) {
29
+ if (
30
+ attr.type === "JSXAttribute" &&
31
+ attr.name?.type === "JSXIdentifier" &&
32
+ attr.name.name === "onChange"
33
+ ) {
34
+ const nameSpan = getSpan(attr.name)
35
+ context.report({
36
+ message: `Use \`onInput\` instead of \`onChange\` on \`<${currentTag}>\` for keypress-by-keypress updates.`,
37
+ span: getSpan(attr),
38
+ fix: { span: nameSpan, replacement: "onInput" },
39
+ })
40
+ }
41
+ }
42
+ },
43
+ }
44
+ return callbacks
45
+ },
46
+ }
@@ -0,0 +1,64 @@
1
+ import type { Rule, VisitorCallbacks } from "../../types"
2
+ import { getSpan, isDestructuring } from "../../utils/ast"
3
+
4
+ function containsJSXReturn(node: any): boolean {
5
+ if (!node) return false
6
+ // Arrow with expression body returning JSX
7
+ if (node.type === "JSXElement" || node.type === "JSXFragment") return true
8
+ if (node.type === "ParenthesizedExpression") return containsJSXReturn(node.expression)
9
+
10
+ // Block body — look for return statements with JSX
11
+ if (node.type === "BlockStatement") {
12
+ for (const stmt of node.body ?? []) {
13
+ if (stmt.type === "ReturnStatement" && containsJSXReturn(stmt.argument)) {
14
+ return true
15
+ }
16
+ }
17
+ }
18
+ return false
19
+ }
20
+
21
+ export const noPropsDestructure: Rule = {
22
+ meta: {
23
+ id: "pyreon/no-props-destructure",
24
+ category: "jsx",
25
+ description:
26
+ "Disallow destructuring props in component functions — it breaks signal reactivity.",
27
+ severity: "error",
28
+ fixable: false,
29
+ },
30
+ create(context) {
31
+ const callbacks: VisitorCallbacks = {
32
+ ArrowFunctionExpression(node: any) {
33
+ checkFunction(node, context)
34
+ },
35
+ FunctionDeclaration(node: any) {
36
+ checkFunction(node, context)
37
+ },
38
+ FunctionExpression(node: any) {
39
+ checkFunction(node, context)
40
+ },
41
+ }
42
+ return callbacks
43
+ },
44
+ }
45
+
46
+ function checkFunction(node: any, context: any) {
47
+ const params = node.params
48
+ if (!params || params.length === 0) return
49
+
50
+ const firstParam = params[0]
51
+ if (!isDestructuring(firstParam)) return
52
+
53
+ // Check if this function returns JSX
54
+ const body = node.body
55
+ if (!body) return
56
+
57
+ if (containsJSXReturn(body)) {
58
+ context.report({
59
+ message:
60
+ "Destructured props in a component function — this breaks signal reactivity. Use `props.x` or `splitProps()` instead.",
61
+ span: getSpan(firstParam),
62
+ })
63
+ }
64
+ }
@@ -0,0 +1,32 @@
1
+ import type { Rule, VisitorCallbacks } from "../../types"
2
+ import { getSpan, isTernaryWithJSX } from "../../utils/ast"
3
+
4
+ export const noTernaryConditional: Rule = {
5
+ meta: {
6
+ id: "pyreon/no-ternary-conditional",
7
+ category: "jsx",
8
+ description: "Prefer <Show> over ternary expressions with JSX branches.",
9
+ severity: "warn",
10
+ fixable: false,
11
+ },
12
+ create(context) {
13
+ let jsxExpressionDepth = 0
14
+ const callbacks: VisitorCallbacks = {
15
+ JSXExpressionContainer() {
16
+ jsxExpressionDepth++
17
+ },
18
+ "JSXExpressionContainer:exit"() {
19
+ jsxExpressionDepth--
20
+ },
21
+ ConditionalExpression(node: any) {
22
+ if (jsxExpressionDepth === 0) return
23
+ if (!isTernaryWithJSX(node)) return
24
+ context.report({
25
+ message: "Ternary with JSX — use `<Show>` for more efficient conditional rendering.",
26
+ span: getSpan(node),
27
+ })
28
+ },
29
+ }
30
+ return callbacks
31
+ },
32
+ }
@@ -0,0 +1,33 @@
1
+ import type { Rule, VisitorCallbacks } from "../../types"
2
+ import { getJSXAttribute, getSpan, hasJSXAttribute } from "../../utils/ast"
3
+
4
+ export const useByNotKey: Rule = {
5
+ meta: {
6
+ id: "pyreon/use-by-not-key",
7
+ category: "jsx",
8
+ description:
9
+ "Use `by` prop on <For> instead of `key` — JSX reserves `key` for VNode reconciliation.",
10
+ severity: "error",
11
+ fixable: true,
12
+ },
13
+ create(context) {
14
+ const callbacks: VisitorCallbacks = {
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")
19
+ if (!keyAttr) return
20
+ if (hasJSXAttribute(node, "by")) return // already has by
21
+
22
+ const attrSpan = getSpan(keyAttr.name)
23
+ context.report({
24
+ message:
25
+ "Use `by` prop on `<For>` instead of `key` — JSX reserves `key` for VNode reconciliation.",
26
+ span: getSpan(keyAttr),
27
+ fix: { span: attrSpan, replacement: "by" },
28
+ })
29
+ },
30
+ }
31
+ return callbacks
32
+ },
33
+ }