@atlisp/lint 0.2.16 → 0.2.18

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 (80) hide show
  1. package/README.md +1 -1
  2. package/atlisp-lint.default.json +1 -1
  3. package/dist/checks/arg-count.d.ts +2 -1
  4. package/dist/checks/arg-count.js +37 -19
  5. package/dist/checks/builtin-arg-count.d.ts +2 -1
  6. package/dist/checks/builtin-arg-count.js +3 -2
  7. package/dist/checks/cond-duplicate.js +5 -6
  8. package/dist/checks/constant-condition.js +9 -9
  9. package/dist/checks/dangerous-calls.js +1 -1
  10. package/dist/checks/dangling-defun.d.ts +0 -7
  11. package/dist/checks/dangling-defun.js +0 -63
  12. package/dist/checks/double-not.js +1 -1
  13. package/dist/checks/empty-branch.js +1 -1
  14. package/dist/checks/empty-catch.d.ts +2 -1
  15. package/dist/checks/empty-catch.js +8 -7
  16. package/dist/checks/function-complexity.js +5 -18
  17. package/dist/checks/global-naming.js +1 -1
  18. package/dist/checks/identical-branches.js +3 -3
  19. package/dist/checks/if-without-else.js +1 -1
  20. package/dist/checks/misplaced-else.js +2 -2
  21. package/dist/checks/missing-export.js +3 -3
  22. package/dist/checks/missing-princ.js +1 -1
  23. package/dist/checks/multiple-setq.js +1 -1
  24. package/dist/checks/no-return.d.ts +2 -1
  25. package/dist/checks/no-return.js +4 -3
  26. package/dist/checks/nth-usage.js +1 -1
  27. package/dist/checks/parameter-naming.js +2 -2
  28. package/dist/checks/quote-vs-function.js +1 -1
  29. package/dist/checks/recursive-call.js +1 -1
  30. package/dist/checks/redundant-let.js +1 -1
  31. package/dist/checks/redundant-nil-else.js +1 -1
  32. package/dist/checks/redundant-progn.js +1 -1
  33. package/dist/checks/redundant-setq.js +1 -1
  34. package/dist/checks/self-compare.js +1 -1
  35. package/dist/checks/setq-single-arg.js +3 -2
  36. package/dist/checks/single-arg-and-or.js +1 -1
  37. package/dist/checks/undeclared-setq.d.ts +2 -1
  38. package/dist/checks/undeclared-setq.js +6 -5
  39. package/dist/checks/unused-let.js +3 -3
  40. package/dist/checks/unused-local-fun.js +2 -2
  41. package/dist/checks/unused-package-dep.d.ts +0 -1
  42. package/dist/checks/unused-package-dep.js +6 -93
  43. package/dist/checks/unused-param.js +2 -2
  44. package/dist/checks/unused-variable.js +2 -2
  45. package/dist/checks/variable-shadow.js +3 -3
  46. package/dist/checks/while-constant.js +1 -1
  47. package/dist/cli/collector.js +26 -19
  48. package/dist/config.d.ts +1 -1
  49. package/dist/config.js +18 -9
  50. package/dist/fixes/index.d.ts +4 -0
  51. package/dist/fixes/index.js +843 -140
  52. package/dist/formatter.d.ts +0 -1
  53. package/dist/formatter.js +141 -31
  54. package/dist/index.js +59 -51
  55. package/dist/project.d.ts +1 -1
  56. package/dist/project.js +98 -112
  57. package/dist/runner.d.ts +1 -1
  58. package/dist/runner.js +17 -8
  59. package/dist/utils.js +21 -30
  60. package/dist/validate.js +3 -3
  61. package/dist/visitor-runner.d.ts +0 -28
  62. package/dist/visitor-runner.js +3 -809
  63. package/dist/visitors/equal-nil.d.ts +2 -2
  64. package/dist/visitors/equal-nil.js +5 -4
  65. package/dist/visitors/index.d.ts +1 -1
  66. package/dist/visitors/progn-in-cond.d.ts +2 -2
  67. package/dist/visitors/progn-in-cond.js +5 -4
  68. package/dist/visitors/redundant-and-or.d.ts +2 -2
  69. package/dist/visitors/redundant-and-or.js +3 -3
  70. package/dist/visitors/redundant-car-cdr.d.ts +2 -2
  71. package/dist/visitors/redundant-car-cdr.js +3 -3
  72. package/dist/visitors/register.d.ts +6 -0
  73. package/dist/visitors/register.js +625 -0
  74. package/dist/visitors/shared.d.ts +40 -0
  75. package/dist/visitors/shared.js +129 -0
  76. package/dist/visitors/types.d.ts +2 -7
  77. package/dist/visitors/types.js +0 -69
  78. package/dist/watch.js +33 -12
  79. package/dist/worker.js +1 -1
  80. package/package.json +2 -2
@@ -1,3 +1,2 @@
1
1
  export declare function formatCode(content: string, indentSize?: number): string;
2
- export declare function formatCodeStable(content: string, indentSize?: number): string;
3
2
  //# sourceMappingURL=formatter.d.ts.map
package/dist/formatter.js CHANGED
@@ -1,15 +1,109 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.formatCode = formatCode;
4
- exports.formatCodeStable = formatCodeStable;
5
4
  const utils_1 = require("./utils");
5
+ const parser_1 = require("@atlisp/parser");
6
6
  function isCommentLine(line) {
7
7
  const trimmed = line.trim();
8
8
  return trimmed.startsWith(';') || trimmed.startsWith(';|');
9
9
  }
10
+ function findMultiPairSetq(content) {
11
+ try {
12
+ const ast = (0, parser_1.parseAst)(content, { errorRecovery: true });
13
+ if (!ast)
14
+ return [];
15
+ const ranges = [];
16
+ for (const node of (0, parser_1.astWalk)(ast)) {
17
+ if (!(0, parser_1.astIsList)(node))
18
+ continue;
19
+ const children = node.children;
20
+ if (children.length < 6)
21
+ continue;
22
+ const first = children[0];
23
+ if (!(0, parser_1.astIsSymbol)(first) || first.name.toLowerCase() !== 'setq')
24
+ continue;
25
+ const start = node.pos.offset;
26
+ const end = node.end?.offset ?? start;
27
+ const startLine = content.substring(0, start).split('\n').length - 1;
28
+ const endLine = content.substring(0, end).split('\n').length - 1;
29
+ ranges.push({ startLine, endLine, startOffset: start, endOffset: end });
30
+ }
31
+ return ranges;
32
+ }
33
+ catch {
34
+ return [];
35
+ }
36
+ }
37
+ function formatSetqFromNode(node, depth, indentSize, content) {
38
+ const children = node.children;
39
+ const pairs = [];
40
+ for (let i = 1; i + 1 < children.length; i += 2) {
41
+ const varNode = children[i];
42
+ const valNode = children[i + 1];
43
+ let varText;
44
+ if ((0, parser_1.astIsSymbol)(varNode)) {
45
+ varText = varNode.name;
46
+ }
47
+ else {
48
+ const vs = varNode.pos.offset;
49
+ const ve = varNode.end?.offset ?? vs;
50
+ varText = content.slice(vs, ve);
51
+ }
52
+ const vs = valNode.pos.offset;
53
+ const ve = valNode.end?.offset ?? vs;
54
+ const valText = content.slice(vs, ve);
55
+ pairs.push({ varText, valText });
56
+ }
57
+ if (pairs.length < 2) {
58
+ const indent = ' '.repeat(depth * indentSize);
59
+ return [indent + '(setq ' + pairs.map(p => p.varText + ' ' + p.valText).join(' ') + ')'];
60
+ }
61
+ const maxVarLen = Math.max(...pairs.map(p => p.varText.length));
62
+ const baseIndent = ' '.repeat(depth * indentSize);
63
+ const setqPrefix = '(setq ';
64
+ const varColumn = depth * indentSize + setqPrefix.length;
65
+ const lines = [];
66
+ lines.push(baseIndent + setqPrefix + pairs[0].varText + ' ' + pairs[0].valText);
67
+ const continuationIndent = ' '.repeat(varColumn);
68
+ for (let i = 1; i < pairs.length; i++) {
69
+ const padding = maxVarLen - pairs[i].varText.length;
70
+ const isLast = i === pairs.length - 1;
71
+ lines.push(continuationIndent + pairs[i].varText + ' '.repeat(padding + 1) + pairs[i].valText + (isLast ? ')' : ''));
72
+ }
73
+ return lines;
74
+ }
75
+ function runStandardFormat(lines, lineIdx, depth, _indentSize, rawLine, trimmed) {
76
+ const isComment = isCommentLine(trimmed);
77
+ const stripped = (0, utils_1.stripLine)(trimmed);
78
+ const trimmedStripped = stripped.trim();
79
+ if (trimmedStripped === '') {
80
+ return {
81
+ formattedLines: [' '.repeat(depth * _indentSize) + trimmed.trimStart()],
82
+ newDepth: depth
83
+ };
84
+ }
85
+ const opens = (trimmedStripped.match(/\(/g) || []).length;
86
+ const closes = (trimmedStripped.match(/\)/g) || []).length;
87
+ let leadingCloseCount = 0;
88
+ for (const ch of trimmedStripped) {
89
+ if (ch === ')')
90
+ leadingCloseCount++;
91
+ else
92
+ break;
93
+ }
94
+ let effectiveDepth = depth;
95
+ if (leadingCloseCount > 0) {
96
+ effectiveDepth = Math.max(0, depth - leadingCloseCount);
97
+ }
98
+ return {
99
+ formattedLines: [' '.repeat(effectiveDepth * _indentSize) + trimmed.trimStart()],
100
+ newDepth: depth + opens - closes
101
+ };
102
+ }
10
103
  function formatCode(content, indentSize = 2) {
11
104
  if (content === '' || content === '\n')
12
105
  return content;
106
+ const setqRanges = findMultiPairSetq(content);
13
107
  const lines = content.split('\n');
14
108
  const result = [];
15
109
  let depth = 0;
@@ -20,33 +114,54 @@ function formatCode(content, indentSize = 2) {
20
114
  result.push('');
21
115
  continue;
22
116
  }
23
- const isComment = isCommentLine(trimmed);
24
- const stripped = (0, utils_1.stripLine)(trimmed);
25
- const trimmedStripped = stripped.trim();
26
- if (trimmedStripped === '' && isComment) {
27
- result.push(' '.repeat(depth * indentSize) + trimmed.trimStart());
117
+ const sqRange = setqRanges.find(sq => sq.startLine === lineIdx);
118
+ if (sqRange) {
119
+ const setqText = content.slice(sqRange.startOffset, sqRange.endOffset);
120
+ const ast = (0, parser_1.parseAst)(setqText, { errorRecovery: true });
121
+ let sqNode;
122
+ if (ast) {
123
+ for (const n of (0, parser_1.astWalk)(ast)) {
124
+ if ((0, parser_1.astIsList)(n)) {
125
+ const f = n.children[0];
126
+ if ((0, parser_1.astIsSymbol)(f) && f.name.toLowerCase() === 'setq') {
127
+ sqNode = n;
128
+ break;
129
+ }
130
+ }
131
+ }
132
+ }
133
+ if (sqNode && (0, parser_1.astIsList)(sqNode)) {
134
+ const formattedLines = formatSetqFromNode(sqNode, depth, indentSize, setqText);
135
+ result.push(...formattedLines);
136
+ }
137
+ else {
138
+ const indent = ' '.repeat(depth * indentSize);
139
+ result.push(indent + trimmed.trimStart());
140
+ }
141
+ // Emit any trailing content after setq's end on the same line (e.g., extra ))
142
+ const afterSetqText = content.slice(sqRange.endOffset);
143
+ const afterSetqLine = afterSetqText.split('\n')[0];
144
+ if (afterSetqLine.trim()) {
145
+ const sAfter = (0, utils_1.stripLine)(afterSetqLine);
146
+ const tAfter = sAfter.trim();
147
+ if (tAfter) {
148
+ const opensAfter = (tAfter.match(/\(/g) || []).length;
149
+ const closesAfter = (tAfter.match(/\)/g) || []).length;
150
+ depth += opensAfter - closesAfter;
151
+ if (depth < 0)
152
+ depth = 0;
153
+ const leadingCloseCount = (tAfter.match(/^\)+/g) || [''])[0].length;
154
+ const outdent = depth - leadingCloseCount + (opensAfter - closesAfter);
155
+ const effectiveDepth = Math.max(0, depth - leadingCloseCount + (opensAfter - closesAfter));
156
+ result.push(' '.repeat(effectiveDepth * indentSize) + afterSetqLine.trim());
157
+ }
158
+ }
159
+ lineIdx = sqRange.endLine;
28
160
  continue;
29
161
  }
30
- if (trimmedStripped === '') {
31
- result.push(' '.repeat(depth * indentSize) + trimmed.trimStart());
32
- continue;
33
- }
34
- const opens = (trimmedStripped.match(/\(/g) || []).length;
35
- const closes = (trimmedStripped.match(/\)/g) || []).length;
36
- let leadingCloseCount = 0;
37
- for (const ch of trimmedStripped) {
38
- if (ch === ')')
39
- leadingCloseCount++;
40
- else
41
- break;
42
- }
43
- let effectiveDepth = depth;
44
- if (leadingCloseCount > 0) {
45
- effectiveDepth = Math.max(0, depth - leadingCloseCount);
46
- }
47
- const indent = ' '.repeat(effectiveDepth * indentSize);
48
- result.push(indent + trimmed.trimStart());
49
- depth += opens - closes;
162
+ const { formattedLines, newDepth } = runStandardFormat(lines, lineIdx, depth, indentSize, rawLine, trimmed);
163
+ result.push(...formattedLines);
164
+ depth = newDepth;
50
165
  if (depth < 0)
51
166
  depth = 0;
52
167
  }
@@ -56,9 +171,4 @@ function formatCode(content, indentSize = 2) {
56
171
  }
57
172
  return output;
58
173
  }
59
- function formatCodeStable(content, indentSize = 2) {
60
- const first = formatCode(content, indentSize);
61
- const second = formatCode(first, indentSize);
62
- return second;
63
- }
64
174
  //# sourceMappingURL=formatter.js.map
package/dist/index.js CHANGED
@@ -215,7 +215,7 @@ async function main() {
215
215
  if (!opts.staged)
216
216
  return;
217
217
  }
218
- // Filter cached files (pre-read content to avoid double I/O)
218
+ // Filter cached files (pre-read content to avoid double I/O for linting and display)
219
219
  const contentCache = new Map();
220
220
  const filesToLint = opts.cache
221
221
  ? files.filter(f => {
@@ -234,70 +234,67 @@ async function main() {
234
234
  }
235
235
  return !cached;
236
236
  })
237
- : files;
237
+ : files.filter(f => {
238
+ try {
239
+ const content = fs.readFileSync(f, 'utf-8');
240
+ contentCache.set(f, content);
241
+ }
242
+ catch {
243
+ /* ignore */
244
+ }
245
+ return true;
246
+ });
238
247
  if (filesToLint.length === 0) {
239
248
  console.log((0, locale_1.t)('index.all_cached'));
240
249
  return;
241
250
  }
242
251
  // Phase 1: Static checks (parallel or sequential)
243
- const contentArg = opts.cache && contentCache.size > 0 ? contentCache : undefined;
252
+ const contentArg = contentCache.size > 0 ? contentCache : undefined;
244
253
  const issues = opts.parallel
245
- ? await (0, runner_1.lintFilesParallel)(filesToLint, config, rootDir)
254
+ ? await (0, runner_1.lintFilesParallel)(filesToLint, config, rootDir, contentArg)
246
255
  : (0, runner_1.lintFiles)(filesToLint, config, rootDir, contentArg);
247
256
  // Phase 1b: Project-level cross-file checks
248
257
  if (opts.project) {
249
- const projectIssues = (0, project_1.lintProject)(filesToLint, config, rootDir);
258
+ const projectIssues = await (0, project_1.lintProject)(filesToLint, config, rootDir);
250
259
  issues.push(...projectIssues);
251
260
  }
252
- // Apply rule-based fixes (after lint, when --fix is set)
261
+ // Apply rule-based fixes + format_indent (merged single pass, when --fix is set)
253
262
  if (opts.fix) {
263
+ const indentSize = config.line_length.tab_width;
264
+ const formatEnabled = config.checks['format_indent'] !== 'off';
254
265
  for (const f of filesToLint) {
255
266
  const relPath = path.relative(rootDir, f);
256
267
  const fileIssues = issues.filter(i => i.file === relPath);
257
- if (fileIssues.length > 0) {
258
- try {
259
- const content = fs.readFileSync(f, 'utf-8');
260
- const fixed = (0, runner_1.applyFixes)(fileIssues, content, relPath);
261
- if (fixed !== content) {
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
- }
269
- }
270
- }
271
- catch (e) {
272
- console.warn(`[lint] applyFixes failed for ${relPath}: ${e instanceof Error ? e.message : String(e)}`);
268
+ const hasFormatIssue = formatEnabled && issues.some(i => i.file === relPath && i.rule === 'format_indent');
269
+ if (fileIssues.length === 0 && !hasFormatIssue)
270
+ continue;
271
+ try {
272
+ const content = fs.readFileSync(f, 'utf-8');
273
+ let changed = content;
274
+ if (fileIssues.length > 0) {
275
+ changed = (0, runner_1.applyFixes)(fileIssues, changed, relPath);
273
276
  }
274
- }
275
- }
276
- // format_indent fix: apply formatCode to files with format_indent issues
277
- if (config.checks['format_indent'] !== 'off') {
278
- const indentSize = config.line_length.tab_width;
279
- for (const f of filesToLint) {
280
- const relPath = path.relative(rootDir, f);
281
- const hasFormatIssue = issues.some(i => i.file === relPath && i.rule === 'format_indent');
282
277
  if (hasFormatIssue) {
283
- try {
284
- const content = fs.readFileSync(f, 'utf-8');
285
- const formatted = (0, formatter_1.formatCode)(content, indentSize);
286
- if (formatted !== content) {
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
- }
294
- }
278
+ changed = (0, formatter_1.formatCode)(changed, indentSize);
279
+ }
280
+ if (changed !== content) {
281
+ if (opts.dryRun) {
282
+ const parts = [];
283
+ if (fileIssues.length > 0)
284
+ parts.push(`would apply ${fileIssues.length} fix(es)`);
285
+ if (hasFormatIssue)
286
+ parts.push('would fix format_indent');
287
+ console.log(`${(0, locale_1.t)('summary.tag_info')}: ${relPath} — ${parts.join('; ')}`);
295
288
  }
296
- catch (e) {
297
- console.warn(`[lint] format_indent fix failed for ${relPath}: ${e instanceof Error ? e.message : String(e)}`);
289
+ else {
290
+ fs.writeFileSync(f, changed, 'utf-8');
291
+ console.log(`${(0, locale_1.t)('summary.tag_fail')}: ${relPath} — ${(0, locale_1.t)('index.fixed')}`);
298
292
  }
299
293
  }
300
294
  }
295
+ catch (e) {
296
+ console.warn(`[lint] fix failed for ${relPath}: ${e instanceof Error ? e.message : String(e)}`);
297
+ }
301
298
  }
302
299
  }
303
300
  // Mark as cached
@@ -324,22 +321,33 @@ async function main() {
324
321
  });
325
322
  }
326
323
  }
327
- // Build file content map for code line display
324
+ // Build file content map for code line display (reuse pre-read content)
328
325
  const fileContents = new Map();
329
326
  for (const iss of issues) {
330
327
  if (!fileContents.has(iss.file)) {
331
328
  const fp = path.resolve(rootDir, iss.file);
332
- try {
333
- fileContents.set(iss.file, fs.readFileSync(fp, 'utf-8'));
329
+ const cached = contentCache.get(fp);
330
+ if (cached !== undefined) {
331
+ fileContents.set(iss.file, cached);
334
332
  }
335
- catch (e) {
336
- console.warn(`[lint] failed to read ${fp} for display: ${e instanceof Error ? e.message : String(e)}`);
333
+ else {
334
+ try {
335
+ fileContents.set(iss.file, fs.readFileSync(fp, 'utf-8'));
336
+ }
337
+ catch (e) {
338
+ console.warn(`[lint] failed to read ${fp} for display: ${e instanceof Error ? e.message : String(e)}`);
339
+ }
337
340
  }
338
341
  }
339
342
  }
340
343
  // Format & output
341
- const errorCount = issues.filter(i => i.severity === 'error').length;
342
- const warningCount = issues.filter(i => i.severity === 'warn').length;
344
+ let errorCount = 0, warningCount = 0;
345
+ for (const i of issues) {
346
+ if (i.severity === 'error')
347
+ errorCount++;
348
+ else if (i.severity === 'warn')
349
+ warningCount++;
350
+ }
343
351
  if (opts.format === 'json') {
344
352
  console.log((0, formatters_1.formatJson)(issues, errorCount, warningCount));
345
353
  }
package/dist/project.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  import { Issue, LintConfig } from './types';
2
- export declare function lintProject(files: string[], config: LintConfig, rootDir: string): Issue[];
2
+ export declare function lintProject(files: string[], config: LintConfig, rootDir: string): Promise<Issue[]>;
3
3
  //# sourceMappingURL=project.d.ts.map