@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.
- package/CHANGELOG.md +103 -0
- package/README.md +1089 -0
- package/bin/flux.js +1397 -0
- package/dist/flux.cjs.js +6664 -0
- package/dist/flux.esm.js +6674 -0
- package/dist/flux.min.js +263 -0
- package/index.d.ts +202 -0
- package/index.js +26 -0
- package/package.json +77 -0
- package/scripts/build.js +76 -0
- package/src/bundler.js +216 -0
- package/src/checker.js +322 -0
- package/src/codegen.js +785 -0
- package/src/css-preprocessor.js +399 -0
- package/src/formatter.js +140 -0
- package/src/jsx.js +480 -0
- package/src/lexer.js +518 -0
- package/src/linter.js +758 -0
- package/src/mangler.js +280 -0
- package/src/parser.js +1671 -0
- package/src/self/bundler.flux +167 -0
- package/src/self/bundler.js +187 -0
- package/src/self/checker.flux +249 -0
- package/src/self/checker.js +338 -0
- package/src/self/codegen.flux +555 -0
- package/src/self/codegen.js +784 -0
- package/src/self/css-preprocessor.flux +373 -0
- package/src/self/css-preprocessor.js +387 -0
- package/src/self/formatter.flux +93 -0
- package/src/self/formatter.js +114 -0
- package/src/self/jsx.flux +430 -0
- package/src/self/jsx.js +396 -0
- package/src/self/lexer.flux +529 -0
- package/src/self/lexer.js +709 -0
- package/src/self/lexer.stage2.js +700 -0
- package/src/self/linter.flux +515 -0
- package/src/self/linter.js +804 -0
- package/src/self/mangler.flux +253 -0
- package/src/self/mangler.js +348 -0
- package/src/self/parser.flux +1146 -0
- package/src/self/parser.js +1571 -0
- package/src/self/sourcemap.flux +66 -0
- package/src/self/sourcemap.js +72 -0
- package/src/self/stdlib.flux +356 -0
- package/src/self/stdlib.js +396 -0
- package/src/self/test-runner.flux +201 -0
- package/src/self/test-runner.js +132 -0
- package/src/self/transpiler.flux +123 -0
- package/src/self/transpiler.js +83 -0
- package/src/self/type-checker.flux +821 -0
- package/src/self/type-checker.js +1106 -0
- package/src/sourcemap.js +82 -0
- package/src/stdlib.js +436 -0
- package/src/test-runner.js +239 -0
- package/src/transpiler.js +172 -0
- 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 };
|