cli-usereq 0.68.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. cli_usereq-0.68.0.dist-info/METADATA +1134 -0
  2. cli_usereq-0.68.0.dist-info/RECORD +37 -0
  3. cli_usereq-0.68.0.dist-info/WHEEL +5 -0
  4. cli_usereq-0.68.0.dist-info/entry_points.txt +4 -0
  5. cli_usereq-0.68.0.dist-info/licenses/LICENSE +674 -0
  6. cli_usereq-0.68.0.dist-info/top_level.txt +1 -0
  7. usereq/__init__.py +62 -0
  8. usereq/__main__.py +17 -0
  9. usereq/cli.py +4963 -0
  10. usereq/compress.py +393 -0
  11. usereq/compress_files.py +134 -0
  12. usereq/doxygen_parser.py +172 -0
  13. usereq/find_constructs.py +394 -0
  14. usereq/generate_markdown.py +158 -0
  15. usereq/resources/docs/Document_Source_Code_in_Doxygen_Style.md +130 -0
  16. usereq/resources/docs/HDT_Test_Authoring_Guide.md +318 -0
  17. usereq/resources/docs/Requirements_Template.md +78 -0
  18. usereq/resources/prompts/analyze.md +191 -0
  19. usereq/resources/prompts/change.md +279 -0
  20. usereq/resources/prompts/check.md +190 -0
  21. usereq/resources/prompts/cover.md +271 -0
  22. usereq/resources/prompts/create.md +134 -0
  23. usereq/resources/prompts/fix.md +273 -0
  24. usereq/resources/prompts/flowchart.md +272 -0
  25. usereq/resources/prompts/implement.md +194 -0
  26. usereq/resources/prompts/new.md +278 -0
  27. usereq/resources/prompts/readme.md +234 -0
  28. usereq/resources/prompts/recreate.md +265 -0
  29. usereq/resources/prompts/refactor.md +265 -0
  30. usereq/resources/prompts/references.md +131 -0
  31. usereq/resources/prompts/renumber.md +150 -0
  32. usereq/resources/prompts/workflow.md +254 -0
  33. usereq/resources/prompts/write.md +129 -0
  34. usereq/resources/prompts-version.txt +1 -0
  35. usereq/source_analyzer.py +2280 -0
  36. usereq/static_check.py +812 -0
  37. usereq/token_counter.py +123 -0
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env python3
2
+ """!
3
+ @file doxygen_parser.py
4
+ @brief Doxygen comment parser for extracting structured documentation fields.
5
+ @details Parses Doxygen-formatted documentation comments and extracts recognized tags (@brief, @param, @return, etc.) into structured dictionaries for downstream LLM-optimized markdown rendering.
6
+ @author GitHub Copilot
7
+ @version 0.0.70
8
+ """
9
+
10
+ import re
11
+ from typing import Dict, List
12
+
13
+
14
+ # Supported Doxygen tags in extraction order
15
+ DOXYGEN_TAGS = [
16
+ 'brief',
17
+ 'details',
18
+ 'param',
19
+ 'param[in]',
20
+ 'param[out]',
21
+ 'param[in,out]',
22
+ 'return',
23
+ 'retval',
24
+ 'exception',
25
+ 'throws',
26
+ 'warning',
27
+ 'deprecated',
28
+ 'note',
29
+ 'see',
30
+ 'sa',
31
+ 'satisfies',
32
+ 'pre',
33
+ 'post',
34
+ ]
35
+
36
+ _NON_PARAM_TAGS = [tag for tag in DOXYGEN_TAGS if tag not in {'param', 'param[in]', 'param[in,out]', 'param[out]'}]
37
+ """! @brief Supported non-param Doxygen tags used to build the parser pattern."""
38
+
39
+ _NON_PARAM_TAG_ALTERNATION = "|".join(
40
+ re.escape(tag) for tag in sorted(_NON_PARAM_TAGS, key=len, reverse=True)
41
+ )
42
+ """! @brief Regex alternation for non-param tags ordered by descending length."""
43
+
44
+ DOXYGEN_TAG_PATTERN = re.compile(
45
+ r'[@\\]'
46
+ rf'(?:(param)(\[[^\]]+\])?|({_NON_PARAM_TAG_ALTERNATION}))'
47
+ )
48
+ """! @brief Compiled regex that matches supported @tag / \\tag tokens."""
49
+
50
+
51
+ def parse_doxygen_comment(comment_text: str) -> Dict[str, List[str]]:
52
+ """!
53
+ @brief Extract Doxygen fields from a documentation comment block.
54
+ @details Parses both @tag and \\tag syntax. Each tag's content extends until the next tag or end of comment. Multiple occurrences of the same tag accumulate in the returned list. Whitespace is normalized.
55
+ @param comment_text Raw comment string potentially containing Doxygen tags.
56
+ @return Dictionary mapping normalized tag names to lists of extracted content strings.
57
+ @note Returns empty dict if no Doxygen tags are found.
58
+ @see DOXYGEN_TAGS for recognized tag list.
59
+ """
60
+ if not comment_text or not comment_text.strip():
61
+ return {}
62
+
63
+ result: Dict[str, List[str]] = {}
64
+
65
+ # Normalize line endings and strip comment delimiters
66
+ text = comment_text.replace('\r\n', '\n').replace('\r', '\n')
67
+ text = _strip_comment_delimiters(text)
68
+
69
+ matches = list(DOXYGEN_TAG_PATTERN.finditer(text))
70
+
71
+ if not matches:
72
+ return {}
73
+
74
+ for i, match in enumerate(matches):
75
+ param_tag = match.group(1)
76
+ direction = match.group(2) if match.group(2) else ''
77
+ non_param_tag = match.group(3)
78
+
79
+ # Normalize tag key: param[in], param[out], param (no direction)
80
+ if param_tag:
81
+ normalized_tag = f'param{direction}' if direction else 'param'
82
+ else:
83
+ normalized_tag = non_param_tag
84
+
85
+ # Extract content from end of tag to start of next tag (or end of text)
86
+ start_pos = match.end()
87
+ end_pos = matches[i + 1].start() if i + 1 < len(matches) else len(text)
88
+
89
+ content = text[start_pos:end_pos].strip()
90
+ content = _normalize_whitespace(content)
91
+
92
+ if content:
93
+ if normalized_tag not in result:
94
+ result[normalized_tag] = []
95
+ result[normalized_tag].append(content)
96
+
97
+ return result
98
+
99
+
100
+ def _strip_comment_delimiters(text: str) -> str:
101
+ """!
102
+ @brief Remove common comment delimiters from text block.
103
+ @details Strips leading/trailing /**, */, //, #, triple quotes, and intermediate * column markers. Preserves content while removing comment syntax artifacts.
104
+ @param text Raw comment block possibly containing comment delimiters.
105
+ @return Cleaned text with delimiters removed.
106
+ """
107
+ lines = text.split('\n')
108
+ cleaned_lines = []
109
+
110
+ for line in lines:
111
+ stripped = line.strip()
112
+
113
+ # Remove common comment starters/enders
114
+ if stripped in ('/**', '/*', '*/', '"""', "'''", '/*!', '///', '//!'):
115
+ continue
116
+
117
+ # Remove leading comment markers: //, #, *, etc.
118
+ stripped = re.sub(r'^[/*#]+\s*', '', stripped)
119
+ stripped = re.sub(r'^\*\s*', '', stripped) # leading * from multi-line C-style
120
+ stripped = re.sub(r'^///?!?\s*', '', stripped) # // or /// or //!
121
+ stripped = re.sub(r'^#+\s*', '', stripped) # Python #
122
+
123
+ if stripped:
124
+ cleaned_lines.append(stripped)
125
+
126
+ return '\n'.join(cleaned_lines)
127
+
128
+
129
+ def _normalize_whitespace(text: str) -> str:
130
+ """!
131
+ @brief Normalize internal whitespace in extracted tag content.
132
+ @details Collapses multiple spaces to single space, preserves single newlines, removes redundant blank lines.
133
+ @param text Tag content with potentially irregular whitespace.
134
+ @return Whitespace-normalized content.
135
+ """
136
+ # Collapse multiple spaces to single space
137
+ text = re.sub(r' +', ' ', text)
138
+ # Remove leading/trailing whitespace per line
139
+ lines = [line.strip() for line in text.split('\n')]
140
+ # Remove consecutive blank lines
141
+ normalized_lines = []
142
+ prev_blank = False
143
+ for line in lines:
144
+ if not line:
145
+ if not prev_blank:
146
+ normalized_lines.append(line)
147
+ prev_blank = True
148
+ else:
149
+ normalized_lines.append(line)
150
+ prev_blank = False
151
+
152
+ return '\n'.join(normalized_lines).strip()
153
+
154
+
155
+ def format_doxygen_fields_as_markdown(doxygen_fields: Dict[str, List[str]]) -> List[str]:
156
+ """!
157
+ @brief Format extracted Doxygen fields as Markdown bulleted list.
158
+ @details Emits fields in fixed order (DOXYGEN_TAGS) preserving original Doxygen tag tokens with `@` prefix and no `:` suffix. Skips tags not present in input. Each extracted field occurrence is emitted as an independent markdown bullet.
159
+ @param doxygen_fields Dictionary of tag -> content list from parse_doxygen_comment().
160
+ @return List of Markdown lines (each starting with '- ').
161
+ @note Output order matches DOXYGEN_TAGS sequence.
162
+ """
163
+ lines = []
164
+ for tag in DOXYGEN_TAGS:
165
+ values = doxygen_fields.get(tag, [])
166
+ if not values:
167
+ continue
168
+ # Preserve original Doxygen tag token.
169
+ label = f'@{tag}'
170
+ for content in values:
171
+ lines.append(f'- {label} {content}')
172
+ return lines
@@ -0,0 +1,394 @@
1
+ #!/usr/bin/env python3
2
+ """!
3
+ @file find_constructs.py
4
+ @brief Find and extract specific constructs from source files.
5
+ @details Filters source code constructs (CLASS, FUNCTION, etc.) by type tag and name regex pattern, generating markdown output with complete code extracts.
6
+ @author GitHub Copilot
7
+ @version 0.0.70
8
+ """
9
+
10
+ import os
11
+ import re
12
+ import sys
13
+
14
+ from .doxygen_parser import format_doxygen_fields_as_markdown, parse_doxygen_comment
15
+ from .source_analyzer import SourceAnalyzer
16
+ from .compress import compress_source, detect_language
17
+
18
+
19
+ # ── Language-specific TAG support map ────────────────────────────────────────
20
+ LANGUAGE_TAGS = {
21
+ "python": {"CLASS", "FUNCTION", "DECORATOR", "IMPORT", "VARIABLE"},
22
+ "c": {"STRUCT", "UNION", "ENUM", "TYPEDEF", "MACRO", "FUNCTION", "IMPORT", "VARIABLE"},
23
+ "cpp": {"CLASS", "STRUCT", "ENUM", "NAMESPACE", "FUNCTION", "MACRO", "IMPORT", "TYPE_ALIAS"},
24
+ "rust": {"FUNCTION", "STRUCT", "ENUM", "TRAIT", "IMPL", "MODULE", "MACRO", "CONSTANT", "TYPE_ALIAS", "IMPORT", "DECORATOR"},
25
+ "javascript": {"CLASS", "FUNCTION", "COMPONENT", "CONSTANT", "IMPORT", "MODULE"},
26
+ "typescript": {"INTERFACE", "TYPE_ALIAS", "ENUM", "CLASS", "FUNCTION", "NAMESPACE", "MODULE", "IMPORT", "DECORATOR"},
27
+ "java": {"CLASS", "INTERFACE", "ENUM", "FUNCTION", "IMPORT", "MODULE", "DECORATOR", "CONSTANT"},
28
+ "go": {"FUNCTION", "METHOD", "STRUCT", "INTERFACE", "TYPE_ALIAS", "CONSTANT", "IMPORT", "MODULE"},
29
+ "ruby": {"CLASS", "MODULE", "FUNCTION", "CONSTANT", "IMPORT", "DECORATOR"},
30
+ "php": {"CLASS", "INTERFACE", "TRAIT", "FUNCTION", "NAMESPACE", "IMPORT", "CONSTANT"},
31
+ "swift": {"CLASS", "STRUCT", "ENUM", "PROTOCOL", "EXTENSION", "FUNCTION", "IMPORT", "CONSTANT", "VARIABLE"},
32
+ "kotlin": {"CLASS", "INTERFACE", "ENUM", "FUNCTION", "CONSTANT", "VARIABLE", "MODULE", "IMPORT", "DECORATOR"},
33
+ "scala": {"CLASS", "TRAIT", "MODULE", "FUNCTION", "CONSTANT", "VARIABLE", "TYPE_ALIAS", "IMPORT"},
34
+ "lua": {"FUNCTION", "VARIABLE"},
35
+ "shell": {"FUNCTION", "VARIABLE", "IMPORT"},
36
+ "perl": {"FUNCTION", "MODULE", "IMPORT", "CONSTANT"},
37
+ "haskell": {"MODULE", "TYPE_ALIAS", "STRUCT", "CLASS", "FUNCTION", "IMPORT"},
38
+ "zig": {"FUNCTION", "STRUCT", "ENUM", "UNION", "CONSTANT", "VARIABLE", "IMPORT"},
39
+ "elixir": {"MODULE", "FUNCTION", "PROTOCOL", "IMPL", "STRUCT", "IMPORT"},
40
+ "csharp": {"CLASS", "INTERFACE", "STRUCT", "ENUM", "NAMESPACE", "FUNCTION", "PROPERTY", "IMPORT", "DECORATOR", "CONSTANT"},
41
+ }
42
+
43
+
44
+ def format_available_tags() -> str:
45
+ """! @brief Generate formatted list of available TAGs per language.
46
+ @return Multi-line string listing each language with its supported TAGs.
47
+ @details Iterates LANGUAGE_TAGS dictionary, formats each entry as "- Language: TAG1, TAG2, ..." with language capitalized and tags alphabetically sorted and comma-separated.
48
+ """
49
+ lines = []
50
+ for lang in sorted(LANGUAGE_TAGS.keys()):
51
+ tags = sorted(LANGUAGE_TAGS[lang])
52
+ lang_display = lang.capitalize()
53
+ tags_str = ", ".join(tags)
54
+ lines.append(f"- {lang_display}: {tags_str}")
55
+ return "\n".join(lines)
56
+
57
+
58
+ def parse_tag_filter(tag_string: str) -> set[str]:
59
+ """! @brief Parse pipe-separated tag filter into a normalized set.
60
+ @param tag_string Raw tag filter string (e.g., "CLASS|FUNCTION").
61
+ @return Set of uppercase tag identifiers.
62
+ @details Splits the input string by pipe character `|` and strips whitespace from each component.
63
+ """
64
+ return {tag.strip().upper() for tag in tag_string.split("|") if tag.strip()}
65
+
66
+
67
+ def language_supports_tags(lang: str, tag_set: set[str]) -> bool:
68
+ """! @brief Check if the language supports at least one of the requested tags.
69
+ @param lang Normalized language identifier.
70
+ @param tag_set Set of requested TAG identifiers.
71
+ @return True if intersection is non-empty, False otherwise.
72
+ @details Lookups the language in `LANGUAGE_TAGS` and checks if any of `tag_set` exists in the supported tags.
73
+ """
74
+ supported = LANGUAGE_TAGS.get(lang, set())
75
+ return bool(supported & tag_set)
76
+
77
+
78
+ def construct_matches(element, tag_set: set[str], pattern: str) -> bool:
79
+ """! @brief Check if a source element matches tag filter and regex pattern.
80
+ @param element SourceElement instance from analyzer.
81
+ @param tag_set Set of requested TAG identifiers.
82
+ @param pattern Regex pattern string to test against element name.
83
+ @return True if element type is in tag_set and name matches pattern.
84
+ @details Validates the element type and then applies the regex search on the element name.
85
+ """
86
+ if element.type_label not in tag_set:
87
+ return False
88
+ if not element.name:
89
+ return False
90
+ try:
91
+ return bool(re.search(pattern, element.name))
92
+ except re.error:
93
+ return False
94
+
95
+
96
+ def _merge_doxygen_fields(
97
+ base_fields: dict[str, list[str]],
98
+ extra_fields: dict[str, list[str]],
99
+ ) -> dict[str, list[str]]:
100
+ """! @brief Merge Doxygen fields preserving per-tag content order.
101
+ @details Appends extra field values to base field lists for matching tags and initializes missing tags. Mutates and returns base_fields.
102
+ @param base_fields Canonical field dictionary to update.
103
+ @param extra_fields Additional parsed fields to append.
104
+ @return Updated base_fields dictionary.
105
+ """
106
+ for tag, values in extra_fields.items():
107
+ if tag not in base_fields:
108
+ base_fields[tag] = []
109
+ base_fields[tag].extend(values)
110
+ return base_fields
111
+
112
+
113
+ def _extract_construct_doxygen_fields(element) -> dict[str, list[str]]:
114
+ """! @brief Build aggregate Doxygen fields for one construct.
115
+ @details Uses canonical `element.doxygen_fields` from `SourceAnalyzer.enrich()` and merges only body comments located at construct start (first 3 lines) to retain docstring-style Doxygen blocks while preventing internal-body duplication.
116
+ @param element SourceElement instance potentially enriched with doxygen_fields and body_comments.
117
+ @return Dictionary tag->list preserving tag content insertion order.
118
+ """
119
+ aggregate: dict[str, list[str]] = {}
120
+
121
+ if hasattr(element, "doxygen_fields") and element.doxygen_fields:
122
+ _merge_doxygen_fields(aggregate, element.doxygen_fields)
123
+
124
+ for body_comment in getattr(element, "body_comments", []):
125
+ if not isinstance(body_comment, tuple) or len(body_comment) < 3:
126
+ continue
127
+ comment_line_start = body_comment[0]
128
+ if comment_line_start > element.line_start + 3:
129
+ continue
130
+ parsed_fields = parse_doxygen_comment(body_comment[2])
131
+ if parsed_fields:
132
+ _merge_doxygen_fields(aggregate, parsed_fields)
133
+
134
+ return aggregate
135
+
136
+
137
+ def _extract_file_level_doxygen_fields(elements: list) -> dict[str, list[str]]:
138
+ """! @brief Extract file-level Doxygen fields from the first comment containing `@file`.
139
+ @param elements SourceAnalyzer output for one source file.
140
+ @return Parsed Doxygen fields from the file-level comment; empty dictionary if absent.
141
+ @details Scans non-inline comment elements in source order and parses the first block containing `@file` or `\\file` markers.
142
+ """
143
+ file_tag_pattern = re.compile(r"(?<!\w)(?:@|\\)file\b")
144
+ comment_elements = sorted(
145
+ [
146
+ element for element in elements
147
+ if element.element_type.name.startswith("COMMENT")
148
+ and getattr(element, "name", "") != "inline"
149
+ ],
150
+ key=lambda element: (element.line_start, element.line_end),
151
+ )
152
+ for comment in comment_elements:
153
+ comment_text = comment.comment_source or comment.extract
154
+ if not comment_text:
155
+ continue
156
+ if not file_tag_pattern.search(comment_text):
157
+ continue
158
+ return parse_doxygen_comment(comment_text)
159
+ return {}
160
+
161
+
162
+ def _strip_construct_comments(
163
+ code_lines: list[str],
164
+ language: str,
165
+ line_start: int,
166
+ include_line_numbers: bool,
167
+ ) -> str:
168
+ """! @brief Remove comments from extracted construct code while preserving source line mapping.
169
+ @param code_lines Raw construct code lines sliced by SourceElement line range.
170
+ @param language Normalized language key used by the compression parser.
171
+ @param line_start Absolute start line number of the construct in the original file.
172
+ @param include_line_numbers If True, emit `<n>:` prefixes with absolute source line numbers.
173
+ @return Comment-stripped construct code string.
174
+ @details Delegates comment stripping to `compress_source()` to remove inline, single-line, and multi-line comments while preserving string literals. When line numbers are enabled, remaps local compressed line indices back to absolute file line numbers using `line_start`.
175
+ """
176
+ raw_source = "".join(code_lines)
177
+ stripped_with_local_numbers = compress_source(
178
+ raw_source,
179
+ language,
180
+ include_line_numbers=True,
181
+ )
182
+ stripped_lines = stripped_with_local_numbers.splitlines()
183
+
184
+ if not include_line_numbers:
185
+ return "\n".join(
186
+ line.split(": ", 1)[1] if ": " in line else line
187
+ for line in stripped_lines
188
+ )
189
+
190
+ remapped_lines: list[str] = []
191
+ for line in stripped_lines:
192
+ match = re.match(r"^(\d+):\s(.*)$", line)
193
+ if not match:
194
+ remapped_lines.append(line)
195
+ continue
196
+ local_line = int(match.group(1))
197
+ absolute_line = line_start + local_line - 1
198
+ remapped_lines.append(f"{absolute_line}: {match.group(2)}")
199
+ return "\n".join(remapped_lines)
200
+
201
+
202
+ def format_construct(
203
+ element,
204
+ source_lines: list[str],
205
+ include_line_numbers: bool,
206
+ language: str = "python",
207
+ ) -> str:
208
+ """! @brief Format a single matched construct for markdown output with complete code extraction.
209
+ @param element SourceElement instance containing line range indices.
210
+ @param source_lines Complete source file content as list of lines.
211
+ @param include_line_numbers If True, prefix code lines with <n>: format.
212
+ @param language Normalized source language key used for comment stripping.
213
+ @return Formatted markdown block for the construct with complete code from line_start to line_end.
214
+ @details Extracts construct code directly from source_lines using element.line_start and element.line_end indices, removes inline/single-line/multi-line comments from the extracted block, and inserts Doxygen metadata before the code fence when available.
215
+ """
216
+ lines = []
217
+ lines.append(f"### {element.type_label}: `{element.name}`")
218
+ if element.signature:
219
+ lines.append(f"> Signature: `{element.signature}`")
220
+ lines.append(f"> Lines: {element.line_start}-{element.line_end}")
221
+
222
+ # Insert Doxygen fields if present (DOX-010, DOX-011)
223
+ doxygen_fields = _extract_construct_doxygen_fields(element)
224
+ if doxygen_fields:
225
+ dox_lines = format_doxygen_fields_as_markdown(doxygen_fields)
226
+ lines.extend(dox_lines)
227
+
228
+ # Extract COMPLETE code block from source file and strip comments
229
+ code_lines = source_lines[element.line_start - 1:element.line_end]
230
+ formatted = _strip_construct_comments(
231
+ code_lines=code_lines,
232
+ language=language,
233
+ line_start=element.line_start,
234
+ include_line_numbers=include_line_numbers,
235
+ )
236
+
237
+ lines.append("```")
238
+ lines.append(formatted)
239
+ lines.append("```")
240
+
241
+ return "\n".join(lines)
242
+
243
+
244
+ def find_constructs_in_files(
245
+ filepaths: list[str],
246
+ tag_filter: str,
247
+ pattern: str,
248
+ include_line_numbers: bool = True,
249
+ verbose: bool = False,
250
+ ) -> str:
251
+ """! @brief Find and extract constructs matching tag filter and regex pattern from multiple files.
252
+ @param filepaths List of source file paths.
253
+ @param tag_filter Pipe-separated TAG identifiers (e.g., "CLASS|FUNCTION").
254
+ @param pattern Regex pattern for construct name matching.
255
+ @param include_line_numbers If True (default), prefix code lines with <n>: format.
256
+ @param verbose If True, emits progress status messages on stderr.
257
+ @return Concatenated markdown output string.
258
+ @throws ValueError If no files could be processed or no constructs found.
259
+ @details Analyzes each file with SourceAnalyzer, filters elements by tag and name pattern, formats results as markdown with file headers.
260
+ """
261
+ tag_set = parse_tag_filter(tag_filter)
262
+ if not tag_set:
263
+ available = format_available_tags()
264
+ raise ValueError(f"No valid tags specified in tag filter.\n\nAvailable tags by language:\n{available}")
265
+
266
+ parts = []
267
+ ok_count = 0
268
+ skip_count = 0
269
+ fail_count = 0
270
+ total_matches = 0
271
+
272
+ for fpath in filepaths:
273
+ if not os.path.isfile(fpath):
274
+ if verbose:
275
+ print(f" SKIP {fpath} (not found)", file=sys.stderr)
276
+ skip_count += 1
277
+ continue
278
+
279
+ lang = detect_language(fpath)
280
+ if not lang:
281
+ if verbose:
282
+ print(f" SKIP {fpath} (unsupported extension)", file=sys.stderr)
283
+ skip_count += 1
284
+ continue
285
+
286
+ # Check if language supports at least one requested tag
287
+ if not language_supports_tags(lang, tag_set):
288
+ if verbose:
289
+ print(f" SKIP {fpath} (language {lang} does not support any requested tags)", file=sys.stderr)
290
+ skip_count += 1
291
+ continue
292
+
293
+ try:
294
+ # Read complete source file for full construct extraction
295
+ with open(fpath, 'r', encoding='utf-8', errors='replace') as f:
296
+ source_lines = f.readlines()
297
+
298
+ analyzer = SourceAnalyzer()
299
+ elements = analyzer.analyze(fpath, lang)
300
+ analyzer.enrich(elements, lang, fpath)
301
+
302
+ # Filter elements matching tag and pattern
303
+ matches = [el for el in elements if construct_matches(el, tag_set, pattern)]
304
+
305
+ if matches:
306
+ header = f"@@@ {fpath} | {lang}"
307
+ file_level_doxygen_fields = _extract_file_level_doxygen_fields(elements)
308
+ file_level_doxygen_lines = []
309
+ if file_level_doxygen_fields:
310
+ file_level_doxygen_lines = format_doxygen_fields_as_markdown(
311
+ file_level_doxygen_fields
312
+ )
313
+ constructs_md = "\n\n".join(
314
+ format_construct(
315
+ el,
316
+ source_lines,
317
+ include_line_numbers,
318
+ language=lang,
319
+ )
320
+ for el in matches
321
+ )
322
+ if file_level_doxygen_lines:
323
+ file_level_block = "\n".join(file_level_doxygen_lines)
324
+ parts.append(f"{header}\n{file_level_block}\n\n{constructs_md}")
325
+ else:
326
+ parts.append(f"{header}\n\n{constructs_md}")
327
+ total_matches += len(matches)
328
+ ok_count += 1
329
+ if verbose:
330
+ print(f" OK {fpath} ({len(matches)} matches)", file=sys.stderr)
331
+ else:
332
+ if verbose:
333
+ print(f" SKIP {fpath} (no matches)", file=sys.stderr)
334
+ skip_count += 1
335
+
336
+ except Exception as e:
337
+ if verbose:
338
+ print(f" FAIL {fpath} ({e})", file=sys.stderr)
339
+ fail_count += 1
340
+
341
+ if not parts:
342
+ available = format_available_tags()
343
+ raise ValueError(f"No constructs found matching the specified criteria.\n\nAvailable tags by language:\n{available}")
344
+
345
+ if verbose:
346
+ print(
347
+ f"\n Found: {total_matches} constructs in {ok_count} files "
348
+ f"({skip_count} skipped, {fail_count} failed)",
349
+ file=sys.stderr,
350
+ )
351
+
352
+ return "\n\n".join(parts)
353
+
354
+
355
+ def main():
356
+ """!
357
+ @brief Execute the construct finding CLI command.
358
+ @details Parses arguments and calls find_constructs_in_files. Handles exceptions by printing errors to stderr.
359
+ @return {None} Function return value.
360
+ """
361
+ import argparse
362
+
363
+ parser = argparse.ArgumentParser(
364
+ description="Find and extract specific constructs from source files."
365
+ )
366
+ parser.add_argument(
367
+ "tag",
368
+ help="Pipe-separated construct types (e.g., CLASS|FUNCTION).",
369
+ )
370
+ parser.add_argument(
371
+ "pattern",
372
+ help="Regex pattern for construct name matching.",
373
+ )
374
+ parser.add_argument("files", nargs="+", help="Source files to search.")
375
+ parser.add_argument(
376
+ "--enable-line-numbers",
377
+ action="store_true",
378
+ default=False,
379
+ help="Enable line number prefixes (<n>:) in output.",
380
+ )
381
+ args = parser.parse_args()
382
+
383
+ try:
384
+ output = find_constructs_in_files(
385
+ args.files, args.tag, args.pattern, args.enable_line_numbers
386
+ )
387
+ print(output)
388
+ except ValueError as e:
389
+ print(f"Error: {e}", file=sys.stderr)
390
+ sys.exit(1)
391
+
392
+
393
+ if __name__ == "__main__":
394
+ main()