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.
- code_oracle/__init__.py +30 -0
- code_oracle/cli.py +795 -0
- code_oracle/config.py +145 -0
- code_oracle/dataset.py +5325 -0
- code_oracle/dead_code/__init__.py +32 -0
- code_oracle/dead_code/detector.py +379 -0
- code_oracle/dead_code/entrypoints.py +333 -0
- code_oracle/dead_code/models.py +255 -0
- code_oracle/dead_code/semantics.py +416 -0
- code_oracle/decision.py +906 -0
- code_oracle/engine.py +430 -0
- code_oracle/export_onnx.py +436 -0
- code_oracle/hook.py +531 -0
- code_oracle/indexer.py +894 -0
- code_oracle/languages/__init__.py +114 -0
- code_oracle/languages/common.py +127 -0
- code_oracle/languages/go.py +395 -0
- code_oracle/languages/python.py +336 -0
- code_oracle/languages/rust.py +474 -0
- code_oracle/languages/typescript.py +775 -0
- code_oracle/linearizer.py +166 -0
- code_oracle/locator.py +301 -0
- code_oracle/models.py +237 -0
- code_oracle/perf_lint/__init__.py +38 -0
- code_oracle/perf_lint/engine.py +234 -0
- code_oracle/perf_lint/models.py +229 -0
- code_oracle/perf_lint/rules/__init__.py +31 -0
- code_oracle/perf_lint/rules/async_blocking.py +143 -0
- code_oracle/perf_lint/rules/n_plus_one.py +232 -0
- code_oracle/perf_lint/rules/nested_loops.py +137 -0
- code_oracle/perf_lint/rules/unclosed_res.py +494 -0
- code_oracle/perf_lint/visitor.py +299 -0
- code_oracle/server.py +184 -0
- code_oracle/slicer.py +225 -0
- code_oracle/symbolic.py +459 -0
- code_oracle-0.1.0.dist-info/METADATA +225 -0
- code_oracle-0.1.0.dist-info/RECORD +40 -0
- code_oracle-0.1.0.dist-info/WHEEL +4 -0
- code_oracle-0.1.0.dist-info/entry_points.txt +2 -0
- 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
|