@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.
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/lib/analysis/index.js.html +5406 -0
- package/lib/index.js +2955 -0
- package/lib/index.js.map +1 -0
- package/lib/types/index.d.ts +260 -0
- package/lib/types/index.d.ts.map +1 -0
- package/package.json +56 -0
- package/src/cache.ts +51 -0
- package/src/cli.ts +199 -0
- package/src/config/ignore.ts +159 -0
- package/src/config/loader.ts +72 -0
- package/src/config/presets.ts +62 -0
- package/src/index.ts +40 -0
- package/src/lint.ts +226 -0
- package/src/reporter.ts +85 -0
- package/src/rules/accessibility/dialog-a11y.ts +32 -0
- package/src/rules/accessibility/overlay-a11y.ts +33 -0
- package/src/rules/accessibility/toast-a11y.ts +38 -0
- package/src/rules/architecture/dev-guard-warnings.ts +57 -0
- package/src/rules/architecture/no-circular-import.ts +59 -0
- package/src/rules/architecture/no-cross-layer-import.ts +75 -0
- package/src/rules/architecture/no-deep-import.ts +32 -0
- package/src/rules/architecture/no-error-without-prefix.ts +75 -0
- package/src/rules/form/no-submit-without-validation.ts +45 -0
- package/src/rules/form/no-unregistered-field.ts +45 -0
- package/src/rules/form/prefer-field-array.ts +41 -0
- package/src/rules/hooks/no-raw-addeventlistener.ts +28 -0
- package/src/rules/hooks/no-raw-localstorage.ts +35 -0
- package/src/rules/hooks/no-raw-setinterval.ts +41 -0
- package/src/rules/index.ts +208 -0
- package/src/rules/jsx/no-and-conditional.ts +32 -0
- package/src/rules/jsx/no-children-access.ts +44 -0
- package/src/rules/jsx/no-classname.ts +27 -0
- package/src/rules/jsx/no-htmlfor.ts +27 -0
- package/src/rules/jsx/no-index-as-by.ts +70 -0
- package/src/rules/jsx/no-map-in-jsx.ts +43 -0
- package/src/rules/jsx/no-missing-for-by.ts +27 -0
- package/src/rules/jsx/no-onchange.ts +46 -0
- package/src/rules/jsx/no-props-destructure.ts +64 -0
- package/src/rules/jsx/no-ternary-conditional.ts +32 -0
- package/src/rules/jsx/use-by-not-key.ts +33 -0
- package/src/rules/lifecycle/no-dom-in-setup.ts +53 -0
- package/src/rules/lifecycle/no-effect-in-mount.ts +36 -0
- package/src/rules/lifecycle/no-missing-cleanup.ts +80 -0
- package/src/rules/lifecycle/no-mount-in-effect.ts +35 -0
- package/src/rules/performance/no-eager-import.ts +28 -0
- package/src/rules/performance/no-effect-in-for.ts +41 -0
- package/src/rules/performance/no-large-for-without-by.ts +28 -0
- package/src/rules/performance/prefer-show-over-display.ts +47 -0
- package/src/rules/reactivity/no-bare-signal-in-jsx.ts +56 -0
- package/src/rules/reactivity/no-effect-assignment.ts +65 -0
- package/src/rules/reactivity/no-nested-effect.ts +33 -0
- package/src/rules/reactivity/no-peek-in-tracked.ts +35 -0
- package/src/rules/reactivity/no-signal-in-loop.ts +59 -0
- package/src/rules/reactivity/no-signal-leak.ts +58 -0
- package/src/rules/reactivity/no-unbatched-updates.ts +77 -0
- package/src/rules/reactivity/prefer-computed.ts +56 -0
- package/src/rules/router/index.ts +4 -0
- package/src/rules/router/no-href-navigation.ts +51 -0
- package/src/rules/router/no-imperative-navigate-in-render.ts +83 -0
- package/src/rules/router/no-missing-fallback.ts +87 -0
- package/src/rules/router/prefer-use-is-active.ts +45 -0
- package/src/rules/ssr/no-mismatch-risk.ts +47 -0
- package/src/rules/ssr/no-window-in-ssr.ts +76 -0
- package/src/rules/ssr/prefer-request-context.ts +56 -0
- package/src/rules/store/no-duplicate-store-id.ts +43 -0
- package/src/rules/store/no-mutate-store-state.ts +37 -0
- package/src/rules/store/no-store-outside-provider.ts +59 -0
- package/src/rules/styling/no-dynamic-styled.ts +60 -0
- package/src/rules/styling/no-inline-style-object.ts +30 -0
- package/src/rules/styling/no-theme-outside-provider.ts +45 -0
- package/src/rules/styling/prefer-cx.ts +44 -0
- package/src/runner.ts +170 -0
- package/src/tests/runner.test.ts +1043 -0
- package/src/types.ts +125 -0
- package/src/utils/ast.ts +192 -0
- package/src/utils/imports.ts +122 -0
- package/src/utils/index.ts +39 -0
- package/src/utils/source.ts +36 -0
- 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
|
+
}
|