@kentwynn/kgraph 0.2.22 → 0.2.24

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.
@@ -0,0 +1,292 @@
1
+ import { parseSource } from './tree-sitter-parser.js';
2
+ const TREE_SITTER_GRAMMAR_BY_LANGUAGE = {
3
+ yaml: 'yaml',
4
+ json: 'json',
5
+ html: 'html',
6
+ css: 'css',
7
+ lua: 'lua',
8
+ dart: 'dart',
9
+ elixir: 'elixir',
10
+ scala: 'scala',
11
+ };
12
+ export async function extractBroadSymbols(sourceText, filePath, language) {
13
+ const result = createExtractionResult();
14
+ if (!sourceText.trim()) {
15
+ return result;
16
+ }
17
+ const broadLanguage = language;
18
+ const grammar = TREE_SITTER_GRAMMAR_BY_LANGUAGE[broadLanguage];
19
+ if (grammar) {
20
+ try {
21
+ const tree = await parseSource(sourceText, grammar);
22
+ tree.delete();
23
+ }
24
+ catch (error) {
25
+ result.warnings.push(`tree-sitter ${grammar} parse failed: ${error instanceof Error ? error.message : String(error)}`);
26
+ }
27
+ }
28
+ const lines = sourceText.split(/\r?\n/);
29
+ switch (broadLanguage) {
30
+ case 'swift':
31
+ collectSwift(lines, filePath, result);
32
+ break;
33
+ case 'terraform':
34
+ collectTerraform(lines, filePath, result);
35
+ break;
36
+ case 'graphql':
37
+ collectGraphql(lines, filePath, result);
38
+ break;
39
+ case 'protobuf':
40
+ collectProtobuf(lines, filePath, result);
41
+ break;
42
+ case 'lua':
43
+ collectLua(lines, filePath, result);
44
+ break;
45
+ case 'dart':
46
+ collectDart(lines, filePath, result);
47
+ break;
48
+ case 'elixir':
49
+ collectElixir(lines, filePath, result);
50
+ break;
51
+ case 'scala':
52
+ collectScala(lines, filePath, result);
53
+ break;
54
+ case 'r':
55
+ collectR(lines, filePath, result);
56
+ break;
57
+ case 'yaml':
58
+ case 'json':
59
+ case 'toml':
60
+ case 'dockerfile':
61
+ collectConfig(lines, filePath, result, broadLanguage);
62
+ break;
63
+ case 'markdown':
64
+ collectMarkdown(lines, filePath, result);
65
+ break;
66
+ case 'html':
67
+ case 'xml':
68
+ collectMarkup(lines, filePath, result);
69
+ break;
70
+ case 'css':
71
+ case 'scss':
72
+ case 'sass':
73
+ case 'less':
74
+ collectStylesheet(lines, filePath, result);
75
+ break;
76
+ }
77
+ return result;
78
+ }
79
+ export function supportsBroadExtraction(language) {
80
+ return [
81
+ 'swift',
82
+ 'terraform',
83
+ 'graphql',
84
+ 'protobuf',
85
+ 'lua',
86
+ 'dart',
87
+ 'elixir',
88
+ 'scala',
89
+ 'r',
90
+ 'yaml',
91
+ 'json',
92
+ 'toml',
93
+ 'dockerfile',
94
+ 'markdown',
95
+ 'html',
96
+ 'css',
97
+ 'scss',
98
+ 'sass',
99
+ 'less',
100
+ 'xml',
101
+ ].includes(language);
102
+ }
103
+ function createExtractionResult() {
104
+ return { symbols: [], dependencies: [], relationships: [], warnings: [] };
105
+ }
106
+ function addSymbol(result, filePath, name, kind, line, exported = false, parentName) {
107
+ const id = [filePath, kind, parentName, name, line].filter(Boolean).join('#');
108
+ const symbol = {
109
+ id,
110
+ name,
111
+ kind,
112
+ filePath,
113
+ startLine: line,
114
+ endLine: line,
115
+ exported,
116
+ parentName,
117
+ };
118
+ result.symbols.push(symbol);
119
+ result.relationships.push({
120
+ sourceType: 'file',
121
+ sourceId: filePath,
122
+ targetType: 'symbol',
123
+ targetId: id,
124
+ relationshipType: 'contains',
125
+ confidence: 'high',
126
+ });
127
+ return symbol;
128
+ }
129
+ function addDependency(result, filePath, specifier, kind = specifier.startsWith('.') ? 'local' : 'package') {
130
+ result.dependencies.push({ fromFile: filePath, specifier, kind });
131
+ }
132
+ function collectSwift(lines, filePath, result) {
133
+ forEachLine(lines, (line, lineNumber) => {
134
+ const importMatch = line.match(/^\s*import\s+([A-Za-z_][\w.]*)/);
135
+ if (importMatch?.[1])
136
+ addDependency(result, filePath, importMatch[1]);
137
+ const typeMatch = line.match(/^\s*(?:public|private|internal|open|final|\s)*(class|struct|enum|protocol)\s+([A-Za-z_][\w]*)/);
138
+ if (typeMatch?.[2]) {
139
+ addSymbol(result, filePath, typeMatch[2], typeMatch[1] === 'protocol' ? 'interface' : 'class', lineNumber, true);
140
+ }
141
+ const functionMatch = line.match(/^\s*(?:public|private|internal|open|static|\s)*func\s+([A-Za-z_][\w]*)/);
142
+ if (functionMatch?.[1]) {
143
+ addSymbol(result, filePath, functionMatch[1], 'function', lineNumber, true);
144
+ }
145
+ });
146
+ }
147
+ function collectTerraform(lines, filePath, result) {
148
+ forEachLine(lines, (line, lineNumber) => {
149
+ const blockMatch = line.match(/^\s*(resource|data|module|variable|output|provider)\s+"([^"]+)"(?:\s+"([^"]+)")?/);
150
+ if (blockMatch?.[1] && blockMatch[2]) {
151
+ addSymbol(result, filePath, [blockMatch[1], blockMatch[2], blockMatch[3]].filter(Boolean).join('.'), 'type', lineNumber, true);
152
+ }
153
+ });
154
+ }
155
+ function collectGraphql(lines, filePath, result) {
156
+ forEachLine(lines, (line, lineNumber) => {
157
+ const typeMatch = line.match(/^\s*(type|interface|enum|input|union|scalar)\s+([A-Za-z_][\w]*)/);
158
+ if (typeMatch?.[2]) {
159
+ addSymbol(result, filePath, typeMatch[2], typeMatch[1] === 'interface' ? 'interface' : 'type', lineNumber, true);
160
+ }
161
+ });
162
+ }
163
+ function collectProtobuf(lines, filePath, result) {
164
+ forEachLine(lines, (line, lineNumber) => {
165
+ const importMatch = line.match(/^\s*import\s+"([^"]+)"/);
166
+ if (importMatch?.[1])
167
+ addDependency(result, filePath, importMatch[1], 'local');
168
+ const typeMatch = line.match(/^\s*(message|service|enum)\s+([A-Za-z_][\w]*)/);
169
+ if (typeMatch?.[2]) {
170
+ addSymbol(result, filePath, typeMatch[2], 'type', lineNumber, true);
171
+ }
172
+ });
173
+ }
174
+ function collectLua(lines, filePath, result) {
175
+ forEachLine(lines, (line, lineNumber) => {
176
+ const requireMatch = line.match(/require\s*\(?\s*['"]([^'"]+)['"]/);
177
+ if (requireMatch?.[1])
178
+ addDependency(result, filePath, requireMatch[1]);
179
+ const functionMatch = line.match(/^\s*(?:local\s+)?function\s+([A-Za-z_][\w.:]*)/);
180
+ if (functionMatch?.[1]) {
181
+ addSymbol(result, filePath, functionMatch[1], 'function', lineNumber, true);
182
+ }
183
+ });
184
+ }
185
+ function collectDart(lines, filePath, result) {
186
+ forEachLine(lines, (line, lineNumber) => {
187
+ const importMatch = line.match(/^\s*import\s+['"]([^'"]+)['"]/);
188
+ if (importMatch?.[1])
189
+ addDependency(result, filePath, importMatch[1]);
190
+ const classMatch = line.match(/^\s*(?:abstract\s+)?class\s+([A-Za-z_][\w]*)/);
191
+ if (classMatch?.[1])
192
+ addSymbol(result, filePath, classMatch[1], 'class', lineNumber, true);
193
+ const functionMatch = line.match(/^\s*(?:[A-Za-z_<>,?]+\s+)+([A-Za-z_][\w]*)\s*\([^;]*\)\s*(?:async\s*)?\{/);
194
+ if (functionMatch?.[1] && !['if', 'for', 'while', 'switch'].includes(functionMatch[1])) {
195
+ addSymbol(result, filePath, functionMatch[1], 'function', lineNumber, true);
196
+ }
197
+ });
198
+ }
199
+ function collectElixir(lines, filePath, result) {
200
+ forEachLine(lines, (line, lineNumber) => {
201
+ const moduleMatch = line.match(/^\s*defmodule\s+([A-Z][\w.]+)/);
202
+ if (moduleMatch?.[1])
203
+ addSymbol(result, filePath, moduleMatch[1], 'class', lineNumber, true);
204
+ const functionMatch = line.match(/^\s*defp?\s+([a-z_][\w!?]*)/);
205
+ if (functionMatch?.[1])
206
+ addSymbol(result, filePath, functionMatch[1], 'function', lineNumber, true);
207
+ });
208
+ }
209
+ function collectScala(lines, filePath, result) {
210
+ forEachLine(lines, (line, lineNumber) => {
211
+ const importMatch = line.match(/^\s*import\s+(.+)/);
212
+ if (importMatch?.[1])
213
+ addDependency(result, filePath, importMatch[1].trim());
214
+ const typeMatch = line.match(/^\s*(?:case\s+)?(class|object|trait|enum)\s+([A-Za-z_][\w]*)/);
215
+ if (typeMatch?.[2]) {
216
+ addSymbol(result, filePath, typeMatch[2], typeMatch[1] === 'trait' ? 'interface' : 'class', lineNumber, true);
217
+ }
218
+ const functionMatch = line.match(/^\s*def\s+([A-Za-z_][\w]*)/);
219
+ if (functionMatch?.[1])
220
+ addSymbol(result, filePath, functionMatch[1], 'function', lineNumber, true);
221
+ });
222
+ }
223
+ function collectR(lines, filePath, result) {
224
+ forEachLine(lines, (line, lineNumber) => {
225
+ const libraryMatch = line.match(/^\s*(?:library|require)\s*\(\s*([A-Za-z.][\w.]*)/);
226
+ if (libraryMatch?.[1])
227
+ addDependency(result, filePath, libraryMatch[1]);
228
+ const functionMatch = line.match(/^\s*([A-Za-z.][\w.]*)\s*(?:<-|=)\s*function\s*\(/);
229
+ if (functionMatch?.[1])
230
+ addSymbol(result, filePath, functionMatch[1], 'function', lineNumber, true);
231
+ });
232
+ }
233
+ function collectConfig(lines, filePath, result, language) {
234
+ forEachLine(lines, (line, lineNumber) => {
235
+ if (language === 'dockerfile') {
236
+ const stageMatch = line.match(/^\s*FROM\s+\S+(?:\s+AS\s+([A-Za-z_][\w-]*))?/i);
237
+ if (stageMatch?.[1])
238
+ addSymbol(result, filePath, stageMatch[1], 'type', lineNumber, true);
239
+ return;
240
+ }
241
+ const keyMatch = language === 'json'
242
+ ? line.match(/^\s*"([^"]+)"\s*:/)
243
+ : line.match(/^\s*([A-Za-z_][\w.-]*)\s*[:=]/);
244
+ if (keyMatch?.[1]) {
245
+ addSymbol(result, filePath, keyMatch[1], 'type', lineNumber);
246
+ }
247
+ });
248
+ }
249
+ function collectMarkdown(lines, filePath, result) {
250
+ forEachLine(lines, (line, lineNumber) => {
251
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)/);
252
+ if (headingMatch?.[2]) {
253
+ addSymbol(result, filePath, headingMatch[2].trim(), 'type', lineNumber);
254
+ }
255
+ });
256
+ }
257
+ function collectMarkup(lines, filePath, result) {
258
+ forEachLine(lines, (line, lineNumber) => {
259
+ for (const idMatch of line.matchAll(/\bid=["']([^"']+)["']/g)) {
260
+ if (idMatch[1])
261
+ addSymbol(result, filePath, `#${idMatch[1]}`, 'type', lineNumber);
262
+ }
263
+ for (const classMatch of line.matchAll(/\bclass=["']([^"']+)["']/g)) {
264
+ for (const className of classMatch[1]?.split(/\s+/) ?? []) {
265
+ if (className)
266
+ addSymbol(result, filePath, `.${className}`, 'type', lineNumber);
267
+ }
268
+ }
269
+ });
270
+ }
271
+ function collectStylesheet(lines, filePath, result) {
272
+ forEachLine(lines, (line, lineNumber) => {
273
+ const importMatch = line.match(/^\s*@(import|use|forward)\s+["']([^"']+)["']/);
274
+ if (importMatch?.[2])
275
+ addDependency(result, filePath, importMatch[2]);
276
+ for (const variableMatch of line.matchAll(/(--[A-Za-z_][\w-]*|\$[A-Za-z_][\w-]*)\s*:/g)) {
277
+ if (variableMatch[1])
278
+ addSymbol(result, filePath, variableMatch[1], 'type', lineNumber);
279
+ }
280
+ const mixinMatch = line.match(/^\s*@(mixin|function|keyframes)\s+([A-Za-z_][\w-]*)/);
281
+ if (mixinMatch?.[2]) {
282
+ addSymbol(result, filePath, mixinMatch[2], mixinMatch[1] === 'function' ? 'function' : 'type', lineNumber);
283
+ }
284
+ for (const selectorMatch of line.matchAll(/([.#][A-Za-z_][\w-]*)/g)) {
285
+ if (selectorMatch[1])
286
+ addSymbol(result, filePath, selectorMatch[1], 'type', lineNumber);
287
+ }
288
+ });
289
+ }
290
+ function forEachLine(lines, callback) {
291
+ lines.forEach((line, index) => callback(line, index + 1));
292
+ }
@@ -0,0 +1,23 @@
1
+ import type { CodeSymbol, Dependency, Relationship } from '../types/maps.js';
2
+ import type { SymbolExtractionResult } from './ts-symbol-extractor.js';
3
+ export declare class ExtractionContext {
4
+ private readonly filePath;
5
+ readonly symbols: CodeSymbol[];
6
+ readonly dependencies: Dependency[];
7
+ readonly relationships: Relationship[];
8
+ readonly warnings: string[];
9
+ constructor(filePath: string);
10
+ addSymbol(options: {
11
+ name: string;
12
+ kind: CodeSymbol['kind'];
13
+ startLine: number;
14
+ endLine?: number;
15
+ exported?: boolean;
16
+ parentName?: string;
17
+ }): CodeSymbol;
18
+ addDependency(specifier: string, kind?: Dependency['kind'], confidence?: Relationship['confidence']): void;
19
+ addSymbolContains(parent: CodeSymbol, child: CodeSymbol): void;
20
+ addWarning(message: string): void;
21
+ toResult(): SymbolExtractionResult;
22
+ }
23
+ export declare function emptyExtractionResult(): SymbolExtractionResult;
@@ -0,0 +1,77 @@
1
+ export class ExtractionContext {
2
+ filePath;
3
+ symbols = [];
4
+ dependencies = [];
5
+ relationships = [];
6
+ warnings = [];
7
+ constructor(filePath) {
8
+ this.filePath = filePath;
9
+ }
10
+ addSymbol(options) {
11
+ const id = [
12
+ this.filePath,
13
+ options.kind,
14
+ options.parentName,
15
+ options.name,
16
+ options.startLine,
17
+ options.endLine ?? options.startLine,
18
+ ]
19
+ .filter(Boolean)
20
+ .join('#');
21
+ const symbol = {
22
+ id,
23
+ name: options.name,
24
+ kind: options.kind,
25
+ filePath: this.filePath,
26
+ startLine: options.startLine,
27
+ endLine: options.endLine ?? options.startLine,
28
+ exported: options.exported ?? false,
29
+ parentName: options.parentName,
30
+ };
31
+ this.symbols.push(symbol);
32
+ this.relationships.push({
33
+ sourceType: 'file',
34
+ sourceId: this.filePath,
35
+ targetType: 'symbol',
36
+ targetId: id,
37
+ relationshipType: 'contains',
38
+ confidence: 'high',
39
+ });
40
+ return symbol;
41
+ }
42
+ addDependency(specifier, kind = specifier.startsWith('.') ? 'local' : 'package', confidence = 'high') {
43
+ this.dependencies.push({ fromFile: this.filePath, specifier, kind });
44
+ this.relationships.push({
45
+ sourceType: 'file',
46
+ sourceId: this.filePath,
47
+ targetType: kind === 'local' ? 'file' : 'package',
48
+ targetId: specifier,
49
+ relationshipType: 'import',
50
+ confidence,
51
+ });
52
+ }
53
+ addSymbolContains(parent, child) {
54
+ this.relationships.push({
55
+ sourceType: 'symbol',
56
+ sourceId: parent.id,
57
+ targetType: 'symbol',
58
+ targetId: child.id,
59
+ relationshipType: 'symbol-contains',
60
+ confidence: 'high',
61
+ });
62
+ }
63
+ addWarning(message) {
64
+ this.warnings.push(message);
65
+ }
66
+ toResult() {
67
+ return {
68
+ symbols: this.symbols,
69
+ dependencies: this.dependencies,
70
+ relationships: this.relationships,
71
+ warnings: this.warnings,
72
+ };
73
+ }
74
+ }
75
+ export function emptyExtractionResult() {
76
+ return { symbols: [], dependencies: [], relationships: [], warnings: [] };
77
+ }
@@ -53,6 +53,7 @@ const LANGUAGE_BY_EXTENSION = {
53
53
  '.scss': 'scss',
54
54
  '.sass': 'sass',
55
55
  '.less': 'less',
56
+ '.dockerfile': 'dockerfile',
56
57
  '.vue': 'vue',
57
58
  '.svelte': 'svelte',
58
59
  // Data / Config
@@ -84,6 +85,13 @@ const LANGUAGE_BY_EXTENSION = {
84
85
  '.proto': 'protobuf',
85
86
  '.sql': 'sql',
86
87
  };
88
+ const LANGUAGE_BY_BASENAME = {
89
+ Dockerfile: 'dockerfile',
90
+ Containerfile: 'dockerfile',
91
+ Makefile: 'shell',
92
+ Rakefile: 'ruby',
93
+ Gemfile: 'ruby',
94
+ };
87
95
  export function shouldExclude(repoPath, config) {
88
96
  const normalizedPath = normalizeRepoPath(repoPath);
89
97
  return config.exclude.some((pattern) => matchesExcludePattern(normalizedPath, pattern));
@@ -120,10 +128,15 @@ export async function readGitignorePatterns(rootPath) {
120
128
  }
121
129
  }
122
130
  export function detectLanguage(filePath) {
123
- return LANGUAGE_BY_EXTENSION[path.extname(filePath)] ?? 'unknown';
131
+ const basename = path.basename(filePath);
132
+ return (LANGUAGE_BY_BASENAME[basename] ??
133
+ LANGUAGE_BY_EXTENSION[path.extname(filePath)] ??
134
+ 'unknown');
124
135
  }
125
136
  export function isPreciseLanguage(filePath, config) {
126
- return config.languages.precise.includes(path.extname(filePath));
137
+ const basename = path.basename(filePath);
138
+ return (config.languages.precise.includes(path.extname(filePath)) ||
139
+ Object.hasOwn(LANGUAGE_BY_BASENAME, basename));
127
140
  }
128
141
  function matchesExcludePattern(repoPath, pattern) {
129
142
  const normalized = normalizeRepoPath(pattern).replace(/\/$/, '');
@@ -12,6 +12,11 @@ export declare function getCurrentCommit(rootPath: string): Promise<string | nul
12
12
  * Returns an empty array if git is unavailable or the ref is unknown.
13
13
  */
14
14
  export declare function getChangedFilesSince(rootPath: string, ref: string): Promise<string[]>;
15
+ /**
16
+ * Returns the subset of paths ignored by Git, including nested .gitignore rules.
17
+ * Falls back to an empty set when Git is unavailable or the directory is not a repo.
18
+ */
19
+ export declare function getGitIgnoredFiles(rootPath: string, paths: string[]): Promise<Set<string>>;
15
20
  /**
16
21
  * Returns paths of files with uncommitted changes (staged or unstaged)
17
22
  * relative to HEAD. Returns an empty array if git is unavailable.
@@ -1,4 +1,4 @@
1
- import { execFile } from 'node:child_process';
1
+ import { execFile, spawn } from 'node:child_process';
2
2
  import { access } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { promisify } from 'node:util';
@@ -47,6 +47,38 @@ export async function getChangedFilesSince(rootPath, ref) {
47
47
  return [];
48
48
  }
49
49
  }
50
+ /**
51
+ * Returns the subset of paths ignored by Git, including nested .gitignore rules.
52
+ * Falls back to an empty set when Git is unavailable or the directory is not a repo.
53
+ */
54
+ export async function getGitIgnoredFiles(rootPath, paths) {
55
+ if (paths.length === 0) {
56
+ return new Set();
57
+ }
58
+ return new Promise((resolve) => {
59
+ const child = spawn('git', ['check-ignore', '--stdin'], {
60
+ cwd: rootPath,
61
+ stdio: ['pipe', 'pipe', 'ignore'],
62
+ });
63
+ let stdout = '';
64
+ child.stdout.setEncoding('utf8');
65
+ child.stdout.on('data', (chunk) => {
66
+ stdout += chunk;
67
+ });
68
+ child.on('error', () => resolve(new Set()));
69
+ child.on('close', (code) => {
70
+ if (code !== 0 && code !== 1) {
71
+ resolve(new Set());
72
+ return;
73
+ }
74
+ resolve(new Set(stdout
75
+ .split('\n')
76
+ .map((line) => line.trim())
77
+ .filter(Boolean)));
78
+ });
79
+ child.stdin.end(`${paths.join('\n')}\n`);
80
+ });
81
+ }
50
82
  /**
51
83
  * Returns paths of files with uncommitted changes (staged or unstaged)
52
84
  * relative to HEAD. Returns an empty array if git is unavailable.
@@ -0,0 +1,2 @@
1
+ import type { SymbolExtractionResult } from './ts-symbol-extractor.js';
2
+ export declare function extractPhpSymbols(sourceText: string, filePath: string): Promise<SymbolExtractionResult>;
@@ -0,0 +1,79 @@
1
+ import { emptyExtractionResult, ExtractionContext, } from './extraction-context.js';
2
+ import { parseSource } from './tree-sitter-parser.js';
3
+ export async function extractPhpSymbols(sourceText, filePath) {
4
+ if (!sourceText.trim()) {
5
+ return emptyExtractionResult();
6
+ }
7
+ const tree = await parseSource(sourceText, 'php');
8
+ const context = new ExtractionContext(filePath);
9
+ function addNamedSymbol(node, kind, parentName) {
10
+ const nameNode = findNameNode(node);
11
+ if (!nameNode)
12
+ return undefined;
13
+ return context.addSymbol({
14
+ name: nameNode.text,
15
+ kind,
16
+ startLine: node.startPosition.row + 1,
17
+ endLine: node.endPosition.row + 1,
18
+ exported: true,
19
+ parentName,
20
+ });
21
+ }
22
+ function walk(node, parentClassName) {
23
+ switch (node.type) {
24
+ case 'namespace_definition':
25
+ case 'namespace_name': {
26
+ if (node.type === 'namespace_name' && !parentClassName) {
27
+ context.addSymbol({
28
+ name: node.text,
29
+ kind: 'type',
30
+ startLine: node.startPosition.row + 1,
31
+ endLine: node.endPosition.row + 1,
32
+ exported: true,
33
+ });
34
+ return;
35
+ }
36
+ break;
37
+ }
38
+ case 'namespace_use_declaration': {
39
+ for (const nameNode of node.descendantsOfType('qualified_name')) {
40
+ context.addDependency(nameNode.text, 'package');
41
+ }
42
+ return;
43
+ }
44
+ case 'class_declaration':
45
+ case 'interface_declaration':
46
+ case 'trait_declaration':
47
+ case 'enum_declaration': {
48
+ const classSymbol = addNamedSymbol(node, node.type === 'interface_declaration' ? 'interface' : 'class', parentClassName);
49
+ const className = classSymbol?.name ?? parentClassName;
50
+ for (const child of node.namedChildren) {
51
+ walk(child, className);
52
+ }
53
+ return;
54
+ }
55
+ case 'function_definition':
56
+ case 'method_declaration': {
57
+ const symbol = addNamedSymbol(node, parentClassName ? 'method' : 'function', parentClassName);
58
+ if (symbol && parentClassName) {
59
+ const parent = context.symbols.find((candidate) => candidate.name === parentClassName && candidate.kind === 'class');
60
+ if (parent)
61
+ context.addSymbolContains(parent, symbol);
62
+ }
63
+ return;
64
+ }
65
+ }
66
+ for (const child of node.namedChildren) {
67
+ walk(child, parentClassName);
68
+ }
69
+ }
70
+ walk(tree.rootNode);
71
+ tree.delete();
72
+ return context.toResult();
73
+ }
74
+ function findNameNode(node) {
75
+ return (node.childForFieldName('name') ??
76
+ node.namedChildren.find((child) => child.type === 'name') ??
77
+ node.namedChildren.find((child) => child.type === 'variable_name') ??
78
+ node.namedChildren.find((child) => child.type === 'identifier'));
79
+ }