@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.
- package/README.md +91 -91
- package/lib/analysis/cli.js.html +1 -1
- package/lib/analysis/index.js.html +1 -1
- package/lib/cli.js +214 -1
- package/lib/cli.js.map +1 -1
- package/lib/index.js +207 -1
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +30 -5
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +15 -15
- package/src/cache.ts +1 -1
- package/src/cli.ts +38 -28
- package/src/config/ignore.ts +23 -23
- package/src/config/loader.ts +8 -8
- package/src/config/presets.ts +11 -11
- package/src/index.ts +14 -12
- package/src/lint.ts +19 -19
- package/src/lsp/index.ts +225 -0
- package/src/reporter.ts +17 -17
- package/src/rules/accessibility/dialog-a11y.ts +10 -10
- package/src/rules/accessibility/overlay-a11y.ts +11 -11
- package/src/rules/accessibility/toast-a11y.ts +11 -11
- package/src/rules/architecture/dev-guard-warnings.ts +19 -19
- package/src/rules/architecture/no-circular-import.ts +16 -16
- package/src/rules/architecture/no-cross-layer-import.ts +35 -35
- package/src/rules/architecture/no-deep-import.ts +7 -7
- package/src/rules/architecture/no-error-without-prefix.ts +20 -20
- package/src/rules/form/no-submit-without-validation.ts +13 -13
- package/src/rules/form/no-unregistered-field.ts +12 -12
- package/src/rules/form/prefer-field-array.ts +11 -11
- package/src/rules/hooks/no-raw-addeventlistener.ts +9 -9
- package/src/rules/hooks/no-raw-localstorage.ts +11 -11
- package/src/rules/hooks/no-raw-setinterval.ts +11 -11
- package/src/rules/index.ts +60 -57
- package/src/rules/jsx/no-and-conditional.ts +8 -8
- package/src/rules/jsx/no-children-access.ts +12 -12
- package/src/rules/jsx/no-classname.ts +10 -10
- package/src/rules/jsx/no-htmlfor.ts +10 -10
- package/src/rules/jsx/no-index-as-by.ts +17 -17
- package/src/rules/jsx/no-map-in-jsx.ts +9 -9
- package/src/rules/jsx/no-missing-for-by.ts +9 -9
- package/src/rules/jsx/no-onchange.ts +12 -12
- package/src/rules/jsx/no-props-destructure.ts +11 -11
- package/src/rules/jsx/no-ternary-conditional.ts +8 -8
- package/src/rules/jsx/use-by-not-key.ts +12 -12
- package/src/rules/lifecycle/no-dom-in-setup.ts +18 -18
- package/src/rules/lifecycle/no-effect-in-mount.ts +11 -11
- package/src/rules/lifecycle/no-missing-cleanup.ts +19 -19
- package/src/rules/lifecycle/no-mount-in-effect.ts +11 -11
- package/src/rules/performance/no-eager-import.ts +7 -7
- package/src/rules/performance/no-effect-in-for.ts +10 -10
- package/src/rules/performance/no-large-for-without-by.ts +9 -9
- package/src/rules/performance/prefer-show-over-display.ts +16 -16
- package/src/rules/reactivity/no-bare-signal-in-jsx.ts +10 -10
- package/src/rules/reactivity/no-context-destructure.ts +45 -0
- package/src/rules/reactivity/no-effect-assignment.ts +16 -16
- package/src/rules/reactivity/no-nested-effect.ts +10 -10
- package/src/rules/reactivity/no-peek-in-tracked.ts +10 -10
- package/src/rules/reactivity/no-signal-in-loop.ts +13 -13
- package/src/rules/reactivity/no-signal-leak.ts +9 -9
- package/src/rules/reactivity/no-unbatched-updates.ts +12 -12
- package/src/rules/reactivity/prefer-computed.ts +13 -13
- package/src/rules/router/index.ts +4 -4
- package/src/rules/router/no-href-navigation.ts +14 -14
- package/src/rules/router/no-imperative-navigate-in-render.ts +19 -19
- package/src/rules/router/no-missing-fallback.ts +16 -16
- package/src/rules/router/prefer-use-is-active.ts +11 -11
- package/src/rules/ssr/no-mismatch-risk.ts +11 -11
- package/src/rules/ssr/no-window-in-ssr.ts +22 -22
- package/src/rules/ssr/prefer-request-context.ts +14 -14
- package/src/rules/store/no-duplicate-store-id.ts +9 -9
- package/src/rules/store/no-mutate-store-state.ts +11 -11
- package/src/rules/store/no-store-outside-provider.ts +15 -15
- package/src/rules/styling/no-dynamic-styled.ts +13 -13
- package/src/rules/styling/no-inline-style-object.ts +10 -10
- package/src/rules/styling/no-theme-outside-provider.ts +11 -11
- package/src/rules/styling/prefer-cx.ts +12 -12
- package/src/runner.ts +13 -13
- package/src/tests/lsp.test.ts +88 -0
- package/src/tests/runner.test.ts +325 -325
- package/src/types.ts +15 -15
- package/src/utils/ast.ts +50 -50
- package/src/utils/imports.ts +53 -53
- package/src/utils/index.ts +5 -5
- package/src/utils/source.ts +2 -2
- package/src/watcher.ts +19 -19
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
import type { Rule, VisitorCallbacks } from
|
|
2
|
-
import { getSpan } from
|
|
1
|
+
import type { Rule, VisitorCallbacks } from '../../types'
|
|
2
|
+
import { getSpan } from '../../utils/ast'
|
|
3
3
|
|
|
4
4
|
export const noClassName: Rule = {
|
|
5
5
|
meta: {
|
|
6
|
-
id:
|
|
7
|
-
category:
|
|
8
|
-
description:
|
|
9
|
-
severity:
|
|
6
|
+
id: 'pyreon/no-classname',
|
|
7
|
+
category: 'jsx',
|
|
8
|
+
description: 'Use `class` instead of `className` — Pyreon uses standard HTML attributes.',
|
|
9
|
+
severity: 'error',
|
|
10
10
|
fixable: true,
|
|
11
11
|
},
|
|
12
12
|
create(context) {
|
|
13
13
|
const callbacks: VisitorCallbacks = {
|
|
14
14
|
JSXAttribute(node: any) {
|
|
15
|
-
if (node.name?.type !==
|
|
16
|
-
if (node.name.name !==
|
|
15
|
+
if (node.name?.type !== 'JSXIdentifier') return
|
|
16
|
+
if (node.name.name !== 'className') return
|
|
17
17
|
const nameSpan = getSpan(node.name)
|
|
18
18
|
context.report({
|
|
19
|
-
message:
|
|
19
|
+
message: 'Use `class` instead of `className` — Pyreon uses standard HTML attributes.',
|
|
20
20
|
span: getSpan(node),
|
|
21
|
-
fix: { span: nameSpan, replacement:
|
|
21
|
+
fix: { span: nameSpan, replacement: 'class' },
|
|
22
22
|
})
|
|
23
23
|
},
|
|
24
24
|
}
|
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
import type { Rule, VisitorCallbacks } from
|
|
2
|
-
import { getSpan } from
|
|
1
|
+
import type { Rule, VisitorCallbacks } from '../../types'
|
|
2
|
+
import { getSpan } from '../../utils/ast'
|
|
3
3
|
|
|
4
4
|
export const noHtmlFor: Rule = {
|
|
5
5
|
meta: {
|
|
6
|
-
id:
|
|
7
|
-
category:
|
|
8
|
-
description:
|
|
9
|
-
severity:
|
|
6
|
+
id: 'pyreon/no-htmlfor',
|
|
7
|
+
category: 'jsx',
|
|
8
|
+
description: 'Use `for` instead of `htmlFor` — Pyreon uses standard HTML attributes.',
|
|
9
|
+
severity: 'error',
|
|
10
10
|
fixable: true,
|
|
11
11
|
},
|
|
12
12
|
create(context) {
|
|
13
13
|
const callbacks: VisitorCallbacks = {
|
|
14
14
|
JSXAttribute(node: any) {
|
|
15
|
-
if (node.name?.type !==
|
|
16
|
-
if (node.name.name !==
|
|
15
|
+
if (node.name?.type !== 'JSXIdentifier') return
|
|
16
|
+
if (node.name.name !== 'htmlFor') return
|
|
17
17
|
const nameSpan = getSpan(node.name)
|
|
18
18
|
context.report({
|
|
19
|
-
message:
|
|
19
|
+
message: 'Use `for` instead of `htmlFor` — Pyreon uses standard HTML attributes.',
|
|
20
20
|
span: getSpan(node),
|
|
21
|
-
fix: { span: nameSpan, replacement:
|
|
21
|
+
fix: { span: nameSpan, replacement: 'for' },
|
|
22
22
|
})
|
|
23
23
|
},
|
|
24
24
|
}
|
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
import type { Rule, VisitorCallbacks } from
|
|
2
|
-
import { getJSXAttribute, getSpan } from
|
|
1
|
+
import type { Rule, VisitorCallbacks } from '../../types'
|
|
2
|
+
import { getJSXAttribute, getSpan } from '../../utils/ast'
|
|
3
3
|
|
|
4
4
|
export const noIndexAsBy: Rule = {
|
|
5
5
|
meta: {
|
|
6
|
-
id:
|
|
7
|
-
category:
|
|
8
|
-
description:
|
|
9
|
-
severity:
|
|
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
10
|
fixable: false,
|
|
11
11
|
},
|
|
12
12
|
create(context) {
|
|
13
13
|
const callbacks: VisitorCallbacks = {
|
|
14
14
|
JSXOpeningElement(node: any) {
|
|
15
15
|
const name = node.name
|
|
16
|
-
if (!name || name.type !==
|
|
16
|
+
if (!name || name.type !== 'JSXIdentifier' || name.name !== 'For') return
|
|
17
17
|
|
|
18
|
-
const byAttr = getJSXAttribute(node,
|
|
18
|
+
const byAttr = getJSXAttribute(node, 'by')
|
|
19
19
|
if (!byAttr) return
|
|
20
20
|
|
|
21
21
|
const value = byAttr.value
|
|
22
|
-
if (!value || value.type !==
|
|
22
|
+
if (!value || value.type !== 'JSXExpressionContainer') return
|
|
23
23
|
|
|
24
24
|
const expr = value.expression
|
|
25
25
|
if (!expr) return
|
|
26
26
|
|
|
27
27
|
// Detect: by={(_, i) => i} or by={(item, index) => index}
|
|
28
|
-
if (expr.type ===
|
|
28
|
+
if (expr.type === 'ArrowFunctionExpression' || expr.type === 'FunctionExpression') {
|
|
29
29
|
const params = expr.params
|
|
30
30
|
if (!params || params.length < 2) return
|
|
31
31
|
|
|
32
32
|
const secondParam = params[1]
|
|
33
|
-
if (!secondParam || secondParam.type !==
|
|
33
|
+
if (!secondParam || secondParam.type !== 'Identifier') return
|
|
34
34
|
|
|
35
35
|
const indexName = secondParam.name
|
|
36
36
|
const body = expr.body
|
|
37
37
|
|
|
38
38
|
// Arrow expression body: (_, i) => i
|
|
39
|
-
if (body?.type ===
|
|
39
|
+
if (body?.type === 'Identifier' && body.name === indexName) {
|
|
40
40
|
context.report({
|
|
41
41
|
message:
|
|
42
|
-
|
|
42
|
+
'Using index as `by` prop on `<For>` — use a unique key from the data instead.',
|
|
43
43
|
span: getSpan(byAttr),
|
|
44
44
|
})
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// Block body: (_, i) => { return i }
|
|
48
|
-
if (body?.type ===
|
|
48
|
+
if (body?.type === 'BlockStatement') {
|
|
49
49
|
const stmts = body.body
|
|
50
50
|
if (stmts?.length === 1) {
|
|
51
51
|
const stmt = stmts[0]
|
|
52
52
|
if (
|
|
53
|
-
stmt.type ===
|
|
54
|
-
stmt.argument?.type ===
|
|
53
|
+
stmt.type === 'ReturnStatement' &&
|
|
54
|
+
stmt.argument?.type === 'Identifier' &&
|
|
55
55
|
stmt.argument.name === indexName
|
|
56
56
|
) {
|
|
57
57
|
context.report({
|
|
58
58
|
message:
|
|
59
|
-
|
|
59
|
+
'Using index as `by` prop on `<For>` — use a unique key from the data instead.',
|
|
60
60
|
span: getSpan(byAttr),
|
|
61
61
|
})
|
|
62
62
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import type { Rule, VisitorCallbacks } from
|
|
2
|
-
import { getSpan, isArrayMapCall } from
|
|
1
|
+
import type { Rule, VisitorCallbacks } from '../../types'
|
|
2
|
+
import { getSpan, isArrayMapCall } from '../../utils/ast'
|
|
3
3
|
|
|
4
4
|
export const noMapInJsx: Rule = {
|
|
5
5
|
meta: {
|
|
6
|
-
id:
|
|
7
|
-
category:
|
|
8
|
-
description:
|
|
9
|
-
severity:
|
|
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
10
|
fixable: false,
|
|
11
11
|
},
|
|
12
12
|
create(context) {
|
|
@@ -15,13 +15,13 @@ export const noMapInJsx: Rule = {
|
|
|
15
15
|
JSXElement() {
|
|
16
16
|
jsxDepth++
|
|
17
17
|
},
|
|
18
|
-
|
|
18
|
+
'JSXElement:exit'() {
|
|
19
19
|
jsxDepth--
|
|
20
20
|
},
|
|
21
21
|
JSXFragment() {
|
|
22
22
|
jsxDepth++
|
|
23
23
|
},
|
|
24
|
-
|
|
24
|
+
'JSXFragment:exit'() {
|
|
25
25
|
jsxDepth--
|
|
26
26
|
},
|
|
27
27
|
CallExpression(node: any) {
|
|
@@ -33,7 +33,7 @@ export const noMapInJsx: Rule = {
|
|
|
33
33
|
const callback = args[0]
|
|
34
34
|
if (!callback) return
|
|
35
35
|
context.report({
|
|
36
|
-
message:
|
|
36
|
+
message: '`.map()` in JSX — use `<For>` for reactive list rendering instead.',
|
|
37
37
|
span: getSpan(node),
|
|
38
38
|
})
|
|
39
39
|
},
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import type { Rule, VisitorCallbacks } from
|
|
2
|
-
import { getSpan, hasJSXAttribute } from
|
|
1
|
+
import type { Rule, VisitorCallbacks } from '../../types'
|
|
2
|
+
import { getSpan, hasJSXAttribute } from '../../utils/ast'
|
|
3
3
|
|
|
4
4
|
export const noMissingForBy: Rule = {
|
|
5
5
|
meta: {
|
|
6
|
-
id:
|
|
7
|
-
category:
|
|
8
|
-
description:
|
|
9
|
-
severity:
|
|
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
10
|
fixable: false,
|
|
11
11
|
},
|
|
12
12
|
create(context) {
|
|
13
13
|
const callbacks: VisitorCallbacks = {
|
|
14
14
|
JSXOpeningElement(node: any) {
|
|
15
15
|
const name = node.name
|
|
16
|
-
if (!name || name.type !==
|
|
17
|
-
if (hasJSXAttribute(node,
|
|
16
|
+
if (!name || name.type !== 'JSXIdentifier' || name.name !== 'For') return
|
|
17
|
+
if (hasJSXAttribute(node, 'by')) return
|
|
18
18
|
context.report({
|
|
19
19
|
message:
|
|
20
|
-
|
|
20
|
+
'`<For>` without `by` prop — provide a key function for efficient reconciliation.',
|
|
21
21
|
span: getSpan(node),
|
|
22
22
|
})
|
|
23
23
|
},
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import type { Rule, VisitorCallbacks } from
|
|
2
|
-
import { getSpan } from
|
|
1
|
+
import type { Rule, VisitorCallbacks } from '../../types'
|
|
2
|
+
import { getSpan } from '../../utils/ast'
|
|
3
3
|
|
|
4
|
-
const INPUT_TAGS = new Set([
|
|
4
|
+
const INPUT_TAGS = new Set(['input', 'textarea', 'select'])
|
|
5
5
|
|
|
6
6
|
export const noOnChange: Rule = {
|
|
7
7
|
meta: {
|
|
8
|
-
id:
|
|
9
|
-
category:
|
|
8
|
+
id: 'pyreon/no-onchange',
|
|
9
|
+
category: 'jsx',
|
|
10
10
|
description:
|
|
11
|
-
|
|
12
|
-
severity:
|
|
11
|
+
'Prefer `onInput` over `onChange` on input elements for keypress-by-keypress updates.',
|
|
12
|
+
severity: 'warn',
|
|
13
13
|
fixable: true,
|
|
14
14
|
},
|
|
15
15
|
create(context) {
|
|
@@ -17,7 +17,7 @@ export const noOnChange: Rule = {
|
|
|
17
17
|
const callbacks: VisitorCallbacks = {
|
|
18
18
|
JSXOpeningElement(node: any) {
|
|
19
19
|
const name = node.name
|
|
20
|
-
if (name?.type ===
|
|
20
|
+
if (name?.type === 'JSXIdentifier' && INPUT_TAGS.has(name.name)) {
|
|
21
21
|
currentTag = name.name
|
|
22
22
|
} else {
|
|
23
23
|
currentTag = null
|
|
@@ -27,15 +27,15 @@ export const noOnChange: Rule = {
|
|
|
27
27
|
const attrs = node.attributes ?? []
|
|
28
28
|
for (const attr of attrs) {
|
|
29
29
|
if (
|
|
30
|
-
attr.type ===
|
|
31
|
-
attr.name?.type ===
|
|
32
|
-
attr.name.name ===
|
|
30
|
+
attr.type === 'JSXAttribute' &&
|
|
31
|
+
attr.name?.type === 'JSXIdentifier' &&
|
|
32
|
+
attr.name.name === 'onChange'
|
|
33
33
|
) {
|
|
34
34
|
const nameSpan = getSpan(attr.name)
|
|
35
35
|
context.report({
|
|
36
36
|
message: `Use \`onInput\` instead of \`onChange\` on \`<${currentTag}>\` for keypress-by-keypress updates.`,
|
|
37
37
|
span: getSpan(attr),
|
|
38
|
-
fix: { span: nameSpan, replacement:
|
|
38
|
+
fix: { span: nameSpan, replacement: 'onInput' },
|
|
39
39
|
})
|
|
40
40
|
}
|
|
41
41
|
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import type { Rule, VisitorCallbacks } from
|
|
2
|
-
import { getSpan, isDestructuring } from
|
|
1
|
+
import type { Rule, VisitorCallbacks } from '../../types'
|
|
2
|
+
import { getSpan, isDestructuring } from '../../utils/ast'
|
|
3
3
|
|
|
4
4
|
function containsJSXReturn(node: any): boolean {
|
|
5
5
|
if (!node) return false
|
|
6
6
|
// Arrow with expression body returning JSX
|
|
7
|
-
if (node.type ===
|
|
8
|
-
if (node.type ===
|
|
7
|
+
if (node.type === 'JSXElement' || node.type === 'JSXFragment') return true
|
|
8
|
+
if (node.type === 'ParenthesizedExpression') return containsJSXReturn(node.expression)
|
|
9
9
|
|
|
10
10
|
// Block body — look for return statements with JSX
|
|
11
|
-
if (node.type ===
|
|
11
|
+
if (node.type === 'BlockStatement') {
|
|
12
12
|
for (const stmt of node.body ?? []) {
|
|
13
|
-
if (stmt.type ===
|
|
13
|
+
if (stmt.type === 'ReturnStatement' && containsJSXReturn(stmt.argument)) {
|
|
14
14
|
return true
|
|
15
15
|
}
|
|
16
16
|
}
|
|
@@ -20,11 +20,11 @@ function containsJSXReturn(node: any): boolean {
|
|
|
20
20
|
|
|
21
21
|
export const noPropsDestructure: Rule = {
|
|
22
22
|
meta: {
|
|
23
|
-
id:
|
|
24
|
-
category:
|
|
23
|
+
id: 'pyreon/no-props-destructure',
|
|
24
|
+
category: 'jsx',
|
|
25
25
|
description:
|
|
26
|
-
|
|
27
|
-
severity:
|
|
26
|
+
'Disallow destructuring props in component functions — it breaks signal reactivity.',
|
|
27
|
+
severity: 'error',
|
|
28
28
|
fixable: false,
|
|
29
29
|
},
|
|
30
30
|
create(context) {
|
|
@@ -57,7 +57,7 @@ function checkFunction(node: any, context: any) {
|
|
|
57
57
|
if (containsJSXReturn(body)) {
|
|
58
58
|
context.report({
|
|
59
59
|
message:
|
|
60
|
-
|
|
60
|
+
'Destructured props in a component function — this breaks signal reactivity. Use `props.x` or `splitProps()` instead.',
|
|
61
61
|
span: getSpan(firstParam),
|
|
62
62
|
})
|
|
63
63
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import type { Rule, VisitorCallbacks } from
|
|
2
|
-
import { getSpan, isTernaryWithJSX } from
|
|
1
|
+
import type { Rule, VisitorCallbacks } from '../../types'
|
|
2
|
+
import { getSpan, isTernaryWithJSX } from '../../utils/ast'
|
|
3
3
|
|
|
4
4
|
export const noTernaryConditional: Rule = {
|
|
5
5
|
meta: {
|
|
6
|
-
id:
|
|
7
|
-
category:
|
|
8
|
-
description:
|
|
9
|
-
severity:
|
|
6
|
+
id: 'pyreon/no-ternary-conditional',
|
|
7
|
+
category: 'jsx',
|
|
8
|
+
description: 'Prefer <Show> over ternary expressions with JSX branches.',
|
|
9
|
+
severity: 'warn',
|
|
10
10
|
fixable: false,
|
|
11
11
|
},
|
|
12
12
|
create(context) {
|
|
@@ -15,14 +15,14 @@ export const noTernaryConditional: Rule = {
|
|
|
15
15
|
JSXExpressionContainer() {
|
|
16
16
|
jsxExpressionDepth++
|
|
17
17
|
},
|
|
18
|
-
|
|
18
|
+
'JSXExpressionContainer:exit'() {
|
|
19
19
|
jsxExpressionDepth--
|
|
20
20
|
},
|
|
21
21
|
ConditionalExpression(node: any) {
|
|
22
22
|
if (jsxExpressionDepth === 0) return
|
|
23
23
|
if (!isTernaryWithJSX(node)) return
|
|
24
24
|
context.report({
|
|
25
|
-
message:
|
|
25
|
+
message: 'Ternary with JSX — use `<Show>` for more efficient conditional rendering.',
|
|
26
26
|
span: getSpan(node),
|
|
27
27
|
})
|
|
28
28
|
},
|
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
import type { Rule, VisitorCallbacks } from
|
|
2
|
-
import { getJSXAttribute, getSpan, hasJSXAttribute } from
|
|
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:
|
|
7
|
-
category:
|
|
6
|
+
id: 'pyreon/use-by-not-key',
|
|
7
|
+
category: 'jsx',
|
|
8
8
|
description:
|
|
9
|
-
|
|
10
|
-
severity:
|
|
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 ===
|
|
17
|
-
if (tagName !==
|
|
18
|
-
const keyAttr = getJSXAttribute(node,
|
|
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,
|
|
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
|
-
|
|
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:
|
|
27
|
+
fix: { span: attrSpan, replacement: 'by' },
|
|
28
28
|
})
|
|
29
29
|
},
|
|
30
30
|
}
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
import type { Rule, VisitorCallbacks } from
|
|
2
|
-
import { getSpan, isCallTo } from
|
|
1
|
+
import type { Rule, VisitorCallbacks } from '../../types'
|
|
2
|
+
import { getSpan, isCallTo } from '../../utils/ast'
|
|
3
3
|
|
|
4
4
|
const DOM_METHODS = new Set([
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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:
|
|
15
|
-
category:
|
|
16
|
-
description:
|
|
17
|
-
severity:
|
|
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,
|
|
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 ===
|
|
34
|
-
callee.object?.type ===
|
|
35
|
-
callee.object.name ===
|
|
36
|
-
callee.property?.type ===
|
|
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
|
-
|
|
46
|
-
if (isCallTo(node,
|
|
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
|
|
2
|
-
import { getSpan, isCallTo } from
|
|
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:
|
|
7
|
-
category:
|
|
6
|
+
id: 'pyreon/no-effect-in-mount',
|
|
7
|
+
category: 'lifecycle',
|
|
8
8
|
description:
|
|
9
|
-
|
|
10
|
-
severity:
|
|
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,
|
|
17
|
+
if (isCallTo(node, 'onMount')) {
|
|
18
18
|
mountDepth++
|
|
19
19
|
}
|
|
20
|
-
if (mountDepth > 0 && isCallTo(node,
|
|
20
|
+
if (mountDepth > 0 && isCallTo(node, 'effect')) {
|
|
21
21
|
context.report({
|
|
22
22
|
message:
|
|
23
|
-
|
|
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
|
-
|
|
29
|
-
if (isCallTo(node,
|
|
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
|
|
2
|
-
import { getSpan, isCallTo } from
|
|
1
|
+
import type { Rule, VisitorCallbacks } from '../../types'
|
|
2
|
+
import { getSpan, isCallTo } from '../../utils/ast'
|
|
3
3
|
|
|
4
|
-
const NEEDS_CLEANUP = new Set([
|
|
4
|
+
const NEEDS_CLEANUP = new Set(['setInterval', 'addEventListener'])
|
|
5
5
|
|
|
6
6
|
export const noMissingCleanup: Rule = {
|
|
7
7
|
meta: {
|
|
8
|
-
id:
|
|
9
|
-
category:
|
|
8
|
+
id: 'pyreon/no-missing-cleanup',
|
|
9
|
+
category: 'lifecycle',
|
|
10
10
|
description:
|
|
11
|
-
|
|
12
|
-
severity:
|
|
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,
|
|
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 !==
|
|
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 !==
|
|
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 ===
|
|
37
|
+
if (n.type === 'CallExpression') {
|
|
38
38
|
const callee = n.callee
|
|
39
|
-
if (callee?.type ===
|
|
39
|
+
if (callee?.type === 'Identifier' && NEEDS_CLEANUP.has(callee.name)) {
|
|
40
40
|
hasCleanupTarget = true
|
|
41
41
|
}
|
|
42
42
|
if (
|
|
43
|
-
callee?.type ===
|
|
44
|
-
callee.property?.type ===
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
58
|
+
if (item && typeof item.type === 'string') walk(item)
|
|
59
59
|
}
|
|
60
|
-
} else if (typeof child.type ===
|
|
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
|
-
|
|
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
|
}
|