@atlisp/lint 0.2.16 → 0.2.17

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 +0 -6
  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,7 +1,6 @@
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");
6
5
  function isCommentLine(line) {
7
6
  const trimmed = line.trim();
@@ -56,9 +55,4 @@ function formatCode(content, indentSize = 2) {
56
55
  }
57
56
  return output;
58
57
  }
59
- function formatCodeStable(content, indentSize = 2) {
60
- const first = formatCode(content, indentSize);
61
- const second = formatCode(first, indentSize);
62
- return second;
63
- }
64
58
  //# 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
package/dist/project.js CHANGED
@@ -37,6 +37,7 @@ exports.lintProject = lintProject;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const parser_1 = require("@atlisp/parser");
40
+ const fsp = __importStar(require("fs/promises"));
40
41
  const dangling_defun_1 = require("./checks/dangling-defun");
41
42
  const missing_export_1 = require("./checks/missing-export");
42
43
  const unused_package_dep_1 = require("./checks/unused-package-dep");
@@ -45,72 +46,52 @@ const builtin_arg_count_1 = require("./checks/builtin-arg-count");
45
46
  const config_1 = require("./config");
46
47
  const locale_1 = require("./locale");
47
48
  const locale_2 = require("./locale");
48
- function collectDefunsFromAst(ast) {
49
- const results = [];
50
- const defunNodes = (0, parser_1.astFindAll)(ast, n => (0, parser_1.astIsList)(n, 'defun') || (0, parser_1.astIsList)(n, 'defun-q'));
51
- for (const node of defunNodes) {
52
- if (node.children && node.children.length >= 2 && (0, parser_1.astIsSymbol)(node.children[1])) {
53
- results.push({ name: node.children[1].name, line: node.pos.line });
49
+ const shared_1 = require("./visitors/shared");
50
+ function collectProjectMetadata(ast) {
51
+ const defuns = [];
52
+ const refs = [];
53
+ const allLists = (0, parser_1.astFindAll)(ast, n => (0, parser_1.astIsList)(n));
54
+ for (const node of allLists) {
55
+ if (!(0, parser_1.astIsList)(node) || node.children.length === 0)
56
+ continue;
57
+ const car = node.children[0];
58
+ if (!(0, parser_1.astIsSymbol)(car))
59
+ continue;
60
+ const name = car.name;
61
+ if ((name === 'defun' || name === 'defun-q') && node.children.length >= 2 && (0, parser_1.astIsSymbol)(node.children[1])) {
62
+ defuns.push({ name: node.children[1].name, line: node.pos.line });
54
63
  }
55
- }
56
- return results;
57
- }
58
- function collectReferencesFromAst(ast) {
59
- const results = [];
60
- const allCalls = (0, parser_1.astFindAll)(ast, n => {
61
- if (n.type !== 'list' || !n.children || n.children.length === 0)
62
- return false;
63
- return (0, parser_1.astIsSymbol)(n.children[0]);
64
- });
65
- for (const call of allCalls) {
66
- const name = call.children[0].name;
67
64
  if (name !== 'defun' && name !== 'defun-q' && !name.startsWith('c:') && !name.includes(':')) {
68
- results.push({ name, line: call.pos.line });
65
+ refs.push({ name, line: node.pos.line });
69
66
  }
70
67
  }
71
- return results;
68
+ return { defuns, refs };
72
69
  }
73
70
  function countFunctionArgsFromAst(ast) {
74
71
  const result = new Map();
75
72
  const defunNodes = (0, parser_1.astFindAll)(ast, n => (0, parser_1.astIsList)(n, 'defun') || (0, parser_1.astIsList)(n, 'defun-q'));
76
73
  for (const node of defunNodes) {
77
- if (!node.children || node.children.length < 3)
74
+ if (!(0, parser_1.astIsList)(node) || node.children.length < 3)
78
75
  continue;
79
76
  const name = (0, parser_1.astIsSymbol)(node.children[1]) ? node.children[1].name : '';
80
77
  if (!name)
81
78
  continue;
82
- const paramList = node.children[2];
83
- if (paramList.type !== 'list' || !paramList.children) {
84
- result.set(name, 0);
85
- continue;
86
- }
87
- let count = 0;
88
- for (const child of paramList.children) {
89
- if (child.type === 'symbol' && child.name && child.name !== '/') {
90
- count++;
91
- }
92
- else if (child.type === 'symbol' && child.name === '/') {
93
- break;
94
- }
95
- }
96
- result.set(name, count);
79
+ result.set(name, (0, shared_1.defunParams)(node).length);
97
80
  }
98
81
  return result;
99
82
  }
100
83
  function countCallArgsFromAst(ast) {
101
84
  const result = new Map();
102
- const allCalls = (0, parser_1.astFindAll)(ast, n => {
103
- if (n.type !== 'list' || !n.children || n.children.length === 0)
104
- return false;
105
- return (0, parser_1.astIsSymbol)(n.children[0]);
106
- });
85
+ const allCalls = (0, parser_1.astFindAll)(ast, n => (0, parser_1.astIsList)(n) && n.children.length > 0 && (0, parser_1.astIsSymbol)(n.children[0]));
107
86
  for (const call of allCalls) {
87
+ if (!(0, parser_1.astIsList)(call) || !(0, parser_1.astIsSymbol)(call.children[0]))
88
+ continue;
108
89
  const name = call.children[0].name;
109
90
  if (name === 'defun' || name === 'defun-q' || name.startsWith('c:') || name.includes(':'))
110
91
  continue;
111
92
  if (!result.has(name))
112
93
  result.set(name, []);
113
- const argCount = call.children ? call.children.length - 1 : 0;
94
+ const argCount = call.children.length - 1;
114
95
  result.get(name).push({ line: call.pos.line, count: argCount });
115
96
  }
116
97
  return result;
@@ -120,26 +101,25 @@ function findModuleDepsFromAst(ast) {
120
101
  const exports = [];
121
102
  const inPackageNodes = (0, parser_1.astFindAll)(ast, n => (0, parser_1.astIsList)(n, 'in-package'));
122
103
  for (const node of inPackageNodes) {
123
- if (node.children && node.children.length >= 3 && node.children[1].type === 'symbol') {
124
- const useList = node.children[2];
125
- if (useList.type === 'list' && useList.children) {
126
- for (const use of useList.children) {
127
- if (use.type === 'symbol' && use.name)
128
- imports.push(use.name);
129
- }
104
+ if (!(0, parser_1.astIsList)(node) || node.children.length < 3)
105
+ continue;
106
+ const useList = node.children[2];
107
+ if ((0, parser_1.astIsList)(useList) && useList.children) {
108
+ for (const use of useList.children) {
109
+ if ((0, parser_1.astIsSymbol)(use) && use.name)
110
+ imports.push(use.name);
130
111
  }
131
112
  }
132
113
  }
133
114
  const setqNodes = (0, parser_1.astFindAll)(ast, n => (0, parser_1.astIsList)(n, 'setq'));
134
115
  for (const node of setqNodes) {
135
- if (node.children &&
116
+ if ((0, parser_1.astIsList)(node) &&
136
117
  node.children.length >= 3 &&
137
- node.children[1].type === 'symbol' &&
118
+ (0, parser_1.astIsSymbol)(node.children[1]) &&
138
119
  node.children[1].name === '@::*modules*' &&
139
- node.children[2].type === 'list' &&
140
- node.children[2].children) {
120
+ (0, parser_1.astIsList)(node.children[2])) {
141
121
  for (const exp of node.children[2].children) {
142
- if (exp.type === 'string' && typeof exp.value === 'string') {
122
+ if (typeof exp.value === 'string') {
143
123
  exports.push(exp.value);
144
124
  }
145
125
  }
@@ -249,38 +229,52 @@ function checkModuleCycle(moduleDeps, rootDir, allIssues) {
249
229
  }
250
230
  }
251
231
  }
252
- function lintProject(files, config, rootDir) {
232
+ async function lintProject(files, config, rootDir) {
253
233
  (0, locale_1.setLocale)(config.locale || 'zh');
254
234
  const allIssues = [];
255
- const symbols = { defuns: new Map(), references: new Map() };
235
+ const symbols = {
236
+ defuns: new Map(),
237
+ references: new Map(),
238
+ fileDefuns: new Map(),
239
+ fileRefs: new Map(),
240
+ };
256
241
  const maxFiles = config.project_analysis?.maxFiles ?? 500;
257
242
  if (files.length > maxFiles) {
258
243
  const msg = `${files.length} files exceeds project_analysis.maxFiles (${maxFiles}). Skipping cross-file analysis. To increase, set project_analysis.maxFiles in config.`;
259
244
  console.warn(`[atlisp-lint] ${msg}`);
260
245
  return [];
261
246
  }
262
- const fileContents = new Map();
263
247
  const defunArgs = new Map();
264
248
  const callArgCounts = new Map();
265
249
  const moduleDeps = new Map();
266
- // Phase A: Streaming collection — one file at a time, keep only metadata
267
- for (const filepath of files) {
268
- let content;
250
+ const checks = config.checks;
251
+ const needsContent = checks['unused_package_dep'] !== 'off';
252
+ // Phase A: Read all files in parallel, then process metadata sequentially
253
+ const processedFiles = [];
254
+ const contentStore = needsContent ? new Map() : undefined;
255
+ const readResults = await Promise.all(files.map(async (filepath) => {
269
256
  try {
270
- content = fs.readFileSync(filepath, 'utf-8');
257
+ const content = await fsp.readFile(filepath, 'utf-8');
258
+ return { filepath, content, ok: true };
271
259
  }
272
260
  catch {
273
- continue;
261
+ return { filepath, ok: false };
274
262
  }
263
+ }));
264
+ for (const result of readResults) {
265
+ if (!result.ok)
266
+ continue;
267
+ const { filepath, content } = result;
275
268
  const ast = (0, parser_1.parseAst)(content, { errorRecovery: true });
276
269
  // Extract all metadata from a single AST parse
277
- const defunList = collectDefunsFromAst(ast);
270
+ const { defuns: defunList, refs: refList } = collectProjectMetadata(ast);
271
+ symbols.fileDefuns.set(filepath, defunList);
278
272
  for (const d of defunList) {
279
273
  const list = symbols.defuns.get(d.name) || [];
280
274
  list.push({ file: filepath, line: d.line });
281
275
  symbols.defuns.set(d.name, list);
282
276
  }
283
- const refList = collectReferencesFromAst(ast);
277
+ symbols.fileRefs.set(filepath, refList);
284
278
  for (const r of refList) {
285
279
  const list = symbols.references.get(r.name) || [];
286
280
  list.push({ file: filepath, line: r.line });
@@ -289,31 +283,24 @@ function lintProject(files, config, rootDir) {
289
283
  defunArgs.set(filepath, countFunctionArgsFromAst(ast));
290
284
  callArgCounts.set(filepath, countCallArgsFromAst(ast));
291
285
  moduleDeps.set(filepath, findModuleDepsFromAst(ast));
292
- // Cache file content for checks that still need it
293
- fileContents.set(filepath, content);
294
- // AST goes out of scope here → GC reclaims it
286
+ processedFiles.push(filepath);
287
+ if (contentStore)
288
+ contentStore.set(filepath, content);
295
289
  }
296
290
  // Phase B: Cross-file checks using only symbol tables
297
- for (const [filepath] of fileContents) {
291
+ for (const filepath of processedFiles) {
298
292
  const relPath = path.relative(rootDir, filepath);
299
293
  const override = (0, config_1.findOverrides)(filepath);
300
- const checks = override?.checks || config.checks;
301
- if (checks['dangling_defun'] !== 'off') {
302
- const defunList = [];
303
- for (const [name, locations] of symbols.defuns) {
304
- for (const loc of locations) {
305
- if (loc.file === filepath) {
306
- defunList.push({ name, line: loc.line });
307
- }
308
- }
309
- }
294
+ const fileChecks = override?.checks || checks;
295
+ if (fileChecks['dangling_defun'] !== 'off') {
296
+ const defunList = symbols.fileDefuns.get(filepath) || [];
310
297
  const danglingIssues = (0, dangling_defun_1.checkDanglingDefunFromDefs)(defunList, symbols.references, filepath);
311
298
  for (const iss of danglingIssues) {
312
299
  iss.file = relPath;
313
300
  allIssues.push(iss);
314
301
  }
315
302
  }
316
- if (checks['missing_export'] !== 'off') {
303
+ if (fileChecks['missing_export'] !== 'off') {
317
304
  const pkgDir = findPackageDir(filepath);
318
305
  if (pkgDir) {
319
306
  const missingIssues = (0, missing_export_1.checkMissingExport)(filepath, pkgDir, symbols.defuns);
@@ -323,8 +310,8 @@ function lintProject(files, config, rootDir) {
323
310
  }
324
311
  }
325
312
  }
326
- if (checks['unused_package_dep'] !== 'off') {
327
- const content = fileContents.get(filepath);
313
+ if (fileChecks['unused_package_dep'] !== 'off') {
314
+ const content = contentStore?.get(filepath);
328
315
  if (content) {
329
316
  const depIssues = (0, unused_package_dep_1.checkUnusedPackageDepFromContent)(filepath, content);
330
317
  for (const iss of depIssues) {
@@ -333,65 +320,64 @@ function lintProject(files, config, rootDir) {
333
320
  }
334
321
  }
335
322
  }
336
- if (checks['duplicate_defun'] !== 'off') {
323
+ if (fileChecks['duplicate_defun'] !== 'off') {
337
324
  const dupIssues = (0, duplicate_defun_1.checkDuplicateDefun)(filepath, symbols.defuns);
338
325
  for (const iss of dupIssues) {
339
326
  iss.file = relPath;
340
327
  allIssues.push(iss);
341
328
  }
342
329
  }
343
- if (checks['arg_count_project'] !== 'off') {
330
+ if (fileChecks['arg_count_project'] !== 'off') {
344
331
  const argIssues = checkArgCountProject(filepath, defunArgs, callArgCounts, symbols, relPath);
345
332
  allIssues.push(...argIssues);
346
333
  }
347
- if (checks['function_reference'] !== 'off') {
348
- const definedHere = new Set();
349
- for (const [name, locations] of symbols.defuns) {
350
- for (const loc of locations) {
351
- if (loc.file === filepath)
352
- definedHere.add(name);
353
- }
354
- }
355
- for (const [name, refs] of symbols.references) {
356
- if (builtin_arg_count_1.BUILTIN_FUNCTIONS.has(name))
334
+ if (fileChecks['function_reference'] !== 'off') {
335
+ const definedHere = new Set((symbols.fileDefuns.get(filepath) || []).map(d => d.name));
336
+ const fileRefs = symbols.fileRefs.get(filepath) || [];
337
+ for (const ref of fileRefs) {
338
+ if (builtin_arg_count_1.BUILTIN_FUNCTIONS.has(ref.name))
357
339
  continue;
358
- if (definedHere.has(name))
340
+ if (definedHere.has(ref.name))
359
341
  continue;
360
- if (symbols.defuns.has(name))
342
+ if (symbols.defuns.has(ref.name))
361
343
  continue;
362
- for (const ref of refs) {
363
- if (ref.file === filepath) {
364
- allIssues.push({
365
- file: relPath,
366
- line: ref.line,
367
- severity: 'warn',
368
- rule: 'function_reference',
369
- message: (0, locale_2.t)('function_reference', name),
370
- });
371
- }
372
- }
344
+ allIssues.push({
345
+ file: relPath,
346
+ line: ref.line,
347
+ severity: 'warn',
348
+ rule: 'function_reference',
349
+ message: (0, locale_2.t)('function_reference', ref.name),
350
+ });
373
351
  }
374
352
  }
375
353
  }
376
354
  // Module cycle check: run DFS once (not per file)
377
- const cycleCheckFile = fileContents.keys().next().value;
378
- if (cycleCheckFile) {
355
+ if (processedFiles.length > 0) {
356
+ const cycleCheckFile = processedFiles[0];
379
357
  const override = (0, config_1.findOverrides)(cycleCheckFile);
380
- const checks = override?.checks || config.checks;
381
- if (checks['module_cycle'] !== 'off') {
358
+ const fileChecks = override?.checks || checks;
359
+ if (fileChecks['module_cycle'] !== 'off') {
382
360
  checkModuleCycle(moduleDeps, rootDir, allIssues);
383
361
  }
384
362
  }
385
363
  return allIssues;
386
364
  }
365
+ const packageDirCache = new Map();
387
366
  function findPackageDir(filepath) {
367
+ const cached = packageDirCache.get(filepath);
368
+ if (cached !== undefined)
369
+ return cached;
388
370
  let dir = path.dirname(filepath);
389
371
  for (;;) {
390
- if (fs.existsSync(path.join(dir, 'pkg.lsp')))
372
+ if (fs.existsSync(path.join(dir, 'pkg.lsp'))) {
373
+ packageDirCache.set(filepath, dir);
391
374
  return dir;
375
+ }
392
376
  const parent = path.dirname(dir);
393
- if (parent === dir)
377
+ if (parent === dir) {
378
+ packageDirCache.set(filepath, null);
394
379
  return null;
380
+ }
395
381
  dir = parent;
396
382
  }
397
383
  }
package/dist/runner.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Issue, LintConfig } from './types';
2
2
  export declare function runChecks(content: string, file: string, config: LintConfig): Issue[];
3
3
  export declare function lintFiles(files: string[], config: LintConfig, rootDir: string, contentCache?: Map<string, string>): Issue[];
4
- export declare function lintFilesParallel(files: string[], config: LintConfig, rootDir: string): Promise<Issue[]>;
4
+ export declare function lintFilesParallel(files: string[], config: LintConfig, rootDir: string, contentCache?: Map<string, string>): Promise<Issue[]>;
5
5
  export { applyFixesFromProviders as applyFixes, FIXABLE_RULES } from './fixes/index';
6
6
  export declare function parseIgnoreFile(rootDir: string): string[];
7
7
  export declare function fixFile(filepath: string): string[];
package/dist/runner.js CHANGED
@@ -138,15 +138,21 @@ function runChecks(content, file, config) {
138
138
  addIfEnabled('error_handling', () => (0, error_handling_1.checkErrorHandling)(content, file));
139
139
  addIfEnabled('global_naming', () => (0, global_naming_1.checkGlobalNaming)(content, file));
140
140
  addIfEnabled('extra_parens', () => (0, extra_parens_1.checkExtraParens)(content, file));
141
- addIfEnabled('arg_count', () => (0, arg_count_1.checkArgCount)(content, file));
142
- addIfEnabled('builtin_arg_count', () => (0, builtin_arg_count_1.checkBuiltinArgCount)(content, file));
141
+ // Early AST parse for checks that need AST but aren't visitor-based
142
+ let ast;
143
+ const needsEarlyAst = ['arg_count', 'builtin_arg_count', 'empty_catch', 'no_return', 'undeclared_setq'].some(r => checks[r] !== 'off');
144
+ if (needsEarlyAst) {
145
+ ast = (0, parser_1.parseAst)(content, { errorRecovery: true });
146
+ }
147
+ addIfEnabled('arg_count', () => (0, arg_count_1.checkArgCount)(content, file, ast));
148
+ addIfEnabled('builtin_arg_count', () => (0, builtin_arg_count_1.checkBuiltinArgCount)(content, file, ast));
143
149
  addIfEnabled('strcat_usage', () => (0, strcat_usage_1.checkStrcatUsage)(content, file));
144
150
  addIfEnabled('cond_simplify', () => (0, cond_simplify_1.checkCondSimplify)(content, file));
145
151
  addIfEnabled('quote_style', () => (0, quote_style_1.checkQuoteStyle)(content, file));
146
152
  addIfEnabled('eq_usage', () => (0, eq_usage_1.checkEqUsage)(content, file));
147
153
  addIfEnabled('lambda_syntax', () => (0, lambda_syntax_1.checkLambdaSyntax)(content, file));
148
154
  addIfEnabled('comment_style', () => (0, comment_style_1.checkCommentStyle)(content, file));
149
- addIfEnabled('empty_catch', () => (0, empty_catch_1.checkEmptyCatch)(content, file));
155
+ addIfEnabled('empty_catch', () => (0, empty_catch_1.checkEmptyCatch)(content, file, ast));
150
156
  addIfEnabled('nth_usage', () => (0, nth_usage_1.checkNthUsage)(content, file));
151
157
  addIfEnabled('append_single', () => (0, append_single_1.checkAppendSingle)(content, file));
152
158
  addIfEnabled('setq_multiple', () => (0, setq_multiple_1.checkSetqMultiple)(content, file));
@@ -154,12 +160,12 @@ function runChecks(content, file, config) {
154
160
  addIfEnabled('magic_number', () => (0, magic_number_1.checkMagicNumber)(content, file));
155
161
  addIfEnabled('mixed_indent', () => (0, mixed_indent_1.checkMixedIndent)(content, file));
156
162
  addIfEnabled('long_function_call', () => (0, long_function_call_1.checkLongFunctionCall)(content, file, 6));
157
- addIfEnabled('no_return', () => (0, no_return_1.checkNoReturn)(content, file));
163
+ addIfEnabled('no_return', () => (0, no_return_1.checkNoReturn)(content, file, ast));
158
164
  addIfEnabled('shadow_builtin', () => (0, shadow_builtin_1.checkShadowBuiltin)(content, file));
159
165
  addIfEnabled('dynamic_doc', () => (0, dynamic_doc_1.checkDynamicDoc)(content, file));
160
166
  addIfEnabled('loop_optimization', () => (0, loop_optimization_1.checkLoopOptimization)(content, file));
161
167
  addIfEnabled('redundant_if', () => (0, redundant_if_1.checkRedundantIf)(content, file));
162
- addIfEnabled('undeclared_setq', () => (0, undeclared_setq_1.checkUndeclaredSetq)(content, file));
168
+ addIfEnabled('undeclared_setq', () => (0, undeclared_setq_1.checkUndeclaredSetq)(content, file, ast));
163
169
  addIfEnabled('format_indent', () => (0, format_indent_1.checkFormatIndent)(content, file, config.line_length.tab_width));
164
170
  // New checks
165
171
  addIfEnabled('unused_catch_result', () => (0, unused_catch_result_1.checkUnusedCatchResult)(content, file));
@@ -212,7 +218,9 @@ function runChecks(content, file, config) {
212
218
  const needsAst = Array.from(astRules).some(r => checks[r] !== 'off');
213
219
  if (needsAst) {
214
220
  try {
215
- const ast = (0, parser_1.parseAst)(content, { errorRecovery: true });
221
+ if (!ast) {
222
+ ast = (0, parser_1.parseAst)(content, { errorRecovery: true });
223
+ }
216
224
  const visitorIssues = (0, visitor_runner_1.runChecksWithVisitor)(ast, file, config, disableMap);
217
225
  addIssues(visitorIssues);
218
226
  }
@@ -261,7 +269,7 @@ function lintFiles(files, config, rootDir, contentCache) {
261
269
  }
262
270
  return allIssues;
263
271
  }
264
- function lintFilesParallel(files, config, rootDir) {
272
+ function lintFilesParallel(files, config, rootDir, contentCache) {
265
273
  (0, locale_1.setLocale)(config.locale || 'zh');
266
274
  return new Promise(resolve => {
267
275
  const allIssues = [];
@@ -274,8 +282,9 @@ function lintFilesParallel(files, config, rootDir) {
274
282
  const idx = nextIndex++;
275
283
  const filepath = files[idx];
276
284
  const relPath = path.relative(rootDir, filepath);
285
+ const content = contentCache?.get(filepath);
277
286
  const worker = new worker_threads_1.Worker(path.join(__dirname, 'worker.js'), {
278
- workerData: { filepath, relPath, config },
287
+ workerData: { filepath, relPath, config, content },
279
288
  });
280
289
  worker.on('message', (msg) => {
281
290
  if (completed.has(idx))