@atlisp/lint 0.1.6 → 0.1.8
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 +38 -1
- package/atlisp-lint.default.json +1 -0
- package/dist/checks/format-indent.d.ts +3 -0
- package/dist/checks/format-indent.js +29 -0
- package/dist/config.js +1 -0
- package/dist/formatter.d.ts +2 -0
- package/dist/formatter.js +51 -0
- package/dist/index.js +119 -19
- package/dist/locale.js +52 -0
- package/dist/presets.js +1 -0
- package/dist/rules.js +1 -0
- package/dist/runner.js +131 -1
- package/dist/validate.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,6 +27,12 @@ npx @atlisp/lint --fix
|
|
|
27
27
|
|
|
28
28
|
# 监听文件变化自动重检
|
|
29
29
|
npx @atlisp/lint --watch
|
|
30
|
+
|
|
31
|
+
# 自动格式化代码缩进
|
|
32
|
+
npx @atlisp/lint --format-code
|
|
33
|
+
|
|
34
|
+
# 检查代码格式(不修改文件)
|
|
35
|
+
npx @atlisp/lint --format-check
|
|
30
36
|
```
|
|
31
37
|
|
|
32
38
|
## CLI 选项
|
|
@@ -39,13 +45,17 @@ npx @atlisp/lint --watch
|
|
|
39
45
|
--format <format> 输出格式:default(默认)| json
|
|
40
46
|
--init 在当前目录生成 atlisp-lint.json
|
|
41
47
|
--install-hook 安装 git pre-commit hook
|
|
42
|
-
--fix 自动修复尾部空格、
|
|
48
|
+
--fix 自动修复尾部空格、BOM、多余括号、冗余 progn/let、引号风格等问题
|
|
43
49
|
--cache 启用文件哈希缓存,跳过未变更的文件
|
|
44
50
|
--clear-cache 清除缓存目录
|
|
45
51
|
--parallel 开启多线程并行检查(使用 Worker Threads)
|
|
46
52
|
--project 启用跨文件分析(Phase 1b)
|
|
47
53
|
--watch 监听模式,文件变动自动重新检查
|
|
48
54
|
--hook-args <args> 自定义 pre-commit hook 参数(配合 --install-hook)
|
|
55
|
+
--help 显示帮助信息
|
|
56
|
+
--version 显示版本号
|
|
57
|
+
--format-code 自动格式化代码缩进(基于括号深度 + AST 对齐)
|
|
58
|
+
--format-check 只检查代码缩进格式(不修改文件,不符合则退出码 1)
|
|
49
59
|
```
|
|
50
60
|
|
|
51
61
|
## 架构:三相检测
|
|
@@ -120,6 +130,7 @@ npx @atlisp/lint --watch
|
|
|
120
130
|
| `cond_simplify` | warn | 风格 | 单分支 cond 可简化为 if |
|
|
121
131
|
| `arg_count` | warn | 最佳实践 | 参数数量不匹配 |
|
|
122
132
|
| `bare_function_names` | off | 风格 | defun 缺少命名空间前缀 |
|
|
133
|
+
| `format_indent` | off | 风格 | 代码缩进不符合格式化规范 |
|
|
123
134
|
| `function_order` | off | 最佳实践 | 定义在使用之后 |
|
|
124
135
|
| `magic_number` | off | 风格 | 硬编码魔法数字 |
|
|
125
136
|
| `module_registration` | off | 架构 | 模块文件未注册到 \*modules\* |
|
|
@@ -148,6 +159,26 @@ npx @atlisp/lint --watch
|
|
|
148
159
|
- CL-ism — `#(`、`#\`、`#.` 等 AutoLISP 无效语法
|
|
149
160
|
- defmacro — 默认不允许,除非文件在允许列表中
|
|
150
161
|
|
|
162
|
+
## --format-code 代码格式化
|
|
163
|
+
|
|
164
|
+
基于括号深度自动调整代码缩进,规则:
|
|
165
|
+
|
|
166
|
+
- 每层嵌套增加 2 空格缩进
|
|
167
|
+
- `)` 单独一行时自动回到对应层级的缩进
|
|
168
|
+
- 注释行按当前嵌套深度缩进
|
|
169
|
+
- 字符串中的括号不影响缩进计算
|
|
170
|
+
- 自动删除行尾多余空格
|
|
171
|
+
- 空行保持不变
|
|
172
|
+
- 格式化后进行幂等性检查,若再次格式化结果不一致则输出警告
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
# 格式化所有 .lsp 文件
|
|
176
|
+
npx @atlisp/lint --format-code
|
|
177
|
+
|
|
178
|
+
# 仅检查格式(不修改文件),不符合则退出码 1
|
|
179
|
+
npx @atlisp/lint --format-check
|
|
180
|
+
```
|
|
181
|
+
|
|
151
182
|
## --fix 自动修复
|
|
152
183
|
|
|
153
184
|
### 预扫描修复(fixFile)
|
|
@@ -168,6 +199,12 @@ npx @atlisp/lint --watch
|
|
|
168
199
|
| `redundant_nil_else` | (if cond x nil) → (if cond x) |
|
|
169
200
|
| `single_arg_and_or` | (and x) → x |
|
|
170
201
|
| `redundant_setq` | 删除 (setq x x) 行 |
|
|
202
|
+
| `redundant_progn` | 移除分支中多余的 (progn ...) 包装 |
|
|
203
|
+
| `redundant_let` | (let () ...) → (progn ...) |
|
|
204
|
+
| `quote_style` | (quote x) → 'x |
|
|
205
|
+
| `redundant_if` | 移除 if/when 分支中冗余 progn |
|
|
206
|
+
| `misplaced_else` | (if (not x) a b) → (if x b a) |
|
|
207
|
+
| `format_indent` | 对整个文件应用 `formatCode()` 修复缩进 |
|
|
171
208
|
|
|
172
209
|
## 行内忽略
|
|
173
210
|
|
package/atlisp-lint.default.json
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkFormatIndent = checkFormatIndent;
|
|
4
|
+
const formatter_1 = require("../formatter");
|
|
5
|
+
function checkFormatIndent(content, file) {
|
|
6
|
+
const formatted = (0, formatter_1.formatCode)(content);
|
|
7
|
+
if (content === formatted)
|
|
8
|
+
return [];
|
|
9
|
+
// Find first line that differs for better error location
|
|
10
|
+
const originalLines = content.split('\n');
|
|
11
|
+
const formattedLines = formatted.split('\n');
|
|
12
|
+
let firstDiffLine = 1;
|
|
13
|
+
for (let i = 0; i < Math.max(originalLines.length, formattedLines.length); i++) {
|
|
14
|
+
if (originalLines[i] !== formattedLines[i]) {
|
|
15
|
+
firstDiffLine = i + 1;
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return [
|
|
20
|
+
{
|
|
21
|
+
file,
|
|
22
|
+
line: firstDiffLine,
|
|
23
|
+
severity: 'warn',
|
|
24
|
+
rule: 'format_indent',
|
|
25
|
+
message: 'Code indentation does not match expected format',
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=format-indent.js.map
|
package/dist/config.js
CHANGED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatCode = formatCode;
|
|
4
|
+
const utils_1 = require("./utils");
|
|
5
|
+
function v1Format(content, indentSize) {
|
|
6
|
+
const lines = content.split('\n');
|
|
7
|
+
const result = [];
|
|
8
|
+
let depth = 0;
|
|
9
|
+
for (const line of lines) {
|
|
10
|
+
const trimmedLine = line.trimEnd();
|
|
11
|
+
if (trimmedLine === '') {
|
|
12
|
+
result.push('');
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
const stripped = (0, utils_1.stripLine)(trimmedLine);
|
|
16
|
+
const trimmedStripped = stripped.trim();
|
|
17
|
+
if (trimmedStripped === '') {
|
|
18
|
+
const indent = ' '.repeat(depth * indentSize);
|
|
19
|
+
result.push(indent + trimmedLine.trimStart());
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
let openCount = 0;
|
|
23
|
+
let closeCount = 0;
|
|
24
|
+
for (const ch of trimmedStripped) {
|
|
25
|
+
if (ch === '(')
|
|
26
|
+
openCount++;
|
|
27
|
+
else if (ch === ')')
|
|
28
|
+
closeCount++;
|
|
29
|
+
}
|
|
30
|
+
let effectiveDepth = depth;
|
|
31
|
+
if (trimmedStripped.startsWith(')')) {
|
|
32
|
+
let leadingClose = 0;
|
|
33
|
+
for (const ch of trimmedStripped) {
|
|
34
|
+
if (ch === ')')
|
|
35
|
+
leadingClose++;
|
|
36
|
+
else
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
effectiveDepth = Math.max(0, depth - leadingClose);
|
|
40
|
+
}
|
|
41
|
+
result.push(' '.repeat(effectiveDepth * indentSize) + trimmedLine.trimStart());
|
|
42
|
+
depth += openCount - closeCount;
|
|
43
|
+
if (depth < 0)
|
|
44
|
+
depth = 0;
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
function formatCode(content, indentSize = 2) {
|
|
49
|
+
return v1Format(content, indentSize).join('\n');
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=formatter.js.map
|
package/dist/index.js
CHANGED
|
@@ -42,6 +42,7 @@ 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 formatter_1 = require("./formatter");
|
|
45
46
|
const locale_1 = require("./locale");
|
|
46
47
|
const cache_1 = require("./cache");
|
|
47
48
|
const watch_1 = require("./watch");
|
|
@@ -61,6 +62,11 @@ function parseArgs() {
|
|
|
61
62
|
parallel: false,
|
|
62
63
|
project: false,
|
|
63
64
|
watch: false,
|
|
65
|
+
help: false,
|
|
66
|
+
version: false,
|
|
67
|
+
formatCode: false,
|
|
68
|
+
formatCheck: false,
|
|
69
|
+
sbcl: false,
|
|
64
70
|
};
|
|
65
71
|
for (let i = 0; i < argv.length; i++) {
|
|
66
72
|
switch (argv[i]) {
|
|
@@ -106,6 +112,21 @@ function parseArgs() {
|
|
|
106
112
|
case '--watch':
|
|
107
113
|
opts.watch = true;
|
|
108
114
|
break;
|
|
115
|
+
case '--help':
|
|
116
|
+
opts.help = true;
|
|
117
|
+
break;
|
|
118
|
+
case '--version':
|
|
119
|
+
opts.version = true;
|
|
120
|
+
break;
|
|
121
|
+
case '--format-code':
|
|
122
|
+
opts.formatCode = true;
|
|
123
|
+
break;
|
|
124
|
+
case '--format-check':
|
|
125
|
+
opts.formatCheck = true;
|
|
126
|
+
break;
|
|
127
|
+
case '--sbcl':
|
|
128
|
+
opts.sbcl = true;
|
|
129
|
+
break;
|
|
109
130
|
default:
|
|
110
131
|
break;
|
|
111
132
|
}
|
|
@@ -227,10 +248,27 @@ npx @atlisp/lint ${args} || exit 1
|
|
|
227
248
|
fs.chmodSync(hookPath, 0o755);
|
|
228
249
|
console.log((0, locale_1.t)('index.hook_installed', hookPath));
|
|
229
250
|
}
|
|
251
|
+
function printHelp() {
|
|
252
|
+
console.log((0, locale_1.t)('help.text'));
|
|
253
|
+
}
|
|
230
254
|
async function main() {
|
|
231
255
|
try {
|
|
232
256
|
const opts = parseArgs();
|
|
233
257
|
const rootDir = process.cwd();
|
|
258
|
+
// Load config early so locale is available for --help and other commands
|
|
259
|
+
const configPath = opts.config || path.join(rootDir, 'atlisp-lint.json');
|
|
260
|
+
const config = (0, config_1.loadConfig)(fs.existsSync(configPath) ? configPath : undefined);
|
|
261
|
+
(0, locale_1.setLocale)(config.locale || 'zh');
|
|
262
|
+
if (opts.help) {
|
|
263
|
+
printHelp();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (opts.version) {
|
|
267
|
+
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
268
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
269
|
+
console.log(pkg.version);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
234
272
|
if (opts.init) {
|
|
235
273
|
initConfig(rootDir);
|
|
236
274
|
return;
|
|
@@ -244,16 +282,40 @@ async function main() {
|
|
|
244
282
|
console.log((0, locale_1.t)('index.cache_cleared'));
|
|
245
283
|
return;
|
|
246
284
|
}
|
|
247
|
-
// Load config
|
|
248
|
-
const configPath = opts.config || path.join(rootDir, 'atlisp-lint.json');
|
|
249
|
-
const config = (0, config_1.loadConfig)(fs.existsSync(configPath) ? configPath : undefined);
|
|
250
|
-
(0, locale_1.setLocale)(config.locale || 'zh');
|
|
251
285
|
// Collect files
|
|
252
286
|
const files = opts.staged ? collectStagedFiles(rootDir) : collectFiles(rootDir, opts, config);
|
|
253
287
|
if (files.length === 0) {
|
|
254
288
|
console.log((0, locale_1.t)('index.no_files'));
|
|
255
289
|
return;
|
|
256
290
|
}
|
|
291
|
+
// --format-code mode: format files in place
|
|
292
|
+
if (opts.formatCode || opts.formatCheck) {
|
|
293
|
+
let needsFormat = false;
|
|
294
|
+
for (const f of files) {
|
|
295
|
+
const content = fs.readFileSync(f, 'utf-8');
|
|
296
|
+
const formatted = (0, formatter_1.formatCode)(content);
|
|
297
|
+
if (content !== formatted) {
|
|
298
|
+
if (opts.formatCheck) {
|
|
299
|
+
console.log(`${path.relative(rootDir, f)} — needs formatting`);
|
|
300
|
+
needsFormat = true;
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
fs.writeFileSync(f, formatted, 'utf-8');
|
|
304
|
+
console.log(`${(0, locale_1.t)('summary.tag_fail')}: ${path.relative(rootDir, f)} — ${(0, locale_1.t)('index.fixed', 'format')}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// Idempotency check: format again to verify stability
|
|
308
|
+
if (!opts.formatCheck) {
|
|
309
|
+
const reFormatted = (0, formatter_1.formatCode)(fs.readFileSync(f, 'utf-8'));
|
|
310
|
+
if (reFormatted !== fs.readFileSync(f, 'utf-8')) {
|
|
311
|
+
console.log(`${(0, locale_1.t)('summary.tag_warn')}: ${path.relative(rootDir, f)} — ${(0, locale_1.t)('index.format_unstable')}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (opts.formatCheck && needsFormat)
|
|
316
|
+
process.exit(1);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
257
319
|
// --watch mode
|
|
258
320
|
if (opts.watch) {
|
|
259
321
|
(0, watch_1.watchFiles)(files, { rootDir, format: opts.format, config });
|
|
@@ -267,6 +329,22 @@ async function main() {
|
|
|
267
329
|
console.log(`${(0, locale_1.t)('summary.tag_fail')}: ${path.relative(rootDir, f)} — ${(0, locale_1.t)('index.fixed', rule)}`);
|
|
268
330
|
}
|
|
269
331
|
}
|
|
332
|
+
// format_indent fix for non-staged mode (no lint results available yet)
|
|
333
|
+
if (config.checks['format_indent'] !== 'off') {
|
|
334
|
+
for (const f of files) {
|
|
335
|
+
try {
|
|
336
|
+
const content = fs.readFileSync(f, 'utf-8');
|
|
337
|
+
const formatted = (0, formatter_1.formatCode)(content);
|
|
338
|
+
if (formatted !== content) {
|
|
339
|
+
fs.writeFileSync(f, formatted, 'utf-8');
|
|
340
|
+
console.log(`${(0, locale_1.t)('summary.tag_fail')}: ${path.relative(rootDir, f)} — ${(0, locale_1.t)('index.fixed', 'format_indent')}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
/* ignore */
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
270
348
|
if (!opts.staged)
|
|
271
349
|
return;
|
|
272
350
|
}
|
|
@@ -322,6 +400,26 @@ async function main() {
|
|
|
322
400
|
}
|
|
323
401
|
}
|
|
324
402
|
}
|
|
403
|
+
// format_indent fix: apply formatCode to files with format_indent issues
|
|
404
|
+
if (config.checks['format_indent'] !== 'off') {
|
|
405
|
+
for (const f of filesToLint) {
|
|
406
|
+
const relPath = path.relative(rootDir, f);
|
|
407
|
+
const hasFormatIssue = issues.some(i => i.file === relPath && i.rule === 'format_indent');
|
|
408
|
+
if (hasFormatIssue) {
|
|
409
|
+
try {
|
|
410
|
+
const content = fs.readFileSync(f, 'utf-8');
|
|
411
|
+
const formatted = (0, formatter_1.formatCode)(content);
|
|
412
|
+
if (formatted !== content) {
|
|
413
|
+
fs.writeFileSync(f, formatted, 'utf-8');
|
|
414
|
+
console.log(`${(0, locale_1.t)('summary.tag_fail')}: ${relPath} — ${(0, locale_1.t)('index.fixed', 'format_indent')}`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
/* ignore */
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
325
423
|
}
|
|
326
424
|
// Mark as cached (reuse pre-read content if available)
|
|
327
425
|
if (opts.cache) {
|
|
@@ -329,21 +427,23 @@ async function main() {
|
|
|
329
427
|
(0, cache_1.markCached)(f, rootDir, contentCache.get(f));
|
|
330
428
|
}
|
|
331
429
|
}
|
|
332
|
-
// Phase 2: SBCL syntax validation
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
430
|
+
// Phase 2: SBCL syntax validation (only when --sbcl is specified)
|
|
431
|
+
if (opts.sbcl) {
|
|
432
|
+
const stubPkgPath = path.join(__dirname, '..', 'stub-packages.json');
|
|
433
|
+
try {
|
|
434
|
+
const sbclIssues = (0, sbcl_1.runSbclLint)(rootDir, config.sbcl, stubPkgPath);
|
|
435
|
+
issues.push(...sbclIssues);
|
|
436
|
+
}
|
|
437
|
+
catch (err) {
|
|
438
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
439
|
+
issues.push({
|
|
440
|
+
file: 'sbcl',
|
|
441
|
+
line: 1,
|
|
442
|
+
severity: 'error',
|
|
443
|
+
rule: 'sbcl',
|
|
444
|
+
message: msg,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
347
447
|
}
|
|
348
448
|
// Build file content map for code line display
|
|
349
449
|
const fileContents = new Map();
|
package/dist/locale.js
CHANGED
|
@@ -55,6 +55,7 @@ const messages = {
|
|
|
55
55
|
error_handling: "Functions using command but missing *error*: '{0}'",
|
|
56
56
|
global_naming: "Global variable '{0}' should use *...* naming convention",
|
|
57
57
|
extra_parens: 'Suspicious excessive closing parenthesis: {0} extra )',
|
|
58
|
+
format_indent: 'Code indentation does not match expected format',
|
|
58
59
|
setq_single_arg: "Variable '{0}' is setq'd with no value — likely a typo",
|
|
59
60
|
assoc_without_cdr: 'assoc result is not used — wrap with cdr to extract the value',
|
|
60
61
|
identical_branches: 'if has identical then and else branches — possible copy-paste error',
|
|
@@ -86,6 +87,31 @@ const messages = {
|
|
|
86
87
|
'index.watching': 'Watching',
|
|
87
88
|
'index.watch_hint': 'Press Ctrl+C to stop',
|
|
88
89
|
'index.watch_status': '{0} files checked: {1} error(s), {2} warning(s)',
|
|
90
|
+
'index.format_unstable': 'Formatting is not idempotent — re-running formatCode produces different output',
|
|
91
|
+
'help.text': `@atlisp/lint - AutoLISP static analysis tool
|
|
92
|
+
|
|
93
|
+
Usage: atlisp-lint [options]
|
|
94
|
+
|
|
95
|
+
Options:
|
|
96
|
+
--src <dir> Specify source directory (may be repeated)
|
|
97
|
+
--test <dir> Specify test directory (may be repeated)
|
|
98
|
+
--config <path> Path to config file
|
|
99
|
+
--staged Only check git staged .lsp files
|
|
100
|
+
--format <format> Output format: default | json
|
|
101
|
+
--init Generate atlisp-lint.json in current directory
|
|
102
|
+
--install-hook Install git pre-commit hook
|
|
103
|
+
--fix Auto-fix trailing whitespace, BOM, trailing parens, etc.
|
|
104
|
+
--cache Enable file hash caching, skip unchanged files
|
|
105
|
+
--clear-cache Clear cache directory
|
|
106
|
+
--parallel Enable multi-threaded parallel checking
|
|
107
|
+
--project Enable cross-file analysis (Phase 1b)
|
|
108
|
+
--sbcl Enable SBCL syntax validation (Phase 2)
|
|
109
|
+
--watch Watch mode: re-check on file changes
|
|
110
|
+
--hook-args <args> Custom pre-commit hook arguments (with --install-hook)
|
|
111
|
+
--help Show this help message
|
|
112
|
+
--version Show version number
|
|
113
|
+
--format-code Auto-format code indentation
|
|
114
|
+
--format-check Check if files are formatted (exit 1 if not)`,
|
|
89
115
|
},
|
|
90
116
|
zh: {
|
|
91
117
|
'parens.mismatch': "括号不匹配:{0} 个 '(' vs {1} 个 ')'({2})",
|
|
@@ -138,6 +164,7 @@ const messages = {
|
|
|
138
164
|
error_handling: "使用 command 但缺少 *error* 的函数: '{0}'",
|
|
139
165
|
global_naming: "全局变量 '{0}' 应使用 *...* 命名约定",
|
|
140
166
|
extra_parens: '可疑的过多闭合括号:多余 {0} 个 )',
|
|
167
|
+
format_indent: '代码缩进不符合格式化规范',
|
|
141
168
|
setq_single_arg: "变量 '{0}' 被 setq 但未赋值——可能是笔误",
|
|
142
169
|
assoc_without_cdr: 'assoc 的结果未被使用——应使用 cdr 获取值',
|
|
143
170
|
identical_branches: 'if 的 then 和 else 分支完全相同——可能是复制粘贴错误',
|
|
@@ -169,6 +196,31 @@ const messages = {
|
|
|
169
196
|
'index.watching': '监听中',
|
|
170
197
|
'index.watch_hint': '按 Ctrl+C 停止',
|
|
171
198
|
'index.watch_status': '已检查 {0} 个文件: {1} 个错误, {2} 个警告',
|
|
199
|
+
'index.format_unstable': '格式化结果不稳定——再次格式化后输出不同',
|
|
200
|
+
'help.text': `@atlisp/lint - AutoLISP 静态分析工具
|
|
201
|
+
|
|
202
|
+
用法: atlisp-lint [选项]
|
|
203
|
+
|
|
204
|
+
选项:
|
|
205
|
+
--src <目录> 指定源代码目录(可重复)
|
|
206
|
+
--test <目录> 指定测试目录(可重复)
|
|
207
|
+
--config <路径> 配置文件路径
|
|
208
|
+
--staged 仅检查 git 暂存区的 .lsp 文件
|
|
209
|
+
--format <格式> 输出格式:default | json
|
|
210
|
+
--init 在当前目录生成 atlisp-lint.json
|
|
211
|
+
--install-hook 安装 git pre-commit 钩子
|
|
212
|
+
--fix 自动修复尾部空格、BOM、尾部括号等
|
|
213
|
+
--cache 启用文件哈希缓存,跳过未变更的文件
|
|
214
|
+
--clear-cache 清除缓存目录
|
|
215
|
+
--parallel 启用多线程并行检查
|
|
216
|
+
--project 启用跨文件分析(Phase 1b)
|
|
217
|
+
--sbcl 启用 SBCL 语法验证(Phase 2)
|
|
218
|
+
--watch 监视模式:文件变更时重新检查
|
|
219
|
+
--hook-args <参数> 自定义 pre-commit 钩子参数(与 --install-hook 配合使用)
|
|
220
|
+
--help 显示此帮助信息
|
|
221
|
+
--version 显示版本号
|
|
222
|
+
--format-code 自动格式化代码缩进
|
|
223
|
+
--format-check 检查文件是否已格式化(未格式化则退出码为 1)`,
|
|
172
224
|
},
|
|
173
225
|
};
|
|
174
226
|
let currentLocale = 'zh';
|
package/dist/presets.js
CHANGED
package/dist/rules.js
CHANGED
|
@@ -82,6 +82,7 @@ exports.RULES = [
|
|
|
82
82
|
category: '复杂度',
|
|
83
83
|
},
|
|
84
84
|
{ name: 'function_order', defaultSeverity: 'off', description: '检测函数定义在使用之后', category: '风格' },
|
|
85
|
+
{ name: 'format_indent', defaultSeverity: 'off', description: '检测代码缩进是否符合格式化规范', category: '风格' },
|
|
85
86
|
{
|
|
86
87
|
name: 'identical_branches',
|
|
87
88
|
defaultSeverity: 'warn',
|
package/dist/runner.js
CHANGED
|
@@ -111,6 +111,7 @@ const dynamic_doc_1 = require("./checks/dynamic-doc");
|
|
|
111
111
|
const loop_optimization_1 = require("./checks/loop-optimization");
|
|
112
112
|
const type_check_1 = require("./checks/type-check");
|
|
113
113
|
const redundant_if_1 = require("./checks/redundant-if");
|
|
114
|
+
const format_indent_1 = require("./checks/format-indent");
|
|
114
115
|
const config_1 = require("./config");
|
|
115
116
|
const locale_1 = require("./locale");
|
|
116
117
|
const disable_1 = require("./disable");
|
|
@@ -158,7 +159,7 @@ function runChecks(content, file, config) {
|
|
|
158
159
|
}
|
|
159
160
|
function addIfEnabled(rule, fn) {
|
|
160
161
|
const severity = config.checks[rule];
|
|
161
|
-
if (severity === 'off')
|
|
162
|
+
if (!severity || severity === 'off')
|
|
162
163
|
return;
|
|
163
164
|
const results = fn();
|
|
164
165
|
for (const r of results) {
|
|
@@ -234,6 +235,7 @@ function runChecks(content, file, config) {
|
|
|
234
235
|
addIfEnabled('loop_optimization', () => (0, loop_optimization_1.checkLoopOptimization)(content, file));
|
|
235
236
|
addIfEnabled('type_check', () => (0, type_check_1.checkTypeCheck)(content, file));
|
|
236
237
|
addIfEnabled('redundant_if', () => (0, redundant_if_1.checkRedundantIf)(content, file));
|
|
238
|
+
addIfEnabled('format_indent', () => (0, format_indent_1.checkFormatIndent)(content, file));
|
|
237
239
|
return issues;
|
|
238
240
|
}
|
|
239
241
|
/** Single-pass visitor-based runChecks using AstVisitor */
|
|
@@ -413,6 +415,47 @@ function lintFilesParallel(files, config, rootDir) {
|
|
|
413
415
|
resolve([]);
|
|
414
416
|
});
|
|
415
417
|
}
|
|
418
|
+
/** Find the matching close paren for the first complete form starting at `start` */
|
|
419
|
+
function findMatchingParen(s, start) {
|
|
420
|
+
if (s[start] !== '(')
|
|
421
|
+
return start;
|
|
422
|
+
let depth = 0;
|
|
423
|
+
for (let i = start; i < s.length; i++) {
|
|
424
|
+
if (s[i] === '"') {
|
|
425
|
+
i++;
|
|
426
|
+
while (i < s.length && s[i] !== '"') {
|
|
427
|
+
if (s[i] === '\\')
|
|
428
|
+
i++;
|
|
429
|
+
i++;
|
|
430
|
+
}
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (s[i] === '(')
|
|
434
|
+
depth++;
|
|
435
|
+
else if (s[i] === ')') {
|
|
436
|
+
depth--;
|
|
437
|
+
if (depth === 0)
|
|
438
|
+
return i;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return s.length - 1;
|
|
442
|
+
}
|
|
443
|
+
/** Extract the next top-level form(s) from a string starting at `start`. Returns [form, endIdx]. */
|
|
444
|
+
function extractForm(s, start) {
|
|
445
|
+
let i = start;
|
|
446
|
+
while (i < s.length && s[i] === ' ')
|
|
447
|
+
i++;
|
|
448
|
+
if (i >= s.length)
|
|
449
|
+
return ['', i];
|
|
450
|
+
if (s[i] !== '(') {
|
|
451
|
+
const m = s.slice(i).match(/^\S+/);
|
|
452
|
+
if (m)
|
|
453
|
+
return [m[0], i + m[0].length];
|
|
454
|
+
return ['', i];
|
|
455
|
+
}
|
|
456
|
+
const end = findMatchingParen(s, i);
|
|
457
|
+
return [s.slice(i, end + 1), end + 1];
|
|
458
|
+
}
|
|
416
459
|
function applyFixes(issues, content, filepath) {
|
|
417
460
|
const lines = content.split('\n');
|
|
418
461
|
const fixesByRule = new Map();
|
|
@@ -430,6 +473,11 @@ function applyFixes(issues, content, filepath) {
|
|
|
430
473
|
'redundant_nil_else',
|
|
431
474
|
'single_arg_and_or',
|
|
432
475
|
'redundant_setq',
|
|
476
|
+
'redundant_progn',
|
|
477
|
+
'redundant_let',
|
|
478
|
+
'quote_style',
|
|
479
|
+
'redundant_if',
|
|
480
|
+
'misplaced_else',
|
|
433
481
|
]);
|
|
434
482
|
let result = content;
|
|
435
483
|
for (const [rule, lineSet] of fixesByRule) {
|
|
@@ -474,6 +522,88 @@ function applyFixes(issues, content, filepath) {
|
|
|
474
522
|
}
|
|
475
523
|
break;
|
|
476
524
|
}
|
|
525
|
+
case 'redundant_progn': {
|
|
526
|
+
// (progn <single-form>) → <single-form>
|
|
527
|
+
const proIdx = lineContent.indexOf('(progn ');
|
|
528
|
+
if (proIdx !== -1) {
|
|
529
|
+
const beforePro = lineContent.slice(0, proIdx);
|
|
530
|
+
const close = findMatchingParen(lineContent, proIdx);
|
|
531
|
+
if (close > proIdx) {
|
|
532
|
+
const inner = lineContent.slice(proIdx + 7, close);
|
|
533
|
+
const rest = lineContent.slice(close + 1);
|
|
534
|
+
const fixed = beforePro + inner + rest;
|
|
535
|
+
if (fixed !== lineContent)
|
|
536
|
+
result = replaceLine(result, idx, fixed);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
case 'redundant_let': {
|
|
542
|
+
// (let () body...) → (progn body...)
|
|
543
|
+
const fixed = lineContent.replace(/\(let\s+\(\)\s*/, '(progn ');
|
|
544
|
+
if (fixed !== lineContent)
|
|
545
|
+
result = replaceLine(result, idx, fixed);
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
case 'quote_style': {
|
|
549
|
+
// (quote x) → 'x
|
|
550
|
+
const qIdx = lineContent.indexOf('(quote ');
|
|
551
|
+
if (qIdx !== -1) {
|
|
552
|
+
const beforeQ = lineContent.slice(0, qIdx);
|
|
553
|
+
const close = findMatchingParen(lineContent, qIdx);
|
|
554
|
+
if (close > qIdx) {
|
|
555
|
+
const quoted = lineContent.slice(qIdx + 7, close);
|
|
556
|
+
const rest = lineContent.slice(close + 1);
|
|
557
|
+
const fixed = beforeQ + "'" + quoted + rest;
|
|
558
|
+
if (fixed !== lineContent)
|
|
559
|
+
result = replaceLine(result, idx, fixed);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
case 'redundant_if': {
|
|
565
|
+
// (if/when/unless (progn ...) ...) → remove inner progn
|
|
566
|
+
const pIdx = lineContent.indexOf('(progn ');
|
|
567
|
+
if (pIdx !== -1) {
|
|
568
|
+
const beforeP = lineContent.slice(0, pIdx);
|
|
569
|
+
const close = findMatchingParen(lineContent, pIdx);
|
|
570
|
+
if (close > pIdx) {
|
|
571
|
+
const inner = lineContent.slice(pIdx + 7, close);
|
|
572
|
+
const rest = lineContent.slice(close + 1);
|
|
573
|
+
const fixed = beforeP + inner + rest;
|
|
574
|
+
if (fixed !== lineContent)
|
|
575
|
+
result = replaceLine(result, idx, fixed);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
580
|
+
case 'misplaced_else': {
|
|
581
|
+
// (if (not x) then else) → (if x else then)
|
|
582
|
+
const ifStart = lineContent.indexOf('(if (not ');
|
|
583
|
+
if (ifStart !== -1) {
|
|
584
|
+
const notIdx = lineContent.indexOf('(not ', ifStart);
|
|
585
|
+
if (notIdx === -1)
|
|
586
|
+
break;
|
|
587
|
+
const notClose = findMatchingParen(lineContent, notIdx);
|
|
588
|
+
if (notClose <= notIdx)
|
|
589
|
+
break;
|
|
590
|
+
const ifClose = findMatchingParen(lineContent, ifStart);
|
|
591
|
+
if (ifClose <= ifStart)
|
|
592
|
+
break;
|
|
593
|
+
const indent = lineContent.slice(0, ifStart);
|
|
594
|
+
const condVar = lineContent.slice(notIdx + 5, notClose);
|
|
595
|
+
const branchContent = lineContent.slice(notClose + 1, ifClose).trim();
|
|
596
|
+
const [thenForm, thenEnd] = extractForm(branchContent, 0);
|
|
597
|
+
const elseForm = branchContent.slice(thenEnd).trim();
|
|
598
|
+
if (thenForm && elseForm) {
|
|
599
|
+
const rest = lineContent.slice(ifClose + 1);
|
|
600
|
+
const fixed = indent + '(if ' + condVar + ' ' + elseForm + ' ' + thenForm + ')' + rest;
|
|
601
|
+
if (fixed !== lineContent)
|
|
602
|
+
result = replaceLine(result, idx, fixed);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
break;
|
|
606
|
+
}
|
|
477
607
|
case 'redundant_setq': {
|
|
478
608
|
// Remove (setq x x) lines
|
|
479
609
|
const allLines = result.split('\n');
|
package/dist/validate.js
CHANGED