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,618 @@
|
|
|
1
|
+
"""Ruff Python linter and formatter integration."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import tomllib
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from loguru import logger
|
|
9
|
+
|
|
10
|
+
from lintro.enums.tool_type import ToolType
|
|
11
|
+
from lintro.models.core.tool import ToolConfig, ToolResult
|
|
12
|
+
from lintro.parsers.ruff.ruff_issue import RuffFormatIssue
|
|
13
|
+
from lintro.parsers.ruff.ruff_parser import (
|
|
14
|
+
parse_ruff_format_check_output,
|
|
15
|
+
parse_ruff_output,
|
|
16
|
+
)
|
|
17
|
+
from lintro.tools.core.tool_base import BaseTool
|
|
18
|
+
from lintro.utils.tool_utils import walk_files_with_excludes
|
|
19
|
+
|
|
20
|
+
# Constants for Ruff configuration
|
|
21
|
+
RUFF_DEFAULT_TIMEOUT: int = 30
|
|
22
|
+
RUFF_DEFAULT_PRIORITY: int = 85
|
|
23
|
+
RUFF_FILE_PATTERNS: list[str] = ["*.py", "*.pyi"]
|
|
24
|
+
RUFF_OUTPUT_FORMAT: str = "json"
|
|
25
|
+
RUFF_TEST_MODE_ENV: str = "LINTRO_TEST_MODE"
|
|
26
|
+
RUFF_TEST_MODE_VALUE: str = "1"
|
|
27
|
+
DEFAULT_REMAINING_ISSUES_DISPLAY: int = 5
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _load_ruff_config() -> dict:
|
|
31
|
+
"""Load ruff configuration from pyproject.toml.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
dict: Ruff configuration dictionary.
|
|
35
|
+
"""
|
|
36
|
+
config: dict = {}
|
|
37
|
+
pyproject_path = Path("pyproject.toml")
|
|
38
|
+
|
|
39
|
+
if pyproject_path.exists():
|
|
40
|
+
try:
|
|
41
|
+
with open(pyproject_path, "rb") as f:
|
|
42
|
+
pyproject_data = tomllib.load(f)
|
|
43
|
+
if "tool" in pyproject_data and "ruff" in pyproject_data["tool"]:
|
|
44
|
+
config = pyproject_data["tool"]["ruff"]
|
|
45
|
+
except Exception as e:
|
|
46
|
+
logger.warning(f"Failed to load ruff configuration: {e}")
|
|
47
|
+
|
|
48
|
+
return config
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _load_lintro_ignore() -> list[str]:
|
|
52
|
+
"""Load patterns from .lintro-ignore file.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
list[str]: List of ignore patterns.
|
|
56
|
+
"""
|
|
57
|
+
ignore_patterns: list[str] = []
|
|
58
|
+
lintro_ignore_path = Path(".lintro-ignore")
|
|
59
|
+
|
|
60
|
+
if lintro_ignore_path.exists():
|
|
61
|
+
try:
|
|
62
|
+
with open(lintro_ignore_path, "r", encoding="utf-8") as f:
|
|
63
|
+
for line in f:
|
|
64
|
+
line = line.strip()
|
|
65
|
+
# Skip empty lines and comments
|
|
66
|
+
if line and not line.startswith("#"):
|
|
67
|
+
ignore_patterns.append(line)
|
|
68
|
+
except Exception as e:
|
|
69
|
+
logger.warning(f"Failed to load .lintro-ignore: {e}")
|
|
70
|
+
|
|
71
|
+
return ignore_patterns
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class RuffTool(BaseTool):
|
|
76
|
+
"""Ruff Python linter and formatter integration.
|
|
77
|
+
|
|
78
|
+
Ruff is an extremely fast Python linter and code formatter written in Rust.
|
|
79
|
+
It can replace multiple Python tools like flake8, black, isort, and more.
|
|
80
|
+
|
|
81
|
+
Attributes:
|
|
82
|
+
name: str: Tool name.
|
|
83
|
+
description: str: Tool description.
|
|
84
|
+
can_fix: bool: Whether the tool can fix issues.
|
|
85
|
+
config: ToolConfig: Tool configuration.
|
|
86
|
+
exclude_patterns: list[str]: List of patterns to exclude.
|
|
87
|
+
include_venv: bool: Whether to include virtual environment files.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
name: str = "ruff"
|
|
91
|
+
description: str = (
|
|
92
|
+
"Extremely fast Python linter and formatter that replaces multiple tools"
|
|
93
|
+
)
|
|
94
|
+
can_fix: bool = True # Ruff can both check and fix issues
|
|
95
|
+
config: ToolConfig = field(
|
|
96
|
+
default_factory=lambda: ToolConfig(
|
|
97
|
+
priority=RUFF_DEFAULT_PRIORITY, # High priority, higher than most linters
|
|
98
|
+
conflicts_with=[], # Can work alongside other tools
|
|
99
|
+
file_patterns=RUFF_FILE_PATTERNS, # Python files only
|
|
100
|
+
tool_type=ToolType.LINTER | ToolType.FORMATTER, # Both linter and formatter
|
|
101
|
+
options={
|
|
102
|
+
"timeout": RUFF_DEFAULT_TIMEOUT, # Default timeout in seconds
|
|
103
|
+
"select": None, # Rules to enable
|
|
104
|
+
"ignore": None, # Rules to ignore
|
|
105
|
+
"extend_select": None, # Additional rules to enable
|
|
106
|
+
"extend_ignore": None, # Additional rules to ignore
|
|
107
|
+
"line_length": None, # Line length limit
|
|
108
|
+
"target_version": None, # Python version target
|
|
109
|
+
"fix_only": False, # Only apply fixes, don't report remaining issues
|
|
110
|
+
"unsafe_fixes": False, # Do NOT enable unsafe fixes by default
|
|
111
|
+
"show_fixes": False, # Show enumeration of fixes applied
|
|
112
|
+
# Wrapper-first defaults:
|
|
113
|
+
# format_check: include `ruff format --check` during check
|
|
114
|
+
# format: run `ruff format` during fix
|
|
115
|
+
# Default True: `lintro chk` runs formatting and lint checks.
|
|
116
|
+
"format_check": True,
|
|
117
|
+
# Default to running the formatter during fmt to apply
|
|
118
|
+
# reformatting along with lint fixes
|
|
119
|
+
"format": True,
|
|
120
|
+
# Allow disabling the lint-fix stage if users only want
|
|
121
|
+
# formatting changes
|
|
122
|
+
"lint_fix": True,
|
|
123
|
+
},
|
|
124
|
+
),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def __post_init__(self) -> None:
|
|
128
|
+
"""Initialize the tool with default configuration."""
|
|
129
|
+
super().__post_init__()
|
|
130
|
+
# Load ruff configuration from pyproject.toml
|
|
131
|
+
ruff_config = _load_ruff_config()
|
|
132
|
+
|
|
133
|
+
# Load .lintro-ignore patterns
|
|
134
|
+
lintro_ignore_patterns = _load_lintro_ignore()
|
|
135
|
+
|
|
136
|
+
# Update exclude patterns from configuration and .lintro-ignore
|
|
137
|
+
if "exclude" in ruff_config:
|
|
138
|
+
self.exclude_patterns.extend(ruff_config["exclude"])
|
|
139
|
+
if lintro_ignore_patterns:
|
|
140
|
+
self.exclude_patterns.extend(lintro_ignore_patterns)
|
|
141
|
+
|
|
142
|
+
# Update other options from configuration
|
|
143
|
+
if "line_length" in ruff_config:
|
|
144
|
+
self.options["line_length"] = ruff_config["line_length"]
|
|
145
|
+
if "target_version" in ruff_config:
|
|
146
|
+
self.options["target_version"] = ruff_config["target_version"]
|
|
147
|
+
if "select" in ruff_config:
|
|
148
|
+
self.options["select"] = ruff_config["select"]
|
|
149
|
+
if "ignore" in ruff_config:
|
|
150
|
+
self.options["ignore"] = ruff_config["ignore"]
|
|
151
|
+
if "unsafe_fixes" in ruff_config:
|
|
152
|
+
self.options["unsafe_fixes"] = ruff_config["unsafe_fixes"]
|
|
153
|
+
|
|
154
|
+
def set_options(
|
|
155
|
+
self,
|
|
156
|
+
select: list[str] | None = None,
|
|
157
|
+
ignore: list[str] | None = None,
|
|
158
|
+
extend_select: list[str] | None = None,
|
|
159
|
+
extend_ignore: list[str] | None = None,
|
|
160
|
+
line_length: int | None = None,
|
|
161
|
+
target_version: str | None = None,
|
|
162
|
+
fix_only: bool | None = None,
|
|
163
|
+
unsafe_fixes: bool | None = None,
|
|
164
|
+
show_fixes: bool | None = None,
|
|
165
|
+
format: bool | None = None,
|
|
166
|
+
lint_fix: bool | None = None,
|
|
167
|
+
format_check: bool | None = None,
|
|
168
|
+
**kwargs,
|
|
169
|
+
) -> None:
|
|
170
|
+
"""Set Ruff-specific options.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
select: list[str] | None: Rules to enable.
|
|
174
|
+
ignore: list[str] | None: Rules to ignore.
|
|
175
|
+
extend_select: list[str] | None: Additional rules to enable.
|
|
176
|
+
extend_ignore: list[str] | None: Additional rules to ignore.
|
|
177
|
+
line_length: int | None: Line length limit.
|
|
178
|
+
target_version: str | None: Python version target.
|
|
179
|
+
fix_only: bool | None: Only apply fixes, don't report remaining issues.
|
|
180
|
+
unsafe_fixes: bool | None: Include unsafe fixes.
|
|
181
|
+
show_fixes: bool | None: Show enumeration of fixes applied.
|
|
182
|
+
format: bool | None: Whether to run `ruff format` during fix.
|
|
183
|
+
lint_fix: bool | None: Whether to run `ruff check --fix` during fix.
|
|
184
|
+
format_check: bool | None: Whether to run `ruff format --check` in check.
|
|
185
|
+
**kwargs: Other tool options.
|
|
186
|
+
|
|
187
|
+
Raises:
|
|
188
|
+
ValueError: If an option value is invalid.
|
|
189
|
+
"""
|
|
190
|
+
if select is not None and not isinstance(select, list):
|
|
191
|
+
raise ValueError("select must be a list of rule codes")
|
|
192
|
+
if ignore is not None and not isinstance(ignore, list):
|
|
193
|
+
raise ValueError("ignore must be a list of rule codes")
|
|
194
|
+
if extend_select is not None and not isinstance(extend_select, list):
|
|
195
|
+
raise ValueError("extend_select must be a list of rule codes")
|
|
196
|
+
if extend_ignore is not None and not isinstance(extend_ignore, list):
|
|
197
|
+
raise ValueError("extend_ignore must be a list of rule codes")
|
|
198
|
+
if line_length is not None:
|
|
199
|
+
if not isinstance(line_length, int):
|
|
200
|
+
raise ValueError("line_length must be an integer")
|
|
201
|
+
if line_length <= 0:
|
|
202
|
+
raise ValueError("line_length must be positive")
|
|
203
|
+
if target_version is not None and not isinstance(target_version, str):
|
|
204
|
+
raise ValueError("target_version must be a string")
|
|
205
|
+
if fix_only is not None and not isinstance(fix_only, bool):
|
|
206
|
+
raise ValueError("fix_only must be a boolean")
|
|
207
|
+
if unsafe_fixes is not None and not isinstance(unsafe_fixes, bool):
|
|
208
|
+
raise ValueError("unsafe_fixes must be a boolean")
|
|
209
|
+
if show_fixes is not None and not isinstance(show_fixes, bool):
|
|
210
|
+
raise ValueError("show_fixes must be a boolean")
|
|
211
|
+
if format is not None and not isinstance(format, bool):
|
|
212
|
+
raise ValueError("format must be a boolean")
|
|
213
|
+
if format_check is not None and not isinstance(format_check, bool):
|
|
214
|
+
raise ValueError("format_check must be a boolean")
|
|
215
|
+
|
|
216
|
+
options: dict = {
|
|
217
|
+
"select": select,
|
|
218
|
+
"ignore": ignore,
|
|
219
|
+
"extend_select": extend_select,
|
|
220
|
+
"extend_ignore": extend_ignore,
|
|
221
|
+
"line_length": line_length,
|
|
222
|
+
"target_version": target_version,
|
|
223
|
+
"fix_only": fix_only,
|
|
224
|
+
"unsafe_fixes": unsafe_fixes,
|
|
225
|
+
"show_fixes": show_fixes,
|
|
226
|
+
"format": format,
|
|
227
|
+
"lint_fix": lint_fix,
|
|
228
|
+
"format_check": format_check,
|
|
229
|
+
}
|
|
230
|
+
# Remove None values
|
|
231
|
+
options = {k: v for k, v in options.items() if v is not None}
|
|
232
|
+
super().set_options(**options, **kwargs)
|
|
233
|
+
|
|
234
|
+
def _build_check_command(
|
|
235
|
+
self,
|
|
236
|
+
files: list[str],
|
|
237
|
+
fix: bool = False,
|
|
238
|
+
) -> list[str]:
|
|
239
|
+
"""Build the ruff check command.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
files: list[str]: List of files to check.
|
|
243
|
+
fix: bool: Whether to apply fixes.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
list[str]: List of command arguments.
|
|
247
|
+
"""
|
|
248
|
+
cmd: list[str] = self._get_executable_command(tool_name="ruff") + ["check"]
|
|
249
|
+
|
|
250
|
+
# Add --isolated if in test mode
|
|
251
|
+
if os.environ.get(RUFF_TEST_MODE_ENV) == RUFF_TEST_MODE_VALUE:
|
|
252
|
+
cmd.append("--isolated")
|
|
253
|
+
|
|
254
|
+
# Add configuration options
|
|
255
|
+
if self.options.get("select"):
|
|
256
|
+
cmd.extend(["--select", ",".join(self.options["select"])])
|
|
257
|
+
if self.options.get("ignore"):
|
|
258
|
+
cmd.extend(["--ignore", ",".join(self.options["ignore"])])
|
|
259
|
+
if self.options.get("extend_select"):
|
|
260
|
+
cmd.extend(["--extend-select", ",".join(self.options["extend_select"])])
|
|
261
|
+
if self.options.get("extend_ignore"):
|
|
262
|
+
cmd.extend(["--extend-ignore", ",".join(self.options["extend_ignore"])])
|
|
263
|
+
if self.options.get("line_length"):
|
|
264
|
+
cmd.extend(["--line-length", str(self.options["line_length"])])
|
|
265
|
+
if self.options.get("target_version"):
|
|
266
|
+
cmd.extend(["--target-version", self.options["target_version"]])
|
|
267
|
+
|
|
268
|
+
# Fix options
|
|
269
|
+
if fix:
|
|
270
|
+
cmd.append("--fix")
|
|
271
|
+
if self.options.get("unsafe_fixes"):
|
|
272
|
+
cmd.append("--unsafe-fixes")
|
|
273
|
+
if self.options.get("show_fixes"):
|
|
274
|
+
cmd.append("--show-fixes")
|
|
275
|
+
if self.options.get("fix_only"):
|
|
276
|
+
cmd.append("--fix-only")
|
|
277
|
+
|
|
278
|
+
# Output format
|
|
279
|
+
cmd.extend(["--output-format", RUFF_OUTPUT_FORMAT])
|
|
280
|
+
|
|
281
|
+
# Add files
|
|
282
|
+
cmd.extend(files)
|
|
283
|
+
|
|
284
|
+
return cmd
|
|
285
|
+
|
|
286
|
+
def _build_format_command(
|
|
287
|
+
self,
|
|
288
|
+
files: list[str],
|
|
289
|
+
check_only: bool = False,
|
|
290
|
+
) -> list[str]:
|
|
291
|
+
"""Build the ruff format command.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
files: list[str]: List of files to format.
|
|
295
|
+
check_only: bool: Whether to only check formatting without applying changes.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
list[str]: List of command arguments.
|
|
299
|
+
"""
|
|
300
|
+
cmd: list[str] = self._get_executable_command(tool_name="ruff") + ["format"]
|
|
301
|
+
|
|
302
|
+
if check_only:
|
|
303
|
+
cmd.append("--check")
|
|
304
|
+
|
|
305
|
+
# Add configuration options
|
|
306
|
+
if self.options.get("line_length"):
|
|
307
|
+
cmd.extend(["--line-length", str(self.options["line_length"])])
|
|
308
|
+
if self.options.get("target_version"):
|
|
309
|
+
cmd.extend(["--target-version", self.options["target_version"]])
|
|
310
|
+
|
|
311
|
+
# Add files
|
|
312
|
+
cmd.extend(files)
|
|
313
|
+
|
|
314
|
+
return cmd
|
|
315
|
+
|
|
316
|
+
def check(
|
|
317
|
+
self,
|
|
318
|
+
paths: list[str],
|
|
319
|
+
) -> ToolResult:
|
|
320
|
+
"""Check files with Ruff (lint only by default).
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
paths: list[str]: List of file or directory paths to check.
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
ToolResult: ToolResult instance.
|
|
327
|
+
"""
|
|
328
|
+
self._validate_paths(paths=paths)
|
|
329
|
+
if not paths:
|
|
330
|
+
return ToolResult(
|
|
331
|
+
name=self.name,
|
|
332
|
+
success=True,
|
|
333
|
+
output="No files to check.",
|
|
334
|
+
issues_count=0,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# Use shared utility for file discovery
|
|
338
|
+
python_files: list[str] = walk_files_with_excludes(
|
|
339
|
+
paths=paths,
|
|
340
|
+
file_patterns=self.config.file_patterns,
|
|
341
|
+
exclude_patterns=self.exclude_patterns,
|
|
342
|
+
include_venv=self.include_venv,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
if not python_files:
|
|
346
|
+
return ToolResult(
|
|
347
|
+
name=self.name,
|
|
348
|
+
success=True,
|
|
349
|
+
output="No Python files found to check.",
|
|
350
|
+
issues_count=0,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
logger.debug(f"Files to check: {python_files}")
|
|
354
|
+
|
|
355
|
+
# Ensure Ruff discovers the correct configuration by setting the
|
|
356
|
+
# working directory to the common parent of the target files and by
|
|
357
|
+
# passing file paths relative to that directory.
|
|
358
|
+
cwd: str | None = self.get_cwd(paths=python_files)
|
|
359
|
+
rel_files: list[str] = [
|
|
360
|
+
os.path.relpath(f, cwd) if cwd else f for f in python_files
|
|
361
|
+
]
|
|
362
|
+
|
|
363
|
+
timeout: int = self.options.get("timeout", RUFF_DEFAULT_TIMEOUT)
|
|
364
|
+
# Lint check
|
|
365
|
+
cmd: list[str] = self._build_check_command(files=rel_files, fix=False)
|
|
366
|
+
success_lint: bool
|
|
367
|
+
output_lint: str
|
|
368
|
+
success_lint, output_lint = self._run_subprocess(
|
|
369
|
+
cmd=cmd, timeout=timeout, cwd=cwd
|
|
370
|
+
)
|
|
371
|
+
lint_issues = parse_ruff_output(output=output_lint)
|
|
372
|
+
lint_issues_count: int = len(lint_issues)
|
|
373
|
+
|
|
374
|
+
# Optional format check via `format_check` flag
|
|
375
|
+
format_issues_count: int = 0
|
|
376
|
+
format_files: list[str] = []
|
|
377
|
+
format_issues: list[RuffFormatIssue] = []
|
|
378
|
+
if self.options.get("format_check", False):
|
|
379
|
+
format_cmd: list[str] = self._build_format_command(
|
|
380
|
+
files=rel_files,
|
|
381
|
+
check_only=True,
|
|
382
|
+
)
|
|
383
|
+
success_format: bool
|
|
384
|
+
output_format: str
|
|
385
|
+
success_format, output_format = self._run_subprocess(
|
|
386
|
+
cmd=format_cmd,
|
|
387
|
+
timeout=timeout,
|
|
388
|
+
cwd=cwd,
|
|
389
|
+
)
|
|
390
|
+
format_files = parse_ruff_format_check_output(output=output_format)
|
|
391
|
+
# Normalize files to absolute paths to keep behavior consistent with
|
|
392
|
+
# direct CLI calls and stabilize tests that compare exact paths.
|
|
393
|
+
normalized_files: list[str] = []
|
|
394
|
+
for file_path in format_files:
|
|
395
|
+
if cwd and not os.path.isabs(file_path):
|
|
396
|
+
absolute_path = os.path.abspath(os.path.join(cwd, file_path))
|
|
397
|
+
normalized_files.append(absolute_path)
|
|
398
|
+
else:
|
|
399
|
+
normalized_files.append(file_path)
|
|
400
|
+
format_issues_count = len(normalized_files)
|
|
401
|
+
format_issues = [RuffFormatIssue(file=file) for file in normalized_files]
|
|
402
|
+
|
|
403
|
+
# Combine results
|
|
404
|
+
issues_count: int = lint_issues_count + format_issues_count
|
|
405
|
+
success: bool = issues_count == 0
|
|
406
|
+
|
|
407
|
+
# Suppress narrative blocks; rely on standardized tables and summary lines
|
|
408
|
+
output_summary: str | None = None
|
|
409
|
+
|
|
410
|
+
# Combine linting and formatting issues for the formatters
|
|
411
|
+
all_issues = lint_issues + format_issues
|
|
412
|
+
|
|
413
|
+
return ToolResult(
|
|
414
|
+
name=self.name,
|
|
415
|
+
success=success,
|
|
416
|
+
output=output_summary,
|
|
417
|
+
issues_count=issues_count,
|
|
418
|
+
issues=all_issues,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
def fix(
|
|
422
|
+
self,
|
|
423
|
+
paths: list[str],
|
|
424
|
+
) -> ToolResult:
|
|
425
|
+
"""Fix issues in files with Ruff.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
paths: list[str]: List of file or directory paths to fix.
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
ToolResult: ToolResult instance.
|
|
432
|
+
"""
|
|
433
|
+
self._validate_paths(paths=paths)
|
|
434
|
+
if not paths:
|
|
435
|
+
return ToolResult(
|
|
436
|
+
name=self.name,
|
|
437
|
+
success=True,
|
|
438
|
+
output="No files to fix.",
|
|
439
|
+
issues_count=0,
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
# Use shared utility for file discovery
|
|
443
|
+
python_files: list[str] = walk_files_with_excludes(
|
|
444
|
+
paths=paths,
|
|
445
|
+
file_patterns=self.config.file_patterns,
|
|
446
|
+
exclude_patterns=self.exclude_patterns,
|
|
447
|
+
include_venv=self.include_venv,
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
if not python_files:
|
|
451
|
+
return ToolResult(
|
|
452
|
+
name=self.name,
|
|
453
|
+
success=True,
|
|
454
|
+
output="No Python files found to fix.",
|
|
455
|
+
issues_count=0,
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
logger.debug(f"Files to fix: {python_files}")
|
|
459
|
+
timeout: int = self.options.get("timeout", RUFF_DEFAULT_TIMEOUT)
|
|
460
|
+
all_outputs: list[str] = []
|
|
461
|
+
overall_success: bool = True
|
|
462
|
+
|
|
463
|
+
# Track unsafe fixes for internal decisioning; do not emit as user-facing noise
|
|
464
|
+
unsafe_fixes_enabled: bool = self.options.get("unsafe_fixes", False)
|
|
465
|
+
|
|
466
|
+
# First, count issues before fixing
|
|
467
|
+
cmd_check: list[str] = self._build_check_command(files=python_files, fix=False)
|
|
468
|
+
success_check: bool
|
|
469
|
+
output_check: str
|
|
470
|
+
success_check, output_check = self._run_subprocess(
|
|
471
|
+
cmd=cmd_check,
|
|
472
|
+
timeout=timeout,
|
|
473
|
+
)
|
|
474
|
+
initial_issues = parse_ruff_output(output=output_check)
|
|
475
|
+
initial_count: int = len(initial_issues)
|
|
476
|
+
|
|
477
|
+
# Also check formatting issues before fixing
|
|
478
|
+
format_cmd_check: list[str] = self._build_format_command(
|
|
479
|
+
files=python_files,
|
|
480
|
+
check_only=True,
|
|
481
|
+
)
|
|
482
|
+
success_format_check: bool
|
|
483
|
+
output_format_check: str
|
|
484
|
+
success_format_check, output_format_check = self._run_subprocess(
|
|
485
|
+
cmd=format_cmd_check,
|
|
486
|
+
timeout=timeout,
|
|
487
|
+
)
|
|
488
|
+
format_files = parse_ruff_format_check_output(output=output_format_check)
|
|
489
|
+
initial_format_count: int = len(format_files)
|
|
490
|
+
|
|
491
|
+
# Total initial issues (linting + formatting)
|
|
492
|
+
total_initial_count: int = initial_count + initial_format_count
|
|
493
|
+
|
|
494
|
+
# Optionally run ruff check --fix (lint fixes)
|
|
495
|
+
remaining_issues = []
|
|
496
|
+
remaining_count = 0
|
|
497
|
+
if self.options.get("lint_fix", True):
|
|
498
|
+
cmd: list[str] = self._build_check_command(files=python_files, fix=True)
|
|
499
|
+
success: bool
|
|
500
|
+
output: str
|
|
501
|
+
success, output = self._run_subprocess(cmd=cmd, timeout=timeout)
|
|
502
|
+
remaining_issues = parse_ruff_output(output=output)
|
|
503
|
+
remaining_count = len(remaining_issues)
|
|
504
|
+
|
|
505
|
+
# Calculate how many issues were actually fixed
|
|
506
|
+
# Add formatting fixes if formatter ran
|
|
507
|
+
fixed_count: int = total_initial_count - remaining_count
|
|
508
|
+
|
|
509
|
+
# Do not print raw initial counts; keep output concise and unified
|
|
510
|
+
|
|
511
|
+
# Do not print intermediate fixed counts; unify after formatting phase
|
|
512
|
+
|
|
513
|
+
# If there are remaining issues, check if any are fixable with unsafe fixes
|
|
514
|
+
if remaining_count > 0:
|
|
515
|
+
# If unsafe fixes are disabled, check if any remaining issues are
|
|
516
|
+
# fixable with unsafe fixes
|
|
517
|
+
if not unsafe_fixes_enabled:
|
|
518
|
+
# Try running ruff with unsafe fixes in dry-run mode to see if it
|
|
519
|
+
# would fix more
|
|
520
|
+
cmd_unsafe: list[str] = self._build_check_command(
|
|
521
|
+
files=python_files,
|
|
522
|
+
fix=True,
|
|
523
|
+
)
|
|
524
|
+
if "--unsafe-fixes" not in cmd_unsafe:
|
|
525
|
+
cmd_unsafe.append("--unsafe-fixes")
|
|
526
|
+
# Only run if not already run with unsafe fixes
|
|
527
|
+
success_unsafe: bool
|
|
528
|
+
output_unsafe: str
|
|
529
|
+
success_unsafe, output_unsafe = self._run_subprocess(
|
|
530
|
+
cmd=cmd_unsafe,
|
|
531
|
+
timeout=timeout,
|
|
532
|
+
)
|
|
533
|
+
remaining_unsafe = parse_ruff_output(output=output_unsafe)
|
|
534
|
+
if len(remaining_unsafe) < remaining_count:
|
|
535
|
+
all_outputs.append(
|
|
536
|
+
"Some remaining issues could be fixed by enabling unsafe "
|
|
537
|
+
"fixes (use --tool-options ruff:unsafe_fixes=True)",
|
|
538
|
+
)
|
|
539
|
+
all_outputs.append(
|
|
540
|
+
f"{remaining_count} issue(s) cannot be auto-fixed",
|
|
541
|
+
)
|
|
542
|
+
for issue in remaining_issues[:DEFAULT_REMAINING_ISSUES_DISPLAY]:
|
|
543
|
+
file_path: str = getattr(issue, "file", "")
|
|
544
|
+
try:
|
|
545
|
+
file_rel: str = os.path.relpath(file_path)
|
|
546
|
+
except (ValueError, TypeError):
|
|
547
|
+
file_rel = file_path
|
|
548
|
+
all_outputs.append(
|
|
549
|
+
f" {file_rel}:{getattr(issue, 'line', '?')} - "
|
|
550
|
+
f"{getattr(issue, 'message', 'Unknown issue')}",
|
|
551
|
+
)
|
|
552
|
+
if len(remaining_issues) > DEFAULT_REMAINING_ISSUES_DISPLAY:
|
|
553
|
+
all_outputs.append(
|
|
554
|
+
f" ... and "
|
|
555
|
+
f"{len(remaining_issues) - DEFAULT_REMAINING_ISSUES_DISPLAY} more",
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
if total_initial_count == 0:
|
|
559
|
+
# Avoid duplicate success messages; rely on unified logger
|
|
560
|
+
pass
|
|
561
|
+
elif remaining_count == 0 and fixed_count > 0:
|
|
562
|
+
all_outputs.append("All linting issues were successfully auto-fixed")
|
|
563
|
+
|
|
564
|
+
if not (success and remaining_count == 0):
|
|
565
|
+
overall_success = False
|
|
566
|
+
|
|
567
|
+
# Run ruff format if enabled (default: True)
|
|
568
|
+
if self.options.get("format", False):
|
|
569
|
+
format_cmd: list[str] = self._build_format_command(
|
|
570
|
+
files=python_files,
|
|
571
|
+
check_only=False,
|
|
572
|
+
)
|
|
573
|
+
format_success: bool
|
|
574
|
+
format_output: str
|
|
575
|
+
format_success, format_output = self._run_subprocess(
|
|
576
|
+
cmd=format_cmd,
|
|
577
|
+
timeout=timeout,
|
|
578
|
+
)
|
|
579
|
+
# If we detected formatting issues initially, consider them fixed now
|
|
580
|
+
if initial_format_count > 0:
|
|
581
|
+
fixed_count += initial_format_count
|
|
582
|
+
# Suppress raw formatter output for consistency; rely on unified summary
|
|
583
|
+
# Only consider formatting failure if there are actual formatting
|
|
584
|
+
# issues. Don't fail the overall operation just because formatting
|
|
585
|
+
# failed when there are no issues
|
|
586
|
+
if not format_success and total_initial_count > 0:
|
|
587
|
+
overall_success = False
|
|
588
|
+
|
|
589
|
+
# Build concise, unified summary output for fmt runs
|
|
590
|
+
summary_lines: list[str] = []
|
|
591
|
+
if fixed_count > 0:
|
|
592
|
+
summary_lines.append(f"Fixed {fixed_count} issue(s)")
|
|
593
|
+
if remaining_count > 0:
|
|
594
|
+
summary_lines.append(
|
|
595
|
+
f"Found {remaining_count} issue(s) that cannot be auto-fixed",
|
|
596
|
+
)
|
|
597
|
+
final_output: str = (
|
|
598
|
+
"\n".join(summary_lines) if summary_lines else "No fixes applied."
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
# Success should be based on whether there are remaining issues after fixing
|
|
602
|
+
# If there are no initial issues, success should be True
|
|
603
|
+
if total_initial_count == 0:
|
|
604
|
+
overall_success = True
|
|
605
|
+
else:
|
|
606
|
+
overall_success = remaining_count == 0
|
|
607
|
+
|
|
608
|
+
return ToolResult(
|
|
609
|
+
name=self.name,
|
|
610
|
+
success=overall_success,
|
|
611
|
+
output=final_output,
|
|
612
|
+
# For fix operations, issues_count represents remaining for summaries
|
|
613
|
+
issues_count=remaining_count,
|
|
614
|
+
issues=remaining_issues,
|
|
615
|
+
initial_issues_count=total_initial_count,
|
|
616
|
+
fixed_issues_count=fixed_count,
|
|
617
|
+
remaining_issues_count=remaining_count,
|
|
618
|
+
)
|