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.
Files changed (53) hide show
  1. cicada/ascii_art.py +60 -0
  2. cicada/clean.py +195 -60
  3. cicada/cli.py +757 -0
  4. cicada/colors.py +27 -0
  5. cicada/command_logger.py +14 -16
  6. cicada/dead_code_analyzer.py +12 -19
  7. cicada/extractors/__init__.py +6 -6
  8. cicada/extractors/base.py +3 -3
  9. cicada/extractors/call.py +11 -15
  10. cicada/extractors/dependency.py +39 -51
  11. cicada/extractors/doc.py +8 -9
  12. cicada/extractors/function.py +12 -24
  13. cicada/extractors/module.py +11 -15
  14. cicada/extractors/spec.py +8 -12
  15. cicada/find_dead_code.py +15 -39
  16. cicada/formatter.py +37 -91
  17. cicada/git_helper.py +22 -34
  18. cicada/indexer.py +165 -132
  19. cicada/interactive_setup.py +490 -0
  20. cicada/keybert_extractor.py +286 -0
  21. cicada/keyword_search.py +22 -30
  22. cicada/keyword_test.py +127 -0
  23. cicada/lightweight_keyword_extractor.py +5 -13
  24. cicada/mcp_entry.py +683 -0
  25. cicada/mcp_server.py +110 -232
  26. cicada/parser.py +9 -9
  27. cicada/pr_finder.py +15 -19
  28. cicada/pr_indexer/__init__.py +3 -3
  29. cicada/pr_indexer/cli.py +4 -9
  30. cicada/pr_indexer/github_api_client.py +22 -37
  31. cicada/pr_indexer/indexer.py +17 -29
  32. cicada/pr_indexer/line_mapper.py +8 -12
  33. cicada/pr_indexer/pr_index_builder.py +22 -34
  34. cicada/setup.py +198 -89
  35. cicada/utils/__init__.py +9 -9
  36. cicada/utils/call_site_formatter.py +4 -6
  37. cicada/utils/function_grouper.py +4 -4
  38. cicada/utils/hash_utils.py +12 -15
  39. cicada/utils/index_utils.py +15 -15
  40. cicada/utils/path_utils.py +24 -29
  41. cicada/utils/signature_builder.py +3 -3
  42. cicada/utils/subprocess_runner.py +17 -19
  43. cicada/utils/text_utils.py +1 -2
  44. cicada/version_check.py +2 -5
  45. {cicada_mcp-0.1.5.dist-info → cicada_mcp-0.2.0.dist-info}/METADATA +144 -55
  46. cicada_mcp-0.2.0.dist-info/RECORD +53 -0
  47. cicada_mcp-0.2.0.dist-info/entry_points.txt +4 -0
  48. cicada/install.py +0 -741
  49. cicada_mcp-0.1.5.dist-info/RECORD +0 -47
  50. cicada_mcp-0.1.5.dist-info/entry_points.txt +0 -9
  51. {cicada_mcp-0.1.5.dist-info → cicada_mcp-0.2.0.dist-info}/WHEEL +0 -0
  52. {cicada_mcp-0.1.5.dist-info → cicada_mcp-0.2.0.dist-info}/licenses/LICENSE +0 -0
  53. {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, Dict, Optional
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: Optional[str] = None):
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: Dict[str, Any],
50
+ arguments: dict[str, Any],
51
51
  response: Any,
52
52
  execution_time_ms: float,
53
- timestamp: Optional[datetime] = None,
54
- error: Optional[str] = None,
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: Dict[str, Any],
105
+ arguments: dict[str, Any],
106
106
  response: Any,
107
107
  execution_time_ms: float,
108
- timestamp: Optional[datetime] = None,
109
- error: Optional[str] = None,
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[Dict[str, Any]]:
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, "r", encoding="utf-8") as f:
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: Optional[int] = None) -> int:
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: Optional[CommandLogger] = None
276
+ _global_logger: CommandLogger | None = None
279
277
 
280
278
 
281
- def get_logger(log_dir: Optional[str] = None) -> CommandLogger:
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:
@@ -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 function_def_line and call["line"] < function_def_line:
207
- if (function_def_line - call["line"]) <= 5:
208
- continue
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 other_module, module_data in self.modules.items():
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) -> List[dict]:
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
 
@@ -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 .module import extract_modules
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 .call import extract_function_calls, extract_value_mentions
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
- string_child.start_byte : string_child.end_byte
17
- ].decode("utf-8")
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
- dot_child.start_byte : dot_child.end_byte
72
- ].decode("utf-8")
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
- dot_child.start_byte : dot_child.end_byte
76
- ].decode("utf-8")
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
- child.start_byte : child.end_byte
156
- ].decode("utf-8")
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",
@@ -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
- call_child.start_byte : call_child.end_byte
47
- ].decode("utf-8")
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
- dot_child.start_byte : dot_child.end_byte
89
- ].decode("utf-8")
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
- arg_child.start_byte : arg_child.end_byte
168
- ].decode("utf-8")
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
- call_child.start_byte : call_child.end_byte
179
- ].decode("utf-8")
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
- arg_child.start_byte : arg_child.end_byte
220
- ].decode("utf-8")
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
- call_child.start_byte : call_child.end_byte
231
- ].decode("utf-8")
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
- arg_child.start_byte : arg_child.end_byte
272
- ].decode("utf-8")
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
- call_child.start_byte : call_child.end_byte
283
- ].decode("utf-8")
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
- arg_child.start_byte : arg_child.end_byte
335
- ].decode("utf-8")
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
- arg_child.start_byte : arg_child.end_byte
341
- ].decode("utf-8")
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
- call_child.start_byte : call_child.end_byte
353
- ].decode("utf-8")
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
- call_child.start_byte : call_child.end_byte
35
- ].decode("utf-8")
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
- call_child.start_byte : call_child.end_byte
55
- ].decode("utf-8")
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
@@ -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
- call_child.start_byte : call_child.end_byte
148
- ].decode("utf-8")
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
- op_child.start_byte : op_child.end_byte
226
- ].decode("utf-8")
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