thorns 5.1.9
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/.thornsignore +581 -0
- package/README.md +175 -0
- package/advanced-metrics.js +203 -0
- package/analyzer.js +257 -0
- package/compact-formatter.js +960 -0
- package/dependency-analyzer.js +252 -0
- package/ignore-parser.js +150 -0
- package/index.js +6 -0
- package/lib.js +537 -0
- package/one-liner.sh +24 -0
- package/package.json +71 -0
- package/queries.js +102 -0
- package/run.sh +81 -0
package/lib.js
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
import Parser from 'tree-sitter';
|
|
2
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
|
|
3
|
+
import { join, extname, relative } from 'path';
|
|
4
|
+
import JavaScript from 'tree-sitter-javascript';
|
|
5
|
+
import TypeScript from 'tree-sitter-typescript';
|
|
6
|
+
import Python from 'tree-sitter-python';
|
|
7
|
+
import Rust from 'tree-sitter-rust';
|
|
8
|
+
import Go from 'tree-sitter-go';
|
|
9
|
+
import C from 'tree-sitter-c';
|
|
10
|
+
import Cpp from 'tree-sitter-cpp';
|
|
11
|
+
import Java from 'tree-sitter-java';
|
|
12
|
+
import CSharp from 'tree-sitter-c-sharp';
|
|
13
|
+
import Ruby from 'tree-sitter-ruby';
|
|
14
|
+
import PHP from 'tree-sitter-php';
|
|
15
|
+
import JSONParser from 'tree-sitter-json';
|
|
16
|
+
import { extractEntities, calculateMetrics } from './analyzer.js';
|
|
17
|
+
import { formatUltraCompact } from './compact-formatter.js';
|
|
18
|
+
import { extractDependencies, buildDependencyGraph, analyzeModules } from './dependency-analyzer.js';
|
|
19
|
+
import { extractAdvancedMetrics, detectDuplication, hashFunction, detectCircularDeps, analyzeFileSizes } from './advanced-metrics.js';
|
|
20
|
+
import { buildIgnoreSet, shouldIgnore } from './ignore-parser.js';
|
|
21
|
+
|
|
22
|
+
const LANGUAGES = {
|
|
23
|
+
'.js': { parser: JavaScript, name: 'JavaScript' },
|
|
24
|
+
'.mjs': { parser: JavaScript, name: 'JavaScript' },
|
|
25
|
+
'.cjs': { parser: JavaScript, name: 'JavaScript' },
|
|
26
|
+
'.jsx': { parser: JavaScript, name: 'JSX' },
|
|
27
|
+
'.ts': { parser: TypeScript.typescript, name: 'TypeScript' },
|
|
28
|
+
'.tsx': { parser: TypeScript.tsx, name: 'TSX' },
|
|
29
|
+
'.py': { parser: Python, name: 'Python' },
|
|
30
|
+
'.rs': { parser: Rust, name: 'Rust' },
|
|
31
|
+
'.go': { parser: Go, name: 'Go' },
|
|
32
|
+
'.c': { parser: C, name: 'C' },
|
|
33
|
+
'.h': { parser: C, name: 'C' },
|
|
34
|
+
'.cpp': { parser: Cpp, name: 'C++' },
|
|
35
|
+
'.cc': { parser: Cpp, name: 'C++' },
|
|
36
|
+
'.cxx': { parser: Cpp, name: 'C++' },
|
|
37
|
+
'.hpp': { parser: Cpp, name: 'C++' },
|
|
38
|
+
'.java': { parser: Java, name: 'Java' },
|
|
39
|
+
'.cs': { parser: CSharp, name: 'C#' },
|
|
40
|
+
'.rb': { parser: Ruby, name: 'Ruby' },
|
|
41
|
+
'.php': { parser: PHP, name: 'PHP' },
|
|
42
|
+
'.json': { parser: JSONParser, name: 'JSON' }
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const MAX_FILE_SIZE = 200 * 1024; // 200KB - anything larger is build/generated code
|
|
46
|
+
|
|
47
|
+
function getLanguage(filepath) {
|
|
48
|
+
const ext = extname(filepath);
|
|
49
|
+
return LANGUAGES[ext];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function* walkDir(dir, baseDir = dir, ignorePatterns = new Set()) {
|
|
53
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
const fullPath = join(dir, entry.name);
|
|
56
|
+
const relativePath = relative(baseDir, fullPath);
|
|
57
|
+
|
|
58
|
+
// Check if path should be ignored
|
|
59
|
+
if (shouldIgnore(relativePath, ignorePatterns) || shouldIgnore(entry.name, ignorePatterns)) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (entry.isDirectory()) {
|
|
64
|
+
yield* walkDir(fullPath, baseDir, ignorePatterns);
|
|
65
|
+
} else if (entry.isFile()) {
|
|
66
|
+
const lang = getLanguage(entry.name);
|
|
67
|
+
if (lang) {
|
|
68
|
+
try {
|
|
69
|
+
const stat = statSync(fullPath);
|
|
70
|
+
if (stat.size <= MAX_FILE_SIZE) {
|
|
71
|
+
yield { path: fullPath, relativePath, lang };
|
|
72
|
+
}
|
|
73
|
+
} catch (e) {}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function extractFunctionName(node) {
|
|
80
|
+
for (const child of node.children) {
|
|
81
|
+
if (child.type === 'identifier' || child.type === 'property_identifier') {
|
|
82
|
+
return child.text;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return 'anonymous';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function extractClassName(node) {
|
|
89
|
+
for (const child of node.children) {
|
|
90
|
+
if (child.type === 'identifier' || child.type === 'type_identifier') {
|
|
91
|
+
return child.text;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return 'Anonymous';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function countNodeParams(node) {
|
|
98
|
+
let count = 0;
|
|
99
|
+
function traverse(n) {
|
|
100
|
+
if (n.type === 'parameter' || n.type === 'formal_parameter' || n.type.includes('param')) {
|
|
101
|
+
count++;
|
|
102
|
+
}
|
|
103
|
+
for (const child of n.children) traverse(child);
|
|
104
|
+
}
|
|
105
|
+
traverse(node);
|
|
106
|
+
return count;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function analyzeTree(tree, sourceCode) {
|
|
110
|
+
const stats = {
|
|
111
|
+
functions: 0,
|
|
112
|
+
classes: 0,
|
|
113
|
+
imports: 0,
|
|
114
|
+
exports: 0,
|
|
115
|
+
complexity: 0,
|
|
116
|
+
lines: sourceCode.split('\n').length
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
function traverse(node) {
|
|
120
|
+
const type = node.type;
|
|
121
|
+
|
|
122
|
+
if (type.includes('function') && type.includes('declaration')) stats.functions++;
|
|
123
|
+
if (type.includes('class') && type.includes('declaration')) stats.classes++;
|
|
124
|
+
if (type.includes('import')) stats.imports++;
|
|
125
|
+
if (type.includes('export')) stats.exports++;
|
|
126
|
+
if (['if_statement', 'while_statement', 'for_statement', 'case_statement', 'catch_clause'].includes(type)) {
|
|
127
|
+
stats.complexity++;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (let child of node.children) {
|
|
131
|
+
traverse(child);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
traverse(tree.rootNode);
|
|
136
|
+
return stats;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function detectDeadCode(depGraph, fileMetrics, projectContext) {
|
|
140
|
+
const deadCode = {
|
|
141
|
+
unusedExports: [],
|
|
142
|
+
testFiles: [],
|
|
143
|
+
orphanedFiles: [],
|
|
144
|
+
possiblyDead: []
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
if (!depGraph?.nodes) return deadCode;
|
|
148
|
+
|
|
149
|
+
const reExporters = new Set();
|
|
150
|
+
for (const [file, node] of depGraph.nodes) {
|
|
151
|
+
if (node.importsFrom.size > 0 && node.exportedNames.size > 0) {
|
|
152
|
+
const fileName = file.split('/').pop();
|
|
153
|
+
if (fileName.includes('index.') || fileName.includes('lib.') || fileName.includes('main.')) {
|
|
154
|
+
reExporters.add(file);
|
|
155
|
+
for (const imported of node.importsFrom) {
|
|
156
|
+
const targetNode = depGraph.nodes.get(imported);
|
|
157
|
+
if (targetNode) {
|
|
158
|
+
targetNode.importedBy.add(file + ':reexport');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const [file, node] of depGraph.nodes) {
|
|
166
|
+
const fileName = file.split('/').pop();
|
|
167
|
+
const isTest = fileName.includes('.test.') || fileName.includes('.spec.') ||
|
|
168
|
+
file.includes('/test/') || file.includes('/__tests__/');
|
|
169
|
+
|
|
170
|
+
if (isTest) {
|
|
171
|
+
deadCode.testFiles.push(file);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const realImporters = Array.from(node.importedBy).filter(i => !i.includes(':reexport'));
|
|
176
|
+
const hasReExporter = Array.from(node.importedBy).some(i => i.includes(':reexport'));
|
|
177
|
+
|
|
178
|
+
if (realImporters.length === 0 && !hasReExporter && node.exportedNames.size > 0) {
|
|
179
|
+
const isEntry = fileName.includes('index.') || fileName.includes('main.') ||
|
|
180
|
+
fileName.includes('app.') || fileName.includes('server.') ||
|
|
181
|
+
fileName.includes('lib.') || fileName.includes('cli.');
|
|
182
|
+
const isConfig = fileName.includes('config') || fileName.includes('.config.');
|
|
183
|
+
|
|
184
|
+
if (!isEntry && !isConfig) {
|
|
185
|
+
deadCode.unusedExports.push({
|
|
186
|
+
file,
|
|
187
|
+
exports: Array.from(node.exportedNames).slice(0, 3)
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (node.importedBy.size === 0 && node.importsFrom.size === 0) {
|
|
193
|
+
const isEntry = fileName.includes('index.') || fileName.includes('main.') ||
|
|
194
|
+
fileName.includes('app.') || fileName.includes('server.') ||
|
|
195
|
+
fileName.includes('lib.') || fileName.includes('cli.');
|
|
196
|
+
if (!isEntry) {
|
|
197
|
+
deadCode.orphanedFiles.push(file);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (realImporters.length === 1 && node.importsFrom.size === 0 && !hasReExporter) {
|
|
202
|
+
deadCode.possiblyDead.push({
|
|
203
|
+
file,
|
|
204
|
+
usedBy: realImporters[0]
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return deadCode;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function analyzeProjectContext(rootPath) {
|
|
213
|
+
const context = {
|
|
214
|
+
type: 'unknown',
|
|
215
|
+
framework: null,
|
|
216
|
+
runtime: null,
|
|
217
|
+
packageManager: null,
|
|
218
|
+
scripts: {},
|
|
219
|
+
dependencies: {},
|
|
220
|
+
devDependencies: {},
|
|
221
|
+
entry: null,
|
|
222
|
+
build: null,
|
|
223
|
+
test: null,
|
|
224
|
+
name: null,
|
|
225
|
+
description: null,
|
|
226
|
+
version: null,
|
|
227
|
+
readme: null
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const packagePath = join(rootPath, 'package.json');
|
|
232
|
+
if (existsSync(packagePath)) {
|
|
233
|
+
const pkg = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
234
|
+
context.name = pkg.name;
|
|
235
|
+
context.description = pkg.description;
|
|
236
|
+
context.version = pkg.version;
|
|
237
|
+
context.scripts = pkg.scripts || {};
|
|
238
|
+
context.dependencies = pkg.dependencies || {};
|
|
239
|
+
context.devDependencies = pkg.devDependencies || {};
|
|
240
|
+
|
|
241
|
+
if (pkg.dependencies?.next || pkg.devDependencies?.next) {
|
|
242
|
+
context.framework = 'Next.js';
|
|
243
|
+
context.type = 'web-app';
|
|
244
|
+
} else if (pkg.dependencies?.react || pkg.devDependencies?.react) {
|
|
245
|
+
context.framework = 'React';
|
|
246
|
+
context.type = 'web-app';
|
|
247
|
+
} else if (pkg.dependencies?.vite || pkg.devDependencies?.vite) {
|
|
248
|
+
context.framework = 'Vite';
|
|
249
|
+
context.type = 'web-app';
|
|
250
|
+
} else if (pkg.dependencies?.express || pkg.devDependencies?.express) {
|
|
251
|
+
context.framework = 'Express';
|
|
252
|
+
context.type = 'server';
|
|
253
|
+
} else if (pkg.bin) {
|
|
254
|
+
context.type = 'cli';
|
|
255
|
+
} else if (pkg.main || pkg.exports) {
|
|
256
|
+
context.type = 'library';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (context.scripts.start) context.entry = context.scripts.start;
|
|
260
|
+
if (context.scripts.build) context.build = context.scripts.build;
|
|
261
|
+
if (context.scripts.test) context.test = context.scripts.test;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const readmeFiles = ['README.md', 'readme.md', 'README.txt', 'README'];
|
|
265
|
+
for (const file of readmeFiles) {
|
|
266
|
+
const readmePath = join(rootPath, file);
|
|
267
|
+
if (existsSync(readmePath)) {
|
|
268
|
+
const content = readFileSync(readmePath, 'utf8');
|
|
269
|
+
const firstPara = content.split('\n\n').slice(0, 2).join(' ').replace(/^#+ /, '').replace(/\n/g, ' ').slice(0, 300);
|
|
270
|
+
context.readme = firstPara.trim();
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const denoPath = join(rootPath, 'deno.json');
|
|
276
|
+
if (existsSync(denoPath)) {
|
|
277
|
+
context.runtime = 'Deno';
|
|
278
|
+
const deno = JSON.parse(readFileSync(denoPath, 'utf8'));
|
|
279
|
+
if (deno.tasks) context.scripts = deno.tasks;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (existsSync(join(rootPath, 'yarn.lock'))) context.packageManager = 'yarn';
|
|
283
|
+
else if (existsSync(join(rootPath, 'pnpm-lock.yaml'))) context.packageManager = 'pnpm';
|
|
284
|
+
else if (existsSync(join(rootPath, 'package-lock.json'))) context.packageManager = 'npm';
|
|
285
|
+
} catch (e) {}
|
|
286
|
+
|
|
287
|
+
return context;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function analyzeCodebase(rootPath = '.') {
|
|
291
|
+
const parser = new Parser();
|
|
292
|
+
const stats = { files: 0, totalLines: 0, byLanguage: {}, errors: [] };
|
|
293
|
+
const entities = {};
|
|
294
|
+
const metrics = { depths: [], hotspots: [] };
|
|
295
|
+
const fileMetrics = {};
|
|
296
|
+
const fileAnalysis = {};
|
|
297
|
+
const projectContext = analyzeProjectContext(rootPath);
|
|
298
|
+
|
|
299
|
+
// Build comprehensive ignore set - always exclude build artifacts
|
|
300
|
+
const ignorePatterns = buildIgnoreSet(rootPath);
|
|
301
|
+
|
|
302
|
+
for (const { path, relativePath, lang } of walkDir(rootPath, rootPath, ignorePatterns)) {
|
|
303
|
+
try {
|
|
304
|
+
parser.setLanguage(lang.parser);
|
|
305
|
+
const source = readFileSync(path, 'utf8');
|
|
306
|
+
const tree = parser.parse(source);
|
|
307
|
+
|
|
308
|
+
const basicStats = analyzeTree(tree, source);
|
|
309
|
+
const ents = extractEntities(tree, source, lang.name);
|
|
310
|
+
const mets = calculateMetrics(tree, source);
|
|
311
|
+
const deps = extractDependencies(tree, source, relativePath, lang.name);
|
|
312
|
+
const advanced = extractAdvancedMetrics(tree, source);
|
|
313
|
+
|
|
314
|
+
stats.files++;
|
|
315
|
+
stats.totalLines += basicStats.lines;
|
|
316
|
+
|
|
317
|
+
if (!stats.byLanguage[lang.name]) {
|
|
318
|
+
stats.byLanguage[lang.name] = { files: 0, lines: 0, functions: 0, classes: 0, imports: 0, exports: 0, complexity: 0 };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const langStats = stats.byLanguage[lang.name];
|
|
322
|
+
langStats.files++;
|
|
323
|
+
langStats.lines += basicStats.lines;
|
|
324
|
+
langStats.functions += basicStats.functions;
|
|
325
|
+
langStats.classes += basicStats.classes;
|
|
326
|
+
langStats.imports += basicStats.imports;
|
|
327
|
+
langStats.exports += basicStats.exports;
|
|
328
|
+
langStats.complexity += basicStats.complexity;
|
|
329
|
+
|
|
330
|
+
if (!entities[lang.name]) {
|
|
331
|
+
entities[lang.name] = {
|
|
332
|
+
functions: new Map(),
|
|
333
|
+
classes: new Map(),
|
|
334
|
+
imports: new Set(),
|
|
335
|
+
exports: new Set(),
|
|
336
|
+
patterns: new Map(),
|
|
337
|
+
asyncPatterns: { async: 0, await: 0, promise: 0, callback: 0, thenCatch: 0 },
|
|
338
|
+
errorPatterns: { tryCatch: 0, throw: 0, errorTypes: new Set() },
|
|
339
|
+
internalCalls: new Map(),
|
|
340
|
+
constants: [],
|
|
341
|
+
globalState: [],
|
|
342
|
+
envVars: new Set(),
|
|
343
|
+
urls: new Set(),
|
|
344
|
+
filePaths: new Set(),
|
|
345
|
+
eventPatterns: { emitters: 0, listeners: 0 },
|
|
346
|
+
httpPatterns: { routes: [], fetches: 0, axios: 0 },
|
|
347
|
+
storagePatterns: { sql: 0, fileOps: 0, json: 0 }
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
for (const [sig, data] of ents.functions) {
|
|
352
|
+
const existing = entities[lang.name].functions.get(sig) || { count: 0, ...data };
|
|
353
|
+
existing.count += data.count;
|
|
354
|
+
entities[lang.name].functions.set(sig, existing);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
for (const [name, data] of ents.classes) {
|
|
358
|
+
const existing = entities[lang.name].classes.get(name) || { count: 0, ...data };
|
|
359
|
+
existing.count += data.count;
|
|
360
|
+
entities[lang.name].classes.set(name, existing);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
for (const imp of ents.imports) entities[lang.name].imports.add(imp);
|
|
364
|
+
for (const exp of ents.exports) entities[lang.name].exports.add(exp);
|
|
365
|
+
|
|
366
|
+
for (const [pattern, count] of ents.patterns) {
|
|
367
|
+
entities[lang.name].patterns.set(pattern, (entities[lang.name].patterns.get(pattern) || 0) + count);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (ents.asyncPatterns) {
|
|
371
|
+
entities[lang.name].asyncPatterns.async += ents.asyncPatterns.async;
|
|
372
|
+
entities[lang.name].asyncPatterns.await += ents.asyncPatterns.await;
|
|
373
|
+
entities[lang.name].asyncPatterns.promise += ents.asyncPatterns.promise;
|
|
374
|
+
entities[lang.name].asyncPatterns.callback += ents.asyncPatterns.callback;
|
|
375
|
+
entities[lang.name].asyncPatterns.thenCatch += ents.asyncPatterns.thenCatch;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (ents.errorPatterns) {
|
|
379
|
+
entities[lang.name].errorPatterns.tryCatch += ents.errorPatterns.tryCatch;
|
|
380
|
+
entities[lang.name].errorPatterns.throw += ents.errorPatterns.throw;
|
|
381
|
+
for (const errType of ents.errorPatterns.errorTypes) {
|
|
382
|
+
entities[lang.name].errorPatterns.errorTypes.add(errType);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (ents.internalCalls) {
|
|
387
|
+
for (const [name, count] of ents.internalCalls) {
|
|
388
|
+
entities[lang.name].internalCalls.set(name, (entities[lang.name].internalCalls.get(name) || 0) + count);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (ents.constants) {
|
|
393
|
+
entities[lang.name].constants.push(...ents.constants);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (ents.globalState) {
|
|
397
|
+
entities[lang.name].globalState.push(...ents.globalState);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (ents.envVars) {
|
|
401
|
+
for (const v of ents.envVars) entities[lang.name].envVars.add(v);
|
|
402
|
+
}
|
|
403
|
+
if (ents.urls) {
|
|
404
|
+
for (const u of ents.urls) entities[lang.name].urls.add(u);
|
|
405
|
+
}
|
|
406
|
+
if (ents.filePaths) {
|
|
407
|
+
for (const p of ents.filePaths) entities[lang.name].filePaths.add(p);
|
|
408
|
+
}
|
|
409
|
+
if (ents.eventPatterns) {
|
|
410
|
+
entities[lang.name].eventPatterns.emitters += ents.eventPatterns.emitters;
|
|
411
|
+
entities[lang.name].eventPatterns.listeners += ents.eventPatterns.listeners;
|
|
412
|
+
}
|
|
413
|
+
if (ents.httpPatterns) {
|
|
414
|
+
entities[lang.name].httpPatterns.fetches += ents.httpPatterns.fetches;
|
|
415
|
+
entities[lang.name].httpPatterns.axios += ents.httpPatterns.axios;
|
|
416
|
+
entities[lang.name].httpPatterns.routes.push(...ents.httpPatterns.routes);
|
|
417
|
+
}
|
|
418
|
+
if (ents.storagePatterns) {
|
|
419
|
+
entities[lang.name].storagePatterns.sql += ents.storagePatterns.sql;
|
|
420
|
+
entities[lang.name].storagePatterns.fileOps += ents.storagePatterns.fileOps;
|
|
421
|
+
entities[lang.name].storagePatterns.json += ents.storagePatterns.json;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
metrics.depths.push(mets.maxDepth);
|
|
425
|
+
|
|
426
|
+
if (mets.branches > 10 || mets.maxDepth > 8) {
|
|
427
|
+
metrics.hotspots.push({
|
|
428
|
+
file: relativePath,
|
|
429
|
+
cx: mets.branches,
|
|
430
|
+
depth: mets.maxDepth,
|
|
431
|
+
loc: mets.loc
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Store for dependency/duplication analysis
|
|
436
|
+
fileAnalysis[relativePath] = {
|
|
437
|
+
imports: deps.imports,
|
|
438
|
+
exports: deps.exports,
|
|
439
|
+
importPaths: deps.importPaths,
|
|
440
|
+
exportedNames: deps.exportedNames
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
fileMetrics[relativePath] = {
|
|
444
|
+
loc: mets.loc,
|
|
445
|
+
advanced,
|
|
446
|
+
functionHashes: {},
|
|
447
|
+
functions: [],
|
|
448
|
+
classes: []
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
function collectFunctionHashes(node, depth = 0) {
|
|
452
|
+
if (node.type.includes('function') && node.type.includes('declaration') ||
|
|
453
|
+
node.type === 'method_definition' || node.type === 'function_item') {
|
|
454
|
+
const hash = hashFunction(node);
|
|
455
|
+
const sig = node.text.slice(0, 50);
|
|
456
|
+
fileMetrics[relativePath].functionHashes[sig] = hash;
|
|
457
|
+
|
|
458
|
+
const name = extractFunctionName(node);
|
|
459
|
+
const lines = node.text.split('\n').length;
|
|
460
|
+
const startLine = node.startPosition.row + 1;
|
|
461
|
+
fileMetrics[relativePath].functions.push({
|
|
462
|
+
name,
|
|
463
|
+
lines,
|
|
464
|
+
startLine,
|
|
465
|
+
params: countNodeParams(node)
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (node.type.includes('class') && node.type.includes('declaration') ||
|
|
470
|
+
node.type === 'struct_item' || node.type === 'enum_item' || node.type === 'interface_declaration') {
|
|
471
|
+
const name = extractClassName(node);
|
|
472
|
+
const startLine = node.startPosition.row + 1;
|
|
473
|
+
fileMetrics[relativePath].classes.push({
|
|
474
|
+
name,
|
|
475
|
+
startLine
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
for (const child of node.children) collectFunctionHashes(child, depth + 1);
|
|
480
|
+
}
|
|
481
|
+
collectFunctionHashes(tree.rootNode);
|
|
482
|
+
|
|
483
|
+
} catch (e) {
|
|
484
|
+
stats.errors.push({ file: relativePath, error: e.message });
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
metrics.hotspots.sort((a, b) => b.cx + b.depth - (a.cx + a.depth));
|
|
489
|
+
|
|
490
|
+
// Advanced analysis
|
|
491
|
+
const depGraph = buildDependencyGraph(fileAnalysis);
|
|
492
|
+
const duplicates = detectDuplication(fileMetrics);
|
|
493
|
+
const circular = detectCircularDeps(depGraph);
|
|
494
|
+
const fileSizes = analyzeFileSizes(fileMetrics);
|
|
495
|
+
const modules = analyzeModules(fileAnalysis, rootPath);
|
|
496
|
+
|
|
497
|
+
// Aggregate advanced metrics
|
|
498
|
+
const allIdentifiers = new Map();
|
|
499
|
+
const allFuncLengths = [];
|
|
500
|
+
const allFuncParams = [];
|
|
501
|
+
|
|
502
|
+
for (const [file, data] of Object.entries(fileMetrics)) {
|
|
503
|
+
if (data.advanced) {
|
|
504
|
+
for (const [id, count] of data.advanced.identifiers) {
|
|
505
|
+
allIdentifiers.set(id, (allIdentifiers.get(id) || 0) + count);
|
|
506
|
+
}
|
|
507
|
+
allFuncLengths.push(...data.advanced.functionLengths);
|
|
508
|
+
allFuncParams.push(...data.advanced.functionParams);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const deadCode = detectDeadCode(depGraph, fileMetrics, projectContext);
|
|
513
|
+
|
|
514
|
+
return {
|
|
515
|
+
stats,
|
|
516
|
+
entities,
|
|
517
|
+
metrics,
|
|
518
|
+
depGraph,
|
|
519
|
+
duplicates,
|
|
520
|
+
circular,
|
|
521
|
+
fileSizes,
|
|
522
|
+
modules,
|
|
523
|
+
identifiers: allIdentifiers,
|
|
524
|
+
funcLengths: allFuncLengths,
|
|
525
|
+
funcParams: allFuncParams,
|
|
526
|
+
fileMetrics,
|
|
527
|
+
projectContext,
|
|
528
|
+
deadCode
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export function analyze(rootPath = '.') {
|
|
533
|
+
const aggregated = analyzeCodebase(rootPath);
|
|
534
|
+
return formatUltraCompact(aggregated);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export { analyzeCodebase, formatUltraCompact };
|
package/one-liner.sh
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Minimal one-liner for running Thorns directly from GitHub
|
|
3
|
+
# Requires: bun (or node as fallback)
|
|
4
|
+
# Usage: bash <(curl -fsSL https://raw.githubusercontent.com/AnEntrypoint/mcp-thorns/main/one-liner.sh) [path]
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
T=$(mktemp -d)
|
|
8
|
+
cd "$T"
|
|
9
|
+
trap "rm -rf '$T'" EXIT
|
|
10
|
+
|
|
11
|
+
# Download minimal set of files
|
|
12
|
+
for f in index.js lib.js analyzer.js compact-formatter.js dependency-analyzer.js advanced-metrics.js ignore-parser.js queries.js .thornsignore; do
|
|
13
|
+
curl -fsSL -o "$f" "https://raw.githubusercontent.com/AnEntrypoint/mcp-thorns/main/$f"
|
|
14
|
+
done
|
|
15
|
+
|
|
16
|
+
# Fetch package.json for dependencies info (not executed, just for reference)
|
|
17
|
+
curl -fsSL -o package.json "https://raw.githubusercontent.com/AnEntrypoint/mcp-thorns/main/package.json"
|
|
18
|
+
|
|
19
|
+
# Execute with bun if available, fallback to node
|
|
20
|
+
if command -v bun &> /dev/null; then
|
|
21
|
+
bun index.js "${1:-.}"
|
|
22
|
+
else
|
|
23
|
+
node index.js "${1:-.}"
|
|
24
|
+
fi
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "thorns",
|
|
3
|
+
"version": "5.1.9",
|
|
4
|
+
"description": "Perfect one-shot codebase overview: project context, architecture flow, async/error patterns, dead code, internal call graph",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./lib.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib.js"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"mcp-thorns": "./index.js",
|
|
12
|
+
"thorns": "./index.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"index.js",
|
|
16
|
+
"lib.js",
|
|
17
|
+
"analyzer.js",
|
|
18
|
+
"compact-formatter.js",
|
|
19
|
+
"dependency-analyzer.js",
|
|
20
|
+
"advanced-metrics.js",
|
|
21
|
+
"ignore-parser.js",
|
|
22
|
+
".thornsignore",
|
|
23
|
+
"queries.js",
|
|
24
|
+
"README.md",
|
|
25
|
+
"run.sh",
|
|
26
|
+
"one-liner.sh"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"analyze": "node index.js"
|
|
30
|
+
},
|
|
31
|
+
"bun": {
|
|
32
|
+
"bin": {
|
|
33
|
+
"mcp-thorns": "./index.js",
|
|
34
|
+
"thorns": "./index.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/AnEntrypoint/mcp-thorns.git"
|
|
40
|
+
},
|
|
41
|
+
"author": "AnEntrypoint",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"homepage": "https://github.com/AnEntrypoint/mcp-thorns#readme",
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/AnEntrypoint/mcp-thorns/issues"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"tree-sitter": "^0.21.1",
|
|
49
|
+
"tree-sitter-javascript": "^0.21.0",
|
|
50
|
+
"tree-sitter-typescript": "^0.21.0",
|
|
51
|
+
"tree-sitter-python": "^0.21.0",
|
|
52
|
+
"tree-sitter-rust": "^0.21.0",
|
|
53
|
+
"tree-sitter-go": "^0.21.0",
|
|
54
|
+
"tree-sitter-c": "^0.21.0",
|
|
55
|
+
"tree-sitter-cpp": "^0.22.0",
|
|
56
|
+
"tree-sitter-java": "^0.21.0",
|
|
57
|
+
"tree-sitter-c-sharp": "^0.21.0",
|
|
58
|
+
"tree-sitter-ruby": "^0.21.0",
|
|
59
|
+
"tree-sitter-php": "^0.22.0",
|
|
60
|
+
"tree-sitter-json": "^0.21.0"
|
|
61
|
+
},
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": ">=18.0.0"
|
|
64
|
+
},
|
|
65
|
+
"keywords": [
|
|
66
|
+
"tree-sitter",
|
|
67
|
+
"analysis",
|
|
68
|
+
"ast",
|
|
69
|
+
"codebase"
|
|
70
|
+
]
|
|
71
|
+
}
|