@atlisp/lint 0.2.11 → 0.2.13

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.
Files changed (48) hide show
  1. package/atlisp-lint.schema.json +32 -2
  2. package/dist/cache.js +19 -9
  3. package/dist/checks/builtin-arg-count.d.ts +1 -0
  4. package/dist/checks/builtin-arg-count.js +5 -0
  5. package/dist/checks/dangerous-calls.js +1 -0
  6. package/dist/checks/dangling-defun.d.ts +7 -0
  7. package/dist/checks/dangling-defun.js +19 -0
  8. package/dist/checks/global-function-naming.js +159 -35
  9. package/dist/checks/missing-princ.js +2 -2
  10. package/dist/checks/nth-usage.d.ts +1 -0
  11. package/dist/checks/nth-usage.js +16 -1
  12. package/dist/checks/unused-package-dep.d.ts +1 -0
  13. package/dist/checks/unused-package-dep.js +53 -0
  14. package/dist/cli/collector.d.ts +7 -0
  15. package/dist/cli/collector.js +154 -0
  16. package/dist/cli/parser.d.ts +30 -0
  17. package/dist/cli/parser.js +119 -0
  18. package/dist/config.js +32 -6
  19. package/dist/disable.js +12 -0
  20. package/dist/fixes/index.d.ts +8 -0
  21. package/dist/fixes/index.js +200 -0
  22. package/dist/index.js +56 -236
  23. package/dist/locale.js +30 -2
  24. package/dist/presets.js +24 -0
  25. package/dist/project.js +182 -134
  26. package/dist/rules.d.ts +1 -0
  27. package/dist/rules.js +85 -0
  28. package/dist/runner.d.ts +2 -3
  29. package/dist/runner.js +46 -371
  30. package/dist/types.d.ts +6 -0
  31. package/dist/utils.d.ts +4 -0
  32. package/dist/utils.js +43 -0
  33. package/dist/validate.js +12 -0
  34. package/dist/visitor-runner.d.ts +28 -0
  35. package/dist/visitor-runner.js +115 -1
  36. package/dist/visitors/equal-nil.d.ts +8 -0
  37. package/dist/visitors/equal-nil.js +26 -0
  38. package/dist/visitors/index.d.ts +5 -0
  39. package/dist/visitors/index.js +14 -0
  40. package/dist/visitors/progn-in-cond.d.ts +8 -0
  41. package/dist/visitors/progn-in-cond.js +33 -0
  42. package/dist/visitors/redundant-and-or.d.ts +8 -0
  43. package/dist/visitors/redundant-and-or.js +27 -0
  44. package/dist/visitors/redundant-car-cdr.d.ts +8 -0
  45. package/dist/visitors/redundant-car-cdr.js +35 -0
  46. package/dist/visitors/types.d.ts +13 -0
  47. package/dist/visitors/types.js +72 -0
  48. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -36,7 +36,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.main = main;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
- const child_process_1 = require("child_process");
40
39
  const config_1 = require("./config");
41
40
  const runner_1 = require("./runner");
42
41
  const project_1 = require("./project");
@@ -49,221 +48,8 @@ const locale_1 = require("./locale");
49
48
  const rules_1 = require("./rules");
50
49
  const cache_1 = require("./cache");
51
50
  const watch_1 = require("./watch");
52
- function parseArgs() {
53
- const argv = process.argv.slice(2);
54
- const opts = {
55
- src: [],
56
- test: [],
57
- staged: false,
58
- diff: false,
59
- changed: '',
60
- format: 'default',
61
- init: false,
62
- installHook: false,
63
- fix: false,
64
- hookArgs: '',
65
- cache: false,
66
- clearCache: false,
67
- parallel: false,
68
- project: false,
69
- watch: false,
70
- help: false,
71
- version: false,
72
- formatCode: false,
73
- formatCheck: false,
74
- sbcl: false,
75
- docs: false,
76
- };
77
- for (let i = 0; i < argv.length; i++) {
78
- switch (argv[i]) {
79
- case '--src':
80
- opts.src.push(argv[++i]);
81
- break;
82
- case '--test':
83
- opts.test.push(argv[++i]);
84
- break;
85
- case '--config':
86
- opts.config = argv[++i];
87
- break;
88
- case '--staged':
89
- opts.staged = true;
90
- break;
91
- case '--diff':
92
- opts.diff = true;
93
- break;
94
- case '--changed':
95
- opts.changed = argv[++i] || 'main';
96
- break;
97
- case '--format':
98
- opts.format = argv[++i];
99
- break;
100
- case '--init':
101
- opts.init = true;
102
- break;
103
- case '--install-hook':
104
- opts.installHook = true;
105
- break;
106
- case '--fix':
107
- opts.fix = true;
108
- break;
109
- case '--hook-args':
110
- opts.hookArgs = argv[++i];
111
- break;
112
- case '--cache':
113
- opts.cache = true;
114
- break;
115
- case '--clear-cache':
116
- opts.clearCache = true;
117
- break;
118
- case '--parallel':
119
- opts.parallel = true;
120
- break;
121
- case '--project':
122
- opts.project = true;
123
- break;
124
- case '--watch':
125
- opts.watch = true;
126
- break;
127
- case '--help':
128
- opts.help = true;
129
- break;
130
- case '--version':
131
- opts.version = true;
132
- break;
133
- case '--format-code':
134
- opts.formatCode = true;
135
- break;
136
- case '--format-check':
137
- opts.formatCheck = true;
138
- break;
139
- case '--sbcl':
140
- opts.sbcl = true;
141
- break;
142
- case '--docs':
143
- opts.docs = true;
144
- break;
145
- default:
146
- break;
147
- }
148
- }
149
- return opts;
150
- }
151
- // Zero-dependency glob: **/*.lsp → recursive walk + regex match
152
- function* walkFiles(dir, rootDir, regex) {
153
- let entries;
154
- try {
155
- entries = fs.readdirSync(dir);
156
- }
157
- catch {
158
- return;
159
- }
160
- for (const entry of entries) {
161
- const fullPath = path.join(dir, entry);
162
- let stat;
163
- try {
164
- stat = fs.statSync(fullPath);
165
- }
166
- catch {
167
- continue;
168
- }
169
- const rel = path.relative(rootDir, fullPath).replace(/\\/g, '/');
170
- if (rel.startsWith('..'))
171
- continue;
172
- if (stat.isDirectory()) {
173
- yield* walkFiles(fullPath, rootDir, regex);
174
- }
175
- else if (stat.isFile()) {
176
- if (regex.test(rel))
177
- yield fullPath;
178
- }
179
- }
180
- }
181
- function collectChangedFiles(rootDir, baseBranch) {
182
- try {
183
- const output = (0, child_process_1.execSync)(`git diff --name-only --diff-filter=ACM ${baseBranch}`, {
184
- cwd: rootDir,
185
- encoding: 'utf-8',
186
- });
187
- return output
188
- .split('\n')
189
- .map(l => l.trim())
190
- .filter(l => l.endsWith('.lsp'))
191
- .map(l => path.resolve(rootDir, l))
192
- .filter(f => fs.existsSync(f));
193
- }
194
- catch {
195
- return [];
196
- }
197
- }
198
- // Placeholder regex (dynamically constructed to avoid no-control-regex)
199
- const GLOB_PLACEHOLDER = '\x00GLOB\x00';
200
- const GLOB_RE = new RegExp(GLOB_PLACEHOLDER, 'g');
201
- function globFiles(pattern, rootDir) {
202
- const regexStr = pattern
203
- .replace(/\./g, '\\.')
204
- .replace(/\*\*\//g, GLOB_PLACEHOLDER)
205
- .replace(/\*\*/g, '.*')
206
- .replace(/\*/g, '[^/]*')
207
- .replace(/\?/g, '.')
208
- .replace(GLOB_RE, '(.*/)?');
209
- const regex = new RegExp(`^${regexStr}$`);
210
- return Array.from(walkFiles(rootDir, rootDir, regex));
211
- }
212
- function collectFiles(rootDir, opts, config) {
213
- const files = new Set();
214
- if (opts.src.length > 0 || opts.test.length > 0) {
215
- // Walk specific directories
216
- for (const d of opts.src) {
217
- for (const f of globFiles('**/*.lsp', path.resolve(rootDir, d))) {
218
- files.add(f);
219
- }
220
- }
221
- for (const d of opts.test) {
222
- for (const f of globFiles('**/*.lsp', path.resolve(rootDir, d))) {
223
- files.add(f);
224
- }
225
- }
226
- }
227
- else {
228
- // Use config globs relative to rootDir
229
- for (const pattern of config.source.globs) {
230
- for (const f of globFiles(pattern, rootDir)) {
231
- files.add(f);
232
- }
233
- }
234
- }
235
- const excludePatterns = config.source.exclude.map(e => {
236
- const str = e
237
- .replace(/\./g, '\\.')
238
- .replace(/\*\*\//g, GLOB_PLACEHOLDER)
239
- .replace(/\*\*/g, '.*')
240
- .replace(/\*/g, '[^/]*')
241
- .replace(GLOB_RE, '(.*/)?');
242
- return new RegExp(`^${str}$`);
243
- });
244
- return Array.from(files)
245
- .filter(f => {
246
- const rel = path.relative(rootDir, f).replace(/\\/g, '/');
247
- for (const re of excludePatterns) {
248
- if (re.test(rel))
249
- return false;
250
- }
251
- return true;
252
- })
253
- .sort();
254
- }
255
- function collectStagedFiles(rootDir) {
256
- const output = (0, child_process_1.execSync)('git diff --cached --name-only --diff-filter=ACM', {
257
- cwd: rootDir,
258
- encoding: 'utf-8',
259
- });
260
- return output
261
- .split('\n')
262
- .map(l => l.trim())
263
- .filter(l => l.endsWith('.lsp'))
264
- .map(l => path.resolve(rootDir, l))
265
- .filter(f => fs.existsSync(f));
266
- }
51
+ const parser_1 = require("./cli/parser");
52
+ const collector_1 = require("./cli/collector");
267
53
  function initConfig(rootDir) {
268
54
  const defaultPath = path.join(__dirname, '..', 'atlisp-lint.default.json');
269
55
  const targetPath = path.join(rootDir, 'atlisp-lint.json');
@@ -296,12 +82,20 @@ function printHelp() {
296
82
  }
297
83
  async function main() {
298
84
  try {
299
- const opts = parseArgs();
85
+ const opts = (0, parser_1.parseArgs)();
300
86
  const rootDir = process.cwd();
301
87
  // Load config early so locale is available for --help and other commands
302
88
  const configPath = opts.config || path.join(rootDir, 'atlisp-lint.json');
303
89
  const config = (0, config_1.loadConfig)(fs.existsSync(configPath) ? configPath : undefined);
304
90
  (0, locale_1.setLocale)(config.locale || 'zh');
91
+ if (opts.listRules) {
92
+ console.log('Rule'.padEnd(30) + 'Default'.padEnd(10) + 'Category'.padEnd(14) + 'Description');
93
+ console.log('-'.repeat(90));
94
+ for (const r of rules_1.RULES) {
95
+ console.log(r.name.padEnd(30) + r.defaultSeverity.padEnd(10) + r.category.padEnd(14) + r.description);
96
+ }
97
+ return;
98
+ }
305
99
  if (opts.docs) {
306
100
  console.log((0, rules_1.generateRulesMarkdown)());
307
101
  console.log();
@@ -331,14 +125,18 @@ async function main() {
331
125
  console.log((0, locale_1.t)('index.cache_cleared'));
332
126
  return;
333
127
  }
128
+ if (opts.explain) {
129
+ console.log((0, rules_1.generateRuleDetail)(opts.explain));
130
+ return;
131
+ }
334
132
  // Collect files
335
133
  const files = opts.staged
336
- ? collectStagedFiles(rootDir)
134
+ ? (0, collector_1.collectStagedFiles)(rootDir)
337
135
  : opts.diff
338
- ? collectChangedFiles(rootDir, 'HEAD')
136
+ ? (0, collector_1.collectChangedFiles)(rootDir, 'HEAD')
339
137
  : opts.changed
340
- ? collectChangedFiles(rootDir, opts.changed)
341
- : collectFiles(rootDir, opts, config);
138
+ ? (0, collector_1.collectChangedFiles)(rootDir, opts.changed)
139
+ : (0, collector_1.collectFiles)(rootDir, opts, config);
342
140
  if (files.length === 0) {
343
141
  console.log((0, locale_1.t)('index.no_files'));
344
142
  return;
@@ -384,9 +182,17 @@ async function main() {
384
182
  // --fix mode: auto-fix before linting
385
183
  if (opts.fix) {
386
184
  for (const f of files) {
185
+ const relFile = path.relative(rootDir, f);
186
+ if (opts.dryRun) {
187
+ const content = fs.readFileSync(f, 'utf-8');
188
+ const bomFix = content.charCodeAt(0) === 0xfeff;
189
+ if (bomFix)
190
+ console.log(`${(0, locale_1.t)('summary.tag_info')}: ${relFile} — would fix BOM`);
191
+ continue;
192
+ }
387
193
  const fixes = (0, runner_1.fixFile)(f);
388
194
  for (const rule of fixes) {
389
- console.log(`${(0, locale_1.t)('summary.tag_fail')}: ${path.relative(rootDir, f)} — ${(0, locale_1.t)('index.fixed', rule)}`);
195
+ console.log(`${(0, locale_1.t)('summary.tag_fail')}: ${relFile} — ${(0, locale_1.t)('index.fixed', rule)}`);
390
196
  }
391
197
  }
392
198
  // format_indent fix for non-staged mode (no lint results available yet)
@@ -401,8 +207,8 @@ async function main() {
401
207
  console.log(`${(0, locale_1.t)('summary.tag_fail')}: ${path.relative(rootDir, f)} — ${(0, locale_1.t)('index.fixed', 'format_indent')}`);
402
208
  }
403
209
  }
404
- catch {
405
- /* ignore */
210
+ catch (e) {
211
+ console.warn(`[lint] format_indent fix failed for ${path.relative(rootDir, f)}: ${e instanceof Error ? e.message : String(e)}`);
406
212
  }
407
213
  }
408
214
  }
@@ -434,9 +240,10 @@ async function main() {
434
240
  return;
435
241
  }
436
242
  // Phase 1: Static checks (parallel or sequential)
243
+ const contentArg = opts.cache && contentCache.size > 0 ? contentCache : undefined;
437
244
  const issues = opts.parallel
438
245
  ? await (0, runner_1.lintFilesParallel)(filesToLint, config, rootDir)
439
- : (0, runner_1.lintFiles)(filesToLint, config, rootDir);
246
+ : (0, runner_1.lintFiles)(filesToLint, config, rootDir, contentArg);
440
247
  // Phase 1b: Project-level cross-file checks
441
248
  if (opts.project) {
442
249
  const projectIssues = (0, project_1.lintProject)(filesToLint, config, rootDir);
@@ -452,12 +259,17 @@ async function main() {
452
259
  const content = fs.readFileSync(f, 'utf-8');
453
260
  const fixed = (0, runner_1.applyFixes)(fileIssues, content, relPath);
454
261
  if (fixed !== content) {
455
- fs.writeFileSync(f, fixed, 'utf-8');
456
- console.log(`${(0, locale_1.t)('summary.tag_fail')}: ${relPath} — ${(0, locale_1.t)('index.fixed')}`);
262
+ if (opts.dryRun) {
263
+ console.log(`${(0, locale_1.t)('summary.tag_info')}: ${relPath} — would apply ${fileIssues.length} fix(es)`);
264
+ }
265
+ else {
266
+ fs.writeFileSync(f, fixed, 'utf-8');
267
+ console.log(`${(0, locale_1.t)('summary.tag_fail')}: ${relPath} — ${(0, locale_1.t)('index.fixed')}`);
268
+ }
457
269
  }
458
270
  }
459
- catch {
460
- /* ignore */
271
+ catch (e) {
272
+ console.warn(`[lint] applyFixes failed for ${relPath}: ${e instanceof Error ? e.message : String(e)}`);
461
273
  }
462
274
  }
463
275
  }
@@ -472,18 +284,23 @@ async function main() {
472
284
  const content = fs.readFileSync(f, 'utf-8');
473
285
  const formatted = (0, formatter_1.formatCode)(content, indentSize);
474
286
  if (formatted !== content) {
475
- fs.writeFileSync(f, formatted, 'utf-8');
476
- console.log(`${(0, locale_1.t)('summary.tag_fail')}: ${relPath} — ${(0, locale_1.t)('index.fixed', 'format_indent')}`);
287
+ if (opts.dryRun) {
288
+ console.log(`${(0, locale_1.t)('summary.tag_info')}: ${relPath} — would fix format_indent`);
289
+ }
290
+ else {
291
+ fs.writeFileSync(f, formatted, 'utf-8');
292
+ console.log(`${(0, locale_1.t)('summary.tag_fail')}: ${relPath} — ${(0, locale_1.t)('index.fixed', 'format_indent')}`);
293
+ }
477
294
  }
478
295
  }
479
- catch {
480
- /* ignore */
296
+ catch (e) {
297
+ console.warn(`[lint] format_indent fix failed for ${relPath}: ${e instanceof Error ? e.message : String(e)}`);
481
298
  }
482
299
  }
483
300
  }
484
301
  }
485
302
  }
486
- // Mark as cached (reuse pre-read content if available)
303
+ // Mark as cached
487
304
  if (opts.cache) {
488
305
  for (const f of filesToLint) {
489
306
  (0, cache_1.markCached)(f, rootDir, contentCache.get(f));
@@ -515,8 +332,8 @@ async function main() {
515
332
  try {
516
333
  fileContents.set(iss.file, fs.readFileSync(fp, 'utf-8'));
517
334
  }
518
- catch {
519
- /* ignore */
335
+ catch (e) {
336
+ console.warn(`[lint] failed to read ${fp} for display: ${e instanceof Error ? e.message : String(e)}`);
520
337
  }
521
338
  }
522
339
  }
@@ -535,6 +352,9 @@ async function main() {
535
352
  else {
536
353
  console.log((0, formatters_1.formatDefault)(issues, fileContents));
537
354
  }
355
+ if (opts.maxWarnings >= 0 && warningCount > opts.maxWarnings) {
356
+ process.exit(1);
357
+ }
538
358
  process.exit(errorCount > 0 ? 1 : 0);
539
359
  }
540
360
  catch (err) {
package/dist/locale.js CHANGED
@@ -105,6 +105,19 @@ const messages = {
105
105
  redundant_entmake: '(entmake (list (cons ...))) should use quoted association list for performance',
106
106
  missing_princ: "C: command '{0}' is missing trailing (princ) — may return nil to the command line",
107
107
  global_function_naming: "Function '{0}' lacks a package prefix — consider using namespace: prefix",
108
+ 'dangerous.eval': 'eval call — may execute arbitrary code (code injection risk)',
109
+ command_s: '(command ...) call — use (command-s ...) for better error handling',
110
+ zerop_usage: 'Use (zerop {0}) instead of comparing with zero',
111
+ progn_in_body: 'Redundant (progn ...) in {0} body — {0} already allows multiple forms',
112
+ if_progn_both: 'Both branches of if use progn — use (cond (test ...) (t ...)) instead',
113
+ redundant_cond: 'Single-clause cond — use (if ...) instead',
114
+ selection_set_leak: 'More ssget calls ({0}) than ssset calls ({1}) — possible selection set leak',
115
+ empty_branch_cond: 'Empty cond clause — no body forms after condition',
116
+ function_reference: "Function '{0}' is referenced but never defined",
117
+ progn_in_cond: 'Redundant (progn ...) in cond clause — cond already allows multiple forms',
118
+ redundant_car_cdr: '({0} ({1} x)) can be simplified to ({2} x)',
119
+ equal_nil: 'Use (null {0}) or (not {0}) instead of (equal {0} nil)',
120
+ redundant_and_or: '{0} — may be leftover code',
108
121
  'help.text': `@atlisp/lint - AutoLISP static analysis tool
109
122
 
110
123
  Usage: atlisp-lint [options]
@@ -131,7 +144,8 @@ Options:
131
144
  --format-check Check if files are formatted (exit 1 if not)
132
145
  --diff Only check files changed since last commit
133
146
  --changed <branch> Only check files changed against a branch (default: main)
134
- --docs Print rules documentation as markdown`,
147
+ --docs Print rules documentation as markdown
148
+ --list-rules List all rules with descriptions and default severities`,
135
149
  },
136
150
  zh: {
137
151
  'parens.mismatch': "括号不匹配:{0} 个 '(' vs {1} 个 ')'({2})",
@@ -234,6 +248,19 @@ Options:
234
248
  redundant_entmake: '(entmake (list (cons ...))) 应使用 quoted 关联表以提升性能',
235
249
  missing_princ: "C: 命令 '{0}' 末尾缺少 (princ) — 可能在命令行返回 nil",
236
250
  global_function_naming: "函数 '{0}' 缺少包名前缀 — 建议使用 namespace: 前缀",
251
+ 'dangerous.eval': 'eval 调用 —— 可能执行任意代码(代码注入风险)',
252
+ command_s: '(command ...) 调用 —— 建议使用 (command-s ...) 以获得更好的错误处理',
253
+ zerop_usage: '建议使用 (zerop {0}) 替代与零的比较',
254
+ progn_in_body: '{0} 体中冗余的 (progn ...) —— {0} 已隐含 progn,可直接写多个表达式',
255
+ if_progn_both: 'if 的两个分支都使用了 progn —— 建议改用 (cond (test ...) (t ...))',
256
+ redundant_cond: '单子句 cond —— 建议改用 (if ...)',
257
+ selection_set_leak: 'ssget 调用 ({0} 次) 超过 ssset 调用 ({1} 次) —— 可能造成选择集泄漏',
258
+ empty_branch_cond: 'cond 子句为空 —— 条件后没有表达式',
259
+ function_reference: "函数 '{0}' 被引用但从未定义",
260
+ progn_in_cond: 'cond 子句中冗余的 (progn ...) — cond 已隐含 progn',
261
+ redundant_car_cdr: '({0} ({1} x)) 可简化为 ({2} x)',
262
+ equal_nil: '建议使用 (null {0}) 或 (not {0}) 替代 (equal {0} nil)',
263
+ redundant_and_or: '{0} —— 可能是残留代码',
237
264
  'help.text': `@atlisp/lint - AutoLISP 静态分析工具
238
265
 
239
266
  用法: atlisp-lint [选项]
@@ -260,7 +287,8 @@ Options:
260
287
  --format-check 检查文件是否已格式化(未格式化则退出码为 1)
261
288
  --diff 仅检查自上次提交后变更的文件
262
289
  --changed <分支> 仅检查相对于某分支有变更的文件(默认:main)
263
- --docs 打印规则文档(Markdown 格式)`,
290
+ --docs 打印规则文档(Markdown 格式)
291
+ --list-rules 列出所有规则及其描述和默认严重级别`,
264
292
  },
265
293
  };
266
294
  let currentLocale = 'zh';
package/dist/presets.js CHANGED
@@ -78,6 +78,18 @@ exports.PRESETS = {
78
78
  redundant_entmake: 'warn',
79
79
  missing_princ: 'warn',
80
80
  global_function_naming: 'info',
81
+ command_s: 'warn',
82
+ zerop_usage: 'info',
83
+ progn_in_body: 'info',
84
+ if_progn_both: 'info',
85
+ redundant_cond: 'info',
86
+ selection_set_leak: 'warn',
87
+ empty_branch_cond: 'warn',
88
+ function_reference: 'warn',
89
+ progn_in_cond: 'info',
90
+ redundant_car_cdr: 'info',
91
+ equal_nil: 'info',
92
+ redundant_and_or: 'info',
81
93
  },
82
94
  },
83
95
  strict: {
@@ -168,6 +180,18 @@ exports.PRESETS = {
168
180
  redundant_entmake: 'error',
169
181
  missing_princ: 'error',
170
182
  global_function_naming: 'error',
183
+ command_s: 'error',
184
+ zerop_usage: 'error',
185
+ progn_in_body: 'error',
186
+ if_progn_both: 'error',
187
+ redundant_cond: 'error',
188
+ selection_set_leak: 'error',
189
+ empty_branch_cond: 'error',
190
+ function_reference: 'error',
191
+ progn_in_cond: 'error',
192
+ redundant_car_cdr: 'error',
193
+ equal_nil: 'error',
194
+ redundant_and_or: 'error',
171
195
  },
172
196
  },
173
197
  relaxed: {