mimo-lang 1.1.1 → 2.0.6
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/.gitattributes +24 -0
- package/LICENSE +21 -0
- package/README.md +71 -39
- package/adapters/browserAdapter.js +86 -0
- package/adapters/nodeAdapter.js +101 -0
- package/bin/cli.js +80 -0
- package/bin/commands/convert.js +27 -0
- package/bin/commands/doctor.js +139 -0
- package/bin/commands/eval.js +39 -0
- package/bin/commands/fmt.js +109 -0
- package/bin/commands/help.js +72 -0
- package/bin/commands/lint.js +117 -0
- package/bin/commands/repl.js +24 -0
- package/bin/commands/run.js +64 -0
- package/bin/commands/test.js +126 -0
- package/bin/utils/colors.js +38 -0
- package/bin/utils/formatError.js +47 -0
- package/bin/utils/fs.js +57 -0
- package/bin/utils/version.js +8 -0
- package/build.js +18 -0
- package/bun.lock +74 -0
- package/index.js +48 -77
- package/index.web.js +364 -0
- package/interpreter/BuiltinFunction.js +32 -0
- package/interpreter/ErrorHandler.js +120 -0
- package/interpreter/ExpressionEvaluator.js +106 -0
- package/interpreter/Interpreter.js +172 -0
- package/interpreter/MimoError.js +112 -0
- package/interpreter/ModuleLoader.js +236 -0
- package/interpreter/StatementExecutor.js +107 -0
- package/interpreter/Utils.js +82 -0
- package/interpreter/Values.js +87 -0
- package/interpreter/coreBuiltins.js +490 -0
- package/interpreter/environment.js +99 -0
- package/interpreter/evaluators/binaryExpressionEvaluator.js +111 -0
- package/interpreter/evaluators/collectionEvaluator.js +151 -0
- package/interpreter/evaluators/functionCallEvaluator.js +76 -0
- package/interpreter/evaluators/literalEvaluator.js +27 -0
- package/interpreter/evaluators/moduleAccessEvaluator.js +25 -0
- package/interpreter/evaluators/templateLiteralEvaluator.js +20 -0
- package/interpreter/executors/BaseExecutor.js +37 -0
- package/interpreter/executors/ControlFlowExecutor.js +206 -0
- package/interpreter/executors/FunctionExecutor.js +126 -0
- package/interpreter/executors/PatternMatchExecutor.js +93 -0
- package/interpreter/executors/VariableExecutor.js +144 -0
- package/interpreter/index.js +8 -0
- package/interpreter/stdlib/array/accessFunctions.js +61 -0
- package/interpreter/stdlib/array/arrayUtils.js +36 -0
- package/interpreter/stdlib/array/higherOrderFunctions.js +285 -0
- package/interpreter/stdlib/array/searchFunctions.js +77 -0
- package/interpreter/stdlib/array/setFunctions.js +49 -0
- package/interpreter/stdlib/array/transformationFunctions.js +68 -0
- package/interpreter/stdlib/array.js +85 -0
- package/interpreter/stdlib/assert.js +143 -0
- package/interpreter/stdlib/datetime.js +170 -0
- package/interpreter/stdlib/env.js +54 -0
- package/interpreter/stdlib/fs.js +161 -0
- package/interpreter/stdlib/http.js +92 -0
- package/interpreter/stdlib/json.js +70 -0
- package/interpreter/stdlib/math.js +309 -0
- package/interpreter/stdlib/object.js +142 -0
- package/interpreter/stdlib/path.js +69 -0
- package/interpreter/stdlib/regex.js +134 -0
- package/interpreter/stdlib/string.js +260 -0
- package/interpreter/suggestions.js +46 -0
- package/lexer/Lexer.js +245 -0
- package/lexer/TokenTypes.js +131 -0
- package/lexer/createToken.js +11 -0
- package/lexer/tokenizers/commentTokenizer.js +45 -0
- package/lexer/tokenizers/literalTokenizer.js +163 -0
- package/lexer/tokenizers/symbolTokenizer.js +69 -0
- package/lexer/tokenizers/whitespaceTokenizer.js +36 -0
- package/package.json +29 -13
- package/parser/ASTNodes.js +448 -0
- package/parser/Parser.js +188 -0
- package/parser/expressions/atomicExpressions.js +165 -0
- package/parser/expressions/conditionalExpressions.js +0 -0
- package/parser/expressions/operatorExpressions.js +79 -0
- package/parser/expressions/primaryExpressions.js +77 -0
- package/parser/parseStatement.js +184 -0
- package/parser/parserExpressions.js +115 -0
- package/parser/parserUtils.js +19 -0
- package/parser/statements/controlFlowParsers.js +106 -0
- package/parser/statements/functionParsers.js +314 -0
- package/parser/statements/moduleParsers.js +57 -0
- package/parser/statements/patternMatchParsers.js +124 -0
- package/parser/statements/variableParsers.js +155 -0
- package/repl.js +325 -0
- package/test.js +47 -0
- package/tools/PrettyPrinter.js +3 -0
- package/tools/convert/Args.js +46 -0
- package/tools/convert/Registry.js +91 -0
- package/tools/convert/Transpiler.js +78 -0
- package/tools/convert/plugins/README.md +66 -0
- package/tools/convert/plugins/alya/index.js +10 -0
- package/tools/convert/plugins/alya/to_alya.js +289 -0
- package/tools/convert/plugins/alya/visitors/expressions.js +257 -0
- package/tools/convert/plugins/alya/visitors/statements.js +403 -0
- package/tools/convert/plugins/base_converter.js +228 -0
- package/tools/convert/plugins/javascript/index.js +10 -0
- package/tools/convert/plugins/javascript/mimo_runtime.js +265 -0
- package/tools/convert/plugins/javascript/to_js.js +155 -0
- package/tools/convert/plugins/javascript/visitors/expressions.js +197 -0
- package/tools/convert/plugins/javascript/visitors/patterns.js +102 -0
- package/tools/convert/plugins/javascript/visitors/statements.js +236 -0
- package/tools/convert/plugins/python/index.js +10 -0
- package/tools/convert/plugins/python/mimo_runtime.py +811 -0
- package/tools/convert/plugins/python/to_py.js +329 -0
- package/tools/convert/plugins/python/visitors/expressions.js +272 -0
- package/tools/convert/plugins/python/visitors/patterns.js +100 -0
- package/tools/convert/plugins/python/visitors/statements.js +257 -0
- package/tools/convert.js +102 -0
- package/tools/format/CommentAttacher.js +190 -0
- package/tools/format/CommentLexer.js +152 -0
- package/tools/format/Printer.js +849 -0
- package/tools/format/config.js +107 -0
- package/tools/formatter.js +169 -0
- package/tools/lint/Linter.js +391 -0
- package/tools/lint/config.js +114 -0
- package/tools/lint/rules/consistent-return.js +62 -0
- package/tools/lint/rules/max-depth.js +56 -0
- package/tools/lint/rules/no-empty-function.js +45 -0
- package/tools/lint/rules/no-magic-numbers.js +46 -0
- package/tools/lint/rules/no-shadow.js +113 -0
- package/tools/lint/rules/no-unused-vars.js +26 -0
- package/tools/lint/rules/prefer-const.js +19 -0
- package/tools/linter.js +261 -0
- package/tools/replFormatter.js +93 -0
- package/tools/stamp-version.js +32 -0
- package/web/index.js +9 -0
- package/bun.lockb +0 -0
- package/cli.js +0 -84
- package/compiler/execute/interpreter.js +0 -68
- package/compiler/execute/interpreters/binary.js +0 -12
- package/compiler/execute/interpreters/call.js +0 -10
- package/compiler/execute/interpreters/if.js +0 -10
- package/compiler/execute/interpreters/try-catch.js +0 -10
- package/compiler/execute/interpreters/while.js +0 -8
- package/compiler/execute/utils/createfunction.js +0 -11
- package/compiler/execute/utils/evaluate.js +0 -20
- package/compiler/execute/utils/operate.js +0 -23
- package/compiler/lexer/processToken.js +0 -40
- package/compiler/lexer/tokenTypes.js +0 -4
- package/compiler/lexer/tokenizer.js +0 -74
- package/compiler/parser/expression/comparison.js +0 -18
- package/compiler/parser/expression/identifier.js +0 -29
- package/compiler/parser/expression/number.js +0 -10
- package/compiler/parser/expression/operator.js +0 -21
- package/compiler/parser/expression/punctuation.js +0 -31
- package/compiler/parser/expression/string.js +0 -6
- package/compiler/parser/parseExpression.js +0 -27
- package/compiler/parser/parseStatement.js +0 -34
- package/compiler/parser/parser.js +0 -45
- package/compiler/parser/statement/call.js +0 -26
- package/compiler/parser/statement/function.js +0 -29
- package/compiler/parser/statement/if.js +0 -34
- package/compiler/parser/statement/return.js +0 -10
- package/compiler/parser/statement/set.js +0 -11
- package/compiler/parser/statement/show.js +0 -10
- package/compiler/parser/statement/try-catch.js +0 -25
- package/compiler/parser/statement/while.js +0 -22
- package/converter/go/convert.js +0 -110
- package/converter/js/convert.js +0 -107
- package/jsconfig.json +0 -27
- package/vite.config.js +0 -17
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tools/lint/config.js
|
|
3
|
+
*
|
|
4
|
+
* Loads per-project lint configuration from a `.mimorc` file.
|
|
5
|
+
*
|
|
6
|
+
* File format (JSON):
|
|
7
|
+
* {
|
|
8
|
+
* "rules": {
|
|
9
|
+
* "no-unused-vars": true,
|
|
10
|
+
* "prefer-const": false,
|
|
11
|
+
* "no-magic-numbers": { "severity": "error", "allow": [0, 1, 2] },
|
|
12
|
+
* "max-depth": { "max": 4 }
|
|
13
|
+
* }
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* The resolved config is merged in this priority order (highest wins):
|
|
17
|
+
* CLI --rule flags > .mimorc > DEFAULT_RULES in linter.js
|
|
18
|
+
*
|
|
19
|
+
* loadConfig() is designed to be safe in non-Node environments (bundlers,
|
|
20
|
+
* playground): it accepts an optional `readFileFn` so callers can inject
|
|
21
|
+
* a filesystem reader. When no reader is provided it returns an empty
|
|
22
|
+
* config object (no file system access attempted).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Attempt to load and parse a `.mimorc` file located at `configPath`.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} configPath - Absolute path to the `.mimorc` file.
|
|
29
|
+
* @param {Function|null} readFileFn - A sync `(path) => string` reader,
|
|
30
|
+
* e.g. `(p) => fs.readFileSync(p, 'utf-8')`. Pass `null` or omit to
|
|
31
|
+
* skip file I/O (safe for bundled/browser environments).
|
|
32
|
+
* @returns {{ rules: Object }} Parsed config, or `{ rules: {} }` on any
|
|
33
|
+
* failure (missing file, bad JSON, wrong environment).
|
|
34
|
+
*/
|
|
35
|
+
export function loadConfig(configPath, readFileFn = null) {
|
|
36
|
+
if (typeof readFileFn !== 'function') {
|
|
37
|
+
return { rules: {} };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let raw;
|
|
41
|
+
try {
|
|
42
|
+
raw = readFileFn(configPath);
|
|
43
|
+
} catch {
|
|
44
|
+
// File does not exist or is unreadable — silently return empty config
|
|
45
|
+
return { rules: {} };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let parsed;
|
|
49
|
+
try {
|
|
50
|
+
parsed = JSON.parse(raw);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
throw new Error(`.mimorc: invalid JSON — ${e.message}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
56
|
+
throw new Error('.mimorc: root must be a JSON object');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const rules = parsed.rules;
|
|
60
|
+
if (rules !== undefined && (typeof rules !== 'object' || Array.isArray(rules))) {
|
|
61
|
+
throw new Error('.mimorc: "rules" must be an object');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { rules: rules || {} };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Merge rule configs in priority order.
|
|
69
|
+
* Later arguments take precedence over earlier ones.
|
|
70
|
+
*
|
|
71
|
+
* Each argument is a `{ ruleId: true | false | {...options} }` map.
|
|
72
|
+
*
|
|
73
|
+
* @param {...Object} layers
|
|
74
|
+
* @returns {Object}
|
|
75
|
+
*/
|
|
76
|
+
export function mergeRuleConfigs(...layers) {
|
|
77
|
+
const result = {};
|
|
78
|
+
for (const layer of layers) {
|
|
79
|
+
if (!layer || typeof layer !== 'object') continue;
|
|
80
|
+
for (const [ruleId, value] of Object.entries(layer)) {
|
|
81
|
+
result[ruleId] = value;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Find the `.mimorc` file by walking up from `startDir` to `rootDir`.
|
|
89
|
+
* Returns the path of the first `.mimorc` found, or `null`.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} startDir
|
|
92
|
+
* @param {string} rootDir
|
|
93
|
+
* @param {Function} readFileFn - Same sync reader as in loadConfig().
|
|
94
|
+
* @param {Function} pathJoinFn - `(a, b) => string` path joiner.
|
|
95
|
+
* @returns {string|null}
|
|
96
|
+
*/
|
|
97
|
+
export function findConfigFile(startDir, rootDir, readFileFn, pathJoinFn) {
|
|
98
|
+
if (typeof readFileFn !== 'function' || typeof pathJoinFn !== 'function') return null;
|
|
99
|
+
|
|
100
|
+
let dir = startDir;
|
|
101
|
+
while (true) {
|
|
102
|
+
const candidate = pathJoinFn(dir, '.mimorc');
|
|
103
|
+
try {
|
|
104
|
+
readFileFn(candidate);
|
|
105
|
+
return candidate; // readable → found
|
|
106
|
+
} catch {
|
|
107
|
+
// not here
|
|
108
|
+
}
|
|
109
|
+
const parent = pathJoinFn(dir, '..');
|
|
110
|
+
if (parent === dir || dir === rootDir) break;
|
|
111
|
+
dir = parent;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* consistent-return
|
|
3
|
+
*
|
|
4
|
+
* Requires that functions either always return a value, or never return a
|
|
5
|
+
* value. Mixing `return` (bare) and `return <expr>` in the same function
|
|
6
|
+
* is confusing and often a bug.
|
|
7
|
+
*
|
|
8
|
+
* Uses entry/exit listeners to track per-function state on a stack.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const consistentReturn = {
|
|
12
|
+
meta: {
|
|
13
|
+
type: 'problem',
|
|
14
|
+
description: 'Require consistent use of `return` in functions',
|
|
15
|
+
defaultSeverity: 'warning',
|
|
16
|
+
},
|
|
17
|
+
create: (context) => {
|
|
18
|
+
/**
|
|
19
|
+
* Stack of per-function frames.
|
|
20
|
+
* Each frame: { node, hasValueReturn: bool, hasVoidReturn: bool }
|
|
21
|
+
*/
|
|
22
|
+
const stack = [];
|
|
23
|
+
|
|
24
|
+
function enterFunction(node) {
|
|
25
|
+
stack.push({ node, hasValueReturn: false, hasVoidReturn: false });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function exitFunction() {
|
|
29
|
+
const frame = stack.pop();
|
|
30
|
+
if (!frame) return;
|
|
31
|
+
|
|
32
|
+
if (frame.hasValueReturn && frame.hasVoidReturn) {
|
|
33
|
+
const label = frame.node.name
|
|
34
|
+
? `Function '${frame.node.name}'`
|
|
35
|
+
: 'Anonymous function';
|
|
36
|
+
context.report({
|
|
37
|
+
node: frame.node,
|
|
38
|
+
message: `${label} inconsistently returns a value — some paths return a value and others do not.`,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
FunctionDeclaration: enterFunction,
|
|
45
|
+
'FunctionDeclaration:exit': exitFunction,
|
|
46
|
+
|
|
47
|
+
AnonymousFunction: enterFunction,
|
|
48
|
+
'AnonymousFunction:exit': exitFunction,
|
|
49
|
+
|
|
50
|
+
ReturnStatement: (node) => {
|
|
51
|
+
const frame = stack[stack.length - 1];
|
|
52
|
+
if (!frame) return;
|
|
53
|
+
|
|
54
|
+
if (node.argument !== null && node.argument !== undefined) {
|
|
55
|
+
frame.hasValueReturn = true;
|
|
56
|
+
} else {
|
|
57
|
+
frame.hasVoidReturn = true;
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* max-depth
|
|
3
|
+
*
|
|
4
|
+
* Enforces a maximum depth for nested block-creating constructs (if, while,
|
|
5
|
+
* for, loop, match/case, try). Helps prevent deeply-nested, hard-to-read code.
|
|
6
|
+
*
|
|
7
|
+
* Options:
|
|
8
|
+
* max {number} — maximum allowed nesting depth (default: 4)
|
|
9
|
+
*
|
|
10
|
+
* Example config:
|
|
11
|
+
* "max-depth": { "max": 3 }
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Node types that increase the nesting depth tracked by LinterScopeTracker. */
|
|
15
|
+
const BLOCK_TYPES = new Set([
|
|
16
|
+
'IfStatement',
|
|
17
|
+
'WhileStatement',
|
|
18
|
+
'ForStatement',
|
|
19
|
+
'LoopStatement',
|
|
20
|
+
'MatchStatement',
|
|
21
|
+
'CaseClause',
|
|
22
|
+
'TryStatement',
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
export const maxDepth = {
|
|
26
|
+
meta: {
|
|
27
|
+
type: 'suggestion',
|
|
28
|
+
description: 'Enforce a maximum depth for nested blocks',
|
|
29
|
+
defaultSeverity: 'warning',
|
|
30
|
+
},
|
|
31
|
+
create: (context) => {
|
|
32
|
+
const maxAllowed = typeof context.options?.max === 'number'
|
|
33
|
+
? context.options.max
|
|
34
|
+
: 4;
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
// Use a generic visitor that fires for all block-creating node types.
|
|
38
|
+
// We check the depth *after* enterNode has already incremented it,
|
|
39
|
+
// so the current depth equals the depth of this node.
|
|
40
|
+
...Object.fromEntries(
|
|
41
|
+
[...BLOCK_TYPES].map(nodeType => [
|
|
42
|
+
nodeType,
|
|
43
|
+
(node) => {
|
|
44
|
+
const depth = context.getScope().getCurrentDepth();
|
|
45
|
+
if (depth > maxAllowed) {
|
|
46
|
+
context.report({
|
|
47
|
+
node,
|
|
48
|
+
message: `Block nesting depth (${depth}) exceeds the maximum allowed (${maxAllowed}).`,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
])
|
|
53
|
+
),
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* no-empty-function
|
|
3
|
+
*
|
|
4
|
+
* Disallows function declarations and anonymous functions whose body contains
|
|
5
|
+
* no executable statements.
|
|
6
|
+
*
|
|
7
|
+
* An empty body is defined as `body` being an empty array, or an array that
|
|
8
|
+
* contains only comment-like nodes (nodes without a `type` property, e.g.
|
|
9
|
+
* null/undefined elements). A body with any typed AST node is not empty.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
function isEmptyBody(body) {
|
|
13
|
+
if (!Array.isArray(body) || body.length === 0) return true;
|
|
14
|
+
// Consider the body non-empty if at least one element is a real AST node.
|
|
15
|
+
return !body.some(item => item && typeof item === 'object' && item.type);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const noEmptyFunction = {
|
|
19
|
+
meta: {
|
|
20
|
+
type: 'suggestion',
|
|
21
|
+
description: 'Disallow empty function bodies',
|
|
22
|
+
defaultSeverity: 'warning',
|
|
23
|
+
},
|
|
24
|
+
create: (context) => {
|
|
25
|
+
return {
|
|
26
|
+
FunctionDeclaration: (node) => {
|
|
27
|
+
if (isEmptyBody(node.body)) {
|
|
28
|
+
context.report({
|
|
29
|
+
node,
|
|
30
|
+
message: `Function '${node.name}' has an empty body.`,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
AnonymousFunction: (node) => {
|
|
36
|
+
if (isEmptyBody(node.body)) {
|
|
37
|
+
context.report({
|
|
38
|
+
node,
|
|
39
|
+
message: 'Anonymous function has an empty body.',
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Default set of numbers that are commonly allowed (and not "magic").
|
|
2
|
+
// This can be extended via rule options: { allow: [0, 1, 2, ...] }
|
|
3
|
+
const DEFAULT_ALLOWED = [0, 1, -1, 10, 100, 1000];
|
|
4
|
+
|
|
5
|
+
export const noMagicNumbers = {
|
|
6
|
+
meta: {
|
|
7
|
+
type: 'suggestion',
|
|
8
|
+
description: 'Disallow magic numbers - prefer named constants',
|
|
9
|
+
defaultSeverity: 'warning',
|
|
10
|
+
},
|
|
11
|
+
create: (context) => {
|
|
12
|
+
// Merge default allowed list with any user-supplied values.
|
|
13
|
+
const allowList = Array.isArray(context.options?.allow)
|
|
14
|
+
? new Set([...DEFAULT_ALLOWED, ...context.options.allow])
|
|
15
|
+
: new Set(DEFAULT_ALLOWED);
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
// This listener will run for every Literal node the linter encounters.
|
|
19
|
+
Literal: (node) => {
|
|
20
|
+
// We only care about numeric literals.
|
|
21
|
+
if (typeof node.value !== 'number') {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Ignore numbers that are commonly used and not considered "magic".
|
|
26
|
+
if (allowList.has(node.value)) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Get the parent node to check the context.
|
|
31
|
+
const parent = context.getParent(node);
|
|
32
|
+
|
|
33
|
+
// Allow numbers if they are the direct value of a `const` declaration.
|
|
34
|
+
if (parent?.type === 'VariableDeclaration' && parent.kind === 'const') {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// If it's a "magic number", report it.
|
|
39
|
+
context.report({
|
|
40
|
+
node: node,
|
|
41
|
+
message: `Magic number '${node.value}' detected. Consider declaring it as a named constant.`,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* no-shadow
|
|
3
|
+
*
|
|
4
|
+
* Disallows variable or function declarations that shadow a binding from an
|
|
5
|
+
* outer scope. Shadowing makes code harder to reason about and can hide bugs.
|
|
6
|
+
*
|
|
7
|
+
* Checks:
|
|
8
|
+
* - `let` / `const` / `global` VariableDeclarations
|
|
9
|
+
* - FunctionDeclarations (the function name itself)
|
|
10
|
+
* - Function parameters (declared inside FunctionDeclaration / AnonymousFunction)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const noShadow = {
|
|
14
|
+
meta: {
|
|
15
|
+
type: 'problem',
|
|
16
|
+
description: 'Disallow variable declarations that shadow variables in outer scopes',
|
|
17
|
+
defaultSeverity: 'warning',
|
|
18
|
+
},
|
|
19
|
+
create: (context) => {
|
|
20
|
+
/**
|
|
21
|
+
* Check whether `name` already exists in any *ancestor* scope (not the
|
|
22
|
+
* current scope, since the declaration hasn't been committed yet when the
|
|
23
|
+
* node-entry listener fires for VariableDeclaration, but the scope tracker
|
|
24
|
+
* has already called enterNode which calls declare() — so we look for
|
|
25
|
+
* an owning scope that is an ancestor of the current scope).
|
|
26
|
+
*/
|
|
27
|
+
function isShadowing(name) {
|
|
28
|
+
const tracker = context.getScope();
|
|
29
|
+
const current = tracker.getCurrentScope();
|
|
30
|
+
|
|
31
|
+
// Walk up the parent chain looking for the binding.
|
|
32
|
+
let ancestor = current.parent;
|
|
33
|
+
while (ancestor) {
|
|
34
|
+
if (ancestor.variables.has(name)) return true;
|
|
35
|
+
ancestor = ancestor.parent;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
VariableDeclaration: (node) => {
|
|
42
|
+
if (typeof node.identifier !== 'string') return; // destructuring — skip
|
|
43
|
+
if (node.kind === 'set') return; // re-assignment, not a new binding
|
|
44
|
+
|
|
45
|
+
if (isShadowing(node.identifier)) {
|
|
46
|
+
context.report({
|
|
47
|
+
node,
|
|
48
|
+
message: `'${node.identifier}' shadows a variable from an outer scope.`,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
FunctionDeclaration: (node) => {
|
|
54
|
+
// The function name is declared in the parent scope, so we check
|
|
55
|
+
// whether any *grandparent-or-higher* scope owns the same name.
|
|
56
|
+
const tracker = context.getScope();
|
|
57
|
+
const current = tracker.getCurrentScope(); // already the new fn scope
|
|
58
|
+
|
|
59
|
+
// Function name lives in current.parent (the scope that surrounds the fn).
|
|
60
|
+
// We want to know if current.parent.parent or higher has the same name.
|
|
61
|
+
let ancestor = current.parent?.parent;
|
|
62
|
+
while (ancestor) {
|
|
63
|
+
if (ancestor.variables.has(node.name)) {
|
|
64
|
+
context.report({
|
|
65
|
+
node,
|
|
66
|
+
message: `Function '${node.name}' shadows a variable from an outer scope.`,
|
|
67
|
+
});
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
ancestor = ancestor.parent;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Also check parameters — they are declared in the function's own scope
|
|
74
|
+
// (current), so check current.parent and above.
|
|
75
|
+
const paramScope = current.parent;
|
|
76
|
+
(node.params || []).forEach(param => {
|
|
77
|
+
let a = paramScope;
|
|
78
|
+
while (a) {
|
|
79
|
+
if (a.variables.has(param.name)) {
|
|
80
|
+
context.report({
|
|
81
|
+
node: param,
|
|
82
|
+
message: `Parameter '${param.name}' shadows a variable from an outer scope.`,
|
|
83
|
+
});
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
a = a.parent;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
AnonymousFunction: (node) => {
|
|
92
|
+
// Parameters in the function's scope — check parent and above.
|
|
93
|
+
const tracker = context.getScope();
|
|
94
|
+
const current = tracker.getCurrentScope(); // the anonymous fn scope
|
|
95
|
+
const paramScope = current.parent;
|
|
96
|
+
|
|
97
|
+
(node.params || []).forEach(param => {
|
|
98
|
+
let a = paramScope;
|
|
99
|
+
while (a) {
|
|
100
|
+
if (a.variables.has(param.name)) {
|
|
101
|
+
context.report({
|
|
102
|
+
node: param,
|
|
103
|
+
message: `Parameter '${param.name}' shadows a variable from an outer scope.`,
|
|
104
|
+
});
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
a = a.parent;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const noUnusedVars = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'suggestion',
|
|
4
|
+
description: 'Disallow unused variables and function parameters',
|
|
5
|
+
},
|
|
6
|
+
create: (context) => {
|
|
7
|
+
return {
|
|
8
|
+
// This runs once after the entire file has been traversed.
|
|
9
|
+
Program_exit: (programNode) => {
|
|
10
|
+
const unused = context.getScope().getUnusedVariables();
|
|
11
|
+
unused.forEach(variable => {
|
|
12
|
+
let message;
|
|
13
|
+
if (variable.kind === 'parameter') {
|
|
14
|
+
message = `Parameter '${variable.name}' is defined but never used.`;
|
|
15
|
+
} else {
|
|
16
|
+
message = `Variable '${variable.name}' is defined but never used.`;
|
|
17
|
+
}
|
|
18
|
+
context.report({
|
|
19
|
+
node: variable.node,
|
|
20
|
+
message: message,
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const preferConst = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'suggestion',
|
|
4
|
+
description: 'Suggest using `const` for variables that are never reassigned.',
|
|
5
|
+
},
|
|
6
|
+
create: (context) => {
|
|
7
|
+
return {
|
|
8
|
+
Program_exit: (programNode) => {
|
|
9
|
+
const unmutatedLets = context.getScope().getUnmutatedLets();
|
|
10
|
+
unmutatedLets.forEach(variable => {
|
|
11
|
+
context.report({
|
|
12
|
+
node: variable.node,
|
|
13
|
+
message: `Variable '${variable.name}' is never reassigned. Use 'const' instead.`,
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
};
|