@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.
Files changed (86) hide show
  1. package/README.md +91 -91
  2. package/lib/analysis/cli.js.html +5406 -0
  3. package/lib/analysis/index.js.html +1 -1
  4. package/lib/cli.js +3290 -0
  5. package/lib/cli.js.map +1 -0
  6. package/lib/index.js +220 -29
  7. package/lib/index.js.map +1 -1
  8. package/lib/types/index.d.ts +30 -5
  9. package/lib/types/index.d.ts.map +1 -1
  10. package/package.json +19 -19
  11. package/src/cache.ts +1 -1
  12. package/src/cli.ts +39 -28
  13. package/src/config/ignore.ts +23 -23
  14. package/src/config/loader.ts +8 -8
  15. package/src/config/presets.ts +11 -11
  16. package/src/index.ts +14 -12
  17. package/src/lint.ts +19 -25
  18. package/src/lsp/index.ts +225 -0
  19. package/src/reporter.ts +17 -17
  20. package/src/rules/accessibility/dialog-a11y.ts +10 -10
  21. package/src/rules/accessibility/overlay-a11y.ts +11 -11
  22. package/src/rules/accessibility/toast-a11y.ts +11 -11
  23. package/src/rules/architecture/dev-guard-warnings.ts +19 -19
  24. package/src/rules/architecture/no-circular-import.ts +16 -16
  25. package/src/rules/architecture/no-cross-layer-import.ts +35 -35
  26. package/src/rules/architecture/no-deep-import.ts +7 -7
  27. package/src/rules/architecture/no-error-without-prefix.ts +20 -20
  28. package/src/rules/form/no-submit-without-validation.ts +13 -13
  29. package/src/rules/form/no-unregistered-field.ts +12 -12
  30. package/src/rules/form/prefer-field-array.ts +11 -11
  31. package/src/rules/hooks/no-raw-addeventlistener.ts +9 -9
  32. package/src/rules/hooks/no-raw-localstorage.ts +11 -11
  33. package/src/rules/hooks/no-raw-setinterval.ts +11 -11
  34. package/src/rules/index.ts +60 -57
  35. package/src/rules/jsx/no-and-conditional.ts +8 -8
  36. package/src/rules/jsx/no-children-access.ts +12 -12
  37. package/src/rules/jsx/no-classname.ts +10 -10
  38. package/src/rules/jsx/no-htmlfor.ts +10 -10
  39. package/src/rules/jsx/no-index-as-by.ts +17 -17
  40. package/src/rules/jsx/no-map-in-jsx.ts +9 -9
  41. package/src/rules/jsx/no-missing-for-by.ts +9 -9
  42. package/src/rules/jsx/no-onchange.ts +12 -12
  43. package/src/rules/jsx/no-props-destructure.ts +11 -11
  44. package/src/rules/jsx/no-ternary-conditional.ts +8 -8
  45. package/src/rules/jsx/use-by-not-key.ts +12 -12
  46. package/src/rules/lifecycle/no-dom-in-setup.ts +18 -18
  47. package/src/rules/lifecycle/no-effect-in-mount.ts +11 -11
  48. package/src/rules/lifecycle/no-missing-cleanup.ts +19 -19
  49. package/src/rules/lifecycle/no-mount-in-effect.ts +11 -11
  50. package/src/rules/performance/no-eager-import.ts +7 -7
  51. package/src/rules/performance/no-effect-in-for.ts +10 -10
  52. package/src/rules/performance/no-large-for-without-by.ts +9 -9
  53. package/src/rules/performance/prefer-show-over-display.ts +16 -16
  54. package/src/rules/reactivity/no-bare-signal-in-jsx.ts +10 -10
  55. package/src/rules/reactivity/no-context-destructure.ts +45 -0
  56. package/src/rules/reactivity/no-effect-assignment.ts +16 -16
  57. package/src/rules/reactivity/no-nested-effect.ts +10 -10
  58. package/src/rules/reactivity/no-peek-in-tracked.ts +10 -10
  59. package/src/rules/reactivity/no-signal-in-loop.ts +13 -13
  60. package/src/rules/reactivity/no-signal-leak.ts +9 -9
  61. package/src/rules/reactivity/no-unbatched-updates.ts +12 -12
  62. package/src/rules/reactivity/prefer-computed.ts +13 -13
  63. package/src/rules/router/index.ts +4 -4
  64. package/src/rules/router/no-href-navigation.ts +14 -14
  65. package/src/rules/router/no-imperative-navigate-in-render.ts +19 -19
  66. package/src/rules/router/no-missing-fallback.ts +16 -16
  67. package/src/rules/router/prefer-use-is-active.ts +11 -11
  68. package/src/rules/ssr/no-mismatch-risk.ts +11 -11
  69. package/src/rules/ssr/no-window-in-ssr.ts +22 -22
  70. package/src/rules/ssr/prefer-request-context.ts +14 -14
  71. package/src/rules/store/no-duplicate-store-id.ts +9 -9
  72. package/src/rules/store/no-mutate-store-state.ts +11 -11
  73. package/src/rules/store/no-store-outside-provider.ts +15 -15
  74. package/src/rules/styling/no-dynamic-styled.ts +13 -13
  75. package/src/rules/styling/no-inline-style-object.ts +10 -10
  76. package/src/rules/styling/no-theme-outside-provider.ts +11 -11
  77. package/src/rules/styling/prefer-cx.ts +12 -12
  78. package/src/runner.ts +13 -14
  79. package/src/tests/lsp.test.ts +88 -0
  80. package/src/tests/runner.test.ts +325 -325
  81. package/src/types.ts +15 -15
  82. package/src/utils/ast.ts +50 -50
  83. package/src/utils/imports.ts +53 -53
  84. package/src/utils/index.ts +12 -3
  85. package/src/utils/source.ts +2 -2
  86. package/src/watcher.ts +19 -25
@@ -1,13 +1,13 @@
1
- import type { Rule, VisitorCallbacks } from "../../types"
2
- import { getSpan, isMemberCallTo } from "../../utils/ast"
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getSpan, isMemberCallTo } from '../../utils/ast'
3
3
 
4
4
  export const noMismatchRisk: Rule = {
5
5
  meta: {
6
- id: "pyreon/no-mismatch-risk",
7
- category: "ssr",
6
+ id: 'pyreon/no-mismatch-risk',
7
+ category: 'ssr',
8
8
  description:
9
- "Warn about non-deterministic calls (Date.now, Math.random, crypto.randomUUID) in JSX context that cause hydration mismatches.",
10
- severity: "warn",
9
+ 'Warn about non-deterministic calls (Date.now, Math.random, crypto.randomUUID) in JSX context that cause hydration mismatches.',
10
+ severity: 'warn',
11
11
  fixable: false,
12
12
  },
13
13
  create(context) {
@@ -16,22 +16,22 @@ export const noMismatchRisk: Rule = {
16
16
  JSXElement() {
17
17
  jsxDepth++
18
18
  },
19
- "JSXElement:exit"() {
19
+ 'JSXElement:exit'() {
20
20
  jsxDepth--
21
21
  },
22
22
  JSXFragment() {
23
23
  jsxDepth++
24
24
  },
25
- "JSXFragment:exit"() {
25
+ 'JSXFragment:exit'() {
26
26
  jsxDepth--
27
27
  },
28
28
  CallExpression(node: any) {
29
29
  if (jsxDepth === 0) return
30
30
 
31
31
  if (
32
- isMemberCallTo(node, "Date", "now") ||
33
- isMemberCallTo(node, "Math", "random") ||
34
- isMemberCallTo(node, "crypto", "randomUUID")
32
+ isMemberCallTo(node, 'Date', 'now') ||
33
+ isMemberCallTo(node, 'Math', 'random') ||
34
+ isMemberCallTo(node, 'crypto', 'randomUUID')
35
35
  ) {
36
36
  const callee = node.callee
37
37
  const name = `${callee.object.name}.${callee.property.name}`
@@ -1,13 +1,13 @@
1
- import type { Rule, VisitorCallbacks } from "../../types"
2
- import { getSpan, isCallTo } from "../../utils/ast"
3
- import { BROWSER_GLOBALS } from "../../utils/imports"
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getSpan, isCallTo } from '../../utils/ast'
3
+ import { BROWSER_GLOBALS } from '../../utils/imports'
4
4
 
5
5
  export const noWindowInSsr: Rule = {
6
6
  meta: {
7
- id: "pyreon/no-window-in-ssr",
8
- category: "ssr",
9
- description: "Disallow browser globals outside onMount/effect/typeof guards — they break SSR.",
10
- severity: "error",
7
+ id: 'pyreon/no-window-in-ssr',
8
+ category: 'ssr',
9
+ description: 'Disallow browser globals outside onMount/effect/typeof guards — they break SSR.',
10
+ severity: 'error',
11
11
  fixable: false,
12
12
  },
13
13
  create(context) {
@@ -16,12 +16,12 @@ export const noWindowInSsr: Rule = {
16
16
 
17
17
  const callbacks: VisitorCallbacks = {
18
18
  CallExpression(node: any) {
19
- if (isCallTo(node, "onMount") || isCallTo(node, "effect")) {
19
+ if (isCallTo(node, 'onMount') || isCallTo(node, 'effect')) {
20
20
  safeDepth++
21
21
  }
22
22
  },
23
- "CallExpression:exit"(node: any) {
24
- if (isCallTo(node, "onMount") || isCallTo(node, "effect")) {
23
+ 'CallExpression:exit'(node: any) {
24
+ if (isCallTo(node, 'onMount') || isCallTo(node, 'effect')) {
25
25
  safeDepth--
26
26
  }
27
27
  },
@@ -29,19 +29,19 @@ export const noWindowInSsr: Rule = {
29
29
  // typeof window !== "undefined"
30
30
  const test = node.test
31
31
  if (
32
- test?.type === "BinaryExpression" &&
33
- test.left?.type === "UnaryExpression" &&
34
- test.left.operator === "typeof"
32
+ test?.type === 'BinaryExpression' &&
33
+ test.left?.type === 'UnaryExpression' &&
34
+ test.left.operator === 'typeof'
35
35
  ) {
36
36
  typeofGuardDepth++
37
37
  }
38
38
  },
39
- "IfStatement:exit"(node: any) {
39
+ 'IfStatement:exit'(node: any) {
40
40
  const test = node.test
41
41
  if (
42
- test?.type === "BinaryExpression" &&
43
- test.left?.type === "UnaryExpression" &&
44
- test.left.operator === "typeof"
42
+ test?.type === 'BinaryExpression' &&
43
+ test.left?.type === 'UnaryExpression' &&
44
+ test.left.operator === 'typeof'
45
45
  ) {
46
46
  typeofGuardDepth--
47
47
  }
@@ -51,18 +51,18 @@ export const noWindowInSsr: Rule = {
51
51
  if (!BROWSER_GLOBALS.has(node.name)) return
52
52
 
53
53
  // Skip typeof expressions: typeof window
54
- if (parent?.type === "UnaryExpression" && parent.operator === "typeof") return
54
+ if (parent?.type === 'UnaryExpression' && parent.operator === 'typeof') return
55
55
 
56
56
  // Skip import specifiers
57
57
  if (
58
- parent?.type === "ImportSpecifier" ||
59
- parent?.type === "ImportDefaultSpecifier" ||
60
- parent?.type === "ImportNamespaceSpecifier"
58
+ parent?.type === 'ImportSpecifier' ||
59
+ parent?.type === 'ImportDefaultSpecifier' ||
60
+ parent?.type === 'ImportNamespaceSpecifier'
61
61
  )
62
62
  return
63
63
 
64
64
  // Skip property access on member expressions (only flag when used as the object)
65
- if (parent?.type === "MemberExpression" && parent.property === node && !parent.computed)
65
+ if (parent?.type === 'MemberExpression' && parent.property === node && !parent.computed)
66
66
  return
67
67
 
68
68
  context.report({
@@ -1,22 +1,22 @@
1
- import type { Rule, VisitorCallbacks } from "../../types"
2
- import { getSpan, isCallTo } from "../../utils/ast"
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getSpan, isCallTo } from '../../utils/ast'
3
3
 
4
4
  export const preferRequestContext: Rule = {
5
5
  meta: {
6
- id: "pyreon/prefer-request-context",
7
- category: "ssr",
6
+ id: 'pyreon/prefer-request-context',
7
+ category: 'ssr',
8
8
  description:
9
- "Warn about module-level signal()/createStore() in server files — use request context instead.",
10
- severity: "warn",
9
+ 'Warn about module-level signal()/createStore() in server files — use request context instead.',
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("server") ||
17
- filePath.includes(".server.") ||
18
- filePath.endsWith("server.ts") ||
19
- filePath.endsWith("server.tsx")
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
 
@@ -25,24 +25,24 @@ export const preferRequestContext: Rule = {
25
25
  FunctionDeclaration() {
26
26
  functionDepth++
27
27
  },
28
- "FunctionDeclaration:exit"() {
28
+ 'FunctionDeclaration:exit'() {
29
29
  functionDepth--
30
30
  },
31
31
  FunctionExpression() {
32
32
  functionDepth++
33
33
  },
34
- "FunctionExpression:exit"() {
34
+ 'FunctionExpression:exit'() {
35
35
  functionDepth--
36
36
  },
37
37
  ArrowFunctionExpression() {
38
38
  functionDepth++
39
39
  },
40
- "ArrowFunctionExpression:exit"() {
40
+ 'ArrowFunctionExpression:exit'() {
41
41
  functionDepth--
42
42
  },
43
43
  CallExpression(node: any) {
44
44
  if (functionDepth > 0) return // only flag module-level calls
45
- if (isCallTo(node, "signal") || isCallTo(node, "createStore")) {
45
+ if (isCallTo(node, 'signal') || isCallTo(node, 'createStore')) {
46
46
  const name = node.callee.name
47
47
  context.report({
48
48
  message: `Module-level \`${name}()\` in a server file — this state is shared across all requests. Use \`runWithRequestContext()\` for per-request isolation.`,
@@ -1,12 +1,12 @@
1
- import type { Rule, VisitorCallbacks } from "../../types"
2
- import { getSpan, isCallTo } from "../../utils/ast"
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getSpan, isCallTo } from '../../utils/ast'
3
3
 
4
4
  export const noDuplicateStoreId: Rule = {
5
5
  meta: {
6
- id: "pyreon/no-duplicate-store-id",
7
- category: "store",
8
- description: "Disallow duplicate defineStore() IDs in the same file.",
9
- severity: "error",
6
+ id: 'pyreon/no-duplicate-store-id',
7
+ category: 'store',
8
+ description: 'Disallow duplicate defineStore() IDs in the same file.',
9
+ severity: 'error',
10
10
  fixable: false,
11
11
  },
12
12
  create(context) {
@@ -14,7 +14,7 @@ export const noDuplicateStoreId: Rule = {
14
14
 
15
15
  const callbacks: VisitorCallbacks = {
16
16
  CallExpression(node: any) {
17
- if (!isCallTo(node, "defineStore")) return
17
+ if (!isCallTo(node, 'defineStore')) return
18
18
  const args = node.arguments
19
19
  if (!args || args.length === 0) return
20
20
 
@@ -22,11 +22,11 @@ export const noDuplicateStoreId: Rule = {
22
22
  if (!firstArg) return
23
23
 
24
24
  let id: string | null = null
25
- if (firstArg.type === "Literal" || firstArg.type === "StringLiteral") {
25
+ if (firstArg.type === 'Literal' || firstArg.type === 'StringLiteral') {
26
26
  id = firstArg.value as string
27
27
  }
28
28
 
29
- if (typeof id !== "string") return
29
+ if (typeof id !== 'string') return
30
30
 
31
31
  if (storeIds.has(id)) {
32
32
  context.report({
@@ -1,30 +1,30 @@
1
- import type { Rule, VisitorCallbacks } from "../../types"
2
- import { getSpan } from "../../utils/ast"
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getSpan } from '../../utils/ast'
3
3
 
4
4
  export const noMutateStoreState: Rule = {
5
5
  meta: {
6
- id: "pyreon/no-mutate-store-state",
7
- category: "store",
8
- description: "Warn when directly calling .set() on store signals — use store actions instead.",
9
- severity: "warn",
6
+ id: 'pyreon/no-mutate-store-state',
7
+ category: 'store',
8
+ description: 'Warn when directly calling .set() on store signals — use store actions instead.',
9
+ severity: 'warn',
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 !== "MemberExpression") return
17
- if (callee.property?.type !== "Identifier" || callee.property.name !== "set") return
16
+ if (!callee || callee.type !== 'MemberExpression') return
17
+ if (callee.property?.type !== 'Identifier' || callee.property.name !== 'set') return
18
18
 
19
19
  // Check for store.signal.set() pattern — member.member.set()
20
20
  const obj = callee.object
21
- if (!obj || obj.type !== "MemberExpression") return
21
+ if (!obj || obj.type !== 'MemberExpression') return
22
22
  const outerObj = obj.object
23
- if (!outerObj || outerObj.type !== "Identifier") return
23
+ if (!outerObj || outerObj.type !== 'Identifier') return
24
24
 
25
25
  const name: string = outerObj.name
26
26
  // Heuristic: if the outer object name contains "store" (case-insensitive)
27
- if (name.toLowerCase().includes("store")) {
27
+ if (name.toLowerCase().includes('store')) {
28
28
  context.report({
29
29
  message: `Direct \`.set()\` on store state \`${name}\` — use store actions to mutate state for better traceability.`,
30
30
  span: getSpan(node),
@@ -1,22 +1,22 @@
1
- import type { Rule, VisitorCallbacks } from "../../types"
2
- import { getSpan } from "../../utils/ast"
3
- import { extractImportInfo } from "../../utils/imports"
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: "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",
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("server") ||
17
- filePath.includes(".server.") ||
18
- filePath.endsWith("server.ts") ||
19
- filePath.endsWith("server.tsx")
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 === "setStoreRegistryProvider" || s.imported === "runWithRequestContext",
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 !== "Identifier") return
41
+ if (!callee || callee.type !== 'Identifier') return
42
42
  const name: string = callee.name
43
- if (name.endsWith("Store") && name.startsWith("use")) {
43
+ if (name.endsWith('Store') && name.startsWith('use')) {
44
44
  storeHookCalls.push({ name, span: getSpan(node) })
45
45
  }
46
46
  },
47
- "Program:exit"() {
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 "../../types"
2
- import { getSpan, isCallTo } from "../../utils/ast"
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: "pyreon/no-dynamic-styled",
7
- category: "styling",
6
+ id: 'pyreon/no-dynamic-styled',
7
+ category: 'styling',
8
8
  description:
9
- "Warn when styled() is called inside a function — it creates new CSS on every render.",
10
- severity: "warn",
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
- "FunctionDeclaration:exit"() {
19
+ 'FunctionDeclaration:exit'() {
20
20
  functionDepth--
21
21
  },
22
22
  FunctionExpression() {
23
23
  functionDepth++
24
24
  },
25
- "FunctionExpression:exit"() {
25
+ 'FunctionExpression:exit'() {
26
26
  functionDepth--
27
27
  },
28
28
  ArrowFunctionExpression() {
29
29
  functionDepth++
30
30
  },
31
- "ArrowFunctionExpression:exit"() {
31
+ 'ArrowFunctionExpression:exit'() {
32
32
  functionDepth--
33
33
  },
34
34
  CallExpression(node: any) {
35
35
  if (functionDepth === 0) return
36
- if (isCallTo(node, "styled")) {
36
+ if (isCallTo(node, 'styled')) {
37
37
  context.report({
38
38
  message:
39
- "`styled()` inside a function — this creates new CSS rules on every render. Move `styled()` to module scope.",
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 === "CallExpression" && isCallTo(tag, "styled")) {
49
+ if (tag.type === 'CallExpression' && isCallTo(tag, 'styled')) {
50
50
  context.report({
51
51
  message:
52
- "`styled()` tagged template inside a function — this creates new CSS rules on every render. Move to module scope.",
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 "../../types"
2
- import { getSpan } from "../../utils/ast"
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: "pyreon/no-inline-style-object",
7
- category: "styling",
8
- description: "Warn against inline style objects in JSX — prefer styled() or css``.",
9
- severity: "warn",
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 !== "JSXIdentifier" || node.name.name !== "style") return
15
+ if (node.name?.type !== 'JSXIdentifier' || node.name.name !== 'style') return
16
16
  const value = node.value
17
- if (!value || value.type !== "JSXExpressionContainer") return
17
+ if (!value || value.type !== 'JSXExpressionContainer') return
18
18
  const expr = value.expression
19
- if (expr?.type === "ObjectExpression") {
19
+ if (expr?.type === 'ObjectExpression') {
20
20
  context.report({
21
21
  message:
22
- "Inline style object in JSX — consider using `styled()` or `css\\`...\\`` for better performance and caching.",
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 "../../types"
2
- import { getSpan, isCallTo } from "../../utils/ast"
3
- import { extractImportInfo } from "../../utils/imports"
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: "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",
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 === "PyreonUI" || s.imported === "ThemeProvider")
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, "useTheme")) {
28
+ if (isCallTo(node, 'useTheme')) {
29
29
  themeCalls.push({ span: getSpan(node) })
30
30
  }
31
31
  },
32
- "Program:exit"() {
32
+ 'Program:exit'() {
33
33
  if (hasProviderImport) return
34
34
  for (const call of themeCalls) {
35
35
  context.report({
36
36
  message:
37
- "`useTheme()` without a `PyreonUI` or `ThemeProvider` import — the theme context may not be available.",
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 "../../types"
2
- import { getSpan } from "../../utils/ast"
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: "pyreon/prefer-cx",
7
- category: "styling",
6
+ id: 'pyreon/prefer-cx',
7
+ category: 'styling',
8
8
  description:
9
- "Suggest cx() for class composition instead of string concatenation or template literals.",
10
- severity: "info",
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 !== "JSXIdentifier" || node.name.name !== "class") return
16
+ if (node.name?.type !== 'JSXIdentifier' || node.name.name !== 'class') return
17
17
  const value = node.value
18
- if (!value || value.type !== "JSXExpressionContainer") return
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 === "BinaryExpression" && expr.operator === "+") {
23
+ if (expr.type === 'BinaryExpression' && expr.operator === '+') {
24
24
  context.report({
25
25
  message:
26
- "String concatenation in `class` attribute — use `cx()` for cleaner class composition.",
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 === "TemplateLiteral" && expr.expressions?.length > 0) {
33
+ if (expr.type === 'TemplateLiteral' && expr.expressions?.length > 0) {
34
34
  context.report({
35
35
  message:
36
- "Template literal in `class` attribute — use `cx()` for cleaner class composition.",
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 "oxc-parser"
2
- import type { AstCache } from "./cache"
1
+ import { parseSync, Visitor } from 'oxc-parser'
2
+ import type { AstCache } from './cache'
3
3
  import type {
4
4
  Diagnostic,
5
5
  LintConfig,
@@ -8,22 +8,21 @@ import type {
8
8
  RuleContext,
9
9
  Severity,
10
10
  VisitorCallbacks,
11
- } from "./types"
12
- import { LineIndex } from "./utils/source"
13
-
14
- const JS_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs"])
11
+ } from './types'
12
+ import { JS_EXTENSIONS } from './utils/index'
13
+ import { LineIndex } from './utils/source'
15
14
 
16
15
  function getExtension(filePath: string): string {
17
- const lastDot = filePath.lastIndexOf(".")
18
- return lastDot === -1 ? "" : filePath.slice(lastDot)
16
+ const lastDot = filePath.lastIndexOf('.')
17
+ return lastDot === -1 ? '' : filePath.slice(lastDot)
19
18
  }
20
19
 
21
- type OxcLang = "jsx" | "tsx" | "ts" | "js" | "dts"
20
+ type OxcLang = 'jsx' | 'tsx' | 'ts' | 'js' | 'dts'
22
21
 
23
22
  function getLang(ext: string): OxcLang {
24
- if (ext === ".tsx" || ext === ".jsx") return "tsx"
25
- if (ext === ".ts" || ext === ".mts") return "ts"
26
- return "js"
23
+ if (ext === '.tsx' || ext === '.jsx') return 'tsx'
24
+ if (ext === '.ts' || ext === '.mts') return 'ts'
25
+ return 'js'
27
26
  }
28
27
 
29
28
  function createRuleContext(
@@ -114,7 +113,7 @@ export function lintFile(
114
113
  lineIndex = new LineIndex(sourceText)
115
114
  try {
116
115
  const result = parseSync(filePath, sourceText, {
117
- sourceType: "module",
116
+ sourceType: 'module',
118
117
  lang: getLang(ext),
119
118
  })
120
119
  program = result.program
@@ -130,7 +129,7 @@ export function lintFile(
130
129
  const allCallbacks: VisitorCallbacks[] = []
131
130
  for (const rule of rules) {
132
131
  const severity = config.rules[rule.meta.id]
133
- if (severity === undefined || severity === "off") continue
132
+ if (severity === undefined || severity === 'off') continue
134
133
  const ctx = createRuleContext(rule, severity, diagnostics, lineIndex, sourceText, filePath)
135
134
  allCallbacks.push(rule.create(ctx))
136
135
  }