@mishasinitcyn/betterrank 0.2.8 → 0.2.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/package.json +1 -1
- package/src/graph.js +67 -2
- package/src/index.js +9 -1
- package/src/parser.js +116 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mishasinitcyn/betterrank",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
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/graph.js
CHANGED
|
@@ -25,6 +25,13 @@ const IMPORT_RESOLVE_EXTENSIONS = [
|
|
|
25
25
|
'.py', '.rs', '.go', '.rb', '.java',
|
|
26
26
|
'.c', '.h', '.cpp', '.hpp', '.cc', '.cs', '.php',
|
|
27
27
|
];
|
|
28
|
+
const FOCUS_MAX_HOPS = 2;
|
|
29
|
+
const FOCUS_DISTANCE_WEIGHTS = new Map([
|
|
30
|
+
[0, 250],
|
|
31
|
+
[1, 12],
|
|
32
|
+
[2, 2.5],
|
|
33
|
+
]);
|
|
34
|
+
const FOCUS_DEFAULT_WEIGHT = 0.15;
|
|
28
35
|
|
|
29
36
|
/**
|
|
30
37
|
* Disambiguate which targets a reference should wire to.
|
|
@@ -133,7 +140,14 @@ function wireSymbolReferences(records, graph, defIndex, addedRefs, addedImports)
|
|
|
133
140
|
const targets = defIndex.get(ref.name);
|
|
134
141
|
if (!targets) continue;
|
|
135
142
|
|
|
136
|
-
const
|
|
143
|
+
const filteredTargets = ref.kind === 'property'
|
|
144
|
+
? targets.filter(target => {
|
|
145
|
+
try { return graph.getNodeAttribute(target, 'kind') === 'property'; } catch { return false; }
|
|
146
|
+
})
|
|
147
|
+
: targets;
|
|
148
|
+
if (filteredTargets.length === 0) continue;
|
|
149
|
+
|
|
150
|
+
const resolvedTargets = disambiguateTargets(filteredTargets, file, graph);
|
|
137
151
|
|
|
138
152
|
for (const target of resolvedTargets) {
|
|
139
153
|
const targetFile = graph.getNodeAttribute(target, 'file');
|
|
@@ -263,6 +277,56 @@ function removeFileNodes(graph, filePath) {
|
|
|
263
277
|
}
|
|
264
278
|
}
|
|
265
279
|
|
|
280
|
+
function buildFocusDistanceMap(graph, focusFiles, maxHops = FOCUS_MAX_HOPS) {
|
|
281
|
+
const distances = new Map();
|
|
282
|
+
const queue = [];
|
|
283
|
+
|
|
284
|
+
for (const file of focusFiles) {
|
|
285
|
+
if (!graph.hasNode(file)) continue;
|
|
286
|
+
const attrs = graph.getNodeAttributes(file);
|
|
287
|
+
if (attrs.type !== 'file') continue;
|
|
288
|
+
distances.set(file, 0);
|
|
289
|
+
queue.push(file);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
for (let i = 0; i < queue.length; i++) {
|
|
293
|
+
const current = queue[i];
|
|
294
|
+
const currentDistance = distances.get(current);
|
|
295
|
+
if (currentDistance >= maxHops) continue;
|
|
296
|
+
|
|
297
|
+
const visitNeighbor = neighbor => {
|
|
298
|
+
if (!graph.hasNode(neighbor)) return;
|
|
299
|
+
const neighborAttrs = graph.getNodeAttributes(neighbor);
|
|
300
|
+
if (neighborAttrs.type !== 'file') return;
|
|
301
|
+
|
|
302
|
+
const nextDistance = currentDistance + 1;
|
|
303
|
+
const existing = distances.get(neighbor);
|
|
304
|
+
if (existing !== undefined && existing <= nextDistance) return;
|
|
305
|
+
distances.set(neighbor, nextDistance);
|
|
306
|
+
queue.push(neighbor);
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
graph.forEachOutEdge(current, (_edge, attrs, _source, target) => {
|
|
310
|
+
if (attrs.type !== 'IMPORTS') return;
|
|
311
|
+
visitNeighbor(target);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
graph.forEachInEdge(current, (_edge, attrs, source) => {
|
|
315
|
+
if (attrs.type !== 'IMPORTS') return;
|
|
316
|
+
visitNeighbor(source);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return distances;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function getFocusWeight(filePath, focusDistances) {
|
|
324
|
+
if (!focusDistances || focusDistances.size === 0) return 1.0;
|
|
325
|
+
const distance = focusDistances.get(filePath);
|
|
326
|
+
if (distance === undefined) return FOCUS_DEFAULT_WEIGHT;
|
|
327
|
+
return FOCUS_DISTANCE_WEIGHTS.get(distance) || FOCUS_DEFAULT_WEIGHT;
|
|
328
|
+
}
|
|
329
|
+
|
|
266
330
|
// Path-tier dampening: files outside core source directories get their
|
|
267
331
|
// PageRank scores multiplied by a fraction. This prevents scripts, tests,
|
|
268
332
|
// and temp files from dominating the map output over actual source code.
|
|
@@ -298,6 +362,7 @@ function rankedSymbols(graph, focusFiles = [], pathTiers = DEFAULT_PATH_TIERS) {
|
|
|
298
362
|
if (graph.order === 0) return [];
|
|
299
363
|
|
|
300
364
|
const g = graph.copy();
|
|
365
|
+
const focusDistances = focusFiles.length > 0 ? buildFocusDistanceMap(graph, focusFiles) : null;
|
|
301
366
|
|
|
302
367
|
if (focusFiles.length > 0) {
|
|
303
368
|
g.mergeNode('__focus__', { type: 'virtual' });
|
|
@@ -329,7 +394,7 @@ function rankedSymbols(graph, focusFiles = [], pathTiers = DEFAULT_PATH_TIERS) {
|
|
|
329
394
|
.map(([key, score]) => {
|
|
330
395
|
try {
|
|
331
396
|
const file = graph.getNodeAttribute(key, 'file');
|
|
332
|
-
return [key, score * getPathWeight(file, pathTiers)];
|
|
397
|
+
return [key, score * getPathWeight(file, pathTiers) * getFocusWeight(file, focusDistances)];
|
|
333
398
|
} catch {
|
|
334
399
|
return [key, score];
|
|
335
400
|
}
|
package/src/index.js
CHANGED
|
@@ -579,6 +579,8 @@ class CodeIndex {
|
|
|
579
579
|
const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
580
580
|
const callPattern = new RegExp(`(?<![a-zA-Z0-9_])${escaped}\\s*\\(`);
|
|
581
581
|
const jsxPattern = new RegExp(`<\\s*${escaped}(?=[\\s>/.]|$)`);
|
|
582
|
+
const memberPattern = new RegExp(`\\.\\s*${escaped}\\b`);
|
|
583
|
+
const bracketPattern = new RegExp(`\\[\\s*['"\`]${escaped}['"\`]\\s*\\]`);
|
|
582
584
|
// Fallback: import/from lines that reference the symbol
|
|
583
585
|
const importPattern = new RegExp(`(?:import|from)\\s.*\\b${escaped}\\b`);
|
|
584
586
|
|
|
@@ -605,7 +607,13 @@ class CodeIndex {
|
|
|
605
607
|
if (inDef) continue;
|
|
606
608
|
|
|
607
609
|
const line = lines[i];
|
|
608
|
-
if (
|
|
610
|
+
if (
|
|
611
|
+
!callPattern.test(line) &&
|
|
612
|
+
!jsxPattern.test(line) &&
|
|
613
|
+
!memberPattern.test(line) &&
|
|
614
|
+
!bracketPattern.test(line) &&
|
|
615
|
+
!importPattern.test(line)
|
|
616
|
+
) continue;
|
|
609
617
|
|
|
610
618
|
const start = Math.max(0, i - context);
|
|
611
619
|
const end = Math.min(lines.length - 1, i + context);
|
package/src/parser.js
CHANGED
|
@@ -188,6 +188,7 @@ const REF_QUERIES = {
|
|
|
188
188
|
(call_expression function: (identifier) @ref)
|
|
189
189
|
(import_specifier name: (identifier) @ref)
|
|
190
190
|
(import_clause (identifier) @ref)
|
|
191
|
+
(member_expression property: (property_identifier) @prop_ref)
|
|
191
192
|
(jsx_opening_element (identifier) @jsx_ref)
|
|
192
193
|
(jsx_self_closing_element (identifier) @jsx_ref)
|
|
193
194
|
(jsx_opening_element (member_expression (identifier) @jsx_ref))
|
|
@@ -198,6 +199,7 @@ const REF_QUERIES = {
|
|
|
198
199
|
(call_expression function: (identifier) @ref)
|
|
199
200
|
(import_specifier name: (identifier) @ref)
|
|
200
201
|
(import_clause (identifier) @ref)
|
|
202
|
+
(member_expression property: (property_identifier) @prop_ref)
|
|
201
203
|
(type_identifier) @ref
|
|
202
204
|
`,
|
|
203
205
|
|
|
@@ -205,6 +207,7 @@ const REF_QUERIES = {
|
|
|
205
207
|
(call_expression function: (identifier) @ref)
|
|
206
208
|
(import_specifier name: (identifier) @ref)
|
|
207
209
|
(import_clause (identifier) @ref)
|
|
210
|
+
(member_expression property: (property_identifier) @prop_ref)
|
|
208
211
|
(type_identifier) @ref
|
|
209
212
|
(jsx_opening_element (identifier) @jsx_ref)
|
|
210
213
|
(jsx_self_closing_element (identifier) @jsx_ref)
|
|
@@ -287,8 +290,12 @@ const KIND_MAP = {
|
|
|
287
290
|
};
|
|
288
291
|
|
|
289
292
|
const OUTLINE_EXTRA_LANGUAGES = new Set(['javascript', 'typescript', 'tsx']);
|
|
293
|
+
const INDEX_EXTRA_LANGUAGES = new Set(['javascript', 'typescript', 'tsx']);
|
|
290
294
|
const OUTLINE_BODY_TYPES = new Set(['statement_block', 'class_body', 'object', 'array']);
|
|
291
295
|
const OUTLINE_CALL_TYPES = new Set(['call_expression', 'new_expression']);
|
|
296
|
+
const SEMANTIC_CONTAINER_NAME_RE = /(router|routes|handlers|actions|reducers|selectors|queries|mutations|registry|registries|map|maps|config|configs|options|endpoints|procedures)$/i;
|
|
297
|
+
const NON_SEMANTIC_CONTAINER_NAME_RE = /(schema|shape|validator|payload|params?|props?|input|output|response|request|dto)$/i;
|
|
298
|
+
const NON_SEMANTIC_CALLEE_NAME_RE = /^(object|array|enum|union|literal|record|tuple|pick|omit|extend|merge|intersection|partial|strictObject|looseObject)$/i;
|
|
292
299
|
|
|
293
300
|
/**
|
|
294
301
|
* Walk an AST subtree and count node types that reveal structural shape.
|
|
@@ -488,6 +495,111 @@ function hasMultilinePairChildren(node) {
|
|
|
488
495
|
return false;
|
|
489
496
|
}
|
|
490
497
|
|
|
498
|
+
function isTopLevelVariableDeclarator(node) {
|
|
499
|
+
if (!node || node.type !== 'variable_declarator') return false;
|
|
500
|
+
const decl = node.parent;
|
|
501
|
+
if (!decl || decl.type !== 'lexical_declaration') return false;
|
|
502
|
+
const container = decl.parent;
|
|
503
|
+
return !!container && (container.type === 'program' || container.type === 'export_statement');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function extractDeclaratorName(node) {
|
|
507
|
+
const nameNode = node?.childForFieldName('name');
|
|
508
|
+
return nameNode?.type === 'identifier' ? nameNode.text : null;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function extractCalleeLeafName(node) {
|
|
512
|
+
if (!node) return null;
|
|
513
|
+
|
|
514
|
+
if (['identifier', 'property_identifier', 'private_property_identifier', 'type_identifier'].includes(node.type)) {
|
|
515
|
+
return node.text;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (node.type === 'member_expression') {
|
|
519
|
+
const propertyNode = node.childForFieldName('property');
|
|
520
|
+
if (propertyNode) return extractCalleeLeafName(propertyNode);
|
|
521
|
+
return extractCalleeLeafName(node.namedChild(node.namedChildCount - 1));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function extractCallLikeCalleeName(node) {
|
|
528
|
+
if (!node || (node.type !== 'call_expression' && node.type !== 'new_expression')) return null;
|
|
529
|
+
|
|
530
|
+
const calleeNode = node.childForFieldName('function')
|
|
531
|
+
|| node.childForFieldName('constructor')
|
|
532
|
+
|| node.namedChild(0);
|
|
533
|
+
return extractCalleeLeafName(calleeNode);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function shouldIndexSemanticObjectMembers(node) {
|
|
537
|
+
if (!isTopLevelVariableDeclarator(node)) return false;
|
|
538
|
+
|
|
539
|
+
const variableName = extractDeclaratorName(node);
|
|
540
|
+
const valueNode = node.childForFieldName('value');
|
|
541
|
+
const calleeName = extractCallLikeCalleeName(valueNode);
|
|
542
|
+
|
|
543
|
+
const hasSemanticVariableName = !!variableName
|
|
544
|
+
&& SEMANTIC_CONTAINER_NAME_RE.test(variableName)
|
|
545
|
+
&& !NON_SEMANTIC_CONTAINER_NAME_RE.test(variableName);
|
|
546
|
+
const hasSemanticCalleeName = !!calleeName && !NON_SEMANTIC_CALLEE_NAME_RE.test(calleeName);
|
|
547
|
+
|
|
548
|
+
if (valueNode?.type === 'object') {
|
|
549
|
+
return hasSemanticVariableName;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (valueNode?.type === 'call_expression' || valueNode?.type === 'new_expression') {
|
|
553
|
+
return hasSemanticVariableName || hasSemanticCalleeName;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function collectDirectPairDefinitions(node, filePath, langName) {
|
|
560
|
+
if (!node || node.type !== 'object') return [];
|
|
561
|
+
|
|
562
|
+
const definitions = [];
|
|
563
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
564
|
+
const child = node.namedChild(i);
|
|
565
|
+
if (child.type !== 'pair') continue;
|
|
566
|
+
|
|
567
|
+
const name = extractPairName(child);
|
|
568
|
+
if (!name) continue;
|
|
569
|
+
if (child.endPosition.row <= child.startPosition.row) continue;
|
|
570
|
+
|
|
571
|
+
definitions.push(createDefinition(name, child, filePath, langName, {
|
|
572
|
+
bodyStartLine: child.startPosition.row + 2,
|
|
573
|
+
kind: 'property',
|
|
574
|
+
}));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return definitions;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function collectIndexDefinitions(rootNode, filePath, langName) {
|
|
581
|
+
if (!INDEX_EXTRA_LANGUAGES.has(langName)) return [];
|
|
582
|
+
|
|
583
|
+
const definitions = [];
|
|
584
|
+
|
|
585
|
+
function visit(node) {
|
|
586
|
+
if (node.type === 'variable_declarator' && shouldIndexSemanticObjectMembers(node)) {
|
|
587
|
+
const bodyNode = findBodyNode(node);
|
|
588
|
+
if (bodyNode && bodyNode.type === 'object') {
|
|
589
|
+
definitions.push(...collectDirectPairDefinitions(bodyNode, filePath, langName));
|
|
590
|
+
}
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
595
|
+
visit(node.namedChild(i));
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
visit(rootNode);
|
|
600
|
+
return definitions;
|
|
601
|
+
}
|
|
602
|
+
|
|
491
603
|
function collectOutlineDefinitions(rootNode, filePath, langName) {
|
|
492
604
|
if (!OUTLINE_EXTRA_LANGUAGES.has(langName)) return [];
|
|
493
605
|
|
|
@@ -618,6 +730,8 @@ function parseFile(filePath, source, { includeOutlineDefinitions = false } = {})
|
|
|
618
730
|
}
|
|
619
731
|
}
|
|
620
732
|
|
|
733
|
+
definitions.push(...collectIndexDefinitions(tree.rootNode, filePath, langName));
|
|
734
|
+
|
|
621
735
|
if (includeOutlineDefinitions) {
|
|
622
736
|
definitions.push(...collectOutlineDefinitions(tree.rootNode, filePath, langName));
|
|
623
737
|
}
|
|
@@ -628,11 +742,12 @@ function parseFile(filePath, source, { includeOutlineDefinitions = false } = {})
|
|
|
628
742
|
const refQuery = new Parser.Query(lang, refQueryStr);
|
|
629
743
|
for (const match of refQuery.matches(tree.rootNode)) {
|
|
630
744
|
for (const capture of match.captures) {
|
|
631
|
-
if (capture.name !== 'ref' && capture.name !== 'jsx_ref') continue;
|
|
745
|
+
if (capture.name !== 'ref' && capture.name !== 'jsx_ref' && capture.name !== 'prop_ref') continue;
|
|
632
746
|
const name = normalizeReferenceCapture(capture);
|
|
633
747
|
if (!name) continue;
|
|
634
748
|
references.push({
|
|
635
749
|
name,
|
|
750
|
+
kind: capture.name === 'prop_ref' ? 'property' : 'symbol',
|
|
636
751
|
file: filePath,
|
|
637
752
|
line: capture.node.startPosition.row + 1,
|
|
638
753
|
});
|