@mishasinitcyn/betterrank 0.2.6 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mishasinitcyn/betterrank",
3
- "version": "0.2.6",
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
  }
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 findBodyNode(node) {
347
- let body = node.childForFieldName('body');
348
- if (body) return body;
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.childForFieldName('body');
353
- if (body) return body;
354
- for (let j = 0; j < child.namedChildCount; j++) {
355
- body = child.namedChild(j).childForFieldName('body');
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
- // Compute where body content starts (for outline collapsing)
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 {