@atlisp/lint 0.1.5 → 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.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -58
  3. package/atlisp-lint.default.json +32 -1
  4. package/dist/atlisp-lint.default.json +90 -0
  5. package/dist/cache.d.ts +2 -2
  6. package/dist/cache.js +6 -6
  7. package/dist/checks/append-single.d.ts +3 -0
  8. package/dist/checks/append-single.js +17 -0
  9. package/dist/checks/arg-count.d.ts +3 -0
  10. package/dist/checks/arg-count.js +123 -0
  11. package/dist/checks/assoc-without-cdr.d.ts +5 -0
  12. package/dist/checks/assoc-without-cdr.js +32 -0
  13. package/dist/checks/comment-style.d.ts +3 -0
  14. package/dist/checks/comment-style.js +24 -0
  15. package/dist/checks/cond-duplicate.d.ts +5 -0
  16. package/dist/checks/cond-duplicate.js +52 -0
  17. package/dist/checks/cond-simplify.d.ts +3 -0
  18. package/dist/checks/cond-simplify.js +45 -0
  19. package/dist/checks/constant-condition.js +4 -4
  20. package/dist/checks/dangerous-calls.js +2 -2
  21. package/dist/checks/dangling-defun.d.ts +3 -0
  22. package/dist/checks/dangling-defun.js +10 -28
  23. package/dist/checks/double-not.js +1 -1
  24. package/dist/checks/duplicate-defun.d.ts +6 -0
  25. package/dist/checks/duplicate-defun.js +50 -0
  26. package/dist/checks/dynamic-doc.d.ts +3 -0
  27. package/dist/checks/dynamic-doc.js +21 -0
  28. package/dist/checks/empty-branch.js +1 -1
  29. package/dist/checks/empty-catch.d.ts +3 -0
  30. package/dist/checks/empty-catch.js +34 -0
  31. package/dist/checks/eq-usage.d.ts +3 -0
  32. package/dist/checks/eq-usage.js +25 -0
  33. package/dist/checks/error-handling.d.ts +3 -0
  34. package/dist/checks/error-handling.js +56 -0
  35. package/dist/checks/extra-parens.d.ts +3 -0
  36. package/dist/checks/extra-parens.js +45 -0
  37. package/dist/checks/format-indent.d.ts +3 -0
  38. package/dist/checks/format-indent.js +29 -0
  39. package/dist/checks/function-complexity.js +1 -1
  40. package/dist/checks/function-order.d.ts +3 -0
  41. package/dist/checks/function-order.js +33 -0
  42. package/dist/checks/global-naming.d.ts +3 -0
  43. package/dist/checks/global-naming.js +62 -0
  44. package/dist/checks/identical-branches.d.ts +5 -0
  45. package/dist/checks/identical-branches.js +54 -0
  46. package/dist/checks/index.d.ts +3 -0
  47. package/dist/checks/index.js +117 -0
  48. package/dist/checks/lambda-syntax.d.ts +3 -0
  49. package/dist/checks/lambda-syntax.js +25 -0
  50. package/dist/checks/long-function-call.d.ts +3 -0
  51. package/dist/checks/long-function-call.js +54 -0
  52. package/dist/checks/loop-optimization.d.ts +3 -0
  53. package/dist/checks/loop-optimization.js +17 -0
  54. package/dist/checks/magic-number.d.ts +3 -0
  55. package/dist/checks/magic-number.js +21 -0
  56. package/dist/checks/misplaced-else.js +1 -1
  57. package/dist/checks/missing-export.js +2 -2
  58. package/dist/checks/mixed-indent.d.ts +3 -0
  59. package/dist/checks/mixed-indent.js +19 -0
  60. package/dist/checks/module-reg.js +1 -1
  61. package/dist/checks/multiple-setq.js +1 -1
  62. package/dist/checks/no-return.d.ts +3 -0
  63. package/dist/checks/no-return.js +51 -0
  64. package/dist/checks/nth-usage.d.ts +3 -0
  65. package/dist/checks/nth-usage.js +17 -0
  66. package/dist/checks/quote-style.d.ts +3 -0
  67. package/dist/checks/quote-style.js +25 -0
  68. package/dist/checks/quote-vs-function.js +1 -1
  69. package/dist/checks/recursive-call.js +1 -1
  70. package/dist/checks/redundant-if.d.ts +3 -0
  71. package/dist/checks/redundant-if.js +17 -0
  72. package/dist/checks/redundant-let.js +1 -1
  73. package/dist/checks/redundant-nil-else.js +1 -1
  74. package/dist/checks/redundant-progn.js +1 -1
  75. package/dist/checks/redundant-quotes.js +1 -1
  76. package/dist/checks/redundant-setq.js +1 -1
  77. package/dist/checks/self-compare.js +1 -1
  78. package/dist/checks/setq-multiple.d.ts +3 -0
  79. package/dist/checks/setq-multiple.js +17 -0
  80. package/dist/checks/setq-single-arg.d.ts +5 -0
  81. package/dist/checks/setq-single-arg.js +30 -0
  82. package/dist/checks/shadow-builtin.d.ts +3 -0
  83. package/dist/checks/shadow-builtin.js +77 -0
  84. package/dist/checks/single-arg-and-or.js +1 -1
  85. package/dist/checks/strcat-usage.d.ts +3 -0
  86. package/dist/checks/strcat-usage.js +25 -0
  87. package/dist/checks/type-check.d.ts +3 -0
  88. package/dist/checks/type-check.js +26 -0
  89. package/dist/checks/unused-let.js +1 -1
  90. package/dist/checks/unused-package-dep.js +3 -3
  91. package/dist/checks/variable-shadow.js +1 -1
  92. package/dist/checks/while-constant.d.ts +5 -0
  93. package/dist/checks/while-constant.js +40 -0
  94. package/dist/config.d.ts +1 -0
  95. package/dist/config.js +71 -2
  96. package/dist/disable.js +1 -1
  97. package/dist/formatter.d.ts +2 -0
  98. package/dist/formatter.js +51 -0
  99. package/dist/formatters.d.ts +1 -0
  100. package/dist/formatters.js +18 -2
  101. package/dist/index.js +172 -32
  102. package/dist/lib/lint-sbcl.lisp +161 -0
  103. package/dist/locale.js +76 -0
  104. package/dist/presets.d.ts +4 -0
  105. package/dist/presets.js +159 -0
  106. package/dist/project.js +37 -6
  107. package/dist/rules.d.ts +9 -0
  108. package/dist/rules.js +239 -0
  109. package/dist/runner.d.ts +2 -0
  110. package/dist/runner.js +329 -12
  111. package/dist/sbcl.js +1 -1
  112. package/dist/stub-packages.json +41 -0
  113. package/dist/types.d.ts +6 -0
  114. package/dist/validate.d.ts +8 -0
  115. package/dist/validate.js +126 -0
  116. package/dist/watch.d.ts +9 -0
  117. package/dist/watch.js +113 -0
  118. package/package.json +1 -1
package/dist/config.js CHANGED
@@ -34,10 +34,12 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.loadConfig = loadConfig;
37
+ exports.getCheckSeverity = getCheckSeverity;
37
38
  exports.findOverrides = findOverrides;
38
39
  exports.mergeOverrides = mergeOverrides;
39
40
  const fs = __importStar(require("fs"));
40
41
  const path = __importStar(require("path"));
42
+ const presets_1 = require("./presets");
41
43
  const DEFAULT_CONFIG = {
42
44
  locale: 'zh',
43
45
  source: {
@@ -56,14 +58,70 @@ const DEFAULT_CONFIG = {
56
58
  token_in_url: 'warn',
57
59
  open_without_close: 'warn',
58
60
  bare_function_names: 'warn',
59
- trailing_whitespace: 'warn',
60
61
  line_length: 'warn',
61
62
  function_complexity: 'warn',
62
63
  parameter_naming: 'warn',
63
64
  unused_variable: 'warn',
64
65
  missing_doc: 'warn',
66
+ trailing_whitespace: 'warn',
65
67
  module_registration: 'off',
66
68
  namespace_header: 'off',
69
+ unused_parameter: 'warn',
70
+ constant_condition: 'warn',
71
+ redundant_progn: 'warn',
72
+ empty_branch: 'warn',
73
+ unused_let_binding: 'warn',
74
+ recursive_call: 'warn',
75
+ variable_shadow: 'warn',
76
+ redundant_cond: 'warn',
77
+ unused_local_fun: 'warn',
78
+ multiple_setq: 'warn',
79
+ redundant_quotes: 'warn',
80
+ trailing_paren: 'warn',
81
+ empty_comment: 'warn',
82
+ redundant_setq: 'warn',
83
+ redundant_nil_else: 'warn',
84
+ single_arg_and_or: 'warn',
85
+ redundant_let: 'warn',
86
+ self_compare: 'warn',
87
+ misplaced_else: 'warn',
88
+ quote_vs_function: 'warn',
89
+ commented_code: 'warn',
90
+ double_not: 'warn',
91
+ setq_single_arg: 'warn',
92
+ assoc_without_cdr: 'warn',
93
+ identical_branches: 'warn',
94
+ while_constant: 'warn',
95
+ cond_duplicate: 'warn',
96
+ duplicate_defun: 'warn',
97
+ dangling_defun: 'warn',
98
+ missing_export: 'warn',
99
+ unused_package_dep: 'warn',
100
+ error_handling: 'warn',
101
+ global_naming: 'warn',
102
+ extra_parens: 'warn',
103
+ arg_count: 'warn',
104
+ strcat_usage: 'warn',
105
+ cond_simplify: 'warn',
106
+ quote_style: 'warn',
107
+ eq_usage: 'warn',
108
+ lambda_syntax: 'warn',
109
+ comment_style: 'warn',
110
+ empty_catch: 'warn',
111
+ nth_usage: 'warn',
112
+ append_single: 'warn',
113
+ setq_multiple: 'warn',
114
+ function_order: 'off',
115
+ magic_number: 'off',
116
+ mixed_indent: 'warn',
117
+ long_function_call: 'warn',
118
+ no_return: 'warn',
119
+ shadow_builtin: 'warn',
120
+ dynamic_doc: 'warn',
121
+ loop_optimization: 'off',
122
+ type_check: 'off',
123
+ format_indent: 'off',
124
+ redundant_if: 'warn',
67
125
  },
68
126
  line_length: {
69
127
  max: 100,
@@ -111,9 +169,17 @@ function loadConfig(configPath) {
111
169
  }
112
170
  const raw = fs.readFileSync(absPath, 'utf-8');
113
171
  const user = JSON.parse(raw);
114
- // Deep merge: start from default, overlay user values
172
+ // Start from default config
115
173
  const merged = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
174
+ // Apply preset if specified (preset applies before user overrides)
175
+ const preset = user.preset;
176
+ if (preset && presets_1.PRESETS[preset]) {
177
+ deepMerge(merged, presets_1.PRESETS[preset]);
178
+ }
179
+ // Apply user overrides (user settings take precedence)
116
180
  deepMerge(merged, user);
181
+ // Remove non-config keys
182
+ delete merged.preset;
117
183
  return merged;
118
184
  }
119
185
  return JSON.parse(JSON.stringify(DEFAULT_CONFIG));
@@ -133,6 +199,9 @@ function deepMerge(target, source) {
133
199
  }
134
200
  }
135
201
  }
202
+ function getCheckSeverity(config, rule) {
203
+ return config.checks[rule] || 'off';
204
+ }
136
205
  function findOverrides(filepath) {
137
206
  const dir = path.dirname(filepath);
138
207
  const configPath = path.join(dir, '.atlisp-lint.json');
package/dist/disable.js CHANGED
@@ -12,7 +12,7 @@ function parseDisableComments(content, fileLine) {
12
12
  continue;
13
13
  const rules = m[1]
14
14
  .split(',')
15
- .map((r) => r.trim())
15
+ .map(r => r.trim())
16
16
  .filter(Boolean);
17
17
  let targetLine = i + 1;
18
18
  if (fileLine !== undefined)
@@ -0,0 +1,2 @@
1
+ export declare function formatCode(content: string, indentSize?: number): string;
2
+ //# sourceMappingURL=formatter.d.ts.map
@@ -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
@@ -1,4 +1,5 @@
1
1
  import { Issue } from './types';
2
2
  export declare function formatDefault(issues: Issue[], fileContents?: Map<string, string>): string;
3
+ export declare function formatHtml(issues: Issue[], errorCount: number, warningCount: number): string;
3
4
  export declare function formatJson(issues: Issue[], errorCount?: number, warningCount?: number): string;
4
5
  //# sourceMappingURL=formatters.d.ts.map
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.formatDefault = formatDefault;
4
+ exports.formatHtml = formatHtml;
4
5
  exports.formatJson = formatJson;
5
6
  const locale_1 = require("./locale");
6
7
  const colors = {
@@ -68,11 +69,26 @@ function formatDefault(issues, fileContents) {
68
69
  }
69
70
  return lines.join('\n');
70
71
  }
72
+ function formatHtml(issues, errorCount, warningCount) {
73
+ let html = `<!DOCTYPE html>
74
+ <html lang="en">
75
+ <head><meta charset="UTF-8"><title>@atlisp/lint Report</title></head>
76
+ <body>
77
+ <h1>@atlisp/lint Report</h1>
78
+ <p>${errorCount} error(s), ${warningCount} warning(s)</p>
79
+ <ul>
80
+ `;
81
+ for (const iss of issues) {
82
+ html += ` <li>${iss.severity}: ${iss.file}:${iss.line} — ${iss.message}</li>\n`;
83
+ }
84
+ html += '</ul>\n</body>\n</html>';
85
+ return html;
86
+ }
71
87
  function formatJson(issues, errorCount, warningCount) {
72
88
  return JSON.stringify({
73
89
  issues,
74
- error_count: errorCount ?? issues.filter((i) => i.severity === 'error').length,
75
- warning_count: warningCount ?? issues.filter((i) => i.severity === 'warn').length,
90
+ error_count: errorCount ?? issues.filter(i => i.severity === 'error').length,
91
+ warning_count: warningCount ?? issues.filter(i => i.severity === 'warn').length,
76
92
  }, null, 2);
77
93
  }
78
94
  //# sourceMappingURL=formatters.js.map
package/dist/index.js CHANGED
@@ -42,8 +42,10 @@ 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");
48
+ const watch_1 = require("./watch");
47
49
  function parseArgs() {
48
50
  const argv = process.argv.slice(2);
49
51
  const opts = {
@@ -59,6 +61,12 @@ function parseArgs() {
59
61
  clearCache: false,
60
62
  parallel: false,
61
63
  project: false,
64
+ watch: false,
65
+ help: false,
66
+ version: false,
67
+ formatCode: false,
68
+ formatCheck: false,
69
+ sbcl: false,
62
70
  };
63
71
  for (let i = 0; i < argv.length; i++) {
64
72
  switch (argv[i]) {
@@ -101,6 +109,24 @@ function parseArgs() {
101
109
  case '--project':
102
110
  opts.project = true;
103
111
  break;
112
+ case '--watch':
113
+ opts.watch = true;
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;
104
130
  default:
105
131
  break;
106
132
  }
@@ -168,12 +194,12 @@ function collectFiles(rootDir, opts, config) {
168
194
  }
169
195
  }
170
196
  }
171
- const excludePatterns = config.source.exclude.map((e) => {
197
+ const excludePatterns = config.source.exclude.map(e => {
172
198
  const str = e.replace(/\./g, '\\.').replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*');
173
199
  return new RegExp(`^${str}$`);
174
200
  });
175
201
  return Array.from(files)
176
- .filter((f) => {
202
+ .filter(f => {
177
203
  const rel = path.relative(rootDir, f).replace(/\\/g, '/');
178
204
  for (const re of excludePatterns) {
179
205
  if (re.test(rel))
@@ -190,10 +216,10 @@ function collectStagedFiles(rootDir) {
190
216
  });
191
217
  return output
192
218
  .split('\n')
193
- .map((l) => l.trim())
194
- .filter((l) => l.endsWith('.lsp'))
195
- .map((l) => path.resolve(rootDir, l))
196
- .filter((f) => fs.existsSync(f));
219
+ .map(l => l.trim())
220
+ .filter(l => l.endsWith('.lsp'))
221
+ .map(l => path.resolve(rootDir, l))
222
+ .filter(f => fs.existsSync(f));
197
223
  }
198
224
  function initConfig(rootDir) {
199
225
  const defaultPath = path.join(__dirname, '..', 'atlisp-lint.default.json');
@@ -222,10 +248,27 @@ npx @atlisp/lint ${args} || exit 1
222
248
  fs.chmodSync(hookPath, 0o755);
223
249
  console.log((0, locale_1.t)('index.hook_installed', hookPath));
224
250
  }
251
+ function printHelp() {
252
+ console.log((0, locale_1.t)('help.text'));
253
+ }
225
254
  async function main() {
226
255
  try {
227
256
  const opts = parseArgs();
228
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
+ }
229
272
  if (opts.init) {
230
273
  initConfig(rootDir);
231
274
  return;
@@ -239,16 +282,45 @@ async function main() {
239
282
  console.log((0, locale_1.t)('index.cache_cleared'));
240
283
  return;
241
284
  }
242
- // Load config
243
- const configPath = opts.config || path.join(rootDir, 'atlisp-lint.json');
244
- const config = (0, config_1.loadConfig)(fs.existsSync(configPath) ? configPath : undefined);
245
- (0, locale_1.setLocale)(config.locale || 'zh');
246
285
  // Collect files
247
286
  const files = opts.staged ? collectStagedFiles(rootDir) : collectFiles(rootDir, opts, config);
248
287
  if (files.length === 0) {
249
288
  console.log((0, locale_1.t)('index.no_files'));
250
289
  return;
251
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
+ }
319
+ // --watch mode
320
+ if (opts.watch) {
321
+ (0, watch_1.watchFiles)(files, { rootDir, format: opts.format, config });
322
+ return;
323
+ }
252
324
  // --fix mode: auto-fix before linting
253
325
  if (opts.fix) {
254
326
  for (const f of files) {
@@ -257,13 +329,39 @@ async function main() {
257
329
  console.log(`${(0, locale_1.t)('summary.tag_fail')}: ${path.relative(rootDir, f)} — ${(0, locale_1.t)('index.fixed', rule)}`);
258
330
  }
259
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
+ }
260
348
  if (!opts.staged)
261
349
  return;
262
350
  }
263
- // Filter cached files
351
+ // Filter cached files (pre-read content to avoid double I/O)
352
+ const contentCache = new Map();
264
353
  const filesToLint = opts.cache
265
- ? files.filter((f) => {
266
- const cached = (0, cache_1.isCached)(f, rootDir);
354
+ ? files.filter(f => {
355
+ let content;
356
+ try {
357
+ content = fs.readFileSync(f, 'utf-8');
358
+ }
359
+ catch {
360
+ /* ignore */
361
+ }
362
+ if (content !== undefined)
363
+ contentCache.set(f, content);
364
+ const cached = (0, cache_1.isCached)(f, rootDir, content);
267
365
  if (cached) {
268
366
  console.log(`${(0, locale_1.t)('summary.tag_warn')}: ${path.relative(rootDir, f)} — ${(0, locale_1.t)('index.cached')}`);
269
367
  }
@@ -283,27 +381,69 @@ async function main() {
283
381
  const projectIssues = (0, project_1.lintProject)(filesToLint, config, rootDir);
284
382
  issues.push(...projectIssues);
285
383
  }
286
- // Mark as cached
287
- if (opts.cache) {
384
+ // Apply rule-based fixes (after lint, when --fix is set)
385
+ if (opts.fix) {
288
386
  for (const f of filesToLint) {
289
- (0, cache_1.markCached)(f, rootDir);
387
+ const relPath = path.relative(rootDir, f);
388
+ const fileIssues = issues.filter(i => i.file === relPath);
389
+ if (fileIssues.length > 0) {
390
+ try {
391
+ const content = fs.readFileSync(f, 'utf-8');
392
+ const fixed = (0, runner_1.applyFixes)(fileIssues, content, relPath);
393
+ if (fixed !== content) {
394
+ fs.writeFileSync(f, fixed, 'utf-8');
395
+ console.log(`${(0, locale_1.t)('summary.tag_fail')}: ${relPath} — ${(0, locale_1.t)('index.fixed')}`);
396
+ }
397
+ }
398
+ catch {
399
+ /* ignore */
400
+ }
401
+ }
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
+ }
290
422
  }
291
423
  }
292
- // Phase 2: SBCL syntax validation
293
- const stubPkgPath = path.join(__dirname, '..', 'stub-packages.json');
294
- try {
295
- const sbclIssues = (0, sbcl_1.runSbclLint)(rootDir, config.sbcl, stubPkgPath);
296
- issues.push(...sbclIssues);
424
+ // Mark as cached (reuse pre-read content if available)
425
+ if (opts.cache) {
426
+ for (const f of filesToLint) {
427
+ (0, cache_1.markCached)(f, rootDir, contentCache.get(f));
428
+ }
297
429
  }
298
- catch (err) {
299
- const msg = err instanceof Error ? err.message : String(err);
300
- issues.push({
301
- file: 'sbcl',
302
- line: 1,
303
- severity: 'error',
304
- rule: 'sbcl',
305
- message: msg,
306
- });
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
+ }
307
447
  }
308
448
  // Build file content map for code line display
309
449
  const fileContents = new Map();
@@ -319,8 +459,8 @@ async function main() {
319
459
  }
320
460
  }
321
461
  // Format & output
322
- const errorCount = issues.filter((i) => i.severity === 'error').length;
323
- const warningCount = issues.filter((i) => i.severity === 'warn').length;
462
+ const errorCount = issues.filter(i => i.severity === 'error').length;
463
+ const warningCount = issues.filter(i => i.severity === 'warn').length;
324
464
  if (opts.format === 'json') {
325
465
  console.log((0, formatters_1.formatJson)(issues, errorCount, warningCount));
326
466
  }
@@ -0,0 +1,161 @@
1
+ ;; @lisp SBCL Lint — syntax validation via SBCL
2
+ ;; Usage: sbcl --script lint-sbcl.lisp <src-dir> <stub-packages.json> <walk-exclude-json> <defmacro-allow-files-json> <locale>
3
+
4
+ (require :uiop)
5
+
6
+ (defparameter *errors* 0)
7
+ (defparameter *warnings* 0)
8
+ (defparameter *checked* 0)
9
+ (defparameter *locale* "zh")
10
+
11
+ ;; ── i18n ──────────────────────────────────────────────────────────────
12
+ (defun i18n (key)
13
+ (cdr (assoc key
14
+ (if (string= *locale* "zh")
15
+ '((:not-found . "not found symbol")
16
+ (:syntax-error . "syntax error")
17
+ (:vec-syntax . "#( vector syntax")
18
+ (:char-syntax . "# backslash char literal")
19
+ (:eval-syntax . "#. read-time eval")
20
+ (:defmacro . "defmacro")
21
+ (:trailing-ws . "trailing whitespace")
22
+ (:ok . "OK")
23
+ (:fail . "FAIL")
24
+ (:header . " @lisp SBCL Lint")
25
+ (:source . " Source")
26
+ (:found . " Found ~d .lsp files")
27
+ (:scanned . " Scanned")
28
+ (:errors . " Errors")
29
+ (:warnings . " Warnings"))
30
+ '((:not-found . "not found symbol")
31
+ (:syntax-error . "syntax error")
32
+ (:vec-syntax . "#( vector syntax")
33
+ (:char-syntax . "# backslash char literal")
34
+ (:eval-syntax . "#. read-time eval")
35
+ (:defmacro . "defmacro")
36
+ (:trailing-ws . "trailing whitespace")
37
+ (:ok . "OK")
38
+ (:fail . "FAIL")
39
+ (:header . " @lisp SBCL Lint")
40
+ (:source . " Source")
41
+ (:found . " Found ~d .lsp files")
42
+ (:scanned . " Scanned")
43
+ (:errors . " Errors")
44
+ (:warnings . " Warnings"))))))
45
+
46
+ ;; ── Stub packages from external file ──────────────────────────────────
47
+ (defun load-stub-packages (file-path)
48
+ (flet ((ensure-pkg (name &optional use-list)
49
+ (unless (find-package name)
50
+ (make-package name :use (or use-list '())))))
51
+ (with-open-file (s file-path :direction :input)
52
+ (let ((packages (read s)))
53
+ (dolist (pkg packages)
54
+ (ensure-pkg (first pkg) (second pkg)))))))
55
+
56
+ ;; ── File walking ──────────────────────────────────────────────────────
57
+ (defun source-files (dir)
58
+ (sort
59
+ (remove-if-not
60
+ (lambda (p)
61
+ (let ((n (pathname-type p)))
62
+ (and n (string-equal n "lsp"))))
63
+ (uiop:directory-files dir))
64
+ 'string< :key #'namestring))
65
+
66
+ (defun walk-tree (dir exclude-dirs)
67
+ (let ((result (source-files dir)))
68
+ (dolist (sub (uiop:subdirectories dir))
69
+ (let ((name (car (last (pathname-directory sub)))))
70
+ (unless (find name exclude-dirs :test 'string=)
71
+ (setf result (append result (walk-tree sub exclude-dirs))))))
72
+ result))
73
+
74
+ ;; ── Per-file check ────────────────────────────────────────────────────
75
+ (defun check-file (path src-dir defmacro-allow-files)
76
+ (incf *checked*)
77
+ (let* ((rel (enough-namestring path src-dir))
78
+ (fullname (format nil "~a.~a" (pathname-name path) (pathname-type path))))
79
+ (format t " ~a ... " rel)
80
+ (force-output)
81
+ ;; Syntax check via READ
82
+ (handler-case
83
+ (with-open-file (s path :direction :input)
84
+ (loop for form = (read s nil :eof)
85
+ until (eq form :eof)))
86
+ (error (e)
87
+ (let ((msg (princ-to-string e)))
88
+ (cond
89
+ ((search "not found" msg)
90
+ (format t "~% [NOTE] ~a: ~a~%" rel (i18n :not-found))
91
+ (incf *warnings*))
92
+ (t
93
+ (format t "~a~% [ERROR] ~a: ~a~%" (i18n :fail) rel (i18n :syntax-error))
94
+ (incf *errors*)
95
+ (return-from check-file))))))
96
+ ;; Trailing whitespace
97
+ (with-open-file (s path :direction :input)
98
+ (loop for line = (read-line s nil nil)
99
+ for lineno from 1
100
+ while line
101
+ when (and (> (length line) 0)
102
+ (find (char line (1- (length line))) '(#\Space #\Tab)))
103
+ do (progn
104
+ (format t "~% [WARN] ~a line ~d: ~a~%" rel lineno (i18n :trailing-ws))
105
+ (incf *warnings*))))
106
+ ;; CL-ism checks: raw content scan
107
+ (with-open-file (s path :direction :input)
108
+ (let ((content (make-string (file-length s))))
109
+ (file-position s 0)
110
+ (read-sequence content s)
111
+ (when (search "#(" content)
112
+ (format t "~% [WARN] ~a: ~a~%" rel (i18n :vec-syntax))
113
+ (incf *warnings*))
114
+ (when (search "#\\" content)
115
+ (format t "~% [WARN] ~a: ~a~%" rel (i18n :char-syntax))
116
+ (incf *warnings*))
117
+ (when (search "#." content)
118
+ (format t "~% [WARN] ~a: ~a~%" rel (i18n :eval-syntax))
119
+ (incf *warnings*))
120
+ (when (and (search "defmacro" content)
121
+ (not (some (lambda (s) (search s fullname)) defmacro-allow-files)))
122
+ (format t "~% [WARN] ~a: ~a~%" rel (i18n :defmacro))
123
+ (incf *warnings*))))
124
+ (format t "~a~%" (i18n :ok))))
125
+
126
+ ;; ── Main ──────────────────────────────────────────────────────────────
127
+ (defun main ()
128
+ (let* ((args (uiop:command-line-arguments))
129
+ (src-dir (first args))
130
+ (stub-file (second args))
131
+ (walk-exclude (if (third args)
132
+ (read-from-string (third args))
133
+ '(".vscode" "test" "experiment" "tools" ".git")))
134
+ (defmacro-allow (if (fourth args)
135
+ (read-from-string (fourth args))
136
+ '("compat-cl")))
137
+ (locale (fifth args)))
138
+ (when locale (setf *locale* locale))
139
+ (load-stub-packages stub-file)
140
+
141
+ (format t "~%==================================================~%")
142
+ (format t "~a~%" (i18n :header))
143
+ (format t "~a: ~a~%" (i18n :source) (namestring (pathname src-dir)))
144
+ (format t "==================================================~%~%")
145
+ (setf *errors* 0 *warnings* 0 *checked* 0)
146
+
147
+ (let ((files (walk-tree src-dir walk-exclude)))
148
+ (format t "~a~%~%" (format nil (i18n :found) (length files)))
149
+ (dolist (f files)
150
+ (check-file f src-dir defmacro-allow)))
151
+
152
+ (format t "~%==================================================~%")
153
+ (format t "~a: ~d~%" (i18n :scanned) *checked*)
154
+ (format t "~a: ~d~%" (i18n :errors) *errors*)
155
+ (format t "~a: ~d~%" (i18n :warnings) *warnings*)
156
+ (format t "==================================================~%~%")
157
+ (if (> *errors* 0)
158
+ (uiop:quit 1)
159
+ (uiop:quit 0))))
160
+
161
+ (main)