kimchilang 1.0.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 (90) hide show
  1. package/.github/workflows/ci.yml +66 -0
  2. package/README.md +1547 -0
  3. package/create-kimchi-app/README.md +44 -0
  4. package/create-kimchi-app/index.js +214 -0
  5. package/create-kimchi-app/package.json +22 -0
  6. package/editors/README.md +121 -0
  7. package/editors/sublime/KimchiLang.sublime-syntax +138 -0
  8. package/editors/vscode/README.md +90 -0
  9. package/editors/vscode/kimchilang-1.1.0.vsix +0 -0
  10. package/editors/vscode/language-configuration.json +37 -0
  11. package/editors/vscode/package.json +55 -0
  12. package/editors/vscode/src/extension.js +354 -0
  13. package/editors/vscode/syntaxes/kimchi.tmLanguage.json +215 -0
  14. package/examples/api/client.km +36 -0
  15. package/examples/async_pipe.km +58 -0
  16. package/examples/basic.kimchi +109 -0
  17. package/examples/cli_framework/README.md +92 -0
  18. package/examples/cli_framework/calculator.km +61 -0
  19. package/examples/cli_framework/deploy.km +126 -0
  20. package/examples/cli_framework/greeter.km +26 -0
  21. package/examples/config.static +27 -0
  22. package/examples/config.static.js +10 -0
  23. package/examples/env_test.km +37 -0
  24. package/examples/fibonacci.kimchi +17 -0
  25. package/examples/greeter.km +15 -0
  26. package/examples/hello.js +1 -0
  27. package/examples/hello.kimchi +3 -0
  28. package/examples/js_interop.km +42 -0
  29. package/examples/logger_example.km +34 -0
  30. package/examples/memo_fibonacci.km +17 -0
  31. package/examples/myapp/lib/http.js +14 -0
  32. package/examples/myapp/lib/http.km +16 -0
  33. package/examples/myapp/main.km +16 -0
  34. package/examples/myapp/main_with_mock.km +42 -0
  35. package/examples/myapp/services/api.js +18 -0
  36. package/examples/myapp/services/api.km +18 -0
  37. package/examples/new_features.kimchi +52 -0
  38. package/examples/project_example.static +20 -0
  39. package/examples/readme_examples.km +240 -0
  40. package/examples/reduce_pattern_match.km +85 -0
  41. package/examples/regex_match.km +46 -0
  42. package/examples/sample.js +45 -0
  43. package/examples/sample.km +39 -0
  44. package/examples/secrets.static +35 -0
  45. package/examples/secrets.static.js +30 -0
  46. package/examples/shell-example.mjs +144 -0
  47. package/examples/shell_example.km +19 -0
  48. package/examples/stdlib_test.km +22 -0
  49. package/examples/test_example.km +69 -0
  50. package/examples/testing/README.md +88 -0
  51. package/examples/testing/http_client.km +18 -0
  52. package/examples/testing/math.km +48 -0
  53. package/examples/testing/math.test.km +93 -0
  54. package/examples/testing/user_service.km +29 -0
  55. package/examples/testing/user_service.test.km +72 -0
  56. package/examples/use-config.mjs +141 -0
  57. package/examples/use_config.km +13 -0
  58. package/install.sh +59 -0
  59. package/package.json +29 -0
  60. package/pantry/acorn/index.km +1 -0
  61. package/pantry/is_number/index.km +1 -0
  62. package/pantry/is_odd/index.km +2 -0
  63. package/project.static +6 -0
  64. package/src/cli.js +1245 -0
  65. package/src/generator.js +1241 -0
  66. package/src/index.js +141 -0
  67. package/src/js2km.js +568 -0
  68. package/src/lexer.js +822 -0
  69. package/src/linter.js +810 -0
  70. package/src/package-manager.js +307 -0
  71. package/src/parser.js +1876 -0
  72. package/src/static-parser.js +500 -0
  73. package/src/typechecker.js +950 -0
  74. package/stdlib/array.km +0 -0
  75. package/stdlib/bitwise.km +38 -0
  76. package/stdlib/console.km +49 -0
  77. package/stdlib/date.km +97 -0
  78. package/stdlib/function.km +44 -0
  79. package/stdlib/http.km +197 -0
  80. package/stdlib/http.md +333 -0
  81. package/stdlib/index.km +26 -0
  82. package/stdlib/json.km +17 -0
  83. package/stdlib/logger.js +114 -0
  84. package/stdlib/logger.km +104 -0
  85. package/stdlib/math.km +120 -0
  86. package/stdlib/object.km +41 -0
  87. package/stdlib/promise.km +33 -0
  88. package/stdlib/string.km +93 -0
  89. package/stdlib/testing.md +265 -0
  90. package/test/test.js +599 -0
package/src/linter.js ADDED
@@ -0,0 +1,810 @@
1
+ // KimchiLang Linter - Compile-time code quality checks
2
+ import { NodeType } from './parser.js';
3
+
4
+ // Lint rule severity levels
5
+ export const Severity = {
6
+ Error: 'error',
7
+ Warning: 'warning',
8
+ Info: 'info',
9
+ };
10
+
11
+ class LintMessage {
12
+ constructor(rule, message, severity, node, line, column) {
13
+ this.rule = rule;
14
+ this.message = message;
15
+ this.severity = severity;
16
+ this.node = node;
17
+ this.line = line || (node && node.line) || 0;
18
+ this.column = column || (node && node.column) || 0;
19
+ }
20
+
21
+ toString() {
22
+ const prefix = this.severity === Severity.Error ? '❌' :
23
+ this.severity === Severity.Warning ? '⚠️' : 'ℹ️';
24
+ const location = this.line ? ` at line ${this.line}` : '';
25
+ return `${prefix} [${this.rule}]${location}: ${this.message}`;
26
+ }
27
+ }
28
+
29
+ export class Linter {
30
+ constructor(options = {}) {
31
+ this.options = {
32
+ // Enable/disable specific rules
33
+ rules: {
34
+ 'unused-variable': true,
35
+ 'unused-function': true,
36
+ 'unreachable-code': true,
37
+ 'empty-block': true,
38
+ 'constant-condition': true,
39
+ 'duplicate-key': true,
40
+ 'shadow-variable': true,
41
+ 'missing-return': false, // Can be noisy
42
+ 'no-console': false, // print is common in Kimchi
43
+ // Formatting rules
44
+ 'indent': true, // Enforce 2-space indentation
45
+ 'no-tabs': true, // Disallow tabs
46
+ 'no-trailing-spaces': true, // Disallow trailing whitespace
47
+ 'max-line-length': false, // Disabled by default (set to number to enable)
48
+ 'newline-after-function': true, // Require blank line after function declarations
49
+ 'no-multiple-empty-lines': true, // Max 1 consecutive empty line
50
+ ...options.rules,
51
+ },
52
+ // Severity overrides
53
+ severity: {
54
+ 'unused-variable': Severity.Warning,
55
+ 'unused-function': Severity.Warning,
56
+ 'unreachable-code': Severity.Warning,
57
+ 'empty-block': Severity.Info,
58
+ 'constant-condition': Severity.Warning,
59
+ 'duplicate-key': Severity.Error,
60
+ 'shadow-variable': Severity.Warning,
61
+ 'missing-return': Severity.Info,
62
+ // Formatting severities
63
+ 'indent': Severity.Warning,
64
+ 'no-tabs': Severity.Warning,
65
+ 'no-trailing-spaces': Severity.Warning,
66
+ 'max-line-length': Severity.Warning,
67
+ 'newline-after-function': Severity.Info,
68
+ 'no-multiple-empty-lines': Severity.Info,
69
+ ...options.severity,
70
+ },
71
+ // Formatting options
72
+ indentSize: options.indentSize || 2,
73
+ maxLineLength: options.maxLineLength || 120,
74
+ };
75
+
76
+ this.messages = [];
77
+ this.scopes = [];
78
+ this.currentFunction = null;
79
+ this.source = null;
80
+ this.lines = [];
81
+ }
82
+
83
+ lint(ast, source = null) {
84
+ this.messages = [];
85
+ this.scopes = [{ variables: new Map(), functions: new Map() }];
86
+ this.source = source;
87
+ this.lines = source ? source.split('\n') : [];
88
+
89
+ // Format checking pass (if source is provided)
90
+ if (source) {
91
+ this.checkFormatting();
92
+ }
93
+
94
+ // First pass: collect all declarations
95
+ this.collectDeclarations(ast);
96
+
97
+ // Second pass: analyze usage and detect issues
98
+ this.analyzeProgram(ast);
99
+
100
+ // Third pass: check for unused declarations
101
+ this.checkUnused();
102
+
103
+ return this.messages;
104
+ }
105
+
106
+ // Formatting checks
107
+ checkFormatting() {
108
+ let consecutiveEmptyLines = 0;
109
+ let inBlockDepth = 0;
110
+
111
+ for (let i = 0; i < this.lines.length; i++) {
112
+ const line = this.lines[i];
113
+ const lineNum = i + 1;
114
+
115
+ // Track block depth for indentation checking
116
+ const openBraces = (line.match(/\{/g) || []).length;
117
+ const closeBraces = (line.match(/\}/g) || []).length;
118
+
119
+ // Check for tabs
120
+ if (this.isRuleEnabled('no-tabs') && line.includes('\t')) {
121
+ this.addMessageAt('no-tabs', 'Tabs are not allowed, use spaces for indentation', lineNum, line.indexOf('\t') + 1);
122
+ }
123
+
124
+ // Check for trailing whitespace
125
+ if (this.isRuleEnabled('no-trailing-spaces') && /\s+$/.test(line) && line.trim().length > 0) {
126
+ this.addMessageAt('no-trailing-spaces', 'Trailing whitespace is not allowed', lineNum, line.length);
127
+ }
128
+
129
+ // Check max line length
130
+ const maxLen = this.options.maxLineLength;
131
+ if (this.options.rules['max-line-length'] && line.length > maxLen) {
132
+ this.addMessageAt('max-line-length', `Line exceeds maximum length of ${maxLen} characters (${line.length})`, lineNum, maxLen);
133
+ }
134
+
135
+ // Check indentation (only for non-empty lines)
136
+ if (this.isRuleEnabled('indent') && line.trim().length > 0) {
137
+ const leadingSpaces = line.match(/^( *)/)[1].length;
138
+ const indentSize = this.options.indentSize;
139
+
140
+ // Calculate expected indent based on previous context
141
+ // Lines starting with } should be at parent level
142
+ const startsWithClose = line.trim().startsWith('}');
143
+ const expectedDepth = startsWithClose ? Math.max(0, inBlockDepth - 1) : inBlockDepth;
144
+ const expectedIndent = expectedDepth * indentSize;
145
+
146
+ // Allow some flexibility - just check it's a multiple of indentSize
147
+ if (leadingSpaces % indentSize !== 0) {
148
+ this.addMessageAt('indent', `Indentation should be a multiple of ${indentSize} spaces (found ${leadingSpaces})`, lineNum, 1);
149
+ }
150
+ }
151
+
152
+ // Update block depth after processing the line
153
+ inBlockDepth += openBraces - closeBraces;
154
+ if (inBlockDepth < 0) inBlockDepth = 0;
155
+
156
+ // Check for multiple consecutive empty lines
157
+ if (line.trim().length === 0) {
158
+ consecutiveEmptyLines++;
159
+ if (this.isRuleEnabled('no-multiple-empty-lines') && consecutiveEmptyLines > 1) {
160
+ this.addMessageAt('no-multiple-empty-lines', 'Multiple consecutive empty lines are not allowed', lineNum, 1);
161
+ }
162
+ } else {
163
+ consecutiveEmptyLines = 0;
164
+ }
165
+ }
166
+
167
+ // Check for blank line after function declarations
168
+ if (this.isRuleEnabled('newline-after-function')) {
169
+ this.checkNewlineAfterFunctions();
170
+ }
171
+ }
172
+
173
+ checkNewlineAfterFunctions() {
174
+ // Find function declarations and check for blank line after closing brace
175
+ let inFunction = false;
176
+ let braceDepth = 0;
177
+ let functionStartLine = 0;
178
+
179
+ for (let i = 0; i < this.lines.length; i++) {
180
+ const line = this.lines[i];
181
+ const lineNum = i + 1;
182
+ const trimmed = line.trim();
183
+
184
+ // Detect function start
185
+ if (!inFunction && (trimmed.startsWith('fn ') || trimmed.startsWith('expose fn ') ||
186
+ trimmed.startsWith('async fn ') || trimmed.startsWith('memo ') ||
187
+ trimmed.startsWith('expose memo ') || trimmed.startsWith('async memo '))) {
188
+ inFunction = true;
189
+ functionStartLine = lineNum;
190
+ braceDepth = 0;
191
+ }
192
+
193
+ if (inFunction) {
194
+ const openBraces = (line.match(/\{/g) || []).length;
195
+ const closeBraces = (line.match(/\}/g) || []).length;
196
+ braceDepth += openBraces - closeBraces;
197
+
198
+ // Function ended
199
+ if (braceDepth <= 0 && line.includes('}')) {
200
+ inFunction = false;
201
+
202
+ // Check next non-empty line
203
+ let nextLineIdx = i + 1;
204
+ while (nextLineIdx < this.lines.length && this.lines[nextLineIdx].trim() === '') {
205
+ nextLineIdx++;
206
+ }
207
+
208
+ // If there's a next line and it's not EOF, check if there was a blank line
209
+ if (nextLineIdx < this.lines.length) {
210
+ const blankLinesBetween = nextLineIdx - i - 1;
211
+ if (blankLinesBetween === 0) {
212
+ // Only warn if the next line is not a closing brace or another function
213
+ const nextLine = this.lines[nextLineIdx].trim();
214
+ if (!nextLine.startsWith('}') && nextLine.length > 0) {
215
+ this.addMessageAt('newline-after-function', 'Expected blank line after function declaration', lineNum, 1);
216
+ }
217
+ }
218
+ }
219
+ }
220
+ }
221
+ }
222
+ }
223
+
224
+ addMessageAt(rule, message, line, column) {
225
+ if (!this.isRuleEnabled(rule)) return;
226
+ this.messages.push(new LintMessage(rule, message, this.getSeverity(rule), null, line, column));
227
+ }
228
+
229
+ // Scope management
230
+ pushScope() {
231
+ this.scopes.push({ variables: new Map(), functions: new Map() });
232
+ }
233
+
234
+ popScope() {
235
+ const scope = this.scopes.pop();
236
+ // Check for unused variables in this scope
237
+ if (this.isRuleEnabled('unused-variable')) {
238
+ for (const [name, info] of scope.variables) {
239
+ if (!info.used && !name.startsWith('_')) {
240
+ this.addMessage('unused-variable', `Variable '${name}' is declared but never used`, info.node);
241
+ }
242
+ }
243
+ }
244
+ return scope;
245
+ }
246
+
247
+ currentScope() {
248
+ return this.scopes[this.scopes.length - 1];
249
+ }
250
+
251
+ defineVariable(name, node) {
252
+ const scope = this.currentScope();
253
+
254
+ // Check for shadowing
255
+ if (this.isRuleEnabled('shadow-variable')) {
256
+ for (let i = this.scopes.length - 2; i >= 0; i--) {
257
+ if (this.scopes[i].variables.has(name)) {
258
+ this.addMessage('shadow-variable', `Variable '${name}' shadows a variable in an outer scope`, node);
259
+ break;
260
+ }
261
+ }
262
+ }
263
+
264
+ scope.variables.set(name, { node, used: false, assigned: true });
265
+ }
266
+
267
+ defineFunction(name, node) {
268
+ const scope = this.currentScope();
269
+ scope.functions.set(name, { node, used: false, called: false });
270
+ }
271
+
272
+ useVariable(name) {
273
+ for (let i = this.scopes.length - 1; i >= 0; i--) {
274
+ if (this.scopes[i].variables.has(name)) {
275
+ this.scopes[i].variables.get(name).used = true;
276
+ return true;
277
+ }
278
+ if (this.scopes[i].functions.has(name)) {
279
+ this.scopes[i].functions.get(name).used = true;
280
+ return true;
281
+ }
282
+ }
283
+ return false;
284
+ }
285
+
286
+ // Rule helpers
287
+ isRuleEnabled(rule) {
288
+ return this.options.rules[rule] !== false;
289
+ }
290
+
291
+ getSeverity(rule) {
292
+ return this.options.severity[rule] || Severity.Warning;
293
+ }
294
+
295
+ addMessage(rule, message, node) {
296
+ if (!this.isRuleEnabled(rule)) return;
297
+ this.messages.push(new LintMessage(rule, message, this.getSeverity(rule), node));
298
+ }
299
+
300
+ // First pass: collect declarations
301
+ collectDeclarations(ast) {
302
+ for (const stmt of ast.body) {
303
+ if (stmt.type === NodeType.FunctionDeclaration) {
304
+ this.defineFunction(stmt.name, stmt);
305
+ } else if (stmt.type === NodeType.DecDeclaration) {
306
+ if (stmt.destructuring) {
307
+ this.collectDestructuringNames(stmt.pattern, stmt);
308
+ } else {
309
+ this.defineVariable(stmt.name, stmt);
310
+ }
311
+ } else if (stmt.type === NodeType.EnumDeclaration) {
312
+ this.defineVariable(stmt.name, stmt);
313
+ } else if (stmt.type === NodeType.ArgDeclaration) {
314
+ this.defineVariable(stmt.name, stmt);
315
+ } else if (stmt.type === NodeType.EnvDeclaration) {
316
+ this.defineVariable(stmt.name, stmt);
317
+ }
318
+ }
319
+ }
320
+
321
+ collectDestructuringNames(pattern, node) {
322
+ if (pattern.type === NodeType.ObjectPattern) {
323
+ for (const prop of pattern.properties) {
324
+ this.defineVariable(prop.value, node);
325
+ }
326
+ } else if (pattern.type === NodeType.ArrayPattern) {
327
+ for (const elem of pattern.elements) {
328
+ if (elem) {
329
+ this.defineVariable(elem.name, node);
330
+ }
331
+ }
332
+ }
333
+ }
334
+
335
+ // Second pass: analyze program
336
+ analyzeProgram(ast) {
337
+ for (const stmt of ast.body) {
338
+ this.analyzeStatement(stmt);
339
+ }
340
+ }
341
+
342
+ analyzeStatement(node, inUnreachable = false) {
343
+ if (!node) return { returns: false, breaks: false };
344
+
345
+ switch (node.type) {
346
+ case NodeType.DecDeclaration:
347
+ this.analyzeExpression(node.init);
348
+ return { returns: false, breaks: false };
349
+
350
+ case NodeType.FunctionDeclaration:
351
+ return this.analyzeFunctionDeclaration(node);
352
+
353
+ case NodeType.IfStatement:
354
+ return this.analyzeIfStatement(node, inUnreachable);
355
+
356
+ case NodeType.WhileStatement:
357
+ return this.analyzeWhileStatement(node);
358
+
359
+ case NodeType.ForInStatement:
360
+ return this.analyzeForInStatement(node);
361
+
362
+ case NodeType.ReturnStatement:
363
+ if (node.argument) {
364
+ this.analyzeExpression(node.argument);
365
+ }
366
+ return { returns: true, breaks: false };
367
+
368
+ case NodeType.BreakStatement:
369
+ return { returns: false, breaks: true };
370
+
371
+ case NodeType.ContinueStatement:
372
+ return { returns: false, breaks: false };
373
+
374
+ case NodeType.TryStatement:
375
+ return this.analyzeTryStatement(node);
376
+
377
+ case NodeType.ThrowStatement:
378
+ this.analyzeExpression(node.argument);
379
+ return { returns: true, breaks: false }; // throw is like return for control flow
380
+
381
+ case NodeType.PatternMatch:
382
+ return this.analyzePatternMatch(node);
383
+
384
+ case NodeType.PrintStatement:
385
+ this.analyzeExpression(node.argument || node.expression);
386
+ return { returns: false, breaks: false };
387
+
388
+ case NodeType.ExpressionStatement:
389
+ this.analyzeExpression(node.expression);
390
+ return { returns: false, breaks: false };
391
+
392
+ case NodeType.BlockStatement:
393
+ return this.analyzeBlock(node);
394
+
395
+ case NodeType.DepStatement:
396
+ if (node.overrides) {
397
+ this.analyzeExpression(node.overrides);
398
+ }
399
+ return { returns: false, breaks: false };
400
+
401
+ default:
402
+ return { returns: false, breaks: false };
403
+ }
404
+ }
405
+
406
+ analyzeBlock(node, checkEmpty = true) {
407
+ if (!node.body || node.body.length === 0) {
408
+ if (checkEmpty && this.isRuleEnabled('empty-block')) {
409
+ this.addMessage('empty-block', 'Empty block statement', node);
410
+ }
411
+ return { returns: false, breaks: false };
412
+ }
413
+
414
+ let hasUnreachable = false;
415
+ let result = { returns: false, breaks: false };
416
+
417
+ for (let i = 0; i < node.body.length; i++) {
418
+ const stmt = node.body[i];
419
+
420
+ if (hasUnreachable && this.isRuleEnabled('unreachable-code')) {
421
+ this.addMessage('unreachable-code', 'Unreachable code detected', stmt);
422
+ }
423
+
424
+ const stmtResult = this.analyzeStatement(stmt, hasUnreachable);
425
+
426
+ if (stmtResult.returns || stmtResult.breaks) {
427
+ hasUnreachable = true;
428
+ result = stmtResult;
429
+ }
430
+ }
431
+
432
+ return result;
433
+ }
434
+
435
+ analyzeFunctionDeclaration(node) {
436
+ const prevFunction = this.currentFunction;
437
+ this.currentFunction = node;
438
+
439
+ this.pushScope();
440
+
441
+ // Define parameters
442
+ for (const param of node.params) {
443
+ const name = param.name || param.argument;
444
+ if (name) {
445
+ this.defineVariable(name, param);
446
+ }
447
+ }
448
+
449
+ // Analyze body
450
+ if (node.body) {
451
+ this.analyzeBlock(node.body, false);
452
+ }
453
+
454
+ this.popScope();
455
+ this.currentFunction = prevFunction;
456
+
457
+ return { returns: false, breaks: false };
458
+ }
459
+
460
+ analyzeIfStatement(node, inUnreachable) {
461
+ this.analyzeExpression(node.test);
462
+
463
+ // Check for constant condition
464
+ if (this.isRuleEnabled('constant-condition')) {
465
+ const constValue = this.getConstantValue(node.test);
466
+ if (constValue !== null) {
467
+ this.addMessage('constant-condition',
468
+ `Condition is always ${constValue ? 'true' : 'false'}`, node);
469
+ }
470
+ }
471
+
472
+ this.pushScope();
473
+ const consequentResult = node.consequent ?
474
+ this.analyzeBlock(node.consequent) : { returns: false, breaks: false };
475
+ this.popScope();
476
+
477
+ let alternateResult = { returns: false, breaks: false };
478
+ if (node.alternate) {
479
+ this.pushScope();
480
+ if (node.alternate.type === NodeType.BlockStatement) {
481
+ alternateResult = this.analyzeBlock(node.alternate);
482
+ } else {
483
+ alternateResult = this.analyzeStatement(node.alternate);
484
+ }
485
+ this.popScope();
486
+ }
487
+
488
+ // Both branches must return/break for the if to return/break
489
+ return {
490
+ returns: consequentResult.returns && alternateResult.returns,
491
+ breaks: consequentResult.breaks && alternateResult.breaks,
492
+ };
493
+ }
494
+
495
+ analyzeWhileStatement(node) {
496
+ this.analyzeExpression(node.test);
497
+
498
+ // Check for constant condition
499
+ if (this.isRuleEnabled('constant-condition')) {
500
+ const constValue = this.getConstantValue(node.test);
501
+ if (constValue === true) {
502
+ this.addMessage('constant-condition',
503
+ 'Infinite loop: condition is always true', node);
504
+ } else if (constValue === false) {
505
+ this.addMessage('constant-condition',
506
+ 'Loop never executes: condition is always false', node);
507
+ }
508
+ }
509
+
510
+ this.pushScope();
511
+ if (node.body) {
512
+ this.analyzeBlock(node.body);
513
+ }
514
+ this.popScope();
515
+
516
+ return { returns: false, breaks: false };
517
+ }
518
+
519
+ analyzeForInStatement(node) {
520
+ this.analyzeExpression(node.iterable);
521
+
522
+ this.pushScope();
523
+ this.defineVariable(node.variable, node);
524
+
525
+ if (node.body) {
526
+ this.analyzeBlock(node.body);
527
+ }
528
+ this.popScope();
529
+
530
+ return { returns: false, breaks: false };
531
+ }
532
+
533
+ analyzeTryStatement(node) {
534
+ this.pushScope();
535
+ if (node.block) {
536
+ this.analyzeBlock(node.block);
537
+ }
538
+ this.popScope();
539
+
540
+ if (node.handler) {
541
+ this.pushScope();
542
+ if (node.handler.param) {
543
+ this.defineVariable(node.handler.param, node.handler);
544
+ }
545
+ if (node.handler.body) {
546
+ this.analyzeBlock(node.handler.body);
547
+ }
548
+ this.popScope();
549
+ }
550
+
551
+ if (node.finalizer) {
552
+ this.pushScope();
553
+ this.analyzeBlock(node.finalizer);
554
+ this.popScope();
555
+ }
556
+
557
+ return { returns: false, breaks: false };
558
+ }
559
+
560
+ analyzePatternMatch(node) {
561
+ let allReturn = true;
562
+
563
+ for (const matchCase of node.cases) {
564
+ this.analyzeExpression(matchCase.test);
565
+
566
+ // Check for constant condition
567
+ if (this.isRuleEnabled('constant-condition')) {
568
+ const constValue = this.getConstantValue(matchCase.test);
569
+ if (constValue === false) {
570
+ this.addMessage('constant-condition',
571
+ 'Pattern case condition is always false', matchCase);
572
+ }
573
+ }
574
+
575
+ this.pushScope();
576
+ let caseResult;
577
+ if (matchCase.consequent.type === NodeType.BlockStatement) {
578
+ caseResult = this.analyzeBlock(matchCase.consequent);
579
+ } else {
580
+ caseResult = this.analyzeStatement(matchCase.consequent);
581
+ }
582
+ this.popScope();
583
+
584
+ if (!caseResult.returns) {
585
+ allReturn = false;
586
+ }
587
+ }
588
+
589
+ return { returns: allReturn, breaks: false };
590
+ }
591
+
592
+ analyzeExpression(node) {
593
+ if (!node) return;
594
+
595
+ switch (node.type) {
596
+ case NodeType.Identifier:
597
+ this.useVariable(node.name);
598
+ break;
599
+
600
+ case NodeType.BinaryExpression:
601
+ case NodeType.LogicalExpression:
602
+ this.analyzeExpression(node.left);
603
+ this.analyzeExpression(node.right);
604
+ break;
605
+
606
+ case NodeType.UnaryExpression:
607
+ this.analyzeExpression(node.argument);
608
+ break;
609
+
610
+ case NodeType.CallExpression:
611
+ this.analyzeExpression(node.callee);
612
+ for (const arg of node.arguments || []) {
613
+ this.analyzeExpression(arg);
614
+ }
615
+ break;
616
+
617
+ case NodeType.MemberExpression:
618
+ this.analyzeExpression(node.object);
619
+ if (node.computed) {
620
+ this.analyzeExpression(node.property);
621
+ }
622
+ break;
623
+
624
+ case NodeType.ArrayExpression:
625
+ for (const elem of node.elements || []) {
626
+ this.analyzeExpression(elem);
627
+ }
628
+ break;
629
+
630
+ case NodeType.ObjectExpression:
631
+ this.analyzeObjectExpression(node);
632
+ break;
633
+
634
+ case NodeType.ArrowFunctionExpression:
635
+ this.analyzeArrowFunction(node);
636
+ break;
637
+
638
+ case NodeType.ConditionalExpression:
639
+ this.analyzeExpression(node.test);
640
+ this.analyzeExpression(node.consequent);
641
+ this.analyzeExpression(node.alternate);
642
+ break;
643
+
644
+ case NodeType.AssignmentExpression:
645
+ this.analyzeExpression(node.left);
646
+ this.analyzeExpression(node.right);
647
+ break;
648
+
649
+ case NodeType.AwaitExpression:
650
+ this.analyzeExpression(node.argument);
651
+ break;
652
+
653
+ case NodeType.SpreadElement:
654
+ this.analyzeExpression(node.argument);
655
+ break;
656
+
657
+ case NodeType.RangeExpression:
658
+ this.analyzeExpression(node.start);
659
+ this.analyzeExpression(node.end);
660
+ break;
661
+
662
+ case NodeType.TemplateExpression:
663
+ for (const expr of node.expressions || []) {
664
+ this.analyzeExpression(expr);
665
+ }
666
+ break;
667
+ }
668
+ }
669
+
670
+ analyzeObjectExpression(node) {
671
+ const keys = new Set();
672
+
673
+ for (const prop of node.properties || []) {
674
+ if (prop.type === NodeType.SpreadElement) {
675
+ this.analyzeExpression(prop.argument);
676
+ continue;
677
+ }
678
+
679
+ // Check for duplicate keys
680
+ let keyName;
681
+ if (typeof prop.key === 'string') {
682
+ keyName = prop.key;
683
+ } else if (prop.key && prop.key.name) {
684
+ keyName = prop.key.name;
685
+ } else if (prop.key && prop.key.value) {
686
+ keyName = prop.key.value;
687
+ }
688
+
689
+ if (keyName && this.isRuleEnabled('duplicate-key')) {
690
+ if (keys.has(keyName)) {
691
+ this.addMessage('duplicate-key',
692
+ `Duplicate key '${keyName}' in object literal`, prop);
693
+ }
694
+ keys.add(keyName);
695
+ }
696
+
697
+ this.analyzeExpression(prop.value);
698
+ }
699
+ }
700
+
701
+ analyzeArrowFunction(node) {
702
+ this.pushScope();
703
+
704
+ for (const param of node.params || []) {
705
+ const name = param.name || param.argument || param;
706
+ if (typeof name === 'string') {
707
+ this.defineVariable(name, param);
708
+ }
709
+ }
710
+
711
+ if (node.body) {
712
+ if (node.body.type === NodeType.BlockStatement) {
713
+ this.analyzeBlock(node.body);
714
+ } else {
715
+ this.analyzeExpression(node.body);
716
+ }
717
+ }
718
+
719
+ this.popScope();
720
+ }
721
+
722
+ // Third pass: check for unused top-level declarations
723
+ checkUnused() {
724
+ const topScope = this.scopes[0];
725
+
726
+ if (this.isRuleEnabled('unused-function')) {
727
+ for (const [name, info] of topScope.functions) {
728
+ // Skip exposed functions and _describe
729
+ if (info.node.exposed || name === '_describe' || name.startsWith('_')) {
730
+ continue;
731
+ }
732
+ if (!info.used) {
733
+ this.addMessage('unused-function',
734
+ `Function '${name}' is declared but never used`, info.node);
735
+ }
736
+ }
737
+ }
738
+
739
+ if (this.isRuleEnabled('unused-variable')) {
740
+ for (const [name, info] of topScope.variables) {
741
+ // Skip exposed variables and those starting with _
742
+ if (info.node && info.node.exposed) continue;
743
+ if (name.startsWith('_')) continue;
744
+
745
+ if (!info.used) {
746
+ this.addMessage('unused-variable',
747
+ `Variable '${name}' is declared but never used`, info.node);
748
+ }
749
+ }
750
+ }
751
+ }
752
+
753
+ // Helper: get constant value of expression if determinable
754
+ getConstantValue(node) {
755
+ if (!node) return null;
756
+
757
+ if (node.type === NodeType.Literal) {
758
+ if (node.value === true) return true;
759
+ if (node.value === false) return false;
760
+ if (node.value === 'true') return true;
761
+ if (node.value === 'false') return false;
762
+ }
763
+
764
+ if (node.type === NodeType.Identifier) {
765
+ if (node.name === 'true') return true;
766
+ if (node.name === 'false') return false;
767
+ }
768
+
769
+ return null;
770
+ }
771
+
772
+ // Format messages for output
773
+ formatMessages() {
774
+ const errors = this.messages.filter(m => m.severity === Severity.Error);
775
+ const warnings = this.messages.filter(m => m.severity === Severity.Warning);
776
+ const infos = this.messages.filter(m => m.severity === Severity.Info);
777
+
778
+ let output = '';
779
+
780
+ if (errors.length > 0) {
781
+ output += '\nErrors:\n';
782
+ output += errors.map(m => ' ' + m.toString()).join('\n');
783
+ }
784
+
785
+ if (warnings.length > 0) {
786
+ output += '\nWarnings:\n';
787
+ output += warnings.map(m => ' ' + m.toString()).join('\n');
788
+ }
789
+
790
+ if (infos.length > 0) {
791
+ output += '\nInfo:\n';
792
+ output += infos.map(m => ' ' + m.toString()).join('\n');
793
+ }
794
+
795
+ if (this.messages.length > 0) {
796
+ output += `\n\nTotal: ${errors.length} error(s), ${warnings.length} warning(s), ${infos.length} info\n`;
797
+ }
798
+
799
+ return output;
800
+ }
801
+
802
+ hasErrors() {
803
+ return this.messages.some(m => m.severity === Severity.Error);
804
+ }
805
+ }
806
+
807
+ export function lint(ast, options = {}) {
808
+ const linter = new Linter(options);
809
+ return linter.lint(ast);
810
+ }