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.
- package/.github/workflows/ci.yml +66 -0
- package/README.md +1547 -0
- package/create-kimchi-app/README.md +44 -0
- package/create-kimchi-app/index.js +214 -0
- package/create-kimchi-app/package.json +22 -0
- package/editors/README.md +121 -0
- package/editors/sublime/KimchiLang.sublime-syntax +138 -0
- package/editors/vscode/README.md +90 -0
- package/editors/vscode/kimchilang-1.1.0.vsix +0 -0
- package/editors/vscode/language-configuration.json +37 -0
- package/editors/vscode/package.json +55 -0
- package/editors/vscode/src/extension.js +354 -0
- package/editors/vscode/syntaxes/kimchi.tmLanguage.json +215 -0
- package/examples/api/client.km +36 -0
- package/examples/async_pipe.km +58 -0
- package/examples/basic.kimchi +109 -0
- package/examples/cli_framework/README.md +92 -0
- package/examples/cli_framework/calculator.km +61 -0
- package/examples/cli_framework/deploy.km +126 -0
- package/examples/cli_framework/greeter.km +26 -0
- package/examples/config.static +27 -0
- package/examples/config.static.js +10 -0
- package/examples/env_test.km +37 -0
- package/examples/fibonacci.kimchi +17 -0
- package/examples/greeter.km +15 -0
- package/examples/hello.js +1 -0
- package/examples/hello.kimchi +3 -0
- package/examples/js_interop.km +42 -0
- package/examples/logger_example.km +34 -0
- package/examples/memo_fibonacci.km +17 -0
- package/examples/myapp/lib/http.js +14 -0
- package/examples/myapp/lib/http.km +16 -0
- package/examples/myapp/main.km +16 -0
- package/examples/myapp/main_with_mock.km +42 -0
- package/examples/myapp/services/api.js +18 -0
- package/examples/myapp/services/api.km +18 -0
- package/examples/new_features.kimchi +52 -0
- package/examples/project_example.static +20 -0
- package/examples/readme_examples.km +240 -0
- package/examples/reduce_pattern_match.km +85 -0
- package/examples/regex_match.km +46 -0
- package/examples/sample.js +45 -0
- package/examples/sample.km +39 -0
- package/examples/secrets.static +35 -0
- package/examples/secrets.static.js +30 -0
- package/examples/shell-example.mjs +144 -0
- package/examples/shell_example.km +19 -0
- package/examples/stdlib_test.km +22 -0
- package/examples/test_example.km +69 -0
- package/examples/testing/README.md +88 -0
- package/examples/testing/http_client.km +18 -0
- package/examples/testing/math.km +48 -0
- package/examples/testing/math.test.km +93 -0
- package/examples/testing/user_service.km +29 -0
- package/examples/testing/user_service.test.km +72 -0
- package/examples/use-config.mjs +141 -0
- package/examples/use_config.km +13 -0
- package/install.sh +59 -0
- package/package.json +29 -0
- package/pantry/acorn/index.km +1 -0
- package/pantry/is_number/index.km +1 -0
- package/pantry/is_odd/index.km +2 -0
- package/project.static +6 -0
- package/src/cli.js +1245 -0
- package/src/generator.js +1241 -0
- package/src/index.js +141 -0
- package/src/js2km.js +568 -0
- package/src/lexer.js +822 -0
- package/src/linter.js +810 -0
- package/src/package-manager.js +307 -0
- package/src/parser.js +1876 -0
- package/src/static-parser.js +500 -0
- package/src/typechecker.js +950 -0
- package/stdlib/array.km +0 -0
- package/stdlib/bitwise.km +38 -0
- package/stdlib/console.km +49 -0
- package/stdlib/date.km +97 -0
- package/stdlib/function.km +44 -0
- package/stdlib/http.km +197 -0
- package/stdlib/http.md +333 -0
- package/stdlib/index.km +26 -0
- package/stdlib/json.km +17 -0
- package/stdlib/logger.js +114 -0
- package/stdlib/logger.km +104 -0
- package/stdlib/math.km +120 -0
- package/stdlib/object.km +41 -0
- package/stdlib/promise.km +33 -0
- package/stdlib/string.km +93 -0
- package/stdlib/testing.md +265 -0
- 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
|
+
}
|