@mishasinitcyn/betterrank 0.2.9 → 0.2.10
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/cli.js +22 -6
- package/src/graph.js +42 -1
- package/src/index.js +60 -35
- package/src/parser.js +50 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mishasinitcyn/betterrank",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
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/cli.js
CHANGED
|
@@ -51,6 +51,10 @@ function/class signatures with bodies replaced by "... (N lines)".
|
|
|
51
51
|
With symbol names (comma-separated): shows the full source of those
|
|
52
52
|
specific functions/classes with line numbers.
|
|
53
53
|
|
|
54
|
+
Also works well for indexed JS/TS object members like router procedures:
|
|
55
|
+
use \`outline file.ts getTeamStatistics\` instead of expanding a whole giant
|
|
56
|
+
\`createTRPCRouter({ ... })\` blob.
|
|
57
|
+
|
|
54
58
|
Options:
|
|
55
59
|
--root <path> Resolve file path relative to this directory
|
|
56
60
|
--annotate Show caller counts next to each function (requires --root)
|
|
@@ -67,7 +71,8 @@ Examples:
|
|
|
67
71
|
Aider-style repo map: the most structurally important definitions ranked by PageRank.
|
|
68
72
|
|
|
69
73
|
Options:
|
|
70
|
-
--focus <files> Comma-separated files to bias ranking toward
|
|
74
|
+
--focus <files> Comma-separated files to strongly bias ranking toward the
|
|
75
|
+
local import neighborhood of those files
|
|
71
76
|
--count Return total symbol count only
|
|
72
77
|
--offset N Skip first N symbols
|
|
73
78
|
--limit N Max symbols to return (default: ${DEFAULT_LIMIT})
|
|
@@ -83,7 +88,7 @@ Substring search on symbol names + full signatures (param names, types, defaults
|
|
|
83
88
|
Results ranked by PageRank (most structurally important first).
|
|
84
89
|
|
|
85
90
|
Options:
|
|
86
|
-
--kind <type> Filter: function, class, type, variable, namespace, import
|
|
91
|
+
--kind <type> Filter: function, class, type, variable, property, namespace, import
|
|
87
92
|
--count Return match count only
|
|
88
93
|
--offset N Skip first N results
|
|
89
94
|
--limit N Max results (default: ${DEFAULT_LIMIT})
|
|
@@ -92,6 +97,7 @@ Tips:
|
|
|
92
97
|
Use short substrings (3-5 chars) — PageRank ranking handles noise.
|
|
93
98
|
"imp" finds encrypt_imp_payload, increment_impression, etc.
|
|
94
99
|
Searches match against both symbol names AND full signatures (param names, types).
|
|
100
|
+
JS/TS router procedures and other indexed object members appear as [property].
|
|
95
101
|
|
|
96
102
|
Examples:
|
|
97
103
|
betterrank search resolve --root ./backend
|
|
@@ -115,19 +121,23 @@ Results ranked by PageRank (most structurally important first).
|
|
|
115
121
|
|
|
116
122
|
Options:
|
|
117
123
|
--file <path> Filter to a specific file (relative to --root)
|
|
118
|
-
--kind <type> Filter: function, class, type, variable, namespace, import
|
|
124
|
+
--kind <type> Filter: function, class, type, variable, property, namespace, import
|
|
119
125
|
--count Return count only
|
|
120
126
|
--offset N Skip first N results
|
|
121
127
|
--limit N Max results (default: ${DEFAULT_LIMIT})
|
|
122
128
|
|
|
123
129
|
Examples:
|
|
124
130
|
betterrank symbols --file src/auth/handlers.ts --root ./backend
|
|
125
|
-
betterrank symbols --kind class --root . --limit 20
|
|
131
|
+
betterrank symbols --kind class --root . --limit 20
|
|
132
|
+
betterrank symbols --file src/server/api/routers/project.ts --kind property --root .`,
|
|
126
133
|
|
|
127
134
|
callers: `betterrank callers <symbol> [--file path] [--context [N]] [--root <path>]
|
|
128
135
|
|
|
129
136
|
Find all files that reference a symbol. Ranked by file-level PageRank.
|
|
130
137
|
|
|
138
|
+
For React/TS apps this includes JSX render sites like <Providers /> and
|
|
139
|
+
property access sites like api.project.getTeamStatistics.useQuery().
|
|
140
|
+
|
|
131
141
|
Options:
|
|
132
142
|
--file <path> Disambiguate when multiple symbols share a name
|
|
133
143
|
--context [N] Show N lines of context around each call site (default: 2)
|
|
@@ -142,7 +152,7 @@ Examples:
|
|
|
142
152
|
|
|
143
153
|
context: `betterrank context <symbol> [--file path] [--root <path>]
|
|
144
154
|
|
|
145
|
-
Everything you need to understand a function in one shot.
|
|
155
|
+
Everything you need to understand a function or indexed property in one shot.
|
|
146
156
|
|
|
147
157
|
Shows: the function's source, signatures of all functions/types it references,
|
|
148
158
|
expanded type definitions from the signature, and a callers summary.
|
|
@@ -155,7 +165,8 @@ Options:
|
|
|
155
165
|
|
|
156
166
|
Examples:
|
|
157
167
|
betterrank context calculate_bid --root .
|
|
158
|
-
betterrank context Router --file src/llm.py --root
|
|
168
|
+
betterrank context Router --file src/llm.py --root .
|
|
169
|
+
betterrank context getTeamStatistics --file src/server/api/routers/project.ts --root .`,
|
|
159
170
|
|
|
160
171
|
history: `betterrank history <symbol> [--file path] [--patch] [--limit N] [--root <path>]
|
|
161
172
|
|
|
@@ -212,6 +223,8 @@ Examples:
|
|
|
212
223
|
|
|
213
224
|
What this file imports / depends on. Ranked by PageRank.
|
|
214
225
|
|
|
226
|
+
Resolves explicit import paths including TS/JS aliases and Python relative imports.
|
|
227
|
+
|
|
215
228
|
Options:
|
|
216
229
|
--count Return count only
|
|
217
230
|
--offset N Skip first N results
|
|
@@ -224,6 +237,9 @@ Examples:
|
|
|
224
237
|
|
|
225
238
|
What files import this file. Ranked by PageRank.
|
|
226
239
|
|
|
240
|
+
Resolves explicit import paths including side-effect imports, TS/JS aliases,
|
|
241
|
+
and Python relative imports.
|
|
242
|
+
|
|
227
243
|
Options:
|
|
228
244
|
--count Return count only
|
|
229
245
|
--offset N Skip first N results
|
package/src/graph.js
CHANGED
|
@@ -84,6 +84,37 @@ function buildFileLookup(graph) {
|
|
|
84
84
|
return fileLookup;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
function isPythonFile(filePath) {
|
|
88
|
+
return normalizeFilePath(filePath).endsWith('.py');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function resolvePythonImportBase(sourceFile, specifier) {
|
|
92
|
+
if (!isPythonFile(sourceFile)) return null;
|
|
93
|
+
if (!specifier) return null;
|
|
94
|
+
|
|
95
|
+
if (specifier.startsWith('.')) {
|
|
96
|
+
const match = specifier.match(/^(\.+)(.*)$/);
|
|
97
|
+
if (!match) return null;
|
|
98
|
+
|
|
99
|
+
const dots = match[1].length;
|
|
100
|
+
let baseDir = posix.dirname(sourceFile);
|
|
101
|
+
for (let i = 1; i < dots; i++) {
|
|
102
|
+
baseDir = posix.dirname(baseDir);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const remainder = match[2].replace(/^\./, '');
|
|
106
|
+
if (!remainder) return baseDir;
|
|
107
|
+
|
|
108
|
+
return posix.normalize(posix.join(baseDir, remainder.replace(/\./g, '/')));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(specifier)) {
|
|
112
|
+
return specifier.replace(/\./g, '/');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
87
118
|
function resolveImportBase(sourceFile, specifier) {
|
|
88
119
|
const normalizedSource = normalizeFilePath(sourceFile);
|
|
89
120
|
const normalizedSpecifier = normalizeFilePath(specifier);
|
|
@@ -105,6 +136,9 @@ function resolveImportBase(sourceFile, specifier) {
|
|
|
105
136
|
return posix.normalize(normalizedSpecifier);
|
|
106
137
|
}
|
|
107
138
|
|
|
139
|
+
const pythonBase = resolvePythonImportBase(normalizedSource, normalizedSpecifier);
|
|
140
|
+
if (pythonBase) return pythonBase;
|
|
141
|
+
|
|
108
142
|
return null;
|
|
109
143
|
}
|
|
110
144
|
|
|
@@ -121,8 +155,11 @@ function resolveImportTargetFile(sourceFile, specifier, fileLookup) {
|
|
|
121
155
|
for (const ext of IMPORT_RESOLVE_EXTENSIONS) {
|
|
122
156
|
candidates.push(`${importBase}${ext}`);
|
|
123
157
|
}
|
|
158
|
+
const packageEntryNames = isPythonFile(sourceFile) ? ['__init__'] : ['index'];
|
|
124
159
|
for (const ext of IMPORT_RESOLVE_EXTENSIONS) {
|
|
125
|
-
|
|
160
|
+
for (const entryName of packageEntryNames) {
|
|
161
|
+
candidates.push(posix.join(importBase, `${entryName}${ext}`));
|
|
162
|
+
}
|
|
126
163
|
}
|
|
127
164
|
}
|
|
128
165
|
|
|
@@ -236,6 +273,10 @@ function updateGraphFiles(graph, removedFiles, newSymbols) {
|
|
|
236
273
|
continue;
|
|
237
274
|
}
|
|
238
275
|
|
|
276
|
+
if (!graph.hasNode(filePath)) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
|
|
239
280
|
const outgoingEdges = [];
|
|
240
281
|
graph.forEachOutEdge(filePath, edge => {
|
|
241
282
|
outgoingEdges.push(edge);
|
package/src/index.js
CHANGED
|
@@ -232,6 +232,26 @@ function paginate(arr, { offset = 0, limit } = {}) {
|
|
|
232
232
|
return { items, total };
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
const CONTEXT_NOISE_NAMES = new Set([
|
|
236
|
+
'get', 'set', 'put', 'post', 'delete', 'head', 'patch',
|
|
237
|
+
'start', 'stop', 'run', 'main', 'init', 'setup', 'close',
|
|
238
|
+
'dict', 'list', 'str', 'int', 'bool', 'float', 'type',
|
|
239
|
+
'key', 'value', 'name', 'data', 'config', 'result', 'error',
|
|
240
|
+
'test', 'self', 'cls', 'app', 'log', 'logger',
|
|
241
|
+
'enabled', 'default', 'constructor', 'length', 'size',
|
|
242
|
+
'fetch', 'send', 'table', 'one', 'append', 'write', 'read',
|
|
243
|
+
'update', 'create', 'find', 'add', 'remove', 'index', 'map',
|
|
244
|
+
'filter', 'sort', 'join', 'split', 'trim', 'replace',
|
|
245
|
+
'push', 'pop', 'shift', 'reduce', 'keys', 'values', 'items',
|
|
246
|
+
'search', 'match', 'query', 'count', 'call', 'apply', 'bind',
|
|
247
|
+
]);
|
|
248
|
+
|
|
249
|
+
const CONTEXT_KIND_ORDER = { function: 0, class: 1, type: 2, property: 3, variable: 4 };
|
|
250
|
+
|
|
251
|
+
function escapeRegExp(value) {
|
|
252
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
253
|
+
}
|
|
254
|
+
|
|
235
255
|
class CodeIndex {
|
|
236
256
|
constructor(projectRoot, opts = {}) {
|
|
237
257
|
this.projectRoot = projectRoot;
|
|
@@ -1117,55 +1137,60 @@ class CodeIndex {
|
|
|
1117
1137
|
const bodyLines = lines.slice(target.lineStart - 1, target.lineEnd);
|
|
1118
1138
|
const bodyText = bodyLines.join('\n');
|
|
1119
1139
|
|
|
1120
|
-
// Build a set of all symbol names in the graph
|
|
1121
|
-
const allSymbols = new Map(); // name -> [{ file, kind, signature, lineStart }]
|
|
1140
|
+
// Build a set of all symbol names in the graph.
|
|
1141
|
+
const allSymbols = new Map(); // name -> [{ file, kind, signature, lineStart, score }]
|
|
1122
1142
|
graph.forEachNode((node, attrs) => {
|
|
1123
1143
|
if (attrs.type !== 'symbol') return;
|
|
1124
1144
|
if (attrs.file === target.file && attrs.name === target.name) return; // skip self
|
|
1125
1145
|
if (!allSymbols.has(attrs.name)) allSymbols.set(attrs.name, []);
|
|
1126
1146
|
allSymbols.get(attrs.name).push({
|
|
1147
|
+
key: node,
|
|
1127
1148
|
file: attrs.file,
|
|
1128
1149
|
kind: attrs.kind,
|
|
1129
1150
|
signature: attrs.signature,
|
|
1130
1151
|
lineStart: attrs.lineStart,
|
|
1131
1152
|
lineEnd: attrs.lineEnd,
|
|
1153
|
+
score: scoreMap.get(node) || 0,
|
|
1132
1154
|
});
|
|
1133
1155
|
});
|
|
1134
1156
|
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
for
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
const best = sameFile || defs[0];
|
|
1163
|
-
usedSymbols.push({ name, ...best });
|
|
1157
|
+
for (const defs of allSymbols.values()) {
|
|
1158
|
+
defs.sort((a, b) =>
|
|
1159
|
+
Number(b.file === target.file) - Number(a.file === target.file)
|
|
1160
|
+
|| (b.score || 0) - (a.score || 0)
|
|
1161
|
+
|| a.file.localeCompare(b.file)
|
|
1162
|
+
|| a.lineStart - b.lineStart,
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// Find symbols referenced in the body, preferring parser-scoped local refs.
|
|
1167
|
+
const referenceNames = new Set();
|
|
1168
|
+
for (const name of target.localRefs || []) {
|
|
1169
|
+
if (!name || name.length < 3) continue;
|
|
1170
|
+
if (CONTEXT_NOISE_NAMES.has(name)) continue;
|
|
1171
|
+
if (!allSymbols.has(name)) continue;
|
|
1172
|
+
referenceNames.add(name);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// Fallback for older caches or definitions without scoped refs.
|
|
1176
|
+
if (referenceNames.size === 0) {
|
|
1177
|
+
for (const name of allSymbols.keys()) {
|
|
1178
|
+
if (name.length < 3) continue;
|
|
1179
|
+
if (CONTEXT_NOISE_NAMES.has(name)) continue;
|
|
1180
|
+
const pattern = new RegExp(`(?<![a-zA-Z0-9_])${escapeRegExp(name)}(?![a-zA-Z0-9_])`);
|
|
1181
|
+
if (pattern.test(bodyText)) {
|
|
1182
|
+
referenceNames.add(name);
|
|
1183
|
+
}
|
|
1164
1184
|
}
|
|
1165
1185
|
}
|
|
1166
|
-
|
|
1167
|
-
const
|
|
1168
|
-
|
|
1186
|
+
|
|
1187
|
+
const usedSymbols = [];
|
|
1188
|
+
for (const name of referenceNames) {
|
|
1189
|
+
const defs = allSymbols.get(name);
|
|
1190
|
+
if (!defs || defs.length === 0) continue;
|
|
1191
|
+
usedSymbols.push({ name, ...defs[0] });
|
|
1192
|
+
}
|
|
1193
|
+
usedSymbols.sort((a, b) => (CONTEXT_KIND_ORDER[a.kind] ?? 9) - (CONTEXT_KIND_ORDER[b.kind] ?? 9) || a.name.localeCompare(b.name));
|
|
1169
1194
|
|
|
1170
1195
|
// Resolve type annotations in the signature
|
|
1171
1196
|
// Extract type-like tokens from the signature (capitalized words, common patterns)
|
|
@@ -1181,7 +1206,7 @@ class CodeIndex {
|
|
|
1181
1206
|
const typeDefs = allSymbols.get(typeName);
|
|
1182
1207
|
if (!typeDefs) continue;
|
|
1183
1208
|
// Find the type definition and get its fields (expand its body)
|
|
1184
|
-
const best = typeDefs
|
|
1209
|
+
const best = typeDefs[0];
|
|
1185
1210
|
if (best.kind === 'class' || best.kind === 'type') {
|
|
1186
1211
|
let fields = null;
|
|
1187
1212
|
try {
|
package/src/parser.js
CHANGED
|
@@ -693,6 +693,52 @@ function normalizeImportPathCapture(capture) {
|
|
|
693
693
|
return text.replace(/^['"`]|['"`]$/g, '');
|
|
694
694
|
}
|
|
695
695
|
|
|
696
|
+
function createImportEntry(path, filePath, node) {
|
|
697
|
+
if (!path || !filePath || !node) return null;
|
|
698
|
+
return {
|
|
699
|
+
path,
|
|
700
|
+
file: filePath,
|
|
701
|
+
line: node.startPosition.row + 1,
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function collectPythonImports(rootNode, filePath) {
|
|
706
|
+
const imports = [];
|
|
707
|
+
|
|
708
|
+
function push(path, node) {
|
|
709
|
+
const entry = createImportEntry(path, filePath, node);
|
|
710
|
+
if (entry) imports.push(entry);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function visit(node) {
|
|
714
|
+
if (node.type === 'import_statement') {
|
|
715
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
716
|
+
const child = node.namedChild(i);
|
|
717
|
+
if (child.type === 'dotted_name') {
|
|
718
|
+
push(child.text, child);
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
if (child.type === 'aliased_import') {
|
|
722
|
+
const moduleNode = child.namedChild(0);
|
|
723
|
+
if (moduleNode?.type === 'dotted_name') {
|
|
724
|
+
push(moduleNode.text, moduleNode);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
} else if (node.type === 'import_from_statement') {
|
|
729
|
+
const moduleNode = node.childForFieldName('module_name');
|
|
730
|
+
if (moduleNode) push(moduleNode.text, moduleNode);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
734
|
+
visit(node.namedChild(i));
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
visit(rootNode);
|
|
739
|
+
return imports;
|
|
740
|
+
}
|
|
741
|
+
|
|
696
742
|
/**
|
|
697
743
|
* Parse a single source file and extract definitions, references, and imports.
|
|
698
744
|
* Returns null if the language is unsupported.
|
|
@@ -781,6 +827,10 @@ function parseFile(filePath, source, { includeOutlineDefinitions = false } = {})
|
|
|
781
827
|
}
|
|
782
828
|
}
|
|
783
829
|
|
|
830
|
+
if (langName === 'python') {
|
|
831
|
+
imports.push(...collectPythonImports(tree.rootNode, filePath));
|
|
832
|
+
}
|
|
833
|
+
|
|
784
834
|
// Associate each reference with its enclosing definition (by line range).
|
|
785
835
|
// This gives us per-function reference sets for similarity analysis.
|
|
786
836
|
// Sort definitions by lineStart for binary search.
|