|
| 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 PyreflyLintHandlerConfig(code_action.ActionHandlerConfig): |
| 15 | + ... |
| 16 | + |
| 17 | + |
| 18 | +class PyreflyLintHandler( |
| 19 | + code_action.ActionHandler[lint_action.LintAction, PyreflyLintHandlerConfig] |
| 20 | +): |
| 21 | + CACHE_KEY = "PyreflyLinter" |
| 22 | + |
| 23 | + def __init__( |
| 24 | + self, |
| 25 | + config: PyreflyLintHandlerConfig, |
| 26 | + cache: icache.ICache, |
| 27 | + logger: ilogger.ILogger, |
| 28 | + file_manager: ifilemanager.IFileManager, |
| 29 | + command_runner: icommandrunner.ICommandRunner, |
| 30 | + ) -> None: |
| 31 | + self.config = config |
| 32 | + self.cache = cache |
| 33 | + self.logger = logger |
| 34 | + self.file_manager = file_manager |
| 35 | + self.command_runner = command_runner |
| 36 | + |
| 37 | + self.pyrefly_bin_path = Path(sys.executable).parent / "pyrefly" |
| 38 | + |
| 39 | + async def run_on_single_file( |
| 40 | + self, file_path: Path |
| 41 | + ) -> lint_action.LintRunResult: |
| 42 | + messages = {} |
| 43 | + try: |
| 44 | + cached_lint_messages = await self.cache.get_file_cache( |
| 45 | + file_path, self.CACHE_KEY |
| 46 | + ) |
| 47 | + messages[str(file_path)] = cached_lint_messages |
| 48 | + return lint_action.LintRunResult(messages=messages) |
| 49 | + except icache.CacheMissException: |
| 50 | + pass |
| 51 | + |
| 52 | + file_version = await self.file_manager.get_file_version(file_path) |
| 53 | + file_content = await self.file_manager.get_content(file_path) |
| 54 | + lint_messages = await self.run_pyrefly_lint_on_single_file(file_path, file_content) |
| 55 | + messages[str(file_path)] = lint_messages |
| 56 | + await self.cache.save_file_cache( |
| 57 | + file_path, file_version, self.CACHE_KEY, lint_messages |
| 58 | + ) |
| 59 | + |
| 60 | + return lint_action.LintRunResult(messages=messages) |
| 61 | + |
| 62 | + async def run( |
| 63 | + self, |
| 64 | + payload: lint_action.LintRunPayload, |
| 65 | + run_context: code_action.RunActionWithPartialResultsContext, |
| 66 | + ) -> None: |
| 67 | + file_paths = [file_path async for file_path in payload] |
| 68 | + |
| 69 | + for file_path in file_paths: |
| 70 | + run_context.partial_result_scheduler.schedule( |
| 71 | + file_path, |
| 72 | + self.run_on_single_file(file_path), |
| 73 | + ) |
| 74 | + |
| 75 | + async def run_pyrefly_lint_on_single_file( |
| 76 | + self, |
| 77 | + file_path: Path, |
| 78 | + file_content: str, |
| 79 | + ) -> list[lint_action.LintMessage]: |
| 80 | + """Run pyrefly type checking on a single file""" |
| 81 | + lint_messages: list[lint_action.LintMessage] = [] |
| 82 | + |
| 83 | + cmd = [ |
| 84 | + str(self.pyrefly_bin_path), |
| 85 | + "check", |
| 86 | + "--output-format", |
| 87 | + "json", |
| 88 | + str(file_path), |
| 89 | + ] |
| 90 | + |
| 91 | + cmd_str = " ".join(cmd) |
| 92 | + pyrefly_process = await self.command_runner.run(cmd_str) |
| 93 | + |
| 94 | + pyrefly_process.write_to_stdin(file_content) |
| 95 | + pyrefly_process.close_stdin() # Signal EOF |
| 96 | + |
| 97 | + await pyrefly_process.wait_for_end() |
| 98 | + |
| 99 | + output = pyrefly_process.get_output() |
| 100 | + try: |
| 101 | + pyrefly_results = json.loads(output) |
| 102 | + self.logger.info(pyrefly_results) |
| 103 | + for error in pyrefly_results['errors']: |
| 104 | + lint_message = map_pyrefly_error_to_lint_message(error) |
| 105 | + lint_messages.append(lint_message) |
| 106 | + except json.JSONDecodeError: |
| 107 | + raise code_action.ActionFailedException(f'Output of pyrefly is not json: {output}') |
| 108 | + |
| 109 | + return lint_messages |
| 110 | + |
| 111 | + |
| 112 | +def map_pyrefly_error_to_lint_message(error: dict) -> lint_action.LintMessage: |
| 113 | + """Map a pyrefly error to a lint message""" |
| 114 | + # Extract line/column info (pyrefly uses 1-based indexing) |
| 115 | + start_line = error['line'] |
| 116 | + start_column = error['column'] |
| 117 | + end_line = error['stop_line'] |
| 118 | + end_column = error['stop_column'] |
| 119 | + |
| 120 | + # Determine severity based on error type |
| 121 | + error_code = error.get('code', '') |
| 122 | + code_description = error.get("name", "") |
| 123 | + severity = lint_action.LintMessageSeverity.ERROR |
| 124 | + |
| 125 | + return lint_action.LintMessage( |
| 126 | + range=lint_action.Range( |
| 127 | + start=lint_action.Position(line=start_line, character=start_column), |
| 128 | + end=lint_action.Position(line=end_line, character=end_column), |
| 129 | + ), |
| 130 | + message=error.get("description", ""), |
| 131 | + code=error_code, |
| 132 | + code_description=code_description, |
| 133 | + source="pyrefly", |
| 134 | + severity=severity, |
| 135 | + ) |
0 commit comments