cicada-mcp 0.2.0__py3-none-any.whl → 0.3.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.
- cicada/_version_hash.py +4 -0
- cicada/cli.py +6 -748
- cicada/commands.py +1255 -0
- cicada/dead_code/__init__.py +1 -0
- cicada/{find_dead_code.py → dead_code/finder.py} +2 -1
- cicada/dependency_analyzer.py +147 -0
- cicada/entry_utils.py +92 -0
- cicada/extractors/base.py +9 -9
- cicada/extractors/call.py +17 -20
- cicada/extractors/common.py +64 -0
- cicada/extractors/dependency.py +117 -235
- cicada/extractors/doc.py +2 -49
- cicada/extractors/function.py +10 -14
- cicada/extractors/keybert.py +228 -0
- cicada/extractors/keyword.py +191 -0
- cicada/extractors/module.py +6 -10
- cicada/extractors/spec.py +8 -56
- cicada/format/__init__.py +20 -0
- cicada/{ascii_art.py → format/ascii_art.py} +1 -1
- cicada/format/formatter.py +1145 -0
- cicada/git_helper.py +134 -7
- cicada/indexer.py +322 -89
- cicada/interactive_setup.py +251 -323
- cicada/interactive_setup_helpers.py +302 -0
- cicada/keyword_expander.py +437 -0
- cicada/keyword_search.py +208 -422
- cicada/keyword_test.py +383 -16
- cicada/mcp/__init__.py +10 -0
- cicada/mcp/entry.py +17 -0
- cicada/mcp/filter_utils.py +107 -0
- cicada/mcp/pattern_utils.py +118 -0
- cicada/{mcp_server.py → mcp/server.py} +819 -73
- cicada/mcp/tools.py +473 -0
- cicada/pr_finder.py +2 -3
- cicada/pr_indexer/indexer.py +3 -2
- cicada/setup.py +167 -35
- cicada/tier.py +225 -0
- cicada/utils/__init__.py +9 -2
- cicada/utils/fuzzy_match.py +54 -0
- cicada/utils/index_utils.py +9 -0
- cicada/utils/path_utils.py +18 -0
- cicada/utils/text_utils.py +52 -1
- cicada/utils/tree_utils.py +47 -0
- cicada/version_check.py +99 -0
- cicada/watch_manager.py +320 -0
- cicada/watcher.py +431 -0
- cicada_mcp-0.3.0.dist-info/METADATA +541 -0
- cicada_mcp-0.3.0.dist-info/RECORD +70 -0
- cicada_mcp-0.3.0.dist-info/entry_points.txt +4 -0
- cicada/formatter.py +0 -864
- cicada/keybert_extractor.py +0 -286
- cicada/lightweight_keyword_extractor.py +0 -290
- cicada/mcp_entry.py +0 -683
- cicada/mcp_tools.py +0 -291
- cicada_mcp-0.2.0.dist-info/METADATA +0 -735
- cicada_mcp-0.2.0.dist-info/RECORD +0 -53
- cicada_mcp-0.2.0.dist-info/entry_points.txt +0 -4
- /cicada/{dead_code_analyzer.py → dead_code/analyzer.py} +0 -0
- /cicada/{colors.py → format/colors.py} +0 -0
- {cicada_mcp-0.2.0.dist-info → cicada_mcp-0.3.0.dist-info}/WHEEL +0 -0
- {cicada_mcp-0.2.0.dist-info → cicada_mcp-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {cicada_mcp-0.2.0.dist-info → cicada_mcp-0.3.0.dist-info}/top_level.txt +0 -0
cicada/extractors/dependency.py
CHANGED
|
@@ -4,53 +4,49 @@ Dependency extraction logic (alias, import, require, use).
|
|
|
4
4
|
Author: Cursor(Auto)
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
from cicada.utils import extract_text_from_node
|
|
8
|
+
|
|
9
|
+
from .common import _find_nodes_recursive
|
|
10
|
+
|
|
7
11
|
|
|
8
12
|
def extract_aliases(node, source_code: bytes) -> dict:
|
|
9
13
|
"""Extract all alias declarations from a module body."""
|
|
10
|
-
aliases =
|
|
14
|
+
aliases = []
|
|
11
15
|
_find_aliases_recursive(node, source_code, aliases)
|
|
12
|
-
return aliases
|
|
13
16
|
|
|
17
|
+
result = {}
|
|
18
|
+
for alias in aliases:
|
|
19
|
+
if alias:
|
|
20
|
+
result.update(alias)
|
|
21
|
+
return result
|
|
14
22
|
|
|
15
|
-
def _find_aliases_recursive(node, source_code: bytes, aliases: dict):
|
|
16
|
-
"""Recursively find alias declarations."""
|
|
17
|
-
if node.type == "call":
|
|
18
|
-
target = None
|
|
19
|
-
arguments = None
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
arguments = child
|
|
24
|
+
def _parse_alias_call(node, source_code: bytes) -> dict | None:
|
|
25
|
+
"""Parse an alias call and return the alias information."""
|
|
26
|
+
target = None
|
|
27
|
+
arguments = None
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
for child in node.children:
|
|
30
|
+
if child.type == "identifier":
|
|
31
|
+
target = child
|
|
32
|
+
elif child.type == "arguments":
|
|
33
|
+
arguments = child
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
alias_info = _parse_alias(arguments, source_code)
|
|
33
|
-
if alias_info:
|
|
34
|
-
# alias_info is a dict of {short_name: full_name}
|
|
35
|
-
aliases.update(alias_info)
|
|
35
|
+
if target and arguments:
|
|
36
|
+
target_text = extract_text_from_node(target, source_code)
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"utf-8"
|
|
45
|
-
)
|
|
46
|
-
if target_text in ["def", "defp", "defmodule"]:
|
|
47
|
-
is_function_def = True
|
|
48
|
-
break
|
|
38
|
+
if target_text == "alias":
|
|
39
|
+
# Parse the alias
|
|
40
|
+
alias_info = _parse_alias(arguments, source_code)
|
|
41
|
+
if alias_info:
|
|
42
|
+
# alias_info is a dict of {short_name: full_name}
|
|
43
|
+
return alias_info
|
|
44
|
+
return None
|
|
49
45
|
|
|
50
|
-
if is_function_def:
|
|
51
|
-
continue
|
|
52
46
|
|
|
53
|
-
|
|
47
|
+
def _find_aliases_recursive(node, source_code: bytes, aliases: list):
|
|
48
|
+
"""Recursively find alias declarations."""
|
|
49
|
+
_find_nodes_recursive(node, source_code, aliases, "call", _parse_alias_call)
|
|
54
50
|
|
|
55
51
|
|
|
56
52
|
def _parse_alias(arguments_node, source_code: bytes) -> dict | None:
|
|
@@ -67,7 +63,7 @@ def _parse_alias(arguments_node, source_code: bytes) -> dict | None:
|
|
|
67
63
|
for arg_child in arguments_node.children:
|
|
68
64
|
# Simple alias: alias MyApp.User
|
|
69
65
|
if arg_child.type == "alias":
|
|
70
|
-
full_name =
|
|
66
|
+
full_name = extract_text_from_node(arg_child, source_code)
|
|
71
67
|
# Get the last part as the short name
|
|
72
68
|
short_name = full_name.split(".")[-1]
|
|
73
69
|
result[short_name] = full_name
|
|
@@ -80,9 +76,7 @@ def _parse_alias(arguments_node, source_code: bytes) -> dict | None:
|
|
|
80
76
|
|
|
81
77
|
for dot_child in arg_child.children:
|
|
82
78
|
if dot_child.type == "alias":
|
|
83
|
-
module_prefix =
|
|
84
|
-
"utf-8"
|
|
85
|
-
)
|
|
79
|
+
module_prefix = extract_text_from_node(dot_child, source_code)
|
|
86
80
|
elif dot_child.type == "tuple":
|
|
87
81
|
tuple_node = dot_child
|
|
88
82
|
|
|
@@ -90,9 +84,7 @@ def _parse_alias(arguments_node, source_code: bytes) -> dict | None:
|
|
|
90
84
|
# Extract each alias from the tuple
|
|
91
85
|
for tuple_child in tuple_node.children:
|
|
92
86
|
if tuple_child.type == "alias":
|
|
93
|
-
short_name = source_code
|
|
94
|
-
tuple_child.start_byte : tuple_child.end_byte
|
|
95
|
-
].decode("utf-8")
|
|
87
|
+
short_name = extract_text_from_node(tuple_child, source_code)
|
|
96
88
|
full_name = f"{module_prefix}.{short_name}"
|
|
97
89
|
result[short_name] = full_name
|
|
98
90
|
|
|
@@ -106,22 +98,16 @@ def _parse_alias(arguments_node, source_code: bytes) -> dict | None:
|
|
|
106
98
|
for pair_child in kw_child.children:
|
|
107
99
|
if pair_child.type == "keyword":
|
|
108
100
|
# Get keyword text (e.g., "as:")
|
|
109
|
-
key_text = source_code
|
|
110
|
-
pair_child.start_byte : pair_child.end_byte
|
|
111
|
-
].decode("utf-8")
|
|
101
|
+
key_text = extract_text_from_node(pair_child, source_code)
|
|
112
102
|
elif pair_child.type == "alias":
|
|
113
|
-
alias_name = source_code
|
|
114
|
-
pair_child.start_byte : pair_child.end_byte
|
|
115
|
-
].decode("utf-8")
|
|
103
|
+
alias_name = extract_text_from_node(pair_child, source_code)
|
|
116
104
|
|
|
117
105
|
# If we found 'as:', update the result to use custom name
|
|
118
106
|
if key_text and "as" in key_text and alias_name:
|
|
119
107
|
# Get the full module name from previous arg
|
|
120
108
|
for prev_arg in arguments_node.children:
|
|
121
109
|
if prev_arg.type == "alias":
|
|
122
|
-
full_name = source_code
|
|
123
|
-
prev_arg.start_byte : prev_arg.end_byte
|
|
124
|
-
].decode("utf-8")
|
|
110
|
+
full_name = extract_text_from_node(prev_arg, source_code)
|
|
125
111
|
# Remove the default short name and add custom one
|
|
126
112
|
result.clear()
|
|
127
113
|
result[alias_name] = full_name
|
|
@@ -132,153 +118,67 @@ def _parse_alias(arguments_node, source_code: bytes) -> dict | None:
|
|
|
132
118
|
|
|
133
119
|
def extract_imports(node, source_code: bytes) -> list:
|
|
134
120
|
"""Extract all import declarations from a module body."""
|
|
121
|
+
|
|
135
122
|
imports = []
|
|
136
|
-
_find_imports_recursive(node, source_code, imports)
|
|
137
|
-
return imports
|
|
138
123
|
|
|
124
|
+
_find_declarations_recursive(node, source_code, imports, "import")
|
|
139
125
|
|
|
140
|
-
|
|
141
|
-
"""Recursively find import declarations."""
|
|
142
|
-
if node.type == "call":
|
|
143
|
-
target = None
|
|
144
|
-
arguments = None
|
|
126
|
+
return imports
|
|
145
127
|
|
|
146
|
-
for child in node.children:
|
|
147
|
-
if child.type == "identifier":
|
|
148
|
-
target = child
|
|
149
|
-
elif child.type == "arguments":
|
|
150
|
-
arguments = child
|
|
151
|
-
|
|
152
|
-
if target and arguments:
|
|
153
|
-
target_text = source_code[target.start_byte : target.end_byte].decode("utf-8")
|
|
154
|
-
|
|
155
|
-
if target_text == "import":
|
|
156
|
-
# Parse the import - imports are simpler than aliases
|
|
157
|
-
# import MyModule or import MyModule, only: [func: 1]
|
|
158
|
-
for arg_child in arguments.children:
|
|
159
|
-
if arg_child.type == "alias":
|
|
160
|
-
module_name = source_code[arg_child.start_byte : arg_child.end_byte].decode(
|
|
161
|
-
"utf-8"
|
|
162
|
-
)
|
|
163
|
-
imports.append(module_name)
|
|
164
|
-
|
|
165
|
-
# Recursively search children, but skip function bodies
|
|
166
|
-
for child in node.children:
|
|
167
|
-
if child.type == "call":
|
|
168
|
-
is_function_def = False
|
|
169
|
-
for call_child in child.children:
|
|
170
|
-
if call_child.type == "identifier":
|
|
171
|
-
target_text = source_code[call_child.start_byte : call_child.end_byte].decode(
|
|
172
|
-
"utf-8"
|
|
173
|
-
)
|
|
174
|
-
if target_text in ["def", "defp", "defmodule"]:
|
|
175
|
-
is_function_def = True
|
|
176
|
-
break
|
|
177
128
|
|
|
178
|
-
|
|
179
|
-
|
|
129
|
+
def _parse_declaration_call(node, source_code: bytes, declaration_name: str) -> str | None:
|
|
130
|
+
"""Parse a declaration call and return the module name."""
|
|
131
|
+
target = None
|
|
132
|
+
arguments = None
|
|
180
133
|
|
|
181
|
-
|
|
134
|
+
for child in node.children:
|
|
135
|
+
if child.type == "identifier":
|
|
136
|
+
target = child
|
|
137
|
+
elif child.type == "arguments":
|
|
138
|
+
arguments = child
|
|
139
|
+
|
|
140
|
+
if target and arguments:
|
|
141
|
+
target_text = extract_text_from_node(target, source_code)
|
|
142
|
+
|
|
143
|
+
if target_text == declaration_name:
|
|
144
|
+
# Parse the declaration
|
|
145
|
+
for arg_child in arguments.children:
|
|
146
|
+
if arg_child.type == "alias":
|
|
147
|
+
return extract_text_from_node(arg_child, source_code)
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _find_declarations_recursive(
|
|
152
|
+
node, source_code: bytes, declarations: list, declaration_name: str
|
|
153
|
+
):
|
|
154
|
+
"""Recursively find declarations."""
|
|
155
|
+
_find_nodes_recursive(
|
|
156
|
+
node,
|
|
157
|
+
source_code,
|
|
158
|
+
declarations,
|
|
159
|
+
"call",
|
|
160
|
+
lambda n, s: _parse_declaration_call(n, s, declaration_name),
|
|
161
|
+
)
|
|
182
162
|
|
|
183
163
|
|
|
184
164
|
def extract_requires(node, source_code: bytes) -> list:
|
|
185
165
|
"""Extract all require declarations from a module body."""
|
|
186
|
-
requires = []
|
|
187
|
-
_find_requires_recursive(node, source_code, requires)
|
|
188
|
-
return requires
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
def _find_requires_recursive(node, source_code: bytes, requires: list):
|
|
192
|
-
"""Recursively find require declarations."""
|
|
193
|
-
if node.type == "call":
|
|
194
|
-
target = None
|
|
195
|
-
arguments = None
|
|
196
|
-
|
|
197
|
-
for child in node.children:
|
|
198
|
-
if child.type == "identifier":
|
|
199
|
-
target = child
|
|
200
|
-
elif child.type == "arguments":
|
|
201
|
-
arguments = child
|
|
202
|
-
|
|
203
|
-
if target and arguments:
|
|
204
|
-
target_text = source_code[target.start_byte : target.end_byte].decode("utf-8")
|
|
205
166
|
|
|
206
|
-
|
|
207
|
-
# Parse the require
|
|
208
|
-
for arg_child in arguments.children:
|
|
209
|
-
if arg_child.type == "alias":
|
|
210
|
-
module_name = source_code[arg_child.start_byte : arg_child.end_byte].decode(
|
|
211
|
-
"utf-8"
|
|
212
|
-
)
|
|
213
|
-
requires.append(module_name)
|
|
214
|
-
|
|
215
|
-
# Recursively search children, but skip function bodies
|
|
216
|
-
for child in node.children:
|
|
217
|
-
if child.type == "call":
|
|
218
|
-
is_function_def = False
|
|
219
|
-
for call_child in child.children:
|
|
220
|
-
if call_child.type == "identifier":
|
|
221
|
-
target_text = source_code[call_child.start_byte : call_child.end_byte].decode(
|
|
222
|
-
"utf-8"
|
|
223
|
-
)
|
|
224
|
-
if target_text in ["def", "defp", "defmodule"]:
|
|
225
|
-
is_function_def = True
|
|
226
|
-
break
|
|
167
|
+
requires = []
|
|
227
168
|
|
|
228
|
-
|
|
229
|
-
continue
|
|
169
|
+
_find_declarations_recursive(node, source_code, requires, "require")
|
|
230
170
|
|
|
231
|
-
|
|
171
|
+
return requires
|
|
232
172
|
|
|
233
173
|
|
|
234
174
|
def extract_uses(node, source_code: bytes) -> list:
|
|
235
175
|
"""Extract all use declarations from a module body."""
|
|
236
|
-
uses = []
|
|
237
|
-
_find_uses_recursive(node, source_code, uses)
|
|
238
|
-
return uses
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
def _find_uses_recursive(node, source_code: bytes, uses: list):
|
|
242
|
-
"""Recursively find use declarations."""
|
|
243
|
-
if node.type == "call":
|
|
244
|
-
target = None
|
|
245
|
-
arguments = None
|
|
246
|
-
|
|
247
|
-
for child in node.children:
|
|
248
|
-
if child.type == "identifier":
|
|
249
|
-
target = child
|
|
250
|
-
elif child.type == "arguments":
|
|
251
|
-
arguments = child
|
|
252
|
-
|
|
253
|
-
if target and arguments:
|
|
254
|
-
target_text = source_code[target.start_byte : target.end_byte].decode("utf-8")
|
|
255
176
|
|
|
256
|
-
|
|
257
|
-
# Parse the use
|
|
258
|
-
for arg_child in arguments.children:
|
|
259
|
-
if arg_child.type == "alias":
|
|
260
|
-
module_name = source_code[arg_child.start_byte : arg_child.end_byte].decode(
|
|
261
|
-
"utf-8"
|
|
262
|
-
)
|
|
263
|
-
uses.append(module_name)
|
|
264
|
-
|
|
265
|
-
# Recursively search children, but skip function bodies
|
|
266
|
-
for child in node.children:
|
|
267
|
-
if child.type == "call":
|
|
268
|
-
is_function_def = False
|
|
269
|
-
for call_child in child.children:
|
|
270
|
-
if call_child.type == "identifier":
|
|
271
|
-
target_text = source_code[call_child.start_byte : call_child.end_byte].decode(
|
|
272
|
-
"utf-8"
|
|
273
|
-
)
|
|
274
|
-
if target_text in ["def", "defp", "defmodule"]:
|
|
275
|
-
is_function_def = True
|
|
276
|
-
break
|
|
177
|
+
uses = []
|
|
277
178
|
|
|
278
|
-
|
|
279
|
-
continue
|
|
179
|
+
_find_declarations_recursive(node, source_code, uses, "use")
|
|
280
180
|
|
|
281
|
-
|
|
181
|
+
return uses
|
|
282
182
|
|
|
283
183
|
|
|
284
184
|
def extract_behaviours(node, source_code: bytes) -> list:
|
|
@@ -288,62 +188,44 @@ def extract_behaviours(node, source_code: bytes) -> list:
|
|
|
288
188
|
return behaviours
|
|
289
189
|
|
|
290
190
|
|
|
191
|
+
def _parse_behaviour_call(node, source_code: bytes) -> str | None:
|
|
192
|
+
"""Parse a behaviour call and return the module name."""
|
|
193
|
+
# Check if this is an @ operator with behaviour
|
|
194
|
+
is_at_operator = False
|
|
195
|
+
behaviour_call = None
|
|
196
|
+
|
|
197
|
+
for child in node.children:
|
|
198
|
+
if child.type == "@":
|
|
199
|
+
is_at_operator = True
|
|
200
|
+
elif child.type == "call" and is_at_operator:
|
|
201
|
+
behaviour_call = child
|
|
202
|
+
break
|
|
203
|
+
|
|
204
|
+
if behaviour_call:
|
|
205
|
+
# Check if the call is "behaviour"
|
|
206
|
+
identifier_text = None
|
|
207
|
+
arguments_node = None
|
|
208
|
+
|
|
209
|
+
for child in behaviour_call.children:
|
|
210
|
+
if child.type == "identifier":
|
|
211
|
+
identifier_text = extract_text_from_node(child, source_code)
|
|
212
|
+
elif child.type == "arguments":
|
|
213
|
+
arguments_node = child
|
|
214
|
+
|
|
215
|
+
if identifier_text == "behaviour" and arguments_node:
|
|
216
|
+
# Extract the behaviour module name
|
|
217
|
+
for arg_child in arguments_node.children:
|
|
218
|
+
if arg_child.type == "alias":
|
|
219
|
+
# @behaviour ModuleName
|
|
220
|
+
return extract_text_from_node(arg_child, source_code)
|
|
221
|
+
elif arg_child.type == "atom":
|
|
222
|
+
# @behaviour :module_name
|
|
223
|
+
atom_text = extract_text_from_node(arg_child, source_code)
|
|
224
|
+
# Remove leading colon and convert to module format if needed
|
|
225
|
+
return atom_text.lstrip(":")
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
|
|
291
229
|
def _find_behaviours_recursive(node, source_code: bytes, behaviours: list):
|
|
292
230
|
"""Recursively find @behaviour declarations."""
|
|
293
|
-
|
|
294
|
-
# Check if this is an @ operator with behaviour
|
|
295
|
-
is_at_operator = False
|
|
296
|
-
behaviour_call = None
|
|
297
|
-
|
|
298
|
-
for child in node.children:
|
|
299
|
-
if child.type == "@":
|
|
300
|
-
is_at_operator = True
|
|
301
|
-
elif child.type == "call" and is_at_operator:
|
|
302
|
-
behaviour_call = child
|
|
303
|
-
break
|
|
304
|
-
|
|
305
|
-
if behaviour_call:
|
|
306
|
-
# Check if the call is "behaviour"
|
|
307
|
-
identifier_text = None
|
|
308
|
-
arguments_node = None
|
|
309
|
-
|
|
310
|
-
for child in behaviour_call.children:
|
|
311
|
-
if child.type == "identifier":
|
|
312
|
-
identifier_text = source_code[child.start_byte : child.end_byte].decode("utf-8")
|
|
313
|
-
elif child.type == "arguments":
|
|
314
|
-
arguments_node = child
|
|
315
|
-
|
|
316
|
-
if identifier_text == "behaviour" and arguments_node:
|
|
317
|
-
# Extract the behaviour module name
|
|
318
|
-
for arg_child in arguments_node.children:
|
|
319
|
-
if arg_child.type == "alias":
|
|
320
|
-
# @behaviour ModuleName
|
|
321
|
-
module_name = source_code[arg_child.start_byte : arg_child.end_byte].decode(
|
|
322
|
-
"utf-8"
|
|
323
|
-
)
|
|
324
|
-
behaviours.append(module_name)
|
|
325
|
-
elif arg_child.type == "atom":
|
|
326
|
-
# @behaviour :module_name
|
|
327
|
-
atom_text = source_code[arg_child.start_byte : arg_child.end_byte].decode(
|
|
328
|
-
"utf-8"
|
|
329
|
-
)
|
|
330
|
-
# Remove leading colon and convert to module format if needed
|
|
331
|
-
behaviours.append(atom_text.lstrip(":"))
|
|
332
|
-
|
|
333
|
-
# Recursively search children, but skip function bodies
|
|
334
|
-
for child in node.children:
|
|
335
|
-
if child.type == "call":
|
|
336
|
-
is_function_def = False
|
|
337
|
-
for call_child in child.children:
|
|
338
|
-
if call_child.type == "identifier":
|
|
339
|
-
target_text = source_code[call_child.start_byte : call_child.end_byte].decode(
|
|
340
|
-
"utf-8"
|
|
341
|
-
)
|
|
342
|
-
if target_text in ["def", "defp", "defmodule"]:
|
|
343
|
-
is_function_def = True
|
|
344
|
-
break
|
|
345
|
-
|
|
346
|
-
if is_function_def:
|
|
347
|
-
continue
|
|
348
|
-
|
|
349
|
-
_find_behaviours_recursive(child, source_code, behaviours)
|
|
231
|
+
_find_nodes_recursive(node, source_code, behaviours, "unary_operator", _parse_behaviour_call)
|
cicada/extractors/doc.py
CHANGED
|
@@ -5,63 +5,16 @@ Documentation extraction logic.
|
|
|
5
5
|
import textwrap
|
|
6
6
|
|
|
7
7
|
from .base import extract_string_from_arguments
|
|
8
|
+
from .common import _find_attribute_recursive
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def extract_docs(node, source_code: bytes) -> dict:
|
|
11
12
|
"""Extract all @doc attributes from a module body."""
|
|
12
13
|
docs = {}
|
|
13
|
-
|
|
14
|
+
_find_attribute_recursive(node, source_code, docs, "doc", _parse_doc)
|
|
14
15
|
return docs
|
|
15
16
|
|
|
16
17
|
|
|
17
|
-
def _find_docs_recursive(node, source_code: bytes, docs: dict):
|
|
18
|
-
"""Recursively find @doc declarations."""
|
|
19
|
-
# Look for unary_operator nodes (which represent @ attributes)
|
|
20
|
-
if node.type == "unary_operator":
|
|
21
|
-
operator = None
|
|
22
|
-
operand = None
|
|
23
|
-
|
|
24
|
-
for child in node.children:
|
|
25
|
-
if child.type == "@":
|
|
26
|
-
operator = child
|
|
27
|
-
elif child.type == "call":
|
|
28
|
-
operand = child
|
|
29
|
-
|
|
30
|
-
if operator and operand:
|
|
31
|
-
# Check if this is a doc attribute
|
|
32
|
-
for call_child in operand.children:
|
|
33
|
-
if call_child.type == "identifier":
|
|
34
|
-
attr_name = source_code[call_child.start_byte : call_child.end_byte].decode(
|
|
35
|
-
"utf-8"
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
if attr_name == "doc":
|
|
39
|
-
# Extract the doc definition
|
|
40
|
-
doc_info = _parse_doc(operand, source_code, node.start_point[0] + 1)
|
|
41
|
-
if doc_info:
|
|
42
|
-
# Store the entire doc_info dict (includes text and examples)
|
|
43
|
-
docs[doc_info["line"]] = doc_info
|
|
44
|
-
|
|
45
|
-
# Recursively search children
|
|
46
|
-
for child in node.children:
|
|
47
|
-
# Don't recurse into nested defmodule or function definitions
|
|
48
|
-
if child.type == "call":
|
|
49
|
-
is_defmodule_or_def = False
|
|
50
|
-
for call_child in child.children:
|
|
51
|
-
if call_child.type == "identifier":
|
|
52
|
-
target_text = source_code[call_child.start_byte : call_child.end_byte].decode(
|
|
53
|
-
"utf-8"
|
|
54
|
-
)
|
|
55
|
-
if target_text in ["defmodule", "def", "defp"]:
|
|
56
|
-
is_defmodule_or_def = True
|
|
57
|
-
break
|
|
58
|
-
|
|
59
|
-
if is_defmodule_or_def:
|
|
60
|
-
continue
|
|
61
|
-
|
|
62
|
-
_find_docs_recursive(child, source_code, docs)
|
|
63
|
-
|
|
64
|
-
|
|
65
18
|
def _parse_doc(doc_node, source_code: bytes, line: int) -> dict | None:
|
|
66
19
|
"""Parse a @doc attribute to extract its text and examples."""
|
|
67
20
|
# @doc is represented as: doc("text") or doc(false)
|
cicada/extractors/function.py
CHANGED
|
@@ -4,6 +4,8 @@ Function extraction logic.
|
|
|
4
4
|
Author: Cursor(Auto)
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
from cicada.utils import extract_text_from_node
|
|
8
|
+
|
|
7
9
|
from .base import get_param_name
|
|
8
10
|
|
|
9
11
|
|
|
@@ -46,7 +48,7 @@ def _extract_impl_from_prev_sibling(node, source_code: bytes):
|
|
|
46
48
|
|
|
47
49
|
for child in impl_call.children:
|
|
48
50
|
if child.type == "identifier":
|
|
49
|
-
identifier_text =
|
|
51
|
+
identifier_text = extract_text_from_node(child, source_code)
|
|
50
52
|
elif child.type == "arguments":
|
|
51
53
|
arguments_node = child
|
|
52
54
|
|
|
@@ -58,11 +60,11 @@ def _extract_impl_from_prev_sibling(node, source_code: bytes):
|
|
|
58
60
|
for arg_child in arguments_node.children:
|
|
59
61
|
if arg_child.type == "boolean":
|
|
60
62
|
# @impl true or @impl false
|
|
61
|
-
bool_text =
|
|
63
|
+
bool_text = extract_text_from_node(arg_child, source_code)
|
|
62
64
|
return bool_text == "true"
|
|
63
65
|
elif arg_child.type == "alias":
|
|
64
66
|
# @impl ModuleName
|
|
65
|
-
module_name =
|
|
67
|
+
module_name = extract_text_from_node(arg_child, source_code)
|
|
66
68
|
return module_name
|
|
67
69
|
|
|
68
70
|
# @impl without arguments defaults to true
|
|
@@ -90,7 +92,7 @@ def _find_functions_recursive(node, source_code: bytes, functions: list):
|
|
|
90
92
|
|
|
91
93
|
# Check if this is a def or defp call
|
|
92
94
|
if target and arguments:
|
|
93
|
-
target_text =
|
|
95
|
+
target_text = extract_text_from_node(target, source_code)
|
|
94
96
|
|
|
95
97
|
if target_text in ["def", "defp"]:
|
|
96
98
|
# Check if previous sibling is @impl
|
|
@@ -133,9 +135,7 @@ def _parse_function_definition(
|
|
|
133
135
|
# Extract function name from call target
|
|
134
136
|
for call_child in arg_child.children:
|
|
135
137
|
if call_child.type == "identifier":
|
|
136
|
-
func_name =
|
|
137
|
-
"utf-8"
|
|
138
|
-
)
|
|
138
|
+
func_name = extract_text_from_node(call_child, source_code)
|
|
139
139
|
elif call_child.type == "arguments":
|
|
140
140
|
arg_names = _extract_argument_names(call_child, source_code)
|
|
141
141
|
arity = len(arg_names)
|
|
@@ -148,16 +148,14 @@ def _parse_function_definition(
|
|
|
148
148
|
# Extract function name and args from the call
|
|
149
149
|
for call_child in op_child.children:
|
|
150
150
|
if call_child.type == "identifier":
|
|
151
|
-
func_name = source_code
|
|
152
|
-
call_child.start_byte : call_child.end_byte
|
|
153
|
-
].decode("utf-8")
|
|
151
|
+
func_name = extract_text_from_node(call_child, source_code)
|
|
154
152
|
elif call_child.type == "arguments":
|
|
155
153
|
arg_names = _extract_argument_names(call_child, source_code)
|
|
156
154
|
arity = len(arg_names)
|
|
157
155
|
break
|
|
158
156
|
break
|
|
159
157
|
elif arg_child.type == "identifier":
|
|
160
|
-
func_name =
|
|
158
|
+
func_name = extract_text_from_node(arg_child, source_code)
|
|
161
159
|
arity = 0
|
|
162
160
|
arg_names = []
|
|
163
161
|
break
|
|
@@ -209,9 +207,7 @@ def _extract_guards(arguments_node, source_code: bytes) -> list[str]:
|
|
|
209
207
|
elif has_when:
|
|
210
208
|
# This is the guard expression node (comes after 'when')
|
|
211
209
|
# It's typically a binary_operator (like n < 0)
|
|
212
|
-
guard_expr =
|
|
213
|
-
"utf-8"
|
|
214
|
-
)
|
|
210
|
+
guard_expr = extract_text_from_node(op_child, source_code)
|
|
215
211
|
guards.append(guard_expr)
|
|
216
212
|
break
|
|
217
213
|
|