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.
- cli_usereq-0.68.0.dist-info/METADATA +1134 -0
- cli_usereq-0.68.0.dist-info/RECORD +37 -0
- cli_usereq-0.68.0.dist-info/WHEEL +5 -0
- cli_usereq-0.68.0.dist-info/entry_points.txt +4 -0
- cli_usereq-0.68.0.dist-info/licenses/LICENSE +674 -0
- cli_usereq-0.68.0.dist-info/top_level.txt +1 -0
- usereq/__init__.py +62 -0
- usereq/__main__.py +17 -0
- usereq/cli.py +4963 -0
- usereq/compress.py +393 -0
- usereq/compress_files.py +134 -0
- usereq/doxygen_parser.py +172 -0
- usereq/find_constructs.py +394 -0
- usereq/generate_markdown.py +158 -0
- usereq/resources/docs/Document_Source_Code_in_Doxygen_Style.md +130 -0
- usereq/resources/docs/HDT_Test_Authoring_Guide.md +318 -0
- usereq/resources/docs/Requirements_Template.md +78 -0
- usereq/resources/prompts/analyze.md +191 -0
- usereq/resources/prompts/change.md +279 -0
- usereq/resources/prompts/check.md +190 -0
- usereq/resources/prompts/cover.md +271 -0
- usereq/resources/prompts/create.md +134 -0
- usereq/resources/prompts/fix.md +273 -0
- usereq/resources/prompts/flowchart.md +272 -0
- usereq/resources/prompts/implement.md +194 -0
- usereq/resources/prompts/new.md +278 -0
- usereq/resources/prompts/readme.md +234 -0
- usereq/resources/prompts/recreate.md +265 -0
- usereq/resources/prompts/refactor.md +265 -0
- usereq/resources/prompts/references.md +131 -0
- usereq/resources/prompts/renumber.md +150 -0
- usereq/resources/prompts/workflow.md +254 -0
- usereq/resources/prompts/write.md +129 -0
- usereq/resources/prompts-version.txt +1 -0
- usereq/source_analyzer.py +2280 -0
- usereq/static_check.py +812 -0
- 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()
|