@mishasinitcyn/betterrank 0.2.5 → 0.2.7
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/package.json +1 -1
- package/src/outline.js +64 -9
- package/src/parser.js +143 -36
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mishasinitcyn/betterrank",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "Structural code index with PageRank-ranked repo maps, symbol search, call-graph queries, and dependency analysis. Built on tree-sitter and graphology.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/outline.js
CHANGED
|
@@ -25,7 +25,7 @@ export function buildOutline(source, filePath, expandSymbols = [], { callerCount
|
|
|
25
25
|
return rawView(lines, pad);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
const parsed = parseFile(filePath, source);
|
|
28
|
+
const parsed = parseFile(filePath, source, { includeOutlineDefinitions: true });
|
|
29
29
|
if (!parsed || parsed.definitions.length === 0) {
|
|
30
30
|
return rawView(lines, pad);
|
|
31
31
|
}
|
|
@@ -84,24 +84,79 @@ function expandMode(lines, defs, filePath, expandSymbols, pad) {
|
|
|
84
84
|
return output.join('\n').trimEnd();
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
// Detect a contiguous block of import statements at the top of the file.
|
|
88
|
+
// Returns { start, end, lineCount } (1-indexed) or null if block is < 3 lines.
|
|
89
|
+
// Blank lines and leading non-import lines (e.g. "use client") are skipped over.
|
|
90
|
+
function detectImportBlock(lines) {
|
|
91
|
+
let braceDepth = 0;
|
|
92
|
+
let inImport = false;
|
|
93
|
+
let firstImportLine = -1; // 0-indexed
|
|
94
|
+
let lastImportLine = -1; // 0-indexed
|
|
95
|
+
|
|
96
|
+
for (let i = 0; i < lines.length; i++) {
|
|
97
|
+
const trimmed = lines[i].trim();
|
|
98
|
+
if (trimmed === '') continue;
|
|
99
|
+
|
|
100
|
+
if (!inImport) {
|
|
101
|
+
if (trimmed.startsWith('import ') || trimmed === 'import') {
|
|
102
|
+
if (firstImportLine === -1) firstImportLine = i;
|
|
103
|
+
braceDepth = 0;
|
|
104
|
+
for (const ch of lines[i]) {
|
|
105
|
+
if (ch === '{') braceDepth++;
|
|
106
|
+
else if (ch === '}') braceDepth--;
|
|
107
|
+
}
|
|
108
|
+
if (braceDepth > 0) {
|
|
109
|
+
inImport = true;
|
|
110
|
+
} else {
|
|
111
|
+
lastImportLine = i;
|
|
112
|
+
}
|
|
113
|
+
} else if (firstImportLine !== -1) {
|
|
114
|
+
// First non-blank, non-import line after imports began
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
// else: pre-import line ("use client", comments) — keep scanning
|
|
118
|
+
} else {
|
|
119
|
+
for (const ch of lines[i]) {
|
|
120
|
+
if (ch === '{') braceDepth++;
|
|
121
|
+
else if (ch === '}') braceDepth--;
|
|
122
|
+
}
|
|
123
|
+
if (braceDepth <= 0) {
|
|
124
|
+
inImport = false;
|
|
125
|
+
lastImportLine = i;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (firstImportLine === -1 || lastImportLine === -1) return null;
|
|
131
|
+
const lineCount = lastImportLine - firstImportLine + 1;
|
|
132
|
+
if (lineCount < 3) return null;
|
|
133
|
+
return { start: firstImportLine + 1, end: lastImportLine + 1, lineCount, name: null };
|
|
134
|
+
}
|
|
135
|
+
|
|
87
136
|
function outlineMode(lines, defs, pad, callerCounts) {
|
|
88
|
-
//
|
|
89
|
-
|
|
137
|
+
// Mark definitions that are nested inside another definition.
|
|
138
|
+
// Only top-level defs get their own collapse range — nested ones are swallowed
|
|
139
|
+
// by the parent's collapse, preventing dozens of inline-callback markers.
|
|
140
|
+
const nested = new Set();
|
|
90
141
|
for (const def of defs) {
|
|
91
142
|
for (const other of defs) {
|
|
92
143
|
if (other === def) continue;
|
|
93
|
-
if (
|
|
94
|
-
|
|
144
|
+
if (def.lineStart > other.lineStart && def.lineEnd <= other.lineEnd) {
|
|
145
|
+
nested.add(def);
|
|
95
146
|
break;
|
|
96
147
|
}
|
|
97
148
|
}
|
|
98
149
|
}
|
|
99
150
|
|
|
100
|
-
// Build collapse ranges for leaf definitions with sufficient body size
|
|
101
|
-
// Track which definition each collapse range belongs to (for annotations)
|
|
102
151
|
const collapseRanges = [];
|
|
152
|
+
|
|
153
|
+
// Collapse the import block first (covers the lucide/shadcn import wall in TSX)
|
|
154
|
+
const importCollapse = detectImportBlock(lines);
|
|
155
|
+
if (importCollapse) collapseRanges.push(importCollapse);
|
|
156
|
+
|
|
157
|
+
// Collapse all top-level definitions (functions, classes, interfaces, components)
|
|
103
158
|
for (const def of defs) {
|
|
104
|
-
if (
|
|
159
|
+
if (nested.has(def)) continue;
|
|
105
160
|
if (!def.bodyStartLine) continue;
|
|
106
161
|
if (def.bodyStartLine > def.lineEnd) continue;
|
|
107
162
|
|
|
@@ -131,7 +186,7 @@ function outlineMode(lines, defs, pad, callerCounts) {
|
|
|
131
186
|
let marker = `${' '.repeat(pad)}│ ${indent}... (${range.lineCount} lines)`;
|
|
132
187
|
|
|
133
188
|
// Append caller annotation if available
|
|
134
|
-
if (callerCounts && callerCounts.has(range.name)) {
|
|
189
|
+
if (callerCounts && range.name && callerCounts.has(range.name)) {
|
|
135
190
|
const count = callerCounts.get(range.name);
|
|
136
191
|
const annotation = count === 1 ? '← 1 caller' : `← ${count} callers`;
|
|
137
192
|
// Right-pad to align annotations
|
package/src/parser.js
CHANGED
|
@@ -244,8 +244,13 @@ const KIND_MAP = {
|
|
|
244
244
|
variable_declarator: 'variable',
|
|
245
245
|
namespace_definition: 'namespace',
|
|
246
246
|
decorated_definition: 'function',
|
|
247
|
+
pair: 'property',
|
|
247
248
|
};
|
|
248
249
|
|
|
250
|
+
const OUTLINE_EXTRA_LANGUAGES = new Set(['javascript', 'typescript', 'tsx']);
|
|
251
|
+
const OUTLINE_BODY_TYPES = new Set(['statement_block', 'class_body', 'object', 'array']);
|
|
252
|
+
const OUTLINE_CALL_TYPES = new Set(['call_expression', 'new_expression']);
|
|
253
|
+
|
|
249
254
|
/**
|
|
250
255
|
* Walk an AST subtree and count node types that reveal structural shape.
|
|
251
256
|
* Returns a flat object like { if_statement: 3, for_statement: 1, call_expression: 7, ... }
|
|
@@ -343,22 +348,146 @@ function extractParamNames(node) {
|
|
|
343
348
|
* Find the body/block node of a definition, drilling into wrappers like
|
|
344
349
|
* lexical_declaration → variable_declarator → arrow_function → body.
|
|
345
350
|
*/
|
|
346
|
-
function
|
|
347
|
-
|
|
348
|
-
|
|
351
|
+
function findCallArgumentBody(node, depth) {
|
|
352
|
+
if (!node) return null;
|
|
353
|
+
let fallback = null;
|
|
349
354
|
|
|
350
355
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
351
356
|
const child = node.namedChild(i);
|
|
352
|
-
body = child
|
|
353
|
-
if (body)
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
if (body) return body;
|
|
357
|
+
const body = findBodyNode(child, depth + 1);
|
|
358
|
+
if (!body) continue;
|
|
359
|
+
if (body.type === 'statement_block' || body.type === 'class_body') {
|
|
360
|
+
return body;
|
|
357
361
|
}
|
|
362
|
+
if (!fallback) fallback = body;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return fallback;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function findBodyNode(node, depth = 0) {
|
|
369
|
+
if (!node || depth > 12) return null;
|
|
370
|
+
if (OUTLINE_BODY_TYPES.has(node.type)) return node;
|
|
371
|
+
|
|
372
|
+
const body = node.childForFieldName('body');
|
|
373
|
+
if (body) {
|
|
374
|
+
const nestedBody = findBodyNode(body, depth + 1);
|
|
375
|
+
return nestedBody || body;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (OUTLINE_CALL_TYPES.has(node.type)) {
|
|
379
|
+
const argBody = findCallArgumentBody(node.childForFieldName('arguments'), depth);
|
|
380
|
+
if (argBody) return argBody;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
for (const fieldName of ['value', 'expression', 'argument']) {
|
|
384
|
+
const child = node.childForFieldName(fieldName);
|
|
385
|
+
if (!child) continue;
|
|
386
|
+
const nestedBody = findBodyNode(child, depth + 1);
|
|
387
|
+
if (nestedBody) return nestedBody;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
391
|
+
const nestedBody = findBodyNode(node.namedChild(i), depth + 1);
|
|
392
|
+
if (nestedBody) return nestedBody;
|
|
358
393
|
}
|
|
394
|
+
|
|
359
395
|
return null;
|
|
360
396
|
}
|
|
361
397
|
|
|
398
|
+
function computeBodyStartLine(node, bodyNode) {
|
|
399
|
+
if (!bodyNode) return null;
|
|
400
|
+
|
|
401
|
+
const bodyRow = bodyNode.startPosition.row;
|
|
402
|
+
const defRow = node.startPosition.row;
|
|
403
|
+
return bodyRow === defRow ? bodyRow + 2 : bodyRow + 1;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function createDefinition(name, node, filePath, langName, { bodyStartLine = null, kind } = {}) {
|
|
407
|
+
const bodyNode = bodyStartLine == null ? findBodyNode(node) : null;
|
|
408
|
+
const resolvedBodyStartLine = bodyStartLine ?? computeBodyStartLine(node, bodyNode);
|
|
409
|
+
const profileNode = bodyNode || node;
|
|
410
|
+
|
|
411
|
+
return {
|
|
412
|
+
name,
|
|
413
|
+
kind: kind || nodeKind(node.type),
|
|
414
|
+
file: filePath,
|
|
415
|
+
lineStart: node.startPosition.row + 1,
|
|
416
|
+
lineEnd: node.endPosition.row + 1,
|
|
417
|
+
signature: extractSignature(node, langName),
|
|
418
|
+
bodyStartLine: resolvedBodyStartLine,
|
|
419
|
+
astProfile: buildAstProfile(profileNode),
|
|
420
|
+
paramNames: extractParamNames(node),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function extractPairName(node) {
|
|
425
|
+
const keyNode = node.childForFieldName('key') || node.namedChild(0);
|
|
426
|
+
if (!keyNode) return null;
|
|
427
|
+
|
|
428
|
+
if (['property_identifier', 'identifier', 'private_property_identifier', 'number'].includes(keyNode.type)) {
|
|
429
|
+
return keyNode.text;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (keyNode.type === 'string') {
|
|
433
|
+
return keyNode.text.replace(/^['"`]|['"`]$/g, '');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function hasMultilinePairChildren(node) {
|
|
440
|
+
if (!node || node.type !== 'object') return false;
|
|
441
|
+
|
|
442
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
443
|
+
const child = node.namedChild(i);
|
|
444
|
+
if (child.type !== 'pair') continue;
|
|
445
|
+
if (!extractPairName(child)) continue;
|
|
446
|
+
if (child.endPosition.row > child.startPosition.row) return true;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function collectOutlineDefinitions(rootNode, filePath, langName) {
|
|
453
|
+
if (!OUTLINE_EXTRA_LANGUAGES.has(langName)) return [];
|
|
454
|
+
|
|
455
|
+
const definitions = [];
|
|
456
|
+
|
|
457
|
+
function visit(node) {
|
|
458
|
+
if (node.type === 'pair') {
|
|
459
|
+
const name = extractPairName(node);
|
|
460
|
+
if (name && node.endPosition.row > node.startPosition.row) {
|
|
461
|
+
definitions.push(createDefinition(name, node, filePath, langName, {
|
|
462
|
+
bodyStartLine: node.startPosition.row + 2,
|
|
463
|
+
kind: 'property',
|
|
464
|
+
}));
|
|
465
|
+
}
|
|
466
|
+
} else if (node.type === 'variable_declarator') {
|
|
467
|
+
const nameNode = node.childForFieldName('name');
|
|
468
|
+
const bodyNode = findBodyNode(node);
|
|
469
|
+
if (
|
|
470
|
+
nameNode &&
|
|
471
|
+
nameNode.type === 'identifier' &&
|
|
472
|
+
node.endPosition.row > node.startPosition.row &&
|
|
473
|
+
!(bodyNode && bodyNode.type === 'object' && hasMultilinePairChildren(bodyNode))
|
|
474
|
+
) {
|
|
475
|
+
definitions.push(createDefinition(nameNode.text, node, filePath, langName, {
|
|
476
|
+
bodyStartLine: node.startPosition.row + 2,
|
|
477
|
+
kind: 'variable',
|
|
478
|
+
}));
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
483
|
+
visit(node.namedChild(i));
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
visit(rootNode);
|
|
488
|
+
return definitions;
|
|
489
|
+
}
|
|
490
|
+
|
|
362
491
|
function nodeKind(nodeType) {
|
|
363
492
|
return KIND_MAP[nodeType] || 'other';
|
|
364
493
|
}
|
|
@@ -397,7 +526,7 @@ function extractSignature(node, langName) {
|
|
|
397
526
|
* Parse a single source file and extract definitions + references.
|
|
398
527
|
* Returns null if the language is unsupported.
|
|
399
528
|
*/
|
|
400
|
-
function parseFile(filePath, source) {
|
|
529
|
+
function parseFile(filePath, source, { includeOutlineDefinitions = false } = {}) {
|
|
401
530
|
const dotIdx = filePath.lastIndexOf('.');
|
|
402
531
|
if (dotIdx === -1) return null;
|
|
403
532
|
const ext = filePath.substring(dotIdx);
|
|
@@ -422,39 +551,17 @@ function parseFile(filePath, source) {
|
|
|
422
551
|
if (!nameCapture) continue;
|
|
423
552
|
const defNode = defCapture || nameCapture;
|
|
424
553
|
|
|
425
|
-
|
|
426
|
-
const bodyNode = findBodyNode(defNode.node);
|
|
427
|
-
let bodyStartLine = null;
|
|
428
|
-
if (bodyNode) {
|
|
429
|
-
const bodyRow = bodyNode.startPosition.row; // 0-indexed
|
|
430
|
-
const defRow = defNode.node.startPosition.row; // 0-indexed
|
|
431
|
-
// If body opens on same line as def (JS: `function foo() {`),
|
|
432
|
-
// content starts on next line. Otherwise body IS the content.
|
|
433
|
-
bodyStartLine = bodyRow === defRow ? bodyRow + 2 : bodyRow + 1; // 1-indexed
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// Build AST profile from function body (or whole node if no body)
|
|
437
|
-
const profileNode = bodyNode || defNode.node;
|
|
438
|
-
const astProfile = buildAstProfile(profileNode);
|
|
439
|
-
const paramNames = extractParamNames(defNode.node);
|
|
440
|
-
|
|
441
|
-
definitions.push({
|
|
442
|
-
name: nameCapture.node.text,
|
|
443
|
-
kind: nodeKind(defNode.node.type),
|
|
444
|
-
file: filePath,
|
|
445
|
-
lineStart: defNode.node.startPosition.row + 1,
|
|
446
|
-
lineEnd: defNode.node.endPosition.row + 1,
|
|
447
|
-
signature: extractSignature(defNode.node, langName),
|
|
448
|
-
bodyStartLine,
|
|
449
|
-
astProfile,
|
|
450
|
-
paramNames,
|
|
451
|
-
});
|
|
554
|
+
definitions.push(createDefinition(nameCapture.node.text, defNode.node, filePath, langName));
|
|
452
555
|
}
|
|
453
556
|
} catch (e) {
|
|
454
557
|
// Query may fail on some grammar versions; degrade gracefully
|
|
455
558
|
}
|
|
456
559
|
}
|
|
457
560
|
|
|
561
|
+
if (includeOutlineDefinitions) {
|
|
562
|
+
definitions.push(...collectOutlineDefinitions(tree.rootNode, filePath, langName));
|
|
563
|
+
}
|
|
564
|
+
|
|
458
565
|
const refQueryStr = REF_QUERIES[langName] || REF_QUERIES.default;
|
|
459
566
|
if (refQueryStr) {
|
|
460
567
|
try {
|