@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
package/src/lint.ts CHANGED
@@ -1,22 +1,16 @@
1
- import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"
2
- import { join, resolve } from "node:path"
3
- import { AstCache } from "./cache"
4
- import { createIgnoreFilter } from "./config/ignore"
5
- import { loadConfig, loadConfigFromPath } from "./config/loader"
6
- import { getPreset } from "./config/presets"
7
- import { allRules } from "./rules/index"
8
- import { applyFixes, lintFile } from "./runner"
9
- import type { LintConfig, LintFileResult, LintOptions, LintResult, RuleMeta } from "./types"
10
-
11
- const JS_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs"])
1
+ import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
2
+ import { join, resolve } from 'node:path'
3
+ import { AstCache } from './cache'
4
+ import { createIgnoreFilter } from './config/ignore'
5
+ import { loadConfig, loadConfigFromPath } from './config/loader'
6
+ import { getPreset } from './config/presets'
7
+ import { allRules } from './rules/index'
8
+ import { applyFixes, lintFile } from './runner'
9
+ import type { LintConfig, LintFileResult, LintOptions, LintResult, RuleMeta } from './types'
10
+ import { hasJsExtension } from './utils/index'
12
11
 
13
12
  function isHiddenOrVendor(entry: string): boolean {
14
- return entry.startsWith(".") || entry === "node_modules" || entry === "lib" || entry === "dist"
15
- }
16
-
17
- function hasJsExtension(filePath: string): boolean {
18
- const ext = filePath.slice(filePath.lastIndexOf("."))
19
- return JS_EXTENSIONS.has(ext)
13
+ return entry.startsWith('.') || entry === 'node_modules' || entry === 'lib' || entry === 'dist'
20
14
  }
21
15
 
22
16
  function matchesPatterns(
@@ -96,10 +90,10 @@ function buildConfig(options: LintOptions): {
96
90
  exclude: string[] | undefined
97
91
  isIgnored: (filePath: string) => boolean
98
92
  } {
99
- const cwd = resolve(".")
93
+ const cwd = resolve('.')
100
94
  const fileConfig = options.config ? loadConfigFromPath(options.config) : loadConfig(cwd)
101
95
 
102
- const presetName = options.preset ?? fileConfig?.preset ?? "recommended"
96
+ const presetName = options.preset ?? fileConfig?.preset ?? 'recommended'
103
97
  const config = getPreset(presetName)
104
98
 
105
99
  // Merge config file rule overrides
@@ -152,16 +146,16 @@ function applyFixesToFile(fileResult: LintFileResult, source: string): void {
152
146
  const fixable = fileResult.diagnostics.filter((d) => d.fix)
153
147
  if (fixable.length === 0) return
154
148
  const fixed = applyFixes(source, fileResult.diagnostics)
155
- writeFileSync(fileResult.filePath, fixed, "utf-8")
149
+ writeFileSync(fileResult.filePath, fixed, 'utf-8')
156
150
  fileResult.fixedSource = fixed
157
151
  fileResult.diagnostics = fileResult.diagnostics.filter((d) => !d.fix)
158
152
  }
159
153
 
160
154
  function countDiagnostics(fileResult: LintFileResult, results: LintResult): void {
161
155
  for (const d of fileResult.diagnostics) {
162
- if (d.severity === "error") results.totalErrors++
163
- else if (d.severity === "warn") results.totalWarnings++
164
- else if (d.severity === "info") results.totalInfos++
156
+ if (d.severity === 'error') results.totalErrors++
157
+ else if (d.severity === 'warn') results.totalWarnings++
158
+ else if (d.severity === 'info') results.totalInfos++
165
159
  }
166
160
  }
167
161
 
@@ -191,7 +185,7 @@ export function lint(options: LintOptions): LintResult {
191
185
  for (const filePath of files) {
192
186
  let source: string
193
187
  try {
194
- source = readFileSync(filePath, "utf-8")
188
+ source = readFileSync(filePath, 'utf-8')
195
189
  } catch {
196
190
  continue
197
191
  }
@@ -200,7 +194,7 @@ export function lint(options: LintOptions): LintResult {
200
194
  applyFixesToFile(fileResult, source)
201
195
  }
202
196
  if (options.quiet) {
203
- fileResult.diagnostics = fileResult.diagnostics.filter((d) => d.severity === "error")
197
+ fileResult.diagnostics = fileResult.diagnostics.filter((d) => d.severity === 'error')
204
198
  }
205
199
  countDiagnostics(fileResult, results)
206
200
  results.files.push(fileResult)
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Minimal LSP server for @pyreon/lint.
3
+ *
4
+ * Provides real-time Pyreon-specific diagnostics in editors that support
5
+ * the Language Server Protocol (VS Code, Neovim, etc.).
6
+ *
7
+ * Usage: pyreon-lint --lsp
8
+ *
9
+ * The server communicates via JSON-RPC over stdin/stdout following the
10
+ * LSP specification (https://microsoft.github.io/language-server-protocol/).
11
+ *
12
+ * Supported capabilities:
13
+ * - textDocument/didOpen — lint on open
14
+ * - textDocument/didSave — lint on save
15
+ * - textDocument/didChange — lint on change (debounced)
16
+ *
17
+ * @module
18
+ */
19
+
20
+ import { AstCache } from '../cache'
21
+ import { getPreset } from '../config/presets'
22
+ import { allRules } from '../rules/index'
23
+ import { lintFile } from '../runner'
24
+ import type { Diagnostic, LintConfig } from '../types'
25
+
26
+ const cache = new AstCache()
27
+ const config: LintConfig = getPreset('recommended')
28
+
29
+ // ─── JSON-RPC message types ────────────────────────────────────────────────
30
+
31
+ interface JsonRpcMessage {
32
+ jsonrpc: '2.0'
33
+ id?: number | string | undefined
34
+ method?: string | undefined
35
+ params?: any
36
+ result?: any
37
+ }
38
+
39
+ // ─── LSP Diagnostic conversion ─────────────────────────────────────────────
40
+
41
+ interface LspDiagnostic {
42
+ range: { start: { line: number; character: number }; end: { line: number; character: number } }
43
+ severity: number
44
+ source: string
45
+ message: string
46
+ code: string
47
+ }
48
+
49
+ function toLspDiagnostics(diagnostics: Diagnostic[]): LspDiagnostic[] {
50
+ return diagnostics.map((d) => ({
51
+ range: {
52
+ start: { line: d.loc.line - 1, character: d.loc.column - 1 },
53
+ end: { line: d.loc.line - 1, character: d.loc.column - 1 + (d.span.end - d.span.start) },
54
+ },
55
+ severity: d.severity === 'error' ? 1 : d.severity === 'warn' ? 2 : 3,
56
+ source: 'pyreon-lint',
57
+ message: d.message,
58
+ code: d.ruleId,
59
+ }))
60
+ }
61
+
62
+ // ─── Lint a document ───────────────────────────────────────────────────────
63
+
64
+ function lintDocument(uri: string, text: string): LspDiagnostic[] {
65
+ try {
66
+ const filePath = uri.replace('file://', '')
67
+ const result = lintFile(filePath, text, allRules, config, cache)
68
+ return toLspDiagnostics(result.diagnostics)
69
+ } catch {
70
+ // Parse errors, unsupported file types — return empty diagnostics
71
+ return []
72
+ }
73
+ }
74
+
75
+ // ─── Debounce ──────────────────────────────────────────────────────────────
76
+
77
+ const DEBOUNCE_MS = 150
78
+ const debounceTimers = new Map<string, ReturnType<typeof setTimeout>>()
79
+
80
+ function debounceLint(uri: string, text: string): void {
81
+ const existing = debounceTimers.get(uri)
82
+ if (existing) clearTimeout(existing)
83
+ debounceTimers.set(
84
+ uri,
85
+ setTimeout(() => {
86
+ debounceTimers.delete(uri)
87
+ const diagnostics = lintDocument(uri, text)
88
+ sendNotification('textDocument/publishDiagnostics', { uri, diagnostics })
89
+ }, DEBOUNCE_MS),
90
+ )
91
+ }
92
+
93
+ // ─── Message handling ──────────────────────────────────────────────────────
94
+
95
+ const openDocuments = new Map<string, string>()
96
+
97
+ function handleMessage(msg: JsonRpcMessage): JsonRpcMessage | null {
98
+ if (msg.method === 'initialize') {
99
+ return {
100
+ jsonrpc: '2.0',
101
+ id: msg.id,
102
+ result: {
103
+ capabilities: {
104
+ textDocumentSync: 1, // Full sync
105
+ diagnosticProvider: { interFileDependencies: false, workspaceDiagnostics: false },
106
+ },
107
+ serverInfo: { name: 'pyreon-lint', version: '0.11.5' },
108
+ },
109
+ }
110
+ }
111
+
112
+ if (msg.method === 'initialized') {
113
+ return null // no response needed
114
+ }
115
+
116
+ if (msg.method === 'textDocument/didOpen') {
117
+ const { uri, text } = msg.params.textDocument
118
+ openDocuments.set(uri, text)
119
+ const diagnostics = lintDocument(uri, text)
120
+ sendNotification('textDocument/publishDiagnostics', { uri, diagnostics })
121
+ return null
122
+ }
123
+
124
+ if (msg.method === 'textDocument/didChange') {
125
+ const uri = msg.params.textDocument.uri
126
+ const text = msg.params.contentChanges[0]?.text
127
+ if (text != null) {
128
+ openDocuments.set(uri, text)
129
+ // Debounce: wait 150ms after last keystroke before linting
130
+ debounceLint(uri, text)
131
+ }
132
+ return null
133
+ }
134
+
135
+ if (msg.method === 'textDocument/didSave') {
136
+ const uri = msg.params.textDocument.uri
137
+ const text = openDocuments.get(uri)
138
+ if (text) {
139
+ const diagnostics = lintDocument(uri, text)
140
+ sendNotification('textDocument/publishDiagnostics', { uri, diagnostics })
141
+ }
142
+ return null
143
+ }
144
+
145
+ if (msg.method === 'textDocument/didClose') {
146
+ const uri = msg.params.textDocument.uri
147
+ openDocuments.delete(uri)
148
+ sendNotification('textDocument/publishDiagnostics', { uri, diagnostics: [] })
149
+ return null
150
+ }
151
+
152
+ if (msg.method === 'shutdown') {
153
+ return { jsonrpc: '2.0', id: msg.id, result: null }
154
+ }
155
+
156
+ if (msg.method === 'exit') {
157
+ process.exit(0)
158
+ }
159
+
160
+ // Unknown method
161
+ if (msg.id != null) {
162
+ return {
163
+ jsonrpc: '2.0',
164
+ id: msg.id,
165
+ result: null,
166
+ }
167
+ }
168
+
169
+ return null
170
+ }
171
+
172
+ // ─── JSON-RPC transport (stdin/stdout) ─────────────────────────────────────
173
+
174
+ function sendMessage(msg: JsonRpcMessage) {
175
+ const body = JSON.stringify(msg)
176
+ const header = `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n`
177
+ process.stdout.write(header + body)
178
+ }
179
+
180
+ function sendNotification(method: string, params: any) {
181
+ sendMessage({ jsonrpc: '2.0', method, params })
182
+ }
183
+
184
+ /**
185
+ * Start the LSP server. Reads JSON-RPC messages from stdin,
186
+ * processes them, and writes responses to stdout.
187
+ */
188
+ export function startLspServer(): void {
189
+ let buffer = ''
190
+
191
+ process.stdin.setEncoding('utf-8')
192
+ process.stdin.on('data', (chunk: string) => {
193
+ buffer += chunk
194
+
195
+ // Parse Content-Length header + body
196
+ while (true) {
197
+ const headerEnd = buffer.indexOf('\r\n\r\n')
198
+ if (headerEnd === -1) break
199
+
200
+ const header = buffer.slice(0, headerEnd)
201
+ const match = header.match(/Content-Length:\s*(\d+)/i)
202
+ if (!match) {
203
+ buffer = buffer.slice(headerEnd + 4)
204
+ continue
205
+ }
206
+
207
+ const contentLength = Number.parseInt(match[1]!, 10)
208
+ const bodyStart = headerEnd + 4
209
+ if (buffer.length < bodyStart + contentLength) break
210
+
211
+ const body = buffer.slice(bodyStart, bodyStart + contentLength)
212
+ buffer = buffer.slice(bodyStart + contentLength)
213
+
214
+ try {
215
+ const msg = JSON.parse(body) as JsonRpcMessage
216
+ const response = handleMessage(msg)
217
+ if (response) sendMessage(response)
218
+ } catch {
219
+ // malformed JSON — ignore
220
+ }
221
+ }
222
+ })
223
+
224
+ process.stderr.write('[pyreon-lint] LSP server started\n')
225
+ }
package/src/reporter.ts CHANGED
@@ -1,25 +1,25 @@
1
- import type { LintResult, Severity } from "./types"
1
+ import type { LintResult, Severity } from './types'
2
2
 
3
3
  // ANSI colors
4
- const BOLD = "\x1b[1m"
5
- const RED = "\x1b[31m"
6
- const YELLOW = "\x1b[33m"
7
- const BLUE = "\x1b[34m"
8
- const DIM = "\x1b[2m"
9
- const RESET = "\x1b[0m"
4
+ const BOLD = '\x1b[1m'
5
+ const RED = '\x1b[31m'
6
+ const YELLOW = '\x1b[33m'
7
+ const BLUE = '\x1b[34m'
8
+ const DIM = '\x1b[2m'
9
+ const RESET = '\x1b[0m'
10
10
 
11
11
  const SEVERITY_SYMBOL: Record<Severity, string> = {
12
12
  error: `${RED}\u2716${RESET}`,
13
13
  warn: `${YELLOW}\u26A0${RESET}`,
14
14
  info: `${BLUE}\u2139${RESET}`,
15
- off: "",
15
+ off: '',
16
16
  }
17
17
 
18
18
  const SEVERITY_LABEL: Record<Severity, string> = {
19
19
  error: `${RED}error${RESET}`,
20
20
  warn: `${YELLOW}warning${RESET}`,
21
21
  info: `${BLUE}info${RESET}`,
22
- off: "",
22
+ off: '',
23
23
  }
24
24
 
25
25
  /**
@@ -31,7 +31,7 @@ export function formatText(result: LintResult): string {
31
31
  for (const file of result.files) {
32
32
  if (file.diagnostics.length === 0) continue
33
33
 
34
- lines.push("")
34
+ lines.push('')
35
35
  lines.push(`${BOLD}${file.filePath}${RESET}`)
36
36
 
37
37
  for (const d of file.diagnostics) {
@@ -44,20 +44,20 @@ export function formatText(result: LintResult): string {
44
44
 
45
45
  const total = result.totalErrors + result.totalWarnings + result.totalInfos
46
46
  if (total > 0) {
47
- lines.push("")
47
+ lines.push('')
48
48
  const parts: string[] = []
49
49
  if (result.totalErrors > 0)
50
- parts.push(`${RED}${result.totalErrors} error${result.totalErrors === 1 ? "" : "s"}${RESET}`)
50
+ parts.push(`${RED}${result.totalErrors} error${result.totalErrors === 1 ? '' : 's'}${RESET}`)
51
51
  if (result.totalWarnings > 0)
52
52
  parts.push(
53
- `${YELLOW}${result.totalWarnings} warning${result.totalWarnings === 1 ? "" : "s"}${RESET}`,
53
+ `${YELLOW}${result.totalWarnings} warning${result.totalWarnings === 1 ? '' : 's'}${RESET}`,
54
54
  )
55
55
  if (result.totalInfos > 0) parts.push(`${BLUE}${result.totalInfos} info${RESET}`)
56
- lines.push(`${SEVERITY_SYMBOL.error} ${parts.join(", ")}`)
57
- lines.push("")
56
+ lines.push(`${SEVERITY_SYMBOL.error} ${parts.join(', ')}`)
57
+ lines.push('')
58
58
  }
59
59
 
60
- return lines.join("\n")
60
+ return lines.join('\n')
61
61
  }
62
62
 
63
63
  /**
@@ -81,5 +81,5 @@ export function formatCompact(result: LintResult): string {
81
81
  }
82
82
  }
83
83
 
84
- return lines.join("\n")
84
+ return lines.join('\n')
85
85
  }
@@ -1,27 +1,27 @@
1
- import type { Rule, VisitorCallbacks } from "../../types"
2
- import { getSpan, hasJSXAttribute } from "../../utils/ast"
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getSpan, hasJSXAttribute } from '../../utils/ast'
3
3
 
4
4
  export const dialogA11y: Rule = {
5
5
  meta: {
6
- id: "pyreon/dialog-a11y",
7
- category: "accessibility",
8
- description: "Warn when <dialog> is missing aria-label or aria-labelledby.",
9
- severity: "warn",
6
+ id: 'pyreon/dialog-a11y',
7
+ category: 'accessibility',
8
+ description: 'Warn when <dialog> is missing aria-label or aria-labelledby.',
9
+ severity: 'warn',
10
10
  fixable: false,
11
11
  },
12
12
  create(context) {
13
13
  const callbacks: VisitorCallbacks = {
14
14
  JSXOpeningElement(node: any) {
15
15
  const name = node.name
16
- if (!name || name.type !== "JSXIdentifier" || name.name !== "dialog") return
16
+ if (!name || name.type !== 'JSXIdentifier' || name.name !== 'dialog') return
17
17
 
18
- const hasLabel = hasJSXAttribute(node, "aria-label")
19
- const hasLabelledBy = hasJSXAttribute(node, "aria-labelledby")
18
+ const hasLabel = hasJSXAttribute(node, 'aria-label')
19
+ const hasLabelledBy = hasJSXAttribute(node, 'aria-labelledby')
20
20
 
21
21
  if (!hasLabel && !hasLabelledBy) {
22
22
  context.report({
23
23
  message:
24
- "`<dialog>` missing `aria-label` or `aria-labelledby` — provide an accessible label for screen readers.",
24
+ '`<dialog>` missing `aria-label` or `aria-labelledby` — provide an accessible label for screen readers.',
25
25
  span: getSpan(node),
26
26
  })
27
27
  }
@@ -1,28 +1,28 @@
1
- import type { Rule, VisitorCallbacks } from "../../types"
2
- import { getSpan, hasJSXAttribute } from "../../utils/ast"
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getSpan, hasJSXAttribute } from '../../utils/ast'
3
3
 
4
4
  export const overlayA11y: Rule = {
5
5
  meta: {
6
- id: "pyreon/overlay-a11y",
7
- category: "accessibility",
8
- description: "Warn when <Overlay> is missing role, aria-label, or aria-labelledby.",
9
- severity: "warn",
6
+ id: 'pyreon/overlay-a11y',
7
+ category: 'accessibility',
8
+ description: 'Warn when <Overlay> is missing role, aria-label, or aria-labelledby.',
9
+ severity: 'warn',
10
10
  fixable: false,
11
11
  },
12
12
  create(context) {
13
13
  const callbacks: VisitorCallbacks = {
14
14
  JSXOpeningElement(node: any) {
15
15
  const name = node.name
16
- if (!name || name.type !== "JSXIdentifier" || name.name !== "Overlay") return
16
+ if (!name || name.type !== 'JSXIdentifier' || name.name !== 'Overlay') return
17
17
 
18
- const hasRole = hasJSXAttribute(node, "role")
19
- const hasLabel = hasJSXAttribute(node, "aria-label")
20
- const hasLabelledBy = hasJSXAttribute(node, "aria-labelledby")
18
+ const hasRole = hasJSXAttribute(node, 'role')
19
+ const hasLabel = hasJSXAttribute(node, 'aria-label')
20
+ const hasLabelledBy = hasJSXAttribute(node, 'aria-labelledby')
21
21
 
22
22
  if (!hasRole && !hasLabel && !hasLabelledBy) {
23
23
  context.report({
24
24
  message:
25
- "`<Overlay>` missing `role`, `aria-label`, or `aria-labelledby` — provide accessibility attributes for screen readers.",
25
+ '`<Overlay>` missing `role`, `aria-label`, or `aria-labelledby` — provide accessibility attributes for screen readers.',
26
26
  span: getSpan(node),
27
27
  })
28
28
  }
@@ -1,29 +1,29 @@
1
- import type { Rule, VisitorCallbacks } from "../../types"
2
- import { getSpan, hasJSXAttribute } from "../../utils/ast"
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getSpan, hasJSXAttribute } from '../../utils/ast'
3
3
 
4
4
  export const toastA11y: Rule = {
5
5
  meta: {
6
- id: "pyreon/toast-a11y",
7
- category: "accessibility",
8
- description: "Warn when toast-like components are missing role or aria-live attributes.",
9
- severity: "warn",
6
+ id: 'pyreon/toast-a11y',
7
+ category: 'accessibility',
8
+ description: 'Warn when toast-like components are missing role or aria-live attributes.',
9
+ severity: 'warn',
10
10
  fixable: false,
11
11
  },
12
12
  create(context) {
13
13
  const callbacks: VisitorCallbacks = {
14
14
  JSXOpeningElement(node: any) {
15
15
  const name = node.name
16
- if (!name || name.type !== "JSXIdentifier") return
16
+ if (!name || name.type !== 'JSXIdentifier') return
17
17
 
18
18
  const tagName: string = name.name
19
19
  // Skip non-PascalCase and the Toaster container itself
20
- if (tagName === "Toaster") return
20
+ if (tagName === 'Toaster') return
21
21
  const firstChar = tagName[0]
22
22
  if (!firstChar || firstChar !== firstChar.toUpperCase()) return
23
- if (!tagName.toLowerCase().includes("toast")) return
23
+ if (!tagName.toLowerCase().includes('toast')) return
24
24
 
25
- const hasRole = hasJSXAttribute(node, "role")
26
- const hasAriaLive = hasJSXAttribute(node, "aria-live")
25
+ const hasRole = hasJSXAttribute(node, 'role')
26
+ const hasAriaLive = hasJSXAttribute(node, 'aria-live')
27
27
 
28
28
  if (!hasRole && !hasAriaLive) {
29
29
  context.report({
@@ -1,23 +1,23 @@
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 devGuardWarnings: Rule = {
5
5
  meta: {
6
- id: "pyreon/dev-guard-warnings",
7
- category: "architecture",
8
- description: "Require console.warn/error calls to be wrapped in `if (__DEV__)` guards.",
9
- severity: "error",
6
+ id: 'pyreon/dev-guard-warnings',
7
+ category: 'architecture',
8
+ description: 'Require console.warn/error calls to be wrapped in `if (__DEV__)` guards.',
9
+ severity: 'error',
10
10
  fixable: false,
11
11
  },
12
12
  create(context) {
13
13
  const filePath = context.getFilePath()
14
14
  // Skip test and example files
15
15
  if (
16
- filePath.includes("/tests/") ||
17
- filePath.includes("/test/") ||
18
- filePath.includes("/examples/") ||
19
- filePath.includes(".test.") ||
20
- filePath.includes(".spec.")
16
+ filePath.includes('/tests/') ||
17
+ filePath.includes('/test/') ||
18
+ filePath.includes('/examples/') ||
19
+ filePath.includes('.test.') ||
20
+ filePath.includes('.spec.')
21
21
  ) {
22
22
  return {}
23
23
  }
@@ -25,12 +25,12 @@ export const devGuardWarnings: Rule = {
25
25
  let devGuardDepth = 0
26
26
  const callbacks: VisitorCallbacks = {
27
27
  IfStatement(node: any) {
28
- if (node.test?.type === "Identifier" && node.test.name === "__DEV__") {
28
+ if (node.test?.type === 'Identifier' && node.test.name === '__DEV__') {
29
29
  devGuardDepth++
30
30
  }
31
31
  },
32
- "IfStatement:exit"(node: any) {
33
- if (node.test?.type === "Identifier" && node.test.name === "__DEV__") {
32
+ 'IfStatement:exit'(node: any) {
33
+ if (node.test?.type === 'Identifier' && node.test.name === '__DEV__') {
34
34
  devGuardDepth--
35
35
  }
36
36
  },
@@ -39,11 +39,11 @@ export const devGuardWarnings: Rule = {
39
39
 
40
40
  const callee = node.callee
41
41
  if (
42
- callee?.type === "MemberExpression" &&
43
- callee.object?.type === "Identifier" &&
44
- callee.object.name === "console" &&
45
- callee.property?.type === "Identifier" &&
46
- (callee.property.name === "warn" || callee.property.name === "error")
42
+ callee?.type === 'MemberExpression' &&
43
+ callee.object?.type === 'Identifier' &&
44
+ callee.object.name === 'console' &&
45
+ callee.property?.type === 'Identifier' &&
46
+ (callee.property.name === 'warn' || callee.property.name === 'error')
47
47
  ) {
48
48
  context.report({
49
49
  message: `\`console.${callee.property.name}()\` without \`__DEV__\` guard — dev warnings must be tree-shakeable in production. Wrap in \`if (__DEV__) { ... }\`.`,
@@ -1,16 +1,16 @@
1
- import type { Rule, VisitorCallbacks } from "../../types"
2
- import { getSpan } from "../../utils/ast"
3
- import { isPyreonImport } from "../../utils/imports"
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getSpan } from '../../utils/ast'
3
+ import { isPyreonImport } from '../../utils/imports'
4
4
 
5
5
  const LAYER_ORDER: Record<string, number> = {
6
- "@pyreon/reactivity": 0,
7
- "@pyreon/core": 1,
8
- "@pyreon/compiler": 1,
9
- "@pyreon/runtime-dom": 2,
10
- "@pyreon/runtime-server": 2,
11
- "@pyreon/router": 3,
12
- "@pyreon/head": 4,
13
- "@pyreon/server": 5,
6
+ '@pyreon/reactivity': 0,
7
+ '@pyreon/core': 1,
8
+ '@pyreon/compiler': 1,
9
+ '@pyreon/runtime-dom': 2,
10
+ '@pyreon/runtime-server': 2,
11
+ '@pyreon/router': 3,
12
+ '@pyreon/head': 4,
13
+ '@pyreon/server': 5,
14
14
  }
15
15
 
16
16
  function getLayer(source: string): number | null {
@@ -19,7 +19,7 @@ function getLayer(source: string): number | null {
19
19
 
20
20
  function getFileLayer(filePath: string): number | null {
21
21
  for (const [pkg, layer] of Object.entries(LAYER_ORDER)) {
22
- const pkgName = pkg.replace("@pyreon/", "")
22
+ const pkgName = pkg.replace('@pyreon/', '')
23
23
  if (filePath.includes(`/packages/core/${pkgName}/`)) return layer
24
24
  }
25
25
  return null
@@ -27,10 +27,10 @@ function getFileLayer(filePath: string): number | null {
27
27
 
28
28
  export const noCircularImport: Rule = {
29
29
  meta: {
30
- id: "pyreon/no-circular-import",
31
- category: "architecture",
32
- description: "Enforce package layer order to prevent circular imports between core packages.",
33
- severity: "error",
30
+ id: 'pyreon/no-circular-import',
31
+ category: 'architecture',
32
+ description: 'Enforce package layer order to prevent circular imports between core packages.',
33
+ severity: 'error',
34
34
  fixable: false,
35
35
  },
36
36
  create(context) {