@xnoxs/flux-lang 3.1.1

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 (56) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/README.md +1089 -0
  3. package/bin/flux.js +1397 -0
  4. package/dist/flux.cjs.js +6664 -0
  5. package/dist/flux.esm.js +6674 -0
  6. package/dist/flux.min.js +263 -0
  7. package/index.d.ts +202 -0
  8. package/index.js +26 -0
  9. package/package.json +77 -0
  10. package/scripts/build.js +76 -0
  11. package/src/bundler.js +216 -0
  12. package/src/checker.js +322 -0
  13. package/src/codegen.js +785 -0
  14. package/src/css-preprocessor.js +399 -0
  15. package/src/formatter.js +140 -0
  16. package/src/jsx.js +480 -0
  17. package/src/lexer.js +518 -0
  18. package/src/linter.js +758 -0
  19. package/src/mangler.js +280 -0
  20. package/src/parser.js +1671 -0
  21. package/src/self/bundler.flux +167 -0
  22. package/src/self/bundler.js +187 -0
  23. package/src/self/checker.flux +249 -0
  24. package/src/self/checker.js +338 -0
  25. package/src/self/codegen.flux +555 -0
  26. package/src/self/codegen.js +784 -0
  27. package/src/self/css-preprocessor.flux +373 -0
  28. package/src/self/css-preprocessor.js +387 -0
  29. package/src/self/formatter.flux +93 -0
  30. package/src/self/formatter.js +114 -0
  31. package/src/self/jsx.flux +430 -0
  32. package/src/self/jsx.js +396 -0
  33. package/src/self/lexer.flux +529 -0
  34. package/src/self/lexer.js +709 -0
  35. package/src/self/lexer.stage2.js +700 -0
  36. package/src/self/linter.flux +515 -0
  37. package/src/self/linter.js +804 -0
  38. package/src/self/mangler.flux +253 -0
  39. package/src/self/mangler.js +348 -0
  40. package/src/self/parser.flux +1146 -0
  41. package/src/self/parser.js +1571 -0
  42. package/src/self/sourcemap.flux +66 -0
  43. package/src/self/sourcemap.js +72 -0
  44. package/src/self/stdlib.flux +356 -0
  45. package/src/self/stdlib.js +396 -0
  46. package/src/self/test-runner.flux +201 -0
  47. package/src/self/test-runner.js +132 -0
  48. package/src/self/transpiler.flux +123 -0
  49. package/src/self/transpiler.js +83 -0
  50. package/src/self/type-checker.flux +821 -0
  51. package/src/self/type-checker.js +1106 -0
  52. package/src/sourcemap.js +82 -0
  53. package/src/stdlib.js +436 -0
  54. package/src/test-runner.js +239 -0
  55. package/src/transpiler.js +172 -0
  56. package/src/type-checker.js +1206 -0
package/src/linter.js ADDED
@@ -0,0 +1,758 @@
1
+ 'use strict';
2
+
3
+ const { Lexer } = require('./lexer');
4
+ const { Parser } = require('./parser');
5
+
6
+ // ── Flux Linter ───────────────────────────────────────────────────────────────
7
+ // AST-level lint rules (beyond type-safety — these catch logic smells):
8
+ //
9
+ // RULE 1 — unused-var : val/var declared but never read
10
+ // RULE 2 — unreachable : statements after return/throw/break/continue
11
+ // RULE 3 — shadow-val : inner val/var shadows an outer binding
12
+ //
13
+ // Each issue has: { rule, severity ('error'|'warn'|'info'), message, hint, line, col }
14
+
15
+ class LintIssue {
16
+ constructor(rule, severity, message, hint, loc) {
17
+ this.rule = rule;
18
+ this.severity = severity;
19
+ this.message = message;
20
+ this.hint = hint || null;
21
+ this.line = loc ? loc.line : null;
22
+ this.col = loc ? loc.col : null;
23
+ }
24
+ }
25
+
26
+ // ── Scope tracking ────────────────────────────────────────────────────────────
27
+ // Each entry: { kind, loc, used: bool, shadowOf: entry|null }
28
+ class LintScope {
29
+ constructor(parent = null, kind = 'block') {
30
+ this.parent = parent;
31
+ this.kind = kind; // 'block' | 'fn' | 'class'
32
+ this.vars = new Map();
33
+ }
34
+
35
+ define(name, kind, loc) {
36
+ const shadowOf = this.lookupOwn(name) || this._lookupParent(name);
37
+ const entry = { name, kind, loc, used: false, shadowOf };
38
+ this.vars.set(name, entry);
39
+ return entry;
40
+ }
41
+
42
+ lookupOwn(name) {
43
+ return this.vars.get(name) || null;
44
+ }
45
+
46
+ _lookupParent(name) {
47
+ let s = this.parent;
48
+ while (s) {
49
+ if (s.vars.has(name)) return s.vars.get(name);
50
+ s = s.parent;
51
+ }
52
+ return null;
53
+ }
54
+
55
+ lookup(name) {
56
+ return this.lookupOwn(name) || this._lookupParent(name);
57
+ }
58
+
59
+ markUsed(name) {
60
+ const entry = this.lookup(name);
61
+ if (entry) entry.used = true;
62
+ }
63
+
64
+ // Collect all entries defined in THIS scope only
65
+ ownEntries() {
66
+ return [...this.vars.values()];
67
+ }
68
+ }
69
+
70
+ // ── Names to skip for "unused" (exports, builtins, convention prefixes) ───────
71
+ const BUILTIN_NAMES = new Set([
72
+ 'self', 'this', 'arguments', 'exports', 'module', 'require', '__dirname',
73
+ '__filename', 'process', 'console', 'Math', 'JSON', 'Object', 'Array',
74
+ 'String', 'Number', 'Boolean', 'Promise', 'Error', 'Symbol', 'Map', 'Set',
75
+ 'WeakMap', 'WeakSet', 'Proxy', 'Reflect', 'undefined', 'null', 'true', 'false',
76
+ 'NaN', 'Infinity', 'parseInt', 'parseFloat', 'isNaN', 'isFinite', 'setTimeout',
77
+ 'clearTimeout', 'setInterval', 'clearInterval', 'fetch', 'globalThis',
78
+ // Flux stdlib names
79
+ 'print', 'truncate', 'range', 'sum', 'first', 'last', 'zip', 'flatten',
80
+ 'groupBy', 'unique', 'sortBy', 'chunk',
81
+ ]);
82
+
83
+ function isIgnoredName(name) {
84
+ if (BUILTIN_NAMES.has(name)) return true;
85
+ if (name.startsWith('_')) return true; // underscore prefix = intentionally unused
86
+ return false;
87
+ }
88
+
89
+ // ── The Linter ────────────────────────────────────────────────────────────────
90
+ class Linter {
91
+ constructor() {
92
+ this.issues = [];
93
+ }
94
+
95
+ lint(ast) {
96
+ this.issues = [];
97
+ const scope = new LintScope(null, 'module');
98
+ this._walkStmts(ast.body, scope, { inFn: false });
99
+ return this.issues;
100
+ }
101
+
102
+ _issue(rule, severity, message, hint, loc) {
103
+ this.issues.push(new LintIssue(rule, severity, message, hint, loc));
104
+ }
105
+
106
+ // ── Statement list walker ────────────────────────────────────────────────
107
+ _walkStmts(stmts, scope, ctx) {
108
+ if (!stmts || stmts.length === 0) return;
109
+
110
+ for (let i = 0; i < stmts.length; i++) {
111
+ const node = stmts[i];
112
+
113
+ // RULE 2: unreachable code after terminating statement
114
+ if (i > 0 && this._isTerminator(stmts[i - 1])) {
115
+ // Skip blank/comment nodes (they have no loc usually)
116
+ if (node && node.loc) {
117
+ this._issue(
118
+ 'unreachable',
119
+ 'error',
120
+ `Unreachable code after '${this._terminatorName(stmts[i - 1])}'`,
121
+ `Remove or move the unreachable statement`,
122
+ node.loc
123
+ );
124
+ // Report only the first unreachable per block to avoid cascade
125
+ break;
126
+ }
127
+ }
128
+
129
+ this._walkStmt(node, scope, ctx);
130
+ }
131
+
132
+ // After walking the block, check for unused vars declared in this scope
133
+ this._checkUnused(scope);
134
+ }
135
+
136
+ _isTerminator(node) {
137
+ if (!node) return false;
138
+ if (node.type === 'ReturnStmt' || node.type === 'ThrowStmt') return true;
139
+ if (node.type === 'BreakStmt' || node.type === 'ContinueStmt') return true;
140
+ // Inline fn body ending in return
141
+ return false;
142
+ }
143
+
144
+ _terminatorName(node) {
145
+ const map = {
146
+ ReturnStmt: 'return',
147
+ ThrowStmt: 'throw',
148
+ BreakStmt: 'break',
149
+ ContinueStmt: 'continue',
150
+ };
151
+ return map[node.type] || 'terminator';
152
+ }
153
+
154
+ // ── Unused variable check ────────────────────────────────────────────────
155
+ _checkUnused(scope) {
156
+ for (const entry of scope.ownEntries()) {
157
+ if (entry.used) continue;
158
+ if (isIgnoredName(entry.name)) continue;
159
+ // Exported names are considered used by the outside world
160
+ if (entry._exported) continue;
161
+
162
+ this._issue(
163
+ 'unused-var',
164
+ 'warn',
165
+ `'${entry.name}' is declared but never used`,
166
+ `Prefix with '_' (e.g. '_${entry.name}') to silence this warning, or remove the declaration`,
167
+ entry.loc
168
+ );
169
+ }
170
+ }
171
+
172
+ // ── Statement walker ─────────────────────────────────────────────────────
173
+ _walkStmt(node, scope, ctx) {
174
+ if (!node) return;
175
+ switch (node.type) {
176
+
177
+ case 'VarDecl': {
178
+ if (node.init) this._walkExpr(node.init, scope, ctx);
179
+ const entry = scope.define(node.name, node.kind, node.loc);
180
+ // Function declarations at module top-level are implicitly "used" (callable)
181
+ break;
182
+ }
183
+
184
+ case 'DestructureDecl': {
185
+ if (node.init) this._walkExpr(node.init, scope, ctx);
186
+ if (node.patternType === 'object') {
187
+ for (const p of node.pattern) {
188
+ scope.define(p.alias || p.name, node.kind, node.loc);
189
+ }
190
+ } else {
191
+ for (const p of node.pattern) {
192
+ if (p) scope.define(p.name, node.kind, node.loc);
193
+ }
194
+ }
195
+ break;
196
+ }
197
+
198
+ case 'FnDecl': {
199
+ // Function name itself is used (callable), mark as used
200
+ if (node.name) {
201
+ const entry = scope.define(node.name, 'val', node.loc);
202
+ entry.used = true; // fn names are callable references, treat as used
203
+ }
204
+ const inner = new LintScope(scope, 'fn');
205
+ for (const p of node.params) {
206
+ const pe = inner.define(p.name, 'val', node.loc);
207
+ pe.used = true; // params: checked at call site, not by the linter
208
+ }
209
+ if (node.inline) {
210
+ this._walkExpr(node.body, inner, { ...ctx, inFn: true });
211
+ } else {
212
+ this._walkStmts(node.body, inner, { ...ctx, inFn: true });
213
+ }
214
+ break;
215
+ }
216
+
217
+ case 'ClassDecl': {
218
+ const ce = scope.define(node.name, 'val', node.loc);
219
+ ce.used = true; // class names are constructors — external usage possible
220
+ const cls = new LintScope(scope, 'class');
221
+ for (const f of (node.fields || [])) {
222
+ const fe = cls.define(f.name, 'var', node.loc);
223
+ fe.used = true; // fields accessed via self.x — not trackable statically
224
+ }
225
+ for (const m of (node.methods || [])) this._walkStmt(m, cls, ctx);
226
+ break;
227
+ }
228
+
229
+ case 'TypeDecl': {
230
+ // ADT variant constructors are used at construction & pattern-match sites
231
+ for (const v of node.variants) {
232
+ const ve = scope.define(v.name, 'val', node.loc);
233
+ ve.used = true;
234
+ }
235
+ break;
236
+ }
237
+
238
+ case 'InterfaceDecl':
239
+ case 'EnumDecl': {
240
+ const ie = scope.define(node.name, 'val', node.loc);
241
+ ie.used = true;
242
+ break;
243
+ }
244
+
245
+ case 'IfStmt': {
246
+ this._walkExpr(node.cond, scope, ctx);
247
+ const thenScope = new LintScope(scope, 'block');
248
+ this._walkStmts(node.then, thenScope, ctx);
249
+ for (const ei of (node.elseifs || [])) {
250
+ this._walkExpr(ei.cond, scope, ctx);
251
+ const eis = new LintScope(scope, 'block');
252
+ this._walkStmts(ei.body, eis, ctx);
253
+ }
254
+ if (node.else_) {
255
+ const elseScope = new LintScope(scope, 'block');
256
+ this._walkStmts(node.else_, elseScope, ctx);
257
+ }
258
+ break;
259
+ }
260
+
261
+ case 'ForInStmt': {
262
+ this._walkExpr(node.iter, scope, ctx);
263
+ const forScope = new LintScope(scope, 'block');
264
+ const loopVar = forScope.define(node.var, 'val', node.loc);
265
+ loopVar.used = true; // loop var used in body implicitly
266
+ this._walkStmts(node.body, forScope, { ...ctx, inLoop: true });
267
+ break;
268
+ }
269
+
270
+ case 'WhileStmt': {
271
+ this._walkExpr(node.cond, scope, ctx);
272
+ const ws = new LintScope(scope, 'block');
273
+ this._walkStmts(node.body, ws, { ...ctx, inLoop: true });
274
+ break;
275
+ }
276
+
277
+ case 'DoWhileStmt': {
278
+ const ds = new LintScope(scope, 'block');
279
+ this._walkStmts(node.body, ds, { ...ctx, inLoop: true });
280
+ this._walkExpr(node.cond, scope, ctx);
281
+ break;
282
+ }
283
+
284
+ case 'MatchStmt': {
285
+ this._walkExpr(node.subject, scope, ctx);
286
+ for (const arm of node.arms) {
287
+ const armScope = new LintScope(scope, 'block');
288
+ if (arm.pattern && arm.pattern.type === 'VariantPat') {
289
+ for (const b of arm.pattern.bindings) {
290
+ const be = armScope.define(b, 'val', node.loc);
291
+ be.used = true; // bindings used in arm body
292
+ }
293
+ }
294
+ if (arm.guard) this._walkExpr(arm.guard, armScope, ctx);
295
+ if (arm.inline) {
296
+ // Inline arm: single expression body
297
+ const expr = arm.body && arm.body[0] && arm.body[0].expr
298
+ ? arm.body[0].expr : arm.body[0];
299
+ if (expr) this._walkExpr(expr, armScope, ctx);
300
+ } else {
301
+ this._walkStmts(arm.body || [], armScope, ctx);
302
+ }
303
+ }
304
+ break;
305
+ }
306
+
307
+ case 'ReturnStmt':
308
+ if (node.value) this._walkExpr(node.value, scope, ctx);
309
+ break;
310
+
311
+ case 'ThrowStmt':
312
+ this._walkExpr(node.value, scope, ctx);
313
+ break;
314
+
315
+ case 'TryCatchStmt': {
316
+ const tryS = new LintScope(scope, 'block');
317
+ this._walkStmts(node.tryBody, tryS, ctx);
318
+ if (node.catchBody) {
319
+ const catchS = new LintScope(scope, 'block');
320
+ if (node.catchParam) {
321
+ const ce2 = catchS.define(node.catchParam, 'val', node.loc);
322
+ ce2.used = true;
323
+ }
324
+ this._walkStmts(node.catchBody, catchS, ctx);
325
+ }
326
+ if (node.finallyBody) {
327
+ const finS = new LintScope(scope, 'block');
328
+ this._walkStmts(node.finallyBody, finS, ctx);
329
+ }
330
+ break;
331
+ }
332
+
333
+ case 'ImportDecl': {
334
+ // Imported names: mark as used (they come from outside, their usage
335
+ // may be in codepaths we can't fully trace — conservative)
336
+ if (node.defaultName) {
337
+ const ie2 = scope.define(node.defaultName, 'val', node.loc);
338
+ ie2.used = true;
339
+ }
340
+ if (node.namespaceName) {
341
+ const ne = scope.define(node.namespaceName, 'val', node.loc);
342
+ ne.used = true;
343
+ }
344
+ for (const n of (node.names || [])) {
345
+ const nm = typeof n === 'string' ? n : n.alias;
346
+ const ie3 = scope.define(nm, 'val', node.loc);
347
+ ie3.used = true;
348
+ }
349
+ break;
350
+ }
351
+
352
+ case 'ExportDecl': {
353
+ // Exported declarations: mark name as used
354
+ const inner = node.isDefault
355
+ ? { type: 'ExprStmt', expr: node.decl }
356
+ : node.decl;
357
+ this._walkStmt(inner, scope, ctx);
358
+ // Mark the exported name as used
359
+ if (node.decl && node.decl.name) {
360
+ const ee = scope.lookup(node.decl.name);
361
+ if (ee) { ee.used = true; ee._exported = true; }
362
+ }
363
+ break;
364
+ }
365
+
366
+ case 'ExprStmt':
367
+ this._walkExpr(node.expr, scope, ctx);
368
+ break;
369
+
370
+ case 'BreakStmt':
371
+ case 'ContinueStmt':
372
+ break;
373
+
374
+ default:
375
+ // Unknown node — skip silently
376
+ }
377
+ }
378
+
379
+ // ── Expression walker ────────────────────────────────────────────────────
380
+ _walkExpr(node, scope, ctx) {
381
+ if (!node) return;
382
+ switch (node.type) {
383
+
384
+ case 'Identifier':
385
+ scope.markUsed(node.name);
386
+ break;
387
+
388
+ case 'AssignExpr':
389
+ // The target variable IS being written to — mark as used (it's live)
390
+ this._walkExpr(node.target, scope, ctx);
391
+ this._walkExpr(node.value, scope, ctx);
392
+ break;
393
+
394
+ case 'UpdateExpr':
395
+ this._walkExpr(node.operand, scope, ctx);
396
+ break;
397
+
398
+ case 'BinaryExpr':
399
+ case 'PipeExpr':
400
+ this._walkExpr(node.left, scope, ctx);
401
+ this._walkExpr(node.right, scope, ctx);
402
+ break;
403
+
404
+ case 'UnaryExpr':
405
+ case 'AwaitExpr':
406
+ case 'TypeofExpr':
407
+ this._walkExpr(node.operand, scope, ctx);
408
+ break;
409
+
410
+ case 'TernaryExpr':
411
+ this._walkExpr(node.cond, scope, ctx);
412
+ this._walkExpr(node.then, scope, ctx);
413
+ this._walkExpr(node.else_, scope, ctx);
414
+ break;
415
+
416
+ case 'CallExpr':
417
+ case 'OptCallExpr':
418
+ this._walkExpr(node.callee, scope, ctx);
419
+ for (const a of (node.args || [])) this._walkExpr(a, scope, ctx);
420
+ break;
421
+
422
+ case 'MemberExpr':
423
+ case 'OptMemberExpr':
424
+ this._walkExpr(node.obj, scope, ctx);
425
+ break;
426
+
427
+ case 'IndexExpr':
428
+ case 'OptIndexExpr':
429
+ this._walkExpr(node.obj, scope, ctx);
430
+ this._walkExpr(node.idx, scope, ctx);
431
+ break;
432
+
433
+ case 'NewExpr':
434
+ if (node.callee) this._walkExpr(node.callee, scope, ctx);
435
+ for (const a of (node.args || [])) this._walkExpr(a, scope, ctx);
436
+ break;
437
+
438
+ case 'ArrayExpr':
439
+ for (const i of (node.items || [])) if (i) this._walkExpr(i, scope, ctx);
440
+ break;
441
+
442
+ case 'ObjectExpr':
443
+ for (const p of (node.pairs || [])) if (p.value) this._walkExpr(p.value, scope, ctx);
444
+ break;
445
+
446
+ case 'SpreadExpr':
447
+ this._walkExpr(node.expr, scope, ctx);
448
+ break;
449
+
450
+ case 'LambdaExpr': {
451
+ const inner = new LintScope(scope, 'fn');
452
+ for (const p of (node.params || [])) {
453
+ const pe = inner.define(p.name, 'val', null);
454
+ pe.used = true;
455
+ }
456
+ this._walkExpr(node.body, inner, { ...ctx, inFn: true });
457
+ break;
458
+ }
459
+
460
+ case 'FnDecl': {
461
+ // Anonymous fn expression
462
+ if (node.name) {
463
+ const entry = scope.define(node.name, 'val', node.loc);
464
+ entry.used = true;
465
+ }
466
+ const inner = new LintScope(scope, 'fn');
467
+ for (const p of (node.params || [])) {
468
+ const pe = inner.define(p.name, 'val', null);
469
+ pe.used = true;
470
+ }
471
+ if (node.inline) {
472
+ this._walkExpr(node.body, inner, { ...ctx, inFn: true });
473
+ } else {
474
+ this._walkStmts(node.body, inner, { ...ctx, inFn: true });
475
+ }
476
+ break;
477
+ }
478
+
479
+ case 'TemplateLit':
480
+ this._walkTemplateParts(node.parts || [], scope, ctx);
481
+ break;
482
+
483
+ case 'CastExpr':
484
+ case 'AsConstExpr':
485
+ case 'SatisfiesExpr':
486
+ case 'IsExpr':
487
+ case 'NonNullExpr':
488
+ this._walkExpr(node.expr, scope, ctx);
489
+ break;
490
+
491
+ case 'RangeExpr':
492
+ this._walkExpr(node.start, scope, ctx);
493
+ this._walkExpr(node.end, scope, ctx);
494
+ break;
495
+
496
+ case 'NumberLit':
497
+ case 'BoolLit':
498
+ case 'NullLit':
499
+ case 'StringLit':
500
+ case 'SelfExpr':
501
+ break;
502
+
503
+ default:
504
+ // Unknown expression — skip
505
+ }
506
+ }
507
+
508
+ // Template literal parts: {type:"text"|"expr", value: string}
509
+ // Re-parse each expr part so identifiers get marked used.
510
+ _walkTemplateParts(parts, scope, ctx) {
511
+ for (const p of parts) {
512
+ if (!p || p.type !== 'expr') continue;
513
+ try {
514
+ const tokens = new Lexer(p.value).tokenize();
515
+ const expr = new Parser(tokens).parseExpr();
516
+ this._walkExpr(expr, scope, ctx);
517
+ } catch (_) {
518
+ // If re-parse fails, fall back to regex identifier scan
519
+ const re = /\b([a-zA-Z_][a-zA-Z0-9_]*)\b/g;
520
+ let m;
521
+ while ((m = re.exec(p.value)) !== null) scope.markUsed(m[1]);
522
+ }
523
+ }
524
+ }
525
+ }
526
+
527
+ // ── Shadow check (separate pass after unused-var) ─────────────────────────────
528
+ // Walk AST again; when a new scope introduces a name already in a parent scope,
529
+ // report a shadow warning.
530
+ class ShadowChecker {
531
+ constructor() {
532
+ this.issues = [];
533
+ }
534
+
535
+ check(ast) {
536
+ this.issues = [];
537
+ // Build a flat "seen" set per ancestry path using the scope tree
538
+ this._walkStmts(ast.body, new LintScope(null, 'module'));
539
+ return this.issues;
540
+ }
541
+
542
+ _warn(name, inner, outer) {
543
+ this.issues.push(new LintIssue(
544
+ 'shadow-val',
545
+ 'warn',
546
+ `'${name}' shadows an outer declaration`,
547
+ `Rename one of the '${name}' variables to avoid confusion`,
548
+ inner
549
+ ));
550
+ }
551
+
552
+ _define(scope, name, loc) {
553
+ const outer = scope._lookupParent ? scope._lookupParent(name) : null;
554
+ if (outer && !isIgnoredName(name)) {
555
+ this._warn(name, loc, outer.loc);
556
+ }
557
+ scope.define(name, 'val', loc);
558
+ }
559
+
560
+ _walkStmts(stmts, scope) {
561
+ if (!stmts) return;
562
+ for (const node of stmts) this._walkStmt(node, scope);
563
+ }
564
+
565
+ _walkStmt(node, scope) {
566
+ if (!node) return;
567
+ switch (node.type) {
568
+ case 'VarDecl':
569
+ if (node.init) this._walkExpr(node.init, scope);
570
+ this._define(scope, node.name, node.loc);
571
+ break;
572
+
573
+ case 'DestructureDecl':
574
+ if (node.init) this._walkExpr(node.init, scope);
575
+ if (node.patternType === 'object') {
576
+ for (const p of node.pattern) this._define(scope, p.alias || p.name, node.loc);
577
+ } else {
578
+ for (const p of node.pattern) if (p) this._define(scope, p.name, node.loc);
579
+ }
580
+ break;
581
+
582
+ case 'FnDecl': {
583
+ if (node.name) this._define(scope, node.name, node.loc);
584
+ const inner = new LintScope(scope, 'fn');
585
+ for (const p of (node.params || [])) this._define(inner, p.name, node.loc);
586
+ if (node.inline) this._walkExpr(node.body, inner);
587
+ else this._walkStmts(node.body, inner);
588
+ break;
589
+ }
590
+
591
+ case 'ClassDecl': {
592
+ this._define(scope, node.name, node.loc);
593
+ const cls = new LintScope(scope, 'class');
594
+ for (const m of (node.methods || [])) this._walkStmt(m, cls);
595
+ break;
596
+ }
597
+
598
+ case 'IfStmt':
599
+ this._walkExpr(node.cond, scope);
600
+ this._walkStmts(node.then, new LintScope(scope, 'block'));
601
+ for (const ei of (node.elseifs || [])) {
602
+ this._walkExpr(ei.cond, scope);
603
+ this._walkStmts(ei.body, new LintScope(scope, 'block'));
604
+ }
605
+ if (node.else_) this._walkStmts(node.else_, new LintScope(scope, 'block'));
606
+ break;
607
+
608
+ case 'ForInStmt': {
609
+ this._walkExpr(node.iter, scope);
610
+ const fs = new LintScope(scope, 'block');
611
+ this._define(fs, node.var, node.loc);
612
+ this._walkStmts(node.body, fs);
613
+ break;
614
+ }
615
+
616
+ case 'WhileStmt':
617
+ this._walkExpr(node.cond, scope);
618
+ this._walkStmts(node.body, new LintScope(scope, 'block'));
619
+ break;
620
+
621
+ case 'DoWhileStmt':
622
+ this._walkStmts(node.body, new LintScope(scope, 'block'));
623
+ this._walkExpr(node.cond, scope);
624
+ break;
625
+
626
+ case 'MatchStmt':
627
+ this._walkExpr(node.subject, scope);
628
+ for (const arm of (node.arms || [])) {
629
+ const as_ = new LintScope(scope, 'block');
630
+ if (arm.pattern && arm.pattern.type === 'VariantPat') {
631
+ for (const b of arm.pattern.bindings) this._define(as_, b, node.loc);
632
+ }
633
+ if (arm.guard) this._walkExpr(arm.guard, as_);
634
+ if (arm.inline) {
635
+ const expr = arm.body && arm.body[0] && arm.body[0].expr
636
+ ? arm.body[0].expr : arm.body[0];
637
+ if (expr) this._walkExpr(expr, as_);
638
+ } else {
639
+ this._walkStmts(arm.body || [], as_);
640
+ }
641
+ }
642
+ break;
643
+
644
+ case 'ReturnStmt': if (node.value) this._walkExpr(node.value, scope); break;
645
+ case 'ThrowStmt': this._walkExpr(node.value, scope); break;
646
+ case 'ExprStmt': this._walkExpr(node.expr, scope); break;
647
+
648
+ case 'TryCatchStmt':
649
+ this._walkStmts(node.tryBody, new LintScope(scope, 'block'));
650
+ if (node.catchBody) {
651
+ const cs = new LintScope(scope, 'block');
652
+ if (node.catchParam) this._define(cs, node.catchParam, node.loc);
653
+ this._walkStmts(node.catchBody, cs);
654
+ }
655
+ if (node.finallyBody) this._walkStmts(node.finallyBody, new LintScope(scope, 'block'));
656
+ break;
657
+
658
+ case 'ImportDecl':
659
+ if (node.defaultName) this._define(scope, node.defaultName, node.loc);
660
+ if (node.namespaceName) this._define(scope, node.namespaceName, node.loc);
661
+ for (const n of (node.names || [])) {
662
+ this._define(scope, typeof n === 'string' ? n : n.alias, node.loc);
663
+ }
664
+ break;
665
+
666
+ case 'ExportDecl':
667
+ this._walkStmt(node.isDefault
668
+ ? { type: 'ExprStmt', expr: node.decl } : node.decl, scope);
669
+ break;
670
+
671
+ default: break;
672
+ }
673
+ }
674
+
675
+ _walkExpr(node, scope) {
676
+ if (!node) return;
677
+ switch (node.type) {
678
+ case 'LambdaExpr': {
679
+ const inner = new LintScope(scope, 'fn');
680
+ for (const p of (node.params || [])) this._define(inner, p.name, null);
681
+ this._walkExpr(node.body, inner);
682
+ break;
683
+ }
684
+ case 'FnDecl': {
685
+ const inner = new LintScope(scope, 'fn');
686
+ for (const p of (node.params || [])) this._define(inner, p.name, null);
687
+ if (node.inline) this._walkExpr(node.body, inner);
688
+ else this._walkStmts(node.body, inner);
689
+ break;
690
+ }
691
+ case 'BinaryExpr': case 'PipeExpr':
692
+ this._walkExpr(node.left, scope); this._walkExpr(node.right, scope); break;
693
+ case 'UnaryExpr': case 'AwaitExpr': case 'TypeofExpr':
694
+ case 'CastExpr': case 'AsConstExpr': case 'SatisfiesExpr':
695
+ case 'IsExpr': case 'NonNullExpr': case 'SpreadExpr':
696
+ this._walkExpr(node.operand || node.expr, scope); break;
697
+ case 'TernaryExpr':
698
+ this._walkExpr(node.cond, scope);
699
+ this._walkExpr(node.then, scope);
700
+ this._walkExpr(node.else_, scope); break;
701
+ case 'CallExpr': case 'OptCallExpr':
702
+ this._walkExpr(node.callee, scope);
703
+ for (const a of (node.args || [])) this._walkExpr(a, scope); break;
704
+ case 'MemberExpr': case 'OptMemberExpr':
705
+ this._walkExpr(node.obj, scope); break;
706
+ case 'IndexExpr': case 'OptIndexExpr':
707
+ this._walkExpr(node.obj, scope); this._walkExpr(node.idx, scope); break;
708
+ case 'NewExpr':
709
+ if (node.callee) this._walkExpr(node.callee, scope);
710
+ for (const a of (node.args || [])) this._walkExpr(a, scope); break;
711
+ case 'ArrayExpr':
712
+ for (const i of (node.items || [])) if (i) this._walkExpr(i, scope); break;
713
+ case 'ObjectExpr':
714
+ for (const p of (node.pairs || [])) if (p.value) this._walkExpr(p.value, scope); break;
715
+ case 'AssignExpr':
716
+ this._walkExpr(node.target, scope); this._walkExpr(node.value, scope); break;
717
+ case 'UpdateExpr':
718
+ this._walkExpr(node.operand, scope); break;
719
+ case 'RangeExpr':
720
+ this._walkExpr(node.start, scope); this._walkExpr(node.end, scope); break;
721
+ case 'TemplateLit':
722
+ for (const p of (node.parts || [])) {
723
+ if (!p || p.type !== 'expr') continue;
724
+ try {
725
+ const tokens = new Lexer(p.value).tokenize();
726
+ const expr = new Parser(tokens).parseExpr();
727
+ this._walkExpr(expr, scope);
728
+ } catch (_) {
729
+ const re = /\b([a-zA-Z_][a-zA-Z0-9_]*)\b/g;
730
+ let m;
731
+ while ((m = re.exec(p.value)) !== null) {
732
+ const entry = scope.lookup ? scope.lookup(m[1]) : null;
733
+ // ShadowChecker only cares about definitions, not reads
734
+ }
735
+ }
736
+ }
737
+ break;
738
+ default: break;
739
+ }
740
+ }
741
+ }
742
+
743
+ // ── Public API ────────────────────────────────────────────────────────────────
744
+ function lint(ast) {
745
+ const linter = new Linter();
746
+ const shadowChecker = new ShadowChecker();
747
+
748
+ const unusedAndUnreachable = linter.lint(ast);
749
+ const shadows = shadowChecker.check(ast);
750
+
751
+ return [...unusedAndUnreachable, ...shadows].sort((a, b) => {
752
+ const la = a.line || 0;
753
+ const lb = b.line || 0;
754
+ return la - lb;
755
+ });
756
+ }
757
+
758
+ module.exports = { lint, Linter, ShadowChecker, LintIssue };