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
usereq/doxygen_parser.py
ADDED
|
@@ -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()
|