code-oracle 0.1.0__py3-none-any.whl

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 (40) hide show
  1. code_oracle/__init__.py +30 -0
  2. code_oracle/cli.py +795 -0
  3. code_oracle/config.py +145 -0
  4. code_oracle/dataset.py +5325 -0
  5. code_oracle/dead_code/__init__.py +32 -0
  6. code_oracle/dead_code/detector.py +379 -0
  7. code_oracle/dead_code/entrypoints.py +333 -0
  8. code_oracle/dead_code/models.py +255 -0
  9. code_oracle/dead_code/semantics.py +416 -0
  10. code_oracle/decision.py +906 -0
  11. code_oracle/engine.py +430 -0
  12. code_oracle/export_onnx.py +436 -0
  13. code_oracle/hook.py +531 -0
  14. code_oracle/indexer.py +894 -0
  15. code_oracle/languages/__init__.py +114 -0
  16. code_oracle/languages/common.py +127 -0
  17. code_oracle/languages/go.py +395 -0
  18. code_oracle/languages/python.py +336 -0
  19. code_oracle/languages/rust.py +474 -0
  20. code_oracle/languages/typescript.py +775 -0
  21. code_oracle/linearizer.py +166 -0
  22. code_oracle/locator.py +301 -0
  23. code_oracle/models.py +237 -0
  24. code_oracle/perf_lint/__init__.py +38 -0
  25. code_oracle/perf_lint/engine.py +234 -0
  26. code_oracle/perf_lint/models.py +229 -0
  27. code_oracle/perf_lint/rules/__init__.py +31 -0
  28. code_oracle/perf_lint/rules/async_blocking.py +143 -0
  29. code_oracle/perf_lint/rules/n_plus_one.py +232 -0
  30. code_oracle/perf_lint/rules/nested_loops.py +137 -0
  31. code_oracle/perf_lint/rules/unclosed_res.py +494 -0
  32. code_oracle/perf_lint/visitor.py +299 -0
  33. code_oracle/server.py +184 -0
  34. code_oracle/slicer.py +225 -0
  35. code_oracle/symbolic.py +459 -0
  36. code_oracle-0.1.0.dist-info/METADATA +225 -0
  37. code_oracle-0.1.0.dist-info/RECORD +40 -0
  38. code_oracle-0.1.0.dist-info/WHEEL +4 -0
  39. code_oracle-0.1.0.dist-info/entry_points.txt +2 -0
  40. code_oracle-0.1.0.dist-info/licenses/LICENSE +190 -0
@@ -0,0 +1,114 @@
1
+ """
2
+ Modular Multi-Language AST Extraction Engine for Code Oracle.
3
+ Provides unified AST parsing across Tier 1 languages: Python, TypeScript/JavaScript, Go, and Rust.
4
+ """
5
+
6
+ from pathlib import Path
7
+ from typing import Dict, List, Optional, Set
8
+
9
+ from code_oracle.languages.go import extract_go_imports, extract_go_symbols, validate_go_syntax
10
+ from code_oracle.languages.python import (
11
+ extract_python_imports,
12
+ extract_python_symbols,
13
+ validate_python_syntax,
14
+ )
15
+ from code_oracle.languages.rust import extract_rust_imports, extract_rust_symbols, validate_rust_syntax
16
+ from code_oracle.languages.typescript import (
17
+ extract_typescript_imports,
18
+ extract_typescript_symbols,
19
+ validate_typescript_syntax,
20
+ )
21
+ from code_oracle.models import ImportReference, Symbol
22
+
23
+ EXTENSION_TO_LANGUAGE: Dict[str, str] = {
24
+ ".py": "python",
25
+ ".ts": "typescript",
26
+ ".tsx": "typescript",
27
+ ".js": "javascript",
28
+ ".jsx": "javascript",
29
+ ".mjs": "javascript",
30
+ ".cjs": "javascript",
31
+ ".go": "go",
32
+ ".rs": "rust",
33
+ }
34
+
35
+ SUPPORTED_LANGUAGES: Set[str] = {"python", "typescript", "javascript", "go", "rust"}
36
+ SUPPORTED_EXTENSIONS: Set[str] = set(EXTENSION_TO_LANGUAGE.keys())
37
+
38
+
39
+ def detect_language(file_path: str) -> Optional[str]:
40
+ """Detect the language of a file from its extension or path."""
41
+ if not file_path:
42
+ return "python"
43
+ ext = Path(file_path).suffix.lower()
44
+ return EXTENSION_TO_LANGUAGE.get(ext)
45
+
46
+
47
+ def extract_symbols(
48
+ source: str,
49
+ file_path: str = "",
50
+ language: Optional[str] = None,
51
+ ) -> List[Symbol]:
52
+ """
53
+ Extract symbol entities from source code across any supported Tier 1 language.
54
+ Dispatches to language-specific Tree-sitter or AST extractor based on file extension.
55
+ """
56
+ lang = language or detect_language(file_path) or "python"
57
+
58
+ if lang == "python":
59
+ return extract_python_symbols(source, file_path=file_path)
60
+ elif lang in ("typescript", "javascript"):
61
+ return extract_typescript_symbols(source, file_path=file_path)
62
+ elif lang == "go":
63
+ return extract_go_symbols(source, file_path=file_path)
64
+ elif lang == "rust":
65
+ return extract_rust_symbols(source, file_path=file_path)
66
+ else:
67
+ # Fallback to Python AST
68
+ return extract_python_symbols(source, file_path=file_path)
69
+
70
+
71
+ def extract_imports(
72
+ source: str,
73
+ file_path: str = "",
74
+ language: Optional[str] = None,
75
+ ) -> List[ImportReference]:
76
+ """
77
+ Extract import statements from source code across any supported Tier 1 language.
78
+ Dispatches to language-specific Tree-sitter or AST extractor based on file extension.
79
+ """
80
+ lang = language or detect_language(file_path) or "python"
81
+
82
+ if lang == "python":
83
+ return extract_python_imports(source, file_path=file_path)
84
+ elif lang in ("typescript", "javascript"):
85
+ return extract_typescript_imports(source, file_path=file_path)
86
+ elif lang == "go":
87
+ return extract_go_imports(source, file_path=file_path)
88
+ elif lang == "rust":
89
+ return extract_rust_imports(source, file_path=file_path)
90
+ else:
91
+ return extract_python_imports(source, file_path=file_path)
92
+
93
+
94
+ def validate_syntax(
95
+ source: str,
96
+ file_path: str = "",
97
+ language: Optional[str] = None,
98
+ ) -> Optional[str]:
99
+ """
100
+ Validate code syntax for any supported Tier 1 language.
101
+ Returns None if source is syntactically valid, or a descriptive error message if invalid.
102
+ """
103
+ lang = language or detect_language(file_path) or "python"
104
+
105
+ if lang == "python":
106
+ return validate_python_syntax(source, file_path=file_path)
107
+ elif lang in ("typescript", "javascript"):
108
+ return validate_typescript_syntax(source, file_path=file_path)
109
+ elif lang == "go":
110
+ return validate_go_syntax(source, file_path=file_path)
111
+ elif lang == "rust":
112
+ return validate_rust_syntax(source, file_path=file_path)
113
+ else:
114
+ return validate_python_syntax(source, file_path=file_path)
@@ -0,0 +1,127 @@
1
+ """
2
+ Common Tree-sitter AST utilities and helper functions for language extractors.
3
+ """
4
+
5
+ from typing import List, Optional
6
+ from tree_sitter import Node
7
+
8
+
9
+ def find_first_error(node: Node) -> Optional[Node]:
10
+ """Recursively locate the first syntax error node in a Tree-sitter AST."""
11
+ if node.is_error or node.type == "ERROR" or node.is_missing:
12
+ return node
13
+ for child in node.children:
14
+ if child.has_error:
15
+ err = find_first_error(child)
16
+ if err:
17
+ return err
18
+ return node if node.has_error else None
19
+
20
+
21
+ def format_syntax_error(root_node: Node, language: str) -> Optional[str]:
22
+ """Format a descriptive syntax error message if the AST contains errors."""
23
+ if not root_node.has_error:
24
+ return None
25
+
26
+ err_node = find_first_error(root_node)
27
+ if err_node is None:
28
+ return f"SyntaxError: Parsing failed for {language} source."
29
+
30
+ row = err_node.start_point.row + 1
31
+ col = err_node.start_point.column + 1
32
+ snippet = err_node.text.decode("utf-8", errors="ignore").strip().replace("\n", " ")
33
+ if len(snippet) > 40:
34
+ snippet = snippet[:37] + "..."
35
+ if not snippet:
36
+ snippet = err_node.type
37
+
38
+ return f"SyntaxError at line {row}:{col}: Unexpected {language} syntax near '{snippet}'"
39
+
40
+
41
+ def get_node_text(node: Node, source_bytes: bytes) -> str:
42
+ """Safely extract decoded text slice for a Tree-sitter node."""
43
+ return source_bytes[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
44
+
45
+
46
+ def extract_preceding_docstring(node: Node, source_bytes: bytes) -> Optional[str]:
47
+ """
48
+ Extract preceding docstring or doc-comment block for an AST node.
49
+ Inspects previous siblings for comment nodes (e.g. /** ... */, /// ..., or // ...).
50
+ """
51
+ target = node
52
+ # If wrapped in export statement or similar, check outer node
53
+ if target.parent and target.parent.type in (
54
+ "export_statement",
55
+ "export_default_statement",
56
+ "ambient_declaration",
57
+ ):
58
+ target = target.parent
59
+
60
+ comments: List[str] = []
61
+
62
+ # In tree-sitter, iterate backwards through parent's children to find adjacent comments
63
+ if target.parent:
64
+ children = target.parent.children
65
+ try:
66
+ idx = children.index(target)
67
+ i = idx - 1
68
+ while i >= 0:
69
+ prev_node = children[i]
70
+ if prev_node.type in ("comment", "line_comment", "block_comment"):
71
+ raw_text = get_node_text(prev_node, source_bytes)
72
+ comments.insert(0, raw_text)
73
+ i -= 1
74
+ elif prev_node.type in (",", ";", "\n"):
75
+ i -= 1
76
+ else:
77
+ break
78
+ except ValueError:
79
+ pass
80
+
81
+ if not comments:
82
+ curr = target.prev_named_sibling or target.prev_sibling
83
+ if curr and "comment" in curr.type:
84
+ comments.append(get_node_text(curr, source_bytes))
85
+
86
+ if not comments:
87
+ return None
88
+
89
+ cleaned_lines: List[str] = []
90
+ for c in comments:
91
+ c_str = c.strip()
92
+ if c_str.startswith("/*"):
93
+ # Block comment / JSDoc
94
+ c_str = c_str.removeprefix("/*").removesuffix("*/")
95
+ for line in c_str.splitlines():
96
+ line = line.strip()
97
+ if line.startswith("*"):
98
+ line = line[1:].strip()
99
+ if line:
100
+ cleaned_lines.append(line)
101
+ elif c_str.startswith("///"):
102
+ # Rust doc comment
103
+ for line in c_str.splitlines():
104
+ line = line.strip()
105
+ if line.startswith("///"):
106
+ line = line[3:].strip()
107
+ if line:
108
+ cleaned_lines.append(line)
109
+ elif c_str.startswith("//"):
110
+ # Line comment
111
+ for line in c_str.splitlines():
112
+ line = line.strip()
113
+ if line.startswith("//"):
114
+ line = line[2:].strip()
115
+ if line:
116
+ cleaned_lines.append(line)
117
+ elif c_str.startswith("#"):
118
+ # Python/shell comment
119
+ for line in c_str.splitlines():
120
+ line = line.strip()
121
+ if line.startswith("#"):
122
+ line = line[1:].strip()
123
+ if line:
124
+ cleaned_lines.append(line)
125
+
126
+ return "\n".join(cleaned_lines) if cleaned_lines else None
127
+
@@ -0,0 +1,395 @@
1
+ """
2
+ Go AST extractor using Tree-sitter.
3
+ Supports Go packages, structs, interfaces, methods, functions, and import declarations.
4
+ """
5
+
6
+ from typing import List, Optional, Tuple
7
+ from tree_sitter import Language, Node, Parser
8
+ import tree_sitter_go
9
+
10
+ from code_oracle.languages.common import extract_preceding_docstring, format_syntax_error, get_node_text
11
+ from code_oracle.models import CallReference, ImportReference, Parameter, Symbol
12
+
13
+ _GO_LANG = Language(tree_sitter_go.language())
14
+
15
+
16
+ def get_go_parser() -> Parser:
17
+ """Return a Tree-sitter parser configured for Go."""
18
+ return Parser(_GO_LANG)
19
+
20
+
21
+ def validate_go_syntax(source: str, file_path: str = "") -> Optional[str]:
22
+ """Validate Go source syntax, returning an error message if invalid."""
23
+ if not source.strip():
24
+ return None
25
+ parser = get_go_parser()
26
+ tree = parser.parse(source.encode("utf-8"))
27
+ return format_syntax_error(tree.root_node, "Go")
28
+
29
+
30
+ def _extract_go_parameters(params_node: Node, source_bytes: bytes) -> List[Parameter]:
31
+ """Extract parameters from a Go parameter_list node."""
32
+ params: List[Parameter] = []
33
+ if not params_node:
34
+ return params
35
+
36
+ for child in params_node.children:
37
+ if child.type in ("(", ")", ","):
38
+ continue
39
+
40
+ if child.type == "parameter_declaration":
41
+ # Form: 'a, b int' or 'host string'
42
+ type_node = child.child_by_field_name("type") or (
43
+ child.children[-1] if child.children else None
44
+ )
45
+ type_str = get_node_text(type_node, source_bytes) if type_node else None
46
+
47
+ # Collect identifier children
48
+ names = []
49
+ for sc in child.children:
50
+ if sc.type == "identifier":
51
+ names.append(get_node_text(sc, source_bytes))
52
+
53
+ if not names:
54
+ # Unnamed parameter e.g. func(int, string)
55
+ params.append(Parameter(name=f"arg{len(params)}", annotation=type_str))
56
+ else:
57
+ for name in names:
58
+ params.append(Parameter(name=name, annotation=type_str))
59
+
60
+ elif child.type == "variadic_parameter_declaration":
61
+ # Form: 'rest ...int'
62
+ name_node = child.child_by_field_name("name") or (
63
+ child.children[0] if child.children and child.children[0].type == "identifier" else None
64
+ )
65
+ param_name = get_node_text(name_node, source_bytes) if name_node else "rest"
66
+
67
+ type_node = child.child_by_field_name("type") or (
68
+ child.children[-1] if child.children else None
69
+ )
70
+ type_str = get_node_text(type_node, source_bytes) if type_node else None
71
+
72
+ params.append(
73
+ Parameter(
74
+ name=param_name,
75
+ annotation=type_str,
76
+ is_vararg=True,
77
+ )
78
+ )
79
+
80
+ return params
81
+
82
+
83
+ def _extract_go_calls(node: Node, source_bytes: bytes, caller_id: Optional[str] = None) -> List[CallReference]:
84
+ """Recursively extract function and method calls inside a Go AST node."""
85
+ calls: List[CallReference] = []
86
+
87
+ def walk(n: Node):
88
+ if n.type == "call_expression":
89
+ fn_node = n.child_by_field_name("function")
90
+ callee_name = get_node_text(fn_node, source_bytes).strip() if fn_node else ""
91
+
92
+ args_count = 0
93
+ has_vararg = False
94
+ args_node = n.child_by_field_name("arguments")
95
+ if args_node:
96
+ for arg in args_node.children:
97
+ if arg.type in ("(", ")", ",", "comment", "line_comment", "block_comment") or "comment" in arg.type:
98
+ continue
99
+ if arg.type == "...":
100
+ has_vararg = True
101
+ continue
102
+ args_count += 1
103
+ # Also check if last argument ends with ... (e.g. variadic slice expansion)
104
+ if arg.text.decode("utf-8", errors="ignore").endswith("..."):
105
+ has_vararg = True
106
+
107
+ if callee_name:
108
+ calls.append(
109
+ CallReference(
110
+ callee=callee_name,
111
+ args_count=args_count,
112
+ kwargs=[],
113
+ lineno=n.start_point.row + 1,
114
+ caller=caller_id,
115
+ has_vararg=has_vararg,
116
+ )
117
+ )
118
+
119
+ for child in n.children:
120
+ walk(child)
121
+
122
+ walk(node)
123
+ return calls
124
+
125
+
126
+ def extract_go_imports(source: str, file_path: str = "") -> List[ImportReference]:
127
+ """Extract all import declarations from Go source."""
128
+ if not source.strip():
129
+ return []
130
+
131
+ parser = get_go_parser()
132
+ source_bytes = source.encode("utf-8")
133
+ tree = parser.parse(source_bytes)
134
+
135
+ imports: List[ImportReference] = []
136
+
137
+ def walk(node: Node):
138
+ if node.type == "import_declaration":
139
+ for ch in node.children:
140
+ if ch.type == "import_spec":
141
+ _process_import_spec(ch)
142
+ elif ch.type == "import_spec_list":
143
+ for spec in ch.children:
144
+ if spec.type == "import_spec":
145
+ _process_import_spec(spec)
146
+ for child in node.children:
147
+ walk(child)
148
+
149
+ def _process_import_spec(spec_node: Node):
150
+ path_node = spec_node.child_by_field_name("path") or next(
151
+ (c for c in spec_node.children if c.type == "interpreted_string_literal"), None
152
+ )
153
+ if not path_node:
154
+ return
155
+
156
+ import_path = get_node_text(path_node, source_bytes).strip('"')
157
+ name_node = spec_node.child_by_field_name("name") or next(
158
+ (c for c in spec_node.children if c.type == "package_identifier"), None
159
+ )
160
+ asname = get_node_text(name_node, source_bytes) if name_node else None
161
+
162
+ # Go symbol name is either the alias or the package name (last segment)
163
+ pkg_name = asname if asname else import_path.split("/")[-1]
164
+
165
+ imports.append(
166
+ ImportReference(
167
+ module=import_path if "/" in import_path else None,
168
+ name=pkg_name,
169
+ asname=asname,
170
+ lineno=spec_node.start_point.row + 1,
171
+ file_path=file_path,
172
+ )
173
+ )
174
+
175
+ walk(tree.root_node)
176
+ return imports
177
+
178
+
179
+ def extract_go_symbols(source: str, file_path: str = "") -> List[Symbol]:
180
+ """Parse Go source into AST and extract symbol entities with detailed metadata."""
181
+ if not source.strip():
182
+ return []
183
+
184
+ parser = get_go_parser()
185
+ source_bytes = source.encode("utf-8")
186
+ tree = parser.parse(source_bytes)
187
+
188
+ symbols: List[Symbol] = []
189
+
190
+ for child in tree.root_node.children:
191
+ docstring = extract_preceding_docstring(child, source_bytes)
192
+ if child.type == "function_declaration":
193
+ name_node = child.child_by_field_name("name")
194
+ if not name_node:
195
+ continue
196
+ fn_name = get_node_text(name_node, source_bytes)
197
+ sym_id = f"{file_path}::{fn_name}"
198
+
199
+ params_node = child.child_by_field_name("parameters")
200
+ params = _extract_go_parameters(params_node, source_bytes) if params_node else []
201
+
202
+ res_node = child.child_by_field_name("result")
203
+ ret_type = get_node_text(res_node, source_bytes).strip() if res_node else None
204
+
205
+ pos_params = [p for p in params if not p.is_vararg]
206
+ min_args = len(pos_params)
207
+ max_args = None if any(p.is_vararg for p in params) else len(pos_params)
208
+
209
+ body_node = child.child_by_field_name("body")
210
+ calls = _extract_go_calls(body_node, source_bytes, caller_id=sym_id) if body_node else []
211
+
212
+ param_strs = [p.name + (f" {p.annotation}" if p.annotation else "") for p in params]
213
+ ret_suffix = f" {ret_type}" if ret_type else ""
214
+ signature = f"func {fn_name}({', '.join(param_strs)}){ret_suffix}"
215
+
216
+ is_exp = bool(fn_name and fn_name[0].isupper())
217
+ vis = "public" if is_exp else "internal"
218
+
219
+ symbols.append(
220
+ Symbol(
221
+ name=fn_name,
222
+ qualname=fn_name,
223
+ file_path=file_path,
224
+ kind="function",
225
+ lineno=child.start_point.row + 1,
226
+ end_lineno=child.end_point.row + 1,
227
+ signature=signature,
228
+ params=params,
229
+ min_args=min_args,
230
+ max_args=max_args,
231
+ return_type=ret_type,
232
+ calls=calls,
233
+ is_method=False,
234
+ is_static=False,
235
+ docstring=docstring,
236
+ is_exported=is_exp,
237
+ visibility=vis,
238
+ )
239
+ )
240
+
241
+ elif child.type == "method_declaration":
242
+ recv_node = child.child_by_field_name("receiver")
243
+ name_node = child.child_by_field_name("name")
244
+ if not recv_node or not name_node:
245
+ continue
246
+
247
+ method_name = get_node_text(name_node, source_bytes)
248
+
249
+ # Extract receiver type name
250
+ recv_type = "unknown"
251
+ recv_var = "recv"
252
+ for r_child in recv_node.children:
253
+ if r_child.type == "parameter_declaration":
254
+ for sc in r_child.children:
255
+ if sc.type == "identifier":
256
+ recv_var = get_node_text(sc, source_bytes)
257
+ elif sc.type == "pointer_type":
258
+ for ptr_c in sc.children:
259
+ if ptr_c.type == "type_identifier":
260
+ recv_type = get_node_text(ptr_c, source_bytes)
261
+ elif sc.type == "type_identifier":
262
+ recv_type = get_node_text(sc, source_bytes)
263
+
264
+ qualname = f"{recv_type}.{method_name}"
265
+ sym_id = f"{file_path}::{qualname}"
266
+
267
+ params_node = child.child_by_field_name("parameters")
268
+ formal_params = _extract_go_parameters(params_node, source_bytes) if params_node else []
269
+
270
+ # Prepend receiver parameter as params[0] for uniform method abstraction
271
+ receiver_param = Parameter(name=recv_var, annotation=recv_type)
272
+ params = [receiver_param] + formal_params
273
+
274
+ res_node = child.child_by_field_name("result")
275
+ ret_type = get_node_text(res_node, source_bytes).strip() if res_node else None
276
+
277
+ pos_params = [p for p in formal_params if not p.is_vararg]
278
+ min_args = len(pos_params)
279
+ max_args = None if any(p.is_vararg for p in formal_params) else len(pos_params)
280
+
281
+ body_node = child.child_by_field_name("body")
282
+ calls = _extract_go_calls(body_node, source_bytes, caller_id=sym_id) if body_node else []
283
+
284
+ param_strs = [p.name + (f" {p.annotation}" if p.annotation else "") for p in formal_params]
285
+ ret_suffix = f" {ret_type}" if ret_type else ""
286
+ signature = f"func ({recv_var} *{recv_type}) {method_name}({', '.join(param_strs)}){ret_suffix}"
287
+
288
+ is_exp = bool(method_name and method_name[0].isupper() and (not recv_type or recv_type == "unknown" or recv_type[0].isupper()))
289
+ vis = "public" if is_exp else "internal"
290
+
291
+ symbols.append(
292
+ Symbol(
293
+ name=method_name,
294
+ qualname=qualname,
295
+ file_path=file_path,
296
+ kind="method",
297
+ lineno=child.start_point.row + 1,
298
+ end_lineno=child.end_point.row + 1,
299
+ signature=signature,
300
+ params=params,
301
+ min_args=min_args,
302
+ max_args=max_args,
303
+ return_type=ret_type,
304
+ calls=calls,
305
+ is_method=True,
306
+ is_static=False,
307
+ docstring=docstring,
308
+ is_exported=is_exp,
309
+ visibility=vis,
310
+ )
311
+ )
312
+
313
+ elif child.type == "type_declaration":
314
+ for spec in child.children:
315
+ if spec.type == "type_spec":
316
+ name_node = spec.child_by_field_name("name")
317
+ type_node = spec.child_by_field_name("type")
318
+ if name_node:
319
+ t_name = get_node_text(name_node, source_bytes)
320
+ if type_node and type_node.type == "struct_type":
321
+ kind = "struct"
322
+ elif type_node and type_node.type == "interface_type":
323
+ kind = "interface"
324
+ else:
325
+ kind = "type_alias"
326
+
327
+ is_exp = bool(t_name and t_name[0].isupper())
328
+ vis = "public" if is_exp else "internal"
329
+
330
+ symbols.append(
331
+ Symbol(
332
+ name=t_name,
333
+ qualname=t_name,
334
+ file_path=file_path,
335
+ kind=kind,
336
+ lineno=child.start_point.row + 1,
337
+ end_lineno=child.end_point.row + 1,
338
+ signature=f"type {t_name} {kind}",
339
+ docstring=docstring,
340
+ is_exported=is_exp,
341
+ visibility=vis,
342
+ )
343
+ )
344
+
345
+ elif child.type in ("const_declaration", "var_declaration"):
346
+ is_const = child.type == "const_declaration"
347
+ kind = "constant" if is_const else "variable"
348
+ spec_type = "const_spec" if is_const else "var_spec"
349
+ for spec in child.children:
350
+ if spec.type == spec_type:
351
+ for sc in spec.children:
352
+ if sc.type == "identifier":
353
+ v_name = get_node_text(sc, source_bytes)
354
+ is_exp = bool(v_name and v_name[0].isupper())
355
+ vis = "public" if is_exp else "internal"
356
+ symbols.append(
357
+ Symbol(
358
+ name=v_name,
359
+ qualname=v_name,
360
+ file_path=file_path,
361
+ kind=kind,
362
+ lineno=sc.start_point.row + 1,
363
+ end_lineno=sc.end_point.row + 1,
364
+ signature=f"{'const' if is_const else 'var'} {v_name}",
365
+ min_args=0,
366
+ max_args=0,
367
+ docstring=docstring,
368
+ is_exported=is_exp,
369
+ visibility=vis,
370
+ )
371
+ )
372
+
373
+ # Extract package / module calls
374
+ all_calls = _extract_go_calls(tree.root_node, source_bytes, caller_id=f"{file_path}::<module>")
375
+ module_calls = [
376
+ c for c in all_calls
377
+ if not any(s.lineno <= c.lineno <= s.end_lineno for s in symbols if s.kind != "module")
378
+ ]
379
+ if module_calls:
380
+ line_count = len(source.splitlines()) or 1
381
+ module_sym = Symbol(
382
+ name="<module>",
383
+ qualname="<module>",
384
+ file_path=file_path,
385
+ kind="module",
386
+ lineno=1,
387
+ end_lineno=line_count,
388
+ signature=f"// package {file_path}",
389
+ calls=module_calls,
390
+ is_exported=True,
391
+ visibility="public",
392
+ )
393
+ symbols.append(module_sym)
394
+
395
+ return symbols