lintro 0.3.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.
Potentially problematic release.
This version of lintro might be problematic. Click here for more details.
- lintro/__init__.py +3 -0
- lintro/__main__.py +6 -0
- lintro/ascii-art/fail.txt +404 -0
- lintro/ascii-art/success.txt +484 -0
- lintro/cli.py +70 -0
- lintro/cli_utils/__init__.py +7 -0
- lintro/cli_utils/commands/__init__.py +7 -0
- lintro/cli_utils/commands/check.py +210 -0
- lintro/cli_utils/commands/format.py +167 -0
- lintro/cli_utils/commands/list_tools.py +114 -0
- lintro/enums/__init__.py +0 -0
- lintro/enums/action.py +29 -0
- lintro/enums/darglint_strictness.py +22 -0
- lintro/enums/group_by.py +31 -0
- lintro/enums/hadolint_enums.py +46 -0
- lintro/enums/output_format.py +40 -0
- lintro/enums/tool_name.py +36 -0
- lintro/enums/tool_type.py +27 -0
- lintro/enums/yamllint_format.py +22 -0
- lintro/exceptions/__init__.py +0 -0
- lintro/exceptions/errors.py +15 -0
- lintro/formatters/__init__.py +0 -0
- lintro/formatters/core/__init__.py +0 -0
- lintro/formatters/core/output_style.py +21 -0
- lintro/formatters/core/table_descriptor.py +24 -0
- lintro/formatters/styles/__init__.py +17 -0
- lintro/formatters/styles/csv.py +41 -0
- lintro/formatters/styles/grid.py +91 -0
- lintro/formatters/styles/html.py +48 -0
- lintro/formatters/styles/json.py +61 -0
- lintro/formatters/styles/markdown.py +41 -0
- lintro/formatters/styles/plain.py +39 -0
- lintro/formatters/tools/__init__.py +35 -0
- lintro/formatters/tools/darglint_formatter.py +72 -0
- lintro/formatters/tools/hadolint_formatter.py +84 -0
- lintro/formatters/tools/prettier_formatter.py +76 -0
- lintro/formatters/tools/ruff_formatter.py +116 -0
- lintro/formatters/tools/yamllint_formatter.py +87 -0
- lintro/models/__init__.py +0 -0
- lintro/models/core/__init__.py +0 -0
- lintro/models/core/tool.py +104 -0
- lintro/models/core/tool_config.py +23 -0
- lintro/models/core/tool_result.py +39 -0
- lintro/parsers/__init__.py +0 -0
- lintro/parsers/darglint/__init__.py +0 -0
- lintro/parsers/darglint/darglint_issue.py +9 -0
- lintro/parsers/darglint/darglint_parser.py +62 -0
- lintro/parsers/hadolint/__init__.py +1 -0
- lintro/parsers/hadolint/hadolint_issue.py +24 -0
- lintro/parsers/hadolint/hadolint_parser.py +65 -0
- lintro/parsers/prettier/__init__.py +0 -0
- lintro/parsers/prettier/prettier_issue.py +10 -0
- lintro/parsers/prettier/prettier_parser.py +60 -0
- lintro/parsers/ruff/__init__.py +1 -0
- lintro/parsers/ruff/ruff_issue.py +43 -0
- lintro/parsers/ruff/ruff_parser.py +89 -0
- lintro/parsers/yamllint/__init__.py +0 -0
- lintro/parsers/yamllint/yamllint_issue.py +24 -0
- lintro/parsers/yamllint/yamllint_parser.py +68 -0
- lintro/tools/__init__.py +40 -0
- lintro/tools/core/__init__.py +0 -0
- lintro/tools/core/tool_base.py +320 -0
- lintro/tools/core/tool_manager.py +167 -0
- lintro/tools/implementations/__init__.py +0 -0
- lintro/tools/implementations/tool_darglint.py +245 -0
- lintro/tools/implementations/tool_hadolint.py +302 -0
- lintro/tools/implementations/tool_prettier.py +270 -0
- lintro/tools/implementations/tool_ruff.py +618 -0
- lintro/tools/implementations/tool_yamllint.py +240 -0
- lintro/tools/tool_enum.py +17 -0
- lintro/utils/__init__.py +0 -0
- lintro/utils/ascii_normalize_cli.py +84 -0
- lintro/utils/config.py +39 -0
- lintro/utils/console_logger.py +783 -0
- lintro/utils/formatting.py +173 -0
- lintro/utils/output_manager.py +301 -0
- lintro/utils/path_utils.py +41 -0
- lintro/utils/tool_executor.py +443 -0
- lintro/utils/tool_utils.py +431 -0
- lintro-0.3.2.dist-info/METADATA +338 -0
- lintro-0.3.2.dist-info/RECORD +85 -0
- lintro-0.3.2.dist-info/WHEEL +5 -0
- lintro-0.3.2.dist-info/entry_points.txt +2 -0
- lintro-0.3.2.dist-info/licenses/LICENSE +21 -0
- lintro-0.3.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""Darglint docstring linter integration."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
from lintro.enums.darglint_strictness import (
|
|
9
|
+
DarglintStrictness,
|
|
10
|
+
normalize_darglint_strictness,
|
|
11
|
+
)
|
|
12
|
+
from lintro.enums.tool_type import ToolType
|
|
13
|
+
from lintro.models.core.tool import ToolConfig, ToolResult
|
|
14
|
+
from lintro.parsers.darglint.darglint_parser import parse_darglint_output
|
|
15
|
+
from lintro.tools.core.tool_base import BaseTool
|
|
16
|
+
from lintro.utils.tool_utils import walk_files_with_excludes
|
|
17
|
+
|
|
18
|
+
# Constants for Darglint configuration
|
|
19
|
+
DARGLINT_DEFAULT_TIMEOUT: int = 10
|
|
20
|
+
DARGLINT_DEFAULT_PRIORITY: int = 45
|
|
21
|
+
DARGLINT_FILE_PATTERNS: list[str] = ["*.py"]
|
|
22
|
+
DARGLINT_STRICTNESS_LEVELS: tuple[str, ...] = tuple(
|
|
23
|
+
m.name.lower() for m in DarglintStrictness
|
|
24
|
+
)
|
|
25
|
+
DARGLINT_MIN_VERBOSITY: int = 1
|
|
26
|
+
DARGLINT_MAX_VERBOSITY: int = 3
|
|
27
|
+
DARGLINT_DEFAULT_VERBOSITY: int = 2
|
|
28
|
+
DARGLINT_DEFAULT_STRICTNESS: str = "full"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class DarglintTool(BaseTool):
|
|
33
|
+
"""Darglint docstring linter integration.
|
|
34
|
+
|
|
35
|
+
Darglint is a Python docstring linter that checks docstring style and completeness.
|
|
36
|
+
It verifies that docstrings match the function signature and contain all required
|
|
37
|
+
sections.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
name: str: Tool name.
|
|
41
|
+
description: str: Tool description.
|
|
42
|
+
can_fix: bool: Whether the core can fix issues.
|
|
43
|
+
config: ToolConfig: Tool configuration.
|
|
44
|
+
exclude_patterns: list[str]: List of patterns to exclude.
|
|
45
|
+
include_venv: bool: Whether to include virtual environment files.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
name: str = "darglint"
|
|
49
|
+
description: str = (
|
|
50
|
+
"Python docstring linter that checks docstring style and completeness"
|
|
51
|
+
)
|
|
52
|
+
can_fix: bool = False # Darglint can only check, not fix
|
|
53
|
+
config: ToolConfig = field(
|
|
54
|
+
default_factory=lambda: ToolConfig(
|
|
55
|
+
priority=DARGLINT_DEFAULT_PRIORITY, # Lower priority than formatters, \
|
|
56
|
+
# slightly lower than flake8
|
|
57
|
+
conflicts_with=[], # No direct conflicts
|
|
58
|
+
file_patterns=DARGLINT_FILE_PATTERNS, # Only applies to Python files
|
|
59
|
+
tool_type=ToolType.LINTER,
|
|
60
|
+
options={
|
|
61
|
+
"timeout": DARGLINT_DEFAULT_TIMEOUT, # Default timeout in seconds \
|
|
62
|
+
# per file
|
|
63
|
+
"ignore": None, # List of error codes to ignore
|
|
64
|
+
"ignore_regex": None, # Regex pattern for error codes to ignore
|
|
65
|
+
"ignore_syntax": False, # Whether to ignore syntax errors
|
|
66
|
+
"message_template": None, # Custom message template
|
|
67
|
+
"verbosity": DARGLINT_DEFAULT_VERBOSITY, # Verbosity level (1-3) - \
|
|
68
|
+
# use 2 for descriptive messages
|
|
69
|
+
"strictness": DARGLINT_DEFAULT_STRICTNESS, # Strictness level \
|
|
70
|
+
# (short, long, full)
|
|
71
|
+
},
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def set_options(
|
|
76
|
+
self,
|
|
77
|
+
ignore: list[str] | None = None,
|
|
78
|
+
ignore_regex: str | None = None,
|
|
79
|
+
ignore_syntax: bool | None = None,
|
|
80
|
+
message_template: str | None = None,
|
|
81
|
+
verbosity: int | None = None,
|
|
82
|
+
strictness: str | DarglintStrictness | None = None,
|
|
83
|
+
**kwargs,
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Set Darglint-specific options.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
ignore: list[str] | None: List of error codes to ignore.
|
|
89
|
+
ignore_regex: str | None: Regex pattern for error codes to ignore.
|
|
90
|
+
ignore_syntax: bool | None: Whether to ignore syntax errors.
|
|
91
|
+
message_template: str | None: Custom message template.
|
|
92
|
+
verbosity: int | None: Verbosity level (1-3).
|
|
93
|
+
strictness: str | None: Strictness level (short, long, full).
|
|
94
|
+
**kwargs: Other core options.
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
ValueError: If an option value is invalid.
|
|
98
|
+
"""
|
|
99
|
+
if ignore is not None and not isinstance(ignore, list):
|
|
100
|
+
raise ValueError("ignore must be a list of error codes")
|
|
101
|
+
if ignore_regex is not None and not isinstance(ignore_regex, str):
|
|
102
|
+
raise ValueError("ignore_regex must be a string")
|
|
103
|
+
if ignore_syntax is not None and not isinstance(ignore_syntax, bool):
|
|
104
|
+
raise ValueError("ignore_syntax must be a boolean")
|
|
105
|
+
if message_template is not None and not isinstance(message_template, str):
|
|
106
|
+
raise ValueError("message_template must be a string")
|
|
107
|
+
if verbosity is not None:
|
|
108
|
+
if not isinstance(verbosity, int):
|
|
109
|
+
raise ValueError("verbosity must be an integer")
|
|
110
|
+
if not DARGLINT_MIN_VERBOSITY <= verbosity <= DARGLINT_MAX_VERBOSITY:
|
|
111
|
+
raise ValueError(
|
|
112
|
+
f"verbosity must be between {DARGLINT_MIN_VERBOSITY} and "
|
|
113
|
+
f"{DARGLINT_MAX_VERBOSITY}",
|
|
114
|
+
)
|
|
115
|
+
if strictness is not None:
|
|
116
|
+
strict_enum = normalize_darglint_strictness( # type: ignore[arg-type]
|
|
117
|
+
strictness,
|
|
118
|
+
)
|
|
119
|
+
strictness = strict_enum.name.lower()
|
|
120
|
+
|
|
121
|
+
options: dict = {
|
|
122
|
+
"ignore": ignore,
|
|
123
|
+
"ignore_regex": ignore_regex,
|
|
124
|
+
"ignore_syntax": ignore_syntax,
|
|
125
|
+
"message_template": message_template,
|
|
126
|
+
"verbosity": verbosity,
|
|
127
|
+
"strictness": strictness,
|
|
128
|
+
}
|
|
129
|
+
# Remove None values
|
|
130
|
+
options = {k: v for k, v in options.items() if v is not None}
|
|
131
|
+
super().set_options(**options, **kwargs)
|
|
132
|
+
|
|
133
|
+
def _build_command(self) -> list[str]:
|
|
134
|
+
"""Build the Darglint command.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
list[str]: List of command arguments.
|
|
138
|
+
"""
|
|
139
|
+
cmd: list[str] = ["darglint"]
|
|
140
|
+
|
|
141
|
+
# Add configuration options
|
|
142
|
+
if self.options.get("ignore"):
|
|
143
|
+
cmd.extend(["--ignore", ",".join(self.options["ignore"])])
|
|
144
|
+
if self.options.get("ignore_regex"):
|
|
145
|
+
cmd.extend(["--ignore-regex", self.options["ignore_regex"]])
|
|
146
|
+
if self.options.get("ignore_syntax"):
|
|
147
|
+
cmd.append("--ignore-syntax")
|
|
148
|
+
# Remove message_template override to use default output
|
|
149
|
+
# if self.options.get("message_template"):
|
|
150
|
+
# cmd.extend(["--message-template", self.options["message_template"]])
|
|
151
|
+
if self.options.get("verbosity"):
|
|
152
|
+
cmd.extend(["--verbosity", str(self.options["verbosity"])])
|
|
153
|
+
if self.options.get("strictness"):
|
|
154
|
+
cmd.extend(["--strictness", self.options["strictness"]])
|
|
155
|
+
|
|
156
|
+
return cmd
|
|
157
|
+
|
|
158
|
+
def check(
|
|
159
|
+
self,
|
|
160
|
+
paths: list[str],
|
|
161
|
+
) -> ToolResult:
|
|
162
|
+
"""Check Python files for docstring issues with Darglint.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
paths: list[str]: List of file or directory paths to check.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
ToolResult: ToolResult instance.
|
|
169
|
+
"""
|
|
170
|
+
self._validate_paths(paths=paths)
|
|
171
|
+
if not paths:
|
|
172
|
+
return ToolResult(
|
|
173
|
+
name=self.name,
|
|
174
|
+
success=True,
|
|
175
|
+
output="No files to check.",
|
|
176
|
+
issues_count=0,
|
|
177
|
+
)
|
|
178
|
+
# Use shared utility for file discovery
|
|
179
|
+
python_files: list[str] = walk_files_with_excludes(
|
|
180
|
+
paths=paths,
|
|
181
|
+
file_patterns=self.config.file_patterns,
|
|
182
|
+
exclude_patterns=self.exclude_patterns,
|
|
183
|
+
include_venv=self.include_venv,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
logger.debug(f"Files to check: {python_files}")
|
|
187
|
+
|
|
188
|
+
timeout: int = self.options.get("timeout", DARGLINT_DEFAULT_TIMEOUT)
|
|
189
|
+
all_outputs: list[str] = []
|
|
190
|
+
all_success: bool = True
|
|
191
|
+
skipped_files: list[str] = []
|
|
192
|
+
total_issues: int = 0
|
|
193
|
+
|
|
194
|
+
for file_path in python_files:
|
|
195
|
+
cmd: list[str] = self._build_command() + [str(file_path)]
|
|
196
|
+
try:
|
|
197
|
+
success: bool
|
|
198
|
+
output: str
|
|
199
|
+
success, output = self._run_subprocess(cmd=cmd, timeout=timeout)
|
|
200
|
+
issues = parse_darglint_output(output=output)
|
|
201
|
+
issues_count: int = len(issues)
|
|
202
|
+
if not (success and issues_count == 0):
|
|
203
|
+
all_success = False
|
|
204
|
+
total_issues += issues_count
|
|
205
|
+
# Store parsed issues on the aggregate result later via ToolResult
|
|
206
|
+
all_outputs.append(output)
|
|
207
|
+
except subprocess.TimeoutExpired:
|
|
208
|
+
skipped_files.append(file_path)
|
|
209
|
+
all_success = False
|
|
210
|
+
except Exception as e:
|
|
211
|
+
all_outputs.append(f"Error processing {file_path}: {str(e)}")
|
|
212
|
+
all_success = False
|
|
213
|
+
|
|
214
|
+
output: str = "\n".join(all_outputs)
|
|
215
|
+
if skipped_files:
|
|
216
|
+
output += f"\n\nSkipped {len(skipped_files)} files due to timeout:"
|
|
217
|
+
for file in skipped_files:
|
|
218
|
+
output += f"\n - {file}"
|
|
219
|
+
|
|
220
|
+
if not output:
|
|
221
|
+
output = None
|
|
222
|
+
|
|
223
|
+
return ToolResult(
|
|
224
|
+
name=self.name,
|
|
225
|
+
success=all_success,
|
|
226
|
+
output=output,
|
|
227
|
+
issues_count=total_issues,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def fix(
|
|
231
|
+
self,
|
|
232
|
+
paths: list[str],
|
|
233
|
+
) -> ToolResult:
|
|
234
|
+
"""Darglint cannot fix issues, only report them.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
paths: list[str]: List of file or directory paths to fix.
|
|
238
|
+
|
|
239
|
+
Raises:
|
|
240
|
+
NotImplementedError: As Darglint does not support fixing issues.
|
|
241
|
+
"""
|
|
242
|
+
raise NotImplementedError(
|
|
243
|
+
"Darglint cannot automatically fix issues. Run 'lintro check' to see "
|
|
244
|
+
"issues.",
|
|
245
|
+
)
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""Hadolint Dockerfile linter integration."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
from lintro.enums.hadolint_enums import (
|
|
9
|
+
HadolintFailureThreshold,
|
|
10
|
+
HadolintFormat,
|
|
11
|
+
normalize_hadolint_format,
|
|
12
|
+
normalize_hadolint_threshold,
|
|
13
|
+
)
|
|
14
|
+
from lintro.enums.tool_type import ToolType
|
|
15
|
+
from lintro.models.core.tool import ToolConfig, ToolResult
|
|
16
|
+
from lintro.parsers.hadolint.hadolint_parser import parse_hadolint_output
|
|
17
|
+
from lintro.tools.core.tool_base import BaseTool
|
|
18
|
+
from lintro.utils.tool_utils import walk_files_with_excludes
|
|
19
|
+
|
|
20
|
+
# Constants for Hadolint configuration
|
|
21
|
+
HADOLINT_DEFAULT_TIMEOUT: int = 30
|
|
22
|
+
HADOLINT_DEFAULT_PRIORITY: int = 50
|
|
23
|
+
HADOLINT_FILE_PATTERNS: list[str] = ["Dockerfile", "Dockerfile.*"]
|
|
24
|
+
HADOLINT_DEFAULT_FORMAT: str = "tty"
|
|
25
|
+
HADOLINT_DEFAULT_FAILURE_THRESHOLD: str = "info"
|
|
26
|
+
HADOLINT_DEFAULT_NO_COLOR: bool = True
|
|
27
|
+
HADOLINT_FORMATS: tuple[str, ...] = tuple(m.name.lower() for m in HadolintFormat)
|
|
28
|
+
HADOLINT_FAILURE_THRESHOLDS: tuple[str, ...] = tuple(
|
|
29
|
+
m.name.lower() for m in HadolintFailureThreshold
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class HadolintTool(BaseTool):
|
|
35
|
+
"""Hadolint Dockerfile linter integration.
|
|
36
|
+
|
|
37
|
+
Hadolint is a Dockerfile linter that helps you build best practice Docker images.
|
|
38
|
+
It parses the Dockerfile into an AST and performs rules on top of the AST.
|
|
39
|
+
It also uses ShellCheck to lint the Bash code inside RUN instructions.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
name: str: Tool name.
|
|
43
|
+
description: str: Tool description.
|
|
44
|
+
can_fix: bool: Whether the tool can fix issues (hadolint cannot fix issues).
|
|
45
|
+
config: ToolConfig: Tool configuration.
|
|
46
|
+
exclude_patterns: list[str]: List of patterns to exclude.
|
|
47
|
+
include_venv: bool: Whether to include virtual environment files.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
name: str = "hadolint"
|
|
51
|
+
description: str = (
|
|
52
|
+
"Dockerfile linter that helps you build best practice Docker images"
|
|
53
|
+
)
|
|
54
|
+
can_fix: bool = False # Hadolint can only check, not fix
|
|
55
|
+
config: ToolConfig = field(
|
|
56
|
+
default_factory=lambda: ToolConfig(
|
|
57
|
+
priority=HADOLINT_DEFAULT_PRIORITY, # Medium priority for \
|
|
58
|
+
# infrastructure linting
|
|
59
|
+
conflicts_with=[], # No direct conflicts
|
|
60
|
+
file_patterns=HADOLINT_FILE_PATTERNS,
|
|
61
|
+
tool_type=ToolType.LINTER | ToolType.INFRASTRUCTURE,
|
|
62
|
+
options={
|
|
63
|
+
"timeout": HADOLINT_DEFAULT_TIMEOUT, # Default timeout in seconds
|
|
64
|
+
"format": HADOLINT_DEFAULT_FORMAT, # Output format (tty, json, \
|
|
65
|
+
# checkstyle, etc.)
|
|
66
|
+
"failure_threshold": HADOLINT_DEFAULT_FAILURE_THRESHOLD, # \
|
|
67
|
+
# Threshold for failure (error, warning, info, style)
|
|
68
|
+
"ignore": None, # List of rule codes to ignore
|
|
69
|
+
"trusted_registries": None, # List of trusted Docker registries
|
|
70
|
+
"require_labels": None, # List of required labels with schemas
|
|
71
|
+
"strict_labels": False, # Whether to use strict label checking
|
|
72
|
+
"no_fail": False, # Whether to suppress exit codes
|
|
73
|
+
"no_color": HADOLINT_DEFAULT_NO_COLOR, # Disable color output \
|
|
74
|
+
# for parsing
|
|
75
|
+
},
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def set_options(
|
|
80
|
+
self,
|
|
81
|
+
format: str | HadolintFormat | None = None,
|
|
82
|
+
failure_threshold: str | HadolintFailureThreshold | None = None,
|
|
83
|
+
ignore: list[str] | None = None,
|
|
84
|
+
trusted_registries: list[str] | None = None,
|
|
85
|
+
require_labels: list[str] | None = None,
|
|
86
|
+
strict_labels: bool | None = None,
|
|
87
|
+
no_fail: bool | None = None,
|
|
88
|
+
no_color: bool | None = None,
|
|
89
|
+
**kwargs,
|
|
90
|
+
) -> None:
|
|
91
|
+
"""Set Hadolint-specific options.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
format: str | None: Output format (tty, json, checkstyle, codeclimate, \
|
|
95
|
+
etc.).
|
|
96
|
+
failure_threshold: str | None: Exit with failure only when rules with \
|
|
97
|
+
severity >= threshold.
|
|
98
|
+
ignore: list[str] | None: List of rule codes to ignore (e.g., \
|
|
99
|
+
['DL3006', 'SC2086']).
|
|
100
|
+
trusted_registries: list[str] | None: List of trusted Docker registries.
|
|
101
|
+
require_labels: list[str] | None: List of required labels with schemas \
|
|
102
|
+
(e.g., ['version:semver']).
|
|
103
|
+
strict_labels: bool | None: Whether to use strict label checking.
|
|
104
|
+
no_fail: bool | None: Whether to suppress exit codes.
|
|
105
|
+
no_color: bool | None: Whether to disable color output.
|
|
106
|
+
**kwargs: Other tool options.
|
|
107
|
+
|
|
108
|
+
Raises:
|
|
109
|
+
ValueError: If an option value is invalid.
|
|
110
|
+
"""
|
|
111
|
+
if format is not None:
|
|
112
|
+
fmt_enum = normalize_hadolint_format(format) # type: ignore[arg-type]
|
|
113
|
+
format = fmt_enum.name.lower()
|
|
114
|
+
|
|
115
|
+
if failure_threshold is not None:
|
|
116
|
+
thr_enum = normalize_hadolint_threshold( # type: ignore[arg-type]
|
|
117
|
+
failure_threshold,
|
|
118
|
+
)
|
|
119
|
+
failure_threshold = thr_enum.name.lower()
|
|
120
|
+
|
|
121
|
+
if ignore is not None and not isinstance(ignore, list):
|
|
122
|
+
raise ValueError("ignore must be a list of rule codes")
|
|
123
|
+
|
|
124
|
+
if trusted_registries is not None and not isinstance(trusted_registries, list):
|
|
125
|
+
raise ValueError("trusted_registries must be a list of registry URLs")
|
|
126
|
+
|
|
127
|
+
if require_labels is not None and not isinstance(require_labels, list):
|
|
128
|
+
raise ValueError("require_labels must be a list of label schemas")
|
|
129
|
+
|
|
130
|
+
if strict_labels is not None and not isinstance(strict_labels, bool):
|
|
131
|
+
raise ValueError("strict_labels must be a boolean")
|
|
132
|
+
|
|
133
|
+
if no_fail is not None and not isinstance(no_fail, bool):
|
|
134
|
+
raise ValueError("no_fail must be a boolean")
|
|
135
|
+
|
|
136
|
+
if no_color is not None and not isinstance(no_color, bool):
|
|
137
|
+
raise ValueError("no_color must be a boolean")
|
|
138
|
+
|
|
139
|
+
options: dict = {
|
|
140
|
+
"format": format,
|
|
141
|
+
"failure_threshold": failure_threshold,
|
|
142
|
+
"ignore": ignore,
|
|
143
|
+
"trusted_registries": trusted_registries,
|
|
144
|
+
"require_labels": require_labels,
|
|
145
|
+
"strict_labels": strict_labels,
|
|
146
|
+
"no_fail": no_fail,
|
|
147
|
+
"no_color": no_color,
|
|
148
|
+
}
|
|
149
|
+
# Remove None values
|
|
150
|
+
options = {k: v for k, v in options.items() if v is not None}
|
|
151
|
+
super().set_options(**options, **kwargs)
|
|
152
|
+
|
|
153
|
+
def _build_command(self) -> list[str]:
|
|
154
|
+
"""Build the hadolint command.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
list[str]: List of command arguments.
|
|
158
|
+
"""
|
|
159
|
+
cmd: list[str] = ["hadolint"]
|
|
160
|
+
|
|
161
|
+
# Add format option
|
|
162
|
+
format_option: str = self.options.get("format", HADOLINT_DEFAULT_FORMAT)
|
|
163
|
+
cmd.extend(["--format", format_option])
|
|
164
|
+
|
|
165
|
+
# Add failure threshold
|
|
166
|
+
failure_threshold: str = self.options.get(
|
|
167
|
+
"failure_threshold",
|
|
168
|
+
HADOLINT_DEFAULT_FAILURE_THRESHOLD,
|
|
169
|
+
)
|
|
170
|
+
cmd.extend(["--failure-threshold", failure_threshold])
|
|
171
|
+
|
|
172
|
+
# Add ignore rules
|
|
173
|
+
ignore_rules: list[str] | None = self.options.get("ignore")
|
|
174
|
+
if ignore_rules is None:
|
|
175
|
+
ignore_rules = []
|
|
176
|
+
for rule in ignore_rules:
|
|
177
|
+
cmd.extend(["--ignore", rule])
|
|
178
|
+
|
|
179
|
+
# Add trusted registries
|
|
180
|
+
trusted_registries: list[str] | None = self.options.get("trusted_registries")
|
|
181
|
+
if trusted_registries is None:
|
|
182
|
+
trusted_registries = []
|
|
183
|
+
for registry in trusted_registries:
|
|
184
|
+
cmd.extend(["--trusted-registry", registry])
|
|
185
|
+
|
|
186
|
+
# Add required labels
|
|
187
|
+
require_labels: list[str] | None = self.options.get("require_labels")
|
|
188
|
+
if require_labels is None:
|
|
189
|
+
require_labels = []
|
|
190
|
+
for label in require_labels:
|
|
191
|
+
cmd.extend(["--require-label", label])
|
|
192
|
+
|
|
193
|
+
# Add strict labels
|
|
194
|
+
if self.options.get("strict_labels", False):
|
|
195
|
+
cmd.append("--strict-labels")
|
|
196
|
+
|
|
197
|
+
# Add no-fail option
|
|
198
|
+
if self.options.get("no_fail", False):
|
|
199
|
+
cmd.append("--no-fail")
|
|
200
|
+
|
|
201
|
+
# Add no-color option (default to True for better parsing)
|
|
202
|
+
if self.options.get("no_color", HADOLINT_DEFAULT_NO_COLOR):
|
|
203
|
+
cmd.append("--no-color")
|
|
204
|
+
|
|
205
|
+
return cmd
|
|
206
|
+
|
|
207
|
+
def check(
|
|
208
|
+
self,
|
|
209
|
+
paths: list[str],
|
|
210
|
+
) -> ToolResult:
|
|
211
|
+
"""Check files with Hadolint.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
paths: list[str]: List of file or directory paths to check.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
ToolResult: ToolResult instance.
|
|
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
|
+
# Use shared utility for file discovery
|
|
229
|
+
dockerfile_files: list[str] = walk_files_with_excludes(
|
|
230
|
+
paths=paths,
|
|
231
|
+
file_patterns=self.config.file_patterns,
|
|
232
|
+
exclude_patterns=self.exclude_patterns,
|
|
233
|
+
include_venv=self.include_venv,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
logger.debug(f"Files to check: {dockerfile_files}")
|
|
237
|
+
|
|
238
|
+
timeout: int = self.options.get("timeout", HADOLINT_DEFAULT_TIMEOUT)
|
|
239
|
+
all_outputs: list[str] = []
|
|
240
|
+
all_issues: list = []
|
|
241
|
+
all_success: bool = True
|
|
242
|
+
skipped_files: list[str] = []
|
|
243
|
+
total_issues: int = 0
|
|
244
|
+
|
|
245
|
+
for file_path in dockerfile_files:
|
|
246
|
+
cmd: list[str] = self._build_command() + [str(file_path)]
|
|
247
|
+
try:
|
|
248
|
+
success: bool
|
|
249
|
+
output: str
|
|
250
|
+
success, output = self._run_subprocess(cmd=cmd, timeout=timeout)
|
|
251
|
+
issues = parse_hadolint_output(output=output)
|
|
252
|
+
issues_count: int = len(issues)
|
|
253
|
+
# Tool is successful if subprocess succeeds, regardless of issues found
|
|
254
|
+
if not success:
|
|
255
|
+
all_success = False
|
|
256
|
+
total_issues += issues_count
|
|
257
|
+
# Prefer parsed issues for formatted output; keep raw for metadata
|
|
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
|
|
267
|
+
|
|
268
|
+
output: str = "\n".join(all_outputs) if all_outputs else ""
|
|
269
|
+
if skipped_files:
|
|
270
|
+
if output:
|
|
271
|
+
output += "\n\n"
|
|
272
|
+
output += f"Skipped {len(skipped_files)} files due to timeout:"
|
|
273
|
+
for file in skipped_files:
|
|
274
|
+
output += f"\n - {file}"
|
|
275
|
+
|
|
276
|
+
if not output.strip():
|
|
277
|
+
output = None
|
|
278
|
+
|
|
279
|
+
return ToolResult(
|
|
280
|
+
name=self.name,
|
|
281
|
+
success=all_success,
|
|
282
|
+
output=output,
|
|
283
|
+
issues_count=total_issues,
|
|
284
|
+
issues=all_issues,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
def fix(
|
|
288
|
+
self,
|
|
289
|
+
paths: list[str],
|
|
290
|
+
) -> ToolResult:
|
|
291
|
+
"""Hadolint cannot fix issues, only report them.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
paths: list[str]: List of file or directory paths to fix.
|
|
295
|
+
|
|
296
|
+
Raises:
|
|
297
|
+
NotImplementedError: As Hadolint does not support fixing issues.
|
|
298
|
+
"""
|
|
299
|
+
raise NotImplementedError(
|
|
300
|
+
"Hadolint cannot automatically fix issues. Run 'lintro check' to see "
|
|
301
|
+
"issues.",
|
|
302
|
+
)
|