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,201 @@
|
|
|
1
|
+
"""Formatter for pytest issues."""
|
|
2
|
+
|
|
3
|
+
from lintro.formatters.core.table_descriptor import TableDescriptor
|
|
4
|
+
from lintro.formatters.styles.csv import CsvStyle
|
|
5
|
+
from lintro.formatters.styles.grid import GridStyle
|
|
6
|
+
from lintro.formatters.styles.html import HtmlStyle
|
|
7
|
+
from lintro.formatters.styles.json import JsonStyle
|
|
8
|
+
from lintro.formatters.styles.markdown import MarkdownStyle
|
|
9
|
+
from lintro.formatters.styles.plain import PlainStyle
|
|
10
|
+
from lintro.parsers.pytest.pytest_issue import PytestIssue
|
|
11
|
+
from lintro.utils.path_utils import normalize_file_path_for_display
|
|
12
|
+
|
|
13
|
+
# Maximum message length before truncation (reasonable for terminal widths)
|
|
14
|
+
MAX_MESSAGE_LENGTH: int = 100
|
|
15
|
+
|
|
16
|
+
FORMAT_MAP = {
|
|
17
|
+
"plain": PlainStyle(),
|
|
18
|
+
"grid": GridStyle(),
|
|
19
|
+
"markdown": MarkdownStyle(),
|
|
20
|
+
"html": HtmlStyle(),
|
|
21
|
+
"json": JsonStyle(),
|
|
22
|
+
"csv": CsvStyle(),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PytestFailuresTableDescriptor(TableDescriptor):
|
|
27
|
+
"""Describe columns and rows for pytest failed/error issues."""
|
|
28
|
+
|
|
29
|
+
def get_columns(self) -> list[str]:
|
|
30
|
+
"""Return ordered column headers for the pytest failures table.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
list[str]: Column names for the formatted table.
|
|
34
|
+
"""
|
|
35
|
+
return ["File", "Status", "Error"]
|
|
36
|
+
|
|
37
|
+
def get_rows(
|
|
38
|
+
self,
|
|
39
|
+
issues: list[PytestIssue],
|
|
40
|
+
) -> list[list[str]]:
|
|
41
|
+
"""Return rows for the pytest failures table.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
issues: Parsed pytest issues to render.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
list[list[str]]: Table rows with normalized file path and fields.
|
|
48
|
+
"""
|
|
49
|
+
rows = []
|
|
50
|
+
for issue in issues:
|
|
51
|
+
# Only show failed/error tests
|
|
52
|
+
if issue.test_status not in ("FAILED", "ERROR"):
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
message = str(issue.message) if issue.message is not None else ""
|
|
56
|
+
truncated_message = (
|
|
57
|
+
f"{message[:MAX_MESSAGE_LENGTH]}..."
|
|
58
|
+
if len(message) > MAX_MESSAGE_LENGTH + 3
|
|
59
|
+
else message
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
status_emoji = "❌ FAIL" if issue.test_status == "FAILED" else "⚠️ ERROR"
|
|
63
|
+
|
|
64
|
+
rows.append(
|
|
65
|
+
[
|
|
66
|
+
normalize_file_path_for_display(issue.file),
|
|
67
|
+
status_emoji,
|
|
68
|
+
truncated_message,
|
|
69
|
+
],
|
|
70
|
+
)
|
|
71
|
+
return rows
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class PytestSkippedTableDescriptor(TableDescriptor):
|
|
75
|
+
"""Describe columns and rows for pytest skipped issues."""
|
|
76
|
+
|
|
77
|
+
def get_columns(self) -> list[str]:
|
|
78
|
+
"""Return ordered column headers for the pytest skipped table.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
list[str]: Column names for the formatted table.
|
|
82
|
+
"""
|
|
83
|
+
return ["File", "Test", "Skip Reason"]
|
|
84
|
+
|
|
85
|
+
def get_rows(
|
|
86
|
+
self,
|
|
87
|
+
issues: list[PytestIssue],
|
|
88
|
+
) -> list[list[str]]:
|
|
89
|
+
"""Return rows for the pytest skipped table.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
issues: Parsed pytest issues to render.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
list[list[str]]: Table rows with normalized file path and fields.
|
|
96
|
+
"""
|
|
97
|
+
rows = []
|
|
98
|
+
for issue in issues:
|
|
99
|
+
# Only show skipped tests
|
|
100
|
+
if issue.test_status != "SKIPPED":
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
message = str(issue.message) if issue.message is not None else ""
|
|
104
|
+
truncated_message = (
|
|
105
|
+
f"{message[:MAX_MESSAGE_LENGTH]}..."
|
|
106
|
+
if len(message) > MAX_MESSAGE_LENGTH + 3
|
|
107
|
+
else message
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
rows.append(
|
|
111
|
+
[
|
|
112
|
+
normalize_file_path_for_display(issue.file),
|
|
113
|
+
issue.test_name or "Unknown",
|
|
114
|
+
truncated_message,
|
|
115
|
+
],
|
|
116
|
+
)
|
|
117
|
+
return rows
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def format_pytest_failures(
|
|
121
|
+
issues: list[PytestIssue],
|
|
122
|
+
format: str = "grid",
|
|
123
|
+
) -> str:
|
|
124
|
+
"""Format pytest failures and errors into a table.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
issues: List of pytest issues to format.
|
|
128
|
+
format: Output format (plain, grid, markdown, html, json, csv).
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
str: Formatted string with pytest failures table.
|
|
132
|
+
"""
|
|
133
|
+
descriptor = PytestFailuresTableDescriptor()
|
|
134
|
+
formatter = FORMAT_MAP.get(format, GridStyle())
|
|
135
|
+
|
|
136
|
+
columns = descriptor.get_columns()
|
|
137
|
+
rows = descriptor.get_rows(issues)
|
|
138
|
+
|
|
139
|
+
# Always return a table structure, even if empty
|
|
140
|
+
return formatter.format(
|
|
141
|
+
columns=columns,
|
|
142
|
+
rows=rows,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def format_pytest_skipped(
|
|
147
|
+
issues: list[PytestIssue],
|
|
148
|
+
format: str = "grid",
|
|
149
|
+
) -> str:
|
|
150
|
+
"""Format pytest skipped tests into a table.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
issues: List of pytest issues to format.
|
|
154
|
+
format: Output format (plain, grid, markdown, html, json, csv).
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
str: Formatted string with pytest skipped tests table.
|
|
158
|
+
"""
|
|
159
|
+
descriptor = PytestSkippedTableDescriptor()
|
|
160
|
+
formatter = FORMAT_MAP.get(format, GridStyle())
|
|
161
|
+
|
|
162
|
+
columns = descriptor.get_columns()
|
|
163
|
+
rows = descriptor.get_rows(issues)
|
|
164
|
+
|
|
165
|
+
# Always return a table structure, even if empty
|
|
166
|
+
return formatter.format(
|
|
167
|
+
columns=columns,
|
|
168
|
+
rows=rows,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def format_pytest_issues(
|
|
173
|
+
issues: list[PytestIssue],
|
|
174
|
+
format: str = "grid",
|
|
175
|
+
) -> str:
|
|
176
|
+
"""Format pytest issues into tables for failures and skipped tests.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
issues: List of pytest issues to format.
|
|
180
|
+
format: Output format (plain, grid, markdown, html, json, csv).
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
str: Formatted string with pytest issues tables.
|
|
184
|
+
"""
|
|
185
|
+
output_parts = []
|
|
186
|
+
|
|
187
|
+
# Format failures table
|
|
188
|
+
failures_table = format_pytest_failures(issues, format)
|
|
189
|
+
if failures_table.strip():
|
|
190
|
+
output_parts.append("Test Failures:")
|
|
191
|
+
output_parts.append(failures_table)
|
|
192
|
+
|
|
193
|
+
# Format skipped tests table
|
|
194
|
+
skipped_table = format_pytest_skipped(issues, format)
|
|
195
|
+
if skipped_table.strip():
|
|
196
|
+
if output_parts:
|
|
197
|
+
output_parts.append("") # Add blank line between tables
|
|
198
|
+
output_parts.append("Skipped Tests:")
|
|
199
|
+
output_parts.append(skipped_table)
|
|
200
|
+
|
|
201
|
+
return "\n".join(output_parts)
|
lintro/parsers/__init__.py
CHANGED
|
@@ -1,21 +1,81 @@
|
|
|
1
1
|
"""Parser modules for Lintro tools."""
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib import import_module
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
# Type checking imports
|
|
10
|
+
from lintro.parsers import (
|
|
11
|
+
actionlint,
|
|
12
|
+
bandit,
|
|
13
|
+
darglint,
|
|
14
|
+
eslint,
|
|
15
|
+
hadolint,
|
|
16
|
+
markdownlint,
|
|
17
|
+
prettier,
|
|
18
|
+
pytest,
|
|
19
|
+
ruff,
|
|
20
|
+
yamllint,
|
|
21
|
+
)
|
|
12
22
|
|
|
13
23
|
__all__ = [
|
|
14
24
|
"actionlint",
|
|
15
25
|
"bandit",
|
|
16
26
|
"darglint",
|
|
27
|
+
"eslint",
|
|
17
28
|
"hadolint",
|
|
29
|
+
"markdownlint",
|
|
18
30
|
"prettier",
|
|
31
|
+
"pytest",
|
|
19
32
|
"ruff",
|
|
20
33
|
"yamllint",
|
|
21
34
|
]
|
|
35
|
+
|
|
36
|
+
# Lazy-load parser submodules to avoid circular imports
|
|
37
|
+
_SUBMODULES = {
|
|
38
|
+
"actionlint",
|
|
39
|
+
"bandit",
|
|
40
|
+
"darglint",
|
|
41
|
+
"eslint",
|
|
42
|
+
"hadolint",
|
|
43
|
+
"markdownlint",
|
|
44
|
+
"prettier",
|
|
45
|
+
"pytest",
|
|
46
|
+
"ruff",
|
|
47
|
+
"yamllint",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def __getattr__(name: str) -> object:
|
|
52
|
+
"""Lazy-load parser submodules to avoid circular import issues.
|
|
53
|
+
|
|
54
|
+
This function is called when an attribute is accessed that doesn't exist
|
|
55
|
+
in the module. It allows accessing parser submodules without eagerly
|
|
56
|
+
importing them all at package initialization time.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
name: The name of the attribute being accessed.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
The imported submodule.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
AttributeError: If the requested name is not a known submodule.
|
|
66
|
+
"""
|
|
67
|
+
if name in _SUBMODULES:
|
|
68
|
+
module = import_module(f".{name}", __package__)
|
|
69
|
+
# Cache the module in this module's namespace for future access
|
|
70
|
+
globals()[name] = module
|
|
71
|
+
return module
|
|
72
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def __dir__() -> list[str]:
|
|
76
|
+
"""Return list of available attributes for this module.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
List of submodule names and other module attributes.
|
|
80
|
+
"""
|
|
81
|
+
return list(__all__)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Bandit issue model for security vulnerabilities."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class BanditIssue:
|
|
9
|
+
"""Represents a security issue found by Bandit.
|
|
10
|
+
|
|
11
|
+
Attributes:
|
|
12
|
+
file: str: Path to the file containing the issue.
|
|
13
|
+
line: int: Line number where the issue was found.
|
|
14
|
+
col_offset: int: Column offset of the issue.
|
|
15
|
+
issue_severity: str: Severity level (LOW, MEDIUM, HIGH).
|
|
16
|
+
issue_confidence: str: Confidence level (LOW, MEDIUM, HIGH).
|
|
17
|
+
test_id: str: Bandit test ID (e.g., B602, B301).
|
|
18
|
+
test_name: str: Name of the test that found the issue.
|
|
19
|
+
issue_text: str: Description of the security issue.
|
|
20
|
+
more_info: str: URL with more information about the issue.
|
|
21
|
+
cwe: dict[str, Any] | None: CWE (Common Weakness Enumeration) information.
|
|
22
|
+
code: str: Code snippet containing the issue.
|
|
23
|
+
line_range: list[int]: Range of lines containing the issue.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
file: str
|
|
27
|
+
line: int
|
|
28
|
+
col_offset: int
|
|
29
|
+
issue_severity: str
|
|
30
|
+
issue_confidence: str
|
|
31
|
+
test_id: str
|
|
32
|
+
test_name: str
|
|
33
|
+
issue_text: str
|
|
34
|
+
more_info: str
|
|
35
|
+
cwe: dict[str, Any] | None = None
|
|
36
|
+
code: str | None = None
|
|
37
|
+
line_range: list[int] | None = None
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def message(self) -> str:
|
|
41
|
+
"""Get a human-readable message for the issue.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
str: Formatted issue message.
|
|
45
|
+
"""
|
|
46
|
+
return (
|
|
47
|
+
f"[{self.test_id}:{self.test_name}] {self.issue_severity} severity, "
|
|
48
|
+
f"{self.issue_confidence} confidence: {self.issue_text}"
|
|
49
|
+
)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Bandit output parser for security issues."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from loguru import logger
|
|
6
|
+
|
|
7
|
+
from lintro.parsers.bandit.bandit_issue import BanditIssue
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def parse_bandit_output(bandit_data: dict[str, Any]) -> list[BanditIssue]:
|
|
11
|
+
"""Parse Bandit JSON output into BanditIssue objects.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
bandit_data: dict[str, Any]: JSON data from Bandit output.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
list[BanditIssue]: List of parsed security issues.
|
|
18
|
+
|
|
19
|
+
Raises:
|
|
20
|
+
ValueError: If the bandit data structure is invalid.
|
|
21
|
+
"""
|
|
22
|
+
if not isinstance(bandit_data, dict):
|
|
23
|
+
raise ValueError("Bandit data must be a dictionary")
|
|
24
|
+
|
|
25
|
+
results = bandit_data.get("results", [])
|
|
26
|
+
if not isinstance(results, list):
|
|
27
|
+
raise ValueError("Bandit results must be a list")
|
|
28
|
+
|
|
29
|
+
issues: list[BanditIssue] = []
|
|
30
|
+
|
|
31
|
+
for result in results:
|
|
32
|
+
if not isinstance(result, dict):
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
filename = result.get("filename", "")
|
|
37
|
+
line_number = result.get("line_number", 0)
|
|
38
|
+
col_offset = result.get("col_offset", 0)
|
|
39
|
+
issue_severity = result.get("issue_severity", "UNKNOWN")
|
|
40
|
+
issue_confidence = result.get("issue_confidence", "UNKNOWN")
|
|
41
|
+
test_id = result.get("test_id", "")
|
|
42
|
+
test_name = result.get("test_name", "")
|
|
43
|
+
issue_text = result.get("issue_text", "")
|
|
44
|
+
more_info = result.get("more_info", "")
|
|
45
|
+
cwe = result.get("issue_cwe")
|
|
46
|
+
code = result.get("code")
|
|
47
|
+
line_range = result.get("line_range")
|
|
48
|
+
|
|
49
|
+
# Validate critical fields; skip malformed entries
|
|
50
|
+
if not isinstance(filename, str):
|
|
51
|
+
logger.warning("Skipping issue with non-string filename")
|
|
52
|
+
continue
|
|
53
|
+
if not isinstance(line_number, int):
|
|
54
|
+
logger.warning("Skipping issue with non-integer line_number")
|
|
55
|
+
continue
|
|
56
|
+
if not isinstance(col_offset, int):
|
|
57
|
+
col_offset = 0
|
|
58
|
+
|
|
59
|
+
sev = (
|
|
60
|
+
str(issue_severity).upper() if issue_severity is not None else "UNKNOWN"
|
|
61
|
+
)
|
|
62
|
+
conf = (
|
|
63
|
+
str(issue_confidence).upper()
|
|
64
|
+
if issue_confidence is not None
|
|
65
|
+
else "UNKNOWN"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
test_id = test_id if isinstance(test_id, str) else ""
|
|
69
|
+
test_name = test_name if isinstance(test_name, str) else ""
|
|
70
|
+
issue_text = issue_text if isinstance(issue_text, str) else ""
|
|
71
|
+
more_info = more_info if isinstance(more_info, str) else ""
|
|
72
|
+
|
|
73
|
+
# Normalize line_range to list[int] when provided
|
|
74
|
+
if isinstance(line_range, list):
|
|
75
|
+
line_range = [x for x in line_range if isinstance(x, int)] or None
|
|
76
|
+
else:
|
|
77
|
+
line_range = None
|
|
78
|
+
|
|
79
|
+
issue = BanditIssue(
|
|
80
|
+
file=filename,
|
|
81
|
+
line=line_number,
|
|
82
|
+
col_offset=col_offset,
|
|
83
|
+
issue_severity=sev,
|
|
84
|
+
issue_confidence=conf,
|
|
85
|
+
test_id=test_id,
|
|
86
|
+
test_name=test_name,
|
|
87
|
+
issue_text=issue_text,
|
|
88
|
+
more_info=more_info,
|
|
89
|
+
cwe=cwe if isinstance(cwe, dict) else None,
|
|
90
|
+
code=code if isinstance(code, str) else None,
|
|
91
|
+
line_range=line_range,
|
|
92
|
+
)
|
|
93
|
+
issues.append(issue)
|
|
94
|
+
except (KeyError, TypeError, ValueError) as e:
|
|
95
|
+
# Log warning but continue processing other issues
|
|
96
|
+
logger.warning(f"Failed to parse bandit issue: {e}")
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
return issues
|
|
@@ -16,7 +16,11 @@ class BlackIssue:
|
|
|
16
16
|
Attributes:
|
|
17
17
|
file: Path to the file with a formatting difference.
|
|
18
18
|
message: Short human-readable description (e.g., "Would reformat file").
|
|
19
|
+
fixable: Whether this issue can be auto-fixed by Black. Defaults to True
|
|
20
|
+
for standard formatting issues. Set to False for line length violations
|
|
21
|
+
that Black cannot safely wrap.
|
|
19
22
|
"""
|
|
20
23
|
|
|
21
24
|
file: str
|
|
22
25
|
message: str
|
|
26
|
+
fixable: bool = True
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Typed structure representing a single ESLint issue."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class EslintIssue:
|
|
8
|
+
"""Simple container for ESLint findings.
|
|
9
|
+
|
|
10
|
+
Attributes:
|
|
11
|
+
file: File path where the issue occurred.
|
|
12
|
+
line: Line number where the issue occurred.
|
|
13
|
+
column: Column number where the issue occurred.
|
|
14
|
+
code: Rule ID that triggered the issue (e.g., 'no-unused-vars').
|
|
15
|
+
message: Human-readable description of the issue.
|
|
16
|
+
severity: Severity level (1=warning, 2=error).
|
|
17
|
+
fixable: Whether this issue can be auto-fixed.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
file: str
|
|
21
|
+
line: int
|
|
22
|
+
column: int
|
|
23
|
+
code: str
|
|
24
|
+
message: str
|
|
25
|
+
severity: int
|
|
26
|
+
fixable: bool = False
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Parser for ESLint JSON output.
|
|
2
|
+
|
|
3
|
+
Handles ESLint JSON format output from --format json flag.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from lintro.parsers.eslint.eslint_issue import EslintIssue
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse_eslint_output(output: str) -> list[EslintIssue]:
|
|
13
|
+
"""Parse ESLint JSON output into a list of EslintIssue objects.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
output: The raw JSON output from ESLint.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
List of EslintIssue objects.
|
|
20
|
+
"""
|
|
21
|
+
issues: list[EslintIssue] = []
|
|
22
|
+
|
|
23
|
+
if not output:
|
|
24
|
+
return issues
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
# ESLint JSON format is an array of file results
|
|
28
|
+
# Each file result has: filePath, messages[], errorCount, warningCount
|
|
29
|
+
eslint_data: list[dict[str, Any]] = json.loads(output)
|
|
30
|
+
except json.JSONDecodeError:
|
|
31
|
+
# If output is not valid JSON, return empty list
|
|
32
|
+
return issues
|
|
33
|
+
|
|
34
|
+
for file_result in eslint_data:
|
|
35
|
+
file_path: str = file_result.get("filePath", "")
|
|
36
|
+
messages: list[dict[str, Any]] = file_result.get("messages", [])
|
|
37
|
+
|
|
38
|
+
for msg in messages:
|
|
39
|
+
# Extract issue details from ESLint message format
|
|
40
|
+
line: int = msg.get("line", 1)
|
|
41
|
+
column: int = msg.get("column", 1)
|
|
42
|
+
rule_id: str = msg.get("ruleId", "unknown")
|
|
43
|
+
message_text: str = msg.get("message", "")
|
|
44
|
+
severity: int = msg.get("severity", 2) # Default to error (2)
|
|
45
|
+
fixable: bool = msg.get("fix") is not None # Has fix object if fixable
|
|
46
|
+
|
|
47
|
+
# Skip messages without a ruleId (usually syntax errors)
|
|
48
|
+
if not rule_id:
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
issues.append(
|
|
52
|
+
EslintIssue(
|
|
53
|
+
file=file_path,
|
|
54
|
+
line=line,
|
|
55
|
+
column=column,
|
|
56
|
+
code=rule_id,
|
|
57
|
+
message=message_text,
|
|
58
|
+
severity=severity,
|
|
59
|
+
fixable=fixable,
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return issues
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Parser for markdownlint-cli2 output."""
|
|
2
|
+
|
|
3
|
+
from lintro.parsers.markdownlint.markdownlint_issue import MarkdownlintIssue
|
|
4
|
+
from lintro.parsers.markdownlint.markdownlint_parser import parse_markdownlint_output
|
|
5
|
+
|
|
6
|
+
__all__ = ["MarkdownlintIssue", "parse_markdownlint_output"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Markdownlint issue model."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class MarkdownlintIssue:
|
|
8
|
+
"""Represents an issue found by markdownlint-cli2.
|
|
9
|
+
|
|
10
|
+
Attributes:
|
|
11
|
+
file: File path where the issue was found
|
|
12
|
+
line: Line number where the issue occurs
|
|
13
|
+
code: Rule code that was violated (e.g., MD013, MD041)
|
|
14
|
+
message: Description of the issue
|
|
15
|
+
column: Column number where the issue occurs (if available)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
file: str
|
|
19
|
+
line: int
|
|
20
|
+
code: str
|
|
21
|
+
message: str
|
|
22
|
+
column: int | None = None
|