cicada-mcp 0.1.4__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.
Potentially problematic release.
This version of cicada-mcp might be problematic. Click here for more details.
- cicada/__init__.py +30 -0
- cicada/clean.py +297 -0
- cicada/command_logger.py +293 -0
- cicada/dead_code_analyzer.py +282 -0
- cicada/extractors/__init__.py +36 -0
- cicada/extractors/base.py +66 -0
- cicada/extractors/call.py +176 -0
- cicada/extractors/dependency.py +361 -0
- cicada/extractors/doc.py +179 -0
- cicada/extractors/function.py +246 -0
- cicada/extractors/module.py +123 -0
- cicada/extractors/spec.py +151 -0
- cicada/find_dead_code.py +270 -0
- cicada/formatter.py +918 -0
- cicada/git_helper.py +646 -0
- cicada/indexer.py +629 -0
- cicada/install.py +724 -0
- cicada/keyword_extractor.py +364 -0
- cicada/keyword_search.py +553 -0
- cicada/lightweight_keyword_extractor.py +298 -0
- cicada/mcp_server.py +1559 -0
- cicada/mcp_tools.py +291 -0
- cicada/parser.py +124 -0
- cicada/pr_finder.py +435 -0
- cicada/pr_indexer/__init__.py +20 -0
- cicada/pr_indexer/cli.py +62 -0
- cicada/pr_indexer/github_api_client.py +431 -0
- cicada/pr_indexer/indexer.py +297 -0
- cicada/pr_indexer/line_mapper.py +209 -0
- cicada/pr_indexer/pr_index_builder.py +253 -0
- cicada/setup.py +339 -0
- cicada/utils/__init__.py +52 -0
- cicada/utils/call_site_formatter.py +95 -0
- cicada/utils/function_grouper.py +57 -0
- cicada/utils/hash_utils.py +173 -0
- cicada/utils/index_utils.py +290 -0
- cicada/utils/path_utils.py +240 -0
- cicada/utils/signature_builder.py +106 -0
- cicada/utils/storage.py +111 -0
- cicada/utils/subprocess_runner.py +182 -0
- cicada/utils/text_utils.py +90 -0
- cicada/version_check.py +116 -0
- cicada_mcp-0.1.4.dist-info/METADATA +619 -0
- cicada_mcp-0.1.4.dist-info/RECORD +48 -0
- cicada_mcp-0.1.4.dist-info/WHEEL +5 -0
- cicada_mcp-0.1.4.dist-info/entry_points.txt +8 -0
- cicada_mcp-0.1.4.dist-info/licenses/LICENSE +21 -0
- cicada_mcp-0.1.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Function extraction logic.
|
|
3
|
+
|
|
4
|
+
Author: Cursor(Auto)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .base import get_param_name
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def extract_functions(node, source_code: bytes) -> list:
|
|
11
|
+
"""Extract all function definitions from a module body."""
|
|
12
|
+
functions = []
|
|
13
|
+
_find_functions_recursive(node, source_code, functions)
|
|
14
|
+
return functions
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _extract_impl_from_prev_sibling(node, source_code: bytes):
|
|
18
|
+
"""
|
|
19
|
+
Extract @impl value from previous sibling if it's an @impl attribute.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
- True if @impl true
|
|
23
|
+
- Module name (str) if @impl ModuleName
|
|
24
|
+
- None if not an @impl attribute
|
|
25
|
+
"""
|
|
26
|
+
if node is None or node.type != "unary_operator":
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
# Check if this is an @ operator
|
|
30
|
+
is_at_operator = False
|
|
31
|
+
impl_call = None
|
|
32
|
+
|
|
33
|
+
for child in node.children:
|
|
34
|
+
if child.type == "@":
|
|
35
|
+
is_at_operator = True
|
|
36
|
+
elif child.type == "call" and is_at_operator:
|
|
37
|
+
impl_call = child
|
|
38
|
+
break
|
|
39
|
+
|
|
40
|
+
if not impl_call:
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
# Check if the call is "impl"
|
|
44
|
+
identifier_text = None
|
|
45
|
+
arguments_node = None
|
|
46
|
+
|
|
47
|
+
for child in impl_call.children:
|
|
48
|
+
if child.type == "identifier":
|
|
49
|
+
identifier_text = source_code[child.start_byte : child.end_byte].decode(
|
|
50
|
+
"utf-8"
|
|
51
|
+
)
|
|
52
|
+
elif child.type == "arguments":
|
|
53
|
+
arguments_node = child
|
|
54
|
+
|
|
55
|
+
if identifier_text != "impl":
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
# Extract the impl value from arguments
|
|
59
|
+
if arguments_node:
|
|
60
|
+
for arg_child in arguments_node.children:
|
|
61
|
+
if arg_child.type == "boolean":
|
|
62
|
+
# @impl true or @impl false
|
|
63
|
+
bool_text = source_code[
|
|
64
|
+
arg_child.start_byte : arg_child.end_byte
|
|
65
|
+
].decode("utf-8")
|
|
66
|
+
return bool_text == "true"
|
|
67
|
+
elif arg_child.type == "alias":
|
|
68
|
+
# @impl ModuleName
|
|
69
|
+
module_name = source_code[
|
|
70
|
+
arg_child.start_byte : arg_child.end_byte
|
|
71
|
+
].decode("utf-8")
|
|
72
|
+
return module_name
|
|
73
|
+
|
|
74
|
+
# @impl without arguments defaults to true
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _find_functions_recursive(node, source_code: bytes, functions: list):
|
|
79
|
+
"""Recursively find def and defp declarations."""
|
|
80
|
+
# Track previous sibling to detect @impl attributes
|
|
81
|
+
prev_sibling = None
|
|
82
|
+
|
|
83
|
+
# Iterate through children to process siblings
|
|
84
|
+
for child in node.children:
|
|
85
|
+
# Check if this child is a function call (def or defp)
|
|
86
|
+
if child.type == "call":
|
|
87
|
+
# Get the target (function name)
|
|
88
|
+
target = None
|
|
89
|
+
arguments = None
|
|
90
|
+
|
|
91
|
+
for call_child in child.children:
|
|
92
|
+
if call_child.type == "identifier":
|
|
93
|
+
target = call_child
|
|
94
|
+
elif call_child.type == "arguments":
|
|
95
|
+
arguments = call_child
|
|
96
|
+
|
|
97
|
+
# Check if this is a def or defp call
|
|
98
|
+
if target and arguments:
|
|
99
|
+
target_text = source_code[target.start_byte : target.end_byte].decode(
|
|
100
|
+
"utf-8"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if target_text in ["def", "defp"]:
|
|
104
|
+
# Check if previous sibling is @impl
|
|
105
|
+
impl_value = _extract_impl_from_prev_sibling(
|
|
106
|
+
prev_sibling, source_code
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Extract function name and arity
|
|
110
|
+
func_info = _parse_function_definition(
|
|
111
|
+
arguments, source_code, target_text, child.start_point[0] + 1
|
|
112
|
+
)
|
|
113
|
+
if func_info:
|
|
114
|
+
# Add impl attribute if present
|
|
115
|
+
if impl_value is not None:
|
|
116
|
+
func_info["impl"] = impl_value
|
|
117
|
+
else:
|
|
118
|
+
func_info["impl"] = False
|
|
119
|
+
functions.append(func_info)
|
|
120
|
+
prev_sibling = child
|
|
121
|
+
continue # Don't recurse into function body
|
|
122
|
+
|
|
123
|
+
# Recursively process this child
|
|
124
|
+
_find_functions_recursive(child, source_code, functions)
|
|
125
|
+
prev_sibling = child
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _parse_function_definition(
|
|
129
|
+
arguments_node, source_code: bytes, func_type: str, line: int
|
|
130
|
+
) -> dict | None:
|
|
131
|
+
"""Parse a function definition to extract name, arity, argument names, and guards."""
|
|
132
|
+
func_name = None
|
|
133
|
+
arity = 0
|
|
134
|
+
arg_names = []
|
|
135
|
+
guards = []
|
|
136
|
+
|
|
137
|
+
for arg_child in arguments_node.children:
|
|
138
|
+
# The function signature can be either:
|
|
139
|
+
# 1. A call node (function with params): func_name(param1, param2)
|
|
140
|
+
# 2. An identifier (function with no params): func_name
|
|
141
|
+
# 3. A binary_operator (when guards are present): func_name(params) when guard
|
|
142
|
+
if arg_child.type == "call":
|
|
143
|
+
# Extract function name from call target
|
|
144
|
+
for call_child in arg_child.children:
|
|
145
|
+
if call_child.type == "identifier":
|
|
146
|
+
func_name = source_code[
|
|
147
|
+
call_child.start_byte : call_child.end_byte
|
|
148
|
+
].decode("utf-8")
|
|
149
|
+
elif call_child.type == "arguments":
|
|
150
|
+
arg_names = _extract_argument_names(call_child, source_code)
|
|
151
|
+
arity = len(arg_names)
|
|
152
|
+
break
|
|
153
|
+
elif arg_child.type == "binary_operator":
|
|
154
|
+
# This handles guards: func_name(params) when guard_expr
|
|
155
|
+
# The binary_operator contains the call as its first child
|
|
156
|
+
for op_child in arg_child.children:
|
|
157
|
+
if op_child.type == "call":
|
|
158
|
+
# Extract function name and args from the call
|
|
159
|
+
for call_child in op_child.children:
|
|
160
|
+
if call_child.type == "identifier":
|
|
161
|
+
func_name = source_code[
|
|
162
|
+
call_child.start_byte : call_child.end_byte
|
|
163
|
+
].decode("utf-8")
|
|
164
|
+
elif call_child.type == "arguments":
|
|
165
|
+
arg_names = _extract_argument_names(call_child, source_code)
|
|
166
|
+
arity = len(arg_names)
|
|
167
|
+
break
|
|
168
|
+
break
|
|
169
|
+
elif arg_child.type == "identifier":
|
|
170
|
+
func_name = source_code[arg_child.start_byte : arg_child.end_byte].decode(
|
|
171
|
+
"utf-8"
|
|
172
|
+
)
|
|
173
|
+
arity = 0
|
|
174
|
+
arg_names = []
|
|
175
|
+
break
|
|
176
|
+
|
|
177
|
+
# Extract guard clauses
|
|
178
|
+
guards = _extract_guards(arguments_node, source_code)
|
|
179
|
+
|
|
180
|
+
if func_name:
|
|
181
|
+
return {
|
|
182
|
+
"name": func_name,
|
|
183
|
+
"arity": arity,
|
|
184
|
+
"args": arg_names,
|
|
185
|
+
"guards": guards,
|
|
186
|
+
"full_name": f"{func_name}/{arity}",
|
|
187
|
+
"line": line,
|
|
188
|
+
"signature": f"{func_type} {func_name}",
|
|
189
|
+
"type": func_type,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _extract_guards(arguments_node, source_code: bytes) -> list[str]:
|
|
196
|
+
"""
|
|
197
|
+
Extract guard clauses from function definition arguments.
|
|
198
|
+
|
|
199
|
+
Example:
|
|
200
|
+
def abs_value(n) when n < 0, do: -n
|
|
201
|
+
Returns: ["n < 0"]
|
|
202
|
+
|
|
203
|
+
Tree structure:
|
|
204
|
+
arguments:
|
|
205
|
+
binary_operator: # This contains function_call WHEN guard_expr
|
|
206
|
+
call: abs_value(n)
|
|
207
|
+
when: 'when'
|
|
208
|
+
binary_operator: n < 0 # This is the guard expression
|
|
209
|
+
"""
|
|
210
|
+
guards = []
|
|
211
|
+
|
|
212
|
+
for arg_child in arguments_node.children:
|
|
213
|
+
# Guards appear as binary_operator nodes containing 'when'
|
|
214
|
+
if arg_child.type == "binary_operator":
|
|
215
|
+
# Look for 'when' keyword and the guard expression after it
|
|
216
|
+
has_when = False
|
|
217
|
+
|
|
218
|
+
for op_child in arg_child.children:
|
|
219
|
+
if op_child.type == "when":
|
|
220
|
+
has_when = True
|
|
221
|
+
elif has_when:
|
|
222
|
+
# This is the guard expression node (comes after 'when')
|
|
223
|
+
# It's typically a binary_operator (like n < 0)
|
|
224
|
+
guard_expr = source_code[
|
|
225
|
+
op_child.start_byte : op_child.end_byte
|
|
226
|
+
].decode("utf-8")
|
|
227
|
+
guards.append(guard_expr)
|
|
228
|
+
break
|
|
229
|
+
|
|
230
|
+
return guards
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _extract_argument_names(params_node, source_code: bytes) -> list[str]:
|
|
234
|
+
"""Extract parameter names from function arguments."""
|
|
235
|
+
arg_names = []
|
|
236
|
+
|
|
237
|
+
for child in params_node.children:
|
|
238
|
+
if child.type in [",", "(", ")", "[", "]"]:
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
# Extract the argument name (simplified - handles basic cases)
|
|
242
|
+
arg_name = get_param_name(child, source_code)
|
|
243
|
+
if arg_name:
|
|
244
|
+
arg_names.append(arg_name)
|
|
245
|
+
|
|
246
|
+
return arg_names
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module extraction logic.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .base import extract_string_from_arguments
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def extract_modules(root_node, source_code: bytes) -> list:
|
|
9
|
+
"""Extract all modules from the syntax tree."""
|
|
10
|
+
modules = []
|
|
11
|
+
_find_modules_recursive(root_node, source_code, modules)
|
|
12
|
+
return modules
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _find_modules_recursive(node, source_code: bytes, modules: list):
|
|
16
|
+
"""Recursively find defmodule declarations."""
|
|
17
|
+
# Check if this node is a function call (defmodule)
|
|
18
|
+
if node.type == "call":
|
|
19
|
+
# Get the target, arguments, and do_block (all siblings)
|
|
20
|
+
target = None
|
|
21
|
+
arguments = None
|
|
22
|
+
do_block = None
|
|
23
|
+
|
|
24
|
+
for child in node.children:
|
|
25
|
+
if child.type == "identifier":
|
|
26
|
+
target = child
|
|
27
|
+
elif child.type == "arguments":
|
|
28
|
+
arguments = child
|
|
29
|
+
elif child.type == "do_block":
|
|
30
|
+
do_block = child
|
|
31
|
+
|
|
32
|
+
# Check if this is a defmodule call
|
|
33
|
+
if target and arguments:
|
|
34
|
+
target_text = source_code[target.start_byte : target.end_byte].decode(
|
|
35
|
+
"utf-8"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if target_text == "defmodule":
|
|
39
|
+
# Extract module name from arguments
|
|
40
|
+
module_name = None
|
|
41
|
+
|
|
42
|
+
for arg_child in arguments.children:
|
|
43
|
+
if arg_child.type == "alias":
|
|
44
|
+
module_name = source_code[
|
|
45
|
+
arg_child.start_byte : arg_child.end_byte
|
|
46
|
+
].decode("utf-8")
|
|
47
|
+
break
|
|
48
|
+
|
|
49
|
+
if module_name and do_block:
|
|
50
|
+
module_info = {
|
|
51
|
+
"module": module_name,
|
|
52
|
+
"line": node.start_point[0] + 1,
|
|
53
|
+
"moduledoc": extract_moduledoc(do_block, source_code),
|
|
54
|
+
"do_block": do_block, # Store for further extraction
|
|
55
|
+
}
|
|
56
|
+
modules.append(module_info)
|
|
57
|
+
return # Don't recurse into module body
|
|
58
|
+
|
|
59
|
+
# Recursively process children
|
|
60
|
+
for child in node.children:
|
|
61
|
+
_find_modules_recursive(child, source_code, modules)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def extract_moduledoc(node, source_code: bytes) -> str | None:
|
|
65
|
+
"""Extract the @moduledoc attribute from a module's do_block."""
|
|
66
|
+
return _find_moduledoc_recursive(node, source_code)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _find_moduledoc_recursive(node, source_code: bytes) -> str | None:
|
|
70
|
+
"""Recursively search for @moduledoc attribute."""
|
|
71
|
+
# Look for unary_operator nodes (which represent @ attributes)
|
|
72
|
+
if node.type == "unary_operator":
|
|
73
|
+
operator = None
|
|
74
|
+
operand = None
|
|
75
|
+
|
|
76
|
+
for child in node.children:
|
|
77
|
+
if child.type == "@":
|
|
78
|
+
operator = child
|
|
79
|
+
elif child.type == "call":
|
|
80
|
+
# @moduledoc "..." is represented as a call
|
|
81
|
+
operand = child
|
|
82
|
+
|
|
83
|
+
if operator and operand:
|
|
84
|
+
# Check if this is a moduledoc attribute
|
|
85
|
+
for call_child in operand.children:
|
|
86
|
+
if call_child.type == "identifier":
|
|
87
|
+
attr_name = source_code[
|
|
88
|
+
call_child.start_byte : call_child.end_byte
|
|
89
|
+
].decode("utf-8")
|
|
90
|
+
|
|
91
|
+
if attr_name == "moduledoc":
|
|
92
|
+
# Extract the documentation string from the arguments
|
|
93
|
+
for arg_child in operand.children:
|
|
94
|
+
if arg_child.type == "arguments":
|
|
95
|
+
doc_string = extract_string_from_arguments(
|
|
96
|
+
arg_child, source_code
|
|
97
|
+
)
|
|
98
|
+
if doc_string:
|
|
99
|
+
return doc_string
|
|
100
|
+
|
|
101
|
+
# Recursively search children (only in the immediate do_block, not nested modules)
|
|
102
|
+
for child in node.children:
|
|
103
|
+
# Don't recurse into nested defmodule
|
|
104
|
+
if child.type == "call":
|
|
105
|
+
# Check if it's a defmodule
|
|
106
|
+
is_defmodule = False
|
|
107
|
+
for call_child in child.children:
|
|
108
|
+
if call_child.type == "identifier":
|
|
109
|
+
target_text = source_code[
|
|
110
|
+
call_child.start_byte : call_child.end_byte
|
|
111
|
+
].decode("utf-8")
|
|
112
|
+
if target_text == "defmodule":
|
|
113
|
+
is_defmodule = True
|
|
114
|
+
break
|
|
115
|
+
|
|
116
|
+
if is_defmodule:
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
result = _find_moduledoc_recursive(child, source_code)
|
|
120
|
+
if result:
|
|
121
|
+
return result
|
|
122
|
+
|
|
123
|
+
return None
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Type spec extraction logic.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def extract_specs(node, source_code: bytes) -> dict:
|
|
7
|
+
"""Extract all @spec attributes from a module body."""
|
|
8
|
+
specs = {}
|
|
9
|
+
_find_specs_recursive(node, source_code, specs)
|
|
10
|
+
return specs
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _find_specs_recursive(node, source_code: bytes, specs: dict):
|
|
14
|
+
"""Recursively find @spec declarations."""
|
|
15
|
+
# Look for unary_operator nodes (which represent @ attributes)
|
|
16
|
+
if node.type == "unary_operator":
|
|
17
|
+
operator = None
|
|
18
|
+
operand = None
|
|
19
|
+
|
|
20
|
+
for child in node.children:
|
|
21
|
+
if child.type == "@":
|
|
22
|
+
operator = child
|
|
23
|
+
elif child.type == "call":
|
|
24
|
+
operand = child
|
|
25
|
+
|
|
26
|
+
if operator and operand:
|
|
27
|
+
# Check if this is a spec attribute
|
|
28
|
+
for call_child in operand.children:
|
|
29
|
+
if call_child.type == "identifier":
|
|
30
|
+
attr_name = source_code[
|
|
31
|
+
call_child.start_byte : call_child.end_byte
|
|
32
|
+
].decode("utf-8")
|
|
33
|
+
|
|
34
|
+
if attr_name == "spec":
|
|
35
|
+
# Extract the spec definition
|
|
36
|
+
spec_info = _parse_spec(operand, source_code)
|
|
37
|
+
if spec_info:
|
|
38
|
+
key = f"{spec_info['name']}/{spec_info['arity']}"
|
|
39
|
+
specs[key] = spec_info
|
|
40
|
+
|
|
41
|
+
# Recursively search children
|
|
42
|
+
for child in node.children:
|
|
43
|
+
# Don't recurse into nested defmodule or function definitions
|
|
44
|
+
if child.type == "call":
|
|
45
|
+
is_defmodule_or_def = False
|
|
46
|
+
for call_child in child.children:
|
|
47
|
+
if call_child.type == "identifier":
|
|
48
|
+
target_text = source_code[
|
|
49
|
+
call_child.start_byte : call_child.end_byte
|
|
50
|
+
].decode("utf-8")
|
|
51
|
+
if target_text in ["defmodule", "def", "defp"]:
|
|
52
|
+
is_defmodule_or_def = True
|
|
53
|
+
break
|
|
54
|
+
|
|
55
|
+
if is_defmodule_or_def:
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
_find_specs_recursive(child, source_code, specs)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _parse_spec(spec_node, source_code: bytes) -> dict | None:
|
|
62
|
+
"""Parse a @spec attribute to extract function name, arity, parameter types, and return type."""
|
|
63
|
+
# @spec is represented as: spec(function_signature)
|
|
64
|
+
# We need to find the arguments node and parse the typespec
|
|
65
|
+
|
|
66
|
+
for child in spec_node.children:
|
|
67
|
+
if child.type == "arguments":
|
|
68
|
+
# The typespec is in the arguments
|
|
69
|
+
for arg in child.children:
|
|
70
|
+
if arg.type == "binary_operator":
|
|
71
|
+
# This is the :: operator separating params from return type
|
|
72
|
+
# Left side has the function call with params
|
|
73
|
+
# Right side has the return type
|
|
74
|
+
func_call = None
|
|
75
|
+
return_type = None
|
|
76
|
+
found_call = False
|
|
77
|
+
|
|
78
|
+
for op_child in arg.children:
|
|
79
|
+
if op_child.type == "call":
|
|
80
|
+
func_call = op_child
|
|
81
|
+
found_call = True
|
|
82
|
+
elif found_call and op_child.type not in ["::", "operator"]:
|
|
83
|
+
# This is the return type node (after :: operator)
|
|
84
|
+
return_type = source_code[
|
|
85
|
+
op_child.start_byte : op_child.end_byte
|
|
86
|
+
].decode("utf-8")
|
|
87
|
+
|
|
88
|
+
if func_call:
|
|
89
|
+
func_name = None
|
|
90
|
+
param_types = []
|
|
91
|
+
|
|
92
|
+
for fc_child in func_call.children:
|
|
93
|
+
if fc_child.type == "identifier":
|
|
94
|
+
func_name = source_code[
|
|
95
|
+
fc_child.start_byte : fc_child.end_byte
|
|
96
|
+
].decode("utf-8")
|
|
97
|
+
elif fc_child.type == "arguments":
|
|
98
|
+
param_types = _extract_param_types(
|
|
99
|
+
fc_child, source_code
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if func_name:
|
|
103
|
+
return {
|
|
104
|
+
"name": func_name,
|
|
105
|
+
"arity": len(param_types),
|
|
106
|
+
"param_types": param_types,
|
|
107
|
+
"return_type": return_type,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _extract_param_types(params_node, source_code: bytes) -> list[str]:
|
|
114
|
+
"""Extract parameter type strings from @spec arguments."""
|
|
115
|
+
param_types = []
|
|
116
|
+
|
|
117
|
+
for child in params_node.children:
|
|
118
|
+
if child.type in [",", "(", ")", "[", "]"]:
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
# Get the type as a string
|
|
122
|
+
type_str = source_code[child.start_byte : child.end_byte].decode("utf-8")
|
|
123
|
+
param_types.append(type_str)
|
|
124
|
+
|
|
125
|
+
return param_types
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def match_specs_to_functions(functions: list, specs: dict) -> list:
|
|
129
|
+
"""Match specs with functions and add type information to function args and return type."""
|
|
130
|
+
for func in functions:
|
|
131
|
+
key = f"{func['name']}/{func['arity']}"
|
|
132
|
+
if key in specs:
|
|
133
|
+
spec = specs[key]
|
|
134
|
+
# Add types to arguments
|
|
135
|
+
if "args" in func and "param_types" in spec:
|
|
136
|
+
# Create args_with_types list
|
|
137
|
+
args_with_types = []
|
|
138
|
+
for i, arg_name in enumerate(func["args"]):
|
|
139
|
+
if i < len(spec["param_types"]):
|
|
140
|
+
args_with_types.append(
|
|
141
|
+
{"name": arg_name, "type": spec["param_types"][i]}
|
|
142
|
+
)
|
|
143
|
+
else:
|
|
144
|
+
args_with_types.append({"name": arg_name, "type": None})
|
|
145
|
+
func["args_with_types"] = args_with_types
|
|
146
|
+
|
|
147
|
+
# Add return type from spec
|
|
148
|
+
if "return_type" in spec and spec["return_type"]:
|
|
149
|
+
func["return_type"] = spec["return_type"]
|
|
150
|
+
|
|
151
|
+
return functions
|