cli-usereq 0.68.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 (37) hide show
  1. cli_usereq-0.68.0.dist-info/METADATA +1134 -0
  2. cli_usereq-0.68.0.dist-info/RECORD +37 -0
  3. cli_usereq-0.68.0.dist-info/WHEEL +5 -0
  4. cli_usereq-0.68.0.dist-info/entry_points.txt +4 -0
  5. cli_usereq-0.68.0.dist-info/licenses/LICENSE +674 -0
  6. cli_usereq-0.68.0.dist-info/top_level.txt +1 -0
  7. usereq/__init__.py +62 -0
  8. usereq/__main__.py +17 -0
  9. usereq/cli.py +4963 -0
  10. usereq/compress.py +393 -0
  11. usereq/compress_files.py +134 -0
  12. usereq/doxygen_parser.py +172 -0
  13. usereq/find_constructs.py +394 -0
  14. usereq/generate_markdown.py +158 -0
  15. usereq/resources/docs/Document_Source_Code_in_Doxygen_Style.md +130 -0
  16. usereq/resources/docs/HDT_Test_Authoring_Guide.md +318 -0
  17. usereq/resources/docs/Requirements_Template.md +78 -0
  18. usereq/resources/prompts/analyze.md +191 -0
  19. usereq/resources/prompts/change.md +279 -0
  20. usereq/resources/prompts/check.md +190 -0
  21. usereq/resources/prompts/cover.md +271 -0
  22. usereq/resources/prompts/create.md +134 -0
  23. usereq/resources/prompts/fix.md +273 -0
  24. usereq/resources/prompts/flowchart.md +272 -0
  25. usereq/resources/prompts/implement.md +194 -0
  26. usereq/resources/prompts/new.md +278 -0
  27. usereq/resources/prompts/readme.md +234 -0
  28. usereq/resources/prompts/recreate.md +265 -0
  29. usereq/resources/prompts/refactor.md +265 -0
  30. usereq/resources/prompts/references.md +131 -0
  31. usereq/resources/prompts/renumber.md +150 -0
  32. usereq/resources/prompts/workflow.md +254 -0
  33. usereq/resources/prompts/write.md +129 -0
  34. usereq/resources/prompts-version.txt +1 -0
  35. usereq/source_analyzer.py +2280 -0
  36. usereq/static_check.py +812 -0
  37. usereq/token_counter.py +123 -0
@@ -0,0 +1,2280 @@
1
+ #!/usr/bin/env python3
2
+ """!
3
+ @file source_analyzer.py
4
+ @brief Multi-language source code analyzer.
5
+ @details Inspired by tree-sitter, this module analyzes source files across multiple programming languages, extracting: - Definitions of functions, methods, classes, structs, enums, traits, interfaces, modules, components and other constructs - Comments (single-line and multi-line) in language-specific syntax - A structured listing of the entire file with line number prefixes
6
+ @author GitHub Copilot
7
+ @version 0.0.70
8
+ """
9
+
10
+ import argparse
11
+ import os
12
+ import re
13
+ import sys
14
+ from dataclasses import dataclass, field
15
+ from enum import Enum, auto
16
+ from typing import Optional
17
+
18
+ try:
19
+ from .doxygen_parser import parse_doxygen_comment
20
+ except ImportError:
21
+ from usereq.doxygen_parser import parse_doxygen_comment
22
+
23
+
24
+ class ElementType(Enum):
25
+ """! @brief Element types recognized in source code.
26
+ @details Enumeration of all supported syntactic constructs across languages.
27
+ """
28
+ FUNCTION = auto()
29
+ METHOD = auto()
30
+ CLASS = auto()
31
+ STRUCT = auto()
32
+ ENUM = auto()
33
+ TRAIT = auto()
34
+ INTERFACE = auto()
35
+ MODULE = auto()
36
+ IMPL = auto()
37
+ MACRO = auto()
38
+ CONSTANT = auto()
39
+ VARIABLE = auto()
40
+ TYPE_ALIAS = auto()
41
+ IMPORT = auto()
42
+ DECORATOR = auto()
43
+ COMMENT_SINGLE = auto()
44
+ COMMENT_MULTI = auto()
45
+ COMPONENT = auto()
46
+ PROTOCOL = auto()
47
+ EXTENSION = auto()
48
+ UNION = auto()
49
+ NAMESPACE = auto()
50
+ PROPERTY = auto()
51
+ SIGNAL = auto()
52
+ TYPEDEF = auto()
53
+
54
+
55
+ @dataclass
56
+ class SourceElement:
57
+ """! @brief Element found in source file.
58
+ @details Data class representing a single extracted code construct with its metadata.
59
+ """
60
+ element_type: ElementType
61
+ line_start: int
62
+ line_end: int
63
+ extract: str
64
+ """! @brief Truncated display text of the element (max 5 lines per SRS-133)."""
65
+ name: Optional[str] = None
66
+ signature: Optional[str] = None
67
+ visibility: Optional[str] = None
68
+ parent_name: Optional[str] = None
69
+ inherits: Optional[str] = None
70
+ depth: int = 0
71
+ comment_source: Optional[str] = None
72
+ """! @brief Full untruncated comment text for COMMENT_MULTI elements.
73
+ @details Preserves the complete multi-line comment content for Doxygen field
74
+ extraction (SRS-143) while allowing extract to remain truncated (SRS-133).
75
+ None for non-comment elements and single-line comments.
76
+ """
77
+ body_comments: list = field(default_factory=list)
78
+ exit_points: list = field(default_factory=list)
79
+ doxygen_fields: dict = field(default_factory=dict)
80
+
81
+ @property
82
+ def type_label(self) -> str:
83
+ """! @brief Return the normalized printable label for element_type.
84
+ @return Stable uppercase label used in markdown rendering output.
85
+ @details Maps internal ElementType enum to a string representation for reporting.
86
+ """
87
+ labels = {
88
+ ElementType.FUNCTION: "FUNCTION",
89
+ ElementType.METHOD: "METHOD",
90
+ ElementType.CLASS: "CLASS",
91
+ ElementType.STRUCT: "STRUCT",
92
+ ElementType.ENUM: "ENUM",
93
+ ElementType.TRAIT: "TRAIT",
94
+ ElementType.INTERFACE: "INTERFACE",
95
+ ElementType.MODULE: "MODULE",
96
+ ElementType.IMPL: "IMPL",
97
+ ElementType.MACRO: "MACRO",
98
+ ElementType.CONSTANT: "CONSTANT",
99
+ ElementType.VARIABLE: "VARIABLE",
100
+ ElementType.TYPE_ALIAS: "TYPE_ALIAS",
101
+ ElementType.IMPORT: "IMPORT",
102
+ ElementType.DECORATOR: "DECORATOR",
103
+ ElementType.COMMENT_SINGLE: "COMMENT",
104
+ ElementType.COMMENT_MULTI: "COMMENT",
105
+ ElementType.COMPONENT: "COMPONENT",
106
+ ElementType.PROTOCOL: "PROTOCOL",
107
+ ElementType.EXTENSION: "EXTENSION",
108
+ ElementType.UNION: "UNION",
109
+ ElementType.NAMESPACE: "NAMESPACE",
110
+ ElementType.PROPERTY: "PROPERTY",
111
+ ElementType.SIGNAL: "SIGNAL",
112
+ ElementType.TYPEDEF: "TYPEDEF",
113
+ }
114
+ return labels.get(self.element_type, "UNKNOWN")
115
+
116
+
117
+ @dataclass
118
+ class LanguageSpec:
119
+ """! @brief Language recognition pattern specification.
120
+ @details Holds regex patterns and configuration for parsing a specific programming language.
121
+ """
122
+ name: str
123
+ single_comment: Optional[str] = None
124
+ multi_comment_start: Optional[str] = None
125
+ multi_comment_end: Optional[str] = None
126
+ string_delimiters: tuple = ("\"", "'")
127
+ patterns: list = field(default_factory=list)
128
+
129
+
130
+ def build_language_specs() -> dict:
131
+ """!
132
+ @brief Build specifications for all supported languages.
133
+ @details Implements the build_language_specs function behavior with deterministic control flow.
134
+ @return {dict} Function return value.
135
+ """
136
+ specs = {}
137
+
138
+ # ── Python ──────────────────────────────────────────────────────────
139
+ specs["python"] = LanguageSpec(
140
+ name="Python",
141
+ single_comment="#",
142
+ multi_comment_start='"""',
143
+ multi_comment_end='"""',
144
+ string_delimiters=("\"", "'", '"""', "'''"),
145
+ patterns=[
146
+ (ElementType.CLASS, re.compile(
147
+ r"^(\s*class\s+(\w+)\s*[\(:])")),
148
+ (ElementType.FUNCTION, re.compile(
149
+ r"^(\s*(?:async\s+)?def\s+(\w+)\s*\()")),
150
+ (ElementType.DECORATOR, re.compile(
151
+ r"^(\s*@(\w[\w.]*)\s*)")),
152
+ (ElementType.IMPORT, re.compile(
153
+ r"^(\s*(?:from\s+\S+\s+)?import\s+(.+))")),
154
+ (ElementType.VARIABLE, re.compile(
155
+ r"^(\s*([A-Z][A-Z_0-9]+)\s*=\s*)")),
156
+ ],
157
+ )
158
+
159
+ # ── C ───────────────────────────────────────────────────────────────
160
+ specs["c"] = LanguageSpec(
161
+ name="C",
162
+ single_comment="//",
163
+ multi_comment_start="/*",
164
+ multi_comment_end="*/",
165
+ patterns=[
166
+ (ElementType.STRUCT, re.compile(
167
+ r"^(\s*(?:typedef\s+)?struct\s+(\w+))")),
168
+ (ElementType.UNION, re.compile(
169
+ r"^(\s*(?:typedef\s+)?union\s+(\w+))")),
170
+ (ElementType.ENUM, re.compile(
171
+ r"^(\s*(?:typedef\s+)?enum\s+(\w+))")),
172
+ (ElementType.TYPEDEF, re.compile(
173
+ r"^(\s*typedef\s+.+?\s+(\w+)\s*;)")),
174
+ (ElementType.MACRO, re.compile(
175
+ r"^(\s*#\s*define\s+(\w+))")),
176
+ (ElementType.FUNCTION, re.compile(
177
+ r"^(\s*(?:static\s+|inline\s+|extern\s+|const\s+)*"
178
+ r"(?:(?:unsigned|signed|long|short|volatile|register)\s+)*"
179
+ r"(?:void|int|char|float|double|long|short|unsigned|signed|"
180
+ r"size_t|ssize_t|uint\d+_t|int\d+_t|bool|_Bool|FILE|"
181
+ r"\w+_t|\w+)\s+(?:\*+\s*)?"
182
+ r"(\w+)\s*\()")),
183
+ (ElementType.IMPORT, re.compile(
184
+ r"^(\s*#\s*include\s+(.+))")),
185
+ (ElementType.VARIABLE, re.compile(
186
+ r"^(\s*(?:static\s+|extern\s+|const\s+)*"
187
+ r"(?:const\s+)?(?:char|int|float|double|void|long|short|unsigned|signed|"
188
+ r"size_t|bool|_Bool|\w+_t)\s*\**\s+"
189
+ r"(\w+)\s*(?:=|;|\[))")),
190
+ ],
191
+ )
192
+
193
+ # ── C++ ─────────────────────────────────────────────────────────────
194
+ specs["cpp"] = LanguageSpec(
195
+ name="C++",
196
+ single_comment="//",
197
+ multi_comment_start="/*",
198
+ multi_comment_end="*/",
199
+ patterns=[
200
+ (ElementType.CLASS, re.compile(
201
+ r"^(\s*(?:template\s*<[^>]*>\s*)?class\s+(\w+))")),
202
+ (ElementType.STRUCT, re.compile(
203
+ r"^(\s*(?:template\s*<[^>]*>\s*)?struct\s+(\w+))")),
204
+ (ElementType.ENUM, re.compile(
205
+ r"^(\s*enum\s+(?:class\s+)?(\w+))")),
206
+ (ElementType.NAMESPACE, re.compile(
207
+ r"^(\s*namespace\s+(\w+))")),
208
+ (ElementType.FUNCTION, re.compile(
209
+ r"^(\s*(?:static\s+|inline\s+|virtual\s+|explicit\s+|"
210
+ r"constexpr\s+|consteval\s+|constinit\s+|extern\s+|const\s+)*"
211
+ r"(?:auto|void|int|char|float|double|long|short|unsigned|signed|"
212
+ r"bool|string|wstring|size_t|\w+(?:::\w+)*)\s*[&*]*\s*"
213
+ r"(\w+(?:::\w+)*)\s*\()")),
214
+ (ElementType.MACRO, re.compile(
215
+ r"^(\s*#\s*define\s+(\w+))")),
216
+ (ElementType.IMPORT, re.compile(
217
+ r"^(\s*#\s*include\s+(.+))")),
218
+ (ElementType.TYPE_ALIAS, re.compile(
219
+ r"^(\s*(?:using|typedef)\s+(\w+))")),
220
+ ],
221
+ )
222
+
223
+ # ── Rust ────────────────────────────────────────────────────────────
224
+ specs["rust"] = LanguageSpec(
225
+ name="Rust",
226
+ single_comment="//",
227
+ multi_comment_start="/*",
228
+ multi_comment_end="*/",
229
+ patterns=[
230
+ (ElementType.FUNCTION, re.compile(
231
+ r"^(\s*(?:pub(?:\(\w+\))?\s+)?(?:async\s+)?(?:unsafe\s+)?(?:extern\s+\"C\"\s+)?fn\s+(\w+))")),
232
+ (ElementType.STRUCT, re.compile(
233
+ r"^(\s*(?:pub(?:\(\w+\))?\s+)?struct\s+(\w+))")),
234
+ (ElementType.ENUM, re.compile(
235
+ r"^(\s*(?:pub(?:\(\w+\))?\s+)?enum\s+(\w+))")),
236
+ (ElementType.TRAIT, re.compile(
237
+ r"^(\s*(?:pub(?:\(\w+\))?\s+)?(?:unsafe\s+)?trait\s+(\w+))")),
238
+ (ElementType.IMPL, re.compile(
239
+ r"^(\s*impl(?:<[^>]*>)?\s+(?:(\w+(?:<[^>]*>)?)\s+for\s+)?(\w+))")),
240
+ (ElementType.MODULE, re.compile(
241
+ r"^(\s*(?:pub(?:\(\w+\))?\s+)?mod\s+(\w+))")),
242
+ (ElementType.MACRO, re.compile(
243
+ r"^(\s*(?:pub(?:\(\w+\))?\s+)?macro_rules!\s+(\w+))")),
244
+ (ElementType.CONSTANT, re.compile(
245
+ r"^(\s*(?:pub(?:\(\w+\))?\s+)?(?:const|static)\s+(\w+))")),
246
+ (ElementType.TYPE_ALIAS, re.compile(
247
+ r"^(\s*(?:pub(?:\(\w+\))?\s+)?type\s+(\w+))")),
248
+ (ElementType.IMPORT, re.compile(
249
+ r"^(\s*use\s+(.+?);)")),
250
+ (ElementType.DECORATOR, re.compile(
251
+ r"^(\s*#\[(\w[^\]]*)\])")),
252
+ ],
253
+ )
254
+
255
+ # ── JavaScript ──────────────────────────────────────────────────────
256
+ specs["javascript"] = LanguageSpec(
257
+ name="JavaScript",
258
+ single_comment="//",
259
+ multi_comment_start="/*",
260
+ multi_comment_end="*/",
261
+ string_delimiters=("\"", "'", "`"),
262
+ patterns=[
263
+ (ElementType.CLASS, re.compile(
264
+ r"^(\s*(?:export\s+)?(?:default\s+)?class\s+(\w+))")),
265
+ (ElementType.FUNCTION, re.compile(
266
+ r"^(\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s+(\w+)\s*\()")),
267
+ (ElementType.FUNCTION, re.compile(
268
+ r"^(\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?"
269
+ r"(?:function|\([^)]*\)\s*=>|[a-zA-Z_]\w*\s*=>))")),
270
+ (ElementType.COMPONENT, re.compile(
271
+ r"^(\s*(?:export\s+)?(?:default\s+)?(?:const|let|var)\s+(\w+)\s*=\s*"
272
+ r"(?:React\.)?(?:memo|forwardRef|lazy)\s*\()")),
273
+ (ElementType.CONSTANT, re.compile(
274
+ r"^(\s*(?:export\s+)?const\s+([A-Z][A-Z_0-9]+)\s*=)")),
275
+ (ElementType.IMPORT, re.compile(
276
+ r"^(\s*import\s+(.+))")),
277
+ (ElementType.MODULE, re.compile(
278
+ r"^(\s*(?:export\s+)?(?:default\s+)?(?:const|let|var)\s+(\w+)\s*=\s*"
279
+ r"require\s*\()")),
280
+ ],
281
+ )
282
+
283
+ # ── TypeScript ──────────────────────────────────────────────────────
284
+ specs["typescript"] = LanguageSpec(
285
+ name="TypeScript",
286
+ single_comment="//",
287
+ multi_comment_start="/*",
288
+ multi_comment_end="*/",
289
+ string_delimiters=("\"", "'", "`"),
290
+ patterns=[
291
+ (ElementType.INTERFACE, re.compile(
292
+ r"^(\s*(?:export\s+)?interface\s+(\w+))")),
293
+ (ElementType.TYPE_ALIAS, re.compile(
294
+ r"^(\s*(?:export\s+)?type\s+(\w+)\s*(?:<[^>]*>)?\s*=)")),
295
+ (ElementType.ENUM, re.compile(
296
+ r"^(\s*(?:export\s+)?(?:const\s+)?enum\s+(\w+))")),
297
+ (ElementType.CLASS, re.compile(
298
+ r"^(\s*(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+(\w+))")),
299
+ (ElementType.FUNCTION, re.compile(
300
+ r"^(\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s+(\w+)\s*)")),
301
+ (ElementType.FUNCTION, re.compile(
302
+ r"^(\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*(?::\s*[^=]+)?\s*=\s*"
303
+ r"(?:async\s+)?(?:function|\([^)]*\)\s*(?::\s*[^=]+)?\s*=>|"
304
+ r"[a-zA-Z_]\w*\s*=>))")),
305
+ (ElementType.NAMESPACE, re.compile(
306
+ r"^(\s*(?:export\s+)?(?:declare\s+)?namespace\s+(\w+))")),
307
+ (ElementType.MODULE, re.compile(
308
+ r"^(\s*(?:export\s+)?(?:declare\s+)?module\s+(\w+))")),
309
+ (ElementType.IMPORT, re.compile(
310
+ r"^(\s*import\s+(.+))")),
311
+ (ElementType.DECORATOR, re.compile(
312
+ r"^(\s*@(\w[\w.]*)\s*)")),
313
+ ],
314
+ )
315
+
316
+ # ── Java ────────────────────────────────────────────────────────────
317
+ specs["java"] = LanguageSpec(
318
+ name="Java",
319
+ single_comment="//",
320
+ multi_comment_start="/*",
321
+ multi_comment_end="*/",
322
+ patterns=[
323
+ (ElementType.CLASS, re.compile(
324
+ r"^(\s*(?:public\s+|private\s+|protected\s+)?(?:static\s+)?"
325
+ r"(?:final\s+)?(?:abstract\s+)?class\s+(\w+))")),
326
+ (ElementType.INTERFACE, re.compile(
327
+ r"^(\s*(?:public\s+|private\s+|protected\s+)?interface\s+(\w+))")),
328
+ (ElementType.ENUM, re.compile(
329
+ r"^(\s*(?:public\s+|private\s+|protected\s+)?enum\s+(\w+))")),
330
+ (ElementType.FUNCTION, re.compile(
331
+ r"^(\s*(?:public\s+|private\s+|protected\s+)?(?:static\s+)?"
332
+ r"(?:final\s+)?(?:synchronized\s+)?(?:native\s+)?"
333
+ r"(?:abstract\s+)?(?:<[^>]+>\s+)?"
334
+ r"(?:void|int|char|float|double|long|short|byte|boolean|"
335
+ r"String|Object|List|Map|Set|Optional|\w+(?:<[^>]*>)?)\s*(?:\[\])?\s+"
336
+ r"(\w+)\s*\()")),
337
+ (ElementType.IMPORT, re.compile(
338
+ r"^(\s*import\s+(?:static\s+)?(.+?);)")),
339
+ (ElementType.MODULE, re.compile(
340
+ r"^(\s*package\s+(.+?);)")),
341
+ (ElementType.DECORATOR, re.compile(
342
+ r"^(\s*@(\w[\w.]*(?:\([^)]*\))?))")),
343
+ (ElementType.CONSTANT, re.compile(
344
+ r"^(\s*(?:public\s+|private\s+|protected\s+)?static\s+final\s+\w+\s+([A-Z_]\w*)\s*=)")),
345
+ ],
346
+ )
347
+
348
+ # ── Go ──────────────────────────────────────────────────────────────
349
+ specs["go"] = LanguageSpec(
350
+ name="Go",
351
+ single_comment="//",
352
+ multi_comment_start="/*",
353
+ multi_comment_end="*/",
354
+ string_delimiters=("\"", "`"),
355
+ patterns=[
356
+ (ElementType.FUNCTION, re.compile(
357
+ r"^(\s*func\s+(\w+)\s*\()")),
358
+ (ElementType.METHOD, re.compile(
359
+ r"^(\s*func\s+\(\s*\w+\s+\*?\w+\s*\)\s+(\w+)\s*\()")),
360
+ (ElementType.STRUCT, re.compile(
361
+ r"^(\s*type\s+(\w+)\s+struct\b)")),
362
+ (ElementType.INTERFACE, re.compile(
363
+ r"^(\s*type\s+(\w+)\s+interface\b)")),
364
+ (ElementType.TYPE_ALIAS, re.compile(
365
+ r"^(\s*type\s+(\w+)\s+(?!struct|interface)\w)")),
366
+ (ElementType.CONSTANT, re.compile(
367
+ r"^(\s*(?:const|var)\s+(\w+))")),
368
+ (ElementType.IMPORT, re.compile(
369
+ r"^(\s*import\s+(.+))")),
370
+ (ElementType.MODULE, re.compile(
371
+ r"^(\s*package\s+(\w+))")),
372
+ ],
373
+ )
374
+
375
+ # ── Ruby ────────────────────────────────────────────────────────────
376
+ specs["ruby"] = LanguageSpec(
377
+ name="Ruby",
378
+ single_comment="#",
379
+ multi_comment_start="=begin",
380
+ multi_comment_end="=end",
381
+ patterns=[
382
+ (ElementType.CLASS, re.compile(
383
+ r"^(\s*class\s+(\w+))")),
384
+ (ElementType.MODULE, re.compile(
385
+ r"^(\s*module\s+(\w+))")),
386
+ (ElementType.FUNCTION, re.compile(
387
+ r"^(\s*def\s+(?:self\.)?(\w+[?!=]?))")),
388
+ (ElementType.CONSTANT, re.compile(
389
+ r"^(\s*([A-Z][A-Z_0-9]+)\s*=)")),
390
+ (ElementType.IMPORT, re.compile(
391
+ r"^(\s*require(?:_relative)?\s+(.+))")),
392
+ (ElementType.DECORATOR, re.compile(
393
+ r"^(\s*attr_(?:reader|writer|accessor)\s+(.+))")),
394
+ ],
395
+ )
396
+
397
+ # ── PHP ─────────────────────────────────────────────────────────────
398
+ specs["php"] = LanguageSpec(
399
+ name="PHP",
400
+ single_comment="//",
401
+ multi_comment_start="/*",
402
+ multi_comment_end="*/",
403
+ patterns=[
404
+ (ElementType.CLASS, re.compile(
405
+ r"^(\s*(?:abstract\s+|final\s+)?class\s+(\w+))")),
406
+ (ElementType.INTERFACE, re.compile(
407
+ r"^(\s*interface\s+(\w+))")),
408
+ (ElementType.TRAIT, re.compile(
409
+ r"^(\s*trait\s+(\w+))")),
410
+ (ElementType.FUNCTION, re.compile(
411
+ r"^(\s*(?:public\s+|private\s+|protected\s+)?(?:static\s+)?function\s+(\w+)\s*\()")),
412
+ (ElementType.NAMESPACE, re.compile(
413
+ r"^(\s*namespace\s+(.+?);)")),
414
+ (ElementType.IMPORT, re.compile(
415
+ r"^(\s*(?:use|require|require_once|include|include_once)\s+(.+?);)")),
416
+ (ElementType.CONSTANT, re.compile(
417
+ r"^(\s*(?:const|define)\s*\(?\s*['\"]?(\w+))")),
418
+ ],
419
+ )
420
+
421
+ # ── Swift ───────────────────────────────────────────────────────────
422
+ specs["swift"] = LanguageSpec(
423
+ name="Swift",
424
+ single_comment="//",
425
+ multi_comment_start="/*",
426
+ multi_comment_end="*/",
427
+ patterns=[
428
+ (ElementType.CLASS, re.compile(
429
+ r"^(\s*(?:public\s+|private\s+|internal\s+|open\s+|fileprivate\s+)?"
430
+ r"(?:final\s+)?class\s+(\w+))")),
431
+ (ElementType.STRUCT, re.compile(
432
+ r"^(\s*(?:public\s+|private\s+|internal\s+)?struct\s+(\w+))")),
433
+ (ElementType.ENUM, re.compile(
434
+ r"^(\s*(?:public\s+|private\s+|internal\s+)?enum\s+(\w+))")),
435
+ (ElementType.PROTOCOL, re.compile(
436
+ r"^(\s*(?:public\s+|private\s+|internal\s+)?protocol\s+(\w+))")),
437
+ (ElementType.EXTENSION, re.compile(
438
+ r"^(\s*(?:public\s+|private\s+|internal\s+)?extension\s+(\w+))")),
439
+ (ElementType.FUNCTION, re.compile(
440
+ r"^(\s*(?:public\s+|private\s+|internal\s+|open\s+)?(?:static\s+|class\s+)?"
441
+ r"(?:override\s+)?func\s+(\w+))")),
442
+ (ElementType.IMPORT, re.compile(
443
+ r"^(\s*import\s+(\w+))")),
444
+ (ElementType.CONSTANT, re.compile(
445
+ r"^(\s*(?:public\s+|private\s+)?(?:static\s+)?let\s+(\w+)\s*(?::|\s*=))")),
446
+ (ElementType.VARIABLE, re.compile(
447
+ r"^(\s*(?:public\s+|private\s+)?(?:static\s+)?var\s+(\w+)\s*(?::|\s*=))")),
448
+ ],
449
+ )
450
+
451
+ # ── Kotlin ──────────────────────────────────────────────────────────
452
+ specs["kotlin"] = LanguageSpec(
453
+ name="Kotlin",
454
+ single_comment="//",
455
+ multi_comment_start="/*",
456
+ multi_comment_end="*/",
457
+ patterns=[
458
+ (ElementType.CLASS, re.compile(
459
+ r"^(\s*(?:open\s+|abstract\s+|sealed\s+|data\s+|inner\s+)*class\s+(\w+))")),
460
+ (ElementType.INTERFACE, re.compile(
461
+ r"^(\s*interface\s+(\w+))")),
462
+ (ElementType.ENUM, re.compile(
463
+ r"^(\s*enum\s+class\s+(\w+))")),
464
+ (ElementType.FUNCTION, re.compile(
465
+ r"^(\s*(?:public\s+|private\s+|protected\s+|internal\s+)?(?:open\s+|override\s+)?"
466
+ r"(?:suspend\s+)?fun\s+(?:<[^>]+>\s+)?(\w+)\s*\()")),
467
+ (ElementType.CONSTANT, re.compile(
468
+ r"^(\s*(?:const\s+)?val\s+(\w+))")),
469
+ (ElementType.VARIABLE, re.compile(
470
+ r"^(\s*var\s+(\w+))")),
471
+ (ElementType.MODULE, re.compile(
472
+ r"^(\s*(?:object|companion\s+object)\s+(\w*))")),
473
+ (ElementType.IMPORT, re.compile(
474
+ r"^(\s*import\s+(.+))")),
475
+ (ElementType.DECORATOR, re.compile(
476
+ r"^(\s*@(\w[\w.]*)\s*)")),
477
+ ],
478
+ )
479
+
480
+ # ── Scala ───────────────────────────────────────────────────────────
481
+ specs["scala"] = LanguageSpec(
482
+ name="Scala",
483
+ single_comment="//",
484
+ multi_comment_start="/*",
485
+ multi_comment_end="*/",
486
+ patterns=[
487
+ (ElementType.CLASS, re.compile(
488
+ r"^(\s*(?:abstract\s+|sealed\s+|case\s+)?class\s+(\w+))")),
489
+ (ElementType.TRAIT, re.compile(
490
+ r"^(\s*trait\s+(\w+))")),
491
+ (ElementType.MODULE, re.compile(
492
+ r"^(\s*object\s+(\w+))")),
493
+ (ElementType.FUNCTION, re.compile(
494
+ r"^(\s*(?:override\s+)?def\s+(\w+))")),
495
+ (ElementType.CONSTANT, re.compile(
496
+ r"^(\s*val\s+(\w+))")),
497
+ (ElementType.VARIABLE, re.compile(
498
+ r"^(\s*var\s+(\w+))")),
499
+ (ElementType.TYPE_ALIAS, re.compile(
500
+ r"^(\s*type\s+(\w+))")),
501
+ (ElementType.IMPORT, re.compile(
502
+ r"^(\s*import\s+(.+))")),
503
+ ],
504
+ )
505
+
506
+ # ── Lua ─────────────────────────────────────────────────────────────
507
+ specs["lua"] = LanguageSpec(
508
+ name="Lua",
509
+ single_comment="--",
510
+ multi_comment_start="--[[",
511
+ multi_comment_end="]]",
512
+ patterns=[
513
+ (ElementType.FUNCTION, re.compile(
514
+ r"^(\s*(?:local\s+)?function\s+(\w[\w.:]*))\s*\(")),
515
+ (ElementType.FUNCTION, re.compile(
516
+ r"^(\s*(?:local\s+)?(\w[\w.]*)\s*=\s*function\s*\()")),
517
+ (ElementType.VARIABLE, re.compile(
518
+ r"^(\s*local\s+(\w+)\s*=)")),
519
+ ],
520
+ )
521
+
522
+ # ── Shell (Bash) ────────────────────────────────────────────────────
523
+ specs["shell"] = LanguageSpec(
524
+ name="Shell",
525
+ single_comment="#",
526
+ multi_comment_start=None,
527
+ multi_comment_end=None,
528
+ patterns=[
529
+ (ElementType.FUNCTION, re.compile(
530
+ r"^(\s*(?:function\s+)?(\w+)\s*\(\s*\))")),
531
+ (ElementType.VARIABLE, re.compile(
532
+ r"^(\s*(?:export\s+|readonly\s+|declare\s+(?:-\w+\s+)*)?"
533
+ r"([A-Z_][A-Z_0-9]*)\s*=)")),
534
+ (ElementType.IMPORT, re.compile(
535
+ r"^(\s*(?:source|\\.)\s+(.+))")),
536
+ ],
537
+ )
538
+ specs["bash"] = specs["shell"]
539
+ specs["sh"] = specs["shell"]
540
+ specs["zsh"] = specs["shell"]
541
+
542
+ # ── Perl ────────────────────────────────────────────────────────────
543
+ specs["perl"] = LanguageSpec(
544
+ name="Perl",
545
+ single_comment="#",
546
+ multi_comment_start="=pod",
547
+ multi_comment_end="=cut",
548
+ patterns=[
549
+ (ElementType.FUNCTION, re.compile(
550
+ r"^(\s*sub\s+(\w+))")),
551
+ (ElementType.MODULE, re.compile(
552
+ r"^(\s*package\s+(\w[\w:]*))")),
553
+ (ElementType.CONSTANT, re.compile(
554
+ r"^(\s*(?:use\s+constant\s+(\w+)))")),
555
+ (ElementType.IMPORT, re.compile(
556
+ r"^(\s*(?:use|require)\s+(.+?);)")),
557
+ ],
558
+ )
559
+
560
+ # ── Haskell ─────────────────────────────────────────────────────────
561
+ specs["haskell"] = LanguageSpec(
562
+ name="Haskell",
563
+ single_comment="--",
564
+ multi_comment_start="{-",
565
+ multi_comment_end="-}",
566
+ patterns=[
567
+ (ElementType.MODULE, re.compile(
568
+ r"^(\s*module\s+(\w[\w.]*))")),
569
+ (ElementType.TYPE_ALIAS, re.compile(
570
+ r"^(\s*type\s+(\w+))")),
571
+ (ElementType.STRUCT, re.compile(
572
+ r"^(\s*data\s+(\w+))")),
573
+ (ElementType.CLASS, re.compile(
574
+ r"^(\s*class\s+(\w+))")),
575
+ (ElementType.FUNCTION, re.compile(
576
+ r"^(([a-z_]\w*)\s*::)")),
577
+ (ElementType.IMPORT, re.compile(
578
+ r"^(\s*import\s+(?:qualified\s+)?(.+))")),
579
+ ],
580
+ )
581
+
582
+ # ── Zig ─────────────────────────────────────────────────────────────
583
+ specs["zig"] = LanguageSpec(
584
+ name="Zig",
585
+ single_comment="//",
586
+ multi_comment_start=None,
587
+ multi_comment_end=None,
588
+ patterns=[
589
+ (ElementType.FUNCTION, re.compile(
590
+ r"^(\s*(?:pub\s+|export\s+)?fn\s+(\w+))")),
591
+ (ElementType.STRUCT, re.compile(
592
+ r"^(\s*(?:pub\s+)?const\s+(\w+)\s*=\s*(?:extern\s+|packed\s+)?struct\b)")),
593
+ (ElementType.ENUM, re.compile(
594
+ r"^(\s*(?:pub\s+)?const\s+(\w+)\s*=\s*enum\b)")),
595
+ (ElementType.UNION, re.compile(
596
+ r"^(\s*(?:pub\s+)?const\s+(\w+)\s*=\s*(?:extern\s+|packed\s+)?union\b)")),
597
+ (ElementType.IMPORT, re.compile(
598
+ r"^(\s*const\s+(\w+)\s*=\s*@import\()")),
599
+ (ElementType.CONSTANT, re.compile(
600
+ r"^(\s*(?:pub\s+)?const\s+(\w+)\s*(?::\s*[^=]+)?\s*=)")),
601
+ (ElementType.VARIABLE, re.compile(
602
+ r"^(\s*(?:pub\s+)?var\s+(\w+))")),
603
+ ],
604
+ )
605
+
606
+ # ── Elixir ──────────────────────────────────────────────────────────
607
+ specs["elixir"] = LanguageSpec(
608
+ name="Elixir",
609
+ single_comment="#",
610
+ multi_comment_start=None,
611
+ multi_comment_end=None,
612
+ patterns=[
613
+ (ElementType.MODULE, re.compile(
614
+ r"^(\s*defmodule\s+(\w[\w.]*))")),
615
+ (ElementType.FUNCTION, re.compile(
616
+ r"^(\s*(?:def|defp|defmacro|defmacrop)\s+(\w+))")),
617
+ (ElementType.PROTOCOL, re.compile(
618
+ r"^(\s*defprotocol\s+(\w[\w.]*))")),
619
+ (ElementType.IMPL, re.compile(
620
+ r"^(\s*defimpl\s+(\w[\w.]*))")),
621
+ (ElementType.STRUCT, re.compile(
622
+ r"^(\s*defstruct\s+(.+))")),
623
+ (ElementType.IMPORT, re.compile(
624
+ r"^(\s*(?:import|alias|use|require)\s+(.+))")),
625
+ ],
626
+ )
627
+
628
+ # ── C# ──────────────────────────────────────────────────────────────
629
+ specs["csharp"] = LanguageSpec(
630
+ name="C#",
631
+ single_comment="//",
632
+ multi_comment_start="/*",
633
+ multi_comment_end="*/",
634
+ patterns=[
635
+ (ElementType.CLASS, re.compile(
636
+ r"^(\s*(?:public\s+|private\s+|protected\s+|internal\s+)?(?:static\s+)?"
637
+ r"(?:sealed\s+|abstract\s+|partial\s+)?class\s+(\w+))")),
638
+ (ElementType.INTERFACE, re.compile(
639
+ r"^(\s*(?:public\s+|private\s+|protected\s+|internal\s+)?interface\s+(\w+))")),
640
+ (ElementType.STRUCT, re.compile(
641
+ r"^(\s*(?:public\s+|private\s+|protected\s+|internal\s+)?"
642
+ r"(?:readonly\s+)?struct\s+(\w+))")),
643
+ (ElementType.ENUM, re.compile(
644
+ r"^(\s*(?:public\s+|private\s+|protected\s+|internal\s+)?enum\s+(\w+))")),
645
+ (ElementType.NAMESPACE, re.compile(
646
+ r"^(\s*namespace\s+(\w[\w.]*))")),
647
+ (ElementType.FUNCTION, re.compile(
648
+ r"^(\s*(?:public\s+|private\s+|protected\s+|internal\s+)?(?:static\s+)?"
649
+ r"(?:async\s+)?(?:virtual\s+|override\s+|abstract\s+)?"
650
+ r"(?:void|int|char|float|double|long|short|byte|bool|decimal|"
651
+ r"string|object|var|Task|IEnumerable|\w+(?:<[^>]*>)?)\s*(?:\[\])?\s+"
652
+ r"(\w+)\s*\()")),
653
+ (ElementType.PROPERTY, re.compile(
654
+ r"^(\s*(?:public\s+|private\s+|protected\s+|internal\s+)?(?:static\s+)?"
655
+ r"(?:virtual\s+|override\s+)?(?:required\s+)?"
656
+ r"\w+(?:<[^>]*>)?\s+(\w+)\s*\{)")),
657
+ (ElementType.IMPORT, re.compile(
658
+ r"^(\s*using\s+(.+?);)")),
659
+ (ElementType.DECORATOR, re.compile(
660
+ r"^(\s*\[(\w[\w.]*(?:\([^)]*\))?)\])")),
661
+ (ElementType.CONSTANT, re.compile(
662
+ r"^(\s*(?:public\s+|private\s+)?const\s+\w+\s+(\w+)\s*=)")),
663
+ ],
664
+ )
665
+ specs["cs"] = specs["csharp"]
666
+
667
+ # Common aliases
668
+ specs["js"] = specs["javascript"]
669
+ specs["ts"] = specs["typescript"]
670
+ specs["rs"] = specs["rust"]
671
+ specs["py"] = specs["python"]
672
+ specs["rb"] = specs["ruby"]
673
+ specs["hs"] = specs["haskell"]
674
+ specs["cc"] = specs["cpp"]
675
+ specs["cxx"] = specs["cpp"]
676
+ specs["h"] = specs["c"]
677
+ specs["hpp"] = specs["cpp"]
678
+ specs["kt"] = specs["kotlin"]
679
+ specs["ex"] = specs["elixir"]
680
+ specs["exs"] = specs["elixir"]
681
+ specs["pl"] = specs["perl"]
682
+
683
+ return specs
684
+
685
+
686
+ class SourceAnalyzer:
687
+ """! @brief Multi-language source file analyzer.
688
+ @details Analyzes a source file identifying definitions, comments and constructs for the specified language. Produces structured output with line numbers, inspired by tree-sitter tags functionality.
689
+ """
690
+
691
+ def __init__(self):
692
+ """!
693
+ @brief Initialize analyzer state with language specifications.
694
+ @details Implements the __init__ function behavior with deterministic control flow.
695
+ @return {None} Function return value.
696
+ """
697
+ self.specs = build_language_specs()
698
+
699
+ def get_supported_languages(self) -> list:
700
+ """!
701
+ @brief Return list of supported languages (without aliases).
702
+ @return Sorted list of unique language identifiers.
703
+ @details Implements the get_supported_languages function behavior with deterministic control flow.
704
+ """
705
+ seen = set()
706
+ result = []
707
+ for key, spec in self.specs.items():
708
+ if id(spec) not in seen:
709
+ seen.add(id(spec))
710
+ result.append(key)
711
+ return sorted(result)
712
+
713
+ def analyze(self, filepath: str, language: str) -> list:
714
+ """! @brief Analyze a source file and return the list of SourceElement found.
715
+ @param filepath Path to the source file.
716
+ @param language Language identifier.
717
+ @return List of SourceElement instances.
718
+ @throws ValueError If language is not supported.
719
+ @details Reads file content, detects single/multi-line comments, and matches regex patterns for definitions.
720
+ """
721
+ language = language.lower().strip().lstrip(".")
722
+ if language not in self.specs:
723
+ raise ValueError(
724
+ f"Language '{language}' not supported.\n"
725
+ f"Supported languages: {', '.join(self.get_supported_languages())}"
726
+ )
727
+
728
+ spec = self.specs[language]
729
+
730
+ with open(filepath, "r", encoding="utf-8", errors="replace") as f:
731
+ lines = f.readlines()
732
+
733
+ elements = []
734
+
735
+ # Multi-line comment state
736
+ in_multiline_comment = False
737
+ multiline_comment_start_line = 0
738
+ multiline_comment_lines = []
739
+
740
+ for line_num, line in enumerate(lines, start=1):
741
+ stripped = line.rstrip("\n\r")
742
+
743
+ # ── Multi-line comment handling ──────────────────────────
744
+ if in_multiline_comment:
745
+ multiline_comment_lines.append(stripped)
746
+ if spec.multi_comment_end and spec.multi_comment_end in stripped:
747
+ in_multiline_comment = False
748
+ full_text = "\n".join(multiline_comment_lines)
749
+ mc_extract = multiline_comment_lines
750
+ if len(mc_extract) > 5:
751
+ mc_extract = mc_extract[:4] + [" ..."]
752
+ elements.append(SourceElement(
753
+ element_type=ElementType.COMMENT_MULTI,
754
+ line_start=multiline_comment_start_line,
755
+ line_end=line_num,
756
+ extract="\n".join(mc_extract),
757
+ comment_source=full_text,
758
+ ))
759
+ multiline_comment_lines = []
760
+ continue
761
+
762
+ # ── Multi-line comment start ────────────────────────────
763
+ if spec.multi_comment_start:
764
+ # Special handling for Python docstrings and =begin/=pod blocks
765
+ mc_start = spec.multi_comment_start
766
+ if mc_start in stripped:
767
+ start_idx = stripped.index(mc_start)
768
+ # Check not inside a string
769
+ if not self._in_string_context(stripped, start_idx, spec):
770
+ # Check if multi-line comment closes on same line
771
+ after_start = stripped[start_idx + len(mc_start):]
772
+ if (spec.multi_comment_end
773
+ and spec.multi_comment_end in after_start
774
+ and mc_start != spec.multi_comment_end):
775
+ elements.append(SourceElement(
776
+ element_type=ElementType.COMMENT_MULTI,
777
+ line_start=line_num,
778
+ line_end=line_num,
779
+ extract=stripped,
780
+ ))
781
+ continue
782
+ # Python: """ ... """ sulla stessa riga
783
+ if mc_start == '"""' or mc_start == "'''":
784
+ rest = stripped[start_idx + 3:]
785
+ if mc_start in rest:
786
+ elements.append(SourceElement(
787
+ element_type=ElementType.COMMENT_MULTI,
788
+ line_start=line_num,
789
+ line_end=line_num,
790
+ extract=stripped,
791
+ ))
792
+ continue
793
+
794
+ in_multiline_comment = True
795
+ multiline_comment_start_line = line_num
796
+ multiline_comment_lines = [stripped]
797
+ continue
798
+
799
+ # ── Single-line comment ───────────────────────────────────
800
+ if spec.single_comment:
801
+ comment_idx = self._find_comment(stripped, spec)
802
+ if comment_idx is not None:
803
+ # If comment is the entire line (aside from whitespace)
804
+ before_comment = stripped[:comment_idx].strip()
805
+ if not before_comment:
806
+ elements.append(SourceElement(
807
+ element_type=ElementType.COMMENT_SINGLE,
808
+ line_start=line_num,
809
+ line_end=line_num,
810
+ extract=stripped,
811
+ ))
812
+ continue
813
+ else:
814
+ # Inline comment: add both element and comment
815
+ comment_text = stripped[comment_idx:]
816
+ elements.append(SourceElement(
817
+ element_type=ElementType.COMMENT_SINGLE,
818
+ line_start=line_num,
819
+ line_end=line_num,
820
+ extract=comment_text,
821
+ name="inline",
822
+ ))
823
+
824
+ # ── Language patterns ─────────────────────────────────────
825
+ if not stripped.strip():
826
+ continue
827
+
828
+ for elem_type, pattern in spec.patterns:
829
+ match = pattern.match(stripped)
830
+ if match:
831
+ name = None
832
+ if match.lastindex and match.lastindex >= 2:
833
+ name = match.group(2)
834
+ elif match.lastindex and match.lastindex >= 1:
835
+ name = match.group(1)
836
+
837
+ # Single-line types: don't search for block
838
+ single_line_types = (
839
+ ElementType.IMPORT, ElementType.CONSTANT,
840
+ ElementType.VARIABLE, ElementType.DECORATOR,
841
+ ElementType.MACRO, ElementType.TYPE_ALIAS,
842
+ ElementType.TYPEDEF, ElementType.PROPERTY,
843
+ )
844
+
845
+ if elem_type in single_line_types:
846
+ block_end = line_num
847
+ else:
848
+ block_end = self._find_block_end(
849
+ lines, line_num - 1, language, stripped)
850
+
851
+ extract_lines = [lines[i].rstrip("\n\r")
852
+ for i in range(line_num - 1, block_end)]
853
+
854
+ # Limit extract to max 5 lines for readability
855
+ if len(extract_lines) > 5:
856
+ extract_lines = extract_lines[:4] + [" ..."]
857
+
858
+ elements.append(SourceElement(
859
+ element_type=elem_type,
860
+ line_start=line_num,
861
+ line_end=block_end,
862
+ extract="\n".join(extract_lines),
863
+ name=name,
864
+ ))
865
+ break
866
+
867
+ return elements
868
+
869
+ def _in_string_context(self, line: str, pos: int, spec: LanguageSpec) -> bool:
870
+ """!
871
+ @brief Check if position pos is inside a string literal.
872
+ @param line The line of code.
873
+ @param pos The column index.
874
+ @param spec The LanguageSpec instance.
875
+ @return True if pos is within a string.
876
+ @details Implements the _in_string_context function behavior with deterministic control flow.
877
+ """
878
+ in_string = False
879
+ current_delim = None
880
+ i = 0
881
+ while i < pos:
882
+ ch = line[i]
883
+ if in_string:
884
+ if ch == "\\" and i + 1 < len(line):
885
+ i += 2
886
+ continue
887
+ if current_delim and line[i:].startswith(current_delim):
888
+ in_string = False
889
+ i += len(current_delim)
890
+ continue
891
+ else:
892
+ for delim in sorted(spec.string_delimiters, key=len, reverse=True):
893
+ if line[i:].startswith(delim):
894
+ in_string = True
895
+ current_delim = delim
896
+ i += len(delim)
897
+ break
898
+ else:
899
+ i += 1
900
+ continue
901
+ continue
902
+ i += 1
903
+ return in_string
904
+
905
+ def _find_comment(self, line: str, spec: LanguageSpec) -> Optional[int]:
906
+ """!
907
+ @brief Find position of single-line comment, ignoring strings.
908
+ @param line The line of code.
909
+ @param spec The LanguageSpec instance.
910
+ @return Column index of comment start, or None.
911
+ @details Implements the _find_comment function behavior with deterministic control flow.
912
+ """
913
+ if not spec.single_comment:
914
+ return None
915
+
916
+ in_string = False
917
+ current_delim = None
918
+ i = 0
919
+ while i < len(line):
920
+ ch = line[i]
921
+ if in_string:
922
+ if ch == "\\" and i + 1 < len(line):
923
+ i += 2
924
+ continue
925
+ if current_delim and line[i:].startswith(current_delim):
926
+ in_string = False
927
+ i += len(current_delim)
928
+ continue
929
+ else:
930
+ if line[i:].startswith(spec.single_comment):
931
+ return i
932
+ for delim in sorted(spec.string_delimiters, key=len, reverse=True):
933
+ if line[i:].startswith(delim):
934
+ in_string = True
935
+ current_delim = delim
936
+ i += len(delim)
937
+ break
938
+ else:
939
+ i += 1
940
+ continue
941
+ continue
942
+ i += 1
943
+ return None
944
+
945
+ def _find_block_end(self, lines: list, start_idx: int,
946
+ language: str, first_line: str) -> int:
947
+ """! @brief Find the end of a block (function, class, struct, etc.).
948
+ @param lines List of all file lines.
949
+ @param start_idx Index of the start line.
950
+ @param language Language identifier.
951
+ @param first_line Content of the start line.
952
+ @return 1-based index of the end line.
953
+ @details Returns the index (1-based) of the final line of the block. Limits search for performance.
954
+ """
955
+ # Per Python: basato sull'indentazione
956
+ if language in ("python", "py"):
957
+ indent = len(first_line) - len(first_line.lstrip())
958
+ end = start_idx + 1
959
+ while end < min(len(lines), start_idx + 200):
960
+ line = lines[end].rstrip("\n\r")
961
+ if line.strip() == "":
962
+ end += 1
963
+ continue
964
+ line_indent = len(line) - len(line.lstrip())
965
+ if line_indent <= indent and line.strip():
966
+ break
967
+ end += 1
968
+ return end
969
+
970
+ # Per linguaggi con parentesi graffe
971
+ if language in ("c", "cpp", "cc", "cxx", "h", "hpp", "rust", "rs",
972
+ "javascript", "js", "typescript", "ts", "java",
973
+ "go", "csharp", "cs", "swift", "kotlin", "kt",
974
+ "php", "scala", "zig"):
975
+ brace_count = 0
976
+ found_open = False
977
+ end = start_idx
978
+ while end < min(len(lines), start_idx + 300):
979
+ line = lines[end].rstrip("\n\r")
980
+ for ch in line:
981
+ if ch == "{":
982
+ brace_count += 1
983
+ found_open = True
984
+ elif ch == "}":
985
+ brace_count -= 1
986
+ if found_open and brace_count <= 0:
987
+ return end + 1
988
+ end += 1
989
+ # If no opening braces found, return just the first line
990
+ if not found_open:
991
+ return start_idx + 1
992
+ return end
993
+
994
+ # Per Ruby/Elixir/Lua: basato su end keyword
995
+ if language in ("ruby", "rb", "elixir", "ex", "exs", "lua"):
996
+ indent = len(first_line) - len(first_line.lstrip())
997
+ end = start_idx + 1
998
+ while end < min(len(lines), start_idx + 200):
999
+ line = lines[end].rstrip("\n\r")
1000
+ if line.strip() == "end" or line.strip().startswith("end "):
1001
+ line_indent = len(line) - len(line.lstrip())
1002
+ if line_indent <= indent:
1003
+ return end + 1
1004
+ end += 1
1005
+ return start_idx + 1
1006
+
1007
+ # Per Haskell: basato sull'indentazione
1008
+ if language in ("haskell", "hs"):
1009
+ indent = len(first_line) - len(first_line.lstrip())
1010
+ end = start_idx + 1
1011
+ while end < min(len(lines), start_idx + 100):
1012
+ line = lines[end].rstrip("\n\r")
1013
+ if line.strip() == "":
1014
+ end += 1
1015
+ continue
1016
+ line_indent = len(line) - len(line.lstrip())
1017
+ if line_indent <= indent:
1018
+ break
1019
+ end += 1
1020
+ return end
1021
+
1022
+ return start_idx + 1
1023
+
1024
+ # ── Enrichment methods for LLM-optimized output ───────────────────
1025
+
1026
+ def enrich(self, elements: list, language: str,
1027
+ filepath: Optional[str] = None) -> list:
1028
+ """!
1029
+ @brief Enrich elements with signatures, hierarchy, visibility, inheritance.
1030
+ @details Call after analyze() to add metadata for LLM-optimized markdown output. Modifies elements in-place and returns them. If filepath is provided, also extracts body comments and exit points.
1031
+ @param elements Input parameter `elements`.
1032
+ @param language Input parameter `language`.
1033
+ @param filepath Input parameter `filepath`.
1034
+ @return {list} Function return value.
1035
+ """
1036
+ language = language.lower().strip().lstrip(".")
1037
+ self._clean_names(elements, language)
1038
+ self._extract_signatures(elements, language)
1039
+ self._detect_hierarchy(elements)
1040
+ self._extract_visibility(elements, language)
1041
+ self._extract_inheritance(elements, language)
1042
+ if filepath:
1043
+ self._extract_body_annotations(elements, language, filepath)
1044
+ self._extract_doxygen_fields(elements)
1045
+ return elements
1046
+
1047
+ def _clean_names(self, elements: list, language: str):
1048
+ """!
1049
+ @brief Extract clean identifiers from name fields.
1050
+ @details Due to regex group nesting, name may contain the full match expression (e.g. 'class MyClass:' instead of 'MyClass'). This method extracts the actual identifier.
1051
+ @param elements Input parameter `elements`.
1052
+ @param language Input parameter `language`.
1053
+ @return {None} Function return value.
1054
+ """
1055
+ for elem in elements:
1056
+ if not elem.name:
1057
+ continue
1058
+ # Try to re-extract the name from the element's extract line
1059
+ # using the original pattern (which has group 2 as the identifier)
1060
+ spec = self.specs.get(language)
1061
+ if spec:
1062
+ first_line = elem.extract.split("\n")[0]
1063
+ for etype, pattern in spec.patterns:
1064
+ if etype == elem.element_type:
1065
+ m = pattern.match(first_line)
1066
+ if m:
1067
+ # Take highest non-None non-empty group
1068
+ # (group 2+ = identifier, group 1 = full match)
1069
+ for gi in range(len(m.groups()), 0, -1):
1070
+ g = m.group(gi)
1071
+ if g is not None and g.strip():
1072
+ elem.name = g.strip()
1073
+ break
1074
+ break
1075
+
1076
+ def _extract_signatures(self, elements: list, language: str):
1077
+ """!
1078
+ @brief Extract clean signatures from element extracts.
1079
+ @details Implements the _extract_signatures function behavior with deterministic control flow.
1080
+ @param elements Input parameter `elements`.
1081
+ @param language Input parameter `language`.
1082
+ @return {None} Function return value.
1083
+ """
1084
+ skip_types = (ElementType.COMMENT_SINGLE, ElementType.COMMENT_MULTI,
1085
+ ElementType.IMPORT, ElementType.DECORATOR)
1086
+ for elem in elements:
1087
+ if elem.element_type in skip_types:
1088
+ continue
1089
+ first_line = elem.extract.split("\n")[0].strip()
1090
+ sig = first_line
1091
+ for suffix in (" {", "{", ":", ";"):
1092
+ if sig.endswith(suffix) and not sig.endswith("::"):
1093
+ sig = sig[:-len(suffix)].rstrip()
1094
+ break
1095
+ elem.signature = sig
1096
+
1097
+ def _detect_hierarchy(self, elements: list):
1098
+ """!
1099
+ @brief Detect parent-child relationships between elements.
1100
+ @details Containers (class, struct, module, etc.) remain at depth=0. Non-container elements inside containers get depth=1 and parent_name set.
1101
+ @param elements Input parameter `elements`.
1102
+ @return {None} Function return value.
1103
+ """
1104
+ container_types = (
1105
+ ElementType.CLASS, ElementType.STRUCT, ElementType.MODULE,
1106
+ ElementType.IMPL, ElementType.INTERFACE, ElementType.TRAIT,
1107
+ ElementType.NAMESPACE, ElementType.ENUM, ElementType.EXTENSION,
1108
+ ElementType.PROTOCOL)
1109
+ containers = [e for e in elements if e.element_type in container_types]
1110
+ skip_types = (ElementType.COMMENT_SINGLE, ElementType.COMMENT_MULTI,
1111
+ ElementType.IMPORT)
1112
+ for elem in elements:
1113
+ if elem.element_type in skip_types:
1114
+ continue
1115
+ if elem.element_type in container_types:
1116
+ continue
1117
+ best = None
1118
+ for c in containers:
1119
+ if c is elem:
1120
+ continue
1121
+ if (c.line_start <= elem.line_start
1122
+ and c.line_end >= elem.line_end):
1123
+ if best is None:
1124
+ best = c
1125
+ elif c.line_start > best.line_start:
1126
+ best = c
1127
+ elif (c.line_start == best.line_start
1128
+ and c.line_end < best.line_end):
1129
+ best = c
1130
+ if best is not None:
1131
+ elem.parent_name = best.name
1132
+ elem.depth = 1
1133
+
1134
+ def _extract_visibility(self, elements: list, language: str):
1135
+ """!
1136
+ @brief Extract visibility/access modifiers from elements.
1137
+ @details Implements the _extract_visibility function behavior with deterministic control flow.
1138
+ @param elements Input parameter `elements`.
1139
+ @param language Input parameter `language`.
1140
+ @return {None} Function return value.
1141
+ """
1142
+ for elem in elements:
1143
+ if elem.element_type in (ElementType.COMMENT_SINGLE,
1144
+ ElementType.COMMENT_MULTI,
1145
+ ElementType.IMPORT):
1146
+ continue
1147
+ sig = elem.extract.split("\n")[0].strip()
1148
+ vis = self._parse_visibility(sig, elem.name, language)
1149
+ if vis:
1150
+ elem.visibility = vis
1151
+
1152
+ def _parse_visibility(self, sig: str, name: Optional[str],
1153
+ language: str) -> Optional[str]:
1154
+ """!
1155
+ @brief Parse visibility modifier from a signature line.
1156
+ @details Implements the _parse_visibility function behavior with deterministic control flow.
1157
+ @param sig Input parameter `sig`.
1158
+ @param name Input parameter `name`.
1159
+ @param language Input parameter `language`.
1160
+ @return {Optional[str]} Function return value.
1161
+ """
1162
+ if language in ("python", "py"):
1163
+ if name and name.startswith("__") and not name.endswith("__"):
1164
+ return "priv"
1165
+ if name and name.startswith("_"):
1166
+ return "priv"
1167
+ return "pub"
1168
+ if language in ("java", "csharp", "cs", "kotlin", "kt", "php"):
1169
+ if re.search(r'\bpublic\b', sig):
1170
+ return "pub"
1171
+ if re.search(r'\bprivate\b', sig):
1172
+ return "priv"
1173
+ if re.search(r'\bprotected\b', sig):
1174
+ return "prot"
1175
+ if re.search(r'\binternal\b', sig):
1176
+ return "int"
1177
+ return None
1178
+ if language in ("rust", "rs", "zig"):
1179
+ if re.match(r'\s*pub\b', sig):
1180
+ return "pub"
1181
+ return "priv"
1182
+ if language in ("go",):
1183
+ if name and name[0:1].isupper():
1184
+ return "pub"
1185
+ return "priv"
1186
+ if language in ("swift",):
1187
+ if re.search(r'\bprivate\b', sig):
1188
+ return "priv"
1189
+ if re.search(r'\bfileprivate\b', sig):
1190
+ return "fpriv"
1191
+ if re.search(r'\b(?:public|open)\b', sig):
1192
+ return "pub"
1193
+ return None
1194
+ if language in ("cpp", "cc", "cxx", "h", "hpp"):
1195
+ if re.search(r'\bpublic\b', sig):
1196
+ return "pub"
1197
+ if re.search(r'\bprivate\b', sig):
1198
+ return "priv"
1199
+ if re.search(r'\bprotected\b', sig):
1200
+ return "prot"
1201
+ return None
1202
+ return None
1203
+
1204
+ def _extract_inheritance(self, elements: list, language: str):
1205
+ """!
1206
+ @brief Extract inheritance/implementation info from class-like elements.
1207
+ @details Implements the _extract_inheritance function behavior with deterministic control flow.
1208
+ @param elements Input parameter `elements`.
1209
+ @param language Input parameter `language`.
1210
+ @return {None} Function return value.
1211
+ """
1212
+ for elem in elements:
1213
+ if elem.element_type not in (ElementType.CLASS, ElementType.STRUCT,
1214
+ ElementType.INTERFACE):
1215
+ continue
1216
+ first_line = elem.extract.split("\n")[0].strip()
1217
+ inh = self._parse_inheritance(first_line, language)
1218
+ if inh:
1219
+ elem.inherits = inh
1220
+
1221
+ def _parse_inheritance(self, first_line: str,
1222
+ language: str) -> Optional[str]:
1223
+ """!
1224
+ @brief Parse inheritance info from a class/struct declaration line.
1225
+ @details Implements the _parse_inheritance function behavior with deterministic control flow.
1226
+ @param first_line Input parameter `first_line`.
1227
+ @param language Input parameter `language`.
1228
+ @return {Optional[str]} Function return value.
1229
+ """
1230
+ if language in ("python", "py"):
1231
+ m = re.search(r'class\s+\w+\s*\(([^)]+)\)', first_line)
1232
+ return m.group(1).strip() if m else None
1233
+ if language in ("java", "typescript", "ts", "javascript", "js"):
1234
+ parts = []
1235
+ m = re.search(r'\bextends\s+([\w.<>, ]+)', first_line)
1236
+ if m:
1237
+ parts.append(m.group(1).strip())
1238
+ m = re.search(r'\bimplements\s+([\w.<>, ]+)', first_line)
1239
+ if m:
1240
+ parts.append(m.group(1).strip())
1241
+ return ", ".join(parts) if parts else None
1242
+ if language in ("cpp", "cc", "cxx", "hpp", "csharp", "cs", "swift"):
1243
+ m = re.search(
1244
+ r'(?:class|struct)\s+\w+\s*:\s*(.+?)(?:\s*\{|$)', first_line)
1245
+ return m.group(1).strip() if m else None
1246
+ if language in ("kotlin", "kt"):
1247
+ m = re.search(
1248
+ r'class\s+\w+\s*(?:\([^)]*\))?\s*:\s*(.+?)(?:\s*\{|$)',
1249
+ first_line)
1250
+ return m.group(1).strip() if m else None
1251
+ if language in ("ruby", "rb"):
1252
+ m = re.search(r'class\s+\w+\s*<\s*(\w+)', first_line)
1253
+ return m.group(1) if m else None
1254
+ return None
1255
+
1256
+ # ── Exit point patterns per language family ──────────────────────
1257
+
1258
+ _EXIT_PATTERNS_RETURN = re.compile(
1259
+ r'^\s*(return\b.*|yield\b.*|raise\b.*|throw\b.*|panic!\(.*)')
1260
+ _EXIT_PATTERNS_IMPLICIT = re.compile(
1261
+ r'^\s*(sys\.exit\(.*|os\._exit\(.*|exit\(.*|process\.exit\(.*)')
1262
+
1263
+ def _extract_body_annotations(self, elements: list,
1264
+ language: str, filepath: str):
1265
+ """!
1266
+ @brief Extract comments and exit points from within function/class bodies.
1267
+ @details Reads the source file and scans each definition's line range for: - Single-line comments (# or // etc.) - Multi-line comments (docstrings, /* */ blocks) - Exit points (return, yield, raise, throw, panic!, sys.exit) Populates body_comments and exit_points on each element.
1268
+ @param elements Input parameter `elements`.
1269
+ @param language Input parameter `language`.
1270
+ @param filepath Input parameter `filepath`.
1271
+ @return {None} Function return value.
1272
+ """
1273
+ spec = self.specs.get(language)
1274
+ if not spec:
1275
+ return
1276
+
1277
+ try:
1278
+ with open(filepath, "r", encoding="utf-8", errors="replace") as f:
1279
+ all_lines = f.readlines()
1280
+ except (OSError, IOError):
1281
+ return
1282
+
1283
+ # Only process definitions that span multiple lines
1284
+ single_line_types = (
1285
+ ElementType.IMPORT, ElementType.CONSTANT,
1286
+ ElementType.VARIABLE, ElementType.DECORATOR,
1287
+ ElementType.MACRO, ElementType.TYPE_ALIAS,
1288
+ ElementType.TYPEDEF, ElementType.PROPERTY,
1289
+ ElementType.COMMENT_SINGLE, ElementType.COMMENT_MULTI,
1290
+ )
1291
+
1292
+ for elem in elements:
1293
+ if elem.element_type in single_line_types:
1294
+ continue
1295
+ if elem.line_end <= elem.line_start:
1296
+ continue
1297
+
1298
+ body_comments = []
1299
+ exit_points = []
1300
+
1301
+ # Scan the body (lines after the definition line)
1302
+ body_start = elem.line_start # 1-based, skip def line itself
1303
+ body_end = min(elem.line_end, len(all_lines))
1304
+
1305
+ in_multi = False
1306
+ multi_start = 0
1307
+ multi_lines = []
1308
+
1309
+ for line_idx in range(body_start, body_end):
1310
+ raw = all_lines[line_idx].rstrip("\n\r")
1311
+ stripped = raw.strip()
1312
+
1313
+ if not stripped:
1314
+ continue
1315
+
1316
+ # Multi-line comment tracking within body
1317
+ if in_multi:
1318
+ multi_lines.append(stripped)
1319
+ if spec.multi_comment_end and spec.multi_comment_end in stripped:
1320
+ in_multi = False
1321
+ text = " ".join(
1322
+ self._clean_comment_line(ln, spec)
1323
+ for ln in multi_lines)
1324
+ text = text.strip()
1325
+ if text:
1326
+ body_comments.append(
1327
+ (multi_start, line_idx + 1, text))
1328
+ multi_lines = []
1329
+ continue
1330
+
1331
+ # Check for multi-line comment start
1332
+ if spec.multi_comment_start and spec.multi_comment_start in stripped:
1333
+ mc_start = spec.multi_comment_start
1334
+ start_pos = stripped.index(mc_start)
1335
+ if not self._in_string_context(stripped, start_pos, spec):
1336
+ # Single-line multi-comment
1337
+ if mc_start == '"""' or mc_start == "'''":
1338
+ after = stripped[start_pos + 3:]
1339
+ if mc_start in after:
1340
+ text = self._clean_comment_line(
1341
+ stripped, spec)
1342
+ if text:
1343
+ body_comments.append(
1344
+ (line_idx + 1, line_idx + 1, text))
1345
+ continue
1346
+ elif (spec.multi_comment_end
1347
+ and spec.multi_comment_end in stripped[
1348
+ start_pos + len(mc_start):]):
1349
+ text = self._clean_comment_line(stripped, spec)
1350
+ if text:
1351
+ body_comments.append(
1352
+ (line_idx + 1, line_idx + 1, text))
1353
+ continue
1354
+
1355
+ in_multi = True
1356
+ multi_start = line_idx + 1
1357
+ multi_lines = [stripped]
1358
+ continue
1359
+
1360
+ # Single-line comment (full line)
1361
+ if spec.single_comment:
1362
+ comment_pos = self._find_comment(stripped, spec)
1363
+ if comment_pos is not None:
1364
+ before = stripped[:comment_pos].strip()
1365
+ comment_text = stripped[comment_pos:]
1366
+ cleaned = self._clean_comment_line(
1367
+ comment_text, spec)
1368
+
1369
+ if not before:
1370
+ # Standalone comment line in body
1371
+ if cleaned:
1372
+ body_comments.append(
1373
+ (line_idx + 1, line_idx + 1, cleaned))
1374
+ continue
1375
+ else:
1376
+ # Inline comment after code
1377
+ if cleaned:
1378
+ body_comments.append(
1379
+ (line_idx + 1, line_idx + 1, cleaned))
1380
+
1381
+ # Exit points
1382
+ if self._EXIT_PATTERNS_RETURN.match(stripped):
1383
+ exit_text = stripped.strip()
1384
+ exit_points.append((line_idx + 1, exit_text))
1385
+ elif self._EXIT_PATTERNS_IMPLICIT.match(stripped):
1386
+ exit_text = stripped.strip()
1387
+ exit_points.append((line_idx + 1, exit_text))
1388
+
1389
+ elem.body_comments = body_comments
1390
+ elem.exit_points = exit_points
1391
+
1392
+ def _extract_doxygen_fields(self, elements: list):
1393
+ """!
1394
+ @brief Extract Doxygen tag fields from associated documentation comments.
1395
+ @details For each non-comment element, resolves the nearest associated documentation comment using language-agnostic adjacency rules: same-line postfix comment (`//!<`, `#!<`, `/**<`), nearest preceding standalone comment block within two lines, or nearest following postfix standalone comment within two lines. When the nearest preceding match is a standalone comment, contiguous preceding standalone comments are merged into one logical block before parsing so multi-line tag sets split across `#`/`//` lines are preserved. Parsed fields are stored in element.doxygen_fields.
1396
+ @param elements Input parameter `elements`.
1397
+ @return {None} Function return value.
1398
+ """
1399
+ comment_elements = [e for e in elements if e.element_type in
1400
+ (ElementType.COMMENT_SINGLE, ElementType.COMMENT_MULTI)]
1401
+ non_comment_elements = [
1402
+ e for e in elements
1403
+ if e.element_type not in (ElementType.COMMENT_SINGLE, ElementType.COMMENT_MULTI)
1404
+ ]
1405
+
1406
+ for elem in elements:
1407
+ # Skip comments themselves
1408
+ if elem.element_type in (ElementType.COMMENT_SINGLE, ElementType.COMMENT_MULTI):
1409
+ continue
1410
+
1411
+ associated_comment = None
1412
+
1413
+ def _is_file_level_comment(comment) -> bool:
1414
+ """! @brief Detect whether a comment block is file-scoped Doxygen metadata.
1415
+ @details Resolves canonical text from `comment_source` or fallback `extract` and checks for a standalone `@file` or `\\file` tag token; empty comment payloads are treated as non file-level.
1416
+ @param comment {SourceElement} Candidate comment element.
1417
+ @return {bool} True when the comment declares file-level metadata and must not be bound to a symbol.
1418
+ """
1419
+ comment_text = comment.comment_source or comment.extract
1420
+ if not comment_text:
1421
+ return False
1422
+ return bool(re.search(r"(?<!\w)(?:@|\\)file\b", comment_text))
1423
+
1424
+ same_line_postfix_candidates = [
1425
+ comment
1426
+ for comment in comment_elements
1427
+ if comment.line_start == elem.line_end
1428
+ and comment.name == "inline"
1429
+ and self._is_postfix_doxygen_comment(comment.extract)
1430
+ and not _is_file_level_comment(comment)
1431
+ ]
1432
+ if same_line_postfix_candidates:
1433
+ associated_comment = min(
1434
+ same_line_postfix_candidates,
1435
+ key=lambda comment: comment.line_start,
1436
+ )
1437
+
1438
+ if associated_comment is None:
1439
+ preceding_candidates = [
1440
+ comment
1441
+ for comment in comment_elements
1442
+ if comment.name != "inline"
1443
+ and comment.line_end < elem.line_start
1444
+ and not _is_file_level_comment(comment)
1445
+ ]
1446
+ if preceding_candidates:
1447
+ def _has_blocking_element(comment) -> bool:
1448
+ """! @brief Validate that no non-comment construct exists between comment and target symbol.
1449
+ @details Evaluates direct-span and overlap-span blockers in `non_comment_elements`; returns True when any unrelated construct occupies the interval between `comment.line_end` and `elem.line_start`.
1450
+ @param comment {SourceElement} Candidate preceding comment element.
1451
+ @return {bool} True when association must be rejected due to an intervening non-comment element.
1452
+ """
1453
+ return any(
1454
+ other is not elem
1455
+ and (
1456
+ (
1457
+ other.line_start > comment.line_end
1458
+ and other.line_start < elem.line_start
1459
+ )
1460
+ or (
1461
+ other.line_start <= comment.line_end
1462
+ and comment.line_end < other.line_end
1463
+ and other.line_end < elem.line_start
1464
+ )
1465
+ )
1466
+ for other in non_comment_elements
1467
+ )
1468
+
1469
+ near_preceding_candidates = [
1470
+ comment
1471
+ for comment in preceding_candidates
1472
+ if elem.line_start - comment.line_end <= 2
1473
+ ]
1474
+ if near_preceding_candidates:
1475
+ for comment in sorted(
1476
+ near_preceding_candidates,
1477
+ key=lambda c: (c.line_end, c.line_start),
1478
+ reverse=True,
1479
+ ):
1480
+ if not _has_blocking_element(comment):
1481
+ associated_comment = comment
1482
+ break
1483
+ else:
1484
+ for comment in sorted(
1485
+ preceding_candidates,
1486
+ key=lambda c: (c.line_end, c.line_start),
1487
+ reverse=True,
1488
+ ):
1489
+ comment_text = comment.comment_source or comment.extract
1490
+ if not parse_doxygen_comment(comment_text):
1491
+ continue
1492
+ if not _has_blocking_element(comment):
1493
+ associated_comment = comment
1494
+ break
1495
+
1496
+ if associated_comment is None:
1497
+ following_postfix_candidates = [
1498
+ comment
1499
+ for comment in comment_elements
1500
+ if comment.name != "inline"
1501
+ and comment.line_start > elem.line_end
1502
+ and comment.line_start - elem.line_end <= 2
1503
+ and self._is_postfix_doxygen_comment(comment.extract)
1504
+ and not _is_file_level_comment(comment)
1505
+ ]
1506
+ if following_postfix_candidates:
1507
+ associated_comment = min(
1508
+ following_postfix_candidates,
1509
+ key=lambda comment: (comment.line_start, comment.line_end),
1510
+ )
1511
+
1512
+ if associated_comment:
1513
+ comment_text = associated_comment.comment_source or associated_comment.extract
1514
+ if (
1515
+ associated_comment.name != "inline"
1516
+ and associated_comment.line_end < elem.line_start
1517
+ and parse_doxygen_comment(comment_text)
1518
+ ):
1519
+ merged_preceding_comments = [associated_comment]
1520
+ current_start = associated_comment.line_start
1521
+ while True:
1522
+ contiguous_candidates = [
1523
+ comment
1524
+ for comment in comment_elements
1525
+ if comment.name != "inline"
1526
+ and comment.line_end == current_start - 1
1527
+ ]
1528
+ if not contiguous_candidates:
1529
+ break
1530
+ previous_comment = max(
1531
+ contiguous_candidates,
1532
+ key=lambda comment: (comment.line_end, comment.line_start),
1533
+ )
1534
+ merged_preceding_comments.insert(0, previous_comment)
1535
+ current_start = previous_comment.line_start
1536
+ comment_text = "\n".join(
1537
+ (comment.comment_source or comment.extract) for comment in merged_preceding_comments
1538
+ )
1539
+ elem.doxygen_fields = parse_doxygen_comment(comment_text)
1540
+
1541
+ @staticmethod
1542
+ def _is_postfix_doxygen_comment(comment_text: str) -> bool:
1543
+ """! @brief Detect whether a comment uses postfix Doxygen association markers.
1544
+ @details Returns True for comment prefixes that explicitly bind documentation to a preceding construct, including variants like `#!<`, `//!<`, `///<`, `/*!<`, and `/**<`.
1545
+ @param comment_text Raw extracted comment text.
1546
+ @return True when the comment text starts with a supported postfix marker; otherwise False.
1547
+ """
1548
+ if not comment_text:
1549
+ return False
1550
+ return bool(re.match(r"^\s*(?:#|//+|--|/\*+|;+)!?<", comment_text))
1551
+
1552
+ @staticmethod
1553
+ def _clean_comment_line(text: str, spec) -> str:
1554
+ """!
1555
+ @brief Strip comment markers from a single line of comment text.
1556
+ @details Implements the _clean_comment_line function behavior with deterministic control flow.
1557
+ @param text Input parameter `text`.
1558
+ @param spec Input parameter `spec`.
1559
+ @return {str} Function return value.
1560
+ """
1561
+ s = text.strip()
1562
+ for prefix in ("///", "//!", "//", "#!", "##", "#", "--", ";;"):
1563
+ if s.startswith(prefix):
1564
+ s = s[len(prefix):].strip()
1565
+ break
1566
+ s = s.strip("/*\"'").strip()
1567
+ return s
1568
+
1569
+
1570
+ def _md_loc(elem) -> str:
1571
+ """!
1572
+ @brief Format element location compactly for markdown.
1573
+ @details Implements the _md_loc function behavior with deterministic control flow.
1574
+ @param elem Input parameter `elem`.
1575
+ @return {str} Function return value.
1576
+ """
1577
+ if elem.line_start == elem.line_end:
1578
+ return f"L{elem.line_start}"
1579
+ return f"L{elem.line_start}-{elem.line_end}"
1580
+
1581
+
1582
+ def _md_kind(elem) -> str:
1583
+ """!
1584
+ @brief Short kind label for markdown output.
1585
+ @details Implements the _md_kind function behavior with deterministic control flow.
1586
+ @param elem Input parameter `elem`.
1587
+ @return {str} Function return value.
1588
+ """
1589
+ mapping = {
1590
+ ElementType.FUNCTION: "fn",
1591
+ ElementType.METHOD: "method",
1592
+ ElementType.CLASS: "class",
1593
+ ElementType.STRUCT: "struct",
1594
+ ElementType.ENUM: "enum",
1595
+ ElementType.TRAIT: "trait",
1596
+ ElementType.INTERFACE: "iface",
1597
+ ElementType.MODULE: "mod",
1598
+ ElementType.IMPL: "impl",
1599
+ ElementType.MACRO: "macro",
1600
+ ElementType.CONSTANT: "const",
1601
+ ElementType.VARIABLE: "var",
1602
+ ElementType.TYPE_ALIAS: "type",
1603
+ ElementType.COMPONENT: "comp",
1604
+ ElementType.PROPERTY: "prop",
1605
+ ElementType.DECORATOR: "dec",
1606
+ ElementType.TYPEDEF: "typedef",
1607
+ ElementType.EXTENSION: "ext",
1608
+ ElementType.PROTOCOL: "proto",
1609
+ ElementType.NAMESPACE: "ns",
1610
+ }
1611
+ return mapping.get(elem.element_type, "unk")
1612
+
1613
+
1614
+ def _extract_comment_text(comment_elem, max_length: int = 0) -> str:
1615
+ """!
1616
+ @brief Extract clean text content from a comment element.
1617
+ @details Args: comment_elem: SourceElement with comment content max_length: if >0, truncate to this length. 0 = no truncation.
1618
+ @param comment_elem Input parameter `comment_elem`.
1619
+ @param max_length Input parameter `max_length`.
1620
+ @return {str} Function return value.
1621
+ """
1622
+ lines = comment_elem.extract.split("\n")
1623
+ cleaned = []
1624
+ for ln in lines:
1625
+ s = ln.strip()
1626
+ # Strip comment markers
1627
+ for prefix in ("///", "//!", "//", "#!", "##", "#", "--", ";;"):
1628
+ if s.startswith(prefix):
1629
+ s = s[len(prefix):].strip()
1630
+ break
1631
+ # Strip multi-line markers
1632
+ s = s.strip("/*\"'").strip()
1633
+ if s and not s.startswith("=begin") and not s.startswith("=end"):
1634
+ cleaned.append(s)
1635
+ text = " ".join(cleaned) if cleaned else ""
1636
+ if max_length > 0 and len(text) > max_length:
1637
+ text = text[:max_length - 3] + "..."
1638
+ return text
1639
+
1640
+
1641
+ def _extract_comment_lines(comment_elem) -> list:
1642
+ """!
1643
+ @brief Extract clean text lines from a multi-line comment (preserving structure).
1644
+ @details Implements the _extract_comment_lines function behavior with deterministic control flow.
1645
+ @param comment_elem Input parameter `comment_elem`.
1646
+ @return {list} Function return value.
1647
+ """
1648
+ lines = comment_elem.extract.split("\n")
1649
+ cleaned = []
1650
+ for ln in lines:
1651
+ s = ln.strip()
1652
+ for prefix in ("///", "//!", "//", "#!", "##", "#", "--", ";;"):
1653
+ if s.startswith(prefix):
1654
+ s = s[len(prefix):].strip()
1655
+ break
1656
+ s = s.strip("/*\"'").strip()
1657
+ if s and not s.startswith("=begin") and not s.startswith("=end"):
1658
+ cleaned.append(s)
1659
+ return cleaned
1660
+
1661
+
1662
+ def _build_comment_maps(elements: list) -> tuple:
1663
+ """!
1664
+ @brief Build maps that associate comments with their adjacent definitions.
1665
+ @details Returns: - doc_for_def: dict mapping def line_start -> list of comment texts (comments immediately preceding a definition) - standalone_comments: list of comment elements not attached to defs - file_description: text from the first comment block (file-level docs)
1666
+ @param elements Input parameter `elements`.
1667
+ @return {tuple} Function return value.
1668
+ """
1669
+ all_sorted = sorted(elements, key=lambda e: e.line_start)
1670
+
1671
+ # Identify definition elements
1672
+ def_types = set(ElementType) - {
1673
+ ElementType.COMMENT_SINGLE, ElementType.COMMENT_MULTI,
1674
+ ElementType.IMPORT, ElementType.DECORATOR}
1675
+ def_starts = {e.line_start for e in elements if e.element_type in def_types}
1676
+ import_starts = {e.line_start for e in elements
1677
+ if e.element_type == ElementType.IMPORT}
1678
+
1679
+ # Build adjacency map: comments preceding a definition (within 2 lines)
1680
+ doc_for_def = {}
1681
+ standalone_comments = []
1682
+ file_description = ""
1683
+
1684
+ comments = [e for e in all_sorted
1685
+ if e.element_type in (ElementType.COMMENT_SINGLE,
1686
+ ElementType.COMMENT_MULTI)]
1687
+
1688
+ # Extract file description from first comment(s), skip shebangs
1689
+ for first_c in comments:
1690
+ if first_c.line_start > 10:
1691
+ break
1692
+ text = _extract_comment_text(first_c)
1693
+ # Skip shebang lines and empty comments
1694
+ if text and not text.startswith("/usr/") and not text.startswith("usr/"):
1695
+ file_description = text
1696
+ if len(file_description) > 200:
1697
+ file_description = file_description[:197] + "..."
1698
+ break
1699
+
1700
+ for c in comments:
1701
+ # Skip inline comments (name == "inline")
1702
+ if c.name == "inline":
1703
+ continue
1704
+
1705
+ attached = False
1706
+ # Check if this comment precedes a definition within 2 lines
1707
+ for gap in range(1, 4):
1708
+ target_line = c.line_end + gap
1709
+ if target_line in def_starts:
1710
+ doc_for_def.setdefault(target_line, []).append(c)
1711
+ attached = True
1712
+ break
1713
+ # Stop if we hit another element
1714
+ if target_line in import_starts:
1715
+ break
1716
+
1717
+ if not attached and c != comments[0]:
1718
+ # Skip file-level description (already captured)
1719
+ standalone_comments.append(c)
1720
+ elif not attached and c == comments[0] and not file_description:
1721
+ standalone_comments.append(c)
1722
+
1723
+ return doc_for_def, standalone_comments, file_description
1724
+
1725
+
1726
+ def _render_body_annotations(out: list, elem, indent: str = "",
1727
+ exclude_ranges: Optional[list] = None):
1728
+ """!
1729
+ @brief Render body comments and exit points for a definition element.
1730
+ @details Merges body_comments and exit_points in line-number order, outputting each as L<N>> text. When both a comment and exit point exist on the same line, merges them as: L<N>> `return` — comment text. Skips annotations within exclude_ranges.
1731
+ @param out Input parameter `out`.
1732
+ @param elem Input parameter `elem`.
1733
+ @param indent Input parameter `indent`.
1734
+ @param exclude_ranges Input parameter `exclude_ranges`.
1735
+ @return {None} Function return value.
1736
+ """
1737
+ # Build maps by line number
1738
+ comment_map = {}
1739
+ for (start, end, text) in getattr(elem, 'body_comments', []):
1740
+ comment_map[start] = (start, end, text)
1741
+
1742
+ exit_map = {}
1743
+ for (line_num, text) in getattr(elem, 'exit_points', []):
1744
+ exit_map[line_num] = text
1745
+
1746
+ # Collect all annotated line numbers
1747
+ all_lines = sorted(set(comment_map.keys()) | set(exit_map.keys()))
1748
+
1749
+ for line_num in all_lines:
1750
+ # Skip if within an excluded range (child element)
1751
+ if exclude_ranges:
1752
+ in_child = False
1753
+ for cstart, cend in exclude_ranges:
1754
+ if cstart <= line_num <= cend:
1755
+ in_child = True
1756
+ break
1757
+ if in_child:
1758
+ continue
1759
+
1760
+ has_comment = line_num in comment_map
1761
+ has_exit = line_num in exit_map
1762
+
1763
+ if has_comment and has_exit:
1764
+ # Merge: show exit point code with comment as context
1765
+ exit_text = exit_map[line_num]
1766
+ comment_text = comment_map[line_num][2]
1767
+ # Strip inline comment from exit_text if it contains it
1768
+ exit_clean = exit_text
1769
+ if '#' in exit_clean:
1770
+ exit_clean = exit_clean[:exit_clean.index('#')].strip()
1771
+ elif '//' in exit_clean:
1772
+ exit_clean = exit_clean[:exit_clean.index('//')].strip()
1773
+ out.append(f"{indent}L{line_num}> `{exit_clean}` — {comment_text}")
1774
+ elif has_exit:
1775
+ out.append(f"{indent}L{line_num}> `{exit_map[line_num]}`")
1776
+ elif has_comment:
1777
+ start, end, text = comment_map[line_num]
1778
+ if start == end:
1779
+ out.append(f"{indent}L{start}> {text}")
1780
+ else:
1781
+ out.append(f"{indent}L{start}-{end}> {text}")
1782
+
1783
+
1784
+ def _merge_doxygen_fields(
1785
+ base_fields: dict[str, list[str]],
1786
+ extra_fields: dict[str, list[str]],
1787
+ ) -> dict[str, list[str]]:
1788
+ """!
1789
+ @brief Merge Doxygen field dictionaries preserving per-tag value order.
1790
+ @param base_fields Destination dictionary mutated in place.
1791
+ @param extra_fields Source dictionary containing additional tag values.
1792
+ @return Updated destination dictionary.
1793
+ @details Implements the _merge_doxygen_fields function behavior with deterministic control flow.
1794
+ """
1795
+ for tag, values in extra_fields.items():
1796
+ if tag not in base_fields:
1797
+ base_fields[tag] = []
1798
+ base_fields[tag].extend(values)
1799
+ return base_fields
1800
+
1801
+
1802
+ def _collect_element_doxygen_fields(elem) -> dict[str, list[str]]:
1803
+ """! @brief Aggregate construct Doxygen fields from associated and body comments.
1804
+ @param elem SourceElement containing optional `doxygen_fields` and `body_comments`.
1805
+ @return Dictionary of normalized Doxygen tags to ordered value lists.
1806
+ @details Uses canonical `elem.doxygen_fields` from `SourceAnalyzer.enrich()` and merges only body comments located at construct start (first 3 lines) to retain docstring-style Doxygen blocks while avoiding internal-body duplication.
1807
+ """
1808
+ aggregate: dict[str, list[str]] = {}
1809
+ direct_fields = getattr(elem, "doxygen_fields", None) or {}
1810
+ if direct_fields:
1811
+ _merge_doxygen_fields(aggregate, direct_fields)
1812
+
1813
+ for body_comment in getattr(elem, "body_comments", []):
1814
+ if not isinstance(body_comment, tuple) or len(body_comment) < 3:
1815
+ continue
1816
+ comment_line_start = body_comment[0]
1817
+ if comment_line_start > elem.line_start + 3:
1818
+ continue
1819
+ parsed = parse_doxygen_comment(body_comment[2])
1820
+ if parsed:
1821
+ _merge_doxygen_fields(aggregate, parsed)
1822
+
1823
+ return aggregate
1824
+
1825
+
1826
+ def _collect_file_level_doxygen_fields(elements: list) -> dict[str, list[str]]:
1827
+ """! @brief Extract file-level Doxygen fields from the first `@file` documentation block.
1828
+ @param elements Parsed SourceElement list for one source file.
1829
+ @return Parsed Doxygen fields from the file-level documentation block; empty dictionary if not found.
1830
+ @details Scans non-inline comment elements in source order and selects the first comment containing `@file` or `\\file`, then parses the full comment text through `parse_doxygen_comment()`.
1831
+ """
1832
+ file_tag_pattern = re.compile(r"(?<!\w)(?:@|\\)file\b")
1833
+ comment_elements = sorted(
1834
+ [
1835
+ elem for elem in elements
1836
+ if elem.element_type in (ElementType.COMMENT_SINGLE, ElementType.COMMENT_MULTI)
1837
+ and elem.name != "inline"
1838
+ ],
1839
+ key=lambda elem: (elem.line_start, elem.line_end),
1840
+ )
1841
+ for comment in comment_elements:
1842
+ comment_text = comment.comment_source or comment.extract
1843
+ if not comment_text:
1844
+ continue
1845
+ if not file_tag_pattern.search(comment_text):
1846
+ continue
1847
+ return parse_doxygen_comment(comment_text)
1848
+ return {}
1849
+
1850
+
1851
+ def format_markdown(
1852
+ elements: list,
1853
+ filepath: str,
1854
+ language: str,
1855
+ spec_name: str,
1856
+ total_lines: int,
1857
+ include_legacy_annotations: bool = True,
1858
+ ) -> str:
1859
+ """! @brief Format analysis as compact Markdown optimized for LLM agent consumption.
1860
+ @details Produces token-efficient output with: - File header with language, line count, element summary, and optional file description - Imports in a code block - Hierarchical definitions enriched with ordered Doxygen field bullets when available - Optional legacy comment/exit traces when include_legacy_annotations is enabled - Symbol index table for quick reference by line number.
1861
+ @param elements Parsed and optionally enriched SourceElement list.
1862
+ @param filepath Absolute or relative source file path.
1863
+ @param language Normalized source language key.
1864
+ @param spec_name Display language name from LanguageSpec.
1865
+ @param total_lines Total source file line count.
1866
+ @param include_legacy_annotations Enable legacy L<n>> comment/exit traces and standalone comments section.
1867
+ @return Markdown payload for one analyzed file.
1868
+ """
1869
+ out = []
1870
+ fname = os.path.basename(filepath)
1871
+
1872
+ skip_types = (ElementType.COMMENT_SINGLE, ElementType.COMMENT_MULTI,
1873
+ ElementType.IMPORT, ElementType.DECORATOR)
1874
+ n_defs = sum(1 for e in elements if e.element_type not in skip_types)
1875
+ n_imports = sum(1 for e in elements if e.element_type == ElementType.IMPORT)
1876
+
1877
+ # Build comment association maps
1878
+ doc_for_def, standalone_comments, file_desc = _build_comment_maps(elements)
1879
+ if not include_legacy_annotations:
1880
+ standalone_comments = []
1881
+ file_desc = ""
1882
+
1883
+ try:
1884
+ from .doxygen_parser import format_doxygen_fields_as_markdown
1885
+ except ImportError:
1886
+ from usereq.doxygen_parser import format_doxygen_fields_as_markdown
1887
+ file_level_doxygen_fields = _collect_file_level_doxygen_fields(elements)
1888
+
1889
+ # ── Header ────────────────────────────────────────────────────────
1890
+ n_comments = sum(1 for e in elements
1891
+ if e.element_type in (ElementType.COMMENT_SINGLE,
1892
+ ElementType.COMMENT_MULTI)
1893
+ and e.name != "inline")
1894
+ out.append(f"# {fname} | {spec_name} | {total_lines}L | "
1895
+ f"{n_defs} symbols | {n_imports} imports | {n_comments} comments")
1896
+ out.append(f"> Path: `{filepath}`")
1897
+ if file_desc:
1898
+ out.append(f"> {file_desc}")
1899
+ if file_level_doxygen_fields:
1900
+ out.extend(format_doxygen_fields_as_markdown(file_level_doxygen_fields))
1901
+ out.append("")
1902
+
1903
+ # ── Imports ───────────────────────────────────────────────────────
1904
+ imports = sorted(
1905
+ [e for e in elements if e.element_type == ElementType.IMPORT],
1906
+ key=lambda e: e.line_start)
1907
+ if imports:
1908
+ out.append("## Imports")
1909
+ out.append("```")
1910
+ for imp in imports:
1911
+ first = imp.extract.split("\n")[0].strip()
1912
+ out.append(first)
1913
+ out.append("```")
1914
+ out.append("")
1915
+
1916
+ # ── Build decorator map: line -> decorator text ───────────────────
1917
+ dec_map = {}
1918
+ for e in elements:
1919
+ if e.element_type == ElementType.DECORATOR:
1920
+ dec_map[e.line_start] = e.extract.split("\n")[0].strip()
1921
+
1922
+ # ── Definitions ───────────────────────────────────────────────────
1923
+ defs = sorted(
1924
+ [e for e in elements if e.element_type not in skip_types],
1925
+ key=lambda e: e.line_start)
1926
+
1927
+ top_level = [e for e in defs if e.depth == 0]
1928
+ children_map = {}
1929
+ for e in defs:
1930
+ if e.depth > 0 and e.parent_name:
1931
+ for top in top_level:
1932
+ if (top.name == e.parent_name
1933
+ and top.line_start <= e.line_start
1934
+ and top.line_end >= e.line_end):
1935
+ children_map.setdefault(id(top), []).append(e)
1936
+ break
1937
+
1938
+ inline_types = (ElementType.CONSTANT, ElementType.VARIABLE,
1939
+ ElementType.TYPE_ALIAS, ElementType.TYPEDEF,
1940
+ ElementType.MACRO, ElementType.PROPERTY)
1941
+
1942
+ if top_level:
1943
+ out.append("## Definitions")
1944
+ out.append("")
1945
+
1946
+ i = 0
1947
+ while i < len(top_level):
1948
+ elem = top_level[i]
1949
+ kind = _md_kind(elem)
1950
+ sig = elem.signature or elem.name or ""
1951
+ loc = _md_loc(elem)
1952
+ inherit_str = f" : {elem.inherits}" if elem.inherits else ""
1953
+ vis_str = ""
1954
+ if elem.visibility and elem.visibility not in ("pub", "public"):
1955
+ vis_str = f" `{elem.visibility}`"
1956
+
1957
+ dec_str = ""
1958
+ dec_line = elem.line_start - 1
1959
+ if dec_line in dec_map:
1960
+ dec_str = f" `{dec_map[dec_line]}`"
1961
+
1962
+ # Collect associated doc comments for this definition
1963
+ # Prefer Doxygen fields if present (DOX-009).
1964
+ def_docs = doc_for_def.get(elem.line_start, [])
1965
+ doc_text = ""
1966
+ doc_lines_list = []
1967
+ doc_line_num = 0
1968
+ doxygen_markdown = []
1969
+ aggregate_doxygen_fields = _collect_element_doxygen_fields(elem)
1970
+
1971
+ if aggregate_doxygen_fields:
1972
+ doxygen_markdown = format_doxygen_fields_as_markdown(aggregate_doxygen_fields)
1973
+ # Use brief as inline doc_text if available
1974
+ if (
1975
+ "brief" in aggregate_doxygen_fields
1976
+ and aggregate_doxygen_fields["brief"]
1977
+ ):
1978
+ doc_text = aggregate_doxygen_fields["brief"][0]
1979
+ if len(doc_text) > 150:
1980
+ doc_text = doc_text[:147] + "..."
1981
+ elif include_legacy_annotations and def_docs:
1982
+ doc_lines_list = _extract_comment_lines(def_docs[0])
1983
+ doc_text = " ".join(doc_lines_list) if doc_lines_list else ""
1984
+ doc_line_num = def_docs[0].line_start
1985
+ if len(doc_text) > 150:
1986
+ doc_text = doc_text[:147] + "..."
1987
+
1988
+ is_single_line = (elem.line_start == elem.line_end)
1989
+ is_inline = (elem.element_type in inline_types or is_single_line)
1990
+
1991
+ if is_inline:
1992
+ first_line = elem.extract.split("\n")[0].strip()
1993
+ line = f"- {kind} `{first_line}`{vis_str} (L{elem.line_start})"
1994
+ if include_legacy_annotations and doc_text:
1995
+ line += f" — {doc_text}"
1996
+ out.append(line)
1997
+ if doxygen_markdown:
1998
+ out.extend(doxygen_markdown)
1999
+ i += 1
2000
+ else:
2001
+ # For impl blocks, use the full first line as sig
2002
+ if elem.element_type == ElementType.IMPL:
2003
+ first_line = elem.extract.split("\n")[0].strip()
2004
+ sig = first_line.rstrip(" {")
2005
+
2006
+ out.append(f"### {kind} `{sig}`{inherit_str}{vis_str}"
2007
+ f"{dec_str} ({loc})")
2008
+
2009
+ # Show Doxygen fields if present, else raw comment (DOX-009)
2010
+ if doxygen_markdown:
2011
+ out.extend(doxygen_markdown)
2012
+ elif include_legacy_annotations and doc_lines_list and len(doc_lines_list) > 1:
2013
+ for idx, dline in enumerate(doc_lines_list[:5]):
2014
+ ln = doc_line_num + idx
2015
+ out.append(f"L{ln}> {dline}")
2016
+ if len(doc_lines_list) > 5:
2017
+ out.append(f"L{doc_line_num + 5}> ...")
2018
+ elif include_legacy_annotations and doc_text and doc_line_num:
2019
+ out.append(f"L{doc_line_num}> {doc_text}")
2020
+
2021
+ # Body annotations: comments and exit points
2022
+ # For containers with children, exclude annotations
2023
+ # that fall within a child's line range (including
2024
+ # doc comments that immediately precede the child)
2025
+ kids = children_map.get(id(elem), [])
2026
+ if include_legacy_annotations:
2027
+ child_ranges = []
2028
+ for c in kids:
2029
+ range_start = c.line_start
2030
+ # Extend range to include preceding doc comment
2031
+ c_docs = doc_for_def.get(c.line_start, [])
2032
+ if c_docs:
2033
+ range_start = min(range_start,
2034
+ c_docs[0].line_start)
2035
+ child_ranges.append((range_start, c.line_end))
2036
+ _render_body_annotations(out, elem,
2037
+ exclude_ranges=child_ranges)
2038
+
2039
+ # Children with their doc comments and body annotations
2040
+ if kids:
2041
+ for child in sorted(kids, key=lambda e: e.line_start):
2042
+ child_sig = child.signature or child.name or ""
2043
+ child_loc = _md_loc(child)
2044
+ child_kind = _md_kind(child)
2045
+ child_vis = ""
2046
+ if (child.visibility
2047
+ and child.visibility not in ("pub", "public")):
2048
+ child_vis = f" `{child.visibility}`"
2049
+ child_doc = ""
2050
+ child_doc_ln = ""
2051
+ child_doxygen_markdown = []
2052
+ child_aggregate_doxygen_fields = _collect_element_doxygen_fields(child)
2053
+ if child_aggregate_doxygen_fields:
2054
+ child_doxygen_markdown = format_doxygen_fields_as_markdown(
2055
+ child_aggregate_doxygen_fields
2056
+ )
2057
+ elif include_legacy_annotations:
2058
+ child_docs = doc_for_def.get(child.line_start, [])
2059
+ if child_docs:
2060
+ child_doc_text = _extract_comment_text(
2061
+ child_docs[0], max_length=100)
2062
+ if child_doc_text:
2063
+ child_doc_ln = f" L{child_docs[0].line_start}>"
2064
+ child_doc = f" {child_doc_text}"
2065
+ out.append(f"- {child_kind} `{child_sig}`"
2066
+ f"{child_vis} ({child_loc})"
2067
+ f"{child_doc_ln}{child_doc}")
2068
+ if child_doxygen_markdown:
2069
+ out.extend(f" {line}" for line in child_doxygen_markdown)
2070
+ # Child body annotations (indented)
2071
+ if include_legacy_annotations:
2072
+ _render_body_annotations(out, child, indent=" ")
2073
+
2074
+ out.append("")
2075
+ i += 1
2076
+
2077
+ # ── Standalone Comments (section/region markers, TODOs, notes) ────
2078
+ if include_legacy_annotations and standalone_comments:
2079
+ out.append("## Comments")
2080
+ # Group consecutive comments (within 2 lines of each other)
2081
+ groups = []
2082
+ current_group = [standalone_comments[0]]
2083
+ for c in standalone_comments[1:]:
2084
+ prev = current_group[-1]
2085
+ if c.line_start <= prev.line_end + 2:
2086
+ current_group.append(c)
2087
+ else:
2088
+ groups.append(current_group)
2089
+ current_group = [c]
2090
+ groups.append(current_group)
2091
+
2092
+ for group in groups:
2093
+ if len(group) == 1:
2094
+ c = group[0]
2095
+ text = _extract_comment_text(c, max_length=150)
2096
+ if text:
2097
+ out.append(f"- L{c.line_start}: {text}")
2098
+ else:
2099
+ # Multi-line comment block: show as region
2100
+ start_line = group[0].line_start
2101
+ end_line = group[-1].line_end
2102
+ texts = []
2103
+ for c in group:
2104
+ t = _extract_comment_text(c, max_length=100)
2105
+ if t:
2106
+ texts.append(t)
2107
+ if texts:
2108
+ out.append(f"- L{start_line}-{end_line}: "
2109
+ + " | ".join(texts))
2110
+ out.append("")
2111
+
2112
+ # ── Symbol Index ──────────────────────────────────────────────────
2113
+ indexable = sorted(
2114
+ [e for e in elements if e.element_type not in (
2115
+ ElementType.COMMENT_SINGLE, ElementType.COMMENT_MULTI,
2116
+ ElementType.IMPORT, ElementType.DECORATOR)],
2117
+ key=lambda e: e.line_start)
2118
+
2119
+ if indexable:
2120
+ out.append("## Symbol Index")
2121
+ out.append("|Symbol|Kind|Vis|Lines|Sig|")
2122
+ out.append("|---|---|---|---|---|")
2123
+ for elem in indexable:
2124
+ name = elem.name or "?"
2125
+ if elem.parent_name:
2126
+ name = f"{elem.parent_name}.{name}"
2127
+ kind = _md_kind(elem)
2128
+ vis = elem.visibility or ""
2129
+ loc = (f"{elem.line_start}"
2130
+ if elem.line_start == elem.line_end
2131
+ else f"{elem.line_start}-{elem.line_end}")
2132
+ # Only show sig for functions/methods/classes (not vars/consts
2133
+ # which already show full content in Definitions section)
2134
+ sig = ""
2135
+ if (elem.element_type in (
2136
+ ElementType.FUNCTION, ElementType.METHOD,
2137
+ ElementType.CLASS, ElementType.STRUCT,
2138
+ ElementType.TRAIT, ElementType.INTERFACE,
2139
+ ElementType.IMPL, ElementType.ENUM)
2140
+ and elem.signature
2141
+ and elem.signature != elem.name):
2142
+ sig = elem.signature
2143
+ if len(sig) > 60:
2144
+ sig = sig[:57] + "..."
2145
+ out.append(f"|`{name}`|{kind}|{vis}|{loc}|{sig}|")
2146
+ out.append("")
2147
+
2148
+ return "\n".join(out)
2149
+
2150
+
2151
+ def main():
2152
+ """!
2153
+ @brief Execute the standalone source analyzer CLI command.
2154
+ @details Implements the main function behavior with deterministic control flow.
2155
+ @return {None} Function return value.
2156
+ """
2157
+ parser = argparse.ArgumentParser(
2158
+ description=(
2159
+ "Analyze a source file and extract definitions, comments "
2160
+ "and code structure. Inspired by tree-sitter."
2161
+ ),
2162
+ formatter_class=argparse.RawDescriptionHelpFormatter,
2163
+ epilog=(
2164
+ "Supported languages:\n"
2165
+ " python, c, cpp, rust, javascript, typescript, java, go,\n"
2166
+ " ruby, php, swift, kotlin, scala, lua, shell, perl,\n"
2167
+ " haskell, zig, elixir, csharp\n"
2168
+ "\n"
2169
+ "Common aliases: py, js, ts, rs, rb, cs, sh, bash, kt, hs,\n"
2170
+ " cc, cxx, h, hpp, ex, exs, pl, zsh\n"
2171
+ "\n"
2172
+ "Examples:\n"
2173
+ " %(prog)s main.py python\n"
2174
+ " %(prog)s server.js javascript\n"
2175
+ " %(prog)s lib.rs rust\n"
2176
+ " %(prog)s main.c c\n"
2177
+ ),
2178
+ )
2179
+ parser.add_argument(
2180
+ "file",
2181
+ help="Path to source file to analyze",
2182
+ )
2183
+ parser.add_argument(
2184
+ "language",
2185
+ help="File format/language (e.g.: python, c, rust, javascript)",
2186
+ )
2187
+ parser.add_argument(
2188
+ "-q", "--quiet",
2189
+ action="store_true",
2190
+ help="Show only structured listing, without headers",
2191
+ )
2192
+ parser.add_argument(
2193
+ "-d", "--definitions-only",
2194
+ action="store_true",
2195
+ help="Show only definitions (without comments)",
2196
+ )
2197
+ parser.add_argument(
2198
+ "-c", "--comments-only",
2199
+ action="store_true",
2200
+ help="Show only comments",
2201
+ )
2202
+ parser.add_argument(
2203
+ "-m", "--markdown",
2204
+ action="store_true",
2205
+ help="Output in compact Markdown format optimized for LLM agent context",
2206
+ )
2207
+ parser.add_argument(
2208
+ "--list-languages",
2209
+ action="store_true",
2210
+ help="Show supported languages and exit",
2211
+ )
2212
+
2213
+ args = parser.parse_args()
2214
+ analyzer = SourceAnalyzer()
2215
+
2216
+ if args.list_languages:
2217
+ print("Supported languages:")
2218
+ for lang in analyzer.get_supported_languages():
2219
+ spec = analyzer.specs[lang]
2220
+ print(f" {lang:<14} ({spec.name})")
2221
+ sys.exit(0)
2222
+
2223
+ try:
2224
+ elements = analyzer.analyze(args.file, args.language)
2225
+ except FileNotFoundError:
2226
+ print(f"Error: file '{args.file}' not found.", file=sys.stderr)
2227
+ sys.exit(1)
2228
+ except ValueError as e:
2229
+ print(f"Error: {e}", file=sys.stderr)
2230
+ sys.exit(1)
2231
+
2232
+ # Optional filtering
2233
+ if args.definitions_only:
2234
+ elements = [e for e in elements
2235
+ if e.element_type not in (ElementType.COMMENT_SINGLE,
2236
+ ElementType.COMMENT_MULTI)]
2237
+ elif args.comments_only:
2238
+ elements = [e for e in elements
2239
+ if e.element_type in (ElementType.COMMENT_SINGLE,
2240
+ ElementType.COMMENT_MULTI)]
2241
+
2242
+ lang_key = args.language.lower().strip().lstrip(".")
2243
+ spec = analyzer.specs[lang_key]
2244
+
2245
+ if args.definitions_only:
2246
+ elements = [e for e in elements
2247
+ if e.element_type not in (ElementType.COMMENT_SINGLE,
2248
+ ElementType.COMMENT_MULTI)]
2249
+ if args.comments_only:
2250
+ elements = [e for e in elements
2251
+ if e.element_type in (ElementType.COMMENT_SINGLE,
2252
+ ElementType.COMMENT_MULTI)]
2253
+
2254
+ if args.markdown or not args.quiet:
2255
+ with open(args.file, "r", encoding="utf-8", errors="replace") as f:
2256
+ total_lines = sum(1 for _ in f)
2257
+ analyzer.enrich(elements, lang_key, filepath=args.file)
2258
+ output = format_markdown(
2259
+ elements, args.file, lang_key, spec.name, total_lines)
2260
+ print(output)
2261
+ elif args.quiet:
2262
+ sorted_elements = sorted(elements, key=lambda e: e.line_start)
2263
+ prev_line_end = None
2264
+ for elem in sorted_elements:
2265
+ if prev_line_end is not None and elem.line_start > prev_line_end + 1:
2266
+ print()
2267
+ name_str = f" {elem.name}" if elem.name and elem.name != "inline" else ""
2268
+ location = (f"L{elem.line_start}"
2269
+ if elem.line_start == elem.line_end
2270
+ else f"L{elem.line_start}-L{elem.line_end}")
2271
+ print(f"{elem.line_start:>6} | [{elem.type_label}]{name_str} {location}")
2272
+ first_line = elem.extract.split("\n")[0]
2273
+ if len(first_line) > 72:
2274
+ first_line = first_line[:69] + "..."
2275
+ print(f" | {first_line}")
2276
+ prev_line_end = elem.line_end
2277
+
2278
+
2279
+ if __name__ == "__main__":
2280
+ main()