@atlisp/lint 0.2.11 → 0.2.12

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.
@@ -196,6 +196,13 @@
196
196
  "walk_exclude": { "type": "array", "items": { "type": "string" }, "default": [".vscode", "vendor", ".git"] },
197
197
  "defmacro_allow_files": { "type": "array", "items": { "type": "string" }, "default": ["compat-cl"] }
198
198
  }
199
+ },
200
+ "project_analysis": {
201
+ "type": "object",
202
+ "properties": {
203
+ "maxFiles": { "type": "integer", "minimum": 1, "default": 500, "description": "Maximum number of files for cross-file analysis. Exceeding this threshold skips project analysis." },
204
+ "batchSize": { "type": "integer", "minimum": 1, "default": 50, "description": "Files processed per batch (reserved for future streaming)" }
205
+ }
199
206
  }
200
207
  }
201
208
  }
@@ -6,4 +6,11 @@ export declare function checkDanglingDefun(file: string, allDefuns: Map<string,
6
6
  file: string;
7
7
  line: number;
8
8
  }[]>): Issue[];
9
+ export declare function checkDanglingDefunFromDefs(localDefuns: {
10
+ name: string;
11
+ line: number;
12
+ }[], allReferences: Map<string, {
13
+ file: string;
14
+ line: number;
15
+ }[]>, file: string): Issue[];
9
16
  //# sourceMappingURL=dangling-defun.d.ts.map
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.checkDanglingDefun = checkDanglingDefun;
37
+ exports.checkDanglingDefunFromDefs = checkDanglingDefunFromDefs;
37
38
  const locale_1 = require("../locale");
38
39
  const parser_1 = require("@atlisp/parser");
39
40
  const fs = __importStar(require("fs"));
@@ -64,4 +65,22 @@ function checkDanglingDefun(file, allDefuns, allReferences) {
64
65
  }
65
66
  return issues;
66
67
  }
68
+ function checkDanglingDefunFromDefs(localDefuns, allReferences, file) {
69
+ const issues = [];
70
+ for (const defun of localDefuns) {
71
+ const refs = allReferences.get(defun.name) || [];
72
+ const calledElsewhere = refs.some(r => r.file !== file);
73
+ const calledLocally = refs.some(r => r.file === file);
74
+ if (!calledLocally && !calledElsewhere) {
75
+ issues.push({
76
+ file,
77
+ line: defun.line,
78
+ severity: 'warn',
79
+ rule: 'dangling_defun',
80
+ message: (0, locale_1.t)('dangling_defun', defun.name),
81
+ });
82
+ }
83
+ }
84
+ return issues;
85
+ }
67
86
  //# sourceMappingURL=dangling-defun.js.map
@@ -1,3 +1,4 @@
1
1
  import { Issue } from '../types';
2
2
  export declare function checkUnusedPackageDep(file: string): Issue[];
3
+ export declare function checkUnusedPackageDepFromContent(file: string, content: string): Issue[];
3
4
  //# sourceMappingURL=unused-package-dep.d.ts.map
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.checkUnusedPackageDep = checkUnusedPackageDep;
37
+ exports.checkUnusedPackageDepFromContent = checkUnusedPackageDepFromContent;
37
38
  const locale_1 = require("../locale");
38
39
  const parser_1 = require("@atlisp/parser");
39
40
  const fs = __importStar(require("fs"));
@@ -90,4 +91,56 @@ function checkUnusedPackageDep(file) {
90
91
  }
91
92
  return issues;
92
93
  }
94
+ function checkUnusedPackageDepFromContent(file, content) {
95
+ const issues = [];
96
+ const ast = (0, parser_1.parseAst)(content, { errorRecovery: true });
97
+ const inPackageNodes = (0, parser_1.astFindAll)(ast, n => (0, parser_1.astIsList)(n, 'in-package'));
98
+ if (inPackageNodes.length === 0)
99
+ return issues;
100
+ const allPackages = new Set();
101
+ for (const node of inPackageNodes) {
102
+ if (node.children && node.children.length >= 2 && (0, parser_1.astIsSymbol)(node.children[1])) {
103
+ allPackages.add(node.children[1].name);
104
+ }
105
+ }
106
+ const usedSymbols = new Set();
107
+ const allLists = (0, parser_1.astFindAll)(ast, n => n.type === 'list');
108
+ for (const node of allLists) {
109
+ if (!node.children || node.children.length === 0)
110
+ continue;
111
+ const car = node.children[0];
112
+ if (car.type === 'symbol' && car.name) {
113
+ const parts = car.name.split(':');
114
+ if (parts.length === 2 && parts[0] && parts[1]) {
115
+ usedSymbols.add(parts[0]);
116
+ }
117
+ }
118
+ for (let i = 1; i < node.children.length; i++) {
119
+ const child = node.children[i];
120
+ if (child.type === 'symbol' && child.name) {
121
+ const parts = child.name.split(':');
122
+ if (parts.length === 2 && parts[0] && parts[1]) {
123
+ usedSymbols.add(parts[0]);
124
+ }
125
+ }
126
+ }
127
+ }
128
+ const mainPackage = inPackageNodes.length > 0 && inPackageNodes[0].children && inPackageNodes[0].children[1]
129
+ ? inPackageNodes[0].children[1].name || ''
130
+ : '';
131
+ allPackages.delete(mainPackage);
132
+ for (const pkg of allPackages) {
133
+ if (!usedSymbols.has(pkg)) {
134
+ const node = inPackageNodes.find(n => n.children && n.children.length >= 2 && (0, parser_1.astIsSymbol)(n.children[1]) && n.children[1].name === pkg);
135
+ issues.push({
136
+ file,
137
+ line: node ? node.pos.line : 1,
138
+ severity: 'warn',
139
+ rule: 'unused_package_dep',
140
+ message: (0, locale_1.t)('unused_package_dep', pkg),
141
+ });
142
+ }
143
+ }
144
+ return issues;
145
+ }
93
146
  //# sourceMappingURL=unused-package-dep.js.map
package/dist/config.js CHANGED
@@ -172,6 +172,10 @@ const DEFAULT_CONFIG = {
172
172
  walk_exclude: ['.vscode', 'vendor', '.git'],
173
173
  defmacro_allow_files: ['compat-cl'],
174
174
  },
175
+ project_analysis: {
176
+ maxFiles: 500,
177
+ batchSize: 50,
178
+ },
175
179
  };
176
180
  function loadConfig(configPath) {
177
181
  if (configPath) {
package/dist/project.js CHANGED
@@ -42,9 +42,8 @@ const missing_export_1 = require("./checks/missing-export");
42
42
  const unused_package_dep_1 = require("./checks/unused-package-dep");
43
43
  const duplicate_defun_1 = require("./checks/duplicate-defun");
44
44
  const locale_1 = require("./locale");
45
- function collectDefuns(file) {
46
- const content = fs.readFileSync(file, 'utf-8');
47
- const ast = (0, parser_1.parseAst)(content, { errorRecovery: true });
45
+ const locale_2 = require("./locale");
46
+ function collectDefunsFromAst(ast) {
48
47
  const results = [];
49
48
  const defunNodes = (0, parser_1.astFindAll)(ast, n => (0, parser_1.astIsList)(n, 'defun') || (0, parser_1.astIsList)(n, 'defun-q'));
50
49
  for (const node of defunNodes) {
@@ -54,9 +53,7 @@ function collectDefuns(file) {
54
53
  }
55
54
  return results;
56
55
  }
57
- function collectReferences(file) {
58
- const content = fs.readFileSync(file, 'utf-8');
59
- const ast = (0, parser_1.parseAst)(content, { errorRecovery: true });
56
+ function collectReferencesFromAst(ast) {
60
57
  const results = [];
61
58
  const allCalls = (0, parser_1.astFindAll)(ast, n => {
62
59
  if (n.type !== 'list' || !n.children || n.children.length === 0)
@@ -71,9 +68,7 @@ function collectReferences(file) {
71
68
  }
72
69
  return results;
73
70
  }
74
- function countFunctionArgs(file) {
75
- const content = fs.readFileSync(file, 'utf-8');
76
- const ast = (0, parser_1.parseAst)(content, { errorRecovery: true });
71
+ function countFunctionArgsFromAst(ast) {
77
72
  const result = new Map();
78
73
  const defunNodes = (0, parser_1.astFindAll)(ast, n => (0, parser_1.astIsList)(n, 'defun') || (0, parser_1.astIsList)(n, 'defun-q'));
79
74
  for (const node of defunNodes) {
@@ -100,9 +95,7 @@ function countFunctionArgs(file) {
100
95
  }
101
96
  return result;
102
97
  }
103
- function countCallArgs(file) {
104
- const content = fs.readFileSync(file, 'utf-8');
105
- const ast = (0, parser_1.parseAst)(content, { errorRecovery: true });
98
+ function countCallArgsFromAst(ast) {
106
99
  const result = new Map();
107
100
  const allCalls = (0, parser_1.astFindAll)(ast, n => {
108
101
  if (n.type !== 'list' || !n.children || n.children.length === 0)
@@ -120,9 +113,7 @@ function countCallArgs(file) {
120
113
  }
121
114
  return result;
122
115
  }
123
- function findModuleDeps(filepath) {
124
- const content = fs.readFileSync(filepath, 'utf-8');
125
- const ast = (0, parser_1.parseAst)(content, { errorRecovery: true });
116
+ function findModuleDepsFromAst(ast) {
126
117
  const imports = [];
127
118
  const exports = [];
128
119
  const inPackageNodes = (0, parser_1.astFindAll)(ast, n => (0, parser_1.astIsList)(n, 'in-package'));
@@ -154,56 +145,148 @@ function findModuleDeps(filepath) {
154
145
  }
155
146
  return { imports, exports };
156
147
  }
157
- function collectAllModuleDeps(files) {
158
- const deps = new Map();
159
- for (const f of files) {
160
- deps.set(f, findModuleDeps(f));
148
+ function checkArgCountProject(filepath, defunArgs, callArgCounts, symbols, relPath) {
149
+ const issues = [];
150
+ const fileArgs = defunArgs.get(filepath);
151
+ if (!fileArgs)
152
+ return issues;
153
+ const callArgs = callArgCounts.get(filepath);
154
+ if (!callArgs)
155
+ return issues;
156
+ for (const [fnName, calls] of callArgs) {
157
+ const defs = symbols.defuns.get(fnName);
158
+ if (!defs)
159
+ continue;
160
+ const defArgCount = fileArgs.get(fnName);
161
+ if (defArgCount === undefined)
162
+ continue;
163
+ let allMatch = true;
164
+ for (const def of defs) {
165
+ const otherFileArgs = defunArgs.get(def.file);
166
+ if (otherFileArgs && otherFileArgs.get(fnName) !== defArgCount) {
167
+ allMatch = false;
168
+ break;
169
+ }
170
+ }
171
+ if (!allMatch)
172
+ continue;
173
+ for (const call of calls) {
174
+ if (call.count !== defArgCount) {
175
+ issues.push({
176
+ file: relPath,
177
+ line: call.line,
178
+ severity: 'warn',
179
+ rule: 'arg_count_project',
180
+ message: (0, locale_2.t)('arg_count_project', fnName, String(call.count), String(defArgCount)),
181
+ });
182
+ }
183
+ }
184
+ }
185
+ return issues;
186
+ }
187
+ function checkModuleCycle(moduleDeps, rootDir, filepath, relPath, allIssues) {
188
+ const visited = new Set();
189
+ const stack = new Set();
190
+ const cyclePath = [];
191
+ const dfs = (current) => {
192
+ if (stack.has(current)) {
193
+ const idx = cyclePath.indexOf(current);
194
+ const cycle = cyclePath.slice(idx).concat(current);
195
+ const displayCycle = cycle.map(c => path.relative(rootDir, c)).join(' → ');
196
+ allIssues.push({
197
+ file: relPath,
198
+ line: 1,
199
+ severity: 'warn',
200
+ rule: 'module_cycle',
201
+ message: `Module dependency cycle detected: ${displayCycle}`,
202
+ });
203
+ return true;
204
+ }
205
+ if (visited.has(current))
206
+ return false;
207
+ visited.add(current);
208
+ stack.add(current);
209
+ cyclePath.push(current);
210
+ const deps = moduleDeps.get(current);
211
+ if (deps) {
212
+ for (const imp of deps.imports) {
213
+ for (const [otherFile, otherDeps] of moduleDeps) {
214
+ if (otherDeps.exports.includes(imp)) {
215
+ if (dfs(otherFile))
216
+ return true;
217
+ }
218
+ }
219
+ }
220
+ }
221
+ cyclePath.pop();
222
+ stack.delete(current);
223
+ return false;
224
+ };
225
+ for (const f of moduleDeps.keys()) {
226
+ if (!visited.has(f)) {
227
+ dfs(f);
228
+ }
161
229
  }
162
- return deps;
163
230
  }
164
231
  function lintProject(files, config, rootDir) {
165
232
  (0, locale_1.setLocale)(config.locale || 'zh');
166
233
  const allIssues = [];
167
234
  const symbols = { defuns: new Map(), references: new Map() };
235
+ const maxFiles = config.project_analysis?.maxFiles ?? 500;
236
+ if (files.length > maxFiles) {
237
+ const msg = `${files.length} files exceeds project_analysis.maxFiles (${maxFiles}). Skipping cross-file analysis. To increase, set project_analysis.maxFiles in config.`;
238
+ console.warn(`[atlisp-lint] ${msg}`);
239
+ return [];
240
+ }
168
241
  const fileContents = new Map();
242
+ const defunArgs = new Map();
243
+ const callArgCounts = new Map();
244
+ const moduleDeps = new Map();
245
+ // Phase A: Streaming collection — one file at a time, keep only metadata
169
246
  for (const filepath of files) {
247
+ let content;
170
248
  try {
171
- const content = fs.readFileSync(filepath, 'utf-8');
172
- fileContents.set(filepath, content);
249
+ content = fs.readFileSync(filepath, 'utf-8');
173
250
  }
174
251
  catch {
175
252
  continue;
176
253
  }
177
- }
178
- // Build project symbol table
179
- for (const [filepath] of fileContents) {
180
- const defunList = collectDefuns(filepath);
254
+ const ast = (0, parser_1.parseAst)(content, { errorRecovery: true });
255
+ // Extract all metadata from a single AST parse
256
+ const defunList = collectDefunsFromAst(ast);
181
257
  for (const d of defunList) {
182
258
  const list = symbols.defuns.get(d.name) || [];
183
259
  list.push({ file: filepath, line: d.line });
184
260
  symbols.defuns.set(d.name, list);
185
261
  }
186
- const refList = collectReferences(filepath);
262
+ const refList = collectReferencesFromAst(ast);
187
263
  for (const r of refList) {
188
264
  const list = symbols.references.get(r.name) || [];
189
265
  list.push({ file: filepath, line: r.line });
190
266
  symbols.references.set(r.name, list);
191
267
  }
268
+ defunArgs.set(filepath, countFunctionArgsFromAst(ast));
269
+ callArgCounts.set(filepath, countCallArgsFromAst(ast));
270
+ moduleDeps.set(filepath, findModuleDepsFromAst(ast));
271
+ // Cache file content for checks that still need it
272
+ fileContents.set(filepath, content);
273
+ // AST goes out of scope here → GC reclaims it
192
274
  }
193
- // New: build module dependency graph
194
- const moduleDeps = collectAllModuleDeps(Array.from(fileContents.keys()));
195
- // New: collect function arg counts per file
196
- const defunArgs = new Map();
197
- for (const [filepath] of fileContents) {
198
- defunArgs.set(filepath, countFunctionArgs(filepath));
199
- }
275
+ // Phase B: Cross-file checks using only symbol tables
200
276
  for (const [filepath] of fileContents) {
201
277
  const relPath = path.relative(rootDir, filepath);
202
278
  const override = findProjectOverride(filepath);
203
279
  const checks = override?.checks || config.checks;
204
- // Existing checks
205
280
  if (checks['dangling_defun'] !== 'off') {
206
- const danglingIssues = (0, dangling_defun_1.checkDanglingDefun)(filepath, symbols.defuns, symbols.references);
281
+ const defunList = [];
282
+ for (const [name, locations] of symbols.defuns) {
283
+ for (const loc of locations) {
284
+ if (loc.file === filepath) {
285
+ defunList.push({ name, line: loc.line });
286
+ }
287
+ }
288
+ }
289
+ const danglingIssues = (0, dangling_defun_1.checkDanglingDefunFromDefs)(defunList, symbols.references, filepath);
207
290
  for (const iss of danglingIssues) {
208
291
  iss.file = relPath;
209
292
  allIssues.push(iss);
@@ -220,10 +303,13 @@ function lintProject(files, config, rootDir) {
220
303
  }
221
304
  }
222
305
  if (checks['unused_package_dep'] !== 'off') {
223
- const depIssues = (0, unused_package_dep_1.checkUnusedPackageDep)(filepath);
224
- for (const iss of depIssues) {
225
- iss.file = relPath;
226
- allIssues.push(iss);
306
+ const content = fileContents.get(filepath);
307
+ if (content) {
308
+ const depIssues = (0, unused_package_dep_1.checkUnusedPackageDepFromContent)(filepath, content);
309
+ for (const iss of depIssues) {
310
+ iss.file = relPath;
311
+ allIssues.push(iss);
312
+ }
227
313
  }
228
314
  }
229
315
  if (checks['duplicate_defun'] !== 'off') {
@@ -233,87 +319,19 @@ function lintProject(files, config, rootDir) {
233
319
  allIssues.push(iss);
234
320
  }
235
321
  }
236
- // New: module cycle detection
237
- if (checks['module_cycle'] !== 'off') {
238
- const visited = new Set();
239
- const stack = new Set();
240
- const cyclePath = [];
241
- const dfs = (current) => {
242
- if (stack.has(current)) {
243
- const idx = cyclePath.indexOf(current);
244
- const cycle = cyclePath.slice(idx).concat(current);
245
- const displayCycle = cycle.map(c => path.relative(rootDir, c)).join(' → ');
246
- allIssues.push({
247
- file: relPath,
248
- line: 1,
249
- severity: 'warn',
250
- rule: 'module_cycle',
251
- message: `Module dependency cycle detected: ${displayCycle}`,
252
- });
253
- return true;
254
- }
255
- if (visited.has(current))
256
- return false;
257
- visited.add(current);
258
- stack.add(current);
259
- cyclePath.push(current);
260
- const deps = moduleDeps.get(current);
261
- if (deps) {
262
- for (const imp of deps.imports) {
263
- for (const [otherFile, otherDeps] of moduleDeps) {
264
- if (otherDeps.exports.includes(imp)) {
265
- if (dfs(otherFile))
266
- return true;
267
- }
268
- }
269
- }
270
- }
271
- cyclePath.pop();
272
- stack.delete(current);
273
- return false;
274
- };
275
- for (const f of moduleDeps.keys()) {
276
- if (!visited.has(f)) {
277
- dfs(f);
278
- }
279
- }
280
- }
281
- // New: signature mismatch check
282
322
  if (checks['arg_count_project'] !== 'off') {
283
- const fileArgs = defunArgs.get(filepath);
284
- if (fileArgs) {
285
- const callArgs = countCallArgs(filepath);
286
- for (const [fnName, calls] of callArgs) {
287
- const defs = symbols.defuns.get(fnName);
288
- if (!defs)
289
- continue;
290
- const defArgCount = fileArgs.get(fnName);
291
- if (defArgCount === undefined)
292
- continue;
293
- // Only check if all definitions agree
294
- let allMatch = true;
295
- for (const def of defs) {
296
- const otherFileArgs = defunArgs.get(def.file);
297
- if (otherFileArgs && otherFileArgs.get(fnName) !== defArgCount) {
298
- allMatch = false;
299
- break;
300
- }
301
- }
302
- if (!allMatch)
303
- continue;
304
- for (const call of calls) {
305
- if (call.count !== defArgCount) {
306
- allIssues.push({
307
- file: relPath,
308
- line: call.line,
309
- severity: 'warn',
310
- rule: 'arg_count_project',
311
- message: `Function '${fnName}' called with ${call.count} arguments but defined with ${defArgCount}`,
312
- });
313
- }
314
- }
315
- }
316
- }
323
+ const argIssues = checkArgCountProject(filepath, defunArgs, callArgCounts, symbols, relPath);
324
+ allIssues.push(...argIssues);
325
+ }
326
+ }
327
+ // Module cycle check: run DFS once (not per file)
328
+ const cycleCheckFile = fileContents.keys().next().value;
329
+ if (cycleCheckFile) {
330
+ const override = findProjectOverride(cycleCheckFile);
331
+ const checks = override?.checks || config.checks;
332
+ if (checks['module_cycle'] !== 'off') {
333
+ const relPath = path.relative(rootDir, cycleCheckFile);
334
+ checkModuleCycle(moduleDeps, rootDir, cycleCheckFile, relPath, allIssues);
317
335
  }
318
336
  }
319
337
  return allIssues;
package/dist/types.d.ts CHANGED
@@ -42,6 +42,10 @@ export interface SbclConfig {
42
42
  walk_exclude: string[];
43
43
  defmacro_allow_files: string[];
44
44
  }
45
+ export interface ProjectAnalysisConfig {
46
+ maxFiles: number;
47
+ batchSize: number;
48
+ }
45
49
  export interface SourceConfig {
46
50
  globs: string[];
47
51
  exclude: string[];
@@ -66,6 +70,7 @@ export interface LintConfig {
66
70
  namespace_header: NamespaceHeaderConfig;
67
71
  bare_function_names: BareFunctionNamesConfig;
68
72
  sbcl: SbclConfig;
73
+ project_analysis: ProjectAnalysisConfig;
69
74
  preset?: string;
70
75
  }
71
76
  export interface FormattedResult {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlisp/lint",
3
- "version": "0.2.11",
3
+ "version": "0.2.12",
4
4
  "description": "AutoLISP static analysis tool — parens, security, conventions, SBCL syntax validation",
5
5
  "keywords": [
6
6
  "CAD",