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.

Files changed (48) hide show
  1. cicada/__init__.py +30 -0
  2. cicada/clean.py +297 -0
  3. cicada/command_logger.py +293 -0
  4. cicada/dead_code_analyzer.py +282 -0
  5. cicada/extractors/__init__.py +36 -0
  6. cicada/extractors/base.py +66 -0
  7. cicada/extractors/call.py +176 -0
  8. cicada/extractors/dependency.py +361 -0
  9. cicada/extractors/doc.py +179 -0
  10. cicada/extractors/function.py +246 -0
  11. cicada/extractors/module.py +123 -0
  12. cicada/extractors/spec.py +151 -0
  13. cicada/find_dead_code.py +270 -0
  14. cicada/formatter.py +918 -0
  15. cicada/git_helper.py +646 -0
  16. cicada/indexer.py +629 -0
  17. cicada/install.py +724 -0
  18. cicada/keyword_extractor.py +364 -0
  19. cicada/keyword_search.py +553 -0
  20. cicada/lightweight_keyword_extractor.py +298 -0
  21. cicada/mcp_server.py +1559 -0
  22. cicada/mcp_tools.py +291 -0
  23. cicada/parser.py +124 -0
  24. cicada/pr_finder.py +435 -0
  25. cicada/pr_indexer/__init__.py +20 -0
  26. cicada/pr_indexer/cli.py +62 -0
  27. cicada/pr_indexer/github_api_client.py +431 -0
  28. cicada/pr_indexer/indexer.py +297 -0
  29. cicada/pr_indexer/line_mapper.py +209 -0
  30. cicada/pr_indexer/pr_index_builder.py +253 -0
  31. cicada/setup.py +339 -0
  32. cicada/utils/__init__.py +52 -0
  33. cicada/utils/call_site_formatter.py +95 -0
  34. cicada/utils/function_grouper.py +57 -0
  35. cicada/utils/hash_utils.py +173 -0
  36. cicada/utils/index_utils.py +290 -0
  37. cicada/utils/path_utils.py +240 -0
  38. cicada/utils/signature_builder.py +106 -0
  39. cicada/utils/storage.py +111 -0
  40. cicada/utils/subprocess_runner.py +182 -0
  41. cicada/utils/text_utils.py +90 -0
  42. cicada/version_check.py +116 -0
  43. cicada_mcp-0.1.4.dist-info/METADATA +619 -0
  44. cicada_mcp-0.1.4.dist-info/RECORD +48 -0
  45. cicada_mcp-0.1.4.dist-info/WHEEL +5 -0
  46. cicada_mcp-0.1.4.dist-info/entry_points.txt +8 -0
  47. cicada_mcp-0.1.4.dist-info/licenses/LICENSE +21 -0
  48. cicada_mcp-0.1.4.dist-info/top_level.txt +1 -0
@@ -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()