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
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import subprocess # nosec B404 - used safely with shell disabled
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
5
|
|
|
6
|
+
import click
|
|
6
7
|
from loguru import logger
|
|
7
8
|
|
|
8
9
|
from lintro.enums.hadolint_enums import (
|
|
@@ -216,6 +217,11 @@ class HadolintTool(BaseTool):
|
|
|
216
217
|
Returns:
|
|
217
218
|
ToolResult: ToolResult instance.
|
|
218
219
|
"""
|
|
220
|
+
# Check version requirements
|
|
221
|
+
version_result = self._verify_tool_version()
|
|
222
|
+
if version_result is not None:
|
|
223
|
+
return version_result
|
|
224
|
+
|
|
219
225
|
self._validate_paths(paths=paths)
|
|
220
226
|
if not paths:
|
|
221
227
|
return ToolResult(
|
|
@@ -240,38 +246,101 @@ class HadolintTool(BaseTool):
|
|
|
240
246
|
all_issues: list = []
|
|
241
247
|
all_success: bool = True
|
|
242
248
|
skipped_files: list[str] = []
|
|
249
|
+
execution_failures: int = 0
|
|
243
250
|
total_issues: int = 0
|
|
244
251
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
252
|
+
# Show progress bar only when processing multiple files
|
|
253
|
+
if len(dockerfile_files) >= 2:
|
|
254
|
+
with click.progressbar(
|
|
255
|
+
dockerfile_files,
|
|
256
|
+
label="Processing files",
|
|
257
|
+
bar_template="%(label)s %(info)s",
|
|
258
|
+
) as bar:
|
|
259
|
+
for file_path in bar:
|
|
260
|
+
cmd: list[str] = self._build_command() + [str(file_path)]
|
|
261
|
+
try:
|
|
262
|
+
success: bool
|
|
263
|
+
output: str
|
|
264
|
+
success, output = self._run_subprocess(cmd=cmd, timeout=timeout)
|
|
265
|
+
issues = parse_hadolint_output(output=output)
|
|
266
|
+
issues_count: int = len(issues)
|
|
267
|
+
# Tool is successful if subprocess succeeds,
|
|
268
|
+
# regardless of issues found
|
|
269
|
+
if not success:
|
|
270
|
+
all_success = False
|
|
271
|
+
total_issues += issues_count
|
|
272
|
+
# Prefer parsed issues for formatted output;
|
|
273
|
+
# keep raw for metadata
|
|
274
|
+
# Preserve output when subprocess fails even if parsing
|
|
275
|
+
# yields no issues
|
|
276
|
+
if not success or issues:
|
|
277
|
+
all_outputs.append(output)
|
|
278
|
+
if issues:
|
|
279
|
+
all_issues.extend(issues)
|
|
280
|
+
except subprocess.TimeoutExpired:
|
|
281
|
+
skipped_files.append(file_path)
|
|
282
|
+
all_success = False
|
|
283
|
+
# Count timeout as an execution failure
|
|
284
|
+
execution_failures += 1
|
|
285
|
+
except Exception as e:
|
|
286
|
+
all_outputs.append(f"Error processing {file_path}: {str(e)}")
|
|
287
|
+
all_success = False
|
|
288
|
+
# Count execution errors as failures
|
|
289
|
+
execution_failures += 1
|
|
290
|
+
else:
|
|
291
|
+
# Process without progress bar for single file or no files
|
|
292
|
+
for file_path in dockerfile_files:
|
|
293
|
+
cmd: list[str] = self._build_command() + [str(file_path)]
|
|
294
|
+
try:
|
|
295
|
+
success: bool
|
|
296
|
+
output: str
|
|
297
|
+
success, output = self._run_subprocess(cmd=cmd, timeout=timeout)
|
|
298
|
+
issues = parse_hadolint_output(output=output)
|
|
299
|
+
issues_count: int = len(issues)
|
|
300
|
+
# Tool is successful if subprocess succeeds,
|
|
301
|
+
# regardless of issues found
|
|
302
|
+
if not success:
|
|
303
|
+
all_success = False
|
|
304
|
+
total_issues += issues_count
|
|
305
|
+
# Prefer parsed issues for formatted output;
|
|
306
|
+
# keep raw for metadata
|
|
307
|
+
# Preserve output when subprocess fails even if parsing
|
|
308
|
+
# yields no issues
|
|
309
|
+
if not success or issues:
|
|
310
|
+
all_outputs.append(output)
|
|
311
|
+
if issues:
|
|
312
|
+
all_issues.extend(issues)
|
|
313
|
+
except subprocess.TimeoutExpired:
|
|
314
|
+
skipped_files.append(file_path)
|
|
315
|
+
all_success = False
|
|
316
|
+
# Count timeout as an execution failure
|
|
317
|
+
execution_failures += 1
|
|
318
|
+
except Exception as e:
|
|
319
|
+
all_outputs.append(f"Error processing {file_path}: {str(e)}")
|
|
255
320
|
all_success = False
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if issues:
|
|
259
|
-
all_outputs.append(output)
|
|
260
|
-
all_issues.extend(issues)
|
|
261
|
-
except subprocess.TimeoutExpired:
|
|
262
|
-
skipped_files.append(file_path)
|
|
263
|
-
all_success = False
|
|
264
|
-
except Exception as e:
|
|
265
|
-
all_outputs.append(f"Error processing {file_path}: {str(e)}")
|
|
266
|
-
all_success = False
|
|
321
|
+
# Count execution errors as failures
|
|
322
|
+
execution_failures += 1
|
|
267
323
|
|
|
268
324
|
output: str = "\n".join(all_outputs) if all_outputs else ""
|
|
269
|
-
if
|
|
325
|
+
if execution_failures > 0:
|
|
270
326
|
if output:
|
|
271
327
|
output += "\n\n"
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
328
|
+
if skipped_files:
|
|
329
|
+
output += (
|
|
330
|
+
f"Skipped/failed {execution_failures} file(s) due to "
|
|
331
|
+
f"execution failures (including timeouts)"
|
|
332
|
+
)
|
|
333
|
+
if timeout:
|
|
334
|
+
output += f" (timeout: {timeout}s)"
|
|
335
|
+
output += ":"
|
|
336
|
+
for file in skipped_files:
|
|
337
|
+
output += f"\n - {file}"
|
|
338
|
+
else:
|
|
339
|
+
# Execution failures but no skipped files (all were exceptions)
|
|
340
|
+
output += (
|
|
341
|
+
f"Failed to process {execution_failures} file(s) "
|
|
342
|
+
"due to execution errors"
|
|
343
|
+
)
|
|
275
344
|
|
|
276
345
|
if not output.strip():
|
|
277
346
|
output = None
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"""Markdownlint-cli2 Markdown linter integration."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess # nosec B404 - used safely with shell disabled
|
|
6
|
+
import tempfile
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
from loguru import logger
|
|
10
|
+
|
|
11
|
+
from lintro.enums.tool_type import ToolType
|
|
12
|
+
from lintro.models.core.tool import ToolConfig, ToolResult
|
|
13
|
+
from lintro.parsers.markdownlint.markdownlint_parser import parse_markdownlint_output
|
|
14
|
+
from lintro.tools.core.tool_base import BaseTool
|
|
15
|
+
from lintro.utils.config import get_central_line_length
|
|
16
|
+
from lintro.utils.tool_utils import walk_files_with_excludes
|
|
17
|
+
from lintro.utils.unified_config import DEFAULT_TOOL_PRIORITIES
|
|
18
|
+
|
|
19
|
+
# Constants
|
|
20
|
+
MARKDOWNLINT_DEFAULT_TIMEOUT: int = 30
|
|
21
|
+
# Use centralized priority from unified_config.py for consistency
|
|
22
|
+
MARKDOWNLINT_DEFAULT_PRIORITY: int = DEFAULT_TOOL_PRIORITIES.get("markdownlint", 30)
|
|
23
|
+
MARKDOWNLINT_FILE_PATTERNS: list[str] = [
|
|
24
|
+
"*.md",
|
|
25
|
+
"*.markdown",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class MarkdownlintTool(BaseTool):
|
|
31
|
+
"""Markdownlint-cli2 Markdown linter integration.
|
|
32
|
+
|
|
33
|
+
Markdownlint-cli2 is a linter for Markdown files that checks for style
|
|
34
|
+
issues and best practices.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
name: Tool name
|
|
38
|
+
description: Tool description
|
|
39
|
+
can_fix: Whether the tool can fix issues
|
|
40
|
+
config: Tool configuration
|
|
41
|
+
exclude_patterns: List of patterns to exclude
|
|
42
|
+
include_venv: Whether to include virtual environment files
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
name: str = "markdownlint"
|
|
46
|
+
description: str = "Markdown linter for style checking and best practices"
|
|
47
|
+
can_fix: bool = False
|
|
48
|
+
config: ToolConfig = field(
|
|
49
|
+
default_factory=lambda: ToolConfig(
|
|
50
|
+
priority=MARKDOWNLINT_DEFAULT_PRIORITY,
|
|
51
|
+
conflicts_with=[],
|
|
52
|
+
file_patterns=MARKDOWNLINT_FILE_PATTERNS,
|
|
53
|
+
tool_type=ToolType.LINTER,
|
|
54
|
+
options={
|
|
55
|
+
"timeout": MARKDOWNLINT_DEFAULT_TIMEOUT,
|
|
56
|
+
},
|
|
57
|
+
),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def __post_init__(self) -> None:
|
|
61
|
+
"""Initialize the tool."""
|
|
62
|
+
super().__post_init__()
|
|
63
|
+
|
|
64
|
+
def _verify_tool_version(self) -> ToolResult | None:
|
|
65
|
+
"""Verify that markdownlint-cli2 meets minimum version requirements.
|
|
66
|
+
|
|
67
|
+
Overrides base implementation to use the correct executable name.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Optional[ToolResult]: None if version check passes, or a skip result
|
|
71
|
+
if it fails
|
|
72
|
+
"""
|
|
73
|
+
from lintro.tools.core.version_requirements import check_tool_version
|
|
74
|
+
|
|
75
|
+
# Use the correct command for markdownlint-cli2
|
|
76
|
+
command = self._get_markdownlint_command()
|
|
77
|
+
version_info = check_tool_version(self.name, command)
|
|
78
|
+
|
|
79
|
+
if version_info.version_check_passed:
|
|
80
|
+
return None # Version check passed
|
|
81
|
+
|
|
82
|
+
# Version check failed - return skip result with warning
|
|
83
|
+
skip_message = (
|
|
84
|
+
f"Skipping {self.name}: {version_info.error_message}. "
|
|
85
|
+
f"Minimum required: {version_info.min_version}. "
|
|
86
|
+
f"{version_info.install_hint}"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return ToolResult(
|
|
90
|
+
name=self.name,
|
|
91
|
+
success=True, # Not an error, just skipping
|
|
92
|
+
output=skip_message,
|
|
93
|
+
issues_count=0,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def set_options(
|
|
97
|
+
self,
|
|
98
|
+
timeout: int | None = None,
|
|
99
|
+
line_length: int | None = None,
|
|
100
|
+
**kwargs,
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Set Markdownlint-specific options.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
timeout: Timeout in seconds per file (default: 30)
|
|
106
|
+
line_length: Line length for MD013 rule. If not provided, uses
|
|
107
|
+
central line_length from [tool.lintro] or falls back to Ruff's
|
|
108
|
+
line-length setting.
|
|
109
|
+
**kwargs: Other tool options
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
ValueError: If timeout is not an integer or is not positive, or
|
|
113
|
+
if line_length is not an integer or is not positive
|
|
114
|
+
"""
|
|
115
|
+
if timeout is not None:
|
|
116
|
+
if not isinstance(timeout, int):
|
|
117
|
+
raise ValueError("timeout must be an integer")
|
|
118
|
+
if timeout <= 0:
|
|
119
|
+
raise ValueError("timeout must be positive")
|
|
120
|
+
|
|
121
|
+
# Use provided line_length, or get from central config
|
|
122
|
+
if line_length is None:
|
|
123
|
+
line_length = get_central_line_length()
|
|
124
|
+
|
|
125
|
+
if line_length is not None:
|
|
126
|
+
if not isinstance(line_length, int):
|
|
127
|
+
raise ValueError("line_length must be an integer")
|
|
128
|
+
if line_length <= 0:
|
|
129
|
+
raise ValueError("line_length must be positive")
|
|
130
|
+
# Store for use in check() method
|
|
131
|
+
self.options["line_length"] = line_length
|
|
132
|
+
|
|
133
|
+
super().set_options(timeout=timeout, **kwargs)
|
|
134
|
+
|
|
135
|
+
def _get_markdownlint_command(self) -> list[str]:
|
|
136
|
+
"""Get the command to run markdownlint-cli2.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
list[str]: Command arguments for markdownlint-cli2.
|
|
140
|
+
"""
|
|
141
|
+
import shutil
|
|
142
|
+
|
|
143
|
+
# Use npx to run markdownlint-cli2 (similar to prettier)
|
|
144
|
+
if shutil.which("npx"):
|
|
145
|
+
return ["npx", "--yes", "markdownlint-cli2"]
|
|
146
|
+
# Fallback to direct executable if npx not found
|
|
147
|
+
return ["markdownlint-cli2"]
|
|
148
|
+
|
|
149
|
+
def _create_temp_markdownlint_config(
|
|
150
|
+
self,
|
|
151
|
+
line_length: int,
|
|
152
|
+
) -> str | None:
|
|
153
|
+
"""Create a temporary markdownlint-cli2 config with the specified line length.
|
|
154
|
+
|
|
155
|
+
Creates a temp file with MD013 rule configured. This avoids modifying
|
|
156
|
+
the user's project files.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
line_length: Line length to configure for MD013 rule.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Path to the temporary config file, or None if creation failed.
|
|
163
|
+
"""
|
|
164
|
+
config_wrapper: dict[str, object] = {
|
|
165
|
+
"config": {
|
|
166
|
+
"MD013": {
|
|
167
|
+
"line_length": line_length,
|
|
168
|
+
"code_blocks": False,
|
|
169
|
+
"tables": False,
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
# Create a temp file that persists until explicitly deleted
|
|
176
|
+
# Using delete=False so it survives the subprocess call
|
|
177
|
+
# markdownlint-cli2 requires config files to follow specific naming
|
|
178
|
+
# conventions - the file must end with ".markdownlint-cli2.jsonc"
|
|
179
|
+
# or be named ".markdownlint-cli2.jsonc"
|
|
180
|
+
with tempfile.NamedTemporaryFile(
|
|
181
|
+
mode="w",
|
|
182
|
+
suffix=".markdownlint-cli2.jsonc",
|
|
183
|
+
prefix="lintro-",
|
|
184
|
+
delete=False,
|
|
185
|
+
encoding="utf-8",
|
|
186
|
+
) as f:
|
|
187
|
+
json.dump(config_wrapper, f, indent=2)
|
|
188
|
+
temp_path = f.name
|
|
189
|
+
|
|
190
|
+
logger.debug(
|
|
191
|
+
f"[MarkdownlintTool] Created temp config at {temp_path} "
|
|
192
|
+
f"with line_length={line_length}",
|
|
193
|
+
)
|
|
194
|
+
return temp_path
|
|
195
|
+
|
|
196
|
+
except (PermissionError, OSError) as e:
|
|
197
|
+
logger.warning(
|
|
198
|
+
f"[MarkdownlintTool] Could not create temp config file: {e}",
|
|
199
|
+
)
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
def check(
|
|
203
|
+
self,
|
|
204
|
+
paths: list[str],
|
|
205
|
+
) -> ToolResult:
|
|
206
|
+
"""Check files with Markdownlint.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
paths: List of file or directory paths to check.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
ToolResult: Result of the check operation.
|
|
213
|
+
"""
|
|
214
|
+
# Check version requirements
|
|
215
|
+
version_result = self._verify_tool_version()
|
|
216
|
+
if version_result is not None:
|
|
217
|
+
return version_result
|
|
218
|
+
|
|
219
|
+
self._validate_paths(paths=paths)
|
|
220
|
+
if not paths:
|
|
221
|
+
return ToolResult(
|
|
222
|
+
name=self.name,
|
|
223
|
+
success=True,
|
|
224
|
+
output="No files to check.",
|
|
225
|
+
issues_count=0,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
markdown_files: list[str] = walk_files_with_excludes(
|
|
229
|
+
paths=paths,
|
|
230
|
+
file_patterns=self.config.file_patterns,
|
|
231
|
+
exclude_patterns=self.exclude_patterns,
|
|
232
|
+
include_venv=self.include_venv,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
logger.debug(
|
|
236
|
+
f"[MarkdownlintTool] Discovered {len(markdown_files)} files matching "
|
|
237
|
+
f"patterns: {self.config.file_patterns}",
|
|
238
|
+
)
|
|
239
|
+
logger.debug(
|
|
240
|
+
f"[MarkdownlintTool] Exclude patterns applied: {self.exclude_patterns}",
|
|
241
|
+
)
|
|
242
|
+
if markdown_files:
|
|
243
|
+
logger.debug(
|
|
244
|
+
f"[MarkdownlintTool] Files to check (first 10): "
|
|
245
|
+
f"{markdown_files[:10]}",
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
if not markdown_files:
|
|
249
|
+
return ToolResult(
|
|
250
|
+
name=self.name,
|
|
251
|
+
success=True,
|
|
252
|
+
output="No Markdown files found to check.",
|
|
253
|
+
issues_count=0,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Use relative paths and set cwd to the common parent
|
|
257
|
+
cwd: str | None = self.get_cwd(paths=markdown_files)
|
|
258
|
+
logger.debug(f"[MarkdownlintTool] Working directory: {cwd}")
|
|
259
|
+
rel_files: list[str] = [
|
|
260
|
+
os.path.relpath(f, cwd) if cwd else f for f in markdown_files
|
|
261
|
+
]
|
|
262
|
+
|
|
263
|
+
# Build command
|
|
264
|
+
cmd: list[str] = self._get_markdownlint_command()
|
|
265
|
+
|
|
266
|
+
# Track temp config for cleanup
|
|
267
|
+
temp_config_path: str | None = None
|
|
268
|
+
|
|
269
|
+
# Try Lintro config injection first
|
|
270
|
+
config_args = self._build_config_args()
|
|
271
|
+
if config_args:
|
|
272
|
+
cmd.extend(config_args)
|
|
273
|
+
logger.debug("[MarkdownlintTool] Using Lintro config injection")
|
|
274
|
+
else:
|
|
275
|
+
# Fallback: Apply line_length configuration if set
|
|
276
|
+
line_length = self.options.get("line_length")
|
|
277
|
+
if line_length:
|
|
278
|
+
temp_config_path = self._create_temp_markdownlint_config(
|
|
279
|
+
line_length=line_length,
|
|
280
|
+
)
|
|
281
|
+
if temp_config_path:
|
|
282
|
+
cmd.extend(["--config", temp_config_path])
|
|
283
|
+
|
|
284
|
+
cmd.extend(rel_files)
|
|
285
|
+
|
|
286
|
+
logger.debug(f"[MarkdownlintTool] Running: {' '.join(cmd)} (cwd={cwd})")
|
|
287
|
+
|
|
288
|
+
timeout_val: int = self.options.get("timeout", MARKDOWNLINT_DEFAULT_TIMEOUT)
|
|
289
|
+
try:
|
|
290
|
+
success, output = self._run_subprocess(
|
|
291
|
+
cmd=cmd,
|
|
292
|
+
timeout=timeout_val,
|
|
293
|
+
cwd=cwd,
|
|
294
|
+
)
|
|
295
|
+
except subprocess.TimeoutExpired:
|
|
296
|
+
timeout_msg = (
|
|
297
|
+
f"Markdownlint execution timed out ({timeout_val}s limit exceeded).\n\n"
|
|
298
|
+
"This may indicate:\n"
|
|
299
|
+
" - Large codebase taking too long to process\n"
|
|
300
|
+
" - Need to increase timeout via --tool-options markdownlint:timeout=N"
|
|
301
|
+
)
|
|
302
|
+
return ToolResult(
|
|
303
|
+
name=self.name,
|
|
304
|
+
success=False,
|
|
305
|
+
output=timeout_msg,
|
|
306
|
+
issues_count=1,
|
|
307
|
+
)
|
|
308
|
+
finally:
|
|
309
|
+
# Clean up temp config file if created
|
|
310
|
+
if temp_config_path:
|
|
311
|
+
try:
|
|
312
|
+
os.unlink(temp_config_path)
|
|
313
|
+
logger.debug(
|
|
314
|
+
"[MarkdownlintTool] Cleaned up temp config: "
|
|
315
|
+
f"{temp_config_path}",
|
|
316
|
+
)
|
|
317
|
+
except OSError as e:
|
|
318
|
+
logger.debug(
|
|
319
|
+
f"[MarkdownlintTool] Failed to clean up temp config: {e}",
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# Parse output
|
|
323
|
+
issues = parse_markdownlint_output(output=output)
|
|
324
|
+
issues_count: int = len(issues)
|
|
325
|
+
success_flag: bool = success and issues_count == 0
|
|
326
|
+
|
|
327
|
+
# Suppress output when no issues found
|
|
328
|
+
if success_flag:
|
|
329
|
+
output = None
|
|
330
|
+
|
|
331
|
+
return ToolResult(
|
|
332
|
+
name=self.name,
|
|
333
|
+
success=success_flag,
|
|
334
|
+
output=output,
|
|
335
|
+
issues_count=issues_count,
|
|
336
|
+
issues=issues,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
def fix(
|
|
340
|
+
self,
|
|
341
|
+
paths: list[str],
|
|
342
|
+
) -> ToolResult:
|
|
343
|
+
"""Markdownlint cannot fix issues, only report them.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
paths: List of file or directory paths to fix.
|
|
347
|
+
|
|
348
|
+
Raises:
|
|
349
|
+
NotImplementedError: Markdownlint is a linter only and cannot fix issues.
|
|
350
|
+
"""
|
|
351
|
+
raise NotImplementedError(
|
|
352
|
+
"Markdownlint cannot fix issues; use a Markdown formatter like Prettier "
|
|
353
|
+
"for formatting.",
|
|
354
|
+
)
|