|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +import dataclasses |
| 4 | +import json |
| 5 | +import sys |
| 6 | +from pathlib import Path |
| 7 | + |
| 8 | +from finecode_extension_api import code_action |
| 9 | +from finecode_extension_api.actions import lint as lint_action |
| 10 | +from finecode_extension_api.interfaces import icache, icommandrunner, ilogger, ifilemanager |
| 11 | + |
| 12 | + |
| 13 | +@dataclasses.dataclass |
| 14 | +class RuffLintHandlerConfig(code_action.ActionHandlerConfig): |
| 15 | + line_length: int = 88 |
| 16 | + target_version: str = "py38" |
| 17 | + select: list[str] | None = None # Rules to enable |
| 18 | + ignore: list[str] | None = None # Rules to disable |
| 19 | + preview: bool = False |
| 20 | + |
| 21 | + |
| 22 | +class RuffLintHandler( |
| 23 | + code_action.ActionHandler[lint_action.LintAction, RuffLintHandlerConfig] |
| 24 | +): |
| 25 | + CACHE_KEY = "RuffLinter" |
| 26 | + |
| 27 | + def __init__( |
| 28 | + self, |
| 29 | + config: RuffLintHandlerConfig, |
| 30 | + cache: icache.ICache, |
| 31 | + logger: ilogger.ILogger, |
| 32 | + file_manager: ifilemanager.IFileManager, |
| 33 | + command_runner: icommandrunner.ICommandRunner, |
| 34 | + ) -> None: |
| 35 | + self.config = config |
| 36 | + self.cache = cache |
| 37 | + self.logger = logger |
| 38 | + self.file_manager = file_manager |
| 39 | + self.command_runner = command_runner |
| 40 | + |
| 41 | + self.ruff_bin_path = Path(sys.executable).parent / "ruff" |
| 42 | + |
| 43 | + async def run_on_single_file( |
| 44 | + self, file_path: Path |
| 45 | + ) -> lint_action.LintRunResult: |
| 46 | + messages = {} |
| 47 | + try: |
| 48 | + cached_lint_messages = await self.cache.get_file_cache( |
| 49 | + file_path, self.CACHE_KEY |
| 50 | + ) |
| 51 | + messages[str(file_path)] = cached_lint_messages |
| 52 | + return lint_action.LintRunResult(messages=messages) |
| 53 | + except icache.CacheMissException: |
| 54 | + pass |
| 55 | + |
| 56 | + file_version = await self.file_manager.get_file_version(file_path) |
| 57 | + file_content = await self.file_manager.get_content(file_path) |
| 58 | + lint_messages = await self.run_ruff_lint_on_single_file(file_path, file_content) |
| 59 | + messages[str(file_path)] = lint_messages |
| 60 | + await self.cache.save_file_cache( |
| 61 | + file_path, file_version, self.CACHE_KEY, lint_messages |
| 62 | + ) |
| 63 | + |
| 64 | + return lint_action.LintRunResult(messages=messages) |
| 65 | + |
| 66 | + async def run( |
| 67 | + self, |
| 68 | + payload: lint_action.LintRunPayload, |
| 69 | + run_context: code_action.RunActionWithPartialResultsContext, |
| 70 | + ) -> None: |
| 71 | + file_paths = [file_path async for file_path in payload] |
| 72 | + |
| 73 | + for file_path in file_paths: |
| 74 | + run_context.partial_result_scheduler.schedule( |
| 75 | + file_path, |
| 76 | + self.run_on_single_file(file_path), |
| 77 | + ) |
| 78 | + |
| 79 | + async def run_ruff_lint_on_single_file( |
| 80 | + self, |
| 81 | + file_path: Path, |
| 82 | + file_content: str, |
| 83 | + ) -> list[lint_action.LintMessage]: |
| 84 | + """Run ruff linting on a single file""" |
| 85 | + lint_messages: list[lint_action.LintMessage] = [] |
| 86 | + |
| 87 | + # Build ruff check command |
| 88 | + cmd = [ |
| 89 | + str(self.ruff_bin_path), |
| 90 | + "check", |
| 91 | + "--output-format", |
| 92 | + "json", |
| 93 | + "--line-length", |
| 94 | + str(self.config.line_length), |
| 95 | + "--target-version", |
| 96 | + self.config.target_version, |
| 97 | + "--stdin-filename", |
| 98 | + str(file_path), |
| 99 | + ] |
| 100 | + |
| 101 | + if self.config.select: |
| 102 | + cmd.extend(["--select", ",".join(self.config.select)]) |
| 103 | + if self.config.ignore: |
| 104 | + cmd.extend(["--ignore", ",".join(self.config.ignore)]) |
| 105 | + if self.config.preview: |
| 106 | + cmd.append("--preview") |
| 107 | + |
| 108 | + cmd_str = " ".join(cmd) |
| 109 | + ruff_process = await self.command_runner.run( |
| 110 | + cmd_str, |
| 111 | + ) |
| 112 | + |
| 113 | + ruff_process.write_to_stdin(file_content) |
| 114 | + ruff_process.close_stdin() # Signal EOF |
| 115 | + |
| 116 | + await ruff_process.wait_for_end() |
| 117 | + |
| 118 | + output = ruff_process.get_output() |
| 119 | + try: |
| 120 | + ruff_results = json.loads(output) |
| 121 | + for violation in ruff_results: |
| 122 | + lint_message = map_ruff_violation_to_lint_message(violation) |
| 123 | + lint_messages.append(lint_message) |
| 124 | + except json.JSONDecodeError: |
| 125 | + raise code_action.ActionFailedException(f'Output of ruff is not json: {output}') |
| 126 | + |
| 127 | + return lint_messages |
| 128 | + |
| 129 | + |
| 130 | +def map_ruff_violation_to_lint_message(violation: dict) -> lint_action.LintMessage: |
| 131 | + """Map a ruff violation to a lint message""" |
| 132 | + location = violation.get("location", {}) |
| 133 | + end_location = violation.get("end_location", {}) |
| 134 | + |
| 135 | + # Extract line/column info (ruff uses 1-based indexing) |
| 136 | + start_line = max(1, location.get("row", 1)) |
| 137 | + start_column = max(0, location.get("column", 0)) |
| 138 | + end_line = max(1, end_location.get("row", start_line + 1)) - 1 # Convert to 0-based |
| 139 | + end_column = max(0, end_location.get("column", start_column)) |
| 140 | + |
| 141 | + # Determine severity based on rule code |
| 142 | + code = violation.get("code", "") |
| 143 | + code_description = violation.get("url", "") |
| 144 | + if code.startswith(("E", "F")): # Error codes |
| 145 | + severity = lint_action.LintMessageSeverity.ERROR |
| 146 | + elif code.startswith("W"): # Warning codes |
| 147 | + severity = lint_action.LintMessageSeverity.WARNING |
| 148 | + else: |
| 149 | + severity = lint_action.LintMessageSeverity.INFO |
| 150 | + |
| 151 | + return lint_action.LintMessage( |
| 152 | + range=lint_action.Range( |
| 153 | + start=lint_action.Position(line=start_line, character=start_column), |
| 154 | + end=lint_action.Position(line=end_line, character=end_column), |
| 155 | + ), |
| 156 | + message=violation.get("message", ""), |
| 157 | + code=code, |
| 158 | + code_description=code_description, |
| 159 | + source="ruff", |
| 160 | + severity=severity, |
| 161 | + ) |
0 commit comments