Files
clide/legacy/clide/services/linter_service.py
T
jpmschweitzerandClaude Opus 4.7 a355751437 move python clide to legacy/
Clide is being rebuilt as a Flutter desktop app. The Python Textual
implementation moves wholesale into legacy/ rather than being deleted:
its pane model, panel set, git skills, and panel communication design
are real thought that should remain readable next to the new code
while the rebuild finds its shape. Git's rename tracking preserves
history, so `git log -- legacy/` still works.

The Flutter rebuild lives at the repo root alongside a Go sidecar
(the architecture claudian was heading toward, which folds into
clide as a core component rather than a separate plugin project).
Bootstrap of the new shape lands in subsequent commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:30:51 +02:00

128 lines
4.1 KiB
Python

"""Linter integration service."""
import json
from pathlib import Path
from clide.models.problems import Problem, ProblemsSummary, Severity
from clide.services.process_service import ProcessService
class LinterService:
"""Service for running linters and parsing output."""
def __init__(self, project_path: Path) -> None:
self.project_path = project_path
self._process = ProcessService(cwd=project_path)
async def run_ruff(self) -> list[Problem]:
"""Run ruff linter.
Returns:
List of problems found
"""
result = await self._process.run(
"ruff", "check", "--output-format=json", "."
)
problems: list[Problem] = []
if result.stdout:
try:
data = json.loads(result.stdout)
for item in data:
severity = self._ruff_severity(item.get("code", ""))
problems.append(Problem(
file_path=Path(item["filename"]),
line=item["location"]["row"],
column=item["location"]["column"],
end_line=item.get("end_location", {}).get("row"),
end_column=item.get("end_location", {}).get("column"),
severity=severity,
message=item["message"],
source="ruff",
code=item.get("code"),
))
except json.JSONDecodeError:
pass
return problems
def _ruff_severity(self, code: str) -> Severity:
"""Map ruff code to severity."""
if code.startswith("E") or code.startswith("F"):
return Severity.ERROR
if code.startswith("W"):
return Severity.WARNING
return Severity.INFO
async def run_mypy(self) -> list[Problem]:
"""Run mypy type checker.
Returns:
List of problems found
"""
result = await self._process.run(
"mypy", "--output=json", "."
)
problems: list[Problem] = []
for line in result.stdout.strip().split("\n"):
if not line:
continue
try:
data = json.loads(line)
severity = self._mypy_severity(data.get("severity", "error"))
problems.append(Problem(
file_path=Path(data["file"]),
line=data["line"],
column=data.get("column", 1),
severity=severity,
message=data["message"],
source="mypy",
code=data.get("code"),
))
except (json.JSONDecodeError, KeyError):
continue
return problems
def _mypy_severity(self, severity: str) -> Severity:
"""Map mypy severity to Severity enum."""
mapping = {
"error": Severity.ERROR,
"warning": Severity.WARNING,
"note": Severity.INFO,
}
return mapping.get(severity, Severity.ERROR)
async def run_all(self, linters: list[str]) -> tuple[list[Problem], ProblemsSummary]:
"""Run all configured linters.
Args:
linters: List of linter names to run
Returns:
Tuple of (problems list, summary)
"""
all_problems: list[Problem] = []
for linter in linters:
if linter == "ruff":
all_problems.extend(await self.run_ruff())
elif linter == "mypy":
all_problems.extend(await self.run_mypy())
# Create summary
errors = sum(1 for p in all_problems if p.severity == Severity.ERROR)
warnings = sum(1 for p in all_problems if p.severity == Severity.WARNING)
infos = sum(1 for p in all_problems if p.severity == Severity.INFO)
hints = sum(1 for p in all_problems if p.severity == Severity.HINT)
summary = ProblemsSummary(
errors=errors,
warnings=warnings,
infos=infos,
hints=hints,
)
return all_problems, summary