lintro 0.13.2__py3-none-any.whl → 0.17.2__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.
- lintro/__init__.py +1 -1
- lintro/cli.py +226 -16
- lintro/cli_utils/commands/__init__.py +8 -1
- lintro/cli_utils/commands/check.py +1 -0
- lintro/cli_utils/commands/config.py +325 -0
- lintro/cli_utils/commands/init.py +361 -0
- lintro/cli_utils/commands/list_tools.py +180 -42
- lintro/cli_utils/commands/test.py +316 -0
- lintro/cli_utils/commands/versions.py +81 -0
- lintro/config/__init__.py +62 -0
- lintro/config/config_loader.py +420 -0
- lintro/config/lintro_config.py +189 -0
- lintro/config/tool_config_generator.py +403 -0
- lintro/enums/tool_name.py +2 -0
- lintro/enums/tool_type.py +2 -0
- lintro/formatters/tools/__init__.py +12 -0
- lintro/formatters/tools/eslint_formatter.py +108 -0
- lintro/formatters/tools/markdownlint_formatter.py +88 -0
- lintro/formatters/tools/pytest_formatter.py +201 -0
- lintro/parsers/__init__.py +69 -9
- lintro/parsers/bandit/__init__.py +6 -0
- lintro/parsers/bandit/bandit_issue.py +49 -0
- lintro/parsers/bandit/bandit_parser.py +99 -0
- lintro/parsers/black/black_issue.py +4 -0
- lintro/parsers/eslint/__init__.py +6 -0
- lintro/parsers/eslint/eslint_issue.py +26 -0
- lintro/parsers/eslint/eslint_parser.py +63 -0
- lintro/parsers/markdownlint/__init__.py +6 -0
- lintro/parsers/markdownlint/markdownlint_issue.py +22 -0
- lintro/parsers/markdownlint/markdownlint_parser.py +113 -0
- lintro/parsers/pytest/__init__.py +21 -0
- lintro/parsers/pytest/pytest_issue.py +28 -0
- lintro/parsers/pytest/pytest_parser.py +483 -0
- lintro/tools/__init__.py +2 -0
- lintro/tools/core/timeout_utils.py +112 -0
- lintro/tools/core/tool_base.py +255 -45
- lintro/tools/core/tool_manager.py +77 -24
- lintro/tools/core/version_requirements.py +482 -0
- lintro/tools/implementations/pytest/pytest_command_builder.py +311 -0
- lintro/tools/implementations/pytest/pytest_config.py +200 -0
- lintro/tools/implementations/pytest/pytest_error_handler.py +128 -0
- lintro/tools/implementations/pytest/pytest_executor.py +122 -0
- lintro/tools/implementations/pytest/pytest_handlers.py +375 -0
- lintro/tools/implementations/pytest/pytest_option_validators.py +212 -0
- lintro/tools/implementations/pytest/pytest_output_processor.py +408 -0
- lintro/tools/implementations/pytest/pytest_result_processor.py +113 -0
- lintro/tools/implementations/pytest/pytest_utils.py +697 -0
- lintro/tools/implementations/tool_actionlint.py +106 -16
- lintro/tools/implementations/tool_bandit.py +23 -7
- lintro/tools/implementations/tool_black.py +236 -29
- lintro/tools/implementations/tool_darglint.py +180 -21
- lintro/tools/implementations/tool_eslint.py +374 -0
- lintro/tools/implementations/tool_hadolint.py +94 -25
- lintro/tools/implementations/tool_markdownlint.py +354 -0
- lintro/tools/implementations/tool_prettier.py +313 -26
- lintro/tools/implementations/tool_pytest.py +327 -0
- lintro/tools/implementations/tool_ruff.py +247 -70
- lintro/tools/implementations/tool_yamllint.py +448 -34
- lintro/tools/tool_enum.py +6 -0
- lintro/utils/config.py +41 -18
- lintro/utils/console_logger.py +211 -25
- lintro/utils/path_utils.py +42 -0
- lintro/utils/tool_executor.py +336 -39
- lintro/utils/tool_utils.py +38 -2
- lintro/utils/unified_config.py +926 -0
- {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/METADATA +131 -29
- lintro-0.17.2.dist-info/RECORD +134 -0
- lintro-0.13.2.dist-info/RECORD +0 -96
- {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/WHEEL +0 -0
- {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/entry_points.txt +0 -0
- {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/licenses/LICENSE +0 -0
- {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
"""Tool version requirements and checking utilities.
|
|
2
|
+
|
|
3
|
+
This module centralizes version management for all lintro tools. Version requirements
|
|
4
|
+
are read from pyproject.toml to ensure consistency across the entire codebase.
|
|
5
|
+
|
|
6
|
+
## Adding a New Tool
|
|
7
|
+
|
|
8
|
+
When adding a new tool to lintro, follow these steps:
|
|
9
|
+
|
|
10
|
+
### For Bundled Python Tools (installed with lintro):
|
|
11
|
+
1. Add the tool as a dependency in pyproject.toml:
|
|
12
|
+
```toml
|
|
13
|
+
dependencies = [
|
|
14
|
+
# ... existing deps ...
|
|
15
|
+
"newtool>=1.0.0",
|
|
16
|
+
]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
2. Update get_all_tool_versions() to include the new tool's command:
|
|
20
|
+
```python
|
|
21
|
+
tool_commands = {
|
|
22
|
+
# ... existing tools ...
|
|
23
|
+
"newtool": ["newtool"], # Or ["python", "-m", "newtool"] if module-based
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
3. Add version extraction logic in _extract_version_from_output() if needed.
|
|
28
|
+
|
|
29
|
+
### For External Tools (user must install separately):
|
|
30
|
+
1. Add minimum version to [tool.lintro.versions] in pyproject.toml:
|
|
31
|
+
```toml
|
|
32
|
+
[tool.lintro.versions]
|
|
33
|
+
newtool = "1.0.0"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
2. Update get_all_tool_versions() with the tool's command.
|
|
37
|
+
|
|
38
|
+
3. Add version extraction logic in _extract_version_from_output() if needed.
|
|
39
|
+
|
|
40
|
+
### Implementation Steps:
|
|
41
|
+
1. Create tool implementation class in lintro/tools/implementations/
|
|
42
|
+
2. Add version checking in the tool's check() and fix() methods
|
|
43
|
+
3. Update ToolEnum in lintro/tools/tool_enum.py
|
|
44
|
+
4. Add tool to tool_commands dict in this file
|
|
45
|
+
5. Test with `lintro versions` command
|
|
46
|
+
|
|
47
|
+
The version system automatically reads from pyproject.toml, so Renovate and other
|
|
48
|
+
dependency management tools will keep versions up to date.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
import re
|
|
52
|
+
import subprocess # nosec B404 - used safely with shell disabled
|
|
53
|
+
import tomllib
|
|
54
|
+
from dataclasses import dataclass
|
|
55
|
+
from pathlib import Path
|
|
56
|
+
|
|
57
|
+
from loguru import logger
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _load_pyproject_config() -> dict:
|
|
61
|
+
"""Load pyproject.toml configuration.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
dict: Configuration dictionary from pyproject.toml, or empty dict if not found.
|
|
65
|
+
"""
|
|
66
|
+
pyproject_path = Path("pyproject.toml")
|
|
67
|
+
if not pyproject_path.exists():
|
|
68
|
+
logger.warning("pyproject.toml not found, using default version requirements")
|
|
69
|
+
return {}
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
with open(pyproject_path, "rb") as f:
|
|
73
|
+
return tomllib.load(f)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
logger.warning(f"Failed to load pyproject.toml: {e}")
|
|
76
|
+
return {}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _parse_version_specifier(specifier: str) -> str:
|
|
80
|
+
"""Extract minimum version from a PEP 508 version specifier.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
specifier: PEP 508 version specifier string.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
str: Minimum version string extracted from specifier.
|
|
87
|
+
|
|
88
|
+
Examples:
|
|
89
|
+
">=0.14.0" -> "0.14.0"
|
|
90
|
+
"==1.8.1" -> "1.8.1"
|
|
91
|
+
">=25.0.0,<26.0.0" -> "25.0.0"
|
|
92
|
+
"""
|
|
93
|
+
# Split on comma and take the first constraint
|
|
94
|
+
constraints = [c.strip() for c in specifier.split(",")]
|
|
95
|
+
for constraint in constraints:
|
|
96
|
+
if constraint.startswith(">=") or constraint.startswith("=="):
|
|
97
|
+
return constraint[2:]
|
|
98
|
+
# If no recognized constraint, return the specifier as-is
|
|
99
|
+
return specifier.strip()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _get_minimum_versions() -> dict[str, str]:
|
|
103
|
+
"""Get minimum version requirements for all tools from pyproject.toml.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
dict[str, str]: Dictionary mapping tool names to minimum version strings.
|
|
107
|
+
"""
|
|
108
|
+
config = _load_pyproject_config()
|
|
109
|
+
|
|
110
|
+
versions = {}
|
|
111
|
+
|
|
112
|
+
# Python tools bundled with lintro - extract from dependencies
|
|
113
|
+
python_bundled_tools = {"ruff", "black", "bandit", "yamllint", "darglint"}
|
|
114
|
+
dependencies = config.get("project", {}).get("dependencies", [])
|
|
115
|
+
|
|
116
|
+
for dep in dependencies:
|
|
117
|
+
dep = dep.strip()
|
|
118
|
+
for tool in python_bundled_tools:
|
|
119
|
+
if dep.startswith(f"{tool}>=") or dep.startswith(f"{tool}=="):
|
|
120
|
+
versions[tool] = _parse_version_specifier(dep[len(tool) :])
|
|
121
|
+
break
|
|
122
|
+
|
|
123
|
+
# Other tools - read from [tool.lintro.versions] section
|
|
124
|
+
lintro_versions = config.get("tool", {}).get("lintro", {}).get("versions", {})
|
|
125
|
+
versions.update(lintro_versions)
|
|
126
|
+
|
|
127
|
+
# Fill in any missing tools with defaults (for backward compatibility)
|
|
128
|
+
defaults = {
|
|
129
|
+
"pytest": "8.0.0",
|
|
130
|
+
"prettier": "3.7.0",
|
|
131
|
+
"eslint": "9.0.0",
|
|
132
|
+
"hadolint": "2.12.0",
|
|
133
|
+
"actionlint": "1.7.0",
|
|
134
|
+
"markdownlint": "0.16.0",
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for tool, default_version in defaults.items():
|
|
138
|
+
if tool not in versions:
|
|
139
|
+
versions[tool] = default_version
|
|
140
|
+
|
|
141
|
+
return versions
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _get_install_hints() -> dict[str, str]:
|
|
145
|
+
"""Generate installation hints based on tool type and version requirements.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
dict[str, str]: Dictionary mapping tool names to installation hint strings.
|
|
149
|
+
"""
|
|
150
|
+
versions = _get_minimum_versions()
|
|
151
|
+
hints = {}
|
|
152
|
+
|
|
153
|
+
# Python bundled tools
|
|
154
|
+
python_bundled = {"ruff", "black", "bandit", "yamllint", "darglint"}
|
|
155
|
+
for tool in python_bundled:
|
|
156
|
+
version = versions.get(tool, "latest")
|
|
157
|
+
hints[tool] = (
|
|
158
|
+
f"Install via: pip install {tool}>={version} or uv add {tool}>={version}"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Other tools
|
|
162
|
+
pytest_version = versions.get("pytest", "8.0.0")
|
|
163
|
+
hints.update(
|
|
164
|
+
{
|
|
165
|
+
"pytest": (
|
|
166
|
+
f"Install via: pip install pytest>={pytest_version} "
|
|
167
|
+
f"or uv add pytest>={pytest_version}"
|
|
168
|
+
),
|
|
169
|
+
"prettier": (
|
|
170
|
+
f"Install via: npm install --save-dev "
|
|
171
|
+
f"prettier>={versions.get('prettier', '3.7.0')}"
|
|
172
|
+
),
|
|
173
|
+
"eslint": (
|
|
174
|
+
f"Install via: npm install --save-dev "
|
|
175
|
+
f"eslint>={versions.get('eslint', '9.0.0')}"
|
|
176
|
+
),
|
|
177
|
+
"markdownlint": (
|
|
178
|
+
f"Install via: npm install --save-dev "
|
|
179
|
+
f"markdownlint-cli2>={versions.get('markdownlint', '0.16.0')}"
|
|
180
|
+
),
|
|
181
|
+
"hadolint": (
|
|
182
|
+
f"Install via: https://github.com/hadolint/hadolint/releases "
|
|
183
|
+
f"(v{versions.get('hadolint', '2.12.0')}+)"
|
|
184
|
+
),
|
|
185
|
+
"actionlint": (
|
|
186
|
+
f"Install via: https://github.com/rhysd/actionlint/releases "
|
|
187
|
+
f"(v{versions.get('actionlint', '1.7.0')}+)"
|
|
188
|
+
),
|
|
189
|
+
},
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return hints
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# Cache the loaded versions to avoid re-reading pyproject.toml repeatedly
|
|
196
|
+
_MINIMUM_VERSIONS_CACHE: dict[str, str] | None = None
|
|
197
|
+
_INSTALL_HINTS_CACHE: dict[str, str] | None = None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def get_minimum_versions() -> dict[str, str]:
|
|
201
|
+
"""Get minimum version requirements for all tools.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
dict[str, str]: Dictionary mapping tool names to minimum version strings.
|
|
205
|
+
"""
|
|
206
|
+
global _MINIMUM_VERSIONS_CACHE
|
|
207
|
+
if _MINIMUM_VERSIONS_CACHE is None:
|
|
208
|
+
_MINIMUM_VERSIONS_CACHE = _get_minimum_versions()
|
|
209
|
+
return _MINIMUM_VERSIONS_CACHE
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def get_install_hints() -> dict[str, str]:
|
|
213
|
+
"""Get installation hints for tools that don't meet requirements.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
dict[str, str]: Dictionary mapping tool names to installation hint strings.
|
|
217
|
+
"""
|
|
218
|
+
global _INSTALL_HINTS_CACHE
|
|
219
|
+
if _INSTALL_HINTS_CACHE is None:
|
|
220
|
+
_INSTALL_HINTS_CACHE = _get_install_hints()
|
|
221
|
+
return _INSTALL_HINTS_CACHE
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@dataclass
|
|
225
|
+
class ToolVersionInfo:
|
|
226
|
+
"""Information about a tool's version requirements."""
|
|
227
|
+
|
|
228
|
+
name: str
|
|
229
|
+
min_version: str
|
|
230
|
+
install_hint: str
|
|
231
|
+
current_version: str | None = None
|
|
232
|
+
version_check_passed: bool = False
|
|
233
|
+
error_message: str | None = None
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _parse_version(version_str: str) -> tuple[int, ...]:
|
|
237
|
+
"""Parse a version string into a comparable tuple.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
version_str: Version string like "1.2.3" or "0.14.0"
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
tuple[int, ...]: Comparable version tuple like (1, 2, 3)
|
|
244
|
+
"""
|
|
245
|
+
# Extract version numbers, handling pre-release suffixes
|
|
246
|
+
match = re.match(r"^(\d+(?:\.\d+)*)", version_str.strip())
|
|
247
|
+
if not match:
|
|
248
|
+
return (0,)
|
|
249
|
+
|
|
250
|
+
version_part = match.group(1)
|
|
251
|
+
return tuple(int(part) for part in version_part.split("."))
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _compare_versions(version1: str, version2: str) -> int:
|
|
255
|
+
"""Compare two version strings.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
version1: First version string
|
|
259
|
+
version2: Second version string
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
int: -1 if version1 < version2, 0 if equal, 1 if version1 > version2
|
|
263
|
+
"""
|
|
264
|
+
v1_parts = _parse_version(version1)
|
|
265
|
+
v2_parts = _parse_version(version2)
|
|
266
|
+
|
|
267
|
+
# Pad shorter version to same length
|
|
268
|
+
max_len = max(len(v1_parts), len(v2_parts))
|
|
269
|
+
v1_padded = v1_parts + (0,) * (max_len - len(v1_parts))
|
|
270
|
+
v2_padded = v2_parts + (0,) * (max_len - len(v2_parts))
|
|
271
|
+
|
|
272
|
+
if v1_padded < v2_padded:
|
|
273
|
+
return -1
|
|
274
|
+
elif v1_padded > v2_padded:
|
|
275
|
+
return 1
|
|
276
|
+
else:
|
|
277
|
+
return 0
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def check_tool_version(tool_name: str, command: list[str]) -> ToolVersionInfo:
|
|
281
|
+
"""Check if a tool meets minimum version requirements.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
tool_name: Name of the tool to check
|
|
285
|
+
command: Command list to run the tool (e.g., ["python", "-m", "ruff"])
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
ToolVersionInfo: Version check results
|
|
289
|
+
"""
|
|
290
|
+
minimum_versions = get_minimum_versions()
|
|
291
|
+
install_hints = get_install_hints()
|
|
292
|
+
|
|
293
|
+
min_version = minimum_versions.get(tool_name, "unknown")
|
|
294
|
+
install_hint = install_hints.get(
|
|
295
|
+
tool_name,
|
|
296
|
+
f"Install {tool_name} and ensure it's in PATH",
|
|
297
|
+
)
|
|
298
|
+
has_requirements = tool_name in minimum_versions
|
|
299
|
+
|
|
300
|
+
info = ToolVersionInfo(
|
|
301
|
+
name=tool_name,
|
|
302
|
+
min_version=min_version,
|
|
303
|
+
install_hint=install_hint,
|
|
304
|
+
# If no requirements, assume check passes
|
|
305
|
+
version_check_passed=not has_requirements,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
try:
|
|
309
|
+
# Run the tool with --version flag
|
|
310
|
+
version_cmd = command + ["--version"]
|
|
311
|
+
result = subprocess.run( # nosec B603 - args list, shell=False
|
|
312
|
+
version_cmd,
|
|
313
|
+
capture_output=True,
|
|
314
|
+
text=True,
|
|
315
|
+
timeout=10, # 10 second timeout
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
if result.returncode != 0:
|
|
319
|
+
info.error_message = f"Command failed: {' '.join(version_cmd)}"
|
|
320
|
+
logger.debug(
|
|
321
|
+
f"[VersionCheck] Failed to get version for {tool_name}: "
|
|
322
|
+
f"{info.error_message}",
|
|
323
|
+
)
|
|
324
|
+
return info
|
|
325
|
+
|
|
326
|
+
# Extract version from output
|
|
327
|
+
output = result.stdout + result.stderr
|
|
328
|
+
info.current_version = _extract_version_from_output(output, tool_name)
|
|
329
|
+
|
|
330
|
+
if not info.current_version:
|
|
331
|
+
info.error_message = (
|
|
332
|
+
f"Could not parse version from output: {output.strip()}"
|
|
333
|
+
)
|
|
334
|
+
logger.debug(
|
|
335
|
+
f"[VersionCheck] Failed to parse version for {tool_name}: "
|
|
336
|
+
f"{info.error_message}",
|
|
337
|
+
)
|
|
338
|
+
return info
|
|
339
|
+
|
|
340
|
+
# Compare versions
|
|
341
|
+
comparison = _compare_versions(info.current_version, min_version)
|
|
342
|
+
info.version_check_passed = comparison >= 0
|
|
343
|
+
|
|
344
|
+
if not info.version_check_passed:
|
|
345
|
+
info.error_message = (
|
|
346
|
+
f"Version {info.current_version} is below minimum requirement "
|
|
347
|
+
f"{min_version}"
|
|
348
|
+
)
|
|
349
|
+
logger.debug(
|
|
350
|
+
f"[VersionCheck] Version check failed for {tool_name}: "
|
|
351
|
+
f"{info.error_message}",
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, PermissionError) as e:
|
|
355
|
+
info.error_message = f"Failed to run version check: {e}"
|
|
356
|
+
logger.debug(f"[VersionCheck] Exception checking version for {tool_name}: {e}")
|
|
357
|
+
|
|
358
|
+
return info
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _extract_version_from_output(output: str, tool_name: str) -> str | None:
|
|
362
|
+
"""Extract version string from tool --version output.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
output: Raw output from tool --version
|
|
366
|
+
tool_name: Name of the tool (to handle tool-specific parsing)
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
Optional[str]: Extracted version string, or None if not found
|
|
370
|
+
"""
|
|
371
|
+
output = output.strip()
|
|
372
|
+
|
|
373
|
+
# Tool-specific patterns first (most reliable)
|
|
374
|
+
if tool_name == "black":
|
|
375
|
+
# black: "black, 25.9.0 (compiled: yes)"
|
|
376
|
+
match = re.search(r"black,\s+(\d+(?:\.\d+)*)", output, re.IGNORECASE)
|
|
377
|
+
if match:
|
|
378
|
+
return match.group(1)
|
|
379
|
+
|
|
380
|
+
elif tool_name == "bandit":
|
|
381
|
+
# bandit: "__main__.py 1.8.6"
|
|
382
|
+
match = re.search(r"(\d+(?:\.\d+)*)", output)
|
|
383
|
+
if match:
|
|
384
|
+
return match.group(1)
|
|
385
|
+
|
|
386
|
+
elif tool_name == "hadolint":
|
|
387
|
+
# hadolint: "Haskell Dockerfile Linter 2.14.0"
|
|
388
|
+
match = re.search(r"(\d+(?:\.\d+)*)", output)
|
|
389
|
+
if match:
|
|
390
|
+
return match.group(1)
|
|
391
|
+
|
|
392
|
+
elif tool_name == "prettier":
|
|
393
|
+
# prettier: "Prettier x.y.z" or just version
|
|
394
|
+
match = re.search(r"(\d+(?:\.\d+)*)", output)
|
|
395
|
+
if match:
|
|
396
|
+
return match.group(1)
|
|
397
|
+
|
|
398
|
+
elif tool_name == "eslint":
|
|
399
|
+
# eslint: "v9.0.0" or "9.0.0"
|
|
400
|
+
match = re.search(r"v?(\d+(?:\.\d+)*)", output)
|
|
401
|
+
if match:
|
|
402
|
+
return match.group(1)
|
|
403
|
+
|
|
404
|
+
elif tool_name == "actionlint":
|
|
405
|
+
# actionlint: "actionlint x.y.z" or just version
|
|
406
|
+
match = re.search(r"(\d+(?:\.\d+)*)", output)
|
|
407
|
+
if match:
|
|
408
|
+
return match.group(1)
|
|
409
|
+
|
|
410
|
+
elif tool_name == "darglint":
|
|
411
|
+
# darglint outputs just the version number
|
|
412
|
+
match = re.search(r"(\d+(?:\.\d+)*)", output)
|
|
413
|
+
if match:
|
|
414
|
+
return match.group(1)
|
|
415
|
+
|
|
416
|
+
elif tool_name == "markdownlint":
|
|
417
|
+
# markdownlint-cli2: "markdownlint-cli2 v0.19.1 (markdownlint v0.39.0)"
|
|
418
|
+
# Extract the cli2 version (first version number after "v")
|
|
419
|
+
match = re.search(
|
|
420
|
+
r"markdownlint-cli2\s+v(\d+(?:\.\d+)*)",
|
|
421
|
+
output,
|
|
422
|
+
re.IGNORECASE,
|
|
423
|
+
)
|
|
424
|
+
if match:
|
|
425
|
+
return match.group(1)
|
|
426
|
+
# Fallback: look for any version pattern
|
|
427
|
+
match = re.search(r"v(\d+(?:\.\d+)+)", output)
|
|
428
|
+
if match:
|
|
429
|
+
return match.group(1)
|
|
430
|
+
|
|
431
|
+
# Fallback: look for any version-like pattern
|
|
432
|
+
match = re.search(r"(\d+(?:\.\d+)+)", output)
|
|
433
|
+
if match:
|
|
434
|
+
return match.group(1)
|
|
435
|
+
|
|
436
|
+
return None
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def get_all_tool_versions() -> dict[str, ToolVersionInfo]:
|
|
440
|
+
"""Get version information for all supported tools.
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
dict[str, ToolVersionInfo]: Tool name to version info mapping
|
|
444
|
+
"""
|
|
445
|
+
# Define tool commands - this avoids circular imports
|
|
446
|
+
tool_commands = {
|
|
447
|
+
# Python bundled tools (available as scripts when installed)
|
|
448
|
+
"ruff": ["ruff"],
|
|
449
|
+
"black": ["black"],
|
|
450
|
+
"bandit": ["bandit"],
|
|
451
|
+
"yamllint": ["yamllint"],
|
|
452
|
+
"darglint": ["darglint"],
|
|
453
|
+
# Python user tools
|
|
454
|
+
"pytest": ["python", "-m", "pytest"],
|
|
455
|
+
# Node.js tools
|
|
456
|
+
"prettier": ["npx", "--yes", "prettier"],
|
|
457
|
+
"eslint": ["npx", "--yes", "eslint"],
|
|
458
|
+
"markdownlint": ["npx", "--yes", "markdownlint-cli2"],
|
|
459
|
+
# Binary tools
|
|
460
|
+
"hadolint": ["hadolint"],
|
|
461
|
+
"actionlint": ["actionlint"],
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
results = {}
|
|
465
|
+
minimum_versions = get_minimum_versions()
|
|
466
|
+
install_hints = get_install_hints()
|
|
467
|
+
|
|
468
|
+
for tool_name, command in tool_commands.items():
|
|
469
|
+
try:
|
|
470
|
+
results[tool_name] = check_tool_version(tool_name, command)
|
|
471
|
+
except Exception as e:
|
|
472
|
+
logger.debug(f"Failed to check version for {tool_name}: {e}")
|
|
473
|
+
min_version = minimum_versions.get(tool_name, "unknown")
|
|
474
|
+
install_hint = install_hints.get(tool_name, f"Install {tool_name}")
|
|
475
|
+
results[tool_name] = ToolVersionInfo(
|
|
476
|
+
name=tool_name,
|
|
477
|
+
min_version=min_version,
|
|
478
|
+
install_hint=install_hint,
|
|
479
|
+
error_message=f"Failed to check version: {e}",
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
return results
|