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
lintro/utils/tool_executor.py
CHANGED
|
@@ -11,16 +11,67 @@ from lintro.enums.group_by import GroupBy, normalize_group_by
|
|
|
11
11
|
from lintro.enums.output_format import OutputFormat, normalize_output_format
|
|
12
12
|
from lintro.tools import tool_manager
|
|
13
13
|
from lintro.tools.tool_enum import ToolEnum
|
|
14
|
-
from lintro.utils.config import
|
|
14
|
+
from lintro.utils.config import load_post_checks_config
|
|
15
15
|
from lintro.utils.console_logger import create_logger
|
|
16
16
|
from lintro.utils.output_manager import OutputManager
|
|
17
17
|
from lintro.utils.tool_utils import format_tool_output
|
|
18
|
+
from lintro.utils.unified_config import UnifiedConfigManager
|
|
18
19
|
|
|
19
20
|
# Constants
|
|
20
21
|
DEFAULT_EXIT_CODE_SUCCESS: int = 0
|
|
21
22
|
DEFAULT_EXIT_CODE_FAILURE: int = 1
|
|
22
23
|
DEFAULT_REMAINING_COUNT: int = 1
|
|
23
24
|
|
|
25
|
+
# Mapping from ToolEnum to canonical display names
|
|
26
|
+
_TOOL_DISPLAY_NAMES: dict[ToolEnum, str] = {
|
|
27
|
+
ToolEnum.BLACK: "black",
|
|
28
|
+
ToolEnum.DARGLINT: "darglint",
|
|
29
|
+
ToolEnum.HADOLINT: "hadolint",
|
|
30
|
+
ToolEnum.PRETTIER: "prettier",
|
|
31
|
+
ToolEnum.PYTEST: "pytest",
|
|
32
|
+
ToolEnum.RUFF: "ruff",
|
|
33
|
+
ToolEnum.YAMLLINT: "yamllint",
|
|
34
|
+
ToolEnum.ACTIONLINT: "actionlint",
|
|
35
|
+
ToolEnum.BANDIT: "bandit",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _get_tool_display_name(tool_enum: ToolEnum) -> str:
|
|
40
|
+
"""Get the canonical display name for a tool enum.
|
|
41
|
+
|
|
42
|
+
This function provides a consistent mapping from ToolEnum to user-friendly
|
|
43
|
+
display names. It first attempts to get the tool instance to use its canonical
|
|
44
|
+
name, but falls back to a predefined mapping if the tool cannot be instantiated.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
tool_enum: The ToolEnum instance.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
str: The canonical display name for the tool.
|
|
51
|
+
"""
|
|
52
|
+
# Try to get the tool instance to use its canonical name
|
|
53
|
+
try:
|
|
54
|
+
tool = tool_manager.get_tool(tool_enum)
|
|
55
|
+
return tool.name
|
|
56
|
+
except Exception:
|
|
57
|
+
# Fall back to predefined mapping if tool cannot be instantiated
|
|
58
|
+
return _TOOL_DISPLAY_NAMES.get(tool_enum, tool_enum.name.lower())
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _get_tool_lookup_keys(tool_enum: ToolEnum, tool_name: str) -> set[str]:
|
|
62
|
+
"""Get all possible lookup keys for a tool in tool_option_dict.
|
|
63
|
+
|
|
64
|
+
This includes the tool's display name and enum name (both lowercased).
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
tool_enum: The ToolEnum instance.
|
|
68
|
+
tool_name: The canonical display name for the tool.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
set[str]: Set of lowercase keys to check in tool_option_dict.
|
|
72
|
+
"""
|
|
73
|
+
return {tool_name.lower(), tool_enum.name.lower()}
|
|
74
|
+
|
|
24
75
|
|
|
25
76
|
def _get_tools_to_run(
|
|
26
77
|
tools: str | None,
|
|
@@ -30,7 +81,7 @@ def _get_tools_to_run(
|
|
|
30
81
|
|
|
31
82
|
Args:
|
|
32
83
|
tools: str | None: Comma-separated tool names, "all", or None.
|
|
33
|
-
action: str: "check" or "
|
|
84
|
+
action: str: "check", "fmt", or "test".
|
|
34
85
|
|
|
35
86
|
Returns:
|
|
36
87
|
list[ToolEnum]: List of ToolEnum instances to run.
|
|
@@ -38,19 +89,43 @@ def _get_tools_to_run(
|
|
|
38
89
|
Raises:
|
|
39
90
|
ValueError: If unknown tool names are provided.
|
|
40
91
|
"""
|
|
92
|
+
if action == "test":
|
|
93
|
+
# Test action only supports pytest
|
|
94
|
+
if tools and tools.lower() != "pytest":
|
|
95
|
+
raise ValueError(
|
|
96
|
+
(
|
|
97
|
+
"Only 'pytest' is supported for the test action; "
|
|
98
|
+
"run 'lintro test' without --tools or "
|
|
99
|
+
"use '--tools pytest'"
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
try:
|
|
103
|
+
return [ToolEnum["PYTEST"]]
|
|
104
|
+
except KeyError:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
"pytest tool is not available",
|
|
107
|
+
) from None
|
|
108
|
+
|
|
41
109
|
if tools == "all" or tools is None:
|
|
42
110
|
# Get all available tools for the action
|
|
43
111
|
if action == "fmt":
|
|
44
112
|
available_tools = tool_manager.get_fix_tools()
|
|
45
113
|
else: # check
|
|
46
114
|
available_tools = tool_manager.get_check_tools()
|
|
47
|
-
|
|
115
|
+
# Filter out pytest for check/fmt actions
|
|
116
|
+
return [t for t in available_tools if t.name.upper() != "PYTEST"]
|
|
48
117
|
|
|
49
118
|
# Parse specific tools
|
|
50
119
|
tool_names: list[str] = [name.strip().upper() for name in tools.split(",")]
|
|
51
120
|
tools_to_run: list[ToolEnum] = []
|
|
52
121
|
|
|
53
122
|
for name in tool_names:
|
|
123
|
+
# Reject pytest for check/fmt actions
|
|
124
|
+
if name == "PYTEST":
|
|
125
|
+
raise ValueError(
|
|
126
|
+
"pytest tool is not available for check/fmt actions. "
|
|
127
|
+
"Use 'lintro test' instead.",
|
|
128
|
+
)
|
|
54
129
|
try:
|
|
55
130
|
tool_enum = ToolEnum[name]
|
|
56
131
|
# Verify the tool supports the requested action
|
|
@@ -62,7 +137,9 @@ def _get_tools_to_run(
|
|
|
62
137
|
)
|
|
63
138
|
tools_to_run.append(tool_enum)
|
|
64
139
|
except KeyError:
|
|
65
|
-
available_names: list[str] = [
|
|
140
|
+
available_names: list[str] = [
|
|
141
|
+
e.name.lower() for e in ToolEnum if e.name.upper() != "PYTEST"
|
|
142
|
+
]
|
|
66
143
|
raise ValueError(
|
|
67
144
|
f"Unknown tool '{name.lower()}'. Available tools: {available_names}",
|
|
68
145
|
) from None
|
|
@@ -144,7 +221,7 @@ def _parse_tool_options(tool_options: str | None) -> dict[str, dict[str, object]
|
|
|
144
221
|
# Skip malformed fragment
|
|
145
222
|
continue
|
|
146
223
|
opt_name, opt_value = tool_opt.split("=", 1)
|
|
147
|
-
tool_name = tool_name.strip()
|
|
224
|
+
tool_name = tool_name.strip().lower()
|
|
148
225
|
opt_name = opt_name.strip()
|
|
149
226
|
opt_value = opt_value.strip()
|
|
150
227
|
if not tool_name or not opt_name:
|
|
@@ -156,6 +233,189 @@ def _parse_tool_options(tool_options: str | None) -> dict[str, dict[str, object]
|
|
|
156
233
|
return tool_option_dict
|
|
157
234
|
|
|
158
235
|
|
|
236
|
+
def _write_output_file(
|
|
237
|
+
*,
|
|
238
|
+
output_path: str,
|
|
239
|
+
output_format: OutputFormat,
|
|
240
|
+
all_results: list,
|
|
241
|
+
action: str,
|
|
242
|
+
total_issues: int,
|
|
243
|
+
total_fixed: int,
|
|
244
|
+
) -> None:
|
|
245
|
+
"""Write results to user-specified output file.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
output_path: str: Path to the output file.
|
|
249
|
+
output_format: OutputFormat: Format for the output.
|
|
250
|
+
all_results: list: List of ToolResult objects.
|
|
251
|
+
action: str: The action performed (check, fmt, test).
|
|
252
|
+
total_issues: int: Total number of issues found.
|
|
253
|
+
total_fixed: int: Total number of issues fixed.
|
|
254
|
+
"""
|
|
255
|
+
import csv
|
|
256
|
+
import datetime
|
|
257
|
+
import json
|
|
258
|
+
from pathlib import Path
|
|
259
|
+
|
|
260
|
+
output_file = Path(output_path)
|
|
261
|
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
262
|
+
|
|
263
|
+
if output_format == OutputFormat.JSON:
|
|
264
|
+
# Build JSON structure similar to stdout JSON mode
|
|
265
|
+
json_data = {
|
|
266
|
+
"timestamp": datetime.datetime.now().isoformat(),
|
|
267
|
+
"action": action,
|
|
268
|
+
"summary": {
|
|
269
|
+
"total_issues": total_issues,
|
|
270
|
+
"total_fixed": total_fixed,
|
|
271
|
+
"tools_run": len(all_results),
|
|
272
|
+
},
|
|
273
|
+
"results": [],
|
|
274
|
+
}
|
|
275
|
+
for result in all_results:
|
|
276
|
+
result_data = {
|
|
277
|
+
"tool": result.name,
|
|
278
|
+
"success": getattr(result, "success", True),
|
|
279
|
+
"issues_count": getattr(result, "issues_count", 0),
|
|
280
|
+
"output": getattr(result, "output", ""),
|
|
281
|
+
}
|
|
282
|
+
if hasattr(result, "issues") and result.issues:
|
|
283
|
+
result_data["issues"] = [
|
|
284
|
+
{
|
|
285
|
+
"file": getattr(issue, "file", ""),
|
|
286
|
+
"line": getattr(issue, "line", ""),
|
|
287
|
+
"code": getattr(issue, "code", ""),
|
|
288
|
+
"message": getattr(issue, "message", ""),
|
|
289
|
+
}
|
|
290
|
+
for issue in result.issues
|
|
291
|
+
]
|
|
292
|
+
json_data["results"].append(result_data)
|
|
293
|
+
output_file.write_text(
|
|
294
|
+
json.dumps(json_data, indent=2, ensure_ascii=False),
|
|
295
|
+
encoding="utf-8",
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
elif output_format == OutputFormat.CSV:
|
|
299
|
+
# Write CSV format
|
|
300
|
+
rows: list[list[str]] = []
|
|
301
|
+
header: list[str] = ["tool", "issues_count", "file", "line", "code", "message"]
|
|
302
|
+
for result in all_results:
|
|
303
|
+
if hasattr(result, "issues") and result.issues:
|
|
304
|
+
for issue in result.issues:
|
|
305
|
+
rows.append(
|
|
306
|
+
[
|
|
307
|
+
result.name,
|
|
308
|
+
str(getattr(result, "issues_count", 0)),
|
|
309
|
+
str(getattr(issue, "file", "")),
|
|
310
|
+
str(getattr(issue, "line", "")),
|
|
311
|
+
str(getattr(issue, "code", "")),
|
|
312
|
+
str(getattr(issue, "message", "")),
|
|
313
|
+
],
|
|
314
|
+
)
|
|
315
|
+
else:
|
|
316
|
+
rows.append(
|
|
317
|
+
[
|
|
318
|
+
result.name,
|
|
319
|
+
str(getattr(result, "issues_count", 0)),
|
|
320
|
+
"",
|
|
321
|
+
"",
|
|
322
|
+
"",
|
|
323
|
+
"",
|
|
324
|
+
],
|
|
325
|
+
)
|
|
326
|
+
with output_file.open("w", encoding="utf-8", newline="") as f:
|
|
327
|
+
writer = csv.writer(f)
|
|
328
|
+
writer.writerow(header)
|
|
329
|
+
writer.writerows(rows)
|
|
330
|
+
|
|
331
|
+
elif output_format == OutputFormat.MARKDOWN:
|
|
332
|
+
# Write Markdown format
|
|
333
|
+
lines: list[str] = ["# Lintro Report", ""]
|
|
334
|
+
lines.append("## Summary\n")
|
|
335
|
+
lines.append("| Tool | Issues |")
|
|
336
|
+
lines.append("|------|--------|")
|
|
337
|
+
for result in all_results:
|
|
338
|
+
lines.append(f"| {result.name} | {getattr(result, 'issues_count', 0)} |")
|
|
339
|
+
lines.append("")
|
|
340
|
+
for result in all_results:
|
|
341
|
+
issues_count = getattr(result, "issues_count", 0)
|
|
342
|
+
lines.append(f"### {result.name} ({issues_count} issues)")
|
|
343
|
+
if hasattr(result, "issues") and result.issues:
|
|
344
|
+
lines.append("| File | Line | Code | Message |")
|
|
345
|
+
lines.append("|------|------|------|---------|")
|
|
346
|
+
for issue in result.issues:
|
|
347
|
+
file_val = str(getattr(issue, "file", "")).replace("|", r"\|")
|
|
348
|
+
line_val = getattr(issue, "line", "")
|
|
349
|
+
code_val = str(getattr(issue, "code", "")).replace("|", r"\|")
|
|
350
|
+
msg_val = str(getattr(issue, "message", "")).replace("|", r"\|")
|
|
351
|
+
lines.append(
|
|
352
|
+
f"| {file_val} | {line_val} | {code_val} | {msg_val} |",
|
|
353
|
+
)
|
|
354
|
+
lines.append("")
|
|
355
|
+
else:
|
|
356
|
+
lines.append("No issues found.\n")
|
|
357
|
+
output_file.write_text("\n".join(lines), encoding="utf-8")
|
|
358
|
+
|
|
359
|
+
elif output_format == OutputFormat.HTML:
|
|
360
|
+
# Write HTML format
|
|
361
|
+
html_lines: list[str] = [
|
|
362
|
+
"<html><head><title>Lintro Report</title></head><body>",
|
|
363
|
+
]
|
|
364
|
+
html_lines.append("<h1>Lintro Report</h1>")
|
|
365
|
+
html_lines.append("<h2>Summary</h2>")
|
|
366
|
+
html_lines.append("<table border='1'><tr><th>Tool</th><th>Issues</th></tr>")
|
|
367
|
+
for result in all_results:
|
|
368
|
+
import html
|
|
369
|
+
|
|
370
|
+
safe_name = html.escape(result.name)
|
|
371
|
+
html_lines.append(
|
|
372
|
+
f"<tr><td>{safe_name}</td>"
|
|
373
|
+
f"<td>{getattr(result, 'issues_count', 0)}</td></tr>",
|
|
374
|
+
)
|
|
375
|
+
html_lines.append("</table>")
|
|
376
|
+
for result in all_results:
|
|
377
|
+
import html
|
|
378
|
+
|
|
379
|
+
issues_count = getattr(result, "issues_count", 0)
|
|
380
|
+
html_lines.append(
|
|
381
|
+
f"<h3>{html.escape(result.name)} ({issues_count} issues)</h3>",
|
|
382
|
+
)
|
|
383
|
+
if hasattr(result, "issues") and result.issues:
|
|
384
|
+
html_lines.append(
|
|
385
|
+
"<table border='1'><tr><th>File</th><th>Line</th>"
|
|
386
|
+
"<th>Code</th><th>Message</th></tr>",
|
|
387
|
+
)
|
|
388
|
+
for issue in result.issues:
|
|
389
|
+
f_val = html.escape(str(getattr(issue, "file", "")))
|
|
390
|
+
l_val = getattr(issue, "line", "")
|
|
391
|
+
c_val = html.escape(str(getattr(issue, "code", "")))
|
|
392
|
+
m_val = html.escape(str(getattr(issue, "message", "")))
|
|
393
|
+
html_lines.append(
|
|
394
|
+
f"<tr><td>{f_val}</td><td>{l_val}</td>"
|
|
395
|
+
f"<td>{c_val}</td><td>{m_val}</td></tr>",
|
|
396
|
+
)
|
|
397
|
+
html_lines.append("</table>")
|
|
398
|
+
else:
|
|
399
|
+
html_lines.append("<p>No issues found.</p>")
|
|
400
|
+
html_lines.append("</body></html>")
|
|
401
|
+
output_file.write_text("\n".join(html_lines), encoding="utf-8")
|
|
402
|
+
|
|
403
|
+
else:
|
|
404
|
+
# Plain or Grid format - write formatted text output
|
|
405
|
+
lines: list[str] = [f"Lintro {action.capitalize()} Report", "=" * 40, ""]
|
|
406
|
+
for result in all_results:
|
|
407
|
+
issues_count = getattr(result, "issues_count", 0)
|
|
408
|
+
lines.append(f"{result.name}: {issues_count} issues")
|
|
409
|
+
output_text = getattr(result, "output", "")
|
|
410
|
+
if output_text and output_text.strip():
|
|
411
|
+
lines.append(output_text.strip())
|
|
412
|
+
lines.append("")
|
|
413
|
+
lines.append(f"Total Issues: {total_issues}")
|
|
414
|
+
if action == "fmt":
|
|
415
|
+
lines.append(f"Total Fixed: {total_fixed}")
|
|
416
|
+
output_file.write_text("\n".join(lines), encoding="utf-8")
|
|
417
|
+
|
|
418
|
+
|
|
159
419
|
def run_lint_tools_simple(
|
|
160
420
|
*,
|
|
161
421
|
action: str,
|
|
@@ -168,6 +428,7 @@ def run_lint_tools_simple(
|
|
|
168
428
|
output_format: str,
|
|
169
429
|
verbose: bool,
|
|
170
430
|
raw_output: bool = False,
|
|
431
|
+
output_file: str | None = None,
|
|
171
432
|
) -> int:
|
|
172
433
|
"""Simplified runner using Loguru-based logging with rich formatting.
|
|
173
434
|
|
|
@@ -188,6 +449,7 @@ def run_lint_tools_simple(
|
|
|
188
449
|
output_format: str: Output format for results.
|
|
189
450
|
verbose: bool: Whether to enable verbose output.
|
|
190
451
|
raw_output: bool: Whether to show raw tool output instead of formatted output.
|
|
452
|
+
output_file: str | None: Optional file path to write results to.
|
|
191
453
|
|
|
192
454
|
Returns:
|
|
193
455
|
int: Exit code (0 for success, 1 for failures).
|
|
@@ -275,11 +537,11 @@ def run_lint_tools_simple(
|
|
|
275
537
|
|
|
276
538
|
# Run each tool with rich formatting
|
|
277
539
|
for tool_enum in tools_to_run:
|
|
278
|
-
tool_name: str = tool_enum.name.lower()
|
|
279
540
|
# Resolve the tool instance; if unavailable, record failure and continue
|
|
280
541
|
try:
|
|
281
542
|
tool = tool_manager.get_tool(tool_enum)
|
|
282
543
|
except Exception as e:
|
|
544
|
+
tool_name: str = _get_tool_display_name(tool_enum)
|
|
283
545
|
logger.warning(f"Tool '{tool_name}' unavailable: {e}")
|
|
284
546
|
from lintro.models.core.tool_result import ToolResult
|
|
285
547
|
|
|
@@ -293,22 +555,30 @@ def run_lint_tools_simple(
|
|
|
293
555
|
)
|
|
294
556
|
continue
|
|
295
557
|
|
|
558
|
+
# Use canonical display name for consistent logging
|
|
559
|
+
tool_name: str = _get_tool_display_name(tool_enum)
|
|
296
560
|
# Print rich tool header (skip for JSON mode)
|
|
297
561
|
if not json_output_mode:
|
|
298
562
|
logger.print_tool_header(tool_name=tool_name, action=action)
|
|
299
563
|
|
|
300
564
|
try:
|
|
301
|
-
# Configure tool options
|
|
302
|
-
#
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
565
|
+
# Configure tool options using UnifiedConfigManager
|
|
566
|
+
# Priority: CLI --tool-options > [tool.lintro.<tool>] > global settings
|
|
567
|
+
config_manager = UnifiedConfigManager()
|
|
568
|
+
|
|
569
|
+
# Build CLI overrides from --tool-options
|
|
570
|
+
cli_overrides: dict[str, object] = {}
|
|
571
|
+
lookup_keys = _get_tool_lookup_keys(tool_enum, tool_name)
|
|
572
|
+
for option_key in lookup_keys:
|
|
573
|
+
overrides = tool_option_dict.get(option_key)
|
|
574
|
+
if overrides:
|
|
575
|
+
cli_overrides.update(overrides)
|
|
576
|
+
|
|
577
|
+
# Apply unified config with CLI overrides
|
|
578
|
+
config_manager.apply_config_to_tool(
|
|
579
|
+
tool=tool,
|
|
580
|
+
cli_overrides=cli_overrides if cli_overrides else None,
|
|
581
|
+
)
|
|
312
582
|
|
|
313
583
|
# Set common options
|
|
314
584
|
if exclude:
|
|
@@ -324,19 +594,19 @@ def run_lint_tools_simple(
|
|
|
324
594
|
# CLI or config. This keeps Ruff focused on lint fixes while Black
|
|
325
595
|
# handles formatting.
|
|
326
596
|
if "black" in post_tools_early and tool_name == "ruff":
|
|
327
|
-
#
|
|
328
|
-
|
|
329
|
-
|
|
597
|
+
# Get tool config from manager to check for explicit overrides
|
|
598
|
+
tool_config = config_manager.get_tool_config(tool_name)
|
|
599
|
+
lintro_tool_cfg = tool_config.lintro_tool_config or {}
|
|
330
600
|
if action == "fmt":
|
|
331
601
|
if (
|
|
332
602
|
"format" not in cli_overrides
|
|
333
|
-
and "format" not in
|
|
603
|
+
and "format" not in lintro_tool_cfg
|
|
334
604
|
):
|
|
335
605
|
tool.set_options(format=False)
|
|
336
606
|
else: # check
|
|
337
607
|
if (
|
|
338
608
|
"format_check" not in cli_overrides
|
|
339
|
-
and "format_check" not in
|
|
609
|
+
and "format_check" not in lintro_tool_cfg
|
|
340
610
|
):
|
|
341
611
|
tool.set_options(format_check=False)
|
|
342
612
|
|
|
@@ -440,10 +710,15 @@ def run_lint_tools_simple(
|
|
|
440
710
|
continue
|
|
441
711
|
|
|
442
712
|
# Optionally run post-checks (explicit, after main tools)
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
713
|
+
# Skip post-checks for test action - test commands should only run tests
|
|
714
|
+
if action == "test":
|
|
715
|
+
post_tools = []
|
|
716
|
+
enforce_failure = False
|
|
717
|
+
else:
|
|
718
|
+
post_cfg = post_cfg_early or load_post_checks_config()
|
|
719
|
+
post_enabled = bool(post_cfg.get("enabled", False))
|
|
720
|
+
post_tools = list(post_cfg.get("tools", [])) if post_enabled else []
|
|
721
|
+
enforce_failure = bool(post_cfg.get("enforce_failure", action == "check"))
|
|
447
722
|
|
|
448
723
|
# In JSON mode, we still need exit-code enforcement even if we skip
|
|
449
724
|
# rendering post-check outputs. If a post-check tool is unavailable
|
|
@@ -489,22 +764,19 @@ def run_lint_tools_simple(
|
|
|
489
764
|
f"Post-check '{post_tool_name}' unavailable: {e}",
|
|
490
765
|
)
|
|
491
766
|
continue
|
|
492
|
-
|
|
767
|
+
# Use canonical display name for consistent logging
|
|
768
|
+
tool_name = _get_tool_display_name(tool_enum)
|
|
493
769
|
|
|
494
770
|
# Post-checks run with explicit headers (reuse standard header)
|
|
495
771
|
if not json_output_mode:
|
|
496
772
|
logger.print_tool_header(tool_name=tool_name, action=action)
|
|
497
773
|
|
|
498
774
|
try:
|
|
499
|
-
#
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
except Exception as e:
|
|
505
|
-
logger.debug(
|
|
506
|
-
f"Ignoring invalid config for {tool_name}: {e}",
|
|
507
|
-
)
|
|
775
|
+
# Configure post-check tool using UnifiedConfigManager
|
|
776
|
+
# This replaces manual sync logic with unified config management
|
|
777
|
+
post_config_manager = UnifiedConfigManager()
|
|
778
|
+
post_config_manager.apply_config_to_tool(tool=tool)
|
|
779
|
+
|
|
508
780
|
tool.set_options(include_venv=include_venv)
|
|
509
781
|
if exclude:
|
|
510
782
|
exclude_patterns: list[str] = [
|
|
@@ -630,11 +902,36 @@ def run_lint_tools_simple(
|
|
|
630
902
|
# Log at debug to avoid failing the run for non-critical persistence.
|
|
631
903
|
logger.debug(f"Error saving outputs: {e}")
|
|
632
904
|
|
|
905
|
+
# Write to user-specified output file if provided
|
|
906
|
+
if output_file:
|
|
907
|
+
try:
|
|
908
|
+
_write_output_file(
|
|
909
|
+
output_path=output_file,
|
|
910
|
+
output_format=output_fmt_enum,
|
|
911
|
+
all_results=all_results,
|
|
912
|
+
action=action,
|
|
913
|
+
total_issues=total_issues,
|
|
914
|
+
total_fixed=total_fixed,
|
|
915
|
+
)
|
|
916
|
+
logger.debug(f"Wrote results to {output_file}")
|
|
917
|
+
except Exception as e:
|
|
918
|
+
logger.error(f"Failed to write output to {output_file}: {e}")
|
|
919
|
+
|
|
633
920
|
# Return appropriate exit code
|
|
634
921
|
if action == "fmt":
|
|
635
|
-
# Format operations
|
|
636
|
-
#
|
|
637
|
-
|
|
922
|
+
# Format operations should fail if:
|
|
923
|
+
# 1. Any tool reported failure (execution error)
|
|
924
|
+
# 2. There are remaining unfixable issues after formatting
|
|
925
|
+
any_failed: bool = any(
|
|
926
|
+
not getattr(result, "success", True) for result in all_results
|
|
927
|
+
)
|
|
928
|
+
# Check if there are remaining issues that couldn't be fixed
|
|
929
|
+
has_remaining_issues: bool = total_remaining > 0
|
|
930
|
+
return (
|
|
931
|
+
DEFAULT_EXIT_CODE_SUCCESS
|
|
932
|
+
if (not any_failed and not has_remaining_issues)
|
|
933
|
+
else DEFAULT_EXIT_CODE_FAILURE
|
|
934
|
+
)
|
|
638
935
|
else: # check
|
|
639
936
|
# Check operations fail if issues are found OR any tool reported failure
|
|
640
937
|
any_failed: bool = any(
|
lintro/utils/tool_utils.py
CHANGED
|
@@ -27,14 +27,26 @@ from lintro.formatters.tools.darglint_formatter import (
|
|
|
27
27
|
DarglintTableDescriptor,
|
|
28
28
|
format_darglint_issues,
|
|
29
29
|
)
|
|
30
|
+
from lintro.formatters.tools.eslint_formatter import (
|
|
31
|
+
EslintTableDescriptor,
|
|
32
|
+
format_eslint_issues,
|
|
33
|
+
)
|
|
30
34
|
from lintro.formatters.tools.hadolint_formatter import (
|
|
31
35
|
HadolintTableDescriptor,
|
|
32
36
|
format_hadolint_issues,
|
|
33
37
|
)
|
|
38
|
+
from lintro.formatters.tools.markdownlint_formatter import (
|
|
39
|
+
MarkdownlintTableDescriptor,
|
|
40
|
+
format_markdownlint_issues,
|
|
41
|
+
)
|
|
34
42
|
from lintro.formatters.tools.prettier_formatter import (
|
|
35
43
|
PrettierTableDescriptor,
|
|
36
44
|
format_prettier_issues,
|
|
37
45
|
)
|
|
46
|
+
from lintro.formatters.tools.pytest_formatter import (
|
|
47
|
+
PytestFailuresTableDescriptor,
|
|
48
|
+
format_pytest_issues,
|
|
49
|
+
)
|
|
38
50
|
from lintro.formatters.tools.ruff_formatter import (
|
|
39
51
|
RuffTableDescriptor,
|
|
40
52
|
format_ruff_issues,
|
|
@@ -47,9 +59,13 @@ from lintro.parsers.bandit.bandit_parser import parse_bandit_output
|
|
|
47
59
|
from lintro.parsers.black.black_issue import BlackIssue
|
|
48
60
|
from lintro.parsers.black.black_parser import parse_black_output
|
|
49
61
|
from lintro.parsers.darglint.darglint_parser import parse_darglint_output
|
|
62
|
+
from lintro.parsers.eslint.eslint_issue import EslintIssue
|
|
63
|
+
from lintro.parsers.eslint.eslint_parser import parse_eslint_output
|
|
50
64
|
from lintro.parsers.hadolint.hadolint_parser import parse_hadolint_output
|
|
65
|
+
from lintro.parsers.markdownlint.markdownlint_parser import parse_markdownlint_output
|
|
51
66
|
from lintro.parsers.prettier.prettier_issue import PrettierIssue
|
|
52
67
|
from lintro.parsers.prettier.prettier_parser import parse_prettier_output
|
|
68
|
+
from lintro.parsers.pytest.pytest_parser import parse_pytest_text_output
|
|
53
69
|
from lintro.parsers.ruff.ruff_issue import RuffFormatIssue, RuffIssue
|
|
54
70
|
from lintro.parsers.ruff.ruff_parser import parse_ruff_output
|
|
55
71
|
from lintro.parsers.yamllint.yamllint_parser import parse_yamllint_output
|
|
@@ -57,13 +73,16 @@ from lintro.parsers.yamllint.yamllint_parser import parse_yamllint_output
|
|
|
57
73
|
# Constants
|
|
58
74
|
TOOL_TABLE_FORMATTERS: dict[str, tuple] = {
|
|
59
75
|
"darglint": (DarglintTableDescriptor(), format_darglint_issues),
|
|
76
|
+
"eslint": (EslintTableDescriptor(), format_eslint_issues),
|
|
60
77
|
"hadolint": (HadolintTableDescriptor(), format_hadolint_issues),
|
|
61
78
|
"black": (BlackTableDescriptor(), format_black_issues),
|
|
62
79
|
"prettier": (PrettierTableDescriptor(), format_prettier_issues),
|
|
80
|
+
"pytest": (PytestFailuresTableDescriptor(), format_pytest_issues),
|
|
63
81
|
"ruff": (RuffTableDescriptor(), format_ruff_issues),
|
|
64
82
|
"yamllint": (YamllintTableDescriptor(), format_yamllint_issues),
|
|
65
83
|
"actionlint": (ActionlintTableDescriptor(), format_actionlint_issues),
|
|
66
84
|
"bandit": (BanditTableDescriptor(), format_bandit_issues),
|
|
85
|
+
"markdownlint": (MarkdownlintTableDescriptor(), format_markdownlint_issues),
|
|
67
86
|
}
|
|
68
87
|
VENV_PATTERNS: list[str] = [
|
|
69
88
|
"venv",
|
|
@@ -304,9 +323,19 @@ def format_tool_output(
|
|
|
304
323
|
isinstance(i, RuffIssue) and getattr(i, "fixable", False)
|
|
305
324
|
)
|
|
306
325
|
if tool == "prettier":
|
|
307
|
-
return lambda i: isinstance(i, PrettierIssue)
|
|
326
|
+
return lambda i: isinstance(i, PrettierIssue)
|
|
308
327
|
if tool == "black":
|
|
309
|
-
return lambda i: isinstance(i, BlackIssue)
|
|
328
|
+
return lambda i: isinstance(i, BlackIssue) and getattr(
|
|
329
|
+
i,
|
|
330
|
+
"fixable",
|
|
331
|
+
True,
|
|
332
|
+
)
|
|
333
|
+
if tool == "eslint":
|
|
334
|
+
return lambda i: isinstance(i, EslintIssue) and getattr(
|
|
335
|
+
i,
|
|
336
|
+
"fixable",
|
|
337
|
+
False,
|
|
338
|
+
)
|
|
310
339
|
return None
|
|
311
340
|
|
|
312
341
|
is_fixable = _is_fixable_predicate(tool_name)
|
|
@@ -364,10 +393,14 @@ def format_tool_output(
|
|
|
364
393
|
parsed_issues = parse_black_output(output=output)
|
|
365
394
|
elif tool_name == "darglint":
|
|
366
395
|
parsed_issues = parse_darglint_output(output=output)
|
|
396
|
+
elif tool_name == "eslint":
|
|
397
|
+
parsed_issues = parse_eslint_output(output=output)
|
|
367
398
|
elif tool_name == "hadolint":
|
|
368
399
|
parsed_issues = parse_hadolint_output(output=output)
|
|
369
400
|
elif tool_name == "yamllint":
|
|
370
401
|
parsed_issues = parse_yamllint_output(output=output)
|
|
402
|
+
elif tool_name == "markdownlint":
|
|
403
|
+
parsed_issues = parse_markdownlint_output(output=output)
|
|
371
404
|
elif tool_name == "bandit":
|
|
372
405
|
# Bandit emits JSON; try parsing when raw output is provided
|
|
373
406
|
try:
|
|
@@ -376,6 +409,9 @@ def format_tool_output(
|
|
|
376
409
|
)
|
|
377
410
|
except Exception:
|
|
378
411
|
parsed_issues = []
|
|
412
|
+
elif tool_name == "pytest":
|
|
413
|
+
# Pytest emits text output; parse it
|
|
414
|
+
parsed_issues = parse_pytest_text_output(output=output)
|
|
379
415
|
|
|
380
416
|
if parsed_issues and tool_name in TOOL_TABLE_FORMATTERS:
|
|
381
417
|
_, formatter_func = TOOL_TABLE_FORMATTERS[tool_name]
|