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/static_check.py
ADDED
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
"""!
|
|
2
|
+
@file static_check.py
|
|
3
|
+
@brief Static code analysis dispatch module implementing Dummy/Pylance/Ruff/Command check classes.
|
|
4
|
+
@details Provides a class hierarchy for running static analysis tools against resolved file lists.
|
|
5
|
+
Exposes `run_static_check(argv)` as the primary entry point for `--test-static-check` called
|
|
6
|
+
from cli.py. Also exposes `parse_enable_static_check`, `dispatch_static_check_for_file`,
|
|
7
|
+
`STATIC_CHECK_LANG_CANONICAL`, and `STATIC_CHECK_EXT_TO_LANG` for `--enable-static-check`,
|
|
8
|
+
`files-static-check` and `static-check` subcommand support.
|
|
9
|
+
Class hierarchy: StaticCheckBase (Dummy) -> StaticCheckPylance, StaticCheckRuff, StaticCheckCommand.
|
|
10
|
+
File resolution supports: explicit file paths, glob patterns (with full `**` recursive expansion),
|
|
11
|
+
and direct-children-only directory traversal. No custom `--recursive` flag; recursive traversal
|
|
12
|
+
is expressed via `**` glob syntax (e.g., `src/**/*.py`).
|
|
13
|
+
@author useReq
|
|
14
|
+
@version 0.0.72
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import glob
|
|
20
|
+
import importlib.util
|
|
21
|
+
import shutil
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import List, Optional, Sequence
|
|
26
|
+
|
|
27
|
+
from .cli import ReqError
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Language configuration constants (SRS-258, SRS-259)
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
STATIC_CHECK_LANG_CANONICAL: dict[str, str] = {
|
|
35
|
+
"python": "Python",
|
|
36
|
+
"c": "C",
|
|
37
|
+
"c++": "C++",
|
|
38
|
+
"cpp": "C++",
|
|
39
|
+
"c#": "C#",
|
|
40
|
+
"csharp": "C#",
|
|
41
|
+
"rust": "Rust",
|
|
42
|
+
"javascript": "JavaScript",
|
|
43
|
+
"js": "JavaScript",
|
|
44
|
+
"typescript": "TypeScript",
|
|
45
|
+
"ts": "TypeScript",
|
|
46
|
+
"java": "Java",
|
|
47
|
+
"go": "Go",
|
|
48
|
+
"ruby": "Ruby",
|
|
49
|
+
"php": "PHP",
|
|
50
|
+
"swift": "Swift",
|
|
51
|
+
"kotlin": "Kotlin",
|
|
52
|
+
"scala": "Scala",
|
|
53
|
+
"lua": "Lua",
|
|
54
|
+
"shell": "Shell",
|
|
55
|
+
"sh": "Shell",
|
|
56
|
+
"perl": "Perl",
|
|
57
|
+
"haskell": "Haskell",
|
|
58
|
+
"zig": "Zig",
|
|
59
|
+
"elixir": "Elixir",
|
|
60
|
+
}
|
|
61
|
+
"""!
|
|
62
|
+
@brief Maps lowercase language identifiers (including aliases) to canonical language names.
|
|
63
|
+
@details Covers all 20 languages from SRS-249 plus common aliases (cpp, csharp, js, ts, sh).
|
|
64
|
+
Canonical names: Python, C, C++, C#, Rust, JavaScript, TypeScript, Java, Go, Ruby,
|
|
65
|
+
PHP, Swift, Kotlin, Scala, Lua, Shell, Perl, Haskell, Zig, Elixir.
|
|
66
|
+
Lookup MUST be performed on `value.strip().lower()` of the user-supplied language token.
|
|
67
|
+
@see SRS-258, SRS-249
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
STATIC_CHECK_EXT_TO_LANG: dict[str, str] = {
|
|
71
|
+
".py": "Python",
|
|
72
|
+
".c": "C",
|
|
73
|
+
".cpp": "C++",
|
|
74
|
+
".cs": "C#",
|
|
75
|
+
".rs": "Rust",
|
|
76
|
+
".js": "JavaScript",
|
|
77
|
+
".mjs": "JavaScript",
|
|
78
|
+
".ts": "TypeScript",
|
|
79
|
+
".java": "Java",
|
|
80
|
+
".go": "Go",
|
|
81
|
+
".rb": "Ruby",
|
|
82
|
+
".php": "PHP",
|
|
83
|
+
".swift": "Swift",
|
|
84
|
+
".kt": "Kotlin",
|
|
85
|
+
".scala": "Scala",
|
|
86
|
+
".lua": "Lua",
|
|
87
|
+
".sh": "Shell",
|
|
88
|
+
".pl": "Perl",
|
|
89
|
+
".hs": "Haskell",
|
|
90
|
+
".zig": "Zig",
|
|
91
|
+
".ex": "Elixir",
|
|
92
|
+
}
|
|
93
|
+
"""!
|
|
94
|
+
@brief Maps file extensions (lowercase, dot-prefixed) to canonical language names.
|
|
95
|
+
@details Uses the extension set defined by SRS-131 language coverage, including both `.js` and `.mjs` for JavaScript.
|
|
96
|
+
Lookup MUST be performed on `Path(filepath).suffix.lower()`.
|
|
97
|
+
@see SRS-259, SRS-131
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
#: Canonical module names (lowercase -> display form).
|
|
101
|
+
_CANONICAL_MODULES: dict[str, str] = {
|
|
102
|
+
"dummy": "Dummy",
|
|
103
|
+
"pylance": "Pylance",
|
|
104
|
+
"ruff": "Ruff",
|
|
105
|
+
"command": "Command",
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
# Configuration parsing helpers (SRS-260)
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
def _split_csv_like_tokens(spec_rhs: str) -> list[str]:
|
|
114
|
+
"""!
|
|
115
|
+
@brief Split a comma-separated SPEC right-hand side with quote-aware token boundaries.
|
|
116
|
+
@param spec_rhs Text after `LANG=` in `--enable-static-check`.
|
|
117
|
+
@return Token list where commas inside `'...'` or `"..."` do not split tokens.
|
|
118
|
+
@details
|
|
119
|
+
- Supported quote delimiters: single quote `'` and double quote `"`.
|
|
120
|
+
- Commas split tokens only when parser is outside a quoted segment.
|
|
121
|
+
- Quote delimiters are not included in output tokens.
|
|
122
|
+
- Leading and trailing whitespace for each token is stripped.
|
|
123
|
+
@see SRS-260, SRS-250
|
|
124
|
+
"""
|
|
125
|
+
tokens: list[str] = []
|
|
126
|
+
current: list[str] = []
|
|
127
|
+
active_quote: str | None = None
|
|
128
|
+
|
|
129
|
+
for ch in spec_rhs:
|
|
130
|
+
if active_quote is None:
|
|
131
|
+
if ch in ("'", '"'):
|
|
132
|
+
active_quote = ch
|
|
133
|
+
elif ch == ",":
|
|
134
|
+
tokens.append("".join(current).strip())
|
|
135
|
+
current = []
|
|
136
|
+
else:
|
|
137
|
+
current.append(ch)
|
|
138
|
+
else:
|
|
139
|
+
if ch == active_quote:
|
|
140
|
+
active_quote = None
|
|
141
|
+
else:
|
|
142
|
+
current.append(ch)
|
|
143
|
+
|
|
144
|
+
tokens.append("".join(current).strip())
|
|
145
|
+
return tokens
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def parse_enable_static_check(spec: str) -> tuple[str, dict]:
|
|
149
|
+
"""!
|
|
150
|
+
@brief Parse a single `--enable-static-check` SPEC string into a (lang, config_dict) pair.
|
|
151
|
+
@param spec Raw SPEC string in the format `LANG=MODULE[,CMD[,PARAM...]]`.
|
|
152
|
+
@return Tuple `(canonical_lang, config_dict)` where `config_dict` contains `"module"` and
|
|
153
|
+
optionally `"cmd"` (Command only) and `"params"` (non-empty list only).
|
|
154
|
+
@throws ReqError If `=` separator is absent, language is unknown, or module is unknown.
|
|
155
|
+
@details
|
|
156
|
+
Parse steps:
|
|
157
|
+
1. Split on the first `=`; left side is LANG token, right side is `MODULE[,...]`.
|
|
158
|
+
2. Normalize LANG via `STATIC_CHECK_LANG_CANONICAL` (case-insensitive).
|
|
159
|
+
3. Parse right side as comma-separated tokens; first token is MODULE
|
|
160
|
+
(case-insensitive, validated against `_CANONICAL_MODULES`).
|
|
161
|
+
4. For Command: next token is `cmd` (mandatory); all subsequent tokens are `params`.
|
|
162
|
+
5. For all other modules: all tokens after MODULE are `params`.
|
|
163
|
+
6. `params` key is omitted when the list is empty.
|
|
164
|
+
7. `cmd` key is omitted for non-Command modules.
|
|
165
|
+
8. Surrounding quote delimiters (`'` or `"`) are stripped from parsed tokens.
|
|
166
|
+
Note: PARAM values containing `,` must be wrapped with `'` or `"` in SPEC.
|
|
167
|
+
@see SRS-260, SRS-248, SRS-249, SRS-250
|
|
168
|
+
"""
|
|
169
|
+
if "=" not in spec:
|
|
170
|
+
raise ReqError(
|
|
171
|
+
"Error: --enable-static-check requires format LANG=MODULE[,CMD[,PARAM...]]; "
|
|
172
|
+
"missing '=' separator.",
|
|
173
|
+
1,
|
|
174
|
+
)
|
|
175
|
+
lang_raw, rest = spec.split("=", 1)
|
|
176
|
+
lang_key = lang_raw.strip().lower()
|
|
177
|
+
if lang_key not in STATIC_CHECK_LANG_CANONICAL:
|
|
178
|
+
valid = ", ".join(sorted({v for v in STATIC_CHECK_LANG_CANONICAL.values()}, key=str.lower))
|
|
179
|
+
raise ReqError(
|
|
180
|
+
f"Error: unknown language '{lang_raw.strip()}' in --enable-static-check. "
|
|
181
|
+
f"Valid language names (case-insensitive): {valid}",
|
|
182
|
+
1,
|
|
183
|
+
)
|
|
184
|
+
canonical_lang = STATIC_CHECK_LANG_CANONICAL[lang_key]
|
|
185
|
+
|
|
186
|
+
parts = _split_csv_like_tokens(rest)
|
|
187
|
+
if not parts or not parts[0].strip():
|
|
188
|
+
raise ReqError(
|
|
189
|
+
"Error: --enable-static-check requires MODULE after '='. "
|
|
190
|
+
"Valid modules (case-insensitive): Dummy, Pylance, Ruff, Command",
|
|
191
|
+
1,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
module_raw = parts[0].strip()
|
|
195
|
+
module_key = module_raw.lower()
|
|
196
|
+
if module_key not in _CANONICAL_MODULES:
|
|
197
|
+
raise ReqError(
|
|
198
|
+
f"Error: unknown module '{module_raw}' in --enable-static-check. "
|
|
199
|
+
"Valid modules (case-insensitive): Dummy, Pylance, Ruff, Command",
|
|
200
|
+
1,
|
|
201
|
+
)
|
|
202
|
+
module = _CANONICAL_MODULES[module_key]
|
|
203
|
+
remaining = [p.strip() for p in parts[1:]]
|
|
204
|
+
|
|
205
|
+
config: dict = {"module": module}
|
|
206
|
+
if module == "Command":
|
|
207
|
+
if not remaining or not remaining[0]:
|
|
208
|
+
raise ReqError(
|
|
209
|
+
"Error: Command module requires a cmd argument in --enable-static-check. "
|
|
210
|
+
"Format: LANG=Command,CMD[,PARAM...]",
|
|
211
|
+
1,
|
|
212
|
+
)
|
|
213
|
+
config["cmd"] = remaining[0]
|
|
214
|
+
params = [p for p in remaining[1:] if p]
|
|
215
|
+
if params:
|
|
216
|
+
config["params"] = params
|
|
217
|
+
else:
|
|
218
|
+
params = [p for p in remaining if p]
|
|
219
|
+
if params:
|
|
220
|
+
config["params"] = params
|
|
221
|
+
|
|
222
|
+
return canonical_lang, config
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# ---------------------------------------------------------------------------
|
|
226
|
+
# Per-file dispatch helper (SRS-261)
|
|
227
|
+
# ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
def dispatch_static_check_for_file(
|
|
230
|
+
filepath: str,
|
|
231
|
+
lang_config: dict,
|
|
232
|
+
*,
|
|
233
|
+
fail_only: bool = False,
|
|
234
|
+
project_base: Optional[Path] = None,
|
|
235
|
+
) -> int:
|
|
236
|
+
"""!
|
|
237
|
+
@brief Dispatch static-check for a single file based on a language config dict.
|
|
238
|
+
@param filepath Absolute path of the file to analyse.
|
|
239
|
+
@param lang_config Dict with keys `"module"` (str), optional `"cmd"` (str, Command only),
|
|
240
|
+
optional `"params"` (list[str]).
|
|
241
|
+
@param fail_only When True, suppress all stdout output for passing checks (SRS-253, SRS-256).
|
|
242
|
+
@param project_base Absolute project root used for checker runtime context.
|
|
243
|
+
@return Exit code: 0 on pass, 1 on fail.
|
|
244
|
+
@throws ReqError If module is unknown, or Command module is missing `"cmd"`.
|
|
245
|
+
@details
|
|
246
|
+
Module dispatch table:
|
|
247
|
+
- `"Dummy"` (case-insensitive) -> `StaticCheckBase`
|
|
248
|
+
- `"Pylance"` -> `StaticCheckPylance`
|
|
249
|
+
- `"Ruff"` -> `StaticCheckRuff`
|
|
250
|
+
- `"Command"` -> `StaticCheckCommand` (requires `"cmd"` key)
|
|
251
|
+
Instantiates the checker with `inputs=[filepath]`, `extra_args=params`, and `fail_only`.
|
|
252
|
+
For Pylance, forwards `project_base` runtime context.
|
|
253
|
+
Delegates actual check to `checker.run()`.
|
|
254
|
+
@see SRS-261, SRS-253, SRS-256, SRS-341
|
|
255
|
+
"""
|
|
256
|
+
module = lang_config.get("module", "")
|
|
257
|
+
params: List[str] = lang_config.get("params", [])
|
|
258
|
+
cmd: Optional[str] = lang_config.get("cmd")
|
|
259
|
+
|
|
260
|
+
module_key = module.lower()
|
|
261
|
+
checker: StaticCheckBase
|
|
262
|
+
if module_key == "dummy":
|
|
263
|
+
checker = StaticCheckBase(inputs=[filepath], extra_args=params, fail_only=fail_only)
|
|
264
|
+
elif module_key == "pylance":
|
|
265
|
+
checker = StaticCheckPylance(
|
|
266
|
+
inputs=[filepath],
|
|
267
|
+
extra_args=params,
|
|
268
|
+
fail_only=fail_only,
|
|
269
|
+
project_base=project_base,
|
|
270
|
+
)
|
|
271
|
+
elif module_key == "ruff":
|
|
272
|
+
checker = StaticCheckRuff(inputs=[filepath], extra_args=params, fail_only=fail_only)
|
|
273
|
+
elif module_key == "command":
|
|
274
|
+
if not cmd:
|
|
275
|
+
raise ReqError(
|
|
276
|
+
f"Error: Command module requires 'cmd' in static-check config for '{filepath}'.",
|
|
277
|
+
1,
|
|
278
|
+
)
|
|
279
|
+
checker = StaticCheckCommand(cmd=cmd, inputs=[filepath], extra_args=params, fail_only=fail_only)
|
|
280
|
+
else:
|
|
281
|
+
raise ReqError(
|
|
282
|
+
f"Error: unknown static-check module '{module}'. "
|
|
283
|
+
"Valid modules: Dummy, Pylance, Ruff, Command",
|
|
284
|
+
1,
|
|
285
|
+
)
|
|
286
|
+
return checker.run()
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# ---------------------------------------------------------------------------
|
|
290
|
+
# File resolution helpers
|
|
291
|
+
# ---------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
def _resolve_files(inputs: Sequence[str]) -> List[str]:
|
|
294
|
+
"""!
|
|
295
|
+
@brief Resolve a mixed list of paths, glob patterns, and directories into regular files.
|
|
296
|
+
@param inputs Sequence of raw path strings (file, directory, or glob pattern).
|
|
297
|
+
@return Sorted deduplicated list of resolved absolute file paths (regular files only).
|
|
298
|
+
@details
|
|
299
|
+
Resolution order per element:
|
|
300
|
+
1. If the element contains a glob wildcard character (`*`, `?`, `[`) expand via
|
|
301
|
+
`glob.glob(entry, recursive=True)`, enabling full `**` recursive expansion
|
|
302
|
+
(e.g., `src/**/*.py` matches all `.py` files under `src/` at any depth).
|
|
303
|
+
2. If the element is an existing directory, iterate direct children only (flat traversal).
|
|
304
|
+
3. Otherwise treat as a literal file path; include if it is a regular file.
|
|
305
|
+
Symlinks to regular files are included. Non-existent paths that do not match a glob produce
|
|
306
|
+
a warning on stderr and are skipped.
|
|
307
|
+
"""
|
|
308
|
+
resolved: dict[str, None] = {} # ordered set via dict
|
|
309
|
+
|
|
310
|
+
for entry in inputs:
|
|
311
|
+
if any(c in entry for c in ("*", "?", "[")):
|
|
312
|
+
# Glob pattern; recursive=True enables ** expansion (SRS-245)
|
|
313
|
+
matches = glob.glob(entry, recursive=True)
|
|
314
|
+
for m in sorted(matches):
|
|
315
|
+
p = Path(m)
|
|
316
|
+
if p.is_file():
|
|
317
|
+
resolved[str(p.resolve())] = None
|
|
318
|
+
else:
|
|
319
|
+
p = Path(entry)
|
|
320
|
+
if p.is_dir():
|
|
321
|
+
# Direct children only; recursive traversal is expressed via ** glob patterns
|
|
322
|
+
for child in sorted(p.iterdir()):
|
|
323
|
+
if child.is_file():
|
|
324
|
+
resolved[str(child.resolve())] = None
|
|
325
|
+
elif p.is_file():
|
|
326
|
+
resolved[str(p.resolve())] = None
|
|
327
|
+
else:
|
|
328
|
+
print(
|
|
329
|
+
f" Warning: skipping (not found or not a file): {entry}",
|
|
330
|
+
file=sys.stderr,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
return list(resolved.keys())
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
# ---------------------------------------------------------------------------
|
|
337
|
+
# Base class
|
|
338
|
+
# ---------------------------------------------------------------------------
|
|
339
|
+
|
|
340
|
+
class StaticCheckBase:
|
|
341
|
+
"""!
|
|
342
|
+
@brief Dummy static-check class; base of the static analysis class hierarchy.
|
|
343
|
+
@details Iterates over resolved input files and emits a per-file header line plus `Result: OK`.
|
|
344
|
+
Subclasses override `_check_file` to provide tool-specific logic.
|
|
345
|
+
File resolution is delegated to `_resolve_files`.
|
|
346
|
+
When `fail_only` is True, passing files produce no output (SRS-241, SRS-253, SRS-256).
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
#: Tool label used in the header line. Subclasses override this.
|
|
350
|
+
LABEL: str = "Dummy"
|
|
351
|
+
|
|
352
|
+
def __init__(
|
|
353
|
+
self,
|
|
354
|
+
inputs: Sequence[str],
|
|
355
|
+
extra_args: Optional[Sequence[str]] = None,
|
|
356
|
+
*,
|
|
357
|
+
fail_only: bool = False,
|
|
358
|
+
) -> None:
|
|
359
|
+
"""!
|
|
360
|
+
@brief Initialize the static checker with resolved inputs and options.
|
|
361
|
+
@param inputs Raw path/pattern/directory entries from CLI.
|
|
362
|
+
@param extra_args Additional CLI arguments forwarded to the external tool (may be None).
|
|
363
|
+
@param fail_only When True, suppress all stdout output for passing checks (SRS-241).
|
|
364
|
+
@details Resolves `inputs` immediately into `self._files` via `_resolve_files`.
|
|
365
|
+
Recursive traversal is expressed via `**` glob patterns in `inputs` (e.g., `src/**/*.py`);
|
|
366
|
+
no separate recursive flag exists (SRS-240, SRS-245).
|
|
367
|
+
@return {None} Function return value.
|
|
368
|
+
"""
|
|
369
|
+
self._extra_args: List[str] = list(extra_args) if extra_args else []
|
|
370
|
+
self._fail_only: bool = fail_only
|
|
371
|
+
self._files = _resolve_files(inputs)
|
|
372
|
+
|
|
373
|
+
# ------------------------------------------------------------------
|
|
374
|
+
# Public interface
|
|
375
|
+
# ------------------------------------------------------------------
|
|
376
|
+
|
|
377
|
+
def is_available(self) -> tuple[bool, str]:
|
|
378
|
+
"""!
|
|
379
|
+
@brief Probe checker availability before execution.
|
|
380
|
+
@return {tuple[bool, str]} Tuple `(available, reason)`; `(True, "")` when available.
|
|
381
|
+
@details Base implementation (Dummy): always available. Subclasses override to
|
|
382
|
+
verify tool module or binary availability (SRS-427).
|
|
383
|
+
@satisfies SRS-427
|
|
384
|
+
"""
|
|
385
|
+
return True, ""
|
|
386
|
+
|
|
387
|
+
def run(self) -> int:
|
|
388
|
+
"""!
|
|
389
|
+
@brief Execute the static check for all resolved files.
|
|
390
|
+
@return Exit code: 0 if all files pass (or file list is empty), 1 if any file fails.
|
|
391
|
+
@details
|
|
392
|
+
If the resolved file list is empty a warning is printed to stderr and 0 is returned.
|
|
393
|
+
Before any file is checked the SRS-427 pre-flight availability probe runs; when the
|
|
394
|
+
checker is unavailable every file emits header, `Result: FAIL`, English evidence
|
|
395
|
+
identifying the missing checker or binary, and one trailing blank line in both
|
|
396
|
+
`fail_only` modes, the overall exit code is 1, and no exception is raised (SRS-428).
|
|
397
|
+
For each file `_check_file` is called; the overall return code is the maximum of all
|
|
398
|
+
per-file return codes (0 = all OK, 1 = at least one FAIL).
|
|
399
|
+
When `fail_only` is True, trailing blank separator lines are emitted only for failing files.
|
|
400
|
+
@satisfies SRS-427, SRS-428
|
|
401
|
+
"""
|
|
402
|
+
if not self._files:
|
|
403
|
+
print(
|
|
404
|
+
" Warning: no files resolved for static check.",
|
|
405
|
+
file=sys.stderr,
|
|
406
|
+
)
|
|
407
|
+
return 0
|
|
408
|
+
|
|
409
|
+
available, reason = self.is_available()
|
|
410
|
+
if not available:
|
|
411
|
+
for filepath in self._files:
|
|
412
|
+
self._emit_line(self._header_line(filepath))
|
|
413
|
+
self._emit_line("Result: FAIL")
|
|
414
|
+
self._emit_line("Evidence:")
|
|
415
|
+
self._emit_line(
|
|
416
|
+
f"Error: static checker {self.LABEL} is not available: {reason}"
|
|
417
|
+
)
|
|
418
|
+
self._emit_line("")
|
|
419
|
+
return 1
|
|
420
|
+
|
|
421
|
+
overall = 0
|
|
422
|
+
for filepath in self._files:
|
|
423
|
+
rc = self._check_file(filepath)
|
|
424
|
+
if rc != 0:
|
|
425
|
+
overall = 1
|
|
426
|
+
if not self._fail_only or rc != 0:
|
|
427
|
+
self._emit_line("")
|
|
428
|
+
return overall
|
|
429
|
+
|
|
430
|
+
# ------------------------------------------------------------------
|
|
431
|
+
# Internal helpers
|
|
432
|
+
# ------------------------------------------------------------------
|
|
433
|
+
|
|
434
|
+
def _header_line(self, filepath: str) -> str:
|
|
435
|
+
"""!
|
|
436
|
+
@brief Build the per-file header line for output.
|
|
437
|
+
@param filepath Absolute path of the file being checked.
|
|
438
|
+
@return Formatted header string including label, filename, and extra args.
|
|
439
|
+
@details Format: `# Static-Check(<LABEL>): <filepath> [<extra_args>]`.
|
|
440
|
+
When `extra_args` is empty the bracket section is omitted.
|
|
441
|
+
"""
|
|
442
|
+
opts = f" [{' '.join(self._extra_args)}]" if self._extra_args else ""
|
|
443
|
+
return f"# Static-Check({self.LABEL}): {filepath}{opts}"
|
|
444
|
+
|
|
445
|
+
def _check_file(self, filepath: str) -> int:
|
|
446
|
+
"""!
|
|
447
|
+
@brief Perform the static analysis for a single file.
|
|
448
|
+
@param filepath Absolute path of the file to check.
|
|
449
|
+
@return 0 on pass, non-zero on failure.
|
|
450
|
+
@details Base implementation (Dummy): always passes.
|
|
451
|
+
When `fail_only` is False, prints the header and `Result: OK`.
|
|
452
|
+
When `fail_only` is True, produces no output (SRS-241).
|
|
453
|
+
Subclasses override this method to invoke external tools.
|
|
454
|
+
"""
|
|
455
|
+
if not self._fail_only:
|
|
456
|
+
self._emit_line(self._header_line(filepath))
|
|
457
|
+
self._emit_line("Result: OK")
|
|
458
|
+
return 0
|
|
459
|
+
|
|
460
|
+
def _emit_line(self, line: str) -> None:
|
|
461
|
+
"""!
|
|
462
|
+
@brief Emit one markdown output line.
|
|
463
|
+
@param line Line content to emit on stdout.
|
|
464
|
+
@details Emits `line` followed by a newline.
|
|
465
|
+
@return {None} Function return value.
|
|
466
|
+
"""
|
|
467
|
+
print(line)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
# ---------------------------------------------------------------------------
|
|
471
|
+
# Pylance (pyright) checker
|
|
472
|
+
# ---------------------------------------------------------------------------
|
|
473
|
+
|
|
474
|
+
class StaticCheckPylance(StaticCheckBase):
|
|
475
|
+
"""!
|
|
476
|
+
@brief Pylance static-check class; runs pyright on each resolved file via `sys.executable -m pyright`.
|
|
477
|
+
@details Derived from `StaticCheckBase`; overrides `_check_file` to invoke `pyright`
|
|
478
|
+
via `[sys.executable, '-m', 'pyright']` subprocess using the active runtime interpreter,
|
|
479
|
+
without requiring external PATH availability, and parse its exit code.
|
|
480
|
+
Header label: `Pylance`.
|
|
481
|
+
Evidence block is emitted on failure by concatenating stdout and stderr from pyright.
|
|
482
|
+
@see StaticCheckBase
|
|
483
|
+
@satisfies SRS-242, SRS-339, SRS-341
|
|
484
|
+
"""
|
|
485
|
+
|
|
486
|
+
LABEL = "Pylance"
|
|
487
|
+
|
|
488
|
+
def is_available(self) -> tuple[bool, str]:
|
|
489
|
+
"""!
|
|
490
|
+
@brief Probe pyright module availability in the active interpreter.
|
|
491
|
+
@return {tuple[bool, str]} `(False, reason)` when `importlib.util.find_spec('pyright')`
|
|
492
|
+
is `None`; `(True, "")` otherwise.
|
|
493
|
+
@details Availability matches the `[sys.executable, '-m', 'pyright']` dispatch exactly:
|
|
494
|
+
the module must be importable by the running interpreter (SRS-427).
|
|
495
|
+
@satisfies SRS-427
|
|
496
|
+
"""
|
|
497
|
+
if importlib.util.find_spec("pyright") is None:
|
|
498
|
+
return (
|
|
499
|
+
False,
|
|
500
|
+
f"module 'pyright' is not installed for the active interpreter '{sys.executable}'",
|
|
501
|
+
)
|
|
502
|
+
return True, ""
|
|
503
|
+
|
|
504
|
+
def __init__(
|
|
505
|
+
self,
|
|
506
|
+
inputs: Sequence[str],
|
|
507
|
+
extra_args: Optional[Sequence[str]] = None,
|
|
508
|
+
*,
|
|
509
|
+
fail_only: bool = False,
|
|
510
|
+
project_base: Optional[Path] = None,
|
|
511
|
+
) -> None:
|
|
512
|
+
"""!
|
|
513
|
+
@brief Initialize Pylance checker with runtime context.
|
|
514
|
+
@param inputs Raw path/pattern/directory entries from CLI.
|
|
515
|
+
@param extra_args Additional CLI arguments forwarded to pyright.
|
|
516
|
+
@param fail_only When True, suppress all stdout output for passing checks.
|
|
517
|
+
@param project_base Absolute project root available as runtime context.
|
|
518
|
+
@return {None} Function return value.
|
|
519
|
+
@details Stores runtime context for uv-first pyright invocation. No `.venv` probing is used.
|
|
520
|
+
@satisfies SRS-242, SRS-339, SRS-341
|
|
521
|
+
"""
|
|
522
|
+
super().__init__(inputs=inputs, extra_args=extra_args, fail_only=fail_only)
|
|
523
|
+
self._project_base = project_base.resolve() if project_base is not None else None
|
|
524
|
+
|
|
525
|
+
def _check_file(self, filepath: str) -> int:
|
|
526
|
+
"""!
|
|
527
|
+
@brief Run pyright on `filepath` via `sys.executable -m pyright` and emit OK or FAIL with evidence.
|
|
528
|
+
@param filepath Absolute path of the file to analyse with pyright.
|
|
529
|
+
@return 0 when pyright exits 0, 1 otherwise.
|
|
530
|
+
@details
|
|
531
|
+
Invokes `[sys.executable, '-m', 'pyright', '--pythonpath', sys.executable,
|
|
532
|
+
<filepath>, <extra_args>...]` to use the active runtime interpreter without requiring
|
|
533
|
+
external PATH availability or project `.venv` detection.
|
|
534
|
+
No `--extra-path` argument is passed.
|
|
535
|
+
Captures combined stdout+stderr.
|
|
536
|
+
When `fail_only` is False: prints header, then `Result: OK` or `Result: FAIL` with evidence.
|
|
537
|
+
When `fail_only` is True: on pass produces no output; on fail emits header, FAIL, evidence (SRS-242).
|
|
538
|
+
@exception ReqError Not raised; subprocess errors are surfaced as FAIL evidence.
|
|
539
|
+
@satisfies SRS-242, SRS-339, SRS-341
|
|
540
|
+
"""
|
|
541
|
+
cmd = [
|
|
542
|
+
sys.executable,
|
|
543
|
+
"-m",
|
|
544
|
+
"pyright",
|
|
545
|
+
"--pythonpath",
|
|
546
|
+
sys.executable,
|
|
547
|
+
filepath,
|
|
548
|
+
*self._extra_args,
|
|
549
|
+
]
|
|
550
|
+
try:
|
|
551
|
+
result = subprocess.run(
|
|
552
|
+
cmd,
|
|
553
|
+
capture_output=True,
|
|
554
|
+
text=True,
|
|
555
|
+
)
|
|
556
|
+
except FileNotFoundError:
|
|
557
|
+
self._emit_line(self._header_line(filepath))
|
|
558
|
+
self._emit_line("Result: FAIL")
|
|
559
|
+
self._emit_line("Evidence:")
|
|
560
|
+
self._emit_line(" pyright module not available via sys.executable")
|
|
561
|
+
return 1
|
|
562
|
+
|
|
563
|
+
if result.returncode == 0:
|
|
564
|
+
if not self._fail_only:
|
|
565
|
+
self._emit_line(self._header_line(filepath))
|
|
566
|
+
self._emit_line("Result: OK")
|
|
567
|
+
return 0
|
|
568
|
+
else:
|
|
569
|
+
self._emit_line(self._header_line(filepath))
|
|
570
|
+
self._emit_line("Result: FAIL")
|
|
571
|
+
self._emit_line("Evidence:")
|
|
572
|
+
evidence = (result.stdout or "") + (result.stderr or "")
|
|
573
|
+
self._emit_line(evidence.rstrip())
|
|
574
|
+
return 1
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
# ---------------------------------------------------------------------------
|
|
578
|
+
# Ruff checker
|
|
579
|
+
# ---------------------------------------------------------------------------
|
|
580
|
+
|
|
581
|
+
class StaticCheckRuff(StaticCheckBase):
|
|
582
|
+
"""!
|
|
583
|
+
@brief Ruff static-check class; runs `ruff check` on each resolved file via `sys.executable -m ruff`.
|
|
584
|
+
@details Derived from `StaticCheckBase`; overrides `_check_file` to invoke `ruff check`
|
|
585
|
+
via `[sys.executable, '-m', 'ruff', 'check']` subprocess using the package-installed ruff module,
|
|
586
|
+
without requiring external PATH availability, and parse its exit code.
|
|
587
|
+
Header label: `Ruff`.
|
|
588
|
+
Evidence block is emitted on failure by concatenating stdout and stderr from ruff.
|
|
589
|
+
@see StaticCheckBase
|
|
590
|
+
@satisfies SRS-243, SRS-339
|
|
591
|
+
"""
|
|
592
|
+
|
|
593
|
+
LABEL = "Ruff"
|
|
594
|
+
|
|
595
|
+
def is_available(self) -> tuple[bool, str]:
|
|
596
|
+
"""!
|
|
597
|
+
@brief Probe ruff module availability in the active interpreter.
|
|
598
|
+
@return {tuple[bool, str]} `(False, reason)` when `importlib.util.find_spec('ruff')`
|
|
599
|
+
is `None`; `(True, "")` otherwise.
|
|
600
|
+
@details Availability matches the `[sys.executable, '-m', 'ruff']` dispatch exactly:
|
|
601
|
+
the module must be importable by the running interpreter (SRS-427).
|
|
602
|
+
@satisfies SRS-427
|
|
603
|
+
"""
|
|
604
|
+
if importlib.util.find_spec("ruff") is None:
|
|
605
|
+
return (
|
|
606
|
+
False,
|
|
607
|
+
f"module 'ruff' is not installed for the active interpreter '{sys.executable}'",
|
|
608
|
+
)
|
|
609
|
+
return True, ""
|
|
610
|
+
|
|
611
|
+
def _check_file(self, filepath: str) -> int:
|
|
612
|
+
"""!
|
|
613
|
+
@brief Run `ruff check` on `filepath` via `sys.executable -m ruff` and emit OK or FAIL with evidence.
|
|
614
|
+
@param filepath Absolute path of the file to analyse with ruff.
|
|
615
|
+
@return 0 when ruff exits 0, 1 otherwise.
|
|
616
|
+
@details
|
|
617
|
+
Invokes `[sys.executable, '-m', 'ruff', 'check', <filepath>, <extra_args>...]` to use the
|
|
618
|
+
package-installed ruff module without requiring external PATH availability.
|
|
619
|
+
Captures combined stdout+stderr.
|
|
620
|
+
When `fail_only` is False: prints header, then `Result: OK` or `Result: FAIL` with evidence.
|
|
621
|
+
When `fail_only` is True: on pass produces no output; on fail emits header, FAIL, evidence (SRS-243).
|
|
622
|
+
@exception ReqError Not raised; subprocess errors are surfaced as FAIL evidence.
|
|
623
|
+
@satisfies SRS-243, SRS-339
|
|
624
|
+
"""
|
|
625
|
+
cmd = [sys.executable, "-m", "ruff", "check", filepath] + self._extra_args
|
|
626
|
+
try:
|
|
627
|
+
result = subprocess.run(
|
|
628
|
+
cmd,
|
|
629
|
+
capture_output=True,
|
|
630
|
+
text=True,
|
|
631
|
+
)
|
|
632
|
+
except FileNotFoundError:
|
|
633
|
+
self._emit_line(self._header_line(filepath))
|
|
634
|
+
self._emit_line("Result: FAIL")
|
|
635
|
+
self._emit_line("Evidence:")
|
|
636
|
+
self._emit_line(" ruff module not available via sys.executable")
|
|
637
|
+
return 1
|
|
638
|
+
|
|
639
|
+
if result.returncode == 0:
|
|
640
|
+
if not self._fail_only:
|
|
641
|
+
self._emit_line(self._header_line(filepath))
|
|
642
|
+
self._emit_line("Result: OK")
|
|
643
|
+
return 0
|
|
644
|
+
else:
|
|
645
|
+
self._emit_line(self._header_line(filepath))
|
|
646
|
+
self._emit_line("Result: FAIL")
|
|
647
|
+
self._emit_line("Evidence:")
|
|
648
|
+
evidence = (result.stdout or "") + (result.stderr or "")
|
|
649
|
+
self._emit_line(evidence.rstrip())
|
|
650
|
+
return 1
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
# ---------------------------------------------------------------------------
|
|
654
|
+
# Arbitrary external command checker
|
|
655
|
+
# ---------------------------------------------------------------------------
|
|
656
|
+
|
|
657
|
+
class StaticCheckCommand(StaticCheckBase):
|
|
658
|
+
"""!
|
|
659
|
+
@brief Command static-check class; runs an arbitrary external command on each resolved file.
|
|
660
|
+
@details Derived from `StaticCheckBase`; overrides `_check_file` to invoke the user-supplied
|
|
661
|
+
`cmd` as a subprocess.
|
|
662
|
+
Header label: `Command[<cmd>]`.
|
|
663
|
+
`cmd` availability is resolved at execution time by the SRS-427 pre-flight via
|
|
664
|
+
`shutil.which`; an unavailable `cmd` yields a per-file `Result: FAIL` outcome with
|
|
665
|
+
English evidence and execution continues (SRS-428); the constructor does not raise.
|
|
666
|
+
@see StaticCheckBase
|
|
667
|
+
@satisfies SRS-427, SRS-428
|
|
668
|
+
"""
|
|
669
|
+
|
|
670
|
+
def __init__(
|
|
671
|
+
self,
|
|
672
|
+
cmd: str,
|
|
673
|
+
inputs: Sequence[str],
|
|
674
|
+
extra_args: Optional[Sequence[str]] = None,
|
|
675
|
+
*,
|
|
676
|
+
fail_only: bool = False,
|
|
677
|
+
) -> None:
|
|
678
|
+
"""!
|
|
679
|
+
@brief Initialize the command checker.
|
|
680
|
+
@param cmd External command name (availability resolved by the SRS-427 pre-flight).
|
|
681
|
+
@param inputs Raw path/pattern/directory entries from CLI.
|
|
682
|
+
@param extra_args Additional CLI arguments forwarded to the external command.
|
|
683
|
+
@param fail_only When True, suppress all stdout output for passing checks (SRS-244).
|
|
684
|
+
@details Stores `cmd` and sets `LABEL` dynamically to `Command[<cmd>]`.
|
|
685
|
+
@return {None} Function return value.
|
|
686
|
+
"""
|
|
687
|
+
self._cmd = cmd
|
|
688
|
+
self.LABEL = f"Command[{cmd}]"
|
|
689
|
+
super().__init__(inputs=inputs, extra_args=extra_args, fail_only=fail_only)
|
|
690
|
+
|
|
691
|
+
def is_available(self) -> tuple[bool, str]:
|
|
692
|
+
"""!
|
|
693
|
+
@brief Probe the external command binary on PATH.
|
|
694
|
+
@return {tuple[bool, str]} `(False, reason)` when `shutil.which(self._cmd)` is `None`;
|
|
695
|
+
`(True, "")` otherwise.
|
|
696
|
+
@details Availability of the checker binary is required before any subprocess dispatch
|
|
697
|
+
(SRS-427); unavailability is reported as FAIL evidence without raising (SRS-428).
|
|
698
|
+
@satisfies SRS-427, SRS-428
|
|
699
|
+
"""
|
|
700
|
+
if shutil.which(self._cmd) is None:
|
|
701
|
+
return False, f"command '{self._cmd}' is not available on PATH"
|
|
702
|
+
return True, ""
|
|
703
|
+
|
|
704
|
+
def _check_file(self, filepath: str) -> int:
|
|
705
|
+
"""!
|
|
706
|
+
@brief Run the external command on `filepath` and emit OK or FAIL with evidence.
|
|
707
|
+
@param filepath Absolute path of the file to analyse.
|
|
708
|
+
@return 0 when the command exits 0, 1 otherwise.
|
|
709
|
+
@details
|
|
710
|
+
Invokes `<cmd> [extra_args...] <filepath>`.
|
|
711
|
+
Captures combined stdout+stderr.
|
|
712
|
+
When `fail_only` is False: prints header, then `Result: OK` or `Result: FAIL` with evidence.
|
|
713
|
+
When `fail_only` is True: on pass produces no output; on fail emits header, FAIL, evidence (SRS-244).
|
|
714
|
+
@satisfies SRS-244, SRS-253, SRS-256
|
|
715
|
+
"""
|
|
716
|
+
cmd = [self._cmd] + self._extra_args + [filepath]
|
|
717
|
+
try:
|
|
718
|
+
result = subprocess.run(
|
|
719
|
+
cmd,
|
|
720
|
+
capture_output=True,
|
|
721
|
+
text=True,
|
|
722
|
+
)
|
|
723
|
+
except FileNotFoundError:
|
|
724
|
+
self._emit_line(self._header_line(filepath))
|
|
725
|
+
self._emit_line("Result: FAIL")
|
|
726
|
+
self._emit_line("Evidence:")
|
|
727
|
+
self._emit_line(f" command '{self._cmd}' not found on PATH")
|
|
728
|
+
return 1
|
|
729
|
+
|
|
730
|
+
if result.returncode == 0:
|
|
731
|
+
if not self._fail_only:
|
|
732
|
+
self._emit_line(self._header_line(filepath))
|
|
733
|
+
self._emit_line("Result: OK")
|
|
734
|
+
return 0
|
|
735
|
+
else:
|
|
736
|
+
self._emit_line(self._header_line(filepath))
|
|
737
|
+
self._emit_line("Result: FAIL")
|
|
738
|
+
self._emit_line("Evidence:")
|
|
739
|
+
evidence = (result.stdout or "") + (result.stderr or "")
|
|
740
|
+
self._emit_line(evidence.rstrip())
|
|
741
|
+
return 1
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
# ---------------------------------------------------------------------------
|
|
745
|
+
# CLI dispatcher
|
|
746
|
+
# ---------------------------------------------------------------------------
|
|
747
|
+
|
|
748
|
+
def run_static_check(argv: Sequence[str]) -> int:
|
|
749
|
+
"""!
|
|
750
|
+
@brief Parse `--test-static-check` sub-argv and dispatch to the appropriate checker class.
|
|
751
|
+
@param argv Remaining argument tokens after `--test-static-check` (i.e. [subcommand, ...]).
|
|
752
|
+
@return Process exit code from the selected checker's `run()` method.
|
|
753
|
+
@throws ReqError If subcommand is missing, unknown, or `command` subcommand is missing `cmd`.
|
|
754
|
+
@details
|
|
755
|
+
Expected argument format:
|
|
756
|
+
- `dummy [FILES...]`
|
|
757
|
+
- `pylance [FILES...]`
|
|
758
|
+
- `ruff [FILES...]`
|
|
759
|
+
- `command <cmd> [FILES...]`
|
|
760
|
+
|
|
761
|
+
No custom `--recursive` flag is parsed; recursive traversal is expressed via `**` glob
|
|
762
|
+
patterns in `[FILES]` (e.g., `src/**/*.py`).
|
|
763
|
+
For `command`, the first token after `command` is treated as `<cmd>`.
|
|
764
|
+
All remaining tokens (after subcommand and optional cmd) are treated as FILES.
|
|
765
|
+
|
|
766
|
+
Dispatches to:
|
|
767
|
+
- `dummy` -> `StaticCheckBase`
|
|
768
|
+
- `pylance` -> `StaticCheckPylance`
|
|
769
|
+
- `ruff` -> `StaticCheckRuff`
|
|
770
|
+
- `command` -> `StaticCheckCommand`
|
|
771
|
+
"""
|
|
772
|
+
tokens = list(argv)
|
|
773
|
+
if not tokens:
|
|
774
|
+
raise ReqError(
|
|
775
|
+
"Error: --test-static-check requires a subcommand: dummy, pylance, ruff, command.",
|
|
776
|
+
1,
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
subcommand = tokens.pop(0)
|
|
780
|
+
remaining = tokens
|
|
781
|
+
|
|
782
|
+
if subcommand == "dummy":
|
|
783
|
+
checker: StaticCheckBase = StaticCheckBase(
|
|
784
|
+
inputs=remaining,
|
|
785
|
+
)
|
|
786
|
+
elif subcommand == "pylance":
|
|
787
|
+
checker = StaticCheckPylance(
|
|
788
|
+
inputs=remaining,
|
|
789
|
+
)
|
|
790
|
+
elif subcommand == "ruff":
|
|
791
|
+
checker = StaticCheckRuff(
|
|
792
|
+
inputs=remaining,
|
|
793
|
+
)
|
|
794
|
+
elif subcommand == "command":
|
|
795
|
+
if not remaining:
|
|
796
|
+
raise ReqError(
|
|
797
|
+
"Error: --test-static-check command requires a <cmd> argument.", 1
|
|
798
|
+
)
|
|
799
|
+
cmd = remaining[0]
|
|
800
|
+
remaining = remaining[1:]
|
|
801
|
+
checker = StaticCheckCommand(
|
|
802
|
+
cmd=cmd,
|
|
803
|
+
inputs=remaining,
|
|
804
|
+
)
|
|
805
|
+
else:
|
|
806
|
+
raise ReqError(
|
|
807
|
+
f"Error: unknown --test-static-check subcommand '{subcommand}'. "
|
|
808
|
+
"Valid subcommands: dummy, pylance, ruff, command.",
|
|
809
|
+
1,
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
return checker.run()
|