@optave/codegraph 3.1.5 → 3.2.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.
Files changed (91) hide show
  1. package/README.md +3 -2
  2. package/package.json +7 -7
  3. package/src/ast-analysis/engine.js +252 -258
  4. package/src/ast-analysis/shared.js +0 -12
  5. package/src/ast-analysis/visitors/cfg-visitor.js +635 -649
  6. package/src/ast-analysis/visitors/complexity-visitor.js +135 -139
  7. package/src/ast-analysis/visitors/dataflow-visitor.js +230 -224
  8. package/src/cli/commands/ast.js +2 -1
  9. package/src/cli/commands/audit.js +2 -1
  10. package/src/cli/commands/batch.js +2 -1
  11. package/src/cli/commands/brief.js +12 -0
  12. package/src/cli/commands/cfg.js +2 -1
  13. package/src/cli/commands/check.js +20 -23
  14. package/src/cli/commands/children.js +6 -1
  15. package/src/cli/commands/complexity.js +2 -1
  16. package/src/cli/commands/context.js +6 -1
  17. package/src/cli/commands/dataflow.js +2 -1
  18. package/src/cli/commands/deps.js +8 -3
  19. package/src/cli/commands/flow.js +2 -1
  20. package/src/cli/commands/fn-impact.js +6 -1
  21. package/src/cli/commands/owners.js +4 -2
  22. package/src/cli/commands/query.js +6 -1
  23. package/src/cli/commands/roles.js +2 -1
  24. package/src/cli/commands/search.js +8 -2
  25. package/src/cli/commands/sequence.js +2 -1
  26. package/src/cli/commands/triage.js +38 -27
  27. package/src/db/connection.js +18 -12
  28. package/src/db/migrations.js +41 -64
  29. package/src/db/query-builder.js +60 -4
  30. package/src/db/repository/in-memory-repository.js +27 -16
  31. package/src/db/repository/nodes.js +8 -10
  32. package/src/domain/analysis/brief.js +155 -0
  33. package/src/domain/analysis/context.js +174 -190
  34. package/src/domain/analysis/dependencies.js +200 -146
  35. package/src/domain/analysis/exports.js +3 -2
  36. package/src/domain/analysis/impact.js +267 -152
  37. package/src/domain/analysis/module-map.js +247 -221
  38. package/src/domain/analysis/roles.js +8 -5
  39. package/src/domain/analysis/symbol-lookup.js +7 -5
  40. package/src/domain/graph/builder/helpers.js +1 -1
  41. package/src/domain/graph/builder/incremental.js +116 -90
  42. package/src/domain/graph/builder/pipeline.js +106 -80
  43. package/src/domain/graph/builder/stages/build-edges.js +318 -239
  44. package/src/domain/graph/builder/stages/detect-changes.js +198 -177
  45. package/src/domain/graph/builder/stages/insert-nodes.js +147 -139
  46. package/src/domain/graph/watcher.js +2 -2
  47. package/src/domain/parser.js +20 -11
  48. package/src/domain/queries.js +1 -0
  49. package/src/domain/search/search/filters.js +9 -5
  50. package/src/domain/search/search/keyword.js +12 -5
  51. package/src/domain/search/search/prepare.js +13 -5
  52. package/src/extractors/csharp.js +224 -207
  53. package/src/extractors/go.js +176 -172
  54. package/src/extractors/hcl.js +94 -78
  55. package/src/extractors/java.js +213 -207
  56. package/src/extractors/javascript.js +274 -304
  57. package/src/extractors/php.js +234 -221
  58. package/src/extractors/python.js +252 -250
  59. package/src/extractors/ruby.js +192 -185
  60. package/src/extractors/rust.js +182 -167
  61. package/src/features/ast.js +5 -3
  62. package/src/features/audit.js +4 -2
  63. package/src/features/boundaries.js +98 -83
  64. package/src/features/cfg.js +134 -143
  65. package/src/features/communities.js +68 -53
  66. package/src/features/complexity.js +143 -132
  67. package/src/features/dataflow.js +146 -149
  68. package/src/features/export.js +3 -3
  69. package/src/features/graph-enrichment.js +2 -2
  70. package/src/features/manifesto.js +9 -6
  71. package/src/features/owners.js +4 -3
  72. package/src/features/sequence.js +152 -141
  73. package/src/features/shared/find-nodes.js +31 -0
  74. package/src/features/structure.js +130 -99
  75. package/src/features/triage.js +83 -68
  76. package/src/graph/classifiers/risk.js +3 -2
  77. package/src/graph/classifiers/roles.js +6 -3
  78. package/src/index.js +1 -0
  79. package/src/mcp/server.js +65 -56
  80. package/src/mcp/tool-registry.js +13 -0
  81. package/src/mcp/tools/brief.js +8 -0
  82. package/src/mcp/tools/index.js +2 -0
  83. package/src/presentation/brief.js +51 -0
  84. package/src/presentation/queries-cli/exports.js +21 -14
  85. package/src/presentation/queries-cli/impact.js +55 -39
  86. package/src/presentation/queries-cli/inspect.js +184 -189
  87. package/src/presentation/queries-cli/overview.js +57 -58
  88. package/src/presentation/queries-cli/path.js +36 -29
  89. package/src/presentation/table.js +0 -8
  90. package/src/shared/generators.js +7 -3
  91. package/src/shared/kinds.js +1 -1
@@ -4,196 +4,200 @@ import { findChild, goVisibility, nodeEndLine } from './helpers.js';
4
4
  * Extract symbols from Go files.
5
5
  */
6
6
  export function extractGoSymbols(tree, _filePath) {
7
- const definitions = [];
8
- const calls = [];
9
- const imports = [];
10
- const classes = [];
11
- const exports = [];
7
+ const ctx = {
8
+ definitions: [],
9
+ calls: [],
10
+ imports: [],
11
+ classes: [],
12
+ exports: [],
13
+ };
12
14
 
13
- function walkGoNode(node) {
14
- switch (node.type) {
15
- case 'function_declaration': {
16
- const nameNode = node.childForFieldName('name');
17
- if (nameNode) {
18
- const params = extractGoParameters(node.childForFieldName('parameters'));
19
- definitions.push({
20
- name: nameNode.text,
21
- kind: 'function',
22
- line: node.startPosition.row + 1,
23
- endLine: nodeEndLine(node),
24
- children: params.length > 0 ? params : undefined,
25
- visibility: goVisibility(nameNode.text),
26
- });
27
- }
28
- break;
29
- }
15
+ walkGoNode(tree.rootNode, ctx);
16
+ return ctx;
17
+ }
30
18
 
31
- case 'method_declaration': {
32
- const nameNode = node.childForFieldName('name');
33
- const receiver = node.childForFieldName('receiver');
34
- if (nameNode) {
35
- let receiverType = null;
36
- if (receiver) {
37
- // receiver is a parameter_list like (r *Foo) or (r Foo)
38
- for (let i = 0; i < receiver.childCount; i++) {
39
- const param = receiver.child(i);
40
- if (!param) continue;
41
- const typeNode = param.childForFieldName('type');
42
- if (typeNode) {
43
- receiverType =
44
- typeNode.type === 'pointer_type'
45
- ? typeNode.text.replace(/^\*/, '')
46
- : typeNode.text;
47
- break;
48
- }
49
- }
50
- }
51
- const fullName = receiverType ? `${receiverType}.${nameNode.text}` : nameNode.text;
52
- const params = extractGoParameters(node.childForFieldName('parameters'));
53
- definitions.push({
54
- name: fullName,
55
- kind: 'method',
56
- line: node.startPosition.row + 1,
57
- endLine: nodeEndLine(node),
58
- children: params.length > 0 ? params : undefined,
59
- visibility: goVisibility(nameNode.text),
60
- });
61
- }
62
- break;
63
- }
19
+ function walkGoNode(node, ctx) {
20
+ switch (node.type) {
21
+ case 'function_declaration':
22
+ handleGoFuncDecl(node, ctx);
23
+ break;
24
+ case 'method_declaration':
25
+ handleGoMethodDecl(node, ctx);
26
+ break;
27
+ case 'type_declaration':
28
+ handleGoTypeDecl(node, ctx);
29
+ break;
30
+ case 'import_declaration':
31
+ handleGoImportDecl(node, ctx);
32
+ break;
33
+ case 'const_declaration':
34
+ handleGoConstDecl(node, ctx);
35
+ break;
36
+ case 'call_expression':
37
+ handleGoCallExpr(node, ctx);
38
+ break;
39
+ }
64
40
 
65
- case 'type_declaration': {
66
- for (let i = 0; i < node.childCount; i++) {
67
- const spec = node.child(i);
68
- if (!spec || spec.type !== 'type_spec') continue;
69
- const nameNode = spec.childForFieldName('name');
70
- const typeNode = spec.childForFieldName('type');
71
- if (nameNode && typeNode) {
72
- if (typeNode.type === 'struct_type') {
73
- const fields = extractStructFields(typeNode);
74
- definitions.push({
75
- name: nameNode.text,
76
- kind: 'struct',
77
- line: node.startPosition.row + 1,
78
- endLine: nodeEndLine(node),
79
- children: fields.length > 0 ? fields : undefined,
80
- });
81
- } else if (typeNode.type === 'interface_type') {
82
- definitions.push({
83
- name: nameNode.text,
84
- kind: 'interface',
85
- line: node.startPosition.row + 1,
86
- endLine: nodeEndLine(node),
87
- });
88
- for (let j = 0; j < typeNode.childCount; j++) {
89
- const member = typeNode.child(j);
90
- if (member && member.type === 'method_elem') {
91
- const methName = member.childForFieldName('name');
92
- if (methName) {
93
- definitions.push({
94
- name: `${nameNode.text}.${methName.text}`,
95
- kind: 'method',
96
- line: member.startPosition.row + 1,
97
- endLine: member.endPosition.row + 1,
98
- });
99
- }
100
- }
101
- }
102
- } else {
103
- definitions.push({
104
- name: nameNode.text,
105
- kind: 'type',
106
- line: node.startPosition.row + 1,
107
- endLine: nodeEndLine(node),
108
- });
109
- }
110
- }
111
- }
41
+ for (let i = 0; i < node.childCount; i++) walkGoNode(node.child(i), ctx);
42
+ }
43
+
44
+ // ── Walk-path per-node-type handlers ────────────────────────────────────────
45
+
46
+ function handleGoFuncDecl(node, ctx) {
47
+ const nameNode = node.childForFieldName('name');
48
+ if (!nameNode) return;
49
+ const params = extractGoParameters(node.childForFieldName('parameters'));
50
+ ctx.definitions.push({
51
+ name: nameNode.text,
52
+ kind: 'function',
53
+ line: node.startPosition.row + 1,
54
+ endLine: nodeEndLine(node),
55
+ children: params.length > 0 ? params : undefined,
56
+ visibility: goVisibility(nameNode.text),
57
+ });
58
+ }
59
+
60
+ function handleGoMethodDecl(node, ctx) {
61
+ const nameNode = node.childForFieldName('name');
62
+ const receiver = node.childForFieldName('receiver');
63
+ if (!nameNode) return;
64
+ let receiverType = null;
65
+ if (receiver) {
66
+ for (let i = 0; i < receiver.childCount; i++) {
67
+ const param = receiver.child(i);
68
+ if (!param) continue;
69
+ const typeNode = param.childForFieldName('type');
70
+ if (typeNode) {
71
+ receiverType =
72
+ typeNode.type === 'pointer_type' ? typeNode.text.replace(/^\*/, '') : typeNode.text;
112
73
  break;
113
74
  }
75
+ }
76
+ }
77
+ const fullName = receiverType ? `${receiverType}.${nameNode.text}` : nameNode.text;
78
+ const params = extractGoParameters(node.childForFieldName('parameters'));
79
+ ctx.definitions.push({
80
+ name: fullName,
81
+ kind: 'method',
82
+ line: node.startPosition.row + 1,
83
+ endLine: nodeEndLine(node),
84
+ children: params.length > 0 ? params : undefined,
85
+ visibility: goVisibility(nameNode.text),
86
+ });
87
+ }
114
88
 
115
- case 'import_declaration': {
116
- for (let i = 0; i < node.childCount; i++) {
117
- const child = node.child(i);
118
- if (!child) continue;
119
- if (child.type === 'import_spec') {
120
- const pathNode = child.childForFieldName('path');
121
- if (pathNode) {
122
- const importPath = pathNode.text.replace(/"/g, '');
123
- const nameNode = child.childForFieldName('name');
124
- const alias = nameNode ? nameNode.text : importPath.split('/').pop();
125
- imports.push({
126
- source: importPath,
127
- names: [alias],
128
- line: child.startPosition.row + 1,
129
- goImport: true,
89
+ function handleGoTypeDecl(node, ctx) {
90
+ for (let i = 0; i < node.childCount; i++) {
91
+ const spec = node.child(i);
92
+ if (!spec || spec.type !== 'type_spec') continue;
93
+ const nameNode = spec.childForFieldName('name');
94
+ const typeNode = spec.childForFieldName('type');
95
+ if (nameNode && typeNode) {
96
+ if (typeNode.type === 'struct_type') {
97
+ const fields = extractStructFields(typeNode);
98
+ ctx.definitions.push({
99
+ name: nameNode.text,
100
+ kind: 'struct',
101
+ line: node.startPosition.row + 1,
102
+ endLine: nodeEndLine(node),
103
+ children: fields.length > 0 ? fields : undefined,
104
+ });
105
+ } else if (typeNode.type === 'interface_type') {
106
+ ctx.definitions.push({
107
+ name: nameNode.text,
108
+ kind: 'interface',
109
+ line: node.startPosition.row + 1,
110
+ endLine: nodeEndLine(node),
111
+ });
112
+ for (let j = 0; j < typeNode.childCount; j++) {
113
+ const member = typeNode.child(j);
114
+ if (member && member.type === 'method_elem') {
115
+ const methName = member.childForFieldName('name');
116
+ if (methName) {
117
+ ctx.definitions.push({
118
+ name: `${nameNode.text}.${methName.text}`,
119
+ kind: 'method',
120
+ line: member.startPosition.row + 1,
121
+ endLine: member.endPosition.row + 1,
130
122
  });
131
123
  }
132
124
  }
133
- if (child.type === 'import_spec_list') {
134
- for (let j = 0; j < child.childCount; j++) {
135
- const spec = child.child(j);
136
- if (spec && spec.type === 'import_spec') {
137
- const pathNode = spec.childForFieldName('path');
138
- if (pathNode) {
139
- const importPath = pathNode.text.replace(/"/g, '');
140
- const nameNode = spec.childForFieldName('name');
141
- const alias = nameNode ? nameNode.text : importPath.split('/').pop();
142
- imports.push({
143
- source: importPath,
144
- names: [alias],
145
- line: spec.startPosition.row + 1,
146
- goImport: true,
147
- });
148
- }
149
- }
150
- }
151
- }
152
- }
153
- break;
154
- }
155
-
156
- case 'const_declaration': {
157
- for (let i = 0; i < node.childCount; i++) {
158
- const spec = node.child(i);
159
- if (!spec || spec.type !== 'const_spec') continue;
160
- const constName = spec.childForFieldName('name');
161
- if (constName) {
162
- definitions.push({
163
- name: constName.text,
164
- kind: 'constant',
165
- line: spec.startPosition.row + 1,
166
- endLine: spec.endPosition.row + 1,
167
- });
168
- }
169
125
  }
170
- break;
126
+ } else {
127
+ ctx.definitions.push({
128
+ name: nameNode.text,
129
+ kind: 'type',
130
+ line: node.startPosition.row + 1,
131
+ endLine: nodeEndLine(node),
132
+ });
171
133
  }
134
+ }
135
+ }
136
+ }
172
137
 
173
- case 'call_expression': {
174
- const fn = node.childForFieldName('function');
175
- if (fn) {
176
- if (fn.type === 'identifier') {
177
- calls.push({ name: fn.text, line: node.startPosition.row + 1 });
178
- } else if (fn.type === 'selector_expression') {
179
- const field = fn.childForFieldName('field');
180
- if (field) {
181
- const operand = fn.childForFieldName('operand');
182
- const call = { name: field.text, line: node.startPosition.row + 1 };
183
- if (operand) call.receiver = operand.text;
184
- calls.push(call);
185
- }
186
- }
138
+ function handleGoImportDecl(node, ctx) {
139
+ for (let i = 0; i < node.childCount; i++) {
140
+ const child = node.child(i);
141
+ if (!child) continue;
142
+ if (child.type === 'import_spec') {
143
+ extractGoImportSpec(child, ctx);
144
+ }
145
+ if (child.type === 'import_spec_list') {
146
+ for (let j = 0; j < child.childCount; j++) {
147
+ const spec = child.child(j);
148
+ if (spec && spec.type === 'import_spec') {
149
+ extractGoImportSpec(spec, ctx);
187
150
  }
188
- break;
189
151
  }
190
152
  }
153
+ }
154
+ }
155
+
156
+ function extractGoImportSpec(spec, ctx) {
157
+ const pathNode = spec.childForFieldName('path');
158
+ if (pathNode) {
159
+ const importPath = pathNode.text.replace(/"/g, '');
160
+ const nameNode = spec.childForFieldName('name');
161
+ const alias = nameNode ? nameNode.text : importPath.split('/').pop();
162
+ ctx.imports.push({
163
+ source: importPath,
164
+ names: [alias],
165
+ line: spec.startPosition.row + 1,
166
+ goImport: true,
167
+ });
168
+ }
169
+ }
191
170
 
192
- for (let i = 0; i < node.childCount; i++) walkGoNode(node.child(i));
171
+ function handleGoConstDecl(node, ctx) {
172
+ for (let i = 0; i < node.childCount; i++) {
173
+ const spec = node.child(i);
174
+ if (!spec || spec.type !== 'const_spec') continue;
175
+ const constName = spec.childForFieldName('name');
176
+ if (constName) {
177
+ ctx.definitions.push({
178
+ name: constName.text,
179
+ kind: 'constant',
180
+ line: spec.startPosition.row + 1,
181
+ endLine: spec.endPosition.row + 1,
182
+ });
183
+ }
193
184
  }
185
+ }
194
186
 
195
- walkGoNode(tree.rootNode);
196
- return { definitions, calls, imports, classes, exports };
187
+ function handleGoCallExpr(node, ctx) {
188
+ const fn = node.childForFieldName('function');
189
+ if (!fn) return;
190
+ if (fn.type === 'identifier') {
191
+ ctx.calls.push({ name: fn.text, line: node.startPosition.row + 1 });
192
+ } else if (fn.type === 'selector_expression') {
193
+ const field = fn.childForFieldName('field');
194
+ if (field) {
195
+ const operand = fn.childForFieldName('operand');
196
+ const call = { name: field.text, line: node.startPosition.row + 1 };
197
+ if (operand) call.receiver = operand.text;
198
+ ctx.calls.push(call);
199
+ }
200
+ }
197
201
  }
198
202
 
199
203
  // ── Child extraction helpers ────────────────────────────────────────────────
@@ -4,92 +4,108 @@ import { nodeEndLine } from './helpers.js';
4
4
  * Extract symbols from HCL (Terraform) files.
5
5
  */
6
6
  export function extractHCLSymbols(tree, _filePath) {
7
- const definitions = [];
8
- const imports = [];
7
+ const ctx = { definitions: [], imports: [] };
9
8
 
10
- function walkHclNode(node) {
11
- if (node.type === 'block') {
12
- const children = [];
13
- for (let i = 0; i < node.childCount; i++) children.push(node.child(i));
9
+ walkHclNode(tree.rootNode, ctx);
10
+ return {
11
+ definitions: ctx.definitions,
12
+ calls: [],
13
+ imports: ctx.imports,
14
+ classes: [],
15
+ exports: [],
16
+ };
17
+ }
14
18
 
15
- const identifiers = children.filter((c) => c.type === 'identifier');
16
- const strings = children.filter((c) => c.type === 'string_lit');
19
+ function walkHclNode(node, ctx) {
20
+ if (node.type === 'block') {
21
+ handleHclBlock(node, ctx);
22
+ }
17
23
 
18
- if (identifiers.length > 0) {
19
- const blockType = identifiers[0].text;
20
- let name = '';
24
+ for (let i = 0; i < node.childCount; i++) walkHclNode(node.child(i), ctx);
25
+ }
21
26
 
22
- if (blockType === 'resource' && strings.length >= 2) {
23
- name = `${strings[0].text.replace(/"/g, '')}.${strings[1].text.replace(/"/g, '')}`;
24
- } else if (blockType === 'data' && strings.length >= 2) {
25
- name = `data.${strings[0].text.replace(/"/g, '')}.${strings[1].text.replace(/"/g, '')}`;
26
- } else if (
27
- (blockType === 'variable' || blockType === 'output' || blockType === 'module') &&
28
- strings.length >= 1
29
- ) {
30
- name = `${blockType}.${strings[0].text.replace(/"/g, '')}`;
31
- } else if (blockType === 'locals') {
32
- name = 'locals';
33
- } else if (blockType === 'terraform' || blockType === 'provider') {
34
- name = blockType;
35
- if (strings.length >= 1) name += `.${strings[0].text.replace(/"/g, '')}`;
36
- }
27
+ function handleHclBlock(node, ctx) {
28
+ const children = [];
29
+ for (let i = 0; i < node.childCount; i++) children.push(node.child(i));
37
30
 
38
- if (name) {
39
- // Extract attributes as property children for variable/output blocks
40
- let blockChildren;
41
- if (blockType === 'variable' || blockType === 'output') {
42
- blockChildren = [];
43
- const body = children.find((c) => c.type === 'body');
44
- if (body) {
45
- for (let j = 0; j < body.childCount; j++) {
46
- const attr = body.child(j);
47
- if (attr && attr.type === 'attribute') {
48
- const key = attr.childForFieldName('key') || attr.child(0);
49
- if (key) {
50
- blockChildren.push({
51
- name: key.text,
52
- kind: 'property',
53
- line: attr.startPosition.row + 1,
54
- });
55
- }
56
- }
57
- }
58
- }
59
- }
60
- definitions.push({
61
- name,
62
- kind: blockType,
63
- line: node.startPosition.row + 1,
64
- endLine: nodeEndLine(node),
65
- children: blockChildren?.length > 0 ? blockChildren : undefined,
66
- });
67
- }
31
+ const identifiers = children.filter((c) => c.type === 'identifier');
32
+ const strings = children.filter((c) => c.type === 'string_lit');
68
33
 
69
- if (blockType === 'module') {
70
- const body = children.find((c) => c.type === 'body');
71
- if (body) {
72
- for (let i = 0; i < body.childCount; i++) {
73
- const attr = body.child(i);
74
- if (attr && attr.type === 'attribute') {
75
- const key = attr.childForFieldName('key') || attr.child(0);
76
- const val = attr.childForFieldName('val') || attr.child(2);
77
- if (key && key.text === 'source' && val) {
78
- const src = val.text.replace(/"/g, '');
79
- if (src.startsWith('./') || src.startsWith('../')) {
80
- imports.push({ source: src, names: [], line: attr.startPosition.row + 1 });
81
- }
82
- }
83
- }
84
- }
85
- }
86
- }
87
- }
34
+ if (identifiers.length === 0) return;
35
+ const blockType = identifiers[0].text;
36
+ const name = resolveHclBlockName(blockType, strings);
37
+
38
+ if (name) {
39
+ let blockChildren;
40
+ if (blockType === 'variable' || blockType === 'output') {
41
+ blockChildren = extractHclAttributes(children);
88
42
  }
43
+ ctx.definitions.push({
44
+ name,
45
+ kind: blockType,
46
+ line: node.startPosition.row + 1,
47
+ endLine: nodeEndLine(node),
48
+ children: blockChildren?.length > 0 ? blockChildren : undefined,
49
+ });
50
+ }
89
51
 
90
- for (let i = 0; i < node.childCount; i++) walkHclNode(node.child(i));
52
+ if (blockType === 'module') {
53
+ extractHclModuleSource(children, node, ctx);
91
54
  }
55
+ }
92
56
 
93
- walkHclNode(tree.rootNode);
94
- return { definitions, calls: [], imports, classes: [], exports: [] };
57
+ function resolveHclBlockName(blockType, strings) {
58
+ if (blockType === 'resource' && strings.length >= 2) {
59
+ return `${strings[0].text.replace(/"/g, '')}.${strings[1].text.replace(/"/g, '')}`;
60
+ }
61
+ if (blockType === 'data' && strings.length >= 2) {
62
+ return `data.${strings[0].text.replace(/"/g, '')}.${strings[1].text.replace(/"/g, '')}`;
63
+ }
64
+ if (
65
+ (blockType === 'variable' || blockType === 'output' || blockType === 'module') &&
66
+ strings.length >= 1
67
+ ) {
68
+ return `${blockType}.${strings[0].text.replace(/"/g, '')}`;
69
+ }
70
+ if (blockType === 'locals') return 'locals';
71
+ if (blockType === 'terraform' || blockType === 'provider') {
72
+ let name = blockType;
73
+ if (strings.length >= 1) name += `.${strings[0].text.replace(/"/g, '')}`;
74
+ return name;
75
+ }
76
+ return '';
77
+ }
78
+
79
+ function extractHclAttributes(children) {
80
+ const attrs = [];
81
+ const body = children.find((c) => c.type === 'body');
82
+ if (!body) return attrs;
83
+ for (let j = 0; j < body.childCount; j++) {
84
+ const attr = body.child(j);
85
+ if (attr && attr.type === 'attribute') {
86
+ const key = attr.childForFieldName('key') || attr.child(0);
87
+ if (key) {
88
+ attrs.push({ name: key.text, kind: 'property', line: attr.startPosition.row + 1 });
89
+ }
90
+ }
91
+ }
92
+ return attrs;
93
+ }
94
+
95
+ function extractHclModuleSource(children, _node, ctx) {
96
+ const body = children.find((c) => c.type === 'body');
97
+ if (!body) return;
98
+ for (let i = 0; i < body.childCount; i++) {
99
+ const attr = body.child(i);
100
+ if (attr && attr.type === 'attribute') {
101
+ const key = attr.childForFieldName('key') || attr.child(0);
102
+ const val = attr.childForFieldName('val') || attr.child(2);
103
+ if (key && key.text === 'source' && val) {
104
+ const src = val.text.replace(/"/g, '');
105
+ if (src.startsWith('./') || src.startsWith('../')) {
106
+ ctx.imports.push({ source: src, names: [], line: attr.startPosition.row + 1 });
107
+ }
108
+ }
109
+ }
110
+ }
95
111
  }