@pyreon/lint 0.11.4 → 0.11.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -91
- package/lib/analysis/cli.js.html +5406 -0
- package/lib/analysis/index.js.html +1 -1
- package/lib/cli.js +3290 -0
- package/lib/cli.js.map +1 -0
- package/lib/index.js +220 -29
- 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 +19 -19
- package/src/cache.ts +1 -1
- package/src/cli.ts +39 -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 -25
- 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 -14
- 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 +12 -3
- package/src/utils/source.ts +2 -2
- package/src/watcher.ts +19 -25
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
import type { Rule, VisitorCallbacks } from
|
|
2
|
-
import { getSpan } from
|
|
3
|
-
import { isPyreonImport } from
|
|
1
|
+
import type { Rule, VisitorCallbacks } from '../../types'
|
|
2
|
+
import { getSpan } from '../../utils/ast'
|
|
3
|
+
import { isPyreonImport } from '../../utils/imports'
|
|
4
4
|
|
|
5
|
-
type PackageCategory =
|
|
5
|
+
type PackageCategory = 'core' | 'fundamentals' | 'tools' | 'ui-system'
|
|
6
6
|
|
|
7
7
|
const CORE_PACKAGES = new Set([
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
'@pyreon/reactivity',
|
|
9
|
+
'@pyreon/core',
|
|
10
|
+
'@pyreon/compiler',
|
|
11
|
+
'@pyreon/runtime-dom',
|
|
12
|
+
'@pyreon/runtime-server',
|
|
13
|
+
'@pyreon/router',
|
|
14
|
+
'@pyreon/head',
|
|
15
|
+
'@pyreon/server',
|
|
16
16
|
])
|
|
17
17
|
|
|
18
18
|
const UI_PACKAGES = new Set([
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
'@pyreon/ui-core',
|
|
20
|
+
'@pyreon/styler',
|
|
21
|
+
'@pyreon/unistyle',
|
|
22
|
+
'@pyreon/elements',
|
|
23
|
+
'@pyreon/attrs',
|
|
24
|
+
'@pyreon/rocketstyle',
|
|
25
|
+
'@pyreon/coolgrid',
|
|
26
|
+
'@pyreon/kinetic',
|
|
27
|
+
'@pyreon/kinetic-presets',
|
|
28
|
+
'@pyreon/connector-document',
|
|
29
|
+
'@pyreon/document-primitives',
|
|
30
30
|
])
|
|
31
31
|
|
|
32
32
|
function getImportCategory(source: string): PackageCategory | null {
|
|
33
|
-
if (CORE_PACKAGES.has(source)) return
|
|
34
|
-
if (UI_PACKAGES.has(source)) return
|
|
33
|
+
if (CORE_PACKAGES.has(source)) return 'core'
|
|
34
|
+
if (UI_PACKAGES.has(source)) return 'ui-system'
|
|
35
35
|
return null
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
function getFileCategory(filePath: string): PackageCategory | null {
|
|
39
|
-
if (filePath.includes(
|
|
40
|
-
if (filePath.includes(
|
|
41
|
-
if (filePath.includes(
|
|
42
|
-
if (filePath.includes(
|
|
39
|
+
if (filePath.includes('/packages/core/')) return 'core'
|
|
40
|
+
if (filePath.includes('/packages/ui-system/')) return 'ui-system'
|
|
41
|
+
if (filePath.includes('/packages/fundamentals/')) return 'fundamentals'
|
|
42
|
+
if (filePath.includes('/packages/tools/')) return 'tools'
|
|
43
43
|
return null
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export const noCrossLayerImport: Rule = {
|
|
47
47
|
meta: {
|
|
48
|
-
id:
|
|
49
|
-
category:
|
|
50
|
-
description:
|
|
51
|
-
severity:
|
|
48
|
+
id: 'pyreon/no-cross-layer-import',
|
|
49
|
+
category: 'architecture',
|
|
50
|
+
description: 'Prevent core packages from importing ui-system packages.',
|
|
51
|
+
severity: 'error',
|
|
52
52
|
fixable: false,
|
|
53
53
|
},
|
|
54
54
|
create(context) {
|
|
55
55
|
const filePath = context.getFilePath()
|
|
56
56
|
const fileCategory = getFileCategory(filePath)
|
|
57
|
-
if (fileCategory !==
|
|
57
|
+
if (fileCategory !== 'core') return {}
|
|
58
58
|
|
|
59
59
|
const callbacks: VisitorCallbacks = {
|
|
60
60
|
ImportDeclaration(node: any) {
|
|
@@ -62,7 +62,7 @@ export const noCrossLayerImport: Rule = {
|
|
|
62
62
|
if (!source || !isPyreonImport(source)) return
|
|
63
63
|
|
|
64
64
|
const importCategory = getImportCategory(source)
|
|
65
|
-
if (importCategory ===
|
|
65
|
+
if (importCategory === 'ui-system') {
|
|
66
66
|
context.report({
|
|
67
67
|
message: `Core package importing ui-system package \`${source}\` — core packages must not depend on ui-system.`,
|
|
68
68
|
span: getSpan(node),
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import type { Rule, VisitorCallbacks } from
|
|
2
|
-
import { getSpan } from
|
|
3
|
-
import { isPyreonImport } from
|
|
1
|
+
import type { Rule, VisitorCallbacks } from '../../types'
|
|
2
|
+
import { getSpan } from '../../utils/ast'
|
|
3
|
+
import { isPyreonImport } from '../../utils/imports'
|
|
4
4
|
|
|
5
5
|
const DEEP_IMPORT_PATTERN = /@pyreon\/[^/]+\/(src|dist|lib)\//
|
|
6
6
|
|
|
7
7
|
export const noDeepImport: Rule = {
|
|
8
8
|
meta: {
|
|
9
|
-
id:
|
|
10
|
-
category:
|
|
9
|
+
id: 'pyreon/no-deep-import',
|
|
10
|
+
category: 'architecture',
|
|
11
11
|
description:
|
|
12
|
-
|
|
13
|
-
severity:
|
|
12
|
+
'Disallow importing from @pyreon/*/src/, /dist/, or /lib/ — use public exports instead.',
|
|
13
|
+
severity: 'warn',
|
|
14
14
|
fixable: false,
|
|
15
15
|
},
|
|
16
16
|
create(context) {
|
|
@@ -1,22 +1,22 @@
|
|
|
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 noErrorWithoutPrefix: Rule = {
|
|
5
5
|
meta: {
|
|
6
|
-
id:
|
|
7
|
-
category:
|
|
8
|
-
description:
|
|
9
|
-
severity:
|
|
6
|
+
id: 'pyreon/no-error-without-prefix',
|
|
7
|
+
category: 'architecture',
|
|
8
|
+
description: 'Require error messages to be prefixed with [Pyreon].',
|
|
9
|
+
severity: 'warn',
|
|
10
10
|
fixable: true,
|
|
11
11
|
},
|
|
12
12
|
create(context) {
|
|
13
13
|
const filePath = context.getFilePath()
|
|
14
14
|
// Skip test files
|
|
15
15
|
if (
|
|
16
|
-
filePath.includes(
|
|
17
|
-
filePath.includes(
|
|
18
|
-
filePath.includes(
|
|
19
|
-
filePath.includes(
|
|
16
|
+
filePath.includes('/tests/') ||
|
|
17
|
+
filePath.includes('/test/') ||
|
|
18
|
+
filePath.includes('.test.') ||
|
|
19
|
+
filePath.includes('.spec.')
|
|
20
20
|
) {
|
|
21
21
|
return {}
|
|
22
22
|
}
|
|
@@ -24,9 +24,9 @@ export const noErrorWithoutPrefix: Rule = {
|
|
|
24
24
|
const callbacks: VisitorCallbacks = {
|
|
25
25
|
ThrowStatement(node: any) {
|
|
26
26
|
const arg = node.argument
|
|
27
|
-
if (!arg || arg.type !==
|
|
27
|
+
if (!arg || arg.type !== 'NewExpression') return
|
|
28
28
|
const callee = arg.callee
|
|
29
|
-
if (!callee || callee.type !==
|
|
29
|
+
if (!callee || callee.type !== 'Identifier' || callee.name !== 'Error') return
|
|
30
30
|
|
|
31
31
|
const args = arg.arguments
|
|
32
32
|
if (!args || args.length === 0) return
|
|
@@ -34,34 +34,34 @@ export const noErrorWithoutPrefix: Rule = {
|
|
|
34
34
|
const firstArg = args[0]
|
|
35
35
|
if (!firstArg) return
|
|
36
36
|
|
|
37
|
-
if (firstArg.type ===
|
|
37
|
+
if (firstArg.type === 'Literal' || firstArg.type === 'StringLiteral') {
|
|
38
38
|
const value = firstArg.value as string
|
|
39
|
-
if (typeof value ===
|
|
39
|
+
if (typeof value === 'string' && !value.startsWith('[Pyreon]')) {
|
|
40
40
|
const argSpan = getSpan(firstArg)
|
|
41
41
|
// Fix: add [Pyreon] prefix
|
|
42
42
|
const quote = context.getSourceText()[argSpan.start]
|
|
43
43
|
const fixedValue = `${quote}[Pyreon] ${value}${quote}`
|
|
44
44
|
context.report({
|
|
45
45
|
message:
|
|
46
|
-
|
|
46
|
+
'Error message missing `[Pyreon]` prefix — all framework errors should be prefixed for identification.',
|
|
47
47
|
span: getSpan(node),
|
|
48
48
|
fix: { span: argSpan, replacement: fixedValue },
|
|
49
49
|
})
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
if (firstArg.type ===
|
|
53
|
+
if (firstArg.type === 'TemplateLiteral') {
|
|
54
54
|
const quasis = firstArg.quasis
|
|
55
55
|
if (quasis && quasis.length > 0) {
|
|
56
56
|
const first = quasis[0]
|
|
57
|
-
const raw = first.value?.raw ?? first.value?.cooked ??
|
|
58
|
-
if (!raw.startsWith(
|
|
57
|
+
const raw = first.value?.raw ?? first.value?.cooked ?? ''
|
|
58
|
+
if (!raw.startsWith('[Pyreon]')) {
|
|
59
59
|
const argSpan = getSpan(firstArg)
|
|
60
60
|
const source = context.getSourceText().slice(argSpan.start, argSpan.end)
|
|
61
|
-
const fixed = source.replace(/^`/,
|
|
61
|
+
const fixed = source.replace(/^`/, '`[Pyreon] ')
|
|
62
62
|
context.report({
|
|
63
63
|
message:
|
|
64
|
-
|
|
64
|
+
'Error message missing `[Pyreon]` prefix — all framework errors should be prefixed for identification.',
|
|
65
65
|
span: getSpan(node),
|
|
66
66
|
fix: { span: argSpan, replacement: fixed },
|
|
67
67
|
})
|
|
@@ -1,40 +1,40 @@
|
|
|
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 noSubmitWithoutValidation: Rule = {
|
|
5
5
|
meta: {
|
|
6
|
-
id:
|
|
7
|
-
category:
|
|
8
|
-
description:
|
|
9
|
-
severity:
|
|
6
|
+
id: 'pyreon/no-submit-without-validation',
|
|
7
|
+
category: 'form',
|
|
8
|
+
description: 'Warn when useForm() has onSubmit but no validators or schema.',
|
|
9
|
+
severity: 'warn',
|
|
10
10
|
fixable: false,
|
|
11
11
|
},
|
|
12
12
|
create(context) {
|
|
13
13
|
const callbacks: VisitorCallbacks = {
|
|
14
14
|
CallExpression(node: any) {
|
|
15
|
-
if (!isCallTo(node,
|
|
15
|
+
if (!isCallTo(node, 'useForm')) return
|
|
16
16
|
const args = node.arguments
|
|
17
17
|
if (!args || args.length === 0) return
|
|
18
18
|
|
|
19
19
|
const options = args[0]
|
|
20
|
-
if (!options || options.type !==
|
|
20
|
+
if (!options || options.type !== 'ObjectExpression') return
|
|
21
21
|
|
|
22
22
|
let hasOnSubmit = false
|
|
23
23
|
let hasValidation = false
|
|
24
24
|
|
|
25
25
|
for (const prop of options.properties ?? []) {
|
|
26
|
-
if (prop.type !==
|
|
26
|
+
if (prop.type !== 'Property') continue
|
|
27
27
|
const key = prop.key
|
|
28
28
|
if (!key) continue
|
|
29
|
-
const name = key.type ===
|
|
30
|
-
if (name ===
|
|
31
|
-
if (name ===
|
|
29
|
+
const name = key.type === 'Identifier' ? key.name : null
|
|
30
|
+
if (name === 'onSubmit') hasOnSubmit = true
|
|
31
|
+
if (name === 'validators' || name === 'schema') hasValidation = true
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
if (hasOnSubmit && !hasValidation) {
|
|
35
35
|
context.report({
|
|
36
36
|
message:
|
|
37
|
-
|
|
37
|
+
'`useForm()` has `onSubmit` without `validators` or `schema` — consider adding validation for data integrity.',
|
|
38
38
|
span: getSpan(node),
|
|
39
39
|
})
|
|
40
40
|
}
|
|
@@ -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 noUnregisteredField: Rule = {
|
|
5
5
|
meta: {
|
|
6
|
-
id:
|
|
7
|
-
category:
|
|
8
|
-
description:
|
|
9
|
-
severity:
|
|
6
|
+
id: 'pyreon/no-unregistered-field',
|
|
7
|
+
category: 'form',
|
|
8
|
+
description: 'Warn when useField() is called without a corresponding register() call.',
|
|
9
|
+
severity: 'warn',
|
|
10
10
|
fixable: false,
|
|
11
11
|
},
|
|
12
12
|
create(context) {
|
|
@@ -16,20 +16,20 @@ export const noUnregisteredField: Rule = {
|
|
|
16
16
|
const callbacks: VisitorCallbacks = {
|
|
17
17
|
VariableDeclarator(node: any) {
|
|
18
18
|
const init = node.init
|
|
19
|
-
if (!init || !isCallTo(init,
|
|
19
|
+
if (!init || !isCallTo(init, 'useField')) return
|
|
20
20
|
const id = node.id
|
|
21
|
-
if (!id || id.type !==
|
|
21
|
+
if (!id || id.type !== 'Identifier') return
|
|
22
22
|
fieldDecls.set(id.name, { span: getSpan(node) })
|
|
23
23
|
},
|
|
24
24
|
CallExpression(node: any) {
|
|
25
25
|
const callee = node.callee
|
|
26
|
-
if (!callee || callee.type !==
|
|
27
|
-
if (callee.property?.type !==
|
|
28
|
-
if (callee.object?.type ===
|
|
26
|
+
if (!callee || callee.type !== 'MemberExpression') return
|
|
27
|
+
if (callee.property?.type !== 'Identifier' || callee.property.name !== 'register') return
|
|
28
|
+
if (callee.object?.type === 'Identifier') {
|
|
29
29
|
registeredNames.add(callee.object.name)
|
|
30
30
|
}
|
|
31
31
|
},
|
|
32
|
-
|
|
32
|
+
'Program:exit'() {
|
|
33
33
|
for (const [name, { span }] of fieldDecls) {
|
|
34
34
|
if (!registeredNames.has(name)) {
|
|
35
35
|
context.report({
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import type { Rule, VisitorCallbacks } from
|
|
2
|
-
import { getSpan, isCallTo } from
|
|
3
|
-
import { extractImportInfo } from
|
|
1
|
+
import type { Rule, VisitorCallbacks } from '../../types'
|
|
2
|
+
import { getSpan, isCallTo } from '../../utils/ast'
|
|
3
|
+
import { extractImportInfo } from '../../utils/imports'
|
|
4
4
|
|
|
5
5
|
export const preferFieldArray: Rule = {
|
|
6
6
|
meta: {
|
|
7
|
-
id:
|
|
8
|
-
category:
|
|
9
|
-
description:
|
|
10
|
-
severity:
|
|
7
|
+
id: 'pyreon/prefer-field-array',
|
|
8
|
+
category: 'form',
|
|
9
|
+
description: 'Suggest useFieldArray() instead of signal([]) in files that import @pyreon/form.',
|
|
10
|
+
severity: 'info',
|
|
11
11
|
fixable: false,
|
|
12
12
|
},
|
|
13
13
|
create(context) {
|
|
@@ -16,21 +16,21 @@ export const preferFieldArray: Rule = {
|
|
|
16
16
|
const callbacks: VisitorCallbacks = {
|
|
17
17
|
ImportDeclaration(node: any) {
|
|
18
18
|
const info = extractImportInfo(node)
|
|
19
|
-
if (info && info.source ===
|
|
19
|
+
if (info && info.source === '@pyreon/form') {
|
|
20
20
|
importsForm = true
|
|
21
21
|
}
|
|
22
22
|
},
|
|
23
23
|
CallExpression(node: any) {
|
|
24
24
|
if (!importsForm) return
|
|
25
|
-
if (!isCallTo(node,
|
|
25
|
+
if (!isCallTo(node, 'signal')) return
|
|
26
26
|
|
|
27
27
|
const args = node.arguments
|
|
28
28
|
if (!args || args.length === 0) return
|
|
29
29
|
const firstArg = args[0]
|
|
30
|
-
if (firstArg?.type ===
|
|
30
|
+
if (firstArg?.type === 'ArrayExpression') {
|
|
31
31
|
context.report({
|
|
32
32
|
message:
|
|
33
|
-
|
|
33
|
+
'`signal([])` in a form file — consider using `useFieldArray()` for dynamic array fields with stable keys.',
|
|
34
34
|
span: getSpan(node),
|
|
35
35
|
})
|
|
36
36
|
}
|
|
@@ -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 noRawAddEventListener: Rule = {
|
|
5
5
|
meta: {
|
|
6
|
-
id:
|
|
7
|
-
category:
|
|
8
|
-
description:
|
|
9
|
-
severity:
|
|
6
|
+
id: 'pyreon/no-raw-addeventlistener',
|
|
7
|
+
category: 'hooks',
|
|
8
|
+
description: 'Suggest useEventListener() instead of raw .addEventListener() calls.',
|
|
9
|
+
severity: 'info',
|
|
10
10
|
fixable: false,
|
|
11
11
|
},
|
|
12
12
|
create(context) {
|
|
13
13
|
const callbacks: VisitorCallbacks = {
|
|
14
14
|
CallExpression(node: any) {
|
|
15
15
|
const callee = node.callee
|
|
16
|
-
if (!callee || callee.type !==
|
|
17
|
-
if (callee.property?.type !==
|
|
16
|
+
if (!callee || callee.type !== 'MemberExpression') return
|
|
17
|
+
if (callee.property?.type !== 'Identifier' || callee.property.name !== 'addEventListener')
|
|
18
18
|
return
|
|
19
19
|
context.report({
|
|
20
20
|
message:
|
|
21
|
-
|
|
21
|
+
'Raw `.addEventListener()` — consider using `useEventListener()` from `@pyreon/hooks` for auto-cleanup on unmount.',
|
|
22
22
|
span: getSpan(node),
|
|
23
23
|
})
|
|
24
24
|
},
|
|
@@ -1,26 +1,26 @@
|
|
|
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 STORAGE_OBJECTS = new Set([
|
|
5
|
-
const STORAGE_METHODS = new Set([
|
|
4
|
+
const STORAGE_OBJECTS = new Set(['localStorage', 'sessionStorage'])
|
|
5
|
+
const STORAGE_METHODS = new Set(['getItem', 'setItem', 'removeItem'])
|
|
6
6
|
|
|
7
7
|
export const noRawLocalStorage: Rule = {
|
|
8
8
|
meta: {
|
|
9
|
-
id:
|
|
10
|
-
category:
|
|
11
|
-
description:
|
|
12
|
-
severity:
|
|
9
|
+
id: 'pyreon/no-raw-localstorage',
|
|
10
|
+
category: 'hooks',
|
|
11
|
+
description: 'Suggest useStorage() instead of raw localStorage/sessionStorage access.',
|
|
12
|
+
severity: 'info',
|
|
13
13
|
fixable: false,
|
|
14
14
|
},
|
|
15
15
|
create(context) {
|
|
16
16
|
const callbacks: VisitorCallbacks = {
|
|
17
17
|
CallExpression(node: any) {
|
|
18
18
|
const callee = node.callee
|
|
19
|
-
if (!callee || callee.type !==
|
|
19
|
+
if (!callee || callee.type !== 'MemberExpression') return
|
|
20
20
|
if (
|
|
21
|
-
callee.object?.type ===
|
|
21
|
+
callee.object?.type === 'Identifier' &&
|
|
22
22
|
STORAGE_OBJECTS.has(callee.object.name) &&
|
|
23
|
-
callee.property?.type ===
|
|
23
|
+
callee.property?.type === 'Identifier' &&
|
|
24
24
|
STORAGE_METHODS.has(callee.property.name)
|
|
25
25
|
) {
|
|
26
26
|
context.report({
|
|
@@ -1,28 +1,28 @@
|
|
|
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 TIMER_FNS = new Set([
|
|
4
|
+
const TIMER_FNS = new Set(['setInterval', 'setTimeout'])
|
|
5
5
|
|
|
6
6
|
export const noRawSetInterval: Rule = {
|
|
7
7
|
meta: {
|
|
8
|
-
id:
|
|
9
|
-
category:
|
|
10
|
-
description:
|
|
11
|
-
severity:
|
|
8
|
+
id: 'pyreon/no-raw-setinterval',
|
|
9
|
+
category: 'hooks',
|
|
10
|
+
description: 'Suggest wrapping setInterval/setTimeout in onMount for automatic cleanup.',
|
|
11
|
+
severity: 'info',
|
|
12
12
|
fixable: false,
|
|
13
13
|
},
|
|
14
14
|
create(context) {
|
|
15
15
|
let mountDepth = 0
|
|
16
16
|
const callbacks: VisitorCallbacks = {
|
|
17
17
|
CallExpression(node: any) {
|
|
18
|
-
if (isCallTo(node,
|
|
18
|
+
if (isCallTo(node, 'onMount')) {
|
|
19
19
|
mountDepth++
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
if (mountDepth > 0) return
|
|
23
23
|
|
|
24
24
|
const callee = node.callee
|
|
25
|
-
if (!callee || callee.type !==
|
|
25
|
+
if (!callee || callee.type !== 'Identifier') return
|
|
26
26
|
if (TIMER_FNS.has(callee.name)) {
|
|
27
27
|
context.report({
|
|
28
28
|
message: `\`${callee.name}()\` outside \`onMount\` — wrap in \`onMount(() => { ... return () => clear... })\` for automatic cleanup.`,
|
|
@@ -30,8 +30,8 @@ export const noRawSetInterval: Rule = {
|
|
|
30
30
|
})
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
|
-
|
|
34
|
-
if (isCallTo(node,
|
|
33
|
+
'CallExpression:exit'(node: any) {
|
|
34
|
+
if (isCallTo(node, 'onMount')) {
|
|
35
35
|
mountDepth--
|
|
36
36
|
}
|
|
37
37
|
},
|