Coverage for lintro / tools / implementations / ruff / check.py: 96%
75 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Ruff check execution logic.
3Functions for running ruff check commands and processing results.
4"""
6import os
7import subprocess # nosec B404 - subprocess used safely to execute ruff commands with controlled input
8from typing import TYPE_CHECKING
10from loguru import logger
12from lintro.parsers.ruff.ruff_format_issue import RuffFormatIssue
13from lintro.parsers.ruff.ruff_parser import (
14 parse_ruff_format_check_output,
15 parse_ruff_output,
16)
17from lintro.tools.core.timeout_utils import (
18 create_timeout_result,
19 get_timeout_value,
20 run_subprocess_with_timeout,
21)
22from lintro.utils.path_filtering import walk_files_with_excludes
24if TYPE_CHECKING:
25 from lintro.models.core.tool_result import ToolResult
26 from lintro.tools.definitions.ruff import RuffPlugin
28# Default timeout for Ruff operations
29RUFF_DEFAULT_TIMEOUT: int = 30
32def execute_ruff_check(
33 tool: "RuffPlugin",
34 paths: list[str],
35) -> "ToolResult":
36 """Execute ruff check command and process results.
38 Args:
39 tool: RuffTool instance
40 paths: list[str]: List of file or directory paths to check.
42 Returns:
43 ToolResult: ToolResult instance.
44 """
45 from lintro.models.core.tool_result import ToolResult
46 from lintro.tools.implementations.ruff.commands import (
47 build_ruff_check_command,
48 build_ruff_format_command,
49 )
51 # Check version requirements
52 version_result = tool._verify_tool_version()
53 if version_result is not None:
54 return version_result
56 tool._validate_paths(paths=paths)
57 if not paths:
58 return ToolResult(
59 name=tool.definition.name,
60 success=True,
61 output="No files to check.",
62 issues_count=0,
63 )
65 # Use shared utility for file discovery
66 python_files: list[str] = walk_files_with_excludes(
67 paths=paths,
68 file_patterns=tool.definition.file_patterns,
69 exclude_patterns=tool.exclude_patterns,
70 include_venv=tool.include_venv,
71 incremental=bool(tool.options.get("incremental", False)),
72 tool_name="ruff",
73 )
75 if not python_files:
76 return ToolResult(
77 name=tool.definition.name,
78 success=True,
79 output="No Python files found to check.",
80 issues_count=0,
81 )
83 # Ensure Ruff discovers the correct configuration by setting the
84 # working directory to the common parent of the target files and by
85 # passing file paths relative to that directory.
86 cwd: str | None = tool._get_cwd(paths=python_files)
87 rel_files: list[str] = []
88 for f in python_files:
89 if cwd:
90 try:
91 # Try to get relative path; may fail on Windows
92 # if paths are on different drives
93 rel_path = os.path.relpath(f, cwd)
94 rel_files.append(rel_path)
95 except ValueError:
96 # Paths are on different drives (Windows) or other error
97 # - use absolute path
98 rel_files.append(os.path.abspath(f))
99 else:
100 # No common directory - use absolute paths
101 rel_files.append(os.path.abspath(f))
103 timeout: int = get_timeout_value(tool, RUFF_DEFAULT_TIMEOUT)
104 # Lint check
105 cmd: list[str] = build_ruff_check_command(tool=tool, files=rel_files, fix=False)
106 success_lint: bool
107 output_lint: str
108 try:
109 success_lint, output_lint = run_subprocess_with_timeout(
110 tool=tool,
111 cmd=cmd,
112 timeout=timeout,
113 cwd=cwd,
114 )
115 except subprocess.TimeoutExpired:
116 timeout_result = create_timeout_result(
117 tool=tool,
118 timeout=timeout,
119 cmd=cmd,
120 )
121 return ToolResult(
122 name=tool.definition.name,
123 success=timeout_result.success,
124 output=timeout_result.output,
125 issues_count=timeout_result.issues_count,
126 issues=timeout_result.issues,
127 )
129 # Debug logging for CI diagnostics
130 logger.debug(f"[ruff] check command: {' '.join(cmd)}")
131 logger.debug(f"[ruff] check success: {success_lint}")
132 if not success_lint:
133 # Log full output to debug file only - raw JSON output is parsed and
134 # formatted into tables, so no need to show it in console warnings
135 logger.debug(f"[ruff] check full output:\n{output_lint}")
137 lint_issues = parse_ruff_output(output=output_lint)
138 lint_issues_count: int = len(lint_issues)
140 # Optional format check via `format_check` flag
141 format_issues_count: int = 0
142 format_files: list[str] = []
143 format_issues: list[RuffFormatIssue] = []
144 success_format: bool = True # Default to True when format check is skipped
145 if tool.options.get("format_check", False):
146 format_cmd: list[str] = build_ruff_format_command(
147 tool=tool,
148 files=rel_files,
149 check_only=True,
150 )
151 output_format: str
152 try:
153 success_format, output_format = run_subprocess_with_timeout(
154 tool=tool,
155 cmd=format_cmd,
156 timeout=timeout,
157 cwd=cwd,
158 )
159 except subprocess.TimeoutExpired:
160 timeout_result = create_timeout_result(
161 tool=tool,
162 timeout=timeout,
163 cmd=format_cmd,
164 )
165 return ToolResult(
166 name=tool.definition.name,
167 success=timeout_result.success,
168 output=timeout_result.output,
169 issues_count=lint_issues_count + timeout_result.issues_count,
170 issues=lint_issues + timeout_result.issues,
171 )
173 # Debug logging for CI diagnostics
174 logger.debug(f"[ruff] format --check command: {' '.join(format_cmd)}")
175 logger.debug(f"[ruff] format --check success: {success_format}")
176 if not success_format:
177 # Log full output to debug file only - output is parsed and
178 # formatted into tables, so no need to show it in console warnings
179 logger.debug(f"[ruff] format check full output:\n{output_format}")
181 format_files = parse_ruff_format_check_output(output=output_format)
182 # Normalize files to absolute paths to keep behavior consistent with
183 # direct CLI calls and stabilize tests that compare exact paths.
184 normalized_files: list[str] = []
185 for file_path in format_files:
186 if cwd and not os.path.isabs(file_path):
187 absolute_path = os.path.abspath(os.path.join(cwd, file_path))
188 normalized_files.append(absolute_path)
189 else:
190 normalized_files.append(file_path)
191 format_issues_count = len(normalized_files)
192 format_issues = [RuffFormatIssue(file=file) for file in normalized_files]
194 # Combine results - respect subprocess exit codes and issue counts
195 issues_count: int = lint_issues_count + format_issues_count
196 success: bool = success_lint and success_format and (issues_count == 0)
198 # Diagnostic logging for the "ERROR" case (subprocess failed but no issues parsed)
199 if not success and issues_count == 0:
200 logger.warning(
201 f"ruff subprocess failed (lint={success_lint}, format={success_format}) "
202 f"but no issues were parsed - this indicates a ruff execution error",
203 )
205 # Suppress narrative blocks; rely on standardized tables and summary lines
206 output_summary: str | None = None
208 # Combine linting and formatting issues for the formatters
209 all_issues = lint_issues + format_issues
211 return ToolResult(
212 name=tool.definition.name,
213 success=success,
214 output=output_summary,
215 issues_count=issues_count,
216 issues=all_issues,
217 )