lintro 0.6.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 +230 -14
- 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/format.py +2 -2
- 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/__init__.py +1 -0
- lintro/enums/darglint_strictness.py +10 -0
- lintro/enums/hadolint_enums.py +22 -0
- lintro/enums/tool_name.py +2 -0
- lintro/enums/tool_type.py +2 -0
- lintro/enums/yamllint_format.py +11 -0
- lintro/exceptions/__init__.py +1 -0
- lintro/formatters/__init__.py +1 -0
- lintro/formatters/core/__init__.py +1 -0
- lintro/formatters/core/output_style.py +11 -0
- lintro/formatters/core/table_descriptor.py +8 -0
- lintro/formatters/styles/csv.py +2 -0
- lintro/formatters/styles/grid.py +2 -0
- lintro/formatters/styles/html.py +2 -0
- lintro/formatters/styles/json.py +2 -0
- lintro/formatters/styles/markdown.py +2 -0
- lintro/formatters/styles/plain.py +2 -0
- lintro/formatters/tools/__init__.py +12 -0
- lintro/formatters/tools/black_formatter.py +27 -5
- lintro/formatters/tools/darglint_formatter.py +16 -1
- lintro/formatters/tools/eslint_formatter.py +108 -0
- lintro/formatters/tools/hadolint_formatter.py +13 -0
- lintro/formatters/tools/markdownlint_formatter.py +88 -0
- lintro/formatters/tools/prettier_formatter.py +15 -0
- lintro/formatters/tools/pytest_formatter.py +201 -0
- lintro/formatters/tools/ruff_formatter.py +26 -5
- lintro/formatters/tools/yamllint_formatter.py +14 -1
- lintro/models/__init__.py +1 -0
- lintro/models/core/__init__.py +1 -0
- lintro/models/core/tool_config.py +11 -7
- lintro/parsers/__init__.py +69 -9
- lintro/parsers/actionlint/actionlint_parser.py +1 -1
- 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/darglint/__init__.py +1 -0
- lintro/parsers/darglint/darglint_issue.py +11 -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/prettier/__init__.py +1 -0
- lintro/parsers/prettier/prettier_issue.py +12 -0
- lintro/parsers/prettier/prettier_parser.py +1 -1
- lintro/parsers/pytest/__init__.py +21 -0
- lintro/parsers/pytest/pytest_issue.py +28 -0
- lintro/parsers/pytest/pytest_parser.py +483 -0
- lintro/parsers/ruff/ruff_parser.py +6 -2
- lintro/parsers/yamllint/__init__.py +1 -0
- lintro/tools/__init__.py +3 -1
- lintro/tools/core/__init__.py +1 -0
- lintro/tools/core/timeout_utils.py +112 -0
- lintro/tools/core/tool_base.py +286 -50
- lintro/tools/core/tool_manager.py +77 -24
- lintro/tools/core/version_requirements.py +482 -0
- lintro/tools/implementations/__init__.py +1 -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 +34 -29
- lintro/tools/implementations/tool_black.py +236 -29
- lintro/tools/implementations/tool_darglint.py +183 -22
- 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 +317 -24
- lintro/tools/implementations/tool_pytest.py +327 -0
- lintro/tools/implementations/tool_ruff.py +278 -84
- lintro/tools/implementations/tool_yamllint.py +448 -34
- lintro/tools/tool_enum.py +8 -0
- lintro/utils/__init__.py +1 -0
- lintro/utils/ascii_normalize_cli.py +5 -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 +339 -45
- lintro/utils/tool_utils.py +51 -24
- lintro/utils/unified_config.py +926 -0
- {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/METADATA +172 -30
- lintro-0.17.2.dist-info/RECORD +134 -0
- lintro-0.6.2.dist-info/RECORD +0 -96
- {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/WHEEL +0 -0
- {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/entry_points.txt +0 -0
- {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/licenses/LICENSE +0 -0
- {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/top_level.txt +0 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import subprocess # nosec B404 - vetted use via BaseTool._run_subprocess
|
|
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.darglint_strictness import (
|
|
@@ -11,12 +12,14 @@ from lintro.enums.darglint_strictness import (
|
|
|
11
12
|
)
|
|
12
13
|
from lintro.enums.tool_type import ToolType
|
|
13
14
|
from lintro.models.core.tool import ToolConfig, ToolResult
|
|
15
|
+
from lintro.parsers.darglint.darglint_issue import DarglintIssue
|
|
14
16
|
from lintro.parsers.darglint.darglint_parser import parse_darglint_output
|
|
15
17
|
from lintro.tools.core.tool_base import BaseTool
|
|
16
18
|
from lintro.utils.tool_utils import walk_files_with_excludes
|
|
17
19
|
|
|
18
20
|
# Constants for Darglint configuration
|
|
19
|
-
|
|
21
|
+
# Increased from 30 to handle large files with complex docstrings
|
|
22
|
+
DARGLINT_DEFAULT_TIMEOUT: int = 60
|
|
20
23
|
DARGLINT_DEFAULT_PRIORITY: int = 45
|
|
21
24
|
DARGLINT_FILE_PATTERNS: list[str] = ["*.py"]
|
|
22
25
|
DARGLINT_STRICTNESS_LEVELS: tuple[str, ...] = tuple(
|
|
@@ -28,6 +31,25 @@ DARGLINT_DEFAULT_VERBOSITY: int = 2
|
|
|
28
31
|
DARGLINT_DEFAULT_STRICTNESS: str = "full"
|
|
29
32
|
|
|
30
33
|
|
|
34
|
+
@dataclass
|
|
35
|
+
class FileProcessResult:
|
|
36
|
+
"""Result of processing a single file with Darglint.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
success: Whether the file was processed successfully.
|
|
40
|
+
issues_count: Number of issues found.
|
|
41
|
+
issues: List of parsed issues.
|
|
42
|
+
output: Raw output from the tool, or None if no output.
|
|
43
|
+
timeout_issue: Timeout issue if a timeout occurred, or None.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
success: bool
|
|
47
|
+
issues_count: int
|
|
48
|
+
issues: list[DarglintIssue]
|
|
49
|
+
output: str | None
|
|
50
|
+
timeout_issue: DarglintIssue | None
|
|
51
|
+
|
|
52
|
+
|
|
31
53
|
@dataclass
|
|
32
54
|
class DarglintTool(BaseTool):
|
|
33
55
|
"""Darglint docstring linter integration.
|
|
@@ -136,7 +158,9 @@ class DarglintTool(BaseTool):
|
|
|
136
158
|
Returns:
|
|
137
159
|
list[str]: List of command arguments.
|
|
138
160
|
"""
|
|
139
|
-
|
|
161
|
+
# Prefer running via the active environment (uv run) if available,
|
|
162
|
+
# falling back to a direct executable when necessary.
|
|
163
|
+
cmd: list[str] = self._get_executable_command("darglint")
|
|
140
164
|
|
|
141
165
|
# Add configuration options
|
|
142
166
|
if self.options.get("ignore"):
|
|
@@ -155,6 +179,66 @@ class DarglintTool(BaseTool):
|
|
|
155
179
|
|
|
156
180
|
return cmd
|
|
157
181
|
|
|
182
|
+
def _process_file(
|
|
183
|
+
self,
|
|
184
|
+
file_path: str,
|
|
185
|
+
timeout: int,
|
|
186
|
+
) -> FileProcessResult:
|
|
187
|
+
"""Process a single file with Darglint.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
file_path: Path to the file to process.
|
|
191
|
+
timeout: Timeout in seconds for the subprocess execution.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
FileProcessResult: Result containing success status, issues, and output.
|
|
195
|
+
"""
|
|
196
|
+
cmd: list[str] = self._build_command() + [str(file_path)]
|
|
197
|
+
try:
|
|
198
|
+
success: bool
|
|
199
|
+
output: str
|
|
200
|
+
success, output = self._run_subprocess(cmd=cmd, timeout=timeout)
|
|
201
|
+
issues = parse_darglint_output(output=output)
|
|
202
|
+
issues_count: int = len(issues)
|
|
203
|
+
return FileProcessResult(
|
|
204
|
+
success=success and issues_count == 0,
|
|
205
|
+
issues_count=issues_count,
|
|
206
|
+
issues=issues,
|
|
207
|
+
output=output,
|
|
208
|
+
timeout_issue=None,
|
|
209
|
+
)
|
|
210
|
+
except subprocess.TimeoutExpired:
|
|
211
|
+
# Create a timeout issue object to display in the table
|
|
212
|
+
timeout_issue = DarglintIssue(
|
|
213
|
+
file=str(file_path),
|
|
214
|
+
line=0,
|
|
215
|
+
code="TIMEOUT",
|
|
216
|
+
message=(
|
|
217
|
+
f"Darglint execution timed out "
|
|
218
|
+
f"({timeout}s limit exceeded). "
|
|
219
|
+
"This may indicate:\n"
|
|
220
|
+
" - Large file taking too long to analyze\n"
|
|
221
|
+
" - Complex docstrings requiring extensive parsing\n"
|
|
222
|
+
" - Need to increase timeout via "
|
|
223
|
+
"--tool-options darglint:timeout=N"
|
|
224
|
+
),
|
|
225
|
+
)
|
|
226
|
+
return FileProcessResult(
|
|
227
|
+
success=False,
|
|
228
|
+
issues_count=0,
|
|
229
|
+
issues=[],
|
|
230
|
+
output=None,
|
|
231
|
+
timeout_issue=timeout_issue,
|
|
232
|
+
)
|
|
233
|
+
except Exception as e:
|
|
234
|
+
return FileProcessResult(
|
|
235
|
+
success=False,
|
|
236
|
+
issues_count=0,
|
|
237
|
+
issues=[],
|
|
238
|
+
output=f"Error processing {file_path}: {str(e)}",
|
|
239
|
+
timeout_issue=None,
|
|
240
|
+
)
|
|
241
|
+
|
|
158
242
|
def check(
|
|
159
243
|
self,
|
|
160
244
|
paths: list[str],
|
|
@@ -167,6 +251,11 @@ class DarglintTool(BaseTool):
|
|
|
167
251
|
Returns:
|
|
168
252
|
ToolResult: ToolResult instance.
|
|
169
253
|
"""
|
|
254
|
+
# Check version requirements
|
|
255
|
+
version_result = self._verify_tool_version()
|
|
256
|
+
if version_result is not None:
|
|
257
|
+
return version_result
|
|
258
|
+
|
|
170
259
|
self._validate_paths(paths=paths)
|
|
171
260
|
if not paths:
|
|
172
261
|
return ToolResult(
|
|
@@ -187,44 +276,116 @@ class DarglintTool(BaseTool):
|
|
|
187
276
|
|
|
188
277
|
timeout: int = self.options.get("timeout", DARGLINT_DEFAULT_TIMEOUT)
|
|
189
278
|
all_outputs: list[str] = []
|
|
279
|
+
all_issues: list[DarglintIssue] = []
|
|
190
280
|
all_success: bool = True
|
|
191
281
|
skipped_files: list[str] = []
|
|
282
|
+
execution_failures: int = 0
|
|
192
283
|
total_issues: int = 0
|
|
193
284
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
285
|
+
# Show progress bar only when processing multiple files
|
|
286
|
+
if len(python_files) >= 2:
|
|
287
|
+
with click.progressbar(
|
|
288
|
+
python_files,
|
|
289
|
+
label="Processing files",
|
|
290
|
+
bar_template="%(label)s %(info)s",
|
|
291
|
+
) as bar:
|
|
292
|
+
for file_path in bar:
|
|
293
|
+
result = self._process_file(file_path=file_path, timeout=timeout)
|
|
294
|
+
if not result.success:
|
|
295
|
+
all_success = False
|
|
296
|
+
total_issues += result.issues_count
|
|
297
|
+
if result.issues:
|
|
298
|
+
all_issues.extend(result.issues)
|
|
299
|
+
if result.output:
|
|
300
|
+
all_outputs.append(result.output)
|
|
301
|
+
if result.timeout_issue:
|
|
302
|
+
skipped_files.append(file_path)
|
|
303
|
+
execution_failures += 1
|
|
304
|
+
all_issues.append(result.timeout_issue)
|
|
305
|
+
elif (
|
|
306
|
+
not result.success
|
|
307
|
+
and not result.timeout_issue
|
|
308
|
+
and result.issues_count == 0
|
|
309
|
+
and result.output
|
|
310
|
+
and "Error" in result.output
|
|
311
|
+
):
|
|
312
|
+
# Only count as execution failure if no lint issues were found
|
|
313
|
+
# and there's an actual error (not just lint findings)
|
|
314
|
+
execution_failures += 1
|
|
315
|
+
# Create an execution error issue to keep issues consistent
|
|
316
|
+
# with issues_count
|
|
317
|
+
error_issue = DarglintIssue(
|
|
318
|
+
file=str(file_path),
|
|
319
|
+
line=0,
|
|
320
|
+
code="EXEC_ERROR",
|
|
321
|
+
message=(
|
|
322
|
+
f"Execution error: {result.output.strip()}"
|
|
323
|
+
if result.output
|
|
324
|
+
else "Execution error during darglint processing"
|
|
325
|
+
),
|
|
326
|
+
)
|
|
327
|
+
all_issues.append(error_issue)
|
|
328
|
+
else:
|
|
329
|
+
# Process without progress bar for single file or no files
|
|
330
|
+
for file_path in python_files:
|
|
331
|
+
result = self._process_file(file_path=file_path, timeout=timeout)
|
|
332
|
+
if not result.success:
|
|
203
333
|
all_success = False
|
|
204
|
-
total_issues += issues_count
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
334
|
+
total_issues += result.issues_count
|
|
335
|
+
if result.issues:
|
|
336
|
+
all_issues.extend(result.issues)
|
|
337
|
+
if result.output:
|
|
338
|
+
all_outputs.append(result.output)
|
|
339
|
+
if result.timeout_issue:
|
|
340
|
+
skipped_files.append(file_path)
|
|
341
|
+
execution_failures += 1
|
|
342
|
+
all_issues.append(result.timeout_issue)
|
|
343
|
+
elif (
|
|
344
|
+
not result.success
|
|
345
|
+
and not result.timeout_issue
|
|
346
|
+
and result.issues_count == 0
|
|
347
|
+
and result.output
|
|
348
|
+
and "Error" in result.output
|
|
349
|
+
):
|
|
350
|
+
# Only count as execution failure if no lint issues were found
|
|
351
|
+
# and there's an actual error (not just lint findings)
|
|
352
|
+
execution_failures += 1
|
|
353
|
+
# Create an execution error issue to keep issues consistent
|
|
354
|
+
# with issues_count
|
|
355
|
+
error_issue = DarglintIssue(
|
|
356
|
+
file=str(file_path),
|
|
357
|
+
line=0,
|
|
358
|
+
code="EXEC_ERROR",
|
|
359
|
+
message=(
|
|
360
|
+
f"Execution error: {result.output.strip()}"
|
|
361
|
+
if result.output
|
|
362
|
+
else "Execution error during darglint processing"
|
|
363
|
+
),
|
|
364
|
+
)
|
|
365
|
+
all_issues.append(error_issue)
|
|
213
366
|
|
|
214
367
|
output: str = "\n".join(all_outputs)
|
|
215
368
|
if skipped_files:
|
|
216
|
-
output +=
|
|
369
|
+
output += (
|
|
370
|
+
f"\n\nSkipped {len(skipped_files)} file(s) due to timeout "
|
|
371
|
+
f"({timeout}s limit exceeded):"
|
|
372
|
+
)
|
|
217
373
|
for file in skipped_files:
|
|
218
374
|
output += f"\n - {file}"
|
|
219
375
|
|
|
220
376
|
if not output:
|
|
221
377
|
output = None
|
|
222
378
|
|
|
379
|
+
# Include execution failures (timeouts/errors) in issues_count
|
|
380
|
+
# to properly reflect tool failure status
|
|
381
|
+
total_issues_with_failures = total_issues + execution_failures
|
|
382
|
+
|
|
223
383
|
return ToolResult(
|
|
224
384
|
name=self.name,
|
|
225
385
|
success=all_success,
|
|
226
386
|
output=output,
|
|
227
|
-
issues_count=
|
|
387
|
+
issues_count=total_issues_with_failures,
|
|
388
|
+
issues=all_issues,
|
|
228
389
|
)
|
|
229
390
|
|
|
230
391
|
def fix(
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"""ESLint linter integration."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess # nosec B404 - used safely with shell disabled
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
from loguru import logger
|
|
8
|
+
|
|
9
|
+
from lintro.enums.tool_type import ToolType
|
|
10
|
+
from lintro.models.core.tool import Tool, ToolConfig, ToolResult
|
|
11
|
+
from lintro.parsers.eslint.eslint_issue import EslintIssue
|
|
12
|
+
from lintro.parsers.eslint.eslint_parser import parse_eslint_output
|
|
13
|
+
from lintro.tools.core.tool_base import BaseTool
|
|
14
|
+
from lintro.utils.tool_utils import walk_files_with_excludes
|
|
15
|
+
|
|
16
|
+
# Constants for ESLint configuration
|
|
17
|
+
ESLINT_DEFAULT_TIMEOUT: int = 30
|
|
18
|
+
ESLINT_DEFAULT_PRIORITY: int = 50 # Lower priority than formatters
|
|
19
|
+
ESLINT_FILE_PATTERNS: list[str] = [
|
|
20
|
+
"*.js",
|
|
21
|
+
"*.jsx",
|
|
22
|
+
"*.ts",
|
|
23
|
+
"*.tsx",
|
|
24
|
+
"*.mjs",
|
|
25
|
+
"*.cjs",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class EslintTool(BaseTool):
|
|
31
|
+
"""ESLint linter integration.
|
|
32
|
+
|
|
33
|
+
A linter for JavaScript and TypeScript that identifies problems in code.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
name: str = "eslint"
|
|
37
|
+
description: str = (
|
|
38
|
+
"Linter for JavaScript and TypeScript that identifies and reports "
|
|
39
|
+
"on patterns in code"
|
|
40
|
+
)
|
|
41
|
+
can_fix: bool = True
|
|
42
|
+
config: ToolConfig = field(
|
|
43
|
+
default_factory=lambda: ToolConfig(
|
|
44
|
+
priority=ESLINT_DEFAULT_PRIORITY, # Lower priority than formatters
|
|
45
|
+
conflicts_with=[], # No direct conflicts
|
|
46
|
+
file_patterns=ESLINT_FILE_PATTERNS,
|
|
47
|
+
tool_type=ToolType.LINTER,
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def __post_init__(self) -> None:
|
|
52
|
+
"""Initialize eslint tool."""
|
|
53
|
+
super().__post_init__()
|
|
54
|
+
# Note: .eslintignore is handled natively by ESLint
|
|
55
|
+
# ESLint config files (.eslintrc.*, eslint.config.*) are also
|
|
56
|
+
# discovered natively
|
|
57
|
+
|
|
58
|
+
def set_options(
|
|
59
|
+
self,
|
|
60
|
+
exclude_patterns: list[str] | None = None,
|
|
61
|
+
include_venv: bool = False,
|
|
62
|
+
timeout: int | None = None,
|
|
63
|
+
verbose_fix_output: bool | None = None,
|
|
64
|
+
**kwargs,
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Set options for the core.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
exclude_patterns: List of patterns to exclude
|
|
70
|
+
include_venv: Whether to include virtual environment directories
|
|
71
|
+
timeout: Timeout in seconds per file (default: 30)
|
|
72
|
+
verbose_fix_output: If True, include raw ESLint output in fix()
|
|
73
|
+
**kwargs: Additional options (ignored for compatibility)
|
|
74
|
+
"""
|
|
75
|
+
if exclude_patterns is not None:
|
|
76
|
+
self.exclude_patterns = exclude_patterns.copy()
|
|
77
|
+
self.include_venv = include_venv
|
|
78
|
+
if timeout is not None:
|
|
79
|
+
self.options["timeout"] = timeout
|
|
80
|
+
if verbose_fix_output is not None:
|
|
81
|
+
self.options["verbose_fix_output"] = verbose_fix_output
|
|
82
|
+
|
|
83
|
+
def _create_timeout_result(
|
|
84
|
+
self,
|
|
85
|
+
timeout_val: int,
|
|
86
|
+
initial_issues: list | None = None,
|
|
87
|
+
initial_count: int = 0,
|
|
88
|
+
) -> ToolResult:
|
|
89
|
+
"""Create a ToolResult for timeout scenarios.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
timeout_val: The timeout value that was exceeded.
|
|
93
|
+
initial_issues: Optional list of issues found before timeout.
|
|
94
|
+
initial_count: Optional count of initial issues.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
ToolResult: ToolResult instance representing timeout failure.
|
|
98
|
+
"""
|
|
99
|
+
timeout_msg = (
|
|
100
|
+
f"ESLint execution timed out ({timeout_val}s limit exceeded).\n\n"
|
|
101
|
+
"This may indicate:\n"
|
|
102
|
+
" - Large codebase taking too long to process\n"
|
|
103
|
+
" - Need to increase timeout via --tool-options eslint:timeout=N"
|
|
104
|
+
)
|
|
105
|
+
timeout_issue = EslintIssue(
|
|
106
|
+
file="execution",
|
|
107
|
+
line=1,
|
|
108
|
+
code="TIMEOUT",
|
|
109
|
+
message=timeout_msg,
|
|
110
|
+
column=1,
|
|
111
|
+
severity=2,
|
|
112
|
+
fixable=False,
|
|
113
|
+
)
|
|
114
|
+
combined_issues = (initial_issues or []) + [timeout_issue]
|
|
115
|
+
return ToolResult(
|
|
116
|
+
name=self.name,
|
|
117
|
+
success=False,
|
|
118
|
+
output=timeout_msg,
|
|
119
|
+
issues_count=len(combined_issues),
|
|
120
|
+
issues=combined_issues,
|
|
121
|
+
initial_issues_count=initial_count,
|
|
122
|
+
fixed_issues_count=0,
|
|
123
|
+
remaining_issues_count=len(combined_issues),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def check(
|
|
127
|
+
self,
|
|
128
|
+
paths: list[str],
|
|
129
|
+
) -> ToolResult:
|
|
130
|
+
"""Check files with ESLint without making changes.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
paths: List of file or directory paths to check
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
ToolResult instance
|
|
137
|
+
"""
|
|
138
|
+
# Check version requirements
|
|
139
|
+
version_result = self._verify_tool_version()
|
|
140
|
+
if version_result is not None:
|
|
141
|
+
return version_result
|
|
142
|
+
|
|
143
|
+
self._validate_paths(paths=paths)
|
|
144
|
+
eslint_files: list[str] = walk_files_with_excludes(
|
|
145
|
+
paths=paths,
|
|
146
|
+
file_patterns=self.config.file_patterns,
|
|
147
|
+
exclude_patterns=self.exclude_patterns,
|
|
148
|
+
include_venv=self.include_venv,
|
|
149
|
+
)
|
|
150
|
+
logger.debug(
|
|
151
|
+
f"[EslintTool] Discovered {len(eslint_files)} files matching patterns: "
|
|
152
|
+
f"{self.config.file_patterns}",
|
|
153
|
+
)
|
|
154
|
+
logger.debug(
|
|
155
|
+
f"[EslintTool] Exclude patterns applied: {self.exclude_patterns}",
|
|
156
|
+
)
|
|
157
|
+
if eslint_files:
|
|
158
|
+
logger.debug(
|
|
159
|
+
f"[EslintTool] Files to check (first 10): " f"{eslint_files[:10]}",
|
|
160
|
+
)
|
|
161
|
+
if not eslint_files:
|
|
162
|
+
return Tool.to_result(
|
|
163
|
+
name=self.name,
|
|
164
|
+
success=True,
|
|
165
|
+
output="No files to check.",
|
|
166
|
+
issues_count=0,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Use relative paths and set cwd to the common parent
|
|
170
|
+
cwd: str = self.get_cwd(paths=eslint_files)
|
|
171
|
+
logger.debug(f"[EslintTool] Working directory: {cwd}")
|
|
172
|
+
rel_files: list[str] = [
|
|
173
|
+
os.path.relpath(f, cwd) if cwd else f for f in eslint_files
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
# Build ESLint command with JSON output format
|
|
177
|
+
cmd: list[str] = self._get_executable_command(tool_name="eslint") + [
|
|
178
|
+
"--format",
|
|
179
|
+
"json",
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
# Add Lintro config injection args if available
|
|
183
|
+
config_args = self._build_config_args()
|
|
184
|
+
if config_args:
|
|
185
|
+
cmd.extend(config_args)
|
|
186
|
+
logger.debug(
|
|
187
|
+
"[EslintTool] Using Lintro config injection",
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
cmd.extend(rel_files)
|
|
191
|
+
logger.debug(f"[EslintTool] Running: {' '.join(cmd)} (cwd={cwd})")
|
|
192
|
+
timeout_val: int = self.options.get("timeout", self._default_timeout)
|
|
193
|
+
try:
|
|
194
|
+
result = self._run_subprocess(
|
|
195
|
+
cmd=cmd,
|
|
196
|
+
timeout=timeout_val,
|
|
197
|
+
cwd=cwd,
|
|
198
|
+
)
|
|
199
|
+
except subprocess.TimeoutExpired:
|
|
200
|
+
return self._create_timeout_result(timeout_val=timeout_val)
|
|
201
|
+
output: str = result[1]
|
|
202
|
+
issues: list = parse_eslint_output(output=output)
|
|
203
|
+
issues_count: int = len(issues)
|
|
204
|
+
success: bool = issues_count == 0
|
|
205
|
+
|
|
206
|
+
# Standardize: suppress ESLint's informational output when no issues
|
|
207
|
+
# so the unified logger prints a single, consistent success line.
|
|
208
|
+
if success:
|
|
209
|
+
output = None
|
|
210
|
+
|
|
211
|
+
return ToolResult(
|
|
212
|
+
name=self.name,
|
|
213
|
+
success=success,
|
|
214
|
+
output=output,
|
|
215
|
+
issues_count=issues_count,
|
|
216
|
+
issues=issues,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def fix(
|
|
220
|
+
self,
|
|
221
|
+
paths: list[str],
|
|
222
|
+
) -> ToolResult:
|
|
223
|
+
"""Fix auto-fixable issues in files with ESLint.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
paths: List of file or directory paths to fix
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
ToolResult: Result object with counts and messages.
|
|
230
|
+
"""
|
|
231
|
+
# Check version requirements
|
|
232
|
+
version_result = self._verify_tool_version()
|
|
233
|
+
if version_result is not None:
|
|
234
|
+
return version_result
|
|
235
|
+
|
|
236
|
+
self._validate_paths(paths=paths)
|
|
237
|
+
eslint_files: list[str] = walk_files_with_excludes(
|
|
238
|
+
paths=paths,
|
|
239
|
+
file_patterns=self.config.file_patterns,
|
|
240
|
+
exclude_patterns=self.exclude_patterns,
|
|
241
|
+
include_venv=self.include_venv,
|
|
242
|
+
)
|
|
243
|
+
if not eslint_files:
|
|
244
|
+
return Tool.to_result(
|
|
245
|
+
name=self.name,
|
|
246
|
+
success=True,
|
|
247
|
+
output="No files to fix.",
|
|
248
|
+
issues_count=0,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# First, check for issues before fixing
|
|
252
|
+
cwd: str = self.get_cwd(paths=eslint_files)
|
|
253
|
+
rel_files: list[str] = [
|
|
254
|
+
os.path.relpath(f, cwd) if cwd else f for f in eslint_files
|
|
255
|
+
]
|
|
256
|
+
|
|
257
|
+
# Get Lintro config injection args if available
|
|
258
|
+
config_args = self._build_config_args()
|
|
259
|
+
|
|
260
|
+
# Check for issues first
|
|
261
|
+
check_cmd: list[str] = self._get_executable_command(tool_name="eslint") + [
|
|
262
|
+
"--format",
|
|
263
|
+
"json",
|
|
264
|
+
]
|
|
265
|
+
if config_args:
|
|
266
|
+
check_cmd.extend(config_args)
|
|
267
|
+
check_cmd.extend(rel_files)
|
|
268
|
+
logger.debug(f"[EslintTool] Checking: {' '.join(check_cmd)} (cwd={cwd})")
|
|
269
|
+
timeout_val: int = self.options.get("timeout", self._default_timeout)
|
|
270
|
+
try:
|
|
271
|
+
check_result = self._run_subprocess(
|
|
272
|
+
cmd=check_cmd,
|
|
273
|
+
timeout=timeout_val,
|
|
274
|
+
cwd=cwd,
|
|
275
|
+
)
|
|
276
|
+
except subprocess.TimeoutExpired:
|
|
277
|
+
return self._create_timeout_result(timeout_val=timeout_val)
|
|
278
|
+
check_output: str = check_result[1]
|
|
279
|
+
|
|
280
|
+
# Parse initial issues
|
|
281
|
+
initial_issues: list = parse_eslint_output(output=check_output)
|
|
282
|
+
initial_count: int = len(initial_issues)
|
|
283
|
+
|
|
284
|
+
# Now fix the issues
|
|
285
|
+
fix_cmd: list[str] = self._get_executable_command(tool_name="eslint") + [
|
|
286
|
+
"--fix",
|
|
287
|
+
]
|
|
288
|
+
if config_args:
|
|
289
|
+
fix_cmd.extend(config_args)
|
|
290
|
+
fix_cmd.extend(rel_files)
|
|
291
|
+
logger.debug(f"[EslintTool] Fixing: {' '.join(fix_cmd)} (cwd={cwd})")
|
|
292
|
+
try:
|
|
293
|
+
fix_result = self._run_subprocess(
|
|
294
|
+
cmd=fix_cmd,
|
|
295
|
+
timeout=timeout_val,
|
|
296
|
+
cwd=cwd,
|
|
297
|
+
)
|
|
298
|
+
except subprocess.TimeoutExpired:
|
|
299
|
+
return self._create_timeout_result(
|
|
300
|
+
timeout_val=timeout_val,
|
|
301
|
+
initial_issues=initial_issues,
|
|
302
|
+
initial_count=initial_count,
|
|
303
|
+
)
|
|
304
|
+
fix_output: str = fix_result[1]
|
|
305
|
+
|
|
306
|
+
# Check for remaining issues after fixing
|
|
307
|
+
try:
|
|
308
|
+
final_check_result = self._run_subprocess(
|
|
309
|
+
cmd=check_cmd,
|
|
310
|
+
timeout=timeout_val,
|
|
311
|
+
cwd=cwd,
|
|
312
|
+
)
|
|
313
|
+
except subprocess.TimeoutExpired:
|
|
314
|
+
return self._create_timeout_result(
|
|
315
|
+
timeout_val=timeout_val,
|
|
316
|
+
initial_issues=initial_issues,
|
|
317
|
+
initial_count=initial_count,
|
|
318
|
+
)
|
|
319
|
+
final_check_output: str = final_check_result[1]
|
|
320
|
+
remaining_issues: list = parse_eslint_output(output=final_check_output)
|
|
321
|
+
remaining_count: int = len(remaining_issues)
|
|
322
|
+
|
|
323
|
+
# Calculate fixed issues
|
|
324
|
+
fixed_count: int = max(0, initial_count - remaining_count)
|
|
325
|
+
|
|
326
|
+
# Build output message
|
|
327
|
+
output_lines: list[str] = []
|
|
328
|
+
if fixed_count > 0:
|
|
329
|
+
output_lines.append(f"Fixed {fixed_count} issue(s)")
|
|
330
|
+
|
|
331
|
+
if remaining_count > 0:
|
|
332
|
+
output_lines.append(
|
|
333
|
+
f"Found {remaining_count} issue(s) that cannot be auto-fixed",
|
|
334
|
+
)
|
|
335
|
+
for issue in remaining_issues[:5]:
|
|
336
|
+
output_lines.append(f" {issue.file} - {issue.message}")
|
|
337
|
+
if len(remaining_issues) > 5:
|
|
338
|
+
output_lines.append(f" ... and {len(remaining_issues) - 5} more")
|
|
339
|
+
|
|
340
|
+
# If there were no initial issues, rely on the logger's unified
|
|
341
|
+
# success line (avoid duplicate "No issues found" lines here)
|
|
342
|
+
elif remaining_count == 0 and fixed_count > 0:
|
|
343
|
+
output_lines.append("All issues were successfully auto-fixed")
|
|
344
|
+
|
|
345
|
+
# Add verbose raw fix output only when explicitly requested
|
|
346
|
+
if (
|
|
347
|
+
self.options.get("verbose_fix_output", False)
|
|
348
|
+
and fix_output
|
|
349
|
+
and fix_output.strip()
|
|
350
|
+
):
|
|
351
|
+
output_lines.append(f"Fix output:\n{fix_output}")
|
|
352
|
+
|
|
353
|
+
final_output: str | None = "\n".join(output_lines) if output_lines else None
|
|
354
|
+
|
|
355
|
+
# Success means no remaining issues
|
|
356
|
+
success: bool = remaining_count == 0
|
|
357
|
+
|
|
358
|
+
# Use only remaining issues (post-fix list) to avoid duplicates
|
|
359
|
+
# The formatter relies on metadata counters (initial_issues_count,
|
|
360
|
+
# fixed_issues_count, remaining_issues_count) for summaries
|
|
361
|
+
all_issues = remaining_issues or []
|
|
362
|
+
|
|
363
|
+
return ToolResult(
|
|
364
|
+
name=self.name,
|
|
365
|
+
success=success,
|
|
366
|
+
output=final_output,
|
|
367
|
+
# For fix operations, issues_count represents remaining for summaries
|
|
368
|
+
issues_count=remaining_count,
|
|
369
|
+
# Provide both initial (fixed) and remaining issues for display
|
|
370
|
+
issues=all_issues,
|
|
371
|
+
initial_issues_count=initial_count,
|
|
372
|
+
fixed_issues_count=fixed_count,
|
|
373
|
+
remaining_issues_count=remaining_count,
|
|
374
|
+
)
|