Skip to content

Commit

Permalink
feat: add clang-format check
Browse files Browse the repository at this point in the history
  • Loading branch information
lljbash committed Jan 2, 2025
1 parent da0738a commit 2a4de98
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 42 deletions.
38 changes: 21 additions & 17 deletions clangd_tidy/diagnostic_formatter.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,21 @@
from abc import ABC, abstractmethod
from dataclasses import dataclass
import os
import pathlib
import re
from typing import Iterable, List, Optional
from typing import Dict, Iterable, List, Optional

from .lsp.messages import Diagnostic, DiagnosticSeverity

__all__ = [
"FileDiagnostics",
"DiagnosticCollection",
"DiagnosticFormatter",
"CompactDiagnosticFormatter",
"FancyDiagnosticFormatter",
"GithubActionWorkflowCommandDiagnosticFormatter",
]


@dataclass
class FileDiagnostics:
file: pathlib.Path
diagnostics: List[Diagnostic]


DiagnosticCollection = Iterable[FileDiagnostics]
DiagnosticCollection = Dict[pathlib.Path, List[Diagnostic]]


class DiagnosticFormatter(ABC):
Expand All @@ -35,10 +28,9 @@ class DiagnosticFormatter(ABC):

def format(self, diagnostic_collection: DiagnosticCollection) -> str:
file_outputs: List[str] = []
for file_diagnostics in sorted(
diagnostic_collection, key=lambda d: d.file.as_posix()
for file, diagnostics in sorted(
diagnostic_collection.items(), key=lambda fd: fd[0].as_posix()
):
file, diagnostics = file_diagnostics.file, file_diagnostics.diagnostics
diagnostic_outputs = [
o
for o in [
Expand Down Expand Up @@ -143,7 +135,7 @@ def _make_file_output(
def _make_whole_output(self, file_outputs: Iterable[str]) -> str:
head = "::group::{workflow commands}"
tail = "::endgroup::"
return "\n".join([head, *file_outputs, tail])
return "\n".join(["", head, *file_outputs, tail])


class FancyDiagnosticFormatter(DiagnosticFormatter):
Expand All @@ -155,6 +147,7 @@ class ColorSeqTty:
HINT = "\033[94m"
NOTE = "\033[90m"
GREEN = "\033[92m"
MAGENTA = "\033[95m"
BOLD = "\033[1m"
ENDC = "\033[0m"

Expand All @@ -165,6 +158,7 @@ class ColorSeqNoTty:
HINT = ""
NOTE = ""
GREEN = ""
MAGENTA = ""
BOLD = ""
ENDC = ""

Expand All @@ -188,6 +182,9 @@ def highlight(self, message: str):
def note(self, message: str):
return f"{self.color_seq.NOTE}{message}{self.color_seq.ENDC}"

def format(self, message: str):
return f"{self.color_seq.MAGENTA}{message}{self.color_seq.ENDC}"

def __init__(self, extra_context: int, enable_color: bool):
self._extra_context = extra_context
self._colorizer = self.Colorizer(enable_color)
Expand Down Expand Up @@ -241,7 +238,7 @@ def _code_context(
indicator = self._colorizer.highlight(indicator)
context += self._prepend_line_number(indicator, lino=None)

return context
return context.rstrip()

@staticmethod
def _diagnostic_message(
Expand All @@ -255,10 +252,17 @@ def _diagnostic_message(
) -> str:
return f"{file}:{line_start + 1}:{col_start + 1}: {severity}: {message} {code}\n{context}"

def _formatting_message(self, file: str, message: str) -> str:
return self._colorizer.format(f"{file}: {message}")

def _format_one_diagnostic(
self, file: pathlib.Path, diagnostic: Diagnostic
) -> Optional[str]:
rel_file = os.path.relpath(file)

if diagnostic.source == "clang-format":
return self._formatting_message(rel_file, diagnostic.message)

message: str = diagnostic.message.replace(" (fix available)", "")
message_list = [line for line in message.splitlines() if line.strip()]
message, extra_messages = message_list[0], message_list[1:]
Expand Down Expand Up @@ -306,7 +310,7 @@ def _make_file_output(
self, file: pathlib.Path, diagnostic_outputs: Iterable[str]
) -> str:
del file
return "\n".join(diagnostic_outputs)
return "\n\n".join(diagnostic_outputs)

def _make_whole_output(self, file_outputs: Iterable[str]) -> str:
return "\n".join(file_outputs)
return "\n\n".join(file_outputs)
11 changes: 11 additions & 0 deletions clangd_tidy/lsp/clangd.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
from .client import ClientAsync, RequestResponsePair
from .messages import (
DidOpenTextDocumentParams,
DocumentFormattingParams,
InitializeParams,
LanguageId,
LspNotificationMessage,
NotificationMethod,
RequestMethod,
TextDocumentIdentifier,
TextDocumentItem,
WorkspaceFolder,
)
Expand Down Expand Up @@ -83,6 +85,15 @@ async def did_open(self, path: pathlib.Path) -> None:
),
)

async def formatting(self, path: pathlib.Path) -> None:
assert path.is_file()
await self._client.request(
RequestMethod.FORMATTING,
DocumentFormattingParams(
textDocument=TextDocumentIdentifier(uri=path.as_uri())
),
)

async def shutdown(self) -> None:
await self._client.request(RequestMethod.SHUTDOWN)

Expand Down
4 changes: 3 additions & 1 deletion clangd_tidy/lsp/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ def __init__(self, rpc: RpcEndpointAsync):

async def request(self, method: RequestMethod, params: Params = Params()) -> None:
id = next(self._id)
message = RequestMessage(id=id, method=method, params=params)
message = RequestMessage(
id=id, method=method, params=cattrs.unstructure(params)
)
self._requests[id] = message
await self._rpc.send(cattrs.unstructure(message))

Expand Down
21 changes: 19 additions & 2 deletions clangd_tidy/lsp/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class Message:
class RequestMethod(Enum):
INITIALIZE = "initialize"
SHUTDOWN = "shutdown"
FORMATTING = "textDocument/formatting"


@unique
Expand All @@ -38,7 +39,7 @@ class Params:
class RequestMessage(Message):
id: int
method: RequestMethod
params: Params
params: dict = Factory(dict)


@define
Expand All @@ -51,7 +52,7 @@ class ResponseError:
@define(kw_only=True)
class ResponseMessage(Message):
id: int
result: Optional[dict] = None
result: Any = None
error: Optional[ResponseError] = None


Expand Down Expand Up @@ -134,3 +135,19 @@ class PublishDiagnosticsParams(Params):
uri: str
diagnostics: List[Diagnostic]
version: Optional[int] = None


@define
class WorkDoneProgressParams(Params):
workDoneToken: Any = None


@define
class TextDocumentIdentifier:
uri: str


@define(kw_only=True)
class DocumentFormattingParams(WorkDoneProgressParams):
textDocument: TextDocumentIdentifier
options: dict = Factory(dict)
84 changes: 62 additions & 22 deletions clangd_tidy/main_cli.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#!/usr/bin/env python3

import asyncio
import importlib.util
import pathlib
import sys
from typing import Collection, List, TextIO
Expand All @@ -13,15 +12,20 @@
from .args import parse_args, SEVERITY_INT
from .diagnostic_formatter import (
CompactDiagnosticFormatter,
DiagnosticCollection,
FancyDiagnosticFormatter,
FileDiagnostics,
GithubActionWorkflowCommandDiagnosticFormatter,
)
from .lsp import ClangdAsync, RequestResponsePair
from .lsp.messages import (
Diagnostic,
DocumentFormattingParams,
LspNotificationMessage,
NotificationMethod,
Position,
PublishDiagnosticsParams,
Range,
RequestMethod,
)

__all__ = ["main_cli"]
Expand All @@ -37,42 +41,73 @@ def _is_output_supports_color(output: TextIO) -> bool:

class ClangdRunner:
def __init__(
self, clangd: ClangdAsync, files: Collection[pathlib.Path], tqdm: bool
self,
clangd: ClangdAsync,
files: Collection[pathlib.Path],
tqdm: bool,
max_pending_requests: int,
):
self._clangd = clangd
self._files = files
self._tqdm = tqdm
self._sem = asyncio.Semaphore(max_pending_requests)

def run(self) -> List[FileDiagnostics]:
def acquire_diagnostics(self) -> DiagnosticCollection:
return asyncio.run(self._acquire_diagnostics())

async def _request_diagnostics(self) -> None:
await asyncio.gather(*(self._clangd.did_open(file) for file in self._files))

async def _collect_diagnostics(self) -> List[FileDiagnostics]:
file_diagnostics: List[FileDiagnostics] = []
for file in self._files:
await self._sem.acquire()
await self._clangd.did_open(file)
await self._sem.acquire()
await self._clangd.formatting(file)

async def _collect_diagnostics(self) -> DiagnosticCollection:
diagnostics: DiagnosticCollection = {}
formatting_diagnostics: DiagnosticCollection = {}
with tqdm(
total=len(self._files),
desc="Collecting diagnostics",
disable=not self._tqdm,
) as pbar:
while len(file_diagnostics) < len(self._files):
while len(diagnostics) < len(self._files):
resp = await self._clangd.recv_response_or_notification()
if isinstance(resp, LspNotificationMessage):
if resp.method == NotificationMethod.PUBLISH_DIAGNOSTICS:
params = cattrs.structure(resp.params, PublishDiagnosticsParams)
file = _uri_to_path(params.uri)
file_diagnostics.append(
FileDiagnostics(file, params.diagnostics)
)
diagnostics[file] = params.diagnostics
tqdm.update(pbar)
return file_diagnostics

async def _acquire_diagnostics(self) -> List[FileDiagnostics]:
self._sem.release()
else:
assert resp.request.method == RequestMethod.FORMATTING
assert resp.response.error is None, "Formatting failed"
params = cattrs.structure(
resp.request.params, DocumentFormattingParams
)
file = _uri_to_path(params.textDocument.uri)
formatting_diagnostics[file] = (
[
Diagnostic(
range=Range(start=Position(0, 0), end=Position(0, 0)),
message="File does not conform to the formatting rules (run `clang-format` to fix)",
source="clang-format",
)
]
if resp.response.result
else []
)
return {
file: formatting_diagnostics[file] + diagnostics[file]
for file in self._files
}

async def _acquire_diagnostics(self) -> DiagnosticCollection:
await self._clangd.start()
await self._clangd.initialize(pathlib.Path.cwd())
init_resp = await self._clangd.recv_response_or_notification()
assert isinstance(init_resp, RequestResponsePair)
assert init_resp.request.method == RequestMethod.INITIALIZE
assert init_resp.response.error is None, "Initialization failed"
await self._clangd.initialized()
_, file_diagnostics = await asyncio.gather(
Expand All @@ -96,12 +131,13 @@ def main_cli():
sys.exit(1)

file_diagnostics = ClangdRunner(
ClangdAsync(
clangd=ClangdAsync(
args.clangd_executable, args.compile_commands_dir, args.jobs, args.verbose
),
files,
args.tqdm,
).run()
files=files,
tqdm=args.tqdm,
max_pending_requests=args.jobs * 2,
).acquire_diagnostics()

formatter = (
FancyDiagnosticFormatter(
Expand All @@ -125,9 +161,13 @@ def main_cli():
)
if any(
any(
d.severity and d.severity >= SEVERITY_INT[args.fail_on_severity]
for d in fd.diagnostics
(
diagostic.severity
and diagostic.severity >= SEVERITY_INT[args.fail_on_severity]
)
or diagostic.source == "clang-format"
for diagostic in diagnostics
)
for fd in file_diagnostics
for diagnostics in file_diagnostics.values()
):
exit(1)
2 changes: 2 additions & 0 deletions test/d.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
auto
f() -> int;

0 comments on commit 2a4de98

Please sign in to comment.