@optave/codegraph 2.5.1 → 3.0.0

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.
@@ -1,4 +1,4 @@
1
- import { nodeEndLine } from './helpers.js';
1
+ import { findChild, nodeEndLine } from './helpers.js';
2
2
 
3
3
  /**
4
4
  * Extract symbols from Java files.
@@ -31,11 +31,13 @@ export function extractJavaSymbols(tree, _filePath) {
31
31
  case 'class_declaration': {
32
32
  const nameNode = node.childForFieldName('name');
33
33
  if (nameNode) {
34
+ const classChildren = extractClassFields(node);
34
35
  definitions.push({
35
36
  name: nameNode.text,
36
37
  kind: 'class',
37
38
  line: node.startPosition.row + 1,
38
39
  endLine: nodeEndLine(node),
40
+ children: classChildren.length > 0 ? classChildren : undefined,
39
41
  });
40
42
 
41
43
  const superclass = node.childForFieldName('superclass');
@@ -139,11 +141,13 @@ export function extractJavaSymbols(tree, _filePath) {
139
141
  case 'enum_declaration': {
140
142
  const nameNode = node.childForFieldName('name');
141
143
  if (nameNode) {
144
+ const enumChildren = extractEnumConstants(node);
142
145
  definitions.push({
143
146
  name: nameNode.text,
144
147
  kind: 'enum',
145
148
  line: node.startPosition.row + 1,
146
149
  endLine: nodeEndLine(node),
150
+ children: enumChildren.length > 0 ? enumChildren : undefined,
147
151
  });
148
152
  }
149
153
  break;
@@ -154,11 +158,13 @@ export function extractJavaSymbols(tree, _filePath) {
154
158
  if (nameNode) {
155
159
  const parentClass = findJavaParentClass(node);
156
160
  const fullName = parentClass ? `${parentClass}.${nameNode.text}` : nameNode.text;
161
+ const params = extractJavaParameters(node.childForFieldName('parameters'));
157
162
  definitions.push({
158
163
  name: fullName,
159
164
  kind: 'method',
160
165
  line: node.startPosition.row + 1,
161
166
  endLine: nodeEndLine(node),
167
+ children: params.length > 0 ? params : undefined,
162
168
  });
163
169
  }
164
170
  break;
@@ -169,11 +175,13 @@ export function extractJavaSymbols(tree, _filePath) {
169
175
  if (nameNode) {
170
176
  const parentClass = findJavaParentClass(node);
171
177
  const fullName = parentClass ? `${parentClass}.${nameNode.text}` : nameNode.text;
178
+ const params = extractJavaParameters(node.childForFieldName('parameters'));
172
179
  definitions.push({
173
180
  name: fullName,
174
181
  kind: 'method',
175
182
  line: node.startPosition.row + 1,
176
183
  endLine: nodeEndLine(node),
184
+ children: params.length > 0 ? params : undefined,
177
185
  });
178
186
  }
179
187
  break;
@@ -228,3 +236,55 @@ export function extractJavaSymbols(tree, _filePath) {
228
236
  walkJavaNode(tree.rootNode);
229
237
  return { definitions, calls, imports, classes, exports };
230
238
  }
239
+
240
+ // ── Child extraction helpers ────────────────────────────────────────────────
241
+
242
+ function extractJavaParameters(paramListNode) {
243
+ const params = [];
244
+ if (!paramListNode) return params;
245
+ for (let i = 0; i < paramListNode.childCount; i++) {
246
+ const param = paramListNode.child(i);
247
+ if (!param) continue;
248
+ if (param.type === 'formal_parameter' || param.type === 'spread_parameter') {
249
+ const nameNode = param.childForFieldName('name');
250
+ if (nameNode) {
251
+ params.push({ name: nameNode.text, kind: 'parameter', line: param.startPosition.row + 1 });
252
+ }
253
+ }
254
+ }
255
+ return params;
256
+ }
257
+
258
+ function extractClassFields(classNode) {
259
+ const fields = [];
260
+ const body = classNode.childForFieldName('body') || findChild(classNode, 'class_body');
261
+ if (!body) return fields;
262
+ for (let i = 0; i < body.childCount; i++) {
263
+ const member = body.child(i);
264
+ if (!member || member.type !== 'field_declaration') continue;
265
+ for (let j = 0; j < member.childCount; j++) {
266
+ const child = member.child(j);
267
+ if (!child || child.type !== 'variable_declarator') continue;
268
+ const nameNode = child.childForFieldName('name');
269
+ if (nameNode) {
270
+ fields.push({ name: nameNode.text, kind: 'property', line: member.startPosition.row + 1 });
271
+ }
272
+ }
273
+ }
274
+ return fields;
275
+ }
276
+
277
+ function extractEnumConstants(enumNode) {
278
+ const constants = [];
279
+ const body = enumNode.childForFieldName('body') || findChild(enumNode, 'enum_body');
280
+ if (!body) return constants;
281
+ for (let i = 0; i < body.childCount; i++) {
282
+ const member = body.child(i);
283
+ if (!member || member.type !== 'enum_constant') continue;
284
+ const nameNode = member.childForFieldName('name');
285
+ if (nameNode) {
286
+ constants.push({ name: nameNode.text, kind: 'constant', line: member.startPosition.row + 1 });
287
+ }
288
+ }
289
+ return constants;
290
+ }
@@ -28,31 +28,37 @@ function extractSymbolsQuery(tree, query) {
28
28
 
29
29
  if (c.fn_node) {
30
30
  // function_declaration
31
+ const fnChildren = extractParameters(c.fn_node);
31
32
  definitions.push({
32
33
  name: c.fn_name.text,
33
34
  kind: 'function',
34
35
  line: c.fn_node.startPosition.row + 1,
35
36
  endLine: nodeEndLine(c.fn_node),
37
+ children: fnChildren.length > 0 ? fnChildren : undefined,
36
38
  });
37
39
  } else if (c.varfn_name) {
38
40
  // variable_declarator with arrow_function / function_expression
39
41
  const declNode = c.varfn_name.parent?.parent;
40
42
  const line = declNode ? declNode.startPosition.row + 1 : c.varfn_name.startPosition.row + 1;
43
+ const varFnChildren = extractParameters(c.varfn_value);
41
44
  definitions.push({
42
45
  name: c.varfn_name.text,
43
46
  kind: 'function',
44
47
  line,
45
48
  endLine: nodeEndLine(c.varfn_value),
49
+ children: varFnChildren.length > 0 ? varFnChildren : undefined,
46
50
  });
47
51
  } else if (c.cls_node) {
48
52
  // class_declaration
49
53
  const className = c.cls_name.text;
50
54
  const startLine = c.cls_node.startPosition.row + 1;
55
+ const clsChildren = extractClassProperties(c.cls_node);
51
56
  definitions.push({
52
57
  name: className,
53
58
  kind: 'class',
54
59
  line: startLine,
55
60
  endLine: nodeEndLine(c.cls_node),
61
+ children: clsChildren.length > 0 ? clsChildren : undefined,
56
62
  });
57
63
  const heritage =
58
64
  c.cls_node.childForFieldName('heritage') || findChild(c.cls_node, 'class_heritage');
@@ -69,11 +75,13 @@ function extractSymbolsQuery(tree, query) {
69
75
  const methName = c.meth_name.text;
70
76
  const parentClass = findParentClass(c.meth_node);
71
77
  const fullName = parentClass ? `${parentClass}.${methName}` : methName;
78
+ const methChildren = extractParameters(c.meth_node);
72
79
  definitions.push({
73
80
  name: fullName,
74
81
  kind: 'method',
75
82
  line: c.meth_node.startPosition.row + 1,
76
83
  endLine: nodeEndLine(c.meth_node),
84
+ children: methChildren.length > 0 ? methChildren : undefined,
77
85
  });
78
86
  } else if (c.iface_node) {
79
87
  // interface_declaration (TS/TSX only)
@@ -231,11 +239,13 @@ function extractSymbolsWalk(tree) {
231
239
  case 'function_declaration': {
232
240
  const nameNode = node.childForFieldName('name');
233
241
  if (nameNode) {
242
+ const fnChildren = extractParameters(node);
234
243
  definitions.push({
235
244
  name: nameNode.text,
236
245
  kind: 'function',
237
246
  line: node.startPosition.row + 1,
238
247
  endLine: nodeEndLine(node),
248
+ children: fnChildren.length > 0 ? fnChildren : undefined,
239
249
  });
240
250
  }
241
251
  break;
@@ -246,11 +256,13 @@ function extractSymbolsWalk(tree) {
246
256
  if (nameNode) {
247
257
  const className = nameNode.text;
248
258
  const startLine = node.startPosition.row + 1;
259
+ const clsChildren = extractClassProperties(node);
249
260
  definitions.push({
250
261
  name: className,
251
262
  kind: 'class',
252
263
  line: startLine,
253
264
  endLine: nodeEndLine(node),
265
+ children: clsChildren.length > 0 ? clsChildren : undefined,
254
266
  });
255
267
  const heritage = node.childForFieldName('heritage') || findChild(node, 'class_heritage');
256
268
  if (heritage) {
@@ -272,11 +284,13 @@ function extractSymbolsWalk(tree) {
272
284
  if (nameNode) {
273
285
  const parentClass = findParentClass(node);
274
286
  const fullName = parentClass ? `${parentClass}.${nameNode.text}` : nameNode.text;
287
+ const methChildren = extractParameters(node);
275
288
  definitions.push({
276
289
  name: fullName,
277
290
  kind: 'method',
278
291
  line: node.startPosition.row + 1,
279
292
  endLine: nodeEndLine(node),
293
+ children: methChildren.length > 0 ? methChildren : undefined,
280
294
  });
281
295
  }
282
296
  break;
@@ -317,6 +331,7 @@ function extractSymbolsWalk(tree) {
317
331
 
318
332
  case 'lexical_declaration':
319
333
  case 'variable_declaration': {
334
+ const isConst = node.text.startsWith('const ');
320
335
  for (let i = 0; i < node.childCount; i++) {
321
336
  const declarator = node.child(i);
322
337
  if (declarator && declarator.type === 'variable_declarator') {
@@ -329,15 +344,59 @@ function extractSymbolsWalk(tree) {
329
344
  valType === 'function_expression' ||
330
345
  valType === 'function'
331
346
  ) {
347
+ const varFnChildren = extractParameters(valueN);
332
348
  definitions.push({
333
349
  name: nameN.text,
334
350
  kind: 'function',
335
351
  line: node.startPosition.row + 1,
336
352
  endLine: nodeEndLine(valueN),
353
+ children: varFnChildren.length > 0 ? varFnChildren : undefined,
337
354
  });
355
+ } else if (isConst && nameN.type === 'identifier' && isConstantValue(valueN)) {
356
+ definitions.push({
357
+ name: nameN.text,
358
+ kind: 'constant',
359
+ line: node.startPosition.row + 1,
360
+ endLine: nodeEndLine(node),
361
+ });
362
+ }
363
+ } else if (isConst && nameN && nameN.type === 'identifier' && !valueN) {
364
+ // const with no value (shouldn't happen but be safe)
365
+ }
366
+ }
367
+ }
368
+ break;
369
+ }
370
+
371
+ case 'enum_declaration': {
372
+ // TypeScript enum
373
+ const nameNode = node.childForFieldName('name');
374
+ if (nameNode) {
375
+ const enumChildren = [];
376
+ const body = node.childForFieldName('body') || findChild(node, 'enum_body');
377
+ if (body) {
378
+ for (let i = 0; i < body.childCount; i++) {
379
+ const member = body.child(i);
380
+ if (!member) continue;
381
+ if (member.type === 'enum_assignment' || member.type === 'property_identifier') {
382
+ const mName = member.childForFieldName('name') || member.child(0);
383
+ if (mName) {
384
+ enumChildren.push({
385
+ name: mName.text,
386
+ kind: 'constant',
387
+ line: member.startPosition.row + 1,
388
+ });
389
+ }
338
390
  }
339
391
  }
340
392
  }
393
+ definitions.push({
394
+ name: nameNode.text,
395
+ kind: 'enum',
396
+ line: node.startPosition.row + 1,
397
+ endLine: nodeEndLine(node),
398
+ children: enumChildren.length > 0 ? enumChildren : undefined,
399
+ });
341
400
  }
342
401
  break;
343
402
  }
@@ -471,6 +530,89 @@ function extractSymbolsWalk(tree) {
471
530
  return { definitions, calls, imports, classes, exports };
472
531
  }
473
532
 
533
+ // ── Child extraction helpers ────────────────────────────────────────────────
534
+
535
+ function extractParameters(node) {
536
+ const params = [];
537
+ const paramsNode = node.childForFieldName('parameters') || findChild(node, 'formal_parameters');
538
+ if (!paramsNode) return params;
539
+ for (let i = 0; i < paramsNode.childCount; i++) {
540
+ const child = paramsNode.child(i);
541
+ if (!child) continue;
542
+ const t = child.type;
543
+ if (t === 'identifier') {
544
+ params.push({ name: child.text, kind: 'parameter', line: child.startPosition.row + 1 });
545
+ } else if (
546
+ t === 'required_parameter' ||
547
+ t === 'optional_parameter' ||
548
+ t === 'assignment_pattern'
549
+ ) {
550
+ const nameNode =
551
+ child.childForFieldName('pattern') || child.childForFieldName('left') || child.child(0);
552
+ if (
553
+ nameNode &&
554
+ (nameNode.type === 'identifier' ||
555
+ nameNode.type === 'shorthand_property_identifier_pattern')
556
+ ) {
557
+ params.push({ name: nameNode.text, kind: 'parameter', line: child.startPosition.row + 1 });
558
+ }
559
+ } else if (t === 'rest_pattern' || t === 'rest_element') {
560
+ const nameNode = child.child(1) || child.childForFieldName('name');
561
+ if (nameNode && nameNode.type === 'identifier') {
562
+ params.push({ name: nameNode.text, kind: 'parameter', line: child.startPosition.row + 1 });
563
+ }
564
+ }
565
+ }
566
+ return params;
567
+ }
568
+
569
+ function extractClassProperties(classNode) {
570
+ const props = [];
571
+ const body = classNode.childForFieldName('body') || findChild(classNode, 'class_body');
572
+ if (!body) return props;
573
+ for (let i = 0; i < body.childCount; i++) {
574
+ const child = body.child(i);
575
+ if (!child) continue;
576
+ if (
577
+ child.type === 'field_definition' ||
578
+ child.type === 'public_field_definition' ||
579
+ child.type === 'property_definition'
580
+ ) {
581
+ const nameNode =
582
+ child.childForFieldName('name') || child.childForFieldName('property') || child.child(0);
583
+ if (
584
+ nameNode &&
585
+ (nameNode.type === 'property_identifier' ||
586
+ nameNode.type === 'identifier' ||
587
+ nameNode.type === 'private_property_identifier')
588
+ ) {
589
+ props.push({ name: nameNode.text, kind: 'property', line: child.startPosition.row + 1 });
590
+ }
591
+ }
592
+ }
593
+ return props;
594
+ }
595
+
596
+ function isConstantValue(valueNode) {
597
+ if (!valueNode) return false;
598
+ const t = valueNode.type;
599
+ return (
600
+ t === 'number' ||
601
+ t === 'string' ||
602
+ t === 'template_string' ||
603
+ t === 'true' ||
604
+ t === 'false' ||
605
+ t === 'null' ||
606
+ t === 'undefined' ||
607
+ t === 'array' ||
608
+ t === 'object' ||
609
+ t === 'regex' ||
610
+ t === 'unary_expression' ||
611
+ t === 'binary_expression' ||
612
+ t === 'new_expression'
613
+ );
614
+ }
615
+
474
616
  // ── Shared helpers ──────────────────────────────────────────────────────────
475
617
 
476
618
  function extractInterfaceMethods(bodyNode, interfaceName, definitions) {
@@ -1,5 +1,76 @@
1
1
  import { findChild, nodeEndLine } from './helpers.js';
2
2
 
3
+ function extractPhpParameters(fnNode) {
4
+ const params = [];
5
+ const paramsNode =
6
+ fnNode.childForFieldName('parameters') || findChild(fnNode, 'formal_parameters');
7
+ if (!paramsNode) return params;
8
+ for (let i = 0; i < paramsNode.childCount; i++) {
9
+ const param = paramsNode.child(i);
10
+ if (!param) continue;
11
+ if (param.type === 'simple_parameter' || param.type === 'variadic_parameter') {
12
+ const nameNode = param.childForFieldName('name') || findChild(param, 'variable_name');
13
+ if (nameNode) {
14
+ params.push({ name: nameNode.text, kind: 'parameter', line: param.startPosition.row + 1 });
15
+ }
16
+ }
17
+ }
18
+ return params;
19
+ }
20
+
21
+ function extractPhpClassChildren(classNode) {
22
+ const children = [];
23
+ const body = classNode.childForFieldName('body') || findChild(classNode, 'declaration_list');
24
+ if (!body) return children;
25
+ for (let i = 0; i < body.childCount; i++) {
26
+ const member = body.child(i);
27
+ if (!member) continue;
28
+ if (member.type === 'property_declaration') {
29
+ for (let j = 0; j < member.childCount; j++) {
30
+ const el = member.child(j);
31
+ if (!el || el.type !== 'property_element') continue;
32
+ const varNode = findChild(el, 'variable_name');
33
+ if (varNode) {
34
+ children.push({
35
+ name: varNode.text,
36
+ kind: 'property',
37
+ line: member.startPosition.row + 1,
38
+ });
39
+ }
40
+ }
41
+ } else if (member.type === 'const_declaration') {
42
+ for (let j = 0; j < member.childCount; j++) {
43
+ const el = member.child(j);
44
+ if (!el || el.type !== 'const_element') continue;
45
+ const nameNode = el.childForFieldName('name') || findChild(el, 'name');
46
+ if (nameNode) {
47
+ children.push({
48
+ name: nameNode.text,
49
+ kind: 'constant',
50
+ line: member.startPosition.row + 1,
51
+ });
52
+ }
53
+ }
54
+ }
55
+ }
56
+ return children;
57
+ }
58
+
59
+ function extractPhpEnumCases(enumNode) {
60
+ const children = [];
61
+ const body = enumNode.childForFieldName('body') || findChild(enumNode, 'enum_declaration_list');
62
+ if (!body) return children;
63
+ for (let i = 0; i < body.childCount; i++) {
64
+ const member = body.child(i);
65
+ if (!member || member.type !== 'enum_case') continue;
66
+ const nameNode = member.childForFieldName('name');
67
+ if (nameNode) {
68
+ children.push({ name: nameNode.text, kind: 'constant', line: member.startPosition.row + 1 });
69
+ }
70
+ }
71
+ return children;
72
+ }
73
+
3
74
  /**
4
75
  * Extract symbols from PHP files.
5
76
  */
@@ -31,11 +102,13 @@ export function extractPHPSymbols(tree, _filePath) {
31
102
  case 'function_definition': {
32
103
  const nameNode = node.childForFieldName('name');
33
104
  if (nameNode) {
105
+ const params = extractPhpParameters(node);
34
106
  definitions.push({
35
107
  name: nameNode.text,
36
108
  kind: 'function',
37
109
  line: node.startPosition.row + 1,
38
110
  endLine: nodeEndLine(node),
111
+ children: params.length > 0 ? params : undefined,
39
112
  });
40
113
  }
41
114
  break;
@@ -44,11 +117,13 @@ export function extractPHPSymbols(tree, _filePath) {
44
117
  case 'class_declaration': {
45
118
  const nameNode = node.childForFieldName('name');
46
119
  if (nameNode) {
120
+ const classChildren = extractPhpClassChildren(node);
47
121
  definitions.push({
48
122
  name: nameNode.text,
49
123
  kind: 'class',
50
124
  line: node.startPosition.row + 1,
51
125
  endLine: nodeEndLine(node),
126
+ children: classChildren.length > 0 ? classChildren : undefined,
52
127
  });
53
128
 
54
129
  // Check base clause (extends)
@@ -132,11 +207,13 @@ export function extractPHPSymbols(tree, _filePath) {
132
207
  case 'enum_declaration': {
133
208
  const nameNode = node.childForFieldName('name');
134
209
  if (nameNode) {
210
+ const enumChildren = extractPhpEnumCases(node);
135
211
  definitions.push({
136
212
  name: nameNode.text,
137
213
  kind: 'enum',
138
214
  line: node.startPosition.row + 1,
139
215
  endLine: nodeEndLine(node),
216
+ children: enumChildren.length > 0 ? enumChildren : undefined,
140
217
  });
141
218
  }
142
219
  break;
@@ -147,11 +224,13 @@ export function extractPHPSymbols(tree, _filePath) {
147
224
  if (nameNode) {
148
225
  const parentClass = findPHPParentClass(node);
149
226
  const fullName = parentClass ? `${parentClass}.${nameNode.text}` : nameNode.text;
227
+ const params = extractPhpParameters(node);
150
228
  definitions.push({
151
229
  name: fullName,
152
230
  kind: 'method',
153
231
  line: node.startPosition.row + 1,
154
232
  endLine: nodeEndLine(node),
233
+ children: params.length > 0 ? params : undefined,
155
234
  });
156
235
  }
157
236
  break;
@@ -22,12 +22,14 @@ export function extractPythonSymbols(tree, _filePath) {
22
22
  const parentClass = findPythonParentClass(node);
23
23
  const fullName = parentClass ? `${parentClass}.${nameNode.text}` : nameNode.text;
24
24
  const kind = parentClass ? 'method' : 'function';
25
+ const fnChildren = extractPythonParameters(node);
25
26
  definitions.push({
26
27
  name: fullName,
27
28
  kind,
28
29
  line: node.startPosition.row + 1,
29
30
  endLine: nodeEndLine(node),
30
31
  decorators,
32
+ children: fnChildren.length > 0 ? fnChildren : undefined,
31
33
  });
32
34
  }
33
35
  break;
@@ -36,11 +38,13 @@ export function extractPythonSymbols(tree, _filePath) {
36
38
  case 'class_definition': {
37
39
  const nameNode = node.childForFieldName('name');
38
40
  if (nameNode) {
41
+ const clsChildren = extractPythonClassProperties(node);
39
42
  definitions.push({
40
43
  name: nameNode.text,
41
44
  kind: 'class',
42
45
  line: node.startPosition.row + 1,
43
46
  endLine: nodeEndLine(node),
47
+ children: clsChildren.length > 0 ? clsChildren : undefined,
44
48
  });
45
49
  const superclasses =
46
50
  node.childForFieldName('superclasses') || findChild(node, 'argument_list');
@@ -108,6 +112,24 @@ export function extractPythonSymbols(tree, _filePath) {
108
112
  break;
109
113
  }
110
114
 
115
+ case 'expression_statement': {
116
+ // Module-level UPPER_CASE assignments → constants
117
+ if (node.parent && node.parent.type === 'module') {
118
+ const assignment = findChild(node, 'assignment');
119
+ if (assignment) {
120
+ const left = assignment.childForFieldName('left');
121
+ if (left && left.type === 'identifier' && /^[A-Z_][A-Z0-9_]*$/.test(left.text)) {
122
+ definitions.push({
123
+ name: left.text,
124
+ kind: 'constant',
125
+ line: node.startPosition.row + 1,
126
+ });
127
+ }
128
+ }
129
+ }
130
+ break;
131
+ }
132
+
111
133
  case 'import_from_statement': {
112
134
  let source = '';
113
135
  const names = [];
@@ -133,6 +155,118 @@ export function extractPythonSymbols(tree, _filePath) {
133
155
  for (let i = 0; i < node.childCount; i++) walkPythonNode(node.child(i));
134
156
  }
135
157
 
158
+ function extractPythonParameters(fnNode) {
159
+ const params = [];
160
+ const paramsNode = fnNode.childForFieldName('parameters') || findChild(fnNode, 'parameters');
161
+ if (!paramsNode) return params;
162
+ for (let i = 0; i < paramsNode.childCount; i++) {
163
+ const child = paramsNode.child(i);
164
+ if (!child) continue;
165
+ const t = child.type;
166
+ if (t === 'identifier') {
167
+ params.push({ name: child.text, kind: 'parameter', line: child.startPosition.row + 1 });
168
+ } else if (
169
+ t === 'typed_parameter' ||
170
+ t === 'default_parameter' ||
171
+ t === 'typed_default_parameter'
172
+ ) {
173
+ const nameNode = child.childForFieldName('name') || child.child(0);
174
+ if (nameNode && nameNode.type === 'identifier') {
175
+ params.push({
176
+ name: nameNode.text,
177
+ kind: 'parameter',
178
+ line: child.startPosition.row + 1,
179
+ });
180
+ }
181
+ } else if (t === 'list_splat_pattern' || t === 'dictionary_splat_pattern') {
182
+ // *args, **kwargs
183
+ for (let j = 0; j < child.childCount; j++) {
184
+ const inner = child.child(j);
185
+ if (inner && inner.type === 'identifier') {
186
+ params.push({ name: inner.text, kind: 'parameter', line: child.startPosition.row + 1 });
187
+ break;
188
+ }
189
+ }
190
+ }
191
+ }
192
+ return params;
193
+ }
194
+
195
+ function extractPythonClassProperties(classNode) {
196
+ const props = [];
197
+ const seen = new Set();
198
+ const body = classNode.childForFieldName('body') || findChild(classNode, 'block');
199
+ if (!body) return props;
200
+
201
+ for (let i = 0; i < body.childCount; i++) {
202
+ const child = body.child(i);
203
+ if (!child) continue;
204
+
205
+ // Direct class attribute assignments: x = 5
206
+ if (child.type === 'expression_statement') {
207
+ const assignment = findChild(child, 'assignment');
208
+ if (assignment) {
209
+ const left = assignment.childForFieldName('left');
210
+ if (left && left.type === 'identifier' && !seen.has(left.text)) {
211
+ seen.add(left.text);
212
+ props.push({ name: left.text, kind: 'property', line: child.startPosition.row + 1 });
213
+ }
214
+ }
215
+ }
216
+
217
+ // __init__ method: self.x = ... assignments
218
+ if (child.type === 'function_definition') {
219
+ const fnName = child.childForFieldName('name');
220
+ if (fnName && fnName.text === '__init__') {
221
+ const initBody = child.childForFieldName('body') || findChild(child, 'block');
222
+ if (initBody) {
223
+ walkInitBody(initBody, seen, props);
224
+ }
225
+ }
226
+ }
227
+
228
+ // decorated __init__
229
+ if (child.type === 'decorated_definition') {
230
+ for (let j = 0; j < child.childCount; j++) {
231
+ const inner = child.child(j);
232
+ if (inner && inner.type === 'function_definition') {
233
+ const fnName = inner.childForFieldName('name');
234
+ if (fnName && fnName.text === '__init__') {
235
+ const initBody = inner.childForFieldName('body') || findChild(inner, 'block');
236
+ if (initBody) {
237
+ walkInitBody(initBody, seen, props);
238
+ }
239
+ }
240
+ }
241
+ }
242
+ }
243
+ }
244
+ return props;
245
+ }
246
+
247
+ function walkInitBody(bodyNode, seen, props) {
248
+ for (let i = 0; i < bodyNode.childCount; i++) {
249
+ const stmt = bodyNode.child(i);
250
+ if (!stmt || stmt.type !== 'expression_statement') continue;
251
+ const assignment = findChild(stmt, 'assignment');
252
+ if (!assignment) continue;
253
+ const left = assignment.childForFieldName('left');
254
+ if (!left || left.type !== 'attribute') continue;
255
+ const obj = left.childForFieldName('object');
256
+ const attr = left.childForFieldName('attribute');
257
+ if (
258
+ obj &&
259
+ obj.text === 'self' &&
260
+ attr &&
261
+ attr.type === 'identifier' &&
262
+ !seen.has(attr.text)
263
+ ) {
264
+ seen.add(attr.text);
265
+ props.push({ name: attr.text, kind: 'property', line: stmt.startPosition.row + 1 });
266
+ }
267
+ }
268
+ }
269
+
136
270
  function findPythonParentClass(node) {
137
271
  let current = node.parent;
138
272
  while (current) {