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
cicada/find_dead_code.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI tool for finding dead code (unused public functions) in Elixir codebases.
|
|
3
|
+
|
|
4
|
+
Analyzes the indexed codebase to identify potentially unused public functions
|
|
5
|
+
with confidence levels based on usage patterns.
|
|
6
|
+
|
|
7
|
+
Author: Cursor(Auto)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from cicada.dead_code_analyzer import DeadCodeAnalyzer
|
|
16
|
+
from cicada.utils import load_index
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def format_markdown(results: dict) -> str:
|
|
20
|
+
"""
|
|
21
|
+
Format analysis results as markdown.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
results: Analysis results from DeadCodeAnalyzer
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Formatted markdown string
|
|
28
|
+
"""
|
|
29
|
+
lines = ["# Dead Code Analysis\n"]
|
|
30
|
+
|
|
31
|
+
summary = results["summary"]
|
|
32
|
+
lines.append(
|
|
33
|
+
f"Analyzed {summary['analyzed']} public functions "
|
|
34
|
+
f"(skipped {summary['skipped_impl']} with @impl, "
|
|
35
|
+
f"{summary['skipped_files']} in test/script files)"
|
|
36
|
+
)
|
|
37
|
+
lines.append(
|
|
38
|
+
f"Found **{summary['total_candidates']} potentially unused functions**\n"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
candidates = results["candidates"]
|
|
42
|
+
|
|
43
|
+
# High confidence
|
|
44
|
+
if candidates["high"]:
|
|
45
|
+
count = len(candidates["high"])
|
|
46
|
+
label = f" HIGH CONFIDENCE ({count} function{'s' if count != 1 else ''}) "
|
|
47
|
+
bar_length = 80
|
|
48
|
+
padding = (bar_length - len(label)) // 2
|
|
49
|
+
lines.append(
|
|
50
|
+
f"\n{'═' * padding}{label}{'═' * (bar_length - padding - len(label))}"
|
|
51
|
+
)
|
|
52
|
+
lines.append("Functions with zero usage in codebase\n")
|
|
53
|
+
|
|
54
|
+
# Group by module
|
|
55
|
+
by_module = {}
|
|
56
|
+
for c in candidates["high"]:
|
|
57
|
+
if c["module"] not in by_module:
|
|
58
|
+
by_module[c["module"]] = []
|
|
59
|
+
by_module[c["module"]].append(c)
|
|
60
|
+
|
|
61
|
+
for module, funcs in sorted(by_module.items()):
|
|
62
|
+
lines.append(f"### {module}")
|
|
63
|
+
lines.append(f"{funcs[0]['file']}\n")
|
|
64
|
+
for func in funcs:
|
|
65
|
+
lines.append(
|
|
66
|
+
f"- `{func['function']}/{func['arity']}` (line {func['line']})"
|
|
67
|
+
)
|
|
68
|
+
lines.append("")
|
|
69
|
+
|
|
70
|
+
# Medium confidence
|
|
71
|
+
if candidates["medium"]:
|
|
72
|
+
count = len(candidates["medium"])
|
|
73
|
+
label = f" MEDIUM CONFIDENCE ({count} function{'s' if count != 1 else ''}) "
|
|
74
|
+
bar_length = 80
|
|
75
|
+
padding = (bar_length - len(label)) // 2
|
|
76
|
+
lines.append(
|
|
77
|
+
f"\n{'═' * padding}{label}{'═' * (bar_length - padding - len(label))}"
|
|
78
|
+
)
|
|
79
|
+
lines.append(
|
|
80
|
+
"Functions with zero usage, but module has behaviors/uses (possible callbacks)\n"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Group by module
|
|
84
|
+
by_module = {}
|
|
85
|
+
for c in candidates["medium"]:
|
|
86
|
+
if c["module"] not in by_module:
|
|
87
|
+
by_module[c["module"]] = []
|
|
88
|
+
by_module[c["module"]].append(c)
|
|
89
|
+
|
|
90
|
+
for module, funcs in sorted(by_module.items()):
|
|
91
|
+
lines.append(f"### {module}")
|
|
92
|
+
lines.append(f"{funcs[0]['file']}")
|
|
93
|
+
|
|
94
|
+
# Show behaviors/uses
|
|
95
|
+
behaviours = funcs[0].get("behaviours", [])
|
|
96
|
+
uses = funcs[0].get("uses", [])
|
|
97
|
+
if behaviours:
|
|
98
|
+
lines.append(f"**Behaviours:** {', '.join(behaviours)}")
|
|
99
|
+
if uses:
|
|
100
|
+
lines.append(f"**Uses:** {', '.join(uses)}")
|
|
101
|
+
lines.append("")
|
|
102
|
+
|
|
103
|
+
for func in funcs:
|
|
104
|
+
lines.append(
|
|
105
|
+
f"- `{func['function']}/{func['arity']}` (line {func['line']})"
|
|
106
|
+
)
|
|
107
|
+
lines.append("")
|
|
108
|
+
|
|
109
|
+
# Low confidence
|
|
110
|
+
if candidates["low"]:
|
|
111
|
+
count = len(candidates["low"])
|
|
112
|
+
label = f" LOW CONFIDENCE ({count} function{'s' if count != 1 else ''}) "
|
|
113
|
+
bar_length = 80
|
|
114
|
+
padding = (bar_length - len(label)) // 2
|
|
115
|
+
lines.append(
|
|
116
|
+
f"\n{'═' * padding}{label}{'═' * (bar_length - padding - len(label))}"
|
|
117
|
+
)
|
|
118
|
+
lines.append(
|
|
119
|
+
"Functions with zero usage, but module passed as value (possible dynamic calls)\n"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Group by module
|
|
123
|
+
by_module = {}
|
|
124
|
+
for c in candidates["low"]:
|
|
125
|
+
if c["module"] not in by_module:
|
|
126
|
+
by_module[c["module"]] = []
|
|
127
|
+
by_module[c["module"]].append(c)
|
|
128
|
+
|
|
129
|
+
for module, funcs in sorted(by_module.items()):
|
|
130
|
+
lines.append(f"### {module}")
|
|
131
|
+
lines.append(f"{funcs[0]['file']}")
|
|
132
|
+
|
|
133
|
+
# Show where module is mentioned as value
|
|
134
|
+
mentioned_in = funcs[0].get("mentioned_in", [])
|
|
135
|
+
if mentioned_in:
|
|
136
|
+
lines.append("**Module mentioned as value in:**")
|
|
137
|
+
for mention in mentioned_in:
|
|
138
|
+
lines.append(f"- {mention['module']} ({mention['file']})")
|
|
139
|
+
lines.append("")
|
|
140
|
+
|
|
141
|
+
for func in funcs:
|
|
142
|
+
lines.append(
|
|
143
|
+
f"- `{func['function']}/{func['arity']}` (line {func['line']})"
|
|
144
|
+
)
|
|
145
|
+
lines.append("")
|
|
146
|
+
|
|
147
|
+
if summary["total_candidates"] == 0:
|
|
148
|
+
lines.append("\n*No dead code candidates found!*\n")
|
|
149
|
+
|
|
150
|
+
return "\n".join(lines)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def format_json(results: dict) -> str:
|
|
154
|
+
"""
|
|
155
|
+
Format analysis results as JSON.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
results: Analysis results from DeadCodeAnalyzer
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
JSON string
|
|
162
|
+
"""
|
|
163
|
+
return json.dumps(results, indent=2)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def filter_by_confidence(results: dict, min_confidence: str) -> dict:
|
|
167
|
+
"""
|
|
168
|
+
Filter results to only show candidates at or above minimum confidence level.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
results: Analysis results
|
|
172
|
+
min_confidence: Minimum confidence level ("high", "medium", or "low")
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Filtered results
|
|
176
|
+
"""
|
|
177
|
+
if min_confidence == "low":
|
|
178
|
+
# Show all
|
|
179
|
+
return results
|
|
180
|
+
elif min_confidence == "medium":
|
|
181
|
+
# Show only medium and high
|
|
182
|
+
results["candidates"]["low"] = []
|
|
183
|
+
else: # high
|
|
184
|
+
# Show only high
|
|
185
|
+
results["candidates"]["medium"] = []
|
|
186
|
+
results["candidates"]["low"] = []
|
|
187
|
+
|
|
188
|
+
# Recalculate total
|
|
189
|
+
results["summary"]["total_candidates"] = sum(
|
|
190
|
+
len(results["candidates"][level]) for level in ["high", "medium", "low"]
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
return results
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def main():
|
|
197
|
+
"""Main entry point for the dead code finder CLI."""
|
|
198
|
+
parser = argparse.ArgumentParser(
|
|
199
|
+
description="Find potentially unused public functions in Elixir codebase",
|
|
200
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
201
|
+
epilog="""
|
|
202
|
+
Confidence Levels:
|
|
203
|
+
high - Zero usage, no dynamic call indicators, no behaviors/uses
|
|
204
|
+
medium - Zero usage, but module has behaviors or uses (possible callbacks)
|
|
205
|
+
low - Zero usage, but module passed as value (possible dynamic calls)
|
|
206
|
+
|
|
207
|
+
Examples:
|
|
208
|
+
cicada-find-dead-code # Show high confidence candidates
|
|
209
|
+
cicada-find-dead-code --min-confidence low # Show all candidates
|
|
210
|
+
cicada-find-dead-code --format json # Output as JSON
|
|
211
|
+
""",
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
parser.add_argument(
|
|
215
|
+
"--index",
|
|
216
|
+
default=".cicada/index.json",
|
|
217
|
+
help="Path to index file (default: .cicada/index.json)",
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
parser.add_argument(
|
|
221
|
+
"--format",
|
|
222
|
+
choices=["markdown", "json"],
|
|
223
|
+
default="markdown",
|
|
224
|
+
help="Output format (default: markdown)",
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
parser.add_argument(
|
|
228
|
+
"--min-confidence",
|
|
229
|
+
choices=["high", "medium", "low"],
|
|
230
|
+
default="high",
|
|
231
|
+
help="Minimum confidence level to show (default: high)",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
args = parser.parse_args()
|
|
235
|
+
|
|
236
|
+
# Load index
|
|
237
|
+
index_path = Path(args.index)
|
|
238
|
+
if not index_path.exists():
|
|
239
|
+
print(f"Error: Index file not found: {index_path}", file=sys.stderr)
|
|
240
|
+
print(f"\nRun 'cicada-index' first to create the index.", file=sys.stderr)
|
|
241
|
+
sys.exit(1)
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
index = load_index(index_path, raise_on_error=True)
|
|
245
|
+
except Exception as e:
|
|
246
|
+
print(f"Error loading index: {e}", file=sys.stderr)
|
|
247
|
+
sys.exit(1)
|
|
248
|
+
|
|
249
|
+
if index is None:
|
|
250
|
+
print(f"Error: Could not load index from {index_path}", file=sys.stderr)
|
|
251
|
+
sys.exit(1)
|
|
252
|
+
|
|
253
|
+
# Run analysis
|
|
254
|
+
analyzer = DeadCodeAnalyzer(index)
|
|
255
|
+
results = analyzer.analyze()
|
|
256
|
+
|
|
257
|
+
# Filter by confidence
|
|
258
|
+
results = filter_by_confidence(results, args.min_confidence)
|
|
259
|
+
|
|
260
|
+
# Format output
|
|
261
|
+
if args.format == "json":
|
|
262
|
+
output = format_json(results)
|
|
263
|
+
else:
|
|
264
|
+
output = format_markdown(results)
|
|
265
|
+
|
|
266
|
+
print(output)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
if __name__ == "__main__":
|
|
270
|
+
main()
|