@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,22 +1,22 @@
|
|
|
1
|
-
import type { Rule, VisitorCallbacks } from
|
|
2
|
-
import { getSpan } from
|
|
3
|
-
import { extractImportInfo } from
|
|
1
|
+
import type { Rule, VisitorCallbacks } from '../../types'
|
|
2
|
+
import { getSpan } from '../../utils/ast'
|
|
3
|
+
import { extractImportInfo } from '../../utils/imports'
|
|
4
4
|
|
|
5
5
|
export const noStoreOutsideProvider: Rule = {
|
|
6
6
|
meta: {
|
|
7
|
-
id:
|
|
8
|
-
category:
|
|
9
|
-
description:
|
|
10
|
-
severity:
|
|
7
|
+
id: 'pyreon/no-store-outside-provider',
|
|
8
|
+
category: 'store',
|
|
9
|
+
description: 'Warn when store hooks are used in SSR files without a provider import.',
|
|
10
|
+
severity: 'warn',
|
|
11
11
|
fixable: false,
|
|
12
12
|
},
|
|
13
13
|
create(context) {
|
|
14
14
|
const filePath = context.getFilePath()
|
|
15
15
|
const isServerFile =
|
|
16
|
-
filePath.includes(
|
|
17
|
-
filePath.includes(
|
|
18
|
-
filePath.endsWith(
|
|
19
|
-
filePath.endsWith(
|
|
16
|
+
filePath.includes('server') ||
|
|
17
|
+
filePath.includes('.server.') ||
|
|
18
|
+
filePath.endsWith('server.ts') ||
|
|
19
|
+
filePath.endsWith('server.tsx')
|
|
20
20
|
|
|
21
21
|
if (!isServerFile) return {}
|
|
22
22
|
|
|
@@ -30,7 +30,7 @@ export const noStoreOutsideProvider: Rule = {
|
|
|
30
30
|
if (
|
|
31
31
|
info.specifiers.some(
|
|
32
32
|
(s) =>
|
|
33
|
-
s.imported ===
|
|
33
|
+
s.imported === 'setStoreRegistryProvider' || s.imported === 'runWithRequestContext',
|
|
34
34
|
)
|
|
35
35
|
) {
|
|
36
36
|
hasProviderImport = true
|
|
@@ -38,13 +38,13 @@ export const noStoreOutsideProvider: Rule = {
|
|
|
38
38
|
},
|
|
39
39
|
CallExpression(node: any) {
|
|
40
40
|
const callee = node.callee
|
|
41
|
-
if (!callee || callee.type !==
|
|
41
|
+
if (!callee || callee.type !== 'Identifier') return
|
|
42
42
|
const name: string = callee.name
|
|
43
|
-
if (name.endsWith(
|
|
43
|
+
if (name.endsWith('Store') && name.startsWith('use')) {
|
|
44
44
|
storeHookCalls.push({ name, span: getSpan(node) })
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
|
-
|
|
47
|
+
'Program:exit'() {
|
|
48
48
|
if (hasProviderImport) return
|
|
49
49
|
for (const call of storeHookCalls) {
|
|
50
50
|
context.report({
|
|
@@ -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 noDynamicStyled: Rule = {
|
|
5
5
|
meta: {
|
|
6
|
-
id:
|
|
7
|
-
category:
|
|
6
|
+
id: 'pyreon/no-dynamic-styled',
|
|
7
|
+
category: 'styling',
|
|
8
8
|
description:
|
|
9
|
-
|
|
10
|
-
severity:
|
|
9
|
+
'Warn when styled() is called inside a function — it creates new CSS on every render.',
|
|
10
|
+
severity: 'warn',
|
|
11
11
|
fixable: false,
|
|
12
12
|
},
|
|
13
13
|
create(context) {
|
|
@@ -16,27 +16,27 @@ export const noDynamicStyled: Rule = {
|
|
|
16
16
|
FunctionDeclaration() {
|
|
17
17
|
functionDepth++
|
|
18
18
|
},
|
|
19
|
-
|
|
19
|
+
'FunctionDeclaration:exit'() {
|
|
20
20
|
functionDepth--
|
|
21
21
|
},
|
|
22
22
|
FunctionExpression() {
|
|
23
23
|
functionDepth++
|
|
24
24
|
},
|
|
25
|
-
|
|
25
|
+
'FunctionExpression:exit'() {
|
|
26
26
|
functionDepth--
|
|
27
27
|
},
|
|
28
28
|
ArrowFunctionExpression() {
|
|
29
29
|
functionDepth++
|
|
30
30
|
},
|
|
31
|
-
|
|
31
|
+
'ArrowFunctionExpression:exit'() {
|
|
32
32
|
functionDepth--
|
|
33
33
|
},
|
|
34
34
|
CallExpression(node: any) {
|
|
35
35
|
if (functionDepth === 0) return
|
|
36
|
-
if (isCallTo(node,
|
|
36
|
+
if (isCallTo(node, 'styled')) {
|
|
37
37
|
context.report({
|
|
38
38
|
message:
|
|
39
|
-
|
|
39
|
+
'`styled()` inside a function — this creates new CSS rules on every render. Move `styled()` to module scope.',
|
|
40
40
|
span: getSpan(node),
|
|
41
41
|
})
|
|
42
42
|
}
|
|
@@ -46,10 +46,10 @@ export const noDynamicStyled: Rule = {
|
|
|
46
46
|
const tag = node.tag
|
|
47
47
|
if (!tag) return
|
|
48
48
|
// styled('div')`...` — tag is a CallExpression of styled
|
|
49
|
-
if (tag.type ===
|
|
49
|
+
if (tag.type === 'CallExpression' && isCallTo(tag, 'styled')) {
|
|
50
50
|
context.report({
|
|
51
51
|
message:
|
|
52
|
-
|
|
52
|
+
'`styled()` tagged template inside a function — this creates new CSS rules on every render. Move to module scope.',
|
|
53
53
|
span: getSpan(node),
|
|
54
54
|
})
|
|
55
55
|
}
|
|
@@ -1,25 +1,25 @@
|
|
|
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 noInlineStyleObject: Rule = {
|
|
5
5
|
meta: {
|
|
6
|
-
id:
|
|
7
|
-
category:
|
|
8
|
-
description:
|
|
9
|
-
severity:
|
|
6
|
+
id: 'pyreon/no-inline-style-object',
|
|
7
|
+
category: 'styling',
|
|
8
|
+
description: 'Warn against inline style objects in JSX — prefer styled() or css``.',
|
|
9
|
+
severity: 'warn',
|
|
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?.type ===
|
|
19
|
+
if (expr?.type === 'ObjectExpression') {
|
|
20
20
|
context.report({
|
|
21
21
|
message:
|
|
22
|
-
|
|
22
|
+
'Inline style object in JSX — consider using `styled()` or `css\\`...\\`` for better performance and caching.',
|
|
23
23
|
span: getSpan(node),
|
|
24
24
|
})
|
|
25
25
|
}
|
|
@@ -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 noThemeOutsideProvider: Rule = {
|
|
6
6
|
meta: {
|
|
7
|
-
id:
|
|
8
|
-
category:
|
|
9
|
-
description:
|
|
10
|
-
severity:
|
|
7
|
+
id: 'pyreon/no-theme-outside-provider',
|
|
8
|
+
category: 'styling',
|
|
9
|
+
description: 'Warn when useTheme() is used without PyreonUI or ThemeProvider in the same file.',
|
|
10
|
+
severity: 'warn',
|
|
11
11
|
fixable: false,
|
|
12
12
|
},
|
|
13
13
|
create(context) {
|
|
@@ -19,22 +19,22 @@ export const noThemeOutsideProvider: Rule = {
|
|
|
19
19
|
const info = extractImportInfo(node)
|
|
20
20
|
if (!info) return
|
|
21
21
|
if (
|
|
22
|
-
info.specifiers.some((s) => s.imported ===
|
|
22
|
+
info.specifiers.some((s) => s.imported === 'PyreonUI' || s.imported === 'ThemeProvider')
|
|
23
23
|
) {
|
|
24
24
|
hasProviderImport = true
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
27
|
CallExpression(node: any) {
|
|
28
|
-
if (isCallTo(node,
|
|
28
|
+
if (isCallTo(node, 'useTheme')) {
|
|
29
29
|
themeCalls.push({ span: getSpan(node) })
|
|
30
30
|
}
|
|
31
31
|
},
|
|
32
|
-
|
|
32
|
+
'Program:exit'() {
|
|
33
33
|
if (hasProviderImport) return
|
|
34
34
|
for (const call of themeCalls) {
|
|
35
35
|
context.report({
|
|
36
36
|
message:
|
|
37
|
-
|
|
37
|
+
'`useTheme()` without a `PyreonUI` or `ThemeProvider` import — the theme context may not be available.',
|
|
38
38
|
span: call.span,
|
|
39
39
|
})
|
|
40
40
|
}
|
|
@@ -1,39 +1,39 @@
|
|
|
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 preferCx: Rule = {
|
|
5
5
|
meta: {
|
|
6
|
-
id:
|
|
7
|
-
category:
|
|
6
|
+
id: 'pyreon/prefer-cx',
|
|
7
|
+
category: 'styling',
|
|
8
8
|
description:
|
|
9
|
-
|
|
10
|
-
severity:
|
|
9
|
+
'Suggest cx() for class composition instead of string concatenation or template literals.',
|
|
10
|
+
severity: 'info',
|
|
11
11
|
fixable: false,
|
|
12
12
|
},
|
|
13
13
|
create(context) {
|
|
14
14
|
const callbacks: VisitorCallbacks = {
|
|
15
15
|
JSXAttribute(node: any) {
|
|
16
|
-
if (node.name?.type !==
|
|
16
|
+
if (node.name?.type !== 'JSXIdentifier' || node.name.name !== 'class') return
|
|
17
17
|
const value = node.value
|
|
18
|
-
if (!value || value.type !==
|
|
18
|
+
if (!value || value.type !== 'JSXExpressionContainer') return
|
|
19
19
|
const expr = value.expression
|
|
20
20
|
if (!expr) return
|
|
21
21
|
|
|
22
22
|
// String concatenation: "foo " + bar
|
|
23
|
-
if (expr.type ===
|
|
23
|
+
if (expr.type === 'BinaryExpression' && expr.operator === '+') {
|
|
24
24
|
context.report({
|
|
25
25
|
message:
|
|
26
|
-
|
|
26
|
+
'String concatenation in `class` attribute — use `cx()` for cleaner class composition.',
|
|
27
27
|
span: getSpan(expr),
|
|
28
28
|
})
|
|
29
29
|
return
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
// Template literal: `foo ${bar}`
|
|
33
|
-
if (expr.type ===
|
|
33
|
+
if (expr.type === 'TemplateLiteral' && expr.expressions?.length > 0) {
|
|
34
34
|
context.report({
|
|
35
35
|
message:
|
|
36
|
-
|
|
36
|
+
'Template literal in `class` attribute — use `cx()` for cleaner class composition.',
|
|
37
37
|
span: getSpan(expr),
|
|
38
38
|
})
|
|
39
39
|
}
|
package/src/runner.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { parseSync, Visitor } from
|
|
2
|
-
import type { AstCache } from
|
|
1
|
+
import { parseSync, Visitor } from 'oxc-parser'
|
|
2
|
+
import type { AstCache } from './cache'
|
|
3
3
|
import type {
|
|
4
4
|
Diagnostic,
|
|
5
5
|
LintConfig,
|
|
@@ -8,21 +8,21 @@ import type {
|
|
|
8
8
|
RuleContext,
|
|
9
9
|
Severity,
|
|
10
10
|
VisitorCallbacks,
|
|
11
|
-
} from
|
|
12
|
-
import { JS_EXTENSIONS } from
|
|
13
|
-
import { LineIndex } from
|
|
11
|
+
} from './types'
|
|
12
|
+
import { JS_EXTENSIONS } from './utils/index'
|
|
13
|
+
import { LineIndex } from './utils/source'
|
|
14
14
|
|
|
15
15
|
function getExtension(filePath: string): string {
|
|
16
|
-
const lastDot = filePath.lastIndexOf(
|
|
17
|
-
return lastDot === -1 ?
|
|
16
|
+
const lastDot = filePath.lastIndexOf('.')
|
|
17
|
+
return lastDot === -1 ? '' : filePath.slice(lastDot)
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
type OxcLang =
|
|
20
|
+
type OxcLang = 'jsx' | 'tsx' | 'ts' | 'js' | 'dts'
|
|
21
21
|
|
|
22
22
|
function getLang(ext: string): OxcLang {
|
|
23
|
-
if (ext ===
|
|
24
|
-
if (ext ===
|
|
25
|
-
return
|
|
23
|
+
if (ext === '.tsx' || ext === '.jsx') return 'tsx'
|
|
24
|
+
if (ext === '.ts' || ext === '.mts') return 'ts'
|
|
25
|
+
return 'js'
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function createRuleContext(
|
|
@@ -113,7 +113,7 @@ export function lintFile(
|
|
|
113
113
|
lineIndex = new LineIndex(sourceText)
|
|
114
114
|
try {
|
|
115
115
|
const result = parseSync(filePath, sourceText, {
|
|
116
|
-
sourceType:
|
|
116
|
+
sourceType: 'module',
|
|
117
117
|
lang: getLang(ext),
|
|
118
118
|
})
|
|
119
119
|
program = result.program
|
|
@@ -129,7 +129,7 @@ export function lintFile(
|
|
|
129
129
|
const allCallbacks: VisitorCallbacks[] = []
|
|
130
130
|
for (const rule of rules) {
|
|
131
131
|
const severity = config.rules[rule.meta.id]
|
|
132
|
-
if (severity === undefined || severity ===
|
|
132
|
+
if (severity === undefined || severity === 'off') continue
|
|
133
133
|
const ctx = createRuleContext(rule, severity, diagnostics, lineIndex, sourceText, filePath)
|
|
134
134
|
allCallbacks.push(rule.create(ctx))
|
|
135
135
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { getPreset } from '../config/presets'
|
|
2
|
+
import { allRules } from '../rules/index'
|
|
3
|
+
import { lintFile } from '../runner'
|
|
4
|
+
|
|
5
|
+
describe('LSP diagnostic conversion', () => {
|
|
6
|
+
const config = getPreset('recommended')
|
|
7
|
+
|
|
8
|
+
it('converts lint diagnostics to LSP format', () => {
|
|
9
|
+
const source = `
|
|
10
|
+
import { useContext } from '@pyreon/core'
|
|
11
|
+
const { mode } = useContext(ThemeCtx)
|
|
12
|
+
`
|
|
13
|
+
const result = lintFile('test.tsx', source, allRules, config)
|
|
14
|
+
const diag = result.diagnostics.find(
|
|
15
|
+
(d) => d.ruleId === 'pyreon/no-context-destructure',
|
|
16
|
+
)
|
|
17
|
+
expect(diag).toBeDefined()
|
|
18
|
+
expect(diag!.loc.line).toBeGreaterThan(0)
|
|
19
|
+
expect(diag!.loc.column).toBeGreaterThan(0)
|
|
20
|
+
expect(diag!.span.start).toBeLessThan(diag!.span.end)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('LSP severity mapping — error=1, warn=2, info=3', () => {
|
|
24
|
+
const severityMap: Record<string, number> = {
|
|
25
|
+
error: 1,
|
|
26
|
+
warn: 2,
|
|
27
|
+
info: 3,
|
|
28
|
+
}
|
|
29
|
+
expect(severityMap.error).toBe(1)
|
|
30
|
+
expect(severityMap.warn).toBe(2)
|
|
31
|
+
expect(severityMap.info).toBe(3)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('source diagnostics use 1-based lines (LSP layer converts to 0-based)', () => {
|
|
35
|
+
const source = `import { signal } from '@pyreon/reactivity'\nconst x = signal(0)\n{x}\n`
|
|
36
|
+
const result = lintFile('test.tsx', source, allRules, config)
|
|
37
|
+
|
|
38
|
+
for (const d of result.diagnostics) {
|
|
39
|
+
expect(d.loc.line).toBeGreaterThanOrEqual(1)
|
|
40
|
+
expect(d.loc.column).toBeGreaterThanOrEqual(1)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('handles empty source without crashing', () => {
|
|
45
|
+
const result = lintFile('test.tsx', '', allRules, config)
|
|
46
|
+
expect(result.diagnostics).toEqual([])
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('handles non-JS file gracefully', () => {
|
|
50
|
+
const result = lintFile('test.css', 'body { color: red }', allRules, config)
|
|
51
|
+
expect(result.diagnostics).toEqual([])
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('handles syntax errors gracefully', () => {
|
|
55
|
+
const result = lintFile('test.tsx', 'const x = {{{', allRules, config)
|
|
56
|
+
// Should not throw — returns empty or partial diagnostics
|
|
57
|
+
expect(result.diagnostics).toBeDefined()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('multiple diagnostics are sorted by position', () => {
|
|
61
|
+
const source = `
|
|
62
|
+
import { useContext } from '@pyreon/core'
|
|
63
|
+
const { a } = useContext(Ctx1)
|
|
64
|
+
const { b } = useContext(Ctx2)
|
|
65
|
+
`
|
|
66
|
+
const result = lintFile('test.tsx', source, allRules, config)
|
|
67
|
+
const ctxDiags = result.diagnostics.filter(
|
|
68
|
+
(d) => d.ruleId === 'pyreon/no-context-destructure',
|
|
69
|
+
)
|
|
70
|
+
expect(ctxDiags.length).toBe(2)
|
|
71
|
+
expect(ctxDiags[0]!.span.start).toBeLessThan(ctxDiags[1]!.span.start)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('diagnostic span covers the destructured pattern', () => {
|
|
75
|
+
const source = `
|
|
76
|
+
import { useContext } from '@pyreon/core'
|
|
77
|
+
const { mode, theme } = useContext(ThemeCtx)
|
|
78
|
+
`
|
|
79
|
+
const result = lintFile('test.tsx', source, allRules, config)
|
|
80
|
+
const diag = result.diagnostics.find(
|
|
81
|
+
(d) => d.ruleId === 'pyreon/no-context-destructure',
|
|
82
|
+
)
|
|
83
|
+
expect(diag).toBeDefined()
|
|
84
|
+
const spanned = source.slice(diag!.span.start, diag!.span.end)
|
|
85
|
+
expect(spanned).toContain('mode')
|
|
86
|
+
expect(spanned).toContain('theme')
|
|
87
|
+
})
|
|
88
|
+
})
|