@atlisp/lint 0.1.16 → 0.1.19
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/README.md +3 -4
- package/atlisp-lint.default.json +12 -5
- package/dist/checks/bare-names.js +2 -0
- package/dist/checks/constant-condition.js +3 -0
- package/dist/checks/empty-catch.js +68 -23
- package/dist/checks/entget-in-loop.d.ts +3 -0
- package/dist/checks/entget-in-loop.js +56 -0
- package/dist/checks/if-without-else.d.ts +4 -0
- package/dist/checks/if-without-else.js +31 -0
- package/dist/checks/long-function-call.js +1 -1
- package/dist/checks/promise-handler.d.ts +4 -0
- package/dist/checks/promise-handler.js +53 -0
- package/dist/checks/redundant-list.d.ts +4 -0
- package/dist/checks/redundant-list.js +22 -0
- package/dist/checks/string-concat-loop.d.ts +3 -0
- package/dist/checks/string-concat-loop.js +59 -0
- package/dist/checks/undeclared-setq.js +2 -0
- package/dist/checks/unused-catch-result.d.ts +3 -0
- package/dist/checks/unused-catch-result.js +28 -0
- package/dist/config.js +11 -4
- package/dist/formatters-html.d.ts +3 -0
- package/dist/formatters-html.js +91 -0
- package/dist/formatters-sarif.d.ts +3 -0
- package/dist/formatters-sarif.js +55 -0
- package/dist/index.js +41 -2
- package/dist/locale.js +23 -7
- package/dist/presets.js +16 -2
- package/dist/project.js +180 -2
- package/dist/rules.js +8 -1
- package/dist/runner.d.ts +1 -2
- package/dist/runner.js +106 -224
- package/dist/validate.js +8 -1
- package/dist/visitor-runner.d.ts +4 -0
- package/dist/visitor-runner.js +709 -0
- package/package.json +1 -1
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatSarif = formatSarif;
|
|
4
|
+
function formatSarif(issues, errorCount, warningCount) {
|
|
5
|
+
const results = issues.map(iss => {
|
|
6
|
+
const level = iss.severity === 'error' ? 'error' : 'warning';
|
|
7
|
+
return {
|
|
8
|
+
level,
|
|
9
|
+
message: {
|
|
10
|
+
text: iss.message,
|
|
11
|
+
},
|
|
12
|
+
locations: [
|
|
13
|
+
{
|
|
14
|
+
physicalLocation: {
|
|
15
|
+
artifactLocation: {
|
|
16
|
+
uri: iss.file,
|
|
17
|
+
},
|
|
18
|
+
region: {
|
|
19
|
+
startLine: iss.line || 1,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
ruleId: iss.rule,
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
const sarif = {
|
|
28
|
+
$schema: 'https://json.schemastore.org/sarif-2.1.0.json',
|
|
29
|
+
version: '2.1.0',
|
|
30
|
+
runs: [
|
|
31
|
+
{
|
|
32
|
+
tool: {
|
|
33
|
+
driver: {
|
|
34
|
+
name: '@atlisp/lint',
|
|
35
|
+
version: '0.1.18',
|
|
36
|
+
informationUri: 'https://github.com/atlisp/lint',
|
|
37
|
+
rules: issues.map(iss => ({
|
|
38
|
+
id: iss.rule,
|
|
39
|
+
name: iss.rule,
|
|
40
|
+
shortDescription: { text: iss.message },
|
|
41
|
+
properties: { severity: iss.severity },
|
|
42
|
+
})),
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
results,
|
|
46
|
+
summary: {
|
|
47
|
+
errorCount,
|
|
48
|
+
warningCount,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
return JSON.stringify(sarif, null, 2);
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=formatters-sarif.js.map
|
package/dist/index.js
CHANGED
|
@@ -42,6 +42,8 @@ const runner_1 = require("./runner");
|
|
|
42
42
|
const project_1 = require("./project");
|
|
43
43
|
const sbcl_1 = require("./sbcl");
|
|
44
44
|
const formatters_1 = require("./formatters");
|
|
45
|
+
const formatters_sarif_1 = require("./formatters-sarif");
|
|
46
|
+
const formatters_html_1 = require("./formatters-html");
|
|
45
47
|
const formatter_1 = require("./formatter");
|
|
46
48
|
const locale_1 = require("./locale");
|
|
47
49
|
const cache_1 = require("./cache");
|
|
@@ -52,6 +54,8 @@ function parseArgs() {
|
|
|
52
54
|
src: [],
|
|
53
55
|
test: [],
|
|
54
56
|
staged: false,
|
|
57
|
+
diff: false,
|
|
58
|
+
changed: '',
|
|
55
59
|
format: 'default',
|
|
56
60
|
init: false,
|
|
57
61
|
installHook: false,
|
|
@@ -82,6 +86,12 @@ function parseArgs() {
|
|
|
82
86
|
case '--staged':
|
|
83
87
|
opts.staged = true;
|
|
84
88
|
break;
|
|
89
|
+
case '--diff':
|
|
90
|
+
opts.diff = true;
|
|
91
|
+
break;
|
|
92
|
+
case '--changed':
|
|
93
|
+
opts.changed = argv[++i] || 'main';
|
|
94
|
+
break;
|
|
85
95
|
case '--format':
|
|
86
96
|
opts.format = argv[++i];
|
|
87
97
|
break;
|
|
@@ -163,6 +173,23 @@ function* walkFiles(dir, rootDir, regex) {
|
|
|
163
173
|
}
|
|
164
174
|
}
|
|
165
175
|
}
|
|
176
|
+
function collectChangedFiles(rootDir, baseBranch) {
|
|
177
|
+
try {
|
|
178
|
+
const output = (0, child_process_1.execSync)(`git diff --name-only --diff-filter=ACM ${baseBranch}`, {
|
|
179
|
+
cwd: rootDir,
|
|
180
|
+
encoding: 'utf-8',
|
|
181
|
+
});
|
|
182
|
+
return output
|
|
183
|
+
.split('\n')
|
|
184
|
+
.map(l => l.trim())
|
|
185
|
+
.filter(l => l.endsWith('.lsp'))
|
|
186
|
+
.map(l => path.resolve(rootDir, l))
|
|
187
|
+
.filter(f => fs.existsSync(f));
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
}
|
|
166
193
|
// Placeholder regex (dynamically constructed to avoid no-control-regex)
|
|
167
194
|
const GLOB_PLACEHOLDER = '\x00GLOB\x00';
|
|
168
195
|
const GLOB_RE = new RegExp(GLOB_PLACEHOLDER, 'g');
|
|
@@ -294,7 +321,13 @@ async function main() {
|
|
|
294
321
|
return;
|
|
295
322
|
}
|
|
296
323
|
// Collect files
|
|
297
|
-
const files = opts.staged
|
|
324
|
+
const files = opts.staged
|
|
325
|
+
? collectStagedFiles(rootDir)
|
|
326
|
+
: opts.diff
|
|
327
|
+
? collectChangedFiles(rootDir, 'HEAD')
|
|
328
|
+
: opts.changed
|
|
329
|
+
? collectChangedFiles(rootDir, opts.changed)
|
|
330
|
+
: collectFiles(rootDir, opts, config);
|
|
298
331
|
if (files.length === 0) {
|
|
299
332
|
console.log((0, locale_1.t)('index.no_files'));
|
|
300
333
|
return;
|
|
@@ -330,7 +363,7 @@ async function main() {
|
|
|
330
363
|
}
|
|
331
364
|
// --watch mode
|
|
332
365
|
if (opts.watch) {
|
|
333
|
-
(0, watch_1.watchFiles)(files, { rootDir, format: opts.format, config });
|
|
366
|
+
(0, watch_1.watchFiles)(files, { rootDir, format: (opts.format === 'sarif' || opts.format === 'html' ? 'default' : opts.format), config });
|
|
334
367
|
return;
|
|
335
368
|
}
|
|
336
369
|
// --fix mode: auto-fix before linting
|
|
@@ -478,6 +511,12 @@ async function main() {
|
|
|
478
511
|
if (opts.format === 'json') {
|
|
479
512
|
console.log((0, formatters_1.formatJson)(issues, errorCount, warningCount));
|
|
480
513
|
}
|
|
514
|
+
else if (opts.format === 'sarif') {
|
|
515
|
+
console.log((0, formatters_sarif_1.formatSarif)(issues, errorCount, warningCount));
|
|
516
|
+
}
|
|
517
|
+
else if (opts.format === 'html') {
|
|
518
|
+
console.log((0, formatters_html_1.formatHtml)(issues, errorCount, warningCount));
|
|
519
|
+
}
|
|
481
520
|
else {
|
|
482
521
|
console.log((0, formatters_1.formatDefault)(issues, fileContents));
|
|
483
522
|
}
|
package/dist/locale.js
CHANGED
|
@@ -38,8 +38,6 @@ const messages = {
|
|
|
38
38
|
unused_let_binding: "Let binding '{0}' is never used",
|
|
39
39
|
recursive_call: "Function '{0}' calls itself — possible infinite recursion",
|
|
40
40
|
variable_shadow: "Variable '{0}' is set with setq inside a let that binds the same name",
|
|
41
|
-
'redundant_cond.single': 'Single-clause {0} — simplify to (if ...)',
|
|
42
|
-
'redundant_cond.t_last': '{0} with T as last clause condition — use (if ...) else branch instead',
|
|
43
41
|
unused_local_fun: "Local function/variable '{0}' is defined in defun with / but never used",
|
|
44
42
|
multiple_setq: 'Consecutive (setq ...) forms — consider merging into a single (setq ...)',
|
|
45
43
|
redundant_quotes: "Redundant double quote — ''x can be simplified to 'x",
|
|
@@ -91,6 +89,14 @@ const messages = {
|
|
|
91
89
|
'index.watch_hint': 'Press Ctrl+C to stop',
|
|
92
90
|
'index.watch_status': '{0} files checked: {1} error(s), {2} warning(s)',
|
|
93
91
|
'index.format_unstable': 'Formatting is not idempotent — re-running formatCode produces different output',
|
|
92
|
+
unused_catch_result: 'vl-catch-all-apply result used as boolean — use vl-catch-all-error-p to check',
|
|
93
|
+
string_concat_loop: 'String concatenation inside {0} — move strcat outside the loop for performance',
|
|
94
|
+
if_without_else: 'if has no else branch and the return value is used — add nil else for clarity',
|
|
95
|
+
redundant_list: '(list) evaluates to nil — use nil directly',
|
|
96
|
+
entget_in_loop: 'entget inside loop — cache entget result before the loop for performance',
|
|
97
|
+
promise_handler: 'vlax-invoke/vlax-invoke-method without callback handler — results may be lost',
|
|
98
|
+
module_cycle: 'Module dependency cycle detected — may cause initialization issues',
|
|
99
|
+
arg_count_project: "Function '{0}' called with {1} arguments but defined with {2} (cross-file)",
|
|
94
100
|
'help.text': `@atlisp/lint - AutoLISP static analysis tool
|
|
95
101
|
|
|
96
102
|
Usage: atlisp-lint [options]
|
|
@@ -113,8 +119,10 @@ Options:
|
|
|
113
119
|
--hook-args <args> Custom pre-commit hook arguments (with --install-hook)
|
|
114
120
|
--help Show this help message
|
|
115
121
|
--version Show version number
|
|
116
|
-
--format-code
|
|
117
|
-
--format-check Check if files are formatted (exit 1 if not)
|
|
122
|
+
--format-code Auto-format code indentation
|
|
123
|
+
--format-check Check if files are formatted (exit 1 if not)
|
|
124
|
+
--diff Only check files changed since last commit
|
|
125
|
+
--changed <branch> Only check files changed against a branch (default: main)`,
|
|
118
126
|
},
|
|
119
127
|
zh: {
|
|
120
128
|
'parens.mismatch': "括号不匹配:{0} 个 '(' vs {1} 个 ')'({2})",
|
|
@@ -150,8 +158,6 @@ Options:
|
|
|
150
158
|
unused_let_binding: "let 绑定 '{0}' 从未被使用",
|
|
151
159
|
recursive_call: "函数 '{0}' 调用了自身——可能存在无限递归",
|
|
152
160
|
variable_shadow: "变量 '{0}' 在 let 绑定了相同名称后被 setq 赋值",
|
|
153
|
-
'redundant_cond.single': '单子句 {0} ——建议简化为 (if ...)',
|
|
154
|
-
'redundant_cond.t_last': '{0} 的最后一条子句条件为 T ——建议改用 (if ...) else 分支',
|
|
155
161
|
unused_local_fun: "局部函数/变量 '{0}' 在 defun 的 / 后定义但从未使用",
|
|
156
162
|
multiple_setq: '连续多个 (setq ...) ——建议合并为一个 (setq ...)',
|
|
157
163
|
redundant_quotes: "冗余双引号 —— ''x 可以简化为 'x",
|
|
@@ -203,6 +209,14 @@ Options:
|
|
|
203
209
|
'index.watch_hint': '按 Ctrl+C 停止',
|
|
204
210
|
'index.watch_status': '已检查 {0} 个文件: {1} 个错误, {2} 个警告',
|
|
205
211
|
'index.format_unstable': '格式化结果不稳定——再次格式化后输出不同',
|
|
212
|
+
unused_catch_result: 'vl-catch-all-apply 的结果被当作布尔值使用——应用 vl-catch-all-error-p 检查',
|
|
213
|
+
string_concat_loop: '循环 {0} 内使用了字符串拼接——为提升性能请将 strcat 移到循环外',
|
|
214
|
+
if_without_else: 'if 没有 else 分支且返回值被使用——建议添加 nil else 分支',
|
|
215
|
+
redundant_list: '(list) 等于 nil——请直接使用 nil',
|
|
216
|
+
entget_in_loop: '循环内使用了 entget——请在循环外缓存 entget 结果',
|
|
217
|
+
promise_handler: 'vlax-invoke/vlax-invoke-method 没有回调处理器——结果可能丢失',
|
|
218
|
+
module_cycle: '检测到模块循环依赖——可能导致初始化问题',
|
|
219
|
+
arg_count_project: "函数 '{0}' 调用时传入了 {1} 个参数,但定义为 {2} 个(跨文件)",
|
|
206
220
|
'help.text': `@atlisp/lint - AutoLISP 静态分析工具
|
|
207
221
|
|
|
208
222
|
用法: atlisp-lint [选项]
|
|
@@ -226,7 +240,9 @@ Options:
|
|
|
226
240
|
--help 显示此帮助信息
|
|
227
241
|
--version 显示版本号
|
|
228
242
|
--format-code 自动格式化代码缩进
|
|
229
|
-
--format-check 检查文件是否已格式化(未格式化则退出码为 1
|
|
243
|
+
--format-check 检查文件是否已格式化(未格式化则退出码为 1)
|
|
244
|
+
--diff 仅检查自上次提交后变更的文件
|
|
245
|
+
--changed <分支> 仅检查相对于某分支有变更的文件(默认:main)`,
|
|
230
246
|
},
|
|
231
247
|
};
|
|
232
248
|
let currentLocale = 'zh';
|
package/dist/presets.js
CHANGED
|
@@ -26,7 +26,6 @@ exports.PRESETS = {
|
|
|
26
26
|
unused_let_binding: 'warn',
|
|
27
27
|
recursive_call: 'warn',
|
|
28
28
|
variable_shadow: 'warn',
|
|
29
|
-
redundant_cond: 'warn',
|
|
30
29
|
unused_local_fun: 'warn',
|
|
31
30
|
multiple_setq: 'warn',
|
|
32
31
|
redundant_quotes: 'warn',
|
|
@@ -66,6 +65,14 @@ exports.PRESETS = {
|
|
|
66
65
|
shadow_builtin: 'warn',
|
|
67
66
|
dynamic_doc: 'warn',
|
|
68
67
|
redundant_if: 'warn',
|
|
68
|
+
unused_catch_result: 'warn',
|
|
69
|
+
string_concat_loop: 'warn',
|
|
70
|
+
if_without_else: 'warn',
|
|
71
|
+
redundant_list: 'warn',
|
|
72
|
+
entget_in_loop: 'warn',
|
|
73
|
+
promise_handler: 'warn',
|
|
74
|
+
module_cycle: 'warn',
|
|
75
|
+
arg_count_project: 'warn',
|
|
69
76
|
},
|
|
70
77
|
},
|
|
71
78
|
strict: {
|
|
@@ -94,7 +101,6 @@ exports.PRESETS = {
|
|
|
94
101
|
unused_let_binding: 'error',
|
|
95
102
|
recursive_call: 'error',
|
|
96
103
|
variable_shadow: 'error',
|
|
97
|
-
redundant_cond: 'error',
|
|
98
104
|
unused_local_fun: 'error',
|
|
99
105
|
multiple_setq: 'warn',
|
|
100
106
|
redundant_quotes: 'error',
|
|
@@ -145,6 +151,14 @@ exports.PRESETS = {
|
|
|
145
151
|
redundant_if: 'error',
|
|
146
152
|
module_registration: 'error',
|
|
147
153
|
namespace_header: 'error',
|
|
154
|
+
unused_catch_result: 'error',
|
|
155
|
+
string_concat_loop: 'warn',
|
|
156
|
+
if_without_else: 'error',
|
|
157
|
+
redundant_list: 'error',
|
|
158
|
+
entget_in_loop: 'warn',
|
|
159
|
+
promise_handler: 'error',
|
|
160
|
+
module_cycle: 'error',
|
|
161
|
+
arg_count_project: 'error',
|
|
148
162
|
},
|
|
149
163
|
},
|
|
150
164
|
relaxed: {
|
package/dist/project.js
CHANGED
|
@@ -54,7 +54,7 @@ function collectDefuns(file) {
|
|
|
54
54
|
}
|
|
55
55
|
return results;
|
|
56
56
|
}
|
|
57
|
-
function collectReferences(file
|
|
57
|
+
function collectReferences(file) {
|
|
58
58
|
const content = fs.readFileSync(file, 'utf-8');
|
|
59
59
|
const ast = (0, parser_1.parseAst)(content);
|
|
60
60
|
const results = [];
|
|
@@ -71,6 +71,93 @@ function collectReferences(file, _filepath) {
|
|
|
71
71
|
}
|
|
72
72
|
return results;
|
|
73
73
|
}
|
|
74
|
+
function countFunctionArgs(file) {
|
|
75
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
76
|
+
const ast = (0, parser_1.parseAst)(content);
|
|
77
|
+
const result = new Map();
|
|
78
|
+
const defunNodes = (0, parser_1.astFindAll)(ast, n => (0, parser_1.astIsList)(n, 'defun') || (0, parser_1.astIsList)(n, 'defun-q'));
|
|
79
|
+
for (const node of defunNodes) {
|
|
80
|
+
if (!node.children || node.children.length < 3)
|
|
81
|
+
continue;
|
|
82
|
+
const name = (0, parser_1.astIsSymbol)(node.children[1]) ? node.children[1].name : '';
|
|
83
|
+
if (!name)
|
|
84
|
+
continue;
|
|
85
|
+
const paramList = node.children[2];
|
|
86
|
+
if (paramList.type !== 'list' || !paramList.children) {
|
|
87
|
+
result.set(name, 0);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
let count = 0;
|
|
91
|
+
for (const child of paramList.children) {
|
|
92
|
+
if (child.type === 'symbol' && child.name && child.name !== '/') {
|
|
93
|
+
count++;
|
|
94
|
+
}
|
|
95
|
+
else if (child.type === 'symbol' && child.name === '/') {
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
result.set(name, count);
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
function countCallArgs(file) {
|
|
104
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
105
|
+
const ast = (0, parser_1.parseAst)(content);
|
|
106
|
+
const result = new Map();
|
|
107
|
+
const allCalls = (0, parser_1.astFindAll)(ast, n => {
|
|
108
|
+
if (n.type !== 'list' || !n.children || n.children.length === 0)
|
|
109
|
+
return false;
|
|
110
|
+
return (0, parser_1.astIsSymbol)(n.children[0]);
|
|
111
|
+
});
|
|
112
|
+
for (const call of allCalls) {
|
|
113
|
+
const name = call.children[0].name;
|
|
114
|
+
if (name === 'defun' || name === 'defun-q' || name.startsWith('c:') || name.includes(':'))
|
|
115
|
+
continue;
|
|
116
|
+
if (!result.has(name))
|
|
117
|
+
result.set(name, []);
|
|
118
|
+
const argCount = call.children ? call.children.length - 1 : 0;
|
|
119
|
+
result.get(name).push({ line: call.pos.line, count: argCount });
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
function findModuleDeps(filepath) {
|
|
124
|
+
const content = fs.readFileSync(filepath, 'utf-8');
|
|
125
|
+
const ast = (0, parser_1.parseAst)(content);
|
|
126
|
+
const imports = [];
|
|
127
|
+
const exports = [];
|
|
128
|
+
const inPackageNodes = (0, parser_1.astFindAll)(ast, n => (0, parser_1.astIsList)(n, 'in-package'));
|
|
129
|
+
for (const node of inPackageNodes) {
|
|
130
|
+
if (node.children && node.children.length >= 3 && node.children[1].type === 'symbol') {
|
|
131
|
+
const useList = node.children[2];
|
|
132
|
+
if (useList.type === 'list' && useList.children) {
|
|
133
|
+
for (const use of useList.children) {
|
|
134
|
+
if (use.type === 'symbol' && use.name)
|
|
135
|
+
imports.push(use.name);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const setqNodes = (0, parser_1.astFindAll)(ast, n => (0, parser_1.astIsList)(n, 'setq'));
|
|
141
|
+
for (const node of setqNodes) {
|
|
142
|
+
if (node.children && node.children.length >= 3 &&
|
|
143
|
+
node.children[1].type === 'symbol' && node.children[1].name === '@::*modules*' &&
|
|
144
|
+
node.children[2].type === 'list' && node.children[2].children) {
|
|
145
|
+
for (const exp of node.children[2].children) {
|
|
146
|
+
if (exp.type === 'string' && typeof exp.value === 'string') {
|
|
147
|
+
exports.push(exp.value);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return { imports, exports };
|
|
153
|
+
}
|
|
154
|
+
function collectAllModuleDeps(files) {
|
|
155
|
+
const deps = new Map();
|
|
156
|
+
for (const f of files) {
|
|
157
|
+
deps.set(f, findModuleDeps(f));
|
|
158
|
+
}
|
|
159
|
+
return deps;
|
|
160
|
+
}
|
|
74
161
|
function lintProject(files, config, rootDir) {
|
|
75
162
|
(0, locale_1.setLocale)(config.locale || 'zh');
|
|
76
163
|
const allIssues = [];
|
|
@@ -85,6 +172,7 @@ function lintProject(files, config, rootDir) {
|
|
|
85
172
|
continue;
|
|
86
173
|
}
|
|
87
174
|
}
|
|
175
|
+
// Build project symbol table
|
|
88
176
|
for (const [filepath] of fileContents) {
|
|
89
177
|
const defunList = collectDefuns(filepath);
|
|
90
178
|
for (const d of defunList) {
|
|
@@ -92,17 +180,25 @@ function lintProject(files, config, rootDir) {
|
|
|
92
180
|
list.push({ file: filepath, line: d.line });
|
|
93
181
|
symbols.defuns.set(d.name, list);
|
|
94
182
|
}
|
|
95
|
-
const refList = collectReferences(filepath
|
|
183
|
+
const refList = collectReferences(filepath);
|
|
96
184
|
for (const r of refList) {
|
|
97
185
|
const list = symbols.references.get(r.name) || [];
|
|
98
186
|
list.push({ file: filepath, line: r.line });
|
|
99
187
|
symbols.references.set(r.name, list);
|
|
100
188
|
}
|
|
101
189
|
}
|
|
190
|
+
// New: build module dependency graph
|
|
191
|
+
const moduleDeps = collectAllModuleDeps(Array.from(fileContents.keys()));
|
|
192
|
+
// New: collect function arg counts per file
|
|
193
|
+
const defunArgs = new Map();
|
|
194
|
+
for (const [filepath] of fileContents) {
|
|
195
|
+
defunArgs.set(filepath, countFunctionArgs(filepath));
|
|
196
|
+
}
|
|
102
197
|
for (const [filepath] of fileContents) {
|
|
103
198
|
const relPath = path.relative(rootDir, filepath);
|
|
104
199
|
const override = findProjectOverride(filepath);
|
|
105
200
|
const checks = override?.checks || config.checks;
|
|
201
|
+
// Existing checks
|
|
106
202
|
if (checks['dangling_defun'] !== 'off') {
|
|
107
203
|
const danglingIssues = (0, dangling_defun_1.checkDanglingDefun)(filepath, symbols.defuns, symbols.references);
|
|
108
204
|
for (const iss of danglingIssues) {
|
|
@@ -134,6 +230,88 @@ function lintProject(files, config, rootDir) {
|
|
|
134
230
|
allIssues.push(iss);
|
|
135
231
|
}
|
|
136
232
|
}
|
|
233
|
+
// New: module cycle detection
|
|
234
|
+
if (checks['module_cycle'] !== 'off') {
|
|
235
|
+
const visited = new Set();
|
|
236
|
+
const stack = new Set();
|
|
237
|
+
const cyclePath = [];
|
|
238
|
+
function dfs(current) {
|
|
239
|
+
if (stack.has(current)) {
|
|
240
|
+
const idx = cyclePath.indexOf(current);
|
|
241
|
+
const cycle = cyclePath.slice(idx).concat(current);
|
|
242
|
+
const displayCycle = cycle.map(c => path.relative(rootDir, c)).join(' → ');
|
|
243
|
+
allIssues.push({
|
|
244
|
+
file: relPath,
|
|
245
|
+
line: 1,
|
|
246
|
+
severity: 'warn',
|
|
247
|
+
rule: 'module_cycle',
|
|
248
|
+
message: `Module dependency cycle detected: ${displayCycle}`,
|
|
249
|
+
});
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
if (visited.has(current))
|
|
253
|
+
return false;
|
|
254
|
+
visited.add(current);
|
|
255
|
+
stack.add(current);
|
|
256
|
+
cyclePath.push(current);
|
|
257
|
+
const deps = moduleDeps.get(current);
|
|
258
|
+
if (deps) {
|
|
259
|
+
for (const imp of deps.imports) {
|
|
260
|
+
for (const [otherFile, otherDeps] of moduleDeps) {
|
|
261
|
+
if (otherDeps.exports.includes(imp)) {
|
|
262
|
+
if (dfs(otherFile))
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
cyclePath.pop();
|
|
269
|
+
stack.delete(current);
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
for (const f of moduleDeps.keys()) {
|
|
273
|
+
if (!visited.has(f)) {
|
|
274
|
+
dfs(f);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// New: signature mismatch check
|
|
279
|
+
if (checks['arg_count_project'] !== 'off') {
|
|
280
|
+
const fileArgs = defunArgs.get(filepath);
|
|
281
|
+
if (fileArgs) {
|
|
282
|
+
const callArgs = countCallArgs(filepath);
|
|
283
|
+
for (const [fnName, calls] of callArgs) {
|
|
284
|
+
const defs = symbols.defuns.get(fnName);
|
|
285
|
+
if (!defs)
|
|
286
|
+
continue;
|
|
287
|
+
const defArgCount = fileArgs.get(fnName);
|
|
288
|
+
if (defArgCount === undefined)
|
|
289
|
+
continue;
|
|
290
|
+
// Only check if all definitions agree
|
|
291
|
+
let allMatch = true;
|
|
292
|
+
for (const def of defs) {
|
|
293
|
+
const otherFileArgs = defunArgs.get(def.file);
|
|
294
|
+
if (otherFileArgs && otherFileArgs.get(fnName) !== defArgCount) {
|
|
295
|
+
allMatch = false;
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (!allMatch)
|
|
300
|
+
continue;
|
|
301
|
+
for (const call of calls) {
|
|
302
|
+
if (call.count !== defArgCount) {
|
|
303
|
+
allIssues.push({
|
|
304
|
+
file: relPath,
|
|
305
|
+
line: call.line,
|
|
306
|
+
severity: 'warn',
|
|
307
|
+
rule: 'arg_count_project',
|
|
308
|
+
message: `Function '${fnName}' called with ${call.count} arguments but defined with ${defArgCount}`,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
137
315
|
}
|
|
138
316
|
return allIssues;
|
|
139
317
|
}
|
package/dist/rules.js
CHANGED
|
@@ -156,7 +156,6 @@ exports.RULES = [
|
|
|
156
156
|
description: '检测函数调用自身(可能无限递归)',
|
|
157
157
|
category: '正确性',
|
|
158
158
|
},
|
|
159
|
-
{ name: 'redundant_cond', defaultSeverity: 'warn', description: '检测单子句 cond 或末尾 T 子句', category: '风格' },
|
|
160
159
|
{ name: 'redundant_if', defaultSeverity: 'warn', description: '检测 if/when 分支中冗余 progn', category: '风格' },
|
|
161
160
|
{ name: 'redundant_let', defaultSeverity: 'warn', description: '检测无绑定的 let 建议改用 progn', category: '风格' },
|
|
162
161
|
{ name: 'redundant_nil_else', defaultSeverity: 'warn', description: '检测 if 中冗余的 nil 分支', category: '风格' },
|
|
@@ -229,6 +228,14 @@ exports.RULES = [
|
|
|
229
228
|
description: '检测 vlax-* 使用前未调用 vl-load-com',
|
|
230
229
|
category: '最佳实践',
|
|
231
230
|
},
|
|
231
|
+
{ name: 'unused_catch_result', defaultSeverity: 'warn', description: '检测 vl-catch-all-apply 结果被当布尔值用', category: '正确性' },
|
|
232
|
+
{ name: 'string_concat_loop', defaultSeverity: 'warn', description: '检测循环内字符串拼接', category: '性能' },
|
|
233
|
+
{ name: 'if_without_else', defaultSeverity: 'warn', description: '检测 if 无 else 分支且返回值被使用', category: '正确性' },
|
|
234
|
+
{ name: 'redundant_list', defaultSeverity: 'warn', description: '检测 (list) 等价于 nil', category: '风格' },
|
|
235
|
+
{ name: 'entget_in_loop', defaultSeverity: 'warn', description: '检测循环内 entget', category: '性能' },
|
|
236
|
+
{ name: 'promise_handler', defaultSeverity: 'warn', description: '检测 vlax-invoke 缺少回调处理器', category: '最佳实践' },
|
|
237
|
+
{ name: 'module_cycle', defaultSeverity: 'warn', description: '检测模块循环依赖', category: '架构' },
|
|
238
|
+
{ name: 'arg_count_project', defaultSeverity: 'warn', description: '跨文件检查函数参数数量', category: '正确性' },
|
|
232
239
|
];
|
|
233
240
|
function generateRulesMarkdown() {
|
|
234
241
|
const lines = [
|
package/dist/runner.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Issue, LintConfig } from './types';
|
|
2
2
|
export declare function runChecks(content: string, file: string, config: LintConfig): Issue[];
|
|
3
|
-
|
|
4
|
-
export declare function runChecksWithVisitor(content: string, file: string, config: LintConfig): Issue[];
|
|
3
|
+
export declare function runChecksWithVisitorWrapper(content: string, file: string, config: LintConfig): Issue[];
|
|
5
4
|
export declare function lintFiles(files: string[], config: LintConfig, rootDir: string): Issue[];
|
|
6
5
|
export declare function lintFilesParallel(files: string[], config: LintConfig, rootDir: string): Promise<Issue[]>;
|
|
7
6
|
export declare function applyFixes(issues: Issue[], content: string, filepath: string): string;
|