@xnoxs/flux-lang 3.2.1 → 3.2.2

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/src/linter.js DELETED
@@ -1,784 +0,0 @@
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 — sequences
79
- 'range', 'zip', 'enumerate', 'flatten', 'chunk', 'unique', 'groupBy', 'sortBy',
80
- // Flux stdlib — array pipe helpers
81
- 'map', 'filter', 'reduce', 'forEach', 'find', 'findIndex',
82
- 'some', 'every', 'join', 'sort', 'flat', 'flatMap', 'includes',
83
- // Flux stdlib — array extras
84
- 'first', 'last', 'take', 'drop', 'takeWhile', 'dropWhile',
85
- 'compact', 'intersection', 'difference', 'arrayUnion', 'unzip',
86
- 'countBy', 'minBy', 'maxBy', 'toPairs', 'partition', 'count',
87
- 'head', 'tail', 'nth', 'rotate', 'sliding',
88
- // Flux stdlib — math
89
- 'clamp', 'sum', 'product', 'min', 'max', 'abs', 'floor', 'ceil', 'round',
90
- 'mean', 'median', 'stdDev', 'lerp', 'randInt', 'sample', 'shuffle',
91
- // Flux stdlib — objects
92
- 'pick', 'omit', 'mapValues', 'filterValues', 'fromEntries',
93
- 'keys', 'values', 'entries', 'merge', 'invert', 'defaults',
94
- 'deepEqual', 'deepClone',
95
- // Flux stdlib — strings
96
- 'capitalize', 'camelCase', 'snakeCase', 'kebabCase', 'truncate', 'pad',
97
- 'padStart', 'padEnd', 'trim', 'trimStart', 'trimEnd',
98
- 'words', 'lines', 'startsWith', 'endsWith', 'repeat', 'replaceAll', 'reverseStr',
99
- // Flux stdlib — type checks
100
- 'isNil', 'isString', 'isNumber', 'isArray', 'isObject', 'isFunction', 'isBool',
101
- // Flux stdlib — async
102
- 'sleep', 'retry', 'memoize', 'timeout', 'debounce', 'throttle', 'allSettled',
103
- // Flux stdlib — functional
104
- 'pipe', 'compose', 'partial', 'curry', 'identity', 'noop', 'once', 'flip', 'complement',
105
- // Flux builtins
106
- 'print',
107
- ]);
108
-
109
- function isIgnoredName(name) {
110
- if (BUILTIN_NAMES.has(name)) return true;
111
- if (name.startsWith('_')) return true; // underscore prefix = intentionally unused
112
- return false;
113
- }
114
-
115
- // ── The Linter ────────────────────────────────────────────────────────────────
116
- class Linter {
117
- constructor() {
118
- this.issues = [];
119
- }
120
-
121
- lint(ast) {
122
- this.issues = [];
123
- const scope = new LintScope(null, 'module');
124
- this._walkStmts(ast.body, scope, { inFn: false });
125
- return this.issues;
126
- }
127
-
128
- _issue(rule, severity, message, hint, loc) {
129
- this.issues.push(new LintIssue(rule, severity, message, hint, loc));
130
- }
131
-
132
- // ── Statement list walker ────────────────────────────────────────────────
133
- _walkStmts(stmts, scope, ctx) {
134
- if (!stmts || stmts.length === 0) return;
135
-
136
- for (let i = 0; i < stmts.length; i++) {
137
- const node = stmts[i];
138
-
139
- // RULE 2: unreachable code after terminating statement
140
- if (i > 0 && this._isTerminator(stmts[i - 1])) {
141
- // Skip blank/comment nodes (they have no loc usually)
142
- if (node && node.loc) {
143
- this._issue(
144
- 'unreachable',
145
- 'error',
146
- `Unreachable code after '${this._terminatorName(stmts[i - 1])}'`,
147
- `Remove or move the unreachable statement`,
148
- node.loc
149
- );
150
- // Report only the first unreachable per block to avoid cascade
151
- break;
152
- }
153
- }
154
-
155
- this._walkStmt(node, scope, ctx);
156
- }
157
-
158
- // After walking the block, check for unused vars declared in this scope
159
- this._checkUnused(scope);
160
- }
161
-
162
- _isTerminator(node) {
163
- if (!node) return false;
164
- if (node.type === 'ReturnStmt' || node.type === 'ThrowStmt') return true;
165
- if (node.type === 'BreakStmt' || node.type === 'ContinueStmt') return true;
166
- // Inline fn body ending in return
167
- return false;
168
- }
169
-
170
- _terminatorName(node) {
171
- const map = {
172
- ReturnStmt: 'return',
173
- ThrowStmt: 'throw',
174
- BreakStmt: 'break',
175
- ContinueStmt: 'continue',
176
- };
177
- return map[node.type] || 'terminator';
178
- }
179
-
180
- // ── Unused variable check ────────────────────────────────────────────────
181
- _checkUnused(scope) {
182
- for (const entry of scope.ownEntries()) {
183
- if (entry.used) continue;
184
- if (isIgnoredName(entry.name)) continue;
185
- // Exported names are considered used by the outside world
186
- if (entry._exported) continue;
187
-
188
- this._issue(
189
- 'unused-var',
190
- 'warn',
191
- `'${entry.name}' is declared but never used`,
192
- `Prefix with '_' (e.g. '_${entry.name}') to silence this warning, or remove the declaration`,
193
- entry.loc
194
- );
195
- }
196
- }
197
-
198
- // ── Statement walker ─────────────────────────────────────────────────────
199
- _walkStmt(node, scope, ctx) {
200
- if (!node) return;
201
- switch (node.type) {
202
-
203
- case 'VarDecl': {
204
- if (node.init) this._walkExpr(node.init, scope, ctx);
205
- const entry = scope.define(node.name, node.kind, node.loc);
206
- // Function declarations at module top-level are implicitly "used" (callable)
207
- break;
208
- }
209
-
210
- case 'DestructureDecl': {
211
- if (node.init) this._walkExpr(node.init, scope, ctx);
212
- if (node.patternType === 'object') {
213
- for (const p of node.pattern) {
214
- scope.define(p.alias || p.name, node.kind, node.loc);
215
- }
216
- } else {
217
- for (const p of node.pattern) {
218
- if (p) scope.define(p.name, node.kind, node.loc);
219
- }
220
- }
221
- break;
222
- }
223
-
224
- case 'FnDecl': {
225
- // Function name itself is used (callable), mark as used
226
- if (node.name) {
227
- const entry = scope.define(node.name, 'val', node.loc);
228
- entry.used = true; // fn names are callable references, treat as used
229
- }
230
- const inner = new LintScope(scope, 'fn');
231
- for (const p of node.params) {
232
- const pe = inner.define(p.name, 'val', node.loc);
233
- pe.used = true; // params: checked at call site, not by the linter
234
- }
235
- if (node.inline) {
236
- this._walkExpr(node.body, inner, { ...ctx, inFn: true });
237
- } else {
238
- this._walkStmts(node.body, inner, { ...ctx, inFn: true });
239
- }
240
- break;
241
- }
242
-
243
- case 'ClassDecl': {
244
- const ce = scope.define(node.name, 'val', node.loc);
245
- ce.used = true; // class names are constructors — external usage possible
246
- const cls = new LintScope(scope, 'class');
247
- for (const f of (node.fields || [])) {
248
- const fe = cls.define(f.name, 'var', node.loc);
249
- fe.used = true; // fields accessed via self.x — not trackable statically
250
- }
251
- for (const m of (node.methods || [])) this._walkStmt(m, cls, ctx);
252
- break;
253
- }
254
-
255
- case 'TypeDecl': {
256
- // ADT variant constructors are used at construction & pattern-match sites
257
- for (const v of node.variants) {
258
- const ve = scope.define(v.name, 'val', node.loc);
259
- ve.used = true;
260
- }
261
- break;
262
- }
263
-
264
- case 'InterfaceDecl':
265
- case 'EnumDecl': {
266
- const ie = scope.define(node.name, 'val', node.loc);
267
- ie.used = true;
268
- break;
269
- }
270
-
271
- case 'IfStmt': {
272
- this._walkExpr(node.cond, scope, ctx);
273
- const thenScope = new LintScope(scope, 'block');
274
- this._walkStmts(node.then, thenScope, ctx);
275
- for (const ei of (node.elseifs || [])) {
276
- this._walkExpr(ei.cond, scope, ctx);
277
- const eis = new LintScope(scope, 'block');
278
- this._walkStmts(ei.body, eis, ctx);
279
- }
280
- if (node.else_) {
281
- const elseScope = new LintScope(scope, 'block');
282
- this._walkStmts(node.else_, elseScope, ctx);
283
- }
284
- break;
285
- }
286
-
287
- case 'ForInStmt': {
288
- this._walkExpr(node.iter, scope, ctx);
289
- const forScope = new LintScope(scope, 'block');
290
- const loopVar = forScope.define(node.var, 'val', node.loc);
291
- loopVar.used = true; // loop var used in body implicitly
292
- this._walkStmts(node.body, forScope, { ...ctx, inLoop: true });
293
- break;
294
- }
295
-
296
- case 'WhileStmt': {
297
- this._walkExpr(node.cond, scope, ctx);
298
- const ws = new LintScope(scope, 'block');
299
- this._walkStmts(node.body, ws, { ...ctx, inLoop: true });
300
- break;
301
- }
302
-
303
- case 'DoWhileStmt': {
304
- const ds = new LintScope(scope, 'block');
305
- this._walkStmts(node.body, ds, { ...ctx, inLoop: true });
306
- this._walkExpr(node.cond, scope, ctx);
307
- break;
308
- }
309
-
310
- case 'MatchStmt': {
311
- this._walkExpr(node.subject, scope, ctx);
312
- for (const arm of node.arms) {
313
- const armScope = new LintScope(scope, 'block');
314
- if (arm.pattern && arm.pattern.type === 'VariantPat') {
315
- for (const b of arm.pattern.bindings) {
316
- const be = armScope.define(b, 'val', node.loc);
317
- be.used = true; // bindings used in arm body
318
- }
319
- }
320
- if (arm.guard) this._walkExpr(arm.guard, armScope, ctx);
321
- if (arm.inline) {
322
- // Inline arm: single expression body
323
- const expr = arm.body && arm.body[0] && arm.body[0].expr
324
- ? arm.body[0].expr : arm.body[0];
325
- if (expr) this._walkExpr(expr, armScope, ctx);
326
- } else {
327
- this._walkStmts(arm.body || [], armScope, ctx);
328
- }
329
- }
330
- break;
331
- }
332
-
333
- case 'ReturnStmt':
334
- if (node.value) this._walkExpr(node.value, scope, ctx);
335
- break;
336
-
337
- case 'ThrowStmt':
338
- this._walkExpr(node.value, scope, ctx);
339
- break;
340
-
341
- case 'TryCatchStmt': {
342
- const tryS = new LintScope(scope, 'block');
343
- this._walkStmts(node.tryBody, tryS, ctx);
344
- if (node.catchBody) {
345
- const catchS = new LintScope(scope, 'block');
346
- if (node.catchParam) {
347
- const ce2 = catchS.define(node.catchParam, 'val', node.loc);
348
- ce2.used = true;
349
- }
350
- this._walkStmts(node.catchBody, catchS, ctx);
351
- }
352
- if (node.finallyBody) {
353
- const finS = new LintScope(scope, 'block');
354
- this._walkStmts(node.finallyBody, finS, ctx);
355
- }
356
- break;
357
- }
358
-
359
- case 'ImportDecl': {
360
- // Imported names: mark as used (they come from outside, their usage
361
- // may be in codepaths we can't fully trace — conservative)
362
- if (node.defaultName) {
363
- const ie2 = scope.define(node.defaultName, 'val', node.loc);
364
- ie2.used = true;
365
- }
366
- if (node.namespaceName) {
367
- const ne = scope.define(node.namespaceName, 'val', node.loc);
368
- ne.used = true;
369
- }
370
- for (const n of (node.names || [])) {
371
- const nm = typeof n === 'string' ? n : n.alias;
372
- const ie3 = scope.define(nm, 'val', node.loc);
373
- ie3.used = true;
374
- }
375
- break;
376
- }
377
-
378
- case 'ExportDecl': {
379
- // Exported declarations: mark name as used
380
- const inner = node.isDefault
381
- ? { type: 'ExprStmt', expr: node.decl }
382
- : node.decl;
383
- this._walkStmt(inner, scope, ctx);
384
- // Mark the exported name as used
385
- if (node.decl && node.decl.name) {
386
- const ee = scope.lookup(node.decl.name);
387
- if (ee) { ee.used = true; ee._exported = true; }
388
- }
389
- break;
390
- }
391
-
392
- case 'ExprStmt':
393
- this._walkExpr(node.expr, scope, ctx);
394
- break;
395
-
396
- case 'BreakStmt':
397
- case 'ContinueStmt':
398
- break;
399
-
400
- default:
401
- // Unknown node — skip silently
402
- }
403
- }
404
-
405
- // ── Expression walker ────────────────────────────────────────────────────
406
- _walkExpr(node, scope, ctx) {
407
- if (!node) return;
408
- switch (node.type) {
409
-
410
- case 'Identifier':
411
- scope.markUsed(node.name);
412
- break;
413
-
414
- case 'AssignExpr':
415
- // The target variable IS being written to — mark as used (it's live)
416
- this._walkExpr(node.target, scope, ctx);
417
- this._walkExpr(node.value, scope, ctx);
418
- break;
419
-
420
- case 'UpdateExpr':
421
- this._walkExpr(node.operand, scope, ctx);
422
- break;
423
-
424
- case 'BinaryExpr':
425
- case 'PipeExpr':
426
- this._walkExpr(node.left, scope, ctx);
427
- this._walkExpr(node.right, scope, ctx);
428
- break;
429
-
430
- case 'UnaryExpr':
431
- case 'AwaitExpr':
432
- case 'TypeofExpr':
433
- this._walkExpr(node.operand, scope, ctx);
434
- break;
435
-
436
- case 'TernaryExpr':
437
- this._walkExpr(node.cond, scope, ctx);
438
- this._walkExpr(node.then, scope, ctx);
439
- this._walkExpr(node.else_, scope, ctx);
440
- break;
441
-
442
- case 'CallExpr':
443
- case 'OptCallExpr':
444
- this._walkExpr(node.callee, scope, ctx);
445
- for (const a of (node.args || [])) this._walkExpr(a, scope, ctx);
446
- break;
447
-
448
- case 'MemberExpr':
449
- case 'OptMemberExpr':
450
- this._walkExpr(node.obj, scope, ctx);
451
- break;
452
-
453
- case 'IndexExpr':
454
- case 'OptIndexExpr':
455
- this._walkExpr(node.obj, scope, ctx);
456
- this._walkExpr(node.idx, scope, ctx);
457
- break;
458
-
459
- case 'NewExpr':
460
- if (node.callee) this._walkExpr(node.callee, scope, ctx);
461
- for (const a of (node.args || [])) this._walkExpr(a, scope, ctx);
462
- break;
463
-
464
- case 'ArrayExpr':
465
- for (const i of (node.items || [])) if (i) this._walkExpr(i, scope, ctx);
466
- break;
467
-
468
- case 'ObjectExpr':
469
- for (const p of (node.pairs || [])) if (p.value) this._walkExpr(p.value, scope, ctx);
470
- break;
471
-
472
- case 'SpreadExpr':
473
- this._walkExpr(node.expr, scope, ctx);
474
- break;
475
-
476
- case 'LambdaExpr': {
477
- const inner = new LintScope(scope, 'fn');
478
- for (const p of (node.params || [])) {
479
- const pe = inner.define(p.name, 'val', null);
480
- pe.used = true;
481
- }
482
- this._walkExpr(node.body, inner, { ...ctx, inFn: true });
483
- break;
484
- }
485
-
486
- case 'FnDecl': {
487
- // Anonymous fn expression
488
- if (node.name) {
489
- const entry = scope.define(node.name, 'val', node.loc);
490
- entry.used = true;
491
- }
492
- const inner = new LintScope(scope, 'fn');
493
- for (const p of (node.params || [])) {
494
- const pe = inner.define(p.name, 'val', null);
495
- pe.used = true;
496
- }
497
- if (node.inline) {
498
- this._walkExpr(node.body, inner, { ...ctx, inFn: true });
499
- } else {
500
- this._walkStmts(node.body, inner, { ...ctx, inFn: true });
501
- }
502
- break;
503
- }
504
-
505
- case 'TemplateLit':
506
- this._walkTemplateParts(node.parts || [], scope, ctx);
507
- break;
508
-
509
- case 'CastExpr':
510
- case 'AsConstExpr':
511
- case 'SatisfiesExpr':
512
- case 'IsExpr':
513
- case 'NonNullExpr':
514
- this._walkExpr(node.expr, scope, ctx);
515
- break;
516
-
517
- case 'RangeExpr':
518
- this._walkExpr(node.start, scope, ctx);
519
- this._walkExpr(node.end, scope, ctx);
520
- break;
521
-
522
- case 'NumberLit':
523
- case 'BoolLit':
524
- case 'NullLit':
525
- case 'StringLit':
526
- case 'SelfExpr':
527
- break;
528
-
529
- default:
530
- // Unknown expression — skip
531
- }
532
- }
533
-
534
- // Template literal parts: {type:"text"|"expr", value: string}
535
- // Re-parse each expr part so identifiers get marked used.
536
- _walkTemplateParts(parts, scope, ctx) {
537
- for (const p of parts) {
538
- if (!p || p.type !== 'expr') continue;
539
- try {
540
- const tokens = new Lexer(p.value).tokenize();
541
- const expr = new Parser(tokens).parseExpr();
542
- this._walkExpr(expr, scope, ctx);
543
- } catch (_) {
544
- // If re-parse fails, fall back to regex identifier scan
545
- const re = /\b([a-zA-Z_][a-zA-Z0-9_]*)\b/g;
546
- let m;
547
- while ((m = re.exec(p.value)) !== null) scope.markUsed(m[1]);
548
- }
549
- }
550
- }
551
- }
552
-
553
- // ── Shadow check (separate pass after unused-var) ─────────────────────────────
554
- // Walk AST again; when a new scope introduces a name already in a parent scope,
555
- // report a shadow warning.
556
- class ShadowChecker {
557
- constructor() {
558
- this.issues = [];
559
- }
560
-
561
- check(ast) {
562
- this.issues = [];
563
- // Build a flat "seen" set per ancestry path using the scope tree
564
- this._walkStmts(ast.body, new LintScope(null, 'module'));
565
- return this.issues;
566
- }
567
-
568
- _warn(name, inner, outer) {
569
- this.issues.push(new LintIssue(
570
- 'shadow-val',
571
- 'warn',
572
- `'${name}' shadows an outer declaration`,
573
- `Rename one of the '${name}' variables to avoid confusion`,
574
- inner
575
- ));
576
- }
577
-
578
- _define(scope, name, loc) {
579
- const outer = scope._lookupParent ? scope._lookupParent(name) : null;
580
- if (outer && !isIgnoredName(name)) {
581
- this._warn(name, loc, outer.loc);
582
- }
583
- scope.define(name, 'val', loc);
584
- }
585
-
586
- _walkStmts(stmts, scope) {
587
- if (!stmts) return;
588
- for (const node of stmts) this._walkStmt(node, scope);
589
- }
590
-
591
- _walkStmt(node, scope) {
592
- if (!node) return;
593
- switch (node.type) {
594
- case 'VarDecl':
595
- if (node.init) this._walkExpr(node.init, scope);
596
- this._define(scope, node.name, node.loc);
597
- break;
598
-
599
- case 'DestructureDecl':
600
- if (node.init) this._walkExpr(node.init, scope);
601
- if (node.patternType === 'object') {
602
- for (const p of node.pattern) this._define(scope, p.alias || p.name, node.loc);
603
- } else {
604
- for (const p of node.pattern) if (p) this._define(scope, p.name, node.loc);
605
- }
606
- break;
607
-
608
- case 'FnDecl': {
609
- if (node.name) this._define(scope, node.name, node.loc);
610
- const inner = new LintScope(scope, 'fn');
611
- for (const p of (node.params || [])) this._define(inner, p.name, node.loc);
612
- if (node.inline) this._walkExpr(node.body, inner);
613
- else this._walkStmts(node.body, inner);
614
- break;
615
- }
616
-
617
- case 'ClassDecl': {
618
- this._define(scope, node.name, node.loc);
619
- const cls = new LintScope(scope, 'class');
620
- for (const m of (node.methods || [])) this._walkStmt(m, cls);
621
- break;
622
- }
623
-
624
- case 'IfStmt':
625
- this._walkExpr(node.cond, scope);
626
- this._walkStmts(node.then, new LintScope(scope, 'block'));
627
- for (const ei of (node.elseifs || [])) {
628
- this._walkExpr(ei.cond, scope);
629
- this._walkStmts(ei.body, new LintScope(scope, 'block'));
630
- }
631
- if (node.else_) this._walkStmts(node.else_, new LintScope(scope, 'block'));
632
- break;
633
-
634
- case 'ForInStmt': {
635
- this._walkExpr(node.iter, scope);
636
- const fs = new LintScope(scope, 'block');
637
- this._define(fs, node.var, node.loc);
638
- this._walkStmts(node.body, fs);
639
- break;
640
- }
641
-
642
- case 'WhileStmt':
643
- this._walkExpr(node.cond, scope);
644
- this._walkStmts(node.body, new LintScope(scope, 'block'));
645
- break;
646
-
647
- case 'DoWhileStmt':
648
- this._walkStmts(node.body, new LintScope(scope, 'block'));
649
- this._walkExpr(node.cond, scope);
650
- break;
651
-
652
- case 'MatchStmt':
653
- this._walkExpr(node.subject, scope);
654
- for (const arm of (node.arms || [])) {
655
- const as_ = new LintScope(scope, 'block');
656
- if (arm.pattern && arm.pattern.type === 'VariantPat') {
657
- for (const b of arm.pattern.bindings) this._define(as_, b, node.loc);
658
- }
659
- if (arm.guard) this._walkExpr(arm.guard, as_);
660
- if (arm.inline) {
661
- const expr = arm.body && arm.body[0] && arm.body[0].expr
662
- ? arm.body[0].expr : arm.body[0];
663
- if (expr) this._walkExpr(expr, as_);
664
- } else {
665
- this._walkStmts(arm.body || [], as_);
666
- }
667
- }
668
- break;
669
-
670
- case 'ReturnStmt': if (node.value) this._walkExpr(node.value, scope); break;
671
- case 'ThrowStmt': this._walkExpr(node.value, scope); break;
672
- case 'ExprStmt': this._walkExpr(node.expr, scope); break;
673
-
674
- case 'TryCatchStmt':
675
- this._walkStmts(node.tryBody, new LintScope(scope, 'block'));
676
- if (node.catchBody) {
677
- const cs = new LintScope(scope, 'block');
678
- if (node.catchParam) this._define(cs, node.catchParam, node.loc);
679
- this._walkStmts(node.catchBody, cs);
680
- }
681
- if (node.finallyBody) this._walkStmts(node.finallyBody, new LintScope(scope, 'block'));
682
- break;
683
-
684
- case 'ImportDecl':
685
- if (node.defaultName) this._define(scope, node.defaultName, node.loc);
686
- if (node.namespaceName) this._define(scope, node.namespaceName, node.loc);
687
- for (const n of (node.names || [])) {
688
- this._define(scope, typeof n === 'string' ? n : n.alias, node.loc);
689
- }
690
- break;
691
-
692
- case 'ExportDecl':
693
- this._walkStmt(node.isDefault
694
- ? { type: 'ExprStmt', expr: node.decl } : node.decl, scope);
695
- break;
696
-
697
- default: break;
698
- }
699
- }
700
-
701
- _walkExpr(node, scope) {
702
- if (!node) return;
703
- switch (node.type) {
704
- case 'LambdaExpr': {
705
- const inner = new LintScope(scope, 'fn');
706
- for (const p of (node.params || [])) this._define(inner, p.name, null);
707
- this._walkExpr(node.body, inner);
708
- break;
709
- }
710
- case 'FnDecl': {
711
- const inner = new LintScope(scope, 'fn');
712
- for (const p of (node.params || [])) this._define(inner, p.name, null);
713
- if (node.inline) this._walkExpr(node.body, inner);
714
- else this._walkStmts(node.body, inner);
715
- break;
716
- }
717
- case 'BinaryExpr': case 'PipeExpr':
718
- this._walkExpr(node.left, scope); this._walkExpr(node.right, scope); break;
719
- case 'UnaryExpr': case 'AwaitExpr': case 'TypeofExpr':
720
- case 'CastExpr': case 'AsConstExpr': case 'SatisfiesExpr':
721
- case 'IsExpr': case 'NonNullExpr': case 'SpreadExpr':
722
- this._walkExpr(node.operand || node.expr, scope); break;
723
- case 'TernaryExpr':
724
- this._walkExpr(node.cond, scope);
725
- this._walkExpr(node.then, scope);
726
- this._walkExpr(node.else_, scope); break;
727
- case 'CallExpr': case 'OptCallExpr':
728
- this._walkExpr(node.callee, scope);
729
- for (const a of (node.args || [])) this._walkExpr(a, scope); break;
730
- case 'MemberExpr': case 'OptMemberExpr':
731
- this._walkExpr(node.obj, scope); break;
732
- case 'IndexExpr': case 'OptIndexExpr':
733
- this._walkExpr(node.obj, scope); this._walkExpr(node.idx, scope); break;
734
- case 'NewExpr':
735
- if (node.callee) this._walkExpr(node.callee, scope);
736
- for (const a of (node.args || [])) this._walkExpr(a, scope); break;
737
- case 'ArrayExpr':
738
- for (const i of (node.items || [])) if (i) this._walkExpr(i, scope); break;
739
- case 'ObjectExpr':
740
- for (const p of (node.pairs || [])) if (p.value) this._walkExpr(p.value, scope); break;
741
- case 'AssignExpr':
742
- this._walkExpr(node.target, scope); this._walkExpr(node.value, scope); break;
743
- case 'UpdateExpr':
744
- this._walkExpr(node.operand, scope); break;
745
- case 'RangeExpr':
746
- this._walkExpr(node.start, scope); this._walkExpr(node.end, scope); break;
747
- case 'TemplateLit':
748
- for (const p of (node.parts || [])) {
749
- if (!p || p.type !== 'expr') continue;
750
- try {
751
- const tokens = new Lexer(p.value).tokenize();
752
- const expr = new Parser(tokens).parseExpr();
753
- this._walkExpr(expr, scope);
754
- } catch (_) {
755
- const re = /\b([a-zA-Z_][a-zA-Z0-9_]*)\b/g;
756
- let m;
757
- while ((m = re.exec(p.value)) !== null) {
758
- const entry = scope.lookup ? scope.lookup(m[1]) : null;
759
- // ShadowChecker only cares about definitions, not reads
760
- }
761
- }
762
- }
763
- break;
764
- default: break;
765
- }
766
- }
767
- }
768
-
769
- // ── Public API ────────────────────────────────────────────────────────────────
770
- function lint(ast) {
771
- const linter = new Linter();
772
- const shadowChecker = new ShadowChecker();
773
-
774
- const unusedAndUnreachable = linter.lint(ast);
775
- const shadows = shadowChecker.check(ast);
776
-
777
- return [...unusedAndUnreachable, ...shadows].sort((a, b) => {
778
- const la = a.line || 0;
779
- const lb = b.line || 0;
780
- return la - lb;
781
- });
782
- }
783
-
784
- module.exports = { lint, Linter, ShadowChecker, LintIssue };