@atlisp/lint 0.2.10 → 0.2.12
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 +1 -0
- package/atlisp-lint.default.json +2 -1
- package/atlisp-lint.schema.json +9 -1
- package/dist/checks/dangling-defun.d.ts +7 -0
- package/dist/checks/dangling-defun.js +19 -0
- package/dist/checks/unused-package-dep.d.ts +1 -0
- package/dist/checks/unused-package-dep.js +53 -0
- package/dist/config.js +7 -2
- package/dist/locale.js +2 -0
- package/dist/presets.js +4 -2
- package/dist/project.js +138 -120
- package/dist/rules.js +6 -0
- package/dist/types.d.ts +5 -0
- package/dist/validate.js +1 -0
- package/dist/visitor-runner.js +23 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -128,6 +128,7 @@ npx @atlisp/lint --format-check
|
|
|
128
128
|
| `shadow_builtin` | warn | 最佳实践 | defun 覆盖内置函数 |
|
|
129
129
|
| `dynamic_doc` | warn | 最佳实践 | C: 命令缺少 (princ) |
|
|
130
130
|
| `open_without_close` | warn | 最佳实践 | open/close 数量不匹配 |
|
|
131
|
+
| `strcat_or_arg` | error | 正确性 | strcat 参数中使用 or(or 返回 T/nil) |
|
|
131
132
|
| `strcat_usage` | warn | 最佳实践 | + 拼接而非 strcat |
|
|
132
133
|
| `cond_simplify` | warn | 风格 | 单分支 cond 可简化为 if |
|
|
133
134
|
| `arg_count` | warn | 最佳实践 | 参数数量不匹配 |
|
package/atlisp-lint.default.json
CHANGED
|
@@ -59,13 +59,14 @@
|
|
|
59
59
|
"extra_parens": "warn",
|
|
60
60
|
"arg_count": "error",
|
|
61
61
|
"builtin_arg_count": "error",
|
|
62
|
+
"strcat_or_arg": "error",
|
|
62
63
|
"strcat_usage": "warn",
|
|
63
64
|
"cond_simplify": "warn",
|
|
64
65
|
"quote_style": "warn",
|
|
65
66
|
"eq_usage": "warn",
|
|
66
67
|
"lambda_syntax": "off",
|
|
67
68
|
"comment_style": "warn",
|
|
68
|
-
"empty_catch": "
|
|
69
|
+
"empty_catch": "info",
|
|
69
70
|
"nth_usage": "warn",
|
|
70
71
|
"append_single": "warn",
|
|
71
72
|
"setq_multiple": "warn",
|
package/atlisp-lint.schema.json
CHANGED
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"duplicate_defun": { "type": "string", "enum": ["off", "info", "warn", "error"], "default": "warn" },
|
|
60
60
|
"dynamic_doc": { "type": "string", "enum": ["off", "info", "warn", "error"], "default": "warn" },
|
|
61
61
|
"empty_branch": { "type": "string", "enum": ["off", "info", "warn", "error"], "default": "warn" },
|
|
62
|
-
"empty_catch": { "type": "string", "enum": ["off", "info", "warn", "error"], "default": "
|
|
62
|
+
"empty_catch": { "type": "string", "enum": ["off", "info", "warn", "error"], "default": "info" },
|
|
63
63
|
"empty_comment": { "type": "string", "enum": ["off", "info", "warn", "error"], "default": "info" },
|
|
64
64
|
"encoding": { "type": "string", "enum": ["off", "info", "warn", "error"], "default": "error" },
|
|
65
65
|
"entget_in_loop": { "type": "string", "enum": ["off", "info", "warn", "error"], "default": "warn" },
|
|
@@ -111,6 +111,7 @@
|
|
|
111
111
|
"shadow_builtin": { "type": "string", "enum": ["off", "info", "warn", "error"], "default": "warn" },
|
|
112
112
|
"single_arg_and_or": { "type": "string", "enum": ["off", "info", "warn", "error"], "default": "info" },
|
|
113
113
|
"startapp": { "type": "string", "enum": ["off", "info", "warn", "error"], "default": "warn" },
|
|
114
|
+
"strcat_or_arg": { "type": "string", "enum": ["off", "info", "warn", "error"], "default": "error" },
|
|
114
115
|
"strcat_usage": { "type": "string", "enum": ["off", "info", "warn", "error"], "default": "info" },
|
|
115
116
|
"string_concat_loop": { "type": "string", "enum": ["off", "info", "warn", "error"], "default": "info" },
|
|
116
117
|
"token_in_url": { "type": "string", "enum": ["off", "info", "warn", "error"], "default": "warn" },
|
|
@@ -195,6 +196,13 @@
|
|
|
195
196
|
"walk_exclude": { "type": "array", "items": { "type": "string" }, "default": [".vscode", "vendor", ".git"] },
|
|
196
197
|
"defmacro_allow_files": { "type": "array", "items": { "type": "string" }, "default": ["compat-cl"] }
|
|
197
198
|
}
|
|
199
|
+
},
|
|
200
|
+
"project_analysis": {
|
|
201
|
+
"type": "object",
|
|
202
|
+
"properties": {
|
|
203
|
+
"maxFiles": { "type": "integer", "minimum": 1, "default": 500, "description": "Maximum number of files for cross-file analysis. Exceeding this threshold skips project analysis." },
|
|
204
|
+
"batchSize": { "type": "integer", "minimum": 1, "default": 50, "description": "Files processed per batch (reserved for future streaming)" }
|
|
205
|
+
}
|
|
198
206
|
}
|
|
199
207
|
}
|
|
200
208
|
}
|
|
@@ -6,4 +6,11 @@ export declare function checkDanglingDefun(file: string, allDefuns: Map<string,
|
|
|
6
6
|
file: string;
|
|
7
7
|
line: number;
|
|
8
8
|
}[]>): Issue[];
|
|
9
|
+
export declare function checkDanglingDefunFromDefs(localDefuns: {
|
|
10
|
+
name: string;
|
|
11
|
+
line: number;
|
|
12
|
+
}[], allReferences: Map<string, {
|
|
13
|
+
file: string;
|
|
14
|
+
line: number;
|
|
15
|
+
}[]>, file: string): Issue[];
|
|
9
16
|
//# sourceMappingURL=dangling-defun.d.ts.map
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.checkDanglingDefun = checkDanglingDefun;
|
|
37
|
+
exports.checkDanglingDefunFromDefs = checkDanglingDefunFromDefs;
|
|
37
38
|
const locale_1 = require("../locale");
|
|
38
39
|
const parser_1 = require("@atlisp/parser");
|
|
39
40
|
const fs = __importStar(require("fs"));
|
|
@@ -64,4 +65,22 @@ function checkDanglingDefun(file, allDefuns, allReferences) {
|
|
|
64
65
|
}
|
|
65
66
|
return issues;
|
|
66
67
|
}
|
|
68
|
+
function checkDanglingDefunFromDefs(localDefuns, allReferences, file) {
|
|
69
|
+
const issues = [];
|
|
70
|
+
for (const defun of localDefuns) {
|
|
71
|
+
const refs = allReferences.get(defun.name) || [];
|
|
72
|
+
const calledElsewhere = refs.some(r => r.file !== file);
|
|
73
|
+
const calledLocally = refs.some(r => r.file === file);
|
|
74
|
+
if (!calledLocally && !calledElsewhere) {
|
|
75
|
+
issues.push({
|
|
76
|
+
file,
|
|
77
|
+
line: defun.line,
|
|
78
|
+
severity: 'warn',
|
|
79
|
+
rule: 'dangling_defun',
|
|
80
|
+
message: (0, locale_1.t)('dangling_defun', defun.name),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return issues;
|
|
85
|
+
}
|
|
67
86
|
//# sourceMappingURL=dangling-defun.js.map
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.checkUnusedPackageDep = checkUnusedPackageDep;
|
|
37
|
+
exports.checkUnusedPackageDepFromContent = checkUnusedPackageDepFromContent;
|
|
37
38
|
const locale_1 = require("../locale");
|
|
38
39
|
const parser_1 = require("@atlisp/parser");
|
|
39
40
|
const fs = __importStar(require("fs"));
|
|
@@ -90,4 +91,56 @@ function checkUnusedPackageDep(file) {
|
|
|
90
91
|
}
|
|
91
92
|
return issues;
|
|
92
93
|
}
|
|
94
|
+
function checkUnusedPackageDepFromContent(file, content) {
|
|
95
|
+
const issues = [];
|
|
96
|
+
const ast = (0, parser_1.parseAst)(content, { errorRecovery: true });
|
|
97
|
+
const inPackageNodes = (0, parser_1.astFindAll)(ast, n => (0, parser_1.astIsList)(n, 'in-package'));
|
|
98
|
+
if (inPackageNodes.length === 0)
|
|
99
|
+
return issues;
|
|
100
|
+
const allPackages = new Set();
|
|
101
|
+
for (const node of inPackageNodes) {
|
|
102
|
+
if (node.children && node.children.length >= 2 && (0, parser_1.astIsSymbol)(node.children[1])) {
|
|
103
|
+
allPackages.add(node.children[1].name);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const usedSymbols = new Set();
|
|
107
|
+
const allLists = (0, parser_1.astFindAll)(ast, n => n.type === 'list');
|
|
108
|
+
for (const node of allLists) {
|
|
109
|
+
if (!node.children || node.children.length === 0)
|
|
110
|
+
continue;
|
|
111
|
+
const car = node.children[0];
|
|
112
|
+
if (car.type === 'symbol' && car.name) {
|
|
113
|
+
const parts = car.name.split(':');
|
|
114
|
+
if (parts.length === 2 && parts[0] && parts[1]) {
|
|
115
|
+
usedSymbols.add(parts[0]);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
for (let i = 1; i < node.children.length; i++) {
|
|
119
|
+
const child = node.children[i];
|
|
120
|
+
if (child.type === 'symbol' && child.name) {
|
|
121
|
+
const parts = child.name.split(':');
|
|
122
|
+
if (parts.length === 2 && parts[0] && parts[1]) {
|
|
123
|
+
usedSymbols.add(parts[0]);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const mainPackage = inPackageNodes.length > 0 && inPackageNodes[0].children && inPackageNodes[0].children[1]
|
|
129
|
+
? inPackageNodes[0].children[1].name || ''
|
|
130
|
+
: '';
|
|
131
|
+
allPackages.delete(mainPackage);
|
|
132
|
+
for (const pkg of allPackages) {
|
|
133
|
+
if (!usedSymbols.has(pkg)) {
|
|
134
|
+
const node = inPackageNodes.find(n => n.children && n.children.length >= 2 && (0, parser_1.astIsSymbol)(n.children[1]) && n.children[1].name === pkg);
|
|
135
|
+
issues.push({
|
|
136
|
+
file,
|
|
137
|
+
line: node ? node.pos.line : 1,
|
|
138
|
+
severity: 'warn',
|
|
139
|
+
rule: 'unused_package_dep',
|
|
140
|
+
message: (0, locale_1.t)('unused_package_dep', pkg),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return issues;
|
|
145
|
+
}
|
|
93
146
|
//# sourceMappingURL=unused-package-dep.js.map
|
package/dist/config.js
CHANGED
|
@@ -102,13 +102,14 @@ const DEFAULT_CONFIG = {
|
|
|
102
102
|
extra_parens: 'warn',
|
|
103
103
|
arg_count: 'warn',
|
|
104
104
|
builtin_arg_count: 'error',
|
|
105
|
+
strcat_or_arg: 'error',
|
|
105
106
|
strcat_usage: 'info',
|
|
106
107
|
cond_simplify: 'info',
|
|
107
108
|
quote_style: 'info',
|
|
108
109
|
eq_usage: 'warn',
|
|
109
110
|
lambda_syntax: 'off',
|
|
110
111
|
comment_style: 'info',
|
|
111
|
-
empty_catch: '
|
|
112
|
+
empty_catch: 'info',
|
|
112
113
|
nth_usage: 'info',
|
|
113
114
|
append_single: 'info',
|
|
114
115
|
setq_multiple: 'warn',
|
|
@@ -120,7 +121,7 @@ const DEFAULT_CONFIG = {
|
|
|
120
121
|
shadow_builtin: 'warn',
|
|
121
122
|
dynamic_doc: 'warn',
|
|
122
123
|
loop_optimization: 'info',
|
|
123
|
-
format_indent: '
|
|
124
|
+
format_indent: 'off',
|
|
124
125
|
redundant_if: 'info',
|
|
125
126
|
unused_catch_result: 'info',
|
|
126
127
|
string_concat_loop: 'info',
|
|
@@ -171,6 +172,10 @@ const DEFAULT_CONFIG = {
|
|
|
171
172
|
walk_exclude: ['.vscode', 'vendor', '.git'],
|
|
172
173
|
defmacro_allow_files: ['compat-cl'],
|
|
173
174
|
},
|
|
175
|
+
project_analysis: {
|
|
176
|
+
maxFiles: 500,
|
|
177
|
+
batchSize: 50,
|
|
178
|
+
},
|
|
174
179
|
};
|
|
175
180
|
function loadConfig(configPath) {
|
|
176
181
|
if (configPath) {
|
package/dist/locale.js
CHANGED
|
@@ -94,6 +94,7 @@ const messages = {
|
|
|
94
94
|
'index.watch_status': '{0} files checked: {1} error(s), {2} warning(s)',
|
|
95
95
|
'index.format_unstable': 'Formatting is not idempotent — re-running formatCode produces different output',
|
|
96
96
|
unused_catch_result: 'vl-catch-all-apply result used as boolean — use vl-catch-all-error-p to check',
|
|
97
|
+
strcat_or_arg: 'or in strcat argument — or returns T/nil, not a string',
|
|
97
98
|
string_concat_loop: 'String concatenation inside {0} — move strcat outside the loop for performance',
|
|
98
99
|
if_without_else: 'if without else branch — else is implicitly nil, which is idiomatic in Lisp',
|
|
99
100
|
redundant_list: '(list) evaluates to nil — use nil directly',
|
|
@@ -222,6 +223,7 @@ Options:
|
|
|
222
223
|
'index.watch_status': '已检查 {0} 个文件: {1} 个错误, {2} 个警告',
|
|
223
224
|
'index.format_unstable': '格式化结果不稳定——再次格式化后输出不同',
|
|
224
225
|
unused_catch_result: 'vl-catch-all-apply 的结果被当作布尔值使用——应用 vl-catch-all-error-p 检查',
|
|
226
|
+
strcat_or_arg: 'strcat 参数中使用了 or 函数调用(or 返回 T 或 nil,不是字符串)',
|
|
225
227
|
string_concat_loop: '循环 {0} 内使用了字符串拼接——为提升性能请将 strcat 移到循环外',
|
|
226
228
|
if_without_else: 'if 没有 else 分支——else 默认为 nil 是 Lisp 惯用写法',
|
|
227
229
|
redundant_list: '(list) 等于 nil——请直接使用 nil',
|
package/dist/presets.js
CHANGED
|
@@ -50,13 +50,14 @@ exports.PRESETS = {
|
|
|
50
50
|
extra_parens: 'warn',
|
|
51
51
|
arg_count: 'warn',
|
|
52
52
|
builtin_arg_count: 'error',
|
|
53
|
+
strcat_or_arg: 'error',
|
|
53
54
|
strcat_usage: 'info',
|
|
54
55
|
cond_simplify: 'info',
|
|
55
56
|
quote_style: 'info',
|
|
56
57
|
eq_usage: 'warn',
|
|
57
58
|
lambda_syntax: 'off',
|
|
58
59
|
comment_style: 'info',
|
|
59
|
-
empty_catch: '
|
|
60
|
+
empty_catch: 'info',
|
|
60
61
|
nth_usage: 'info',
|
|
61
62
|
append_single: 'info',
|
|
62
63
|
setq_multiple: 'warn',
|
|
@@ -133,6 +134,7 @@ exports.PRESETS = {
|
|
|
133
134
|
extra_parens: 'error',
|
|
134
135
|
arg_count: 'error',
|
|
135
136
|
builtin_arg_count: 'error',
|
|
137
|
+
strcat_or_arg: 'error',
|
|
136
138
|
strcat_usage: 'error',
|
|
137
139
|
cond_simplify: 'error',
|
|
138
140
|
quote_style: 'error',
|
|
@@ -151,7 +153,7 @@ exports.PRESETS = {
|
|
|
151
153
|
shadow_builtin: 'error',
|
|
152
154
|
dynamic_doc: 'error',
|
|
153
155
|
loop_optimization: 'warn',
|
|
154
|
-
format_indent: '
|
|
156
|
+
format_indent: 'off',
|
|
155
157
|
redundant_if: 'error',
|
|
156
158
|
module_registration: 'error',
|
|
157
159
|
namespace_header: 'error',
|
package/dist/project.js
CHANGED
|
@@ -42,9 +42,8 @@ const missing_export_1 = require("./checks/missing-export");
|
|
|
42
42
|
const unused_package_dep_1 = require("./checks/unused-package-dep");
|
|
43
43
|
const duplicate_defun_1 = require("./checks/duplicate-defun");
|
|
44
44
|
const locale_1 = require("./locale");
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const ast = (0, parser_1.parseAst)(content, { errorRecovery: true });
|
|
45
|
+
const locale_2 = require("./locale");
|
|
46
|
+
function collectDefunsFromAst(ast) {
|
|
48
47
|
const results = [];
|
|
49
48
|
const defunNodes = (0, parser_1.astFindAll)(ast, n => (0, parser_1.astIsList)(n, 'defun') || (0, parser_1.astIsList)(n, 'defun-q'));
|
|
50
49
|
for (const node of defunNodes) {
|
|
@@ -54,9 +53,7 @@ function collectDefuns(file) {
|
|
|
54
53
|
}
|
|
55
54
|
return results;
|
|
56
55
|
}
|
|
57
|
-
function
|
|
58
|
-
const content = fs.readFileSync(file, 'utf-8');
|
|
59
|
-
const ast = (0, parser_1.parseAst)(content, { errorRecovery: true });
|
|
56
|
+
function collectReferencesFromAst(ast) {
|
|
60
57
|
const results = [];
|
|
61
58
|
const allCalls = (0, parser_1.astFindAll)(ast, n => {
|
|
62
59
|
if (n.type !== 'list' || !n.children || n.children.length === 0)
|
|
@@ -71,9 +68,7 @@ function collectReferences(file) {
|
|
|
71
68
|
}
|
|
72
69
|
return results;
|
|
73
70
|
}
|
|
74
|
-
function
|
|
75
|
-
const content = fs.readFileSync(file, 'utf-8');
|
|
76
|
-
const ast = (0, parser_1.parseAst)(content, { errorRecovery: true });
|
|
71
|
+
function countFunctionArgsFromAst(ast) {
|
|
77
72
|
const result = new Map();
|
|
78
73
|
const defunNodes = (0, parser_1.astFindAll)(ast, n => (0, parser_1.astIsList)(n, 'defun') || (0, parser_1.astIsList)(n, 'defun-q'));
|
|
79
74
|
for (const node of defunNodes) {
|
|
@@ -100,9 +95,7 @@ function countFunctionArgs(file) {
|
|
|
100
95
|
}
|
|
101
96
|
return result;
|
|
102
97
|
}
|
|
103
|
-
function
|
|
104
|
-
const content = fs.readFileSync(file, 'utf-8');
|
|
105
|
-
const ast = (0, parser_1.parseAst)(content, { errorRecovery: true });
|
|
98
|
+
function countCallArgsFromAst(ast) {
|
|
106
99
|
const result = new Map();
|
|
107
100
|
const allCalls = (0, parser_1.astFindAll)(ast, n => {
|
|
108
101
|
if (n.type !== 'list' || !n.children || n.children.length === 0)
|
|
@@ -120,9 +113,7 @@ function countCallArgs(file) {
|
|
|
120
113
|
}
|
|
121
114
|
return result;
|
|
122
115
|
}
|
|
123
|
-
function
|
|
124
|
-
const content = fs.readFileSync(filepath, 'utf-8');
|
|
125
|
-
const ast = (0, parser_1.parseAst)(content, { errorRecovery: true });
|
|
116
|
+
function findModuleDepsFromAst(ast) {
|
|
126
117
|
const imports = [];
|
|
127
118
|
const exports = [];
|
|
128
119
|
const inPackageNodes = (0, parser_1.astFindAll)(ast, n => (0, parser_1.astIsList)(n, 'in-package'));
|
|
@@ -154,56 +145,148 @@ function findModuleDeps(filepath) {
|
|
|
154
145
|
}
|
|
155
146
|
return { imports, exports };
|
|
156
147
|
}
|
|
157
|
-
function
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
148
|
+
function checkArgCountProject(filepath, defunArgs, callArgCounts, symbols, relPath) {
|
|
149
|
+
const issues = [];
|
|
150
|
+
const fileArgs = defunArgs.get(filepath);
|
|
151
|
+
if (!fileArgs)
|
|
152
|
+
return issues;
|
|
153
|
+
const callArgs = callArgCounts.get(filepath);
|
|
154
|
+
if (!callArgs)
|
|
155
|
+
return issues;
|
|
156
|
+
for (const [fnName, calls] of callArgs) {
|
|
157
|
+
const defs = symbols.defuns.get(fnName);
|
|
158
|
+
if (!defs)
|
|
159
|
+
continue;
|
|
160
|
+
const defArgCount = fileArgs.get(fnName);
|
|
161
|
+
if (defArgCount === undefined)
|
|
162
|
+
continue;
|
|
163
|
+
let allMatch = true;
|
|
164
|
+
for (const def of defs) {
|
|
165
|
+
const otherFileArgs = defunArgs.get(def.file);
|
|
166
|
+
if (otherFileArgs && otherFileArgs.get(fnName) !== defArgCount) {
|
|
167
|
+
allMatch = false;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (!allMatch)
|
|
172
|
+
continue;
|
|
173
|
+
for (const call of calls) {
|
|
174
|
+
if (call.count !== defArgCount) {
|
|
175
|
+
issues.push({
|
|
176
|
+
file: relPath,
|
|
177
|
+
line: call.line,
|
|
178
|
+
severity: 'warn',
|
|
179
|
+
rule: 'arg_count_project',
|
|
180
|
+
message: (0, locale_2.t)('arg_count_project', fnName, String(call.count), String(defArgCount)),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return issues;
|
|
186
|
+
}
|
|
187
|
+
function checkModuleCycle(moduleDeps, rootDir, filepath, relPath, allIssues) {
|
|
188
|
+
const visited = new Set();
|
|
189
|
+
const stack = new Set();
|
|
190
|
+
const cyclePath = [];
|
|
191
|
+
const dfs = (current) => {
|
|
192
|
+
if (stack.has(current)) {
|
|
193
|
+
const idx = cyclePath.indexOf(current);
|
|
194
|
+
const cycle = cyclePath.slice(idx).concat(current);
|
|
195
|
+
const displayCycle = cycle.map(c => path.relative(rootDir, c)).join(' → ');
|
|
196
|
+
allIssues.push({
|
|
197
|
+
file: relPath,
|
|
198
|
+
line: 1,
|
|
199
|
+
severity: 'warn',
|
|
200
|
+
rule: 'module_cycle',
|
|
201
|
+
message: `Module dependency cycle detected: ${displayCycle}`,
|
|
202
|
+
});
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
if (visited.has(current))
|
|
206
|
+
return false;
|
|
207
|
+
visited.add(current);
|
|
208
|
+
stack.add(current);
|
|
209
|
+
cyclePath.push(current);
|
|
210
|
+
const deps = moduleDeps.get(current);
|
|
211
|
+
if (deps) {
|
|
212
|
+
for (const imp of deps.imports) {
|
|
213
|
+
for (const [otherFile, otherDeps] of moduleDeps) {
|
|
214
|
+
if (otherDeps.exports.includes(imp)) {
|
|
215
|
+
if (dfs(otherFile))
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
cyclePath.pop();
|
|
222
|
+
stack.delete(current);
|
|
223
|
+
return false;
|
|
224
|
+
};
|
|
225
|
+
for (const f of moduleDeps.keys()) {
|
|
226
|
+
if (!visited.has(f)) {
|
|
227
|
+
dfs(f);
|
|
228
|
+
}
|
|
161
229
|
}
|
|
162
|
-
return deps;
|
|
163
230
|
}
|
|
164
231
|
function lintProject(files, config, rootDir) {
|
|
165
232
|
(0, locale_1.setLocale)(config.locale || 'zh');
|
|
166
233
|
const allIssues = [];
|
|
167
234
|
const symbols = { defuns: new Map(), references: new Map() };
|
|
235
|
+
const maxFiles = config.project_analysis?.maxFiles ?? 500;
|
|
236
|
+
if (files.length > maxFiles) {
|
|
237
|
+
const msg = `${files.length} files exceeds project_analysis.maxFiles (${maxFiles}). Skipping cross-file analysis. To increase, set project_analysis.maxFiles in config.`;
|
|
238
|
+
console.warn(`[atlisp-lint] ${msg}`);
|
|
239
|
+
return [];
|
|
240
|
+
}
|
|
168
241
|
const fileContents = new Map();
|
|
242
|
+
const defunArgs = new Map();
|
|
243
|
+
const callArgCounts = new Map();
|
|
244
|
+
const moduleDeps = new Map();
|
|
245
|
+
// Phase A: Streaming collection — one file at a time, keep only metadata
|
|
169
246
|
for (const filepath of files) {
|
|
247
|
+
let content;
|
|
170
248
|
try {
|
|
171
|
-
|
|
172
|
-
fileContents.set(filepath, content);
|
|
249
|
+
content = fs.readFileSync(filepath, 'utf-8');
|
|
173
250
|
}
|
|
174
251
|
catch {
|
|
175
252
|
continue;
|
|
176
253
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const defunList = collectDefuns(filepath);
|
|
254
|
+
const ast = (0, parser_1.parseAst)(content, { errorRecovery: true });
|
|
255
|
+
// Extract all metadata from a single AST parse
|
|
256
|
+
const defunList = collectDefunsFromAst(ast);
|
|
181
257
|
for (const d of defunList) {
|
|
182
258
|
const list = symbols.defuns.get(d.name) || [];
|
|
183
259
|
list.push({ file: filepath, line: d.line });
|
|
184
260
|
symbols.defuns.set(d.name, list);
|
|
185
261
|
}
|
|
186
|
-
const refList =
|
|
262
|
+
const refList = collectReferencesFromAst(ast);
|
|
187
263
|
for (const r of refList) {
|
|
188
264
|
const list = symbols.references.get(r.name) || [];
|
|
189
265
|
list.push({ file: filepath, line: r.line });
|
|
190
266
|
symbols.references.set(r.name, list);
|
|
191
267
|
}
|
|
268
|
+
defunArgs.set(filepath, countFunctionArgsFromAst(ast));
|
|
269
|
+
callArgCounts.set(filepath, countCallArgsFromAst(ast));
|
|
270
|
+
moduleDeps.set(filepath, findModuleDepsFromAst(ast));
|
|
271
|
+
// Cache file content for checks that still need it
|
|
272
|
+
fileContents.set(filepath, content);
|
|
273
|
+
// AST goes out of scope here → GC reclaims it
|
|
192
274
|
}
|
|
193
|
-
//
|
|
194
|
-
const moduleDeps = collectAllModuleDeps(Array.from(fileContents.keys()));
|
|
195
|
-
// New: collect function arg counts per file
|
|
196
|
-
const defunArgs = new Map();
|
|
197
|
-
for (const [filepath] of fileContents) {
|
|
198
|
-
defunArgs.set(filepath, countFunctionArgs(filepath));
|
|
199
|
-
}
|
|
275
|
+
// Phase B: Cross-file checks using only symbol tables
|
|
200
276
|
for (const [filepath] of fileContents) {
|
|
201
277
|
const relPath = path.relative(rootDir, filepath);
|
|
202
278
|
const override = findProjectOverride(filepath);
|
|
203
279
|
const checks = override?.checks || config.checks;
|
|
204
|
-
// Existing checks
|
|
205
280
|
if (checks['dangling_defun'] !== 'off') {
|
|
206
|
-
const
|
|
281
|
+
const defunList = [];
|
|
282
|
+
for (const [name, locations] of symbols.defuns) {
|
|
283
|
+
for (const loc of locations) {
|
|
284
|
+
if (loc.file === filepath) {
|
|
285
|
+
defunList.push({ name, line: loc.line });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const danglingIssues = (0, dangling_defun_1.checkDanglingDefunFromDefs)(defunList, symbols.references, filepath);
|
|
207
290
|
for (const iss of danglingIssues) {
|
|
208
291
|
iss.file = relPath;
|
|
209
292
|
allIssues.push(iss);
|
|
@@ -220,10 +303,13 @@ function lintProject(files, config, rootDir) {
|
|
|
220
303
|
}
|
|
221
304
|
}
|
|
222
305
|
if (checks['unused_package_dep'] !== 'off') {
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
306
|
+
const content = fileContents.get(filepath);
|
|
307
|
+
if (content) {
|
|
308
|
+
const depIssues = (0, unused_package_dep_1.checkUnusedPackageDepFromContent)(filepath, content);
|
|
309
|
+
for (const iss of depIssues) {
|
|
310
|
+
iss.file = relPath;
|
|
311
|
+
allIssues.push(iss);
|
|
312
|
+
}
|
|
227
313
|
}
|
|
228
314
|
}
|
|
229
315
|
if (checks['duplicate_defun'] !== 'off') {
|
|
@@ -233,87 +319,19 @@ function lintProject(files, config, rootDir) {
|
|
|
233
319
|
allIssues.push(iss);
|
|
234
320
|
}
|
|
235
321
|
}
|
|
236
|
-
// New: module cycle detection
|
|
237
|
-
if (checks['module_cycle'] !== 'off') {
|
|
238
|
-
const visited = new Set();
|
|
239
|
-
const stack = new Set();
|
|
240
|
-
const cyclePath = [];
|
|
241
|
-
const dfs = (current) => {
|
|
242
|
-
if (stack.has(current)) {
|
|
243
|
-
const idx = cyclePath.indexOf(current);
|
|
244
|
-
const cycle = cyclePath.slice(idx).concat(current);
|
|
245
|
-
const displayCycle = cycle.map(c => path.relative(rootDir, c)).join(' → ');
|
|
246
|
-
allIssues.push({
|
|
247
|
-
file: relPath,
|
|
248
|
-
line: 1,
|
|
249
|
-
severity: 'warn',
|
|
250
|
-
rule: 'module_cycle',
|
|
251
|
-
message: `Module dependency cycle detected: ${displayCycle}`,
|
|
252
|
-
});
|
|
253
|
-
return true;
|
|
254
|
-
}
|
|
255
|
-
if (visited.has(current))
|
|
256
|
-
return false;
|
|
257
|
-
visited.add(current);
|
|
258
|
-
stack.add(current);
|
|
259
|
-
cyclePath.push(current);
|
|
260
|
-
const deps = moduleDeps.get(current);
|
|
261
|
-
if (deps) {
|
|
262
|
-
for (const imp of deps.imports) {
|
|
263
|
-
for (const [otherFile, otherDeps] of moduleDeps) {
|
|
264
|
-
if (otherDeps.exports.includes(imp)) {
|
|
265
|
-
if (dfs(otherFile))
|
|
266
|
-
return true;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
cyclePath.pop();
|
|
272
|
-
stack.delete(current);
|
|
273
|
-
return false;
|
|
274
|
-
};
|
|
275
|
-
for (const f of moduleDeps.keys()) {
|
|
276
|
-
if (!visited.has(f)) {
|
|
277
|
-
dfs(f);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
// New: signature mismatch check
|
|
282
322
|
if (checks['arg_count_project'] !== 'off') {
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
for (const def of defs) {
|
|
296
|
-
const otherFileArgs = defunArgs.get(def.file);
|
|
297
|
-
if (otherFileArgs && otherFileArgs.get(fnName) !== defArgCount) {
|
|
298
|
-
allMatch = false;
|
|
299
|
-
break;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
if (!allMatch)
|
|
303
|
-
continue;
|
|
304
|
-
for (const call of calls) {
|
|
305
|
-
if (call.count !== defArgCount) {
|
|
306
|
-
allIssues.push({
|
|
307
|
-
file: relPath,
|
|
308
|
-
line: call.line,
|
|
309
|
-
severity: 'warn',
|
|
310
|
-
rule: 'arg_count_project',
|
|
311
|
-
message: `Function '${fnName}' called with ${call.count} arguments but defined with ${defArgCount}`,
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
323
|
+
const argIssues = checkArgCountProject(filepath, defunArgs, callArgCounts, symbols, relPath);
|
|
324
|
+
allIssues.push(...argIssues);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// Module cycle check: run DFS once (not per file)
|
|
328
|
+
const cycleCheckFile = fileContents.keys().next().value;
|
|
329
|
+
if (cycleCheckFile) {
|
|
330
|
+
const override = findProjectOverride(cycleCheckFile);
|
|
331
|
+
const checks = override?.checks || config.checks;
|
|
332
|
+
if (checks['module_cycle'] !== 'off') {
|
|
333
|
+
const relPath = path.relative(rootDir, cycleCheckFile);
|
|
334
|
+
checkModuleCycle(moduleDeps, rootDir, cycleCheckFile, relPath, allIssues);
|
|
317
335
|
}
|
|
318
336
|
}
|
|
319
337
|
return allIssues;
|
package/dist/rules.js
CHANGED
|
@@ -179,6 +179,12 @@ exports.RULES = [
|
|
|
179
179
|
{ name: 'shadow_builtin', defaultSeverity: 'warn', description: '检测覆盖内置函数的 defun', category: '最佳实践' },
|
|
180
180
|
{ name: 'single_arg_and_or', defaultSeverity: 'info', description: '检测单参数 and/or 建议简化', category: '风格' },
|
|
181
181
|
{ name: 'startapp', defaultSeverity: 'warn', description: '检测 startapp 调用(启动外部程序)', category: '安全' },
|
|
182
|
+
{
|
|
183
|
+
name: 'strcat_or_arg',
|
|
184
|
+
defaultSeverity: 'error',
|
|
185
|
+
description: '检测 strcat 参数中使用 or(or 返回 T/nil,非字符串)',
|
|
186
|
+
category: '正确性',
|
|
187
|
+
},
|
|
182
188
|
{
|
|
183
189
|
name: 'strcat_usage',
|
|
184
190
|
defaultSeverity: 'info',
|
package/dist/types.d.ts
CHANGED
|
@@ -42,6 +42,10 @@ export interface SbclConfig {
|
|
|
42
42
|
walk_exclude: string[];
|
|
43
43
|
defmacro_allow_files: string[];
|
|
44
44
|
}
|
|
45
|
+
export interface ProjectAnalysisConfig {
|
|
46
|
+
maxFiles: number;
|
|
47
|
+
batchSize: number;
|
|
48
|
+
}
|
|
45
49
|
export interface SourceConfig {
|
|
46
50
|
globs: string[];
|
|
47
51
|
exclude: string[];
|
|
@@ -66,6 +70,7 @@ export interface LintConfig {
|
|
|
66
70
|
namespace_header: NamespaceHeaderConfig;
|
|
67
71
|
bare_function_names: BareFunctionNamesConfig;
|
|
68
72
|
sbcl: SbclConfig;
|
|
73
|
+
project_analysis: ProjectAnalysisConfig;
|
|
69
74
|
preset?: string;
|
|
70
75
|
}
|
|
71
76
|
export interface FormattedResult {
|
package/dist/validate.js
CHANGED
package/dist/visitor-runner.js
CHANGED
|
@@ -664,6 +664,29 @@ function runChecksWithVisitor(ast, file, config, disableMap) {
|
|
|
664
664
|
}
|
|
665
665
|
});
|
|
666
666
|
}
|
|
667
|
+
// strcat_or_arg
|
|
668
|
+
if (checks['strcat_or_arg'] !== 'off') {
|
|
669
|
+
visitor.on('*', node => {
|
|
670
|
+
if (node.type === 'list' &&
|
|
671
|
+
node.children &&
|
|
672
|
+
node.children.length > 1 &&
|
|
673
|
+
node.children[0].type === 'symbol' &&
|
|
674
|
+
node.children[0].name === 'strcat') {
|
|
675
|
+
if (isInQuote(node))
|
|
676
|
+
return;
|
|
677
|
+
for (let i = 1; i < node.children.length; i++) {
|
|
678
|
+
const arg = node.children[i];
|
|
679
|
+
if (arg.type === 'list' &&
|
|
680
|
+
arg.children &&
|
|
681
|
+
arg.children.length > 0 &&
|
|
682
|
+
arg.children[0].type === 'symbol' &&
|
|
683
|
+
arg.children[0].name === 'or') {
|
|
684
|
+
addIssue('strcat_or_arg', arg.pos.line, (0, locale_1.t)('strcat_or_arg'));
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
}
|
|
667
690
|
// redundant_list
|
|
668
691
|
if (checks['redundant_list'] !== 'off') {
|
|
669
692
|
visitor.on('list', node => {
|