cicada-mcp 0.1.5__py3-none-any.whl → 0.2.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/ascii_art.py +60 -0
- cicada/clean.py +195 -60
- cicada/cli.py +757 -0
- cicada/colors.py +27 -0
- cicada/command_logger.py +14 -16
- cicada/dead_code_analyzer.py +12 -19
- cicada/extractors/__init__.py +6 -6
- cicada/extractors/base.py +3 -3
- cicada/extractors/call.py +11 -15
- cicada/extractors/dependency.py +39 -51
- cicada/extractors/doc.py +8 -9
- cicada/extractors/function.py +12 -24
- cicada/extractors/module.py +11 -15
- cicada/extractors/spec.py +8 -12
- cicada/find_dead_code.py +15 -39
- cicada/formatter.py +37 -91
- cicada/git_helper.py +22 -34
- cicada/indexer.py +165 -132
- cicada/interactive_setup.py +490 -0
- cicada/keybert_extractor.py +286 -0
- cicada/keyword_search.py +22 -30
- cicada/keyword_test.py +127 -0
- cicada/lightweight_keyword_extractor.py +5 -13
- cicada/mcp_entry.py +683 -0
- cicada/mcp_server.py +110 -232
- cicada/parser.py +9 -9
- cicada/pr_finder.py +15 -19
- cicada/pr_indexer/__init__.py +3 -3
- cicada/pr_indexer/cli.py +4 -9
- cicada/pr_indexer/github_api_client.py +22 -37
- cicada/pr_indexer/indexer.py +17 -29
- cicada/pr_indexer/line_mapper.py +8 -12
- cicada/pr_indexer/pr_index_builder.py +22 -34
- cicada/setup.py +198 -89
- cicada/utils/__init__.py +9 -9
- cicada/utils/call_site_formatter.py +4 -6
- cicada/utils/function_grouper.py +4 -4
- cicada/utils/hash_utils.py +12 -15
- cicada/utils/index_utils.py +15 -15
- cicada/utils/path_utils.py +24 -29
- cicada/utils/signature_builder.py +3 -3
- cicada/utils/subprocess_runner.py +17 -19
- cicada/utils/text_utils.py +1 -2
- cicada/version_check.py +2 -5
- {cicada_mcp-0.1.5.dist-info → cicada_mcp-0.2.0.dist-info}/METADATA +144 -55
- cicada_mcp-0.2.0.dist-info/RECORD +53 -0
- cicada_mcp-0.2.0.dist-info/entry_points.txt +4 -0
- cicada/install.py +0 -741
- cicada_mcp-0.1.5.dist-info/RECORD +0 -47
- cicada_mcp-0.1.5.dist-info/entry_points.txt +0 -9
- {cicada_mcp-0.1.5.dist-info → cicada_mcp-0.2.0.dist-info}/WHEEL +0 -0
- {cicada_mcp-0.1.5.dist-info → cicada_mcp-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {cicada_mcp-0.1.5.dist-info → cicada_mcp-0.2.0.dist-info}/top_level.txt +0 -0
cicada/colors.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared ANSI color codes for Cicada CLI.
|
|
3
|
+
|
|
4
|
+
This module provides consistent color definitions across all Cicada modules
|
|
5
|
+
to eliminate code duplication and ensure brand consistency.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# Brand colors - primary palette
|
|
9
|
+
ORANGE = "\033[38;2;217;119;87m" # #D97757 - Primary brand color (orange)
|
|
10
|
+
YELLOW = "\033[38;2;229;200;144m" # #E5C890 - Selected items/highlights
|
|
11
|
+
SELECTED = YELLOW # Alias for menu selections
|
|
12
|
+
CYAN = ORANGE # Deprecated alias - kept for backwards compatibility
|
|
13
|
+
|
|
14
|
+
# Standard terminal colors
|
|
15
|
+
BLUE = "\033[94m"
|
|
16
|
+
GREEN = "\033[92m"
|
|
17
|
+
RED = "\033[91m"
|
|
18
|
+
GRAY = "\033[90m"
|
|
19
|
+
GREY = GRAY # British spelling alias
|
|
20
|
+
|
|
21
|
+
# Modifiers
|
|
22
|
+
BOLD = "\033[1m"
|
|
23
|
+
DIM = "\033[2m"
|
|
24
|
+
RESET = "\033[0m"
|
|
25
|
+
|
|
26
|
+
# Composite styles (for convenience)
|
|
27
|
+
PRIMARY = ORANGE # Alias for primary brand color
|
cicada/command_logger.py
CHANGED
|
@@ -9,13 +9,13 @@ import json
|
|
|
9
9
|
import tempfile
|
|
10
10
|
from datetime import datetime
|
|
11
11
|
from pathlib import Path
|
|
12
|
-
from typing import Any
|
|
12
|
+
from typing import Any
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class CommandLogger:
|
|
16
16
|
"""Logger for MCP tool executions."""
|
|
17
17
|
|
|
18
|
-
def __init__(self, log_dir:
|
|
18
|
+
def __init__(self, log_dir: str | None = None):
|
|
19
19
|
"""Initialize the command logger.
|
|
20
20
|
|
|
21
21
|
Args:
|
|
@@ -47,11 +47,11 @@ class CommandLogger:
|
|
|
47
47
|
def log_command(
|
|
48
48
|
self,
|
|
49
49
|
tool_name: str,
|
|
50
|
-
arguments:
|
|
50
|
+
arguments: dict[str, Any],
|
|
51
51
|
response: Any,
|
|
52
52
|
execution_time_ms: float,
|
|
53
|
-
timestamp:
|
|
54
|
-
error:
|
|
53
|
+
timestamp: datetime | None = None,
|
|
54
|
+
error: str | None = None,
|
|
55
55
|
) -> None:
|
|
56
56
|
"""Log a command execution.
|
|
57
57
|
|
|
@@ -102,11 +102,11 @@ class CommandLogger:
|
|
|
102
102
|
async def log_command_async(
|
|
103
103
|
self,
|
|
104
104
|
tool_name: str,
|
|
105
|
-
arguments:
|
|
105
|
+
arguments: dict[str, Any],
|
|
106
106
|
response: Any,
|
|
107
107
|
execution_time_ms: float,
|
|
108
|
-
timestamp:
|
|
109
|
-
error:
|
|
108
|
+
timestamp: datetime | None = None,
|
|
109
|
+
error: str | None = None,
|
|
110
110
|
) -> None:
|
|
111
111
|
"""Async version of log_command that runs file I/O in a thread pool.
|
|
112
112
|
|
|
@@ -166,9 +166,7 @@ class CommandLogger:
|
|
|
166
166
|
log_files = sorted(self.log_dir.glob("*.jsonl"))
|
|
167
167
|
return log_files
|
|
168
168
|
|
|
169
|
-
def read_logs(
|
|
170
|
-
self, date: Optional[str] = None, limit: Optional[int] = None
|
|
171
|
-
) -> list[Dict[str, Any]]:
|
|
169
|
+
def read_logs(self, date: str | None = None, limit: int | None = None) -> list[dict[str, Any]]:
|
|
172
170
|
"""Read logs from file(s).
|
|
173
171
|
|
|
174
172
|
Args:
|
|
@@ -206,7 +204,7 @@ class CommandLogger:
|
|
|
206
204
|
|
|
207
205
|
return logs
|
|
208
206
|
|
|
209
|
-
def _read_log_file(self, file_path: Path) -> list[
|
|
207
|
+
def _read_log_file(self, file_path: Path) -> list[dict[str, Any]]:
|
|
210
208
|
"""Read logs from a single JSONL file.
|
|
211
209
|
|
|
212
210
|
Args:
|
|
@@ -217,7 +215,7 @@ class CommandLogger:
|
|
|
217
215
|
"""
|
|
218
216
|
logs = []
|
|
219
217
|
try:
|
|
220
|
-
with open(file_path,
|
|
218
|
+
with open(file_path, encoding="utf-8") as f:
|
|
221
219
|
for line in f:
|
|
222
220
|
line = line.strip()
|
|
223
221
|
if line:
|
|
@@ -232,7 +230,7 @@ class CommandLogger:
|
|
|
232
230
|
|
|
233
231
|
return logs
|
|
234
232
|
|
|
235
|
-
def clear_logs(self, older_than_days:
|
|
233
|
+
def clear_logs(self, older_than_days: int | None = None) -> int:
|
|
236
234
|
"""Clear log files.
|
|
237
235
|
|
|
238
236
|
Args:
|
|
@@ -275,10 +273,10 @@ class CommandLogger:
|
|
|
275
273
|
|
|
276
274
|
|
|
277
275
|
# Global logger instance
|
|
278
|
-
_global_logger:
|
|
276
|
+
_global_logger: CommandLogger | None = None
|
|
279
277
|
|
|
280
278
|
|
|
281
|
-
def get_logger(log_dir:
|
|
279
|
+
def get_logger(log_dir: str | None = None) -> CommandLogger:
|
|
282
280
|
"""Get or create the global command logger instance.
|
|
283
281
|
|
|
284
282
|
Args:
|
cicada/dead_code_analyzer.py
CHANGED
|
@@ -6,8 +6,6 @@ Identifies potentially unused public functions using the indexed codebase data.
|
|
|
6
6
|
Author: Cursor(Auto)
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
from typing import Dict, List, Optional
|
|
10
|
-
|
|
11
9
|
|
|
12
10
|
class DeadCodeAnalyzer:
|
|
13
11
|
"""Analyzes Elixir code index to find potentially unused public functions."""
|
|
@@ -56,9 +54,7 @@ class DeadCodeAnalyzer:
|
|
|
56
54
|
for module_name, module_data in self.modules.items():
|
|
57
55
|
# Skip test files and .exs files entirely
|
|
58
56
|
if self._is_test_file(module_data["file"]):
|
|
59
|
-
skipped_files += sum(
|
|
60
|
-
1 for f in module_data["functions"] if f["type"] == "def"
|
|
61
|
-
)
|
|
57
|
+
skipped_files += sum(1 for f in module_data["functions"] if f["type"] == "def")
|
|
62
58
|
continue
|
|
63
59
|
|
|
64
60
|
# Analyze each function in the module
|
|
@@ -77,9 +73,7 @@ class DeadCodeAnalyzer:
|
|
|
77
73
|
analyzed += 1
|
|
78
74
|
|
|
79
75
|
# Find usages of this function
|
|
80
|
-
usage_count = self._find_usages(
|
|
81
|
-
module_name, function["name"], function["arity"]
|
|
82
|
-
)
|
|
76
|
+
usage_count = self._find_usages(module_name, function["name"], function["arity"])
|
|
83
77
|
|
|
84
78
|
# If function is used, skip it
|
|
85
79
|
if usage_count > 0:
|
|
@@ -149,14 +143,10 @@ class DeadCodeAnalyzer:
|
|
|
149
143
|
# Test files
|
|
150
144
|
"/test/" in file_lower
|
|
151
145
|
or file_lower.startswith("test/")
|
|
152
|
-
or file_lower.endswith("_test.ex")
|
|
153
|
-
# All .exs files (scripts, config files, etc.)
|
|
154
|
-
or file_lower.endswith(".exs")
|
|
146
|
+
or file_lower.endswith(("_test.ex", ".exs"))
|
|
155
147
|
)
|
|
156
148
|
|
|
157
|
-
def _find_usages(
|
|
158
|
-
self, target_module: str, target_function: str, target_arity: int
|
|
159
|
-
) -> int:
|
|
149
|
+
def _find_usages(self, target_module: str, target_function: str, target_arity: int) -> int:
|
|
160
150
|
"""
|
|
161
151
|
Find the number of times a function is called across the codebase.
|
|
162
152
|
|
|
@@ -203,9 +193,12 @@ class DeadCodeAnalyzer:
|
|
|
203
193
|
# Filter out calls that are BEFORE the function definition
|
|
204
194
|
# (@spec, @doc annotations appear 1-5 lines before the def)
|
|
205
195
|
# Only filter if call is before def and within 5 lines
|
|
206
|
-
if
|
|
207
|
-
|
|
208
|
-
|
|
196
|
+
if (
|
|
197
|
+
function_def_line
|
|
198
|
+
and call["line"] < function_def_line
|
|
199
|
+
and (function_def_line - call["line"]) <= 5
|
|
200
|
+
):
|
|
201
|
+
continue
|
|
209
202
|
call_count += 1
|
|
210
203
|
else:
|
|
211
204
|
# Qualified call - resolve the module name
|
|
@@ -260,12 +253,12 @@ class DeadCodeAnalyzer:
|
|
|
260
253
|
Returns:
|
|
261
254
|
True if module appears in value_mentions of any other module
|
|
262
255
|
"""
|
|
263
|
-
for
|
|
256
|
+
for _other_module, module_data in self.modules.items():
|
|
264
257
|
if module_name in module_data.get("value_mentions", []):
|
|
265
258
|
return True
|
|
266
259
|
return False
|
|
267
260
|
|
|
268
|
-
def _find_value_mentioners(self, module_name: str) ->
|
|
261
|
+
def _find_value_mentioners(self, module_name: str) -> list[dict]:
|
|
269
262
|
"""
|
|
270
263
|
Find all modules that mention this module as a value.
|
|
271
264
|
|
cicada/extractors/__init__.py
CHANGED
|
@@ -6,18 +6,18 @@ This package contains specialized extractors for different parts of Elixir modul
|
|
|
6
6
|
Author: Cursor(Auto)
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
from .
|
|
10
|
-
from .function import extract_functions
|
|
11
|
-
from .spec import extract_specs, match_specs_to_functions
|
|
12
|
-
from .doc import extract_docs, match_docs_to_functions
|
|
9
|
+
from .call import extract_function_calls, extract_value_mentions
|
|
13
10
|
from .dependency import (
|
|
14
11
|
extract_aliases,
|
|
12
|
+
extract_behaviours,
|
|
15
13
|
extract_imports,
|
|
16
14
|
extract_requires,
|
|
17
15
|
extract_uses,
|
|
18
|
-
extract_behaviours,
|
|
19
16
|
)
|
|
20
|
-
from .
|
|
17
|
+
from .doc import extract_docs, match_docs_to_functions
|
|
18
|
+
from .function import extract_functions
|
|
19
|
+
from .module import extract_modules
|
|
20
|
+
from .spec import extract_specs, match_specs_to_functions
|
|
21
21
|
|
|
22
22
|
__all__ = [
|
|
23
23
|
"extract_modules",
|
cicada/extractors/base.py
CHANGED
|
@@ -12,9 +12,9 @@ def extract_string_from_arguments(arguments_node, source_code: bytes) -> str | N
|
|
|
12
12
|
string_content = []
|
|
13
13
|
for string_child in child.children:
|
|
14
14
|
if string_child.type == "quoted_content":
|
|
15
|
-
content = source_code[
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
content = source_code[string_child.start_byte : string_child.end_byte].decode(
|
|
16
|
+
"utf-8"
|
|
17
|
+
)
|
|
18
18
|
string_content.append(content)
|
|
19
19
|
|
|
20
20
|
if string_content:
|
cicada/extractors/call.py
CHANGED
|
@@ -19,9 +19,7 @@ def _find_function_calls_recursive(node, source_code: bytes, calls: list):
|
|
|
19
19
|
is_function_def = False
|
|
20
20
|
for child in node.children:
|
|
21
21
|
if child.type == "identifier":
|
|
22
|
-
func_text = source_code[child.start_byte : child.end_byte].decode(
|
|
23
|
-
"utf-8"
|
|
24
|
-
)
|
|
22
|
+
func_text = source_code[child.start_byte : child.end_byte].decode("utf-8")
|
|
25
23
|
if func_text in ["def", "defp", "defmodule"]:
|
|
26
24
|
is_function_def = True
|
|
27
25
|
break
|
|
@@ -67,18 +65,16 @@ def _parse_function_call(call_node, source_code: bytes) -> dict | None:
|
|
|
67
65
|
# Extract module and function from dot
|
|
68
66
|
for dot_child in child.children:
|
|
69
67
|
if dot_child.type == "alias":
|
|
70
|
-
module_name = source_code[
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
module_name = source_code[dot_child.start_byte : dot_child.end_byte].decode(
|
|
69
|
+
"utf-8"
|
|
70
|
+
)
|
|
73
71
|
elif dot_child.type == "identifier":
|
|
74
|
-
function_name = source_code[
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
function_name = source_code[dot_child.start_byte : dot_child.end_byte].decode(
|
|
73
|
+
"utf-8"
|
|
74
|
+
)
|
|
77
75
|
elif child.type == "identifier" and not has_dot:
|
|
78
76
|
# Local function call
|
|
79
|
-
function_name = source_code[child.start_byte : child.end_byte].decode(
|
|
80
|
-
"utf-8"
|
|
81
|
-
)
|
|
77
|
+
function_name = source_code[child.start_byte : child.end_byte].decode("utf-8")
|
|
82
78
|
elif child.type == "arguments":
|
|
83
79
|
arguments_node = child
|
|
84
80
|
|
|
@@ -151,9 +147,9 @@ def _find_value_mentions_recursive(node, source_code: bytes, value_mentions: lis
|
|
|
151
147
|
# Check if this is alias/import/require/use/defmodule
|
|
152
148
|
for child in current.children:
|
|
153
149
|
if child.type == "identifier":
|
|
154
|
-
func_text = source_code[
|
|
155
|
-
|
|
156
|
-
|
|
150
|
+
func_text = source_code[child.start_byte : child.end_byte].decode(
|
|
151
|
+
"utf-8"
|
|
152
|
+
)
|
|
157
153
|
if func_text in [
|
|
158
154
|
"alias",
|
|
159
155
|
"import",
|
cicada/extractors/dependency.py
CHANGED
|
@@ -25,9 +25,7 @@ def _find_aliases_recursive(node, source_code: bytes, aliases: dict):
|
|
|
25
25
|
arguments = child
|
|
26
26
|
|
|
27
27
|
if target and arguments:
|
|
28
|
-
target_text = source_code[target.start_byte : target.end_byte].decode(
|
|
29
|
-
"utf-8"
|
|
30
|
-
)
|
|
28
|
+
target_text = source_code[target.start_byte : target.end_byte].decode("utf-8")
|
|
31
29
|
|
|
32
30
|
if target_text == "alias":
|
|
33
31
|
# Parse the alias
|
|
@@ -42,9 +40,9 @@ def _find_aliases_recursive(node, source_code: bytes, aliases: dict):
|
|
|
42
40
|
is_function_def = False
|
|
43
41
|
for call_child in child.children:
|
|
44
42
|
if call_child.type == "identifier":
|
|
45
|
-
target_text = source_code[
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
target_text = source_code[call_child.start_byte : call_child.end_byte].decode(
|
|
44
|
+
"utf-8"
|
|
45
|
+
)
|
|
48
46
|
if target_text in ["def", "defp", "defmodule"]:
|
|
49
47
|
is_function_def = True
|
|
50
48
|
break
|
|
@@ -69,9 +67,7 @@ def _parse_alias(arguments_node, source_code: bytes) -> dict | None:
|
|
|
69
67
|
for arg_child in arguments_node.children:
|
|
70
68
|
# Simple alias: alias MyApp.User
|
|
71
69
|
if arg_child.type == "alias":
|
|
72
|
-
full_name = source_code[arg_child.start_byte : arg_child.end_byte].decode(
|
|
73
|
-
"utf-8"
|
|
74
|
-
)
|
|
70
|
+
full_name = source_code[arg_child.start_byte : arg_child.end_byte].decode("utf-8")
|
|
75
71
|
# Get the last part as the short name
|
|
76
72
|
short_name = full_name.split(".")[-1]
|
|
77
73
|
result[short_name] = full_name
|
|
@@ -84,9 +80,9 @@ def _parse_alias(arguments_node, source_code: bytes) -> dict | None:
|
|
|
84
80
|
|
|
85
81
|
for dot_child in arg_child.children:
|
|
86
82
|
if dot_child.type == "alias":
|
|
87
|
-
module_prefix = source_code[
|
|
88
|
-
|
|
89
|
-
|
|
83
|
+
module_prefix = source_code[dot_child.start_byte : dot_child.end_byte].decode(
|
|
84
|
+
"utf-8"
|
|
85
|
+
)
|
|
90
86
|
elif dot_child.type == "tuple":
|
|
91
87
|
tuple_node = dot_child
|
|
92
88
|
|
|
@@ -154,18 +150,16 @@ def _find_imports_recursive(node, source_code: bytes, imports: list):
|
|
|
154
150
|
arguments = child
|
|
155
151
|
|
|
156
152
|
if target and arguments:
|
|
157
|
-
target_text = source_code[target.start_byte : target.end_byte].decode(
|
|
158
|
-
"utf-8"
|
|
159
|
-
)
|
|
153
|
+
target_text = source_code[target.start_byte : target.end_byte].decode("utf-8")
|
|
160
154
|
|
|
161
155
|
if target_text == "import":
|
|
162
156
|
# Parse the import - imports are simpler than aliases
|
|
163
157
|
# import MyModule or import MyModule, only: [func: 1]
|
|
164
158
|
for arg_child in arguments.children:
|
|
165
159
|
if arg_child.type == "alias":
|
|
166
|
-
module_name = source_code[
|
|
167
|
-
|
|
168
|
-
|
|
160
|
+
module_name = source_code[arg_child.start_byte : arg_child.end_byte].decode(
|
|
161
|
+
"utf-8"
|
|
162
|
+
)
|
|
169
163
|
imports.append(module_name)
|
|
170
164
|
|
|
171
165
|
# Recursively search children, but skip function bodies
|
|
@@ -174,9 +168,9 @@ def _find_imports_recursive(node, source_code: bytes, imports: list):
|
|
|
174
168
|
is_function_def = False
|
|
175
169
|
for call_child in child.children:
|
|
176
170
|
if call_child.type == "identifier":
|
|
177
|
-
target_text = source_code[
|
|
178
|
-
|
|
179
|
-
|
|
171
|
+
target_text = source_code[call_child.start_byte : call_child.end_byte].decode(
|
|
172
|
+
"utf-8"
|
|
173
|
+
)
|
|
180
174
|
if target_text in ["def", "defp", "defmodule"]:
|
|
181
175
|
is_function_def = True
|
|
182
176
|
break
|
|
@@ -207,17 +201,15 @@ def _find_requires_recursive(node, source_code: bytes, requires: list):
|
|
|
207
201
|
arguments = child
|
|
208
202
|
|
|
209
203
|
if target and arguments:
|
|
210
|
-
target_text = source_code[target.start_byte : target.end_byte].decode(
|
|
211
|
-
"utf-8"
|
|
212
|
-
)
|
|
204
|
+
target_text = source_code[target.start_byte : target.end_byte].decode("utf-8")
|
|
213
205
|
|
|
214
206
|
if target_text == "require":
|
|
215
207
|
# Parse the require
|
|
216
208
|
for arg_child in arguments.children:
|
|
217
209
|
if arg_child.type == "alias":
|
|
218
|
-
module_name = source_code[
|
|
219
|
-
|
|
220
|
-
|
|
210
|
+
module_name = source_code[arg_child.start_byte : arg_child.end_byte].decode(
|
|
211
|
+
"utf-8"
|
|
212
|
+
)
|
|
221
213
|
requires.append(module_name)
|
|
222
214
|
|
|
223
215
|
# Recursively search children, but skip function bodies
|
|
@@ -226,9 +218,9 @@ def _find_requires_recursive(node, source_code: bytes, requires: list):
|
|
|
226
218
|
is_function_def = False
|
|
227
219
|
for call_child in child.children:
|
|
228
220
|
if call_child.type == "identifier":
|
|
229
|
-
target_text = source_code[
|
|
230
|
-
|
|
231
|
-
|
|
221
|
+
target_text = source_code[call_child.start_byte : call_child.end_byte].decode(
|
|
222
|
+
"utf-8"
|
|
223
|
+
)
|
|
232
224
|
if target_text in ["def", "defp", "defmodule"]:
|
|
233
225
|
is_function_def = True
|
|
234
226
|
break
|
|
@@ -259,17 +251,15 @@ def _find_uses_recursive(node, source_code: bytes, uses: list):
|
|
|
259
251
|
arguments = child
|
|
260
252
|
|
|
261
253
|
if target and arguments:
|
|
262
|
-
target_text = source_code[target.start_byte : target.end_byte].decode(
|
|
263
|
-
"utf-8"
|
|
264
|
-
)
|
|
254
|
+
target_text = source_code[target.start_byte : target.end_byte].decode("utf-8")
|
|
265
255
|
|
|
266
256
|
if target_text == "use":
|
|
267
257
|
# Parse the use
|
|
268
258
|
for arg_child in arguments.children:
|
|
269
259
|
if arg_child.type == "alias":
|
|
270
|
-
module_name = source_code[
|
|
271
|
-
|
|
272
|
-
|
|
260
|
+
module_name = source_code[arg_child.start_byte : arg_child.end_byte].decode(
|
|
261
|
+
"utf-8"
|
|
262
|
+
)
|
|
273
263
|
uses.append(module_name)
|
|
274
264
|
|
|
275
265
|
# Recursively search children, but skip function bodies
|
|
@@ -278,9 +268,9 @@ def _find_uses_recursive(node, source_code: bytes, uses: list):
|
|
|
278
268
|
is_function_def = False
|
|
279
269
|
for call_child in child.children:
|
|
280
270
|
if call_child.type == "identifier":
|
|
281
|
-
target_text = source_code[
|
|
282
|
-
|
|
283
|
-
|
|
271
|
+
target_text = source_code[call_child.start_byte : call_child.end_byte].decode(
|
|
272
|
+
"utf-8"
|
|
273
|
+
)
|
|
284
274
|
if target_text in ["def", "defp", "defmodule"]:
|
|
285
275
|
is_function_def = True
|
|
286
276
|
break
|
|
@@ -319,9 +309,7 @@ def _find_behaviours_recursive(node, source_code: bytes, behaviours: list):
|
|
|
319
309
|
|
|
320
310
|
for child in behaviour_call.children:
|
|
321
311
|
if child.type == "identifier":
|
|
322
|
-
identifier_text = source_code[
|
|
323
|
-
child.start_byte : child.end_byte
|
|
324
|
-
].decode("utf-8")
|
|
312
|
+
identifier_text = source_code[child.start_byte : child.end_byte].decode("utf-8")
|
|
325
313
|
elif child.type == "arguments":
|
|
326
314
|
arguments_node = child
|
|
327
315
|
|
|
@@ -330,15 +318,15 @@ def _find_behaviours_recursive(node, source_code: bytes, behaviours: list):
|
|
|
330
318
|
for arg_child in arguments_node.children:
|
|
331
319
|
if arg_child.type == "alias":
|
|
332
320
|
# @behaviour ModuleName
|
|
333
|
-
module_name = source_code[
|
|
334
|
-
|
|
335
|
-
|
|
321
|
+
module_name = source_code[arg_child.start_byte : arg_child.end_byte].decode(
|
|
322
|
+
"utf-8"
|
|
323
|
+
)
|
|
336
324
|
behaviours.append(module_name)
|
|
337
325
|
elif arg_child.type == "atom":
|
|
338
326
|
# @behaviour :module_name
|
|
339
|
-
atom_text = source_code[
|
|
340
|
-
|
|
341
|
-
|
|
327
|
+
atom_text = source_code[arg_child.start_byte : arg_child.end_byte].decode(
|
|
328
|
+
"utf-8"
|
|
329
|
+
)
|
|
342
330
|
# Remove leading colon and convert to module format if needed
|
|
343
331
|
behaviours.append(atom_text.lstrip(":"))
|
|
344
332
|
|
|
@@ -348,9 +336,9 @@ def _find_behaviours_recursive(node, source_code: bytes, behaviours: list):
|
|
|
348
336
|
is_function_def = False
|
|
349
337
|
for call_child in child.children:
|
|
350
338
|
if call_child.type == "identifier":
|
|
351
|
-
target_text = source_code[
|
|
352
|
-
|
|
353
|
-
|
|
339
|
+
target_text = source_code[call_child.start_byte : call_child.end_byte].decode(
|
|
340
|
+
"utf-8"
|
|
341
|
+
)
|
|
354
342
|
if target_text in ["def", "defp", "defmodule"]:
|
|
355
343
|
is_function_def = True
|
|
356
344
|
break
|
cicada/extractors/doc.py
CHANGED
|
@@ -3,6 +3,7 @@ Documentation extraction logic.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import textwrap
|
|
6
|
+
|
|
6
7
|
from .base import extract_string_from_arguments
|
|
7
8
|
|
|
8
9
|
|
|
@@ -30,15 +31,13 @@ def _find_docs_recursive(node, source_code: bytes, docs: dict):
|
|
|
30
31
|
# Check if this is a doc attribute
|
|
31
32
|
for call_child in operand.children:
|
|
32
33
|
if call_child.type == "identifier":
|
|
33
|
-
attr_name = source_code[
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
attr_name = source_code[call_child.start_byte : call_child.end_byte].decode(
|
|
35
|
+
"utf-8"
|
|
36
|
+
)
|
|
36
37
|
|
|
37
38
|
if attr_name == "doc":
|
|
38
39
|
# Extract the doc definition
|
|
39
|
-
doc_info = _parse_doc(
|
|
40
|
-
operand, source_code, node.start_point[0] + 1
|
|
41
|
-
)
|
|
40
|
+
doc_info = _parse_doc(operand, source_code, node.start_point[0] + 1)
|
|
42
41
|
if doc_info:
|
|
43
42
|
# Store the entire doc_info dict (includes text and examples)
|
|
44
43
|
docs[doc_info["line"]] = doc_info
|
|
@@ -50,9 +49,9 @@ def _find_docs_recursive(node, source_code: bytes, docs: dict):
|
|
|
50
49
|
is_defmodule_or_def = False
|
|
51
50
|
for call_child in child.children:
|
|
52
51
|
if call_child.type == "identifier":
|
|
53
|
-
target_text = source_code[
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
target_text = source_code[call_child.start_byte : call_child.end_byte].decode(
|
|
53
|
+
"utf-8"
|
|
54
|
+
)
|
|
56
55
|
if target_text in ["defmodule", "def", "defp"]:
|
|
57
56
|
is_defmodule_or_def = True
|
|
58
57
|
break
|
cicada/extractors/function.py
CHANGED
|
@@ -46,9 +46,7 @@ def _extract_impl_from_prev_sibling(node, source_code: bytes):
|
|
|
46
46
|
|
|
47
47
|
for child in impl_call.children:
|
|
48
48
|
if child.type == "identifier":
|
|
49
|
-
identifier_text = source_code[child.start_byte : child.end_byte].decode(
|
|
50
|
-
"utf-8"
|
|
51
|
-
)
|
|
49
|
+
identifier_text = source_code[child.start_byte : child.end_byte].decode("utf-8")
|
|
52
50
|
elif child.type == "arguments":
|
|
53
51
|
arguments_node = child
|
|
54
52
|
|
|
@@ -60,15 +58,11 @@ def _extract_impl_from_prev_sibling(node, source_code: bytes):
|
|
|
60
58
|
for arg_child in arguments_node.children:
|
|
61
59
|
if arg_child.type == "boolean":
|
|
62
60
|
# @impl true or @impl false
|
|
63
|
-
bool_text = source_code[
|
|
64
|
-
arg_child.start_byte : arg_child.end_byte
|
|
65
|
-
].decode("utf-8")
|
|
61
|
+
bool_text = source_code[arg_child.start_byte : arg_child.end_byte].decode("utf-8")
|
|
66
62
|
return bool_text == "true"
|
|
67
63
|
elif arg_child.type == "alias":
|
|
68
64
|
# @impl ModuleName
|
|
69
|
-
module_name = source_code[
|
|
70
|
-
arg_child.start_byte : arg_child.end_byte
|
|
71
|
-
].decode("utf-8")
|
|
65
|
+
module_name = source_code[arg_child.start_byte : arg_child.end_byte].decode("utf-8")
|
|
72
66
|
return module_name
|
|
73
67
|
|
|
74
68
|
# @impl without arguments defaults to true
|
|
@@ -96,15 +90,11 @@ def _find_functions_recursive(node, source_code: bytes, functions: list):
|
|
|
96
90
|
|
|
97
91
|
# Check if this is a def or defp call
|
|
98
92
|
if target and arguments:
|
|
99
|
-
target_text = source_code[target.start_byte : target.end_byte].decode(
|
|
100
|
-
"utf-8"
|
|
101
|
-
)
|
|
93
|
+
target_text = source_code[target.start_byte : target.end_byte].decode("utf-8")
|
|
102
94
|
|
|
103
95
|
if target_text in ["def", "defp"]:
|
|
104
96
|
# Check if previous sibling is @impl
|
|
105
|
-
impl_value = _extract_impl_from_prev_sibling(
|
|
106
|
-
prev_sibling, source_code
|
|
107
|
-
)
|
|
97
|
+
impl_value = _extract_impl_from_prev_sibling(prev_sibling, source_code)
|
|
108
98
|
|
|
109
99
|
# Extract function name and arity
|
|
110
100
|
func_info = _parse_function_definition(
|
|
@@ -143,9 +133,9 @@ def _parse_function_definition(
|
|
|
143
133
|
# Extract function name from call target
|
|
144
134
|
for call_child in arg_child.children:
|
|
145
135
|
if call_child.type == "identifier":
|
|
146
|
-
func_name = source_code[
|
|
147
|
-
|
|
148
|
-
|
|
136
|
+
func_name = source_code[call_child.start_byte : call_child.end_byte].decode(
|
|
137
|
+
"utf-8"
|
|
138
|
+
)
|
|
149
139
|
elif call_child.type == "arguments":
|
|
150
140
|
arg_names = _extract_argument_names(call_child, source_code)
|
|
151
141
|
arity = len(arg_names)
|
|
@@ -167,9 +157,7 @@ def _parse_function_definition(
|
|
|
167
157
|
break
|
|
168
158
|
break
|
|
169
159
|
elif arg_child.type == "identifier":
|
|
170
|
-
func_name = source_code[arg_child.start_byte : arg_child.end_byte].decode(
|
|
171
|
-
"utf-8"
|
|
172
|
-
)
|
|
160
|
+
func_name = source_code[arg_child.start_byte : arg_child.end_byte].decode("utf-8")
|
|
173
161
|
arity = 0
|
|
174
162
|
arg_names = []
|
|
175
163
|
break
|
|
@@ -221,9 +209,9 @@ def _extract_guards(arguments_node, source_code: bytes) -> list[str]:
|
|
|
221
209
|
elif has_when:
|
|
222
210
|
# This is the guard expression node (comes after 'when')
|
|
223
211
|
# It's typically a binary_operator (like n < 0)
|
|
224
|
-
guard_expr = source_code[
|
|
225
|
-
|
|
226
|
-
|
|
212
|
+
guard_expr = source_code[op_child.start_byte : op_child.end_byte].decode(
|
|
213
|
+
"utf-8"
|
|
214
|
+
)
|
|
227
215
|
guards.append(guard_expr)
|
|
228
216
|
break
|
|
229
217
|
|