@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/runner.js CHANGED
@@ -37,6 +37,8 @@ exports.runChecks = runChecks;
37
37
  exports.runChecksWithVisitor = runChecksWithVisitor;
38
38
  exports.lintFiles = lintFiles;
39
39
  exports.lintFilesParallel = lintFilesParallel;
40
+ exports.applyFixes = applyFixes;
41
+ exports.parseIgnoreFile = parseIgnoreFile;
40
42
  exports.fixFile = fixFile;
41
43
  const fs = __importStar(require("fs"));
42
44
  const path = __importStar(require("path"));
@@ -80,6 +82,36 @@ const misplaced_else_1 = require("./checks/misplaced-else");
80
82
  const quote_vs_function_1 = require("./checks/quote-vs-function");
81
83
  const commented_code_1 = require("./checks/commented-code");
82
84
  const double_not_1 = require("./checks/double-not");
85
+ const setq_single_arg_1 = require("./checks/setq-single-arg");
86
+ const assoc_without_cdr_1 = require("./checks/assoc-without-cdr");
87
+ const identical_branches_1 = require("./checks/identical-branches");
88
+ const while_constant_1 = require("./checks/while-constant");
89
+ const cond_duplicate_1 = require("./checks/cond-duplicate");
90
+ const error_handling_1 = require("./checks/error-handling");
91
+ const global_naming_1 = require("./checks/global-naming");
92
+ const extra_parens_1 = require("./checks/extra-parens");
93
+ const arg_count_1 = require("./checks/arg-count");
94
+ const strcat_usage_1 = require("./checks/strcat-usage");
95
+ const cond_simplify_1 = require("./checks/cond-simplify");
96
+ const quote_style_1 = require("./checks/quote-style");
97
+ const eq_usage_1 = require("./checks/eq-usage");
98
+ const lambda_syntax_1 = require("./checks/lambda-syntax");
99
+ const comment_style_1 = require("./checks/comment-style");
100
+ const empty_catch_1 = require("./checks/empty-catch");
101
+ const nth_usage_1 = require("./checks/nth-usage");
102
+ const append_single_1 = require("./checks/append-single");
103
+ const setq_multiple_1 = require("./checks/setq-multiple");
104
+ const function_order_1 = require("./checks/function-order");
105
+ const magic_number_1 = require("./checks/magic-number");
106
+ const mixed_indent_1 = require("./checks/mixed-indent");
107
+ const long_function_call_1 = require("./checks/long-function-call");
108
+ const no_return_1 = require("./checks/no-return");
109
+ const shadow_builtin_1 = require("./checks/shadow-builtin");
110
+ const dynamic_doc_1 = require("./checks/dynamic-doc");
111
+ const loop_optimization_1 = require("./checks/loop-optimization");
112
+ const type_check_1 = require("./checks/type-check");
113
+ const redundant_if_1 = require("./checks/redundant-if");
114
+ const format_indent_1 = require("./checks/format-indent");
83
115
  const config_1 = require("./config");
84
116
  const locale_1 = require("./locale");
85
117
  const disable_1 = require("./disable");
@@ -87,11 +119,47 @@ const parser_1 = require("@atlisp/parser");
87
119
  function runChecks(content, file, config) {
88
120
  const issues = [];
89
121
  const disableMap = (0, disable_1.parseDisableComments)(content);
90
- // Parse AST once for all checks that need it
91
- const ast = (0, parser_1.parseAst)(content);
122
+ // Deferred AST parsing: only parse if any AST-based checks are enabled
123
+ const astRules = new Set([
124
+ 'quit_exit',
125
+ 'open_without_close',
126
+ 'function_complexity',
127
+ 'parameter_naming',
128
+ 'unused_variable',
129
+ 'unused_parameter',
130
+ 'constant_condition',
131
+ 'redundant_progn',
132
+ 'empty_branch',
133
+ 'unused_let_binding',
134
+ 'recursive_call',
135
+ 'variable_shadow',
136
+ 'redundant_cond',
137
+ 'unused_local_fun',
138
+ 'multiple_setq',
139
+ 'redundant_quotes',
140
+ 'redundant_setq',
141
+ 'redundant_nil_else',
142
+ 'single_arg_and_or',
143
+ 'redundant_let',
144
+ 'self_compare',
145
+ 'misplaced_else',
146
+ 'quote_vs_function',
147
+ 'double_not',
148
+ 'setq_single_arg',
149
+ 'assoc_without_cdr',
150
+ 'identical_branches',
151
+ 'while_constant',
152
+ 'cond_duplicate',
153
+ ]);
154
+ const checks = config.checks;
155
+ const needsAst = Array.from(astRules).some(r => checks[r] !== 'off');
156
+ let ast;
157
+ if (needsAst) {
158
+ ast = (0, parser_1.parseAst)(content);
159
+ }
92
160
  function addIfEnabled(rule, fn) {
93
161
  const severity = config.checks[rule];
94
- if (severity === 'off')
162
+ if (!severity || severity === 'off')
95
163
  return;
96
164
  const results = fn();
97
165
  for (const r of results) {
@@ -138,6 +206,36 @@ function runChecks(content, file, config) {
138
206
  addIfEnabled('quote_vs_function', () => (0, quote_vs_function_1.checkQuoteVsFunctionAst)(ast, file));
139
207
  addIfEnabled('commented_code', () => (0, commented_code_1.checkCommentedCode)(content, file));
140
208
  addIfEnabled('double_not', () => (0, double_not_1.checkDoubleNotAst)(ast, file));
209
+ addIfEnabled('setq_single_arg', () => (0, setq_single_arg_1.checkSetqSingleArgAst)(ast, file));
210
+ addIfEnabled('assoc_without_cdr', () => (0, assoc_without_cdr_1.checkAssocWithoutCdrAst)(ast, file));
211
+ addIfEnabled('identical_branches', () => (0, identical_branches_1.checkIdenticalBranchesAst)(ast, file));
212
+ addIfEnabled('while_constant', () => (0, while_constant_1.checkWhileConstantAst)(ast, file));
213
+ addIfEnabled('cond_duplicate', () => (0, cond_duplicate_1.checkCondDuplicateAst)(ast, file));
214
+ addIfEnabled('error_handling', () => (0, error_handling_1.checkErrorHandling)(content, file));
215
+ addIfEnabled('global_naming', () => (0, global_naming_1.checkGlobalNaming)(content, file));
216
+ addIfEnabled('extra_parens', () => (0, extra_parens_1.checkExtraParens)(content, file));
217
+ addIfEnabled('arg_count', () => (0, arg_count_1.checkArgCount)(content, file));
218
+ addIfEnabled('strcat_usage', () => (0, strcat_usage_1.checkStrcatUsage)(content, file));
219
+ addIfEnabled('cond_simplify', () => (0, cond_simplify_1.checkCondSimplify)(content, file));
220
+ addIfEnabled('quote_style', () => (0, quote_style_1.checkQuoteStyle)(content, file));
221
+ addIfEnabled('eq_usage', () => (0, eq_usage_1.checkEqUsage)(content, file));
222
+ addIfEnabled('lambda_syntax', () => (0, lambda_syntax_1.checkLambdaSyntax)(content, file));
223
+ addIfEnabled('comment_style', () => (0, comment_style_1.checkCommentStyle)(content, file));
224
+ addIfEnabled('empty_catch', () => (0, empty_catch_1.checkEmptyCatch)(content, file));
225
+ addIfEnabled('nth_usage', () => (0, nth_usage_1.checkNthUsage)(content, file));
226
+ addIfEnabled('append_single', () => (0, append_single_1.checkAppendSingle)(content, file));
227
+ addIfEnabled('setq_multiple', () => (0, setq_multiple_1.checkSetqMultiple)(content, file));
228
+ addIfEnabled('function_order', () => (0, function_order_1.checkFunctionOrder)(content, file));
229
+ addIfEnabled('magic_number', () => (0, magic_number_1.checkMagicNumber)(content, file));
230
+ addIfEnabled('mixed_indent', () => (0, mixed_indent_1.checkMixedIndent)(content, file));
231
+ addIfEnabled('long_function_call', () => (0, long_function_call_1.checkLongFunctionCall)(content, file, 6));
232
+ addIfEnabled('no_return', () => (0, no_return_1.checkNoReturn)(content, file));
233
+ addIfEnabled('shadow_builtin', () => (0, shadow_builtin_1.checkShadowBuiltin)(content, file));
234
+ addIfEnabled('dynamic_doc', () => (0, dynamic_doc_1.checkDynamicDoc)(content, file));
235
+ addIfEnabled('loop_optimization', () => (0, loop_optimization_1.checkLoopOptimization)(content, file));
236
+ addIfEnabled('type_check', () => (0, type_check_1.checkTypeCheck)(content, file));
237
+ addIfEnabled('redundant_if', () => (0, redundant_if_1.checkRedundantIf)(content, file));
238
+ addIfEnabled('format_indent', () => (0, format_indent_1.checkFormatIndent)(content, file));
141
239
  return issues;
142
240
  }
143
241
  /** Single-pass visitor-based runChecks using AstVisitor */
@@ -156,7 +254,7 @@ function runChecksWithVisitor(content, file, config) {
156
254
  }
157
255
  // Register all AST-based checks as visitor handlers
158
256
  if (checks['redundant_quotes'] !== 'off') {
159
- visitor.on('quote', (node) => {
257
+ visitor.on('quote', node => {
160
258
  if (node.children &&
161
259
  node.children.length >= 2 &&
162
260
  node.children[1].type === 'list' &&
@@ -169,7 +267,7 @@ function runChecksWithVisitor(content, file, config) {
169
267
  });
170
268
  }
171
269
  if (checks['double_not'] !== 'off') {
172
- visitor.on('not', (node) => {
270
+ visitor.on('not', node => {
173
271
  if (node.children &&
174
272
  node.children.length >= 2 &&
175
273
  node.children[1].type === 'list' &&
@@ -182,7 +280,7 @@ function runChecksWithVisitor(content, file, config) {
182
280
  });
183
281
  }
184
282
  if (checks['misplaced_else'] !== 'off') {
185
- visitor.on('if', (node) => {
283
+ visitor.on('if', node => {
186
284
  if (node.children && node.children.length === 4) {
187
285
  const cond = node.children[1];
188
286
  if (cond.type === 'list' &&
@@ -209,7 +307,7 @@ function runChecksWithVisitor(content, file, config) {
209
307
  'vl-some',
210
308
  'vl-every',
211
309
  ]);
212
- visitor.on('quote', (node) => {
310
+ visitor.on('quote', node => {
213
311
  if (node.children &&
214
312
  node.children.length >= 2 &&
215
313
  node.children[1].type === 'list' &&
@@ -258,7 +356,7 @@ function lintFiles(files, config, rootDir) {
258
356
  }
259
357
  function lintFilesParallel(files, config, rootDir) {
260
358
  (0, locale_1.setLocale)(config.locale || 'zh');
261
- return new Promise((resolve) => {
359
+ return new Promise(resolve => {
262
360
  const allIssues = [];
263
361
  let completed = 0;
264
362
  const maxWorkers = Math.min(os.cpus().length, files.length);
@@ -280,7 +378,7 @@ function lintFilesParallel(files, config, rootDir) {
280
378
  if (completed >= files.length)
281
379
  resolve(allIssues);
282
380
  });
283
- worker.on('error', (err) => {
381
+ worker.on('error', err => {
284
382
  allIssues.push({
285
383
  file: relPath,
286
384
  line: 1,
@@ -294,7 +392,7 @@ function lintFilesParallel(files, config, rootDir) {
294
392
  if (completed >= files.length)
295
393
  resolve(allIssues);
296
394
  });
297
- worker.on('exit', (code) => {
395
+ worker.on('exit', code => {
298
396
  if (code !== 0) {
299
397
  allIssues.push({
300
398
  file: relPath,
@@ -317,6 +415,225 @@ function lintFilesParallel(files, config, rootDir) {
317
415
  resolve([]);
318
416
  });
319
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
+ }
459
+ function applyFixes(issues, content, filepath) {
460
+ const lines = content.split('\n');
461
+ const fixesByRule = new Map();
462
+ for (const iss of issues) {
463
+ if (iss.file !== filepath)
464
+ continue;
465
+ const set = fixesByRule.get(iss.rule) || new Set();
466
+ set.add(iss.line);
467
+ fixesByRule.set(iss.rule, set);
468
+ }
469
+ // Apply fixes from higher line numbers first to preserve offsets
470
+ const fixableRules = new Set([
471
+ 'redundant_quotes',
472
+ 'double_not',
473
+ 'redundant_nil_else',
474
+ 'single_arg_and_or',
475
+ 'redundant_setq',
476
+ 'redundant_progn',
477
+ 'redundant_let',
478
+ 'quote_style',
479
+ 'redundant_if',
480
+ 'misplaced_else',
481
+ ]);
482
+ let result = content;
483
+ for (const [rule, lineSet] of fixesByRule) {
484
+ if (!fixableRules.has(rule))
485
+ continue;
486
+ const sortedLines = Array.from(lineSet).sort((a, b) => b - a);
487
+ for (const line of sortedLines) {
488
+ const idx = line - 1;
489
+ if (idx < 0 || idx >= lines.length)
490
+ continue;
491
+ const lineContent = result.split('\n')[idx];
492
+ switch (rule) {
493
+ case 'redundant_quotes': {
494
+ // Replace ''x with 'x on this line
495
+ const fixed = lineContent.replace(/''(\S)/g, "'$1");
496
+ if (fixed !== lineContent) {
497
+ result = replaceLine(result, idx, fixed);
498
+ }
499
+ break;
500
+ }
501
+ case 'double_not': {
502
+ // Replace (not (not ...)) with (not ...)
503
+ const fixed = lineContent.replace(/\(not\s+\(not\s+/g, '(not ');
504
+ if (fixed !== lineContent) {
505
+ result = replaceLine(result, idx, fixed);
506
+ }
507
+ break;
508
+ }
509
+ case 'redundant_nil_else': {
510
+ // Replace (if ... nil) with (if ...)
511
+ const fixed = lineContent.replace(/\s+nil\s*\)$/, ')');
512
+ if (fixed !== lineContent) {
513
+ result = replaceLine(result, idx, fixed);
514
+ }
515
+ break;
516
+ }
517
+ case 'single_arg_and_or': {
518
+ // Replace (and x) with x, (or x) with x
519
+ const m = lineContent.match(/\((and|or)\s+([^)\s]+)\s*\)/);
520
+ if (m) {
521
+ result = replaceLine(result, idx, m[2]);
522
+ }
523
+ break;
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
+ }
607
+ case 'redundant_setq': {
608
+ // Remove (setq x x) lines
609
+ const allLines = result.split('\n');
610
+ const trimmed = allLines[idx].trim();
611
+ if (/^\(setq\s+(\S+)\s+\1\s*\)$/.test(trimmed)) {
612
+ allLines.splice(idx, 1);
613
+ result = allLines.join('\n');
614
+ }
615
+ break;
616
+ }
617
+ }
618
+ }
619
+ }
620
+ return result;
621
+ }
622
+ function replaceLine(content, lineIdx, newLine) {
623
+ const lines = content.split('\n');
624
+ lines[lineIdx] = newLine;
625
+ return lines.join('\n');
626
+ }
627
+ function parseIgnoreFile(rootDir) {
628
+ const ignoreFile = path.join(rootDir, '.atlisp-lint-ignore');
629
+ if (!fs.existsSync(ignoreFile))
630
+ return [];
631
+ const content = fs.readFileSync(ignoreFile, 'utf-8');
632
+ return content
633
+ .split('\n')
634
+ .map(l => l.trim())
635
+ .filter(l => l.length > 0 && !l.startsWith('#'));
636
+ }
320
637
  const UTF8_BOM = 0xfeff;
321
638
  function fixFile(filepath) {
322
639
  const fixes = [];
@@ -324,7 +641,7 @@ function fixFile(filepath) {
324
641
  // Fix trailing whitespace
325
642
  const wsFixed = content
326
643
  .split('\n')
327
- .map((l) => l.replace(/[ \t]+$/, ''))
644
+ .map(l => l.replace(/[ \t]+$/, ''))
328
645
  .join('\n');
329
646
  if (wsFixed !== content) {
330
647
  content = wsFixed;
@@ -362,7 +679,7 @@ function fixFile(filepath) {
362
679
  // Fix empty comments: remove lines that are only ';' with optional whitespace
363
680
  const lines = content.split('\n');
364
681
  let changed = false;
365
- const fixedLines = lines.map((l) => {
682
+ const fixedLines = lines.map(l => {
366
683
  if (/^\s*;\s*$/.test(l)) {
367
684
  changed = true;
368
685
  return '';
package/dist/sbcl.js CHANGED
@@ -72,7 +72,7 @@ function runSbclLint(srcDir, sbclConfig, stubPackagesPath) {
72
72
  }
73
73
  try {
74
74
  // Convert arrays to Lisp-readable format: ("a" "b")
75
- const toLispList = (arr) => '(' + arr.map((s) => `"${s}"`).join(' ') + ')';
75
+ const toLispList = (arr) => '(' + arr.map(s => `"${s}"`).join(' ') + ')';
76
76
  const result = (0, child_process_1.execFileSync)(sbcl, [
77
77
  '--script',
78
78
  scriptPath,
@@ -0,0 +1,41 @@
1
+ ((AUTOLISP ())
2
+ (@ (COMMON-LISP AUTOLISP))
3
+ (C ())
4
+ (JSON ())
5
+ (SIDEBAR ())
6
+ (FUN ())
7
+ (DCL ())
8
+ (BASE ())
9
+ (STRING ())
10
+ (BLOCK ())
11
+ (ENTITY ())
12
+ (LAYER ())
13
+ (LAYOUT ())
14
+ (CURVE ())
15
+ (LIST ())
16
+ (M ())
17
+ (P ())
18
+ (UI ())
19
+ (SYS ())
20
+ (EXCEL ())
21
+ (WORD ())
22
+ (RE ())
23
+ (VECTRA ())
24
+ (DOS ())
25
+ (AT-PM ())
26
+ (QRENCODE ())
27
+ (NETWORK ())
28
+ (PKGMAN ())
29
+ (DATETIME ())
30
+ (DICT ())
31
+ (@NLP ())
32
+ (AT-SIDEBAR ())
33
+ (S ())
34
+ (VL ())
35
+ (VLR ())
36
+ (VLAX ())
37
+ (THEME ())
38
+ (AJAX ())
39
+ (ATLISP ())
40
+ (LOG ())
41
+ (ATLISP-CORE ()))
package/dist/types.d.ts CHANGED
@@ -1,10 +1,15 @@
1
1
  export type Severity = 'off' | 'warn' | 'error';
2
+ export interface Fix {
3
+ range: [number, number];
4
+ replacement: string;
5
+ }
2
6
  export interface Issue {
3
7
  file: string;
4
8
  line: number;
5
9
  severity: Severity;
6
10
  rule: string;
7
11
  message: string;
12
+ fix?: Fix;
8
13
  }
9
14
  export interface CheckConfig {
10
15
  severity: Severity;
@@ -61,6 +66,7 @@ export interface LintConfig {
61
66
  namespace_header: NamespaceHeaderConfig;
62
67
  bare_function_names: BareFunctionNamesConfig;
63
68
  sbcl: SbclConfig;
69
+ preset?: string;
64
70
  }
65
71
  export interface FormattedResult {
66
72
  issues: Issue[];
@@ -0,0 +1,8 @@
1
+ import { LintConfig } from './types';
2
+ interface ValidationError {
3
+ path: string;
4
+ message: string;
5
+ }
6
+ export declare function validateConfig(config: LintConfig): ValidationError[];
7
+ export {};
8
+ //# sourceMappingURL=validate.d.ts.map
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateConfig = validateConfig;
4
+ const VALID_RULES = [
5
+ 'append_single',
6
+ 'arg_count',
7
+ 'assoc_without_cdr',
8
+ 'bare_function_names',
9
+ 'cl_syntax',
10
+ 'command_shell',
11
+ 'comment_style',
12
+ 'commented_code',
13
+ 'cond_simplify',
14
+ 'cond_duplicate',
15
+ 'constant_condition',
16
+ 'dangling_defun',
17
+ 'double_not',
18
+ 'duplicate_defun',
19
+ 'dynamic_doc',
20
+ 'empty_branch',
21
+ 'empty_catch',
22
+ 'empty_comment',
23
+ 'encoding',
24
+ 'eq_usage',
25
+ 'error_handling',
26
+ 'extra_parens',
27
+ 'format_indent',
28
+ 'function_complexity',
29
+ 'function_order',
30
+ 'global_naming',
31
+ 'identical_branches',
32
+ 'lambda_syntax',
33
+ 'line_length',
34
+ 'long_function_call',
35
+ 'loop_optimization',
36
+ 'magic_number',
37
+ 'misplaced_else',
38
+ 'missing_doc',
39
+ 'missing_export',
40
+ 'mixed_indent',
41
+ 'module_registration',
42
+ 'multiple_setq',
43
+ 'namespace_header',
44
+ 'no_return',
45
+ 'nth_usage',
46
+ 'open_without_close',
47
+ 'parameter_naming',
48
+ 'parens',
49
+ 'quit_exit',
50
+ 'quote_style',
51
+ 'quote_vs_function',
52
+ 'recursive_call',
53
+ 'redundant_cond',
54
+ 'redundant_if',
55
+ 'redundant_let',
56
+ 'redundant_nil_else',
57
+ 'redundant_progn',
58
+ 'redundant_quotes',
59
+ 'redundant_setq',
60
+ 'self_compare',
61
+ 'setq_multiple',
62
+ 'setq_single_arg',
63
+ 'shadow_builtin',
64
+ 'single_arg_and_or',
65
+ 'startapp',
66
+ 'strcat_usage',
67
+ 'token_in_url',
68
+ 'trailing_paren',
69
+ 'trailing_whitespace',
70
+ 'type_check',
71
+ 'unused_let_binding',
72
+ 'unused_local_fun',
73
+ 'unused_package_dep',
74
+ 'unused_parameter',
75
+ 'unused_variable',
76
+ 'variable_shadow',
77
+ 'while_constant',
78
+ 'vl_registry_write',
79
+ 'vlax_without_loading',
80
+ ];
81
+ const VALID_SEVERITIES = ['off', 'warn', 'error'];
82
+ function validateConfig(config) {
83
+ const errors = [];
84
+ if (config.locale && config.locale !== 'zh' && config.locale !== 'en') {
85
+ errors.push({ path: 'locale', message: `Invalid locale '${config.locale}', expected 'zh' or 'en'` });
86
+ }
87
+ if (config.checks) {
88
+ for (const [rule, sev] of Object.entries(config.checks)) {
89
+ if (!VALID_RULES.includes(rule)) {
90
+ errors.push({ path: `checks.${rule}`, message: `Unknown rule '${rule}'` });
91
+ }
92
+ if (!VALID_SEVERITIES.includes(sev)) {
93
+ errors.push({
94
+ path: `checks.${rule}`,
95
+ message: `Invalid severity '${sev}', expected 'off', 'warn', or 'error'`,
96
+ });
97
+ }
98
+ }
99
+ }
100
+ if (config.source) {
101
+ if (!Array.isArray(config.source.globs)) {
102
+ errors.push({ path: 'source.globs', message: 'Must be an array of glob patterns' });
103
+ }
104
+ if (!Array.isArray(config.source.exclude)) {
105
+ errors.push({ path: 'source.exclude', message: 'Must be an array of glob patterns' });
106
+ }
107
+ }
108
+ if (config.line_length) {
109
+ if (typeof config.line_length.max !== 'number' || config.line_length.max < 1) {
110
+ errors.push({ path: 'line_length.max', message: 'Must be a positive number' });
111
+ }
112
+ if (typeof config.line_length.tab_width !== 'number' || config.line_length.tab_width < 1) {
113
+ errors.push({ path: 'line_length.tab_width', message: 'Must be a positive number' });
114
+ }
115
+ }
116
+ if (config.function_complexity) {
117
+ if (typeof config.function_complexity.max_lines !== 'number' || config.function_complexity.max_lines < 1) {
118
+ errors.push({ path: 'function_complexity.max_lines', message: 'Must be a positive number' });
119
+ }
120
+ if (typeof config.function_complexity.max_nesting !== 'number' || config.function_complexity.max_nesting < 1) {
121
+ errors.push({ path: 'function_complexity.max_nesting', message: 'Must be a positive number' });
122
+ }
123
+ }
124
+ return errors;
125
+ }
126
+ //# sourceMappingURL=validate.js.map
@@ -0,0 +1,9 @@
1
+ import { LintConfig } from './types';
2
+ interface WatchOptions {
3
+ rootDir: string;
4
+ format: 'default' | 'json';
5
+ config: LintConfig;
6
+ }
7
+ export declare function watchFiles(files: string[], options: WatchOptions): void;
8
+ export {};
9
+ //# sourceMappingURL=watch.d.ts.map