|
| 1 | +from abc import abstractmethod |
| 2 | +from collections.abc import Iterable |
| 3 | +from dataclasses import dataclass |
| 4 | + |
| 5 | +# Code mapping between LSP, PyLint, and our own diagnostics: |
| 6 | +# | LSP | PyLint | Our | |
| 7 | +# |---------------------------|------------|----------------| |
| 8 | +# | Severity.ERROR | Error | Failure() | |
| 9 | +# | Severity.WARN | Warning | Advisory() | |
| 10 | +# | DiagnosticTag.DEPRECATED | Warning | Deprecation() | |
| 11 | +# | Severity.INFO | Info | Advice() | |
| 12 | +# | Severity.HINT | Convention | Convention() | |
| 13 | +# | DiagnosticTag.UNNECESSARY | Refactor | Convention() | |
| 14 | + |
| 15 | + |
| 16 | +@dataclass |
| 17 | +class Advice: |
| 18 | + code: str |
| 19 | + message: str |
| 20 | + start_line: int |
| 21 | + start_col: int |
| 22 | + end_line: int |
| 23 | + end_col: int |
| 24 | + |
| 25 | + def replace( |
| 26 | + self, |
| 27 | + code: str | None = None, |
| 28 | + message: str | None = None, |
| 29 | + start_line: int | None = None, |
| 30 | + start_col: int | None = None, |
| 31 | + end_line: int | None = None, |
| 32 | + end_col: int | None = None, |
| 33 | + ) -> 'Advice': |
| 34 | + return self.__class__( |
| 35 | + code=code if code is not None else self.code, |
| 36 | + message=message if message is not None else self.message, |
| 37 | + start_line=start_line if start_line is not None else self.start_line, |
| 38 | + start_col=start_col if start_col is not None else self.start_col, |
| 39 | + end_line=end_line if end_line is not None else self.end_line, |
| 40 | + end_col=end_col if end_col is not None else self.end_col, |
| 41 | + ) |
| 42 | + |
| 43 | + def as_advisory(self) -> 'Advisory': |
| 44 | + return Advisory(**self.__dict__) |
| 45 | + |
| 46 | + def as_failure(self) -> 'Failure': |
| 47 | + return Failure(**self.__dict__) |
| 48 | + |
| 49 | + def as_deprecation(self) -> 'Deprecation': |
| 50 | + return Deprecation(**self.__dict__) |
| 51 | + |
| 52 | + def as_convention(self) -> 'Convention': |
| 53 | + return Convention(**self.__dict__) |
| 54 | + |
| 55 | + |
| 56 | +class Advisory(Advice): |
| 57 | + """A warning that does not prevent the code from running.""" |
| 58 | + |
| 59 | + |
| 60 | +class Failure(Advisory): |
| 61 | + """An error that prevents the code from running.""" |
| 62 | + |
| 63 | + |
| 64 | +class Deprecation(Advisory): |
| 65 | + """An advisory that suggests to replace the code with a newer version.""" |
| 66 | + |
| 67 | + |
| 68 | +class Convention(Advice): |
| 69 | + """A suggestion for a better way to write the code.""" |
| 70 | + |
| 71 | + |
| 72 | +class Linter: |
| 73 | + @abstractmethod |
| 74 | + def lint(self, code: str) -> Iterable[Advice]: ... |
| 75 | + |
| 76 | + |
| 77 | +class Fixer: |
| 78 | + @abstractmethod |
| 79 | + def name(self) -> str: ... |
| 80 | + |
| 81 | + @abstractmethod |
| 82 | + def apply(self, code: str) -> str: ... |
| 83 | + |
| 84 | + |
| 85 | +class SequentialLinter(Linter): |
| 86 | + def __init__(self, linters: list[Linter]): |
| 87 | + self._linters = linters |
| 88 | + |
| 89 | + def lint(self, code: str) -> Iterable[Advice]: |
| 90 | + for linter in self._linters: |
| 91 | + yield from linter.lint(code) |
0 commit comments