@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.
- package/atlisp-lint.schema.json +7 -0
- package/dist/checks/dangling-defun.d.ts +7 -0
- package/dist/checks/dangling-defun.js +19 -0
- package/dist/checks/unused-package-dep.d.ts +1 -0
- package/dist/checks/unused-package-dep.js +53 -0
- package/dist/config.js +4 -0
- package/dist/project.js +138 -120
- package/dist/types.d.ts +5 -0
- package/package.json +1 -1
package/atlisp-lint.schema.json
CHANGED
|
@@ -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
|
|
@@ -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
|
-
|
|
46
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
172
|
-
fileContents.set(filepath, content);
|
|
249
|
+
content = fs.readFileSync(filepath, 'utf-8');
|
|
173
250
|
}
|
|
174
251
|
catch {
|
|
175
252
|
continue;
|
|
176
253
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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 =
|
|
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
|
-
//
|
|
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
|
|
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
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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 {
|