@pyreon/lint 0.11.5 → 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.
- 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,31 +1,31 @@
|
|
|
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 noMountInEffect: Rule = {
|
|
5
5
|
meta: {
|
|
6
|
-
id:
|
|
7
|
-
category:
|
|
8
|
-
description:
|
|
9
|
-
severity:
|
|
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,
|
|
16
|
+
if (isCallTo(node, 'effect')) {
|
|
17
17
|
effectDepth++
|
|
18
18
|
}
|
|
19
|
-
if (effectDepth > 0 && isCallTo(node,
|
|
19
|
+
if (effectDepth > 0 && isCallTo(node, 'onMount')) {
|
|
20
20
|
context.report({
|
|
21
21
|
message:
|
|
22
|
-
|
|
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
|
-
|
|
28
|
-
if (isCallTo(node,
|
|
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
|
|
2
|
-
import { getSpan } from
|
|
3
|
-
import { HEAVY_PACKAGES } from
|
|
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:
|
|
8
|
-
category:
|
|
9
|
-
description:
|
|
10
|
-
severity:
|
|
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
|
|
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 noEffectInFor: Rule = {
|
|
5
5
|
meta: {
|
|
6
|
-
id:
|
|
7
|
-
category:
|
|
6
|
+
id: 'pyreon/no-effect-in-for',
|
|
7
|
+
category: 'performance',
|
|
8
8
|
description:
|
|
9
|
-
|
|
10
|
-
severity:
|
|
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 ===
|
|
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 ===
|
|
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,
|
|
30
|
+
if (isCallTo(node, 'effect')) {
|
|
31
31
|
context.report({
|
|
32
32
|
message:
|
|
33
|
-
|
|
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
|
|
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 noLargeForWithoutBy: Rule = {
|
|
5
5
|
meta: {
|
|
6
|
-
id:
|
|
7
|
-
category:
|
|
6
|
+
id: 'pyreon/no-large-for-without-by',
|
|
7
|
+
category: 'performance',
|
|
8
8
|
description:
|
|
9
|
-
|
|
10
|
-
severity:
|
|
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 !==
|
|
18
|
-
if (hasJSXAttribute(node,
|
|
17
|
+
if (!name || name.type !== 'JSXIdentifier' || name.name !== 'For') return
|
|
18
|
+
if (hasJSXAttribute(node, 'by')) return
|
|
19
19
|
context.report({
|
|
20
20
|
message:
|
|
21
|
-
|
|
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
|
|
2
|
-
import { getSpan } from
|
|
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:
|
|
7
|
-
category:
|
|
8
|
-
description:
|
|
9
|
-
severity:
|
|
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 !==
|
|
15
|
+
if (node.name?.type !== 'JSXIdentifier' || node.name.name !== 'style') return
|
|
16
16
|
const value = node.value
|
|
17
|
-
if (!value || value.type !==
|
|
17
|
+
if (!value || value.type !== 'JSXExpressionContainer') return
|
|
18
18
|
const expr = value.expression
|
|
19
|
-
if (!expr || expr.type !==
|
|
19
|
+
if (!expr || expr.type !== 'ObjectExpression') return
|
|
20
20
|
|
|
21
21
|
for (const prop of expr.properties ?? []) {
|
|
22
|
-
if (prop.type !==
|
|
22
|
+
if (prop.type !== 'Property') continue
|
|
23
23
|
const key = prop.key
|
|
24
24
|
if (!key) continue
|
|
25
25
|
const propName =
|
|
26
|
-
key.type ===
|
|
27
|
-
if (propName ===
|
|
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 ===
|
|
32
|
-
val?.type ===
|
|
33
|
-
val?.type ===
|
|
31
|
+
val?.type === 'ConditionalExpression' ||
|
|
32
|
+
val?.type === 'LogicalExpression' ||
|
|
33
|
+
val?.type === 'CallExpression'
|
|
34
34
|
) {
|
|
35
35
|
context.report({
|
|
36
36
|
message:
|
|
37
|
-
|
|
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
|
|
2
|
-
import { getSpan } from
|
|
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:
|
|
9
|
-
category:
|
|
8
|
+
id: 'pyreon/no-bare-signal-in-jsx',
|
|
9
|
+
category: 'reactivity',
|
|
10
10
|
description:
|
|
11
|
-
|
|
12
|
-
severity:
|
|
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
|
-
|
|
21
|
+
'JSXElement:exit'() {
|
|
22
22
|
jsxDepth--
|
|
23
23
|
},
|
|
24
24
|
JSXFragment() {
|
|
25
25
|
jsxDepth++
|
|
26
26
|
},
|
|
27
|
-
|
|
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 !==
|
|
33
|
+
if (!expr || expr.type !== 'CallExpression') return
|
|
34
34
|
const callee = expr.callee
|
|
35
|
-
if (!callee || callee.type !==
|
|
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
|
|
2
|
-
import { getSpan, isCallTo } from
|
|
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 ===
|
|
7
|
-
node.callee?.type ===
|
|
8
|
-
node.callee.property?.type ===
|
|
9
|
-
node.callee.property.name ===
|
|
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:
|
|
16
|
-
category:
|
|
17
|
-
description:
|
|
18
|
-
severity:
|
|
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,
|
|
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 ===
|
|
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
|
-
|
|
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 ===
|
|
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 ===
|
|
52
|
+
if (stmt.type === 'ExpressionStatement' && isUpdateCall(stmt.expression)) {
|
|
53
53
|
context.report({
|
|
54
54
|
message:
|
|
55
|
-
|
|
55
|
+
'Effect contains a single `.update()` — consider using `computed()` for derived values.',
|
|
56
56
|
span: getSpan(node),
|
|
57
57
|
})
|
|
58
58
|
}
|
|
@@ -1,29 +1,29 @@
|
|
|
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 noNestedEffect: Rule = {
|
|
5
5
|
meta: {
|
|
6
|
-
id:
|
|
7
|
-
category:
|
|
8
|
-
description:
|
|
9
|
-
severity:
|
|
6
|
+
id: 'pyreon/no-nested-effect',
|
|
7
|
+
category: 'reactivity',
|
|
8
|
+
description: 'Warn against nesting effect() inside another 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,
|
|
16
|
+
if (!isCallTo(node, 'effect')) return
|
|
17
17
|
if (effectDepth > 0) {
|
|
18
18
|
context.report({
|
|
19
|
-
message:
|
|
19
|
+
message: 'Nested `effect()` — consider using `computed()` for derived values instead.',
|
|
20
20
|
span: getSpan(node),
|
|
21
21
|
})
|
|
22
22
|
}
|
|
23
23
|
effectDepth++
|
|
24
24
|
},
|
|
25
|
-
|
|
26
|
-
if (isCallTo(node,
|
|
25
|
+
'CallExpression:exit'(node: any) {
|
|
26
|
+
if (isCallTo(node, 'effect')) {
|
|
27
27
|
effectDepth--
|
|
28
28
|
}
|
|
29
29
|
},
|
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
import type { Rule, VisitorCallbacks } from
|
|
2
|
-
import { getSpan, isCallTo, isPeekCall } from
|
|
1
|
+
import type { Rule, VisitorCallbacks } from '../../types'
|
|
2
|
+
import { getSpan, isCallTo, isPeekCall } from '../../utils/ast'
|
|
3
3
|
|
|
4
4
|
export const noPeekInTracked: Rule = {
|
|
5
5
|
meta: {
|
|
6
|
-
id:
|
|
7
|
-
category:
|
|
8
|
-
description:
|
|
9
|
-
severity:
|
|
6
|
+
id: 'pyreon/no-peek-in-tracked',
|
|
7
|
+
category: 'reactivity',
|
|
8
|
+
description: 'Disallow .peek() inside effect() or computed() — it bypasses tracking.',
|
|
9
|
+
severity: 'error',
|
|
10
10
|
fixable: false,
|
|
11
11
|
},
|
|
12
12
|
create(context) {
|
|
13
13
|
let trackedDepth = 0
|
|
14
14
|
const callbacks: VisitorCallbacks = {
|
|
15
15
|
CallExpression(node: any) {
|
|
16
|
-
if (isCallTo(node,
|
|
16
|
+
if (isCallTo(node, 'effect') || isCallTo(node, 'computed')) {
|
|
17
17
|
trackedDepth++
|
|
18
18
|
}
|
|
19
19
|
if (trackedDepth > 0 && isPeekCall(node)) {
|
|
20
20
|
context.report({
|
|
21
21
|
message:
|
|
22
|
-
|
|
22
|
+
'`.peek()` inside a tracked scope (effect/computed) bypasses dependency tracking — use a normal signal read instead.',
|
|
23
23
|
span: getSpan(node),
|
|
24
24
|
})
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
|
-
|
|
28
|
-
if (isCallTo(node,
|
|
27
|
+
'CallExpression:exit'(node: any) {
|
|
28
|
+
if (isCallTo(node, 'effect') || isCallTo(node, 'computed')) {
|
|
29
29
|
trackedDepth--
|
|
30
30
|
}
|
|
31
31
|
},
|
|
@@ -1,12 +1,12 @@
|
|
|
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 noSignalInLoop: Rule = {
|
|
5
5
|
meta: {
|
|
6
|
-
id:
|
|
7
|
-
category:
|
|
8
|
-
description:
|
|
9
|
-
severity:
|
|
6
|
+
id: 'pyreon/no-signal-in-loop',
|
|
7
|
+
category: 'reactivity',
|
|
8
|
+
description: 'Disallow creating signals or computeds inside loops.',
|
|
9
|
+
severity: 'error',
|
|
10
10
|
fixable: false,
|
|
11
11
|
},
|
|
12
12
|
create(context) {
|
|
@@ -15,38 +15,38 @@ export const noSignalInLoop: Rule = {
|
|
|
15
15
|
ForStatement() {
|
|
16
16
|
loopDepth++
|
|
17
17
|
},
|
|
18
|
-
|
|
18
|
+
'ForStatement:exit'() {
|
|
19
19
|
loopDepth--
|
|
20
20
|
},
|
|
21
21
|
ForInStatement() {
|
|
22
22
|
loopDepth++
|
|
23
23
|
},
|
|
24
|
-
|
|
24
|
+
'ForInStatement:exit'() {
|
|
25
25
|
loopDepth--
|
|
26
26
|
},
|
|
27
27
|
ForOfStatement() {
|
|
28
28
|
loopDepth++
|
|
29
29
|
},
|
|
30
|
-
|
|
30
|
+
'ForOfStatement:exit'() {
|
|
31
31
|
loopDepth--
|
|
32
32
|
},
|
|
33
33
|
WhileStatement() {
|
|
34
34
|
loopDepth++
|
|
35
35
|
},
|
|
36
|
-
|
|
36
|
+
'WhileStatement:exit'() {
|
|
37
37
|
loopDepth--
|
|
38
38
|
},
|
|
39
39
|
DoWhileStatement() {
|
|
40
40
|
loopDepth++
|
|
41
41
|
},
|
|
42
|
-
|
|
42
|
+
'DoWhileStatement:exit'() {
|
|
43
43
|
loopDepth--
|
|
44
44
|
},
|
|
45
45
|
CallExpression(node: any) {
|
|
46
46
|
if (loopDepth === 0) return
|
|
47
47
|
const callee = node.callee
|
|
48
|
-
if (!callee || callee.type !==
|
|
49
|
-
if (callee.name ===
|
|
48
|
+
if (!callee || callee.type !== 'Identifier') return
|
|
49
|
+
if (callee.name === 'signal' || callee.name === 'computed') {
|
|
50
50
|
context.report({
|
|
51
51
|
message: `\`${callee.name}()\` inside a loop — signals should be created once at component setup, not on every iteration.`,
|
|
52
52
|
span: getSpan(node),
|
|
@@ -1,12 +1,12 @@
|
|
|
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 noSignalLeak: Rule = {
|
|
5
5
|
meta: {
|
|
6
|
-
id:
|
|
7
|
-
category:
|
|
8
|
-
description:
|
|
9
|
-
severity:
|
|
6
|
+
id: 'pyreon/no-signal-leak',
|
|
7
|
+
category: 'reactivity',
|
|
8
|
+
description: 'Warn about unused signal declarations (potential leaks).',
|
|
9
|
+
severity: 'warn',
|
|
10
10
|
fixable: false,
|
|
11
11
|
},
|
|
12
12
|
create(context) {
|
|
@@ -19,9 +19,9 @@ export const noSignalLeak: Rule = {
|
|
|
19
19
|
const callbacks: VisitorCallbacks = {
|
|
20
20
|
VariableDeclarator(node: any) {
|
|
21
21
|
const init = node.init
|
|
22
|
-
if (!init || !isCallTo(init,
|
|
22
|
+
if (!init || !isCallTo(init, 'signal')) return
|
|
23
23
|
const id = node.id
|
|
24
|
-
if (!id || id.type !==
|
|
24
|
+
if (!id || id.type !== 'Identifier') return
|
|
25
25
|
signalDecls.set(id.name, {
|
|
26
26
|
span: getSpan(node),
|
|
27
27
|
declStart: id.start as number,
|
|
@@ -39,7 +39,7 @@ export const noSignalLeak: Rule = {
|
|
|
39
39
|
])
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
|
-
|
|
42
|
+
'Program:exit'() {
|
|
43
43
|
for (const [name, { span, declStart, declEnd }] of signalDecls) {
|
|
44
44
|
const occurrences = identifierOccurrences.get(name) ?? []
|
|
45
45
|
// Filter out the declaration identifier itself
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Rule, VisitorCallbacks } from
|
|
2
|
-
import { getSpan, isCallTo, isSetCall } from
|
|
1
|
+
import type { Rule, VisitorCallbacks } from '../../types'
|
|
2
|
+
import { getSpan, isCallTo, isSetCall } from '../../utils/ast'
|
|
3
3
|
|
|
4
4
|
interface ScopeInfo {
|
|
5
5
|
setCalls: Array<{ span: { start: number; end: number } }>
|
|
@@ -10,10 +10,10 @@ interface ScopeInfo {
|
|
|
10
10
|
|
|
11
11
|
export const noUnbatchedUpdates: Rule = {
|
|
12
12
|
meta: {
|
|
13
|
-
id:
|
|
14
|
-
category:
|
|
15
|
-
description:
|
|
16
|
-
severity:
|
|
13
|
+
id: 'pyreon/no-unbatched-updates',
|
|
14
|
+
category: 'reactivity',
|
|
15
|
+
description: 'Warn when 3+ .set() calls occur in the same function without batch().',
|
|
16
|
+
severity: 'warn',
|
|
17
17
|
fixable: false,
|
|
18
18
|
},
|
|
19
19
|
create(context) {
|
|
@@ -39,24 +39,24 @@ export const noUnbatchedUpdates: Rule = {
|
|
|
39
39
|
FunctionDeclaration(node: any) {
|
|
40
40
|
enterScope(node)
|
|
41
41
|
},
|
|
42
|
-
|
|
42
|
+
'FunctionDeclaration:exit'() {
|
|
43
43
|
exitScope()
|
|
44
44
|
},
|
|
45
45
|
FunctionExpression(node: any) {
|
|
46
46
|
enterScope(node)
|
|
47
47
|
},
|
|
48
|
-
|
|
48
|
+
'FunctionExpression:exit'() {
|
|
49
49
|
exitScope()
|
|
50
50
|
},
|
|
51
51
|
ArrowFunctionExpression(node: any) {
|
|
52
52
|
enterScope(node)
|
|
53
53
|
},
|
|
54
|
-
|
|
54
|
+
'ArrowFunctionExpression:exit'() {
|
|
55
55
|
exitScope()
|
|
56
56
|
},
|
|
57
57
|
CallExpression(node: any) {
|
|
58
58
|
const currentScope = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : undefined
|
|
59
|
-
if (isCallTo(node,
|
|
59
|
+
if (isCallTo(node, 'batch')) {
|
|
60
60
|
batchDepth++
|
|
61
61
|
if (currentScope) {
|
|
62
62
|
currentScope.hasBatch = true
|
|
@@ -66,8 +66,8 @@ export const noUnbatchedUpdates: Rule = {
|
|
|
66
66
|
currentScope.setCalls.push({ span: getSpan(node) })
|
|
67
67
|
}
|
|
68
68
|
},
|
|
69
|
-
|
|
70
|
-
if (isCallTo(node,
|
|
69
|
+
'CallExpression:exit'(node: any) {
|
|
70
|
+
if (isCallTo(node, 'batch')) {
|
|
71
71
|
batchDepth--
|
|
72
72
|
}
|
|
73
73
|
},
|