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,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