Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.8"
python-version: "3.10"
- run: |
python3 -m pip install --upgrade build
python3 -m build
Expand Down
59 changes: 54 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,15 @@ Unfortunately, there seems to be no plan within LLVM to accelerate the standalon

**Cons:**

- clangd-tidy lacks support for the `--fix` option. (Consider using code actions provided by your editor if you have clangd properly configured, as clangd-tidy is primarily designed for speeding up CI checks.)
- clangd-tidy silently disables [several](https://github.com/llvm/llvm-project/blob/main/clang-tools-extra/clangd/TidyProvider.cpp#L197) checks not supported by clangd.
- Diagnostics generated by clangd-tidy might be marginally less aesthetically pleasing compared to clang-tidy.
- Other known discrepancies between clangd and clang-tidy behavior: #7, #15, #16.

## Prerequisites

- [clangd](https://clangd.llvm.org/)
- Python 3.8+ (may work on older versions, but not tested)
- [attrs](https://www.attrs.org/) and [cattrs](https://catt.rs/) (automatically installed if clangd-tidy is installed via pip)
- Python 3.10+
- [attrs](https://www.attrs.org/) and [cattrs >= 25.1.0](https://catt.rs/) (automatically installed if clangd-tidy is installed via pip)
- [tqdm](https://github.com/tqdm/tqdm) (optional, required for progress bar support)

## Installation
Expand All @@ -59,8 +58,10 @@ pip install clangd-tidy

```
usage: clangd-tidy [--allow-extensions ALLOW_EXTENSIONS]
[--fail-on-severity SEVERITY] [-f] [-o OUTPUT]
[--line-filter LINE_FILTER] [--tqdm] [--github]
[--fail-on-severity SEVERITY] [-f] [--fix]
[--format-style <string>] [--export-fixes EXPORT_FIXES]
[--clang-apply-replacements-executable CLANG_APPLY_REPLACEMENTS_EXECUTABLE]
[-o OUTPUT] [--line-filter LINE_FILTER] [--tqdm] [--github]
[--git-root GIT_ROOT] [-c] [--context CONTEXT]
[--color {auto,always,never}] [-v]
[-p COMPILE_COMMANDS_DIR] [-j JOBS]
Expand All @@ -86,6 +87,23 @@ check options:
-f, --format Also check code formatting with clang-format. Exits
with a non-zero status if any file violates formatting
rules.
--fix Apply suggested fixes to the files. Supports cross-
file refactorings. Disables --fail-on-severity.
--format-style <string>
Style for formatting code around applied fixes (only
used with --fix). Options: 'none' (default, no
formatting), 'file' (use .clang-format), 'llvm',
'google', 'webkit', 'mozilla', or '{key: value, ...}'
for inline configuration. See clang-format
documentation for details. Matches clang-tidy's
--format-style behavior.
--export-fixes EXPORT_FIXES
Export fixes to a YAML file. If this is used, --fix is
ignored. Unlike clang-tidy, always uses absolute paths
in the output YAML. Disables --fail-on-severity.
--clang-apply-replacements-executable CLANG_APPLY_REPLACEMENTS_EXECUTABLE
clang-apply-replacements executable. [default: clang-
apply-replacements]

output options:
-o OUTPUT, --output OUTPUT
Expand Down Expand Up @@ -159,6 +177,37 @@ Example usage with git:

```

## Applying Fixes

`clangd-tidy` can automatically apply the suggested fixes for diagnostics.

### Direct Fixes

To apply fixes directly to your files, use the `--fix` flag:

```bash
clangd-tidy --fix your_source_file.cpp
```

You can control the formatting of the code around the applied fixes using the `--format-style` argument. For example, to use the LLVM style:

```bash
clangd-tidy --fix --format-style=llvm your_source_file.cpp
```

See the `clang-format` documentation for more details on the available styles and `clang-apply-replacements` for formatting behavior.

### Exporting Fixes

Alternatively, you can export the fixes to a YAML file, which can be applied later using `clang-apply-replacements`:

```bash
clangd-tidy --export-fixes fixes.yaml your_source_file.cpp
clang-apply-replacements . < fixes.yaml
```

**Note:** When using `--fix` or `--export-fixes`, cross-file fixes may be missed for files not in the compilation database. Make sure all source files in your project are listed in the compilation database to avoid this issue. This is a limitation of clangd.

## Acknowledgement

Special thanks to [@yeger00](https://github.com/yeger00) for his [pylspclient](https://github.com/yeger00/pylspclient), which inspired earlier versions of this project.
Expand Down
3 changes: 2 additions & 1 deletion clangd_tidy/__main__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import sys
from .main_cli import main_cli

main_cli()
sys.exit(main_cli())
26 changes: 26 additions & 0 deletions clangd_tidy/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,32 @@ def parse_args() -> argparse.Namespace:
action="store_true",
help="Also check code formatting with clang-format. Exits with a non-zero status if any file violates formatting rules.",
)
check_group.add_argument(
"--fix",
action="store_true",
help="Apply suggested fixes to the files. Supports cross-file refactorings. Disables --fail-on-severity.",
)
check_group.add_argument(
"--format-style",
type=str,
default="none",
metavar="<string>",
help="Style for formatting code around applied fixes (only used with --fix). "
"Options: 'none' (default, no formatting), 'file' (use .clang-format), "
"'llvm', 'google', 'webkit', 'mozilla', or '{key: value, ...}' for inline configuration. "
"See clang-format documentation for details. Matches clang-tidy's --format-style behavior.",
)
check_group.add_argument(
"--export-fixes",
type=pathlib.Path,
default=None,
help="Export fixes to a YAML file. If this is used, --fix is ignored. Unlike clang-tidy, always uses absolute paths in the output YAML. Disables --fail-on-severity.",
)
check_group.add_argument(
"--clang-apply-replacements-executable",
default="clang-apply-replacements",
help="clang-apply-replacements executable. [default: clang-apply-replacements]",
)

output_group = parser.add_argument_group("output options")
output_group.add_argument(
Expand Down
101 changes: 101 additions & 0 deletions clangd_tidy/event_bus.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import asyncio
import logging
from typing import Any, Callable, Coroutine, List, Type, Tuple, TypeVar, TypeAlias


T = TypeVar("T")
Handler: TypeAlias = Callable[[T], Coroutine[Any, Any, Any]]
Listener: TypeAlias = Tuple[Type[T], Handler[T]]


class EventBus:
"""
A simple asynchronous event bus for dispatching events to registered listeners.
Events are expected to be instances of LSP message types or similar data structures.
"""

def __init__(self):
self._listeners: List[Listener[Any]] = []
self._queue: asyncio.Queue[Any] = asyncio.Queue()
self._finished = asyncio.Future[None]()
self._handler_tasks: set[asyncio.Task[Any]] = set()

def subscribe(self, event_type: Type[T], handler: Handler[T]):
"""
Registers a handler coroutine for a specific event type.

Args:
event_type: The type of event (e.g., an LSP message class) to subscribe to.
handler: An async callable that will be called when an event of the given type is published.
"""
self._listeners.append((event_type, handler))

def publish(self, event: Any):
"""
Publishes an event to the bus. The event will be put into a queue
and dispatched asynchronously.

Args:
event: The event instance (e.g., an LSP message object) to publish.
"""
self._queue.put_nowait(event)

def _check_handler_task_exception(self, task: asyncio.Task[Any]):
"""Check if a handler task raised an exception and log it."""
if not task.cancelled():
try:
task.result()
except Exception as exc:
logging.exception(
"Error in event handler task",
exc_info=exc,
)
self._finished.set_exception(exc)

async def run(self):
"""
The main dispatch loop that continuously pulls events from the queue
and dispatches them to registered listeners.
"""
while not self._finished.done():
# Get the next event from the queue (or exit early if finished)
event_task = asyncio.create_task(self._queue.get())
done, _ = await asyncio.wait(
{event_task, self._finished}, return_when=asyncio.FIRST_COMPLETED
)

if self._finished in done:
# If the finished future is done, cancel the event task. We only set
# _finished in case of error, so propagate the exception.
event_task.cancel()
break

event = event_task.result()
try:
for event_type, handler in self._listeners:
if isinstance(event, event_type):
# Schedule handler as a separate task to avoid blocking the
# dispatch loop. This avoids deadlocks if a handler only
# completes after another event is processed.
task = asyncio.create_task(handler(event))
task.add_done_callback(self._handler_tasks.discard)
task.add_done_callback(self._check_handler_task_exception)
self._handler_tasks.add(task)
except (Exception, asyncio.CancelledError) as e:
logging.error(
f"Error dispatching event {type(event).__name__}: {e}",
exc_info=e,
)
finally:
self._queue.task_done()

# Cancel any remaining handler tasks
for task in self._handler_tasks:
task.cancel()
try:
await task
except asyncio.CancelledError:
pass

logging.debug("Stopping EventBus dispatch loop...")
return await self._finished # Propagate any exception
Empty file added clangd_tidy/flows/__init__.py
Empty file.
Loading
Loading