1
0

tree-sitter-helpers.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. /**
  2. * Tree-sitter Shared Helpers
  3. *
  4. * Utility functions used by the core TreeSitterExtractor and per-language extractors.
  5. * Extracted to a leaf module to avoid circular imports between tree-sitter.ts and languages/.
  6. */
  7. import { Node as SyntaxNode } from 'web-tree-sitter';
  8. import * as crypto from 'crypto';
  9. import { NodeKind } from '../types';
  10. /**
  11. * Generate a unique node ID
  12. *
  13. * Uses a 32-character (128-bit) hash to avoid collisions when indexing
  14. * large codebases with many files containing similar symbols.
  15. */
  16. export function generateNodeId(
  17. filePath: string,
  18. kind: NodeKind,
  19. name: string,
  20. line: number
  21. ): string {
  22. const hash = crypto
  23. .createHash('sha256')
  24. .update(`${filePath}:${kind}:${name}:${line}`)
  25. .digest('hex')
  26. .substring(0, 32);
  27. return `${kind}:${hash}`;
  28. }
  29. /**
  30. * Extract text from a syntax node
  31. */
  32. export function getNodeText(node: SyntaxNode, source: string): string {
  33. return source.substring(node.startIndex, node.endIndex);
  34. }
  35. /**
  36. * Find a child node by field name
  37. */
  38. export function getChildByField(node: SyntaxNode, fieldName: string): SyntaxNode | null {
  39. return node.childForFieldName(fieldName);
  40. }
  41. /**
  42. * Node types that *wrap* a declaration so a leading comment is a sibling of the
  43. * wrapper, not of the emitted (inner) declaration node. CodeGraph emits the
  44. * inner node, so before looking for its preceding comment we climb out through
  45. * these. Examples: `export class X {}` (export_statement), `@dec\ndef f()`
  46. * (decorated_definition), `const f = () => {}` (lexical_declaration →
  47. * variable_declarator). Each wraps exactly one declaration, so climbing can't
  48. * mis-attribute a comment to a sibling. (#780)
  49. */
  50. const DOCSTRING_WRAPPER_TYPES = new Set([
  51. 'export_statement', // JS/TS: export class/function/const ...
  52. 'decorated_definition', // Python: @decorator over def/class
  53. 'lexical_declaration', // JS/TS: const/let x = () => {}
  54. 'variable_declaration', // JS/TS: var x = ...
  55. 'variable_declarator', // JS/TS: the `x = () => {}` inside the declaration
  56. 'ambient_declaration', // TS: declare ...
  57. ]);
  58. /**
  59. * Strip comment-syntax markers from a raw comment so the stored docstring is
  60. * just the prose. Covers the marker styles across every supported language:
  61. * C-family line and block comments and their doc variants, Rust/Swift/Kotlin
  62. * triple-slash and bang doc lines, hash lines (Python/Ruby/shell), Lua/Luau
  63. * line and long-bracket comments, and Pascal brace and paren-star comments.
  64. * (#780)
  65. *
  66. * Paired block delimiters are stripped only when the comment OPENS with one,
  67. * so a line comment that merely happens to END with a closing delimiter is
  68. * never truncated. The per-line markers are anchored at line start, so
  69. * they're safe to apply to any comment.
  70. */
  71. function cleanCommentMarkers(comment: string): string {
  72. let c = comment.trim();
  73. if (c.startsWith('/*')) c = c.replace(/^\/\*+!?/, '').replace(/\*+\/$/, '');
  74. else if (c.startsWith('--[')) c = c.replace(/^--\[=*\[/, '').replace(/\]=*\]$/, '');
  75. else if (c.startsWith('(*')) c = c.replace(/^\(\*/, '').replace(/\*\)$/, '');
  76. else if (c.startsWith('{')) c = c.replace(/^\{/, '').replace(/\}$/, '');
  77. return c
  78. .replace(/^\/\/[/!]?\s?/gm, '') // // , and Rust/Swift doc lines /// //!
  79. .replace(/^--\s?/gm, '') // Lua/Luau line comments
  80. .replace(/^#\s?/gm, '') // Python/Ruby/shell line comments
  81. .replace(/^%+\s?/gm, '') // Erlang line comments (% / %% / %%%)
  82. .replace(/^\s*\*\s?/gm, '') // block-comment continuation (* foo)
  83. .trim();
  84. }
  85. /**
  86. * Get the docstring/comment preceding a node
  87. */
  88. export function getPrecedingDocstring(node: SyntaxNode, source: string): string | undefined {
  89. // Climb out of any wrapper(s) so a comment preceding the WHOLE construct
  90. // (export-, decorator-, or const-arrow-wrapped) is reachable as a sibling.
  91. // The emitted node's own `previousNamedSibling` is empty (export/const) or a
  92. // decorator (Python) in those cases, so without this the docstring was
  93. // dropped. (#780)
  94. let anchor = node;
  95. while (anchor.parent && DOCSTRING_WRAPPER_TYPES.has(anchor.parent.type)) {
  96. anchor = anchor.parent;
  97. }
  98. let sibling = anchor.previousNamedSibling;
  99. const comments: string[] = [];
  100. while (sibling) {
  101. if (
  102. sibling.type === 'comment' ||
  103. sibling.type === 'line_comment' ||
  104. sibling.type === 'block_comment' ||
  105. sibling.type === 'documentation_comment'
  106. ) {
  107. comments.unshift(getNodeText(sibling, source));
  108. sibling = sibling.previousNamedSibling;
  109. } else {
  110. break;
  111. }
  112. }
  113. if (comments.length === 0) return undefined;
  114. // Strip each comment's syntax markers (language-aware), then join.
  115. return comments.map(cleanCommentMarkers).join('\n').trim();
  116. }