Repository: cpp-linter/cpp-linter (language: python)

Issue:
clang-tidy-14 findings of Meson based project not shown in "files changed" tab of pull request
When adding cpp-linter to my repo I noticed that in the console output it finds all issues I added for testing, but these clang-tidy findings are not shown in the Github UI in the "files changed" tab at all. After I enabled thread-comments the findings are shown in the conversation of the pull request.

https://github.com/lurtz/sleep-proxy/pull/6

I guess the reason is using clang-tidy-14. I first used clang-tidy-12 but that had issues with finding system headers and thus provided many false positives.
Filtered file doesn't match any project files
Sorry for another issue ;)

https://github.com/Alexays/Waybar/actions/runs/7265887999/job/19796540906?pr=2755

```
> Posting comment(s)
DEBUG:CPP Linter:/__w/Waybar/src/modules/hyprland/workspaces.cpp doesn't match any project files
DEBUG:CPP Linter:/__w/Waybar/src/modules/hyprland/workspaces.cpp doesn't match any project files
INFO:CPP Linter:0 clang-format-checks-failed
INFO:CPP Linter:0 clang-tidy-checks-failed
INFO:CPP Linter:0 checks-failed
```

Maybe the `/__w/Waybar/` prefix causes the workspaces.cpp file to not match with the filter? I think it's similar to this issue: https://github.com/cpp-linter/cpp-linter/issues/38#issuecomment-1845532894


Discussion / hints:
The feature you're referring to is the option called `file-annotations` which defaults to `true` (enabled). Looking at your workflow's run log I see 1 problem:

The paths output in the workflow annotations (the notices added to the workflow's summary) are relative to the build folder (`/build/../src/emulateHost.cpp`) and they shouldn't be. I think this leads to github silently failing to include the annotation as part of the PR's diff because the file's path is incorrect (must exactly match a path relative to the repo root).

FYI, the commands we use to produce the file annotations are the same commands that produce the workflow summary's annotations, so I think its just a matter of correcting the path. I suspect that this may be a side-effect of using clang-tidy v14 with a compilation database.
you were right, but I found no solution I would consider nice. Meson forces me to do an out of tree build. At the moment my solution is

    sed -i 's/\/build//g' compile_commands.json # strip /build from paths
    sed -i 's/\.\.\///g' compile_commands.json  # strip ../ from paths

to get it done. It is not pretty, but now I get the annotations.

For me the issue is solved, but I wonder if better out of tree build support would be nice in cpp-linter.
I think the solution is to adjust

https://github.com/cpp-linter/cpp-linter/blob/35e64f8fc02fe982294e940cbba9e79fd5da328c/cpp_linter/clang_tidy.py#L43-L45

to `resolve()` the intermittent `..` into an absolute path, like so:
```py
        self.filename = (
            Path(self.filename).resolve().as_posix().replace(Path.cwd().as_posix(), "")
        )
```

But I don't have time to test this right now. I didn't know about the build system's odd path formation, that's also helpful to know when writing the unit test for this case.
@lurtz I'm curious if Meson uses absolute or relative paths in compile_commands,json? CMake uses absolute which is why this hasn't shown up in my tests.

I've tested this using CMake on out-of-source builds, I but didn't encounter this problem. I'm still not sure if it specific to clang-tidy v14 (last tested locally using v12).
The fact that the paths start with a `/` is a serious problem because technically that is an absolute path.

- `build/../src/blah` is a relative path with relative redirects (`..`)
- `/build/../src/blah` is an absolute path with relative redirects (`..`)

The fix I proposed above will not recognize `/build/../src/` as a relative path as you can see from the following interactive python session:
```py
>>> from pathlib import PurePath
>>> PurePath("/build/../src").is_absolute()
True
>>> PurePath("build/../src").is_absolute()
False
```
@lurtz  I found this to be informative https://github.com/mesonbuild/meson/issues/10227
That could explain why it clang-tidy versions prior 14 did not work for me.

This is a compile_commands.json file generated on my notebook:
[compile_commands.json.txt](https://github.com/cpp-linter/cpp-linter/files/10776909/compile_commands.json.txt)
As you can see command, file and output are all relative to directory for each translation unit.

I will create a toy C++ project with CMake and Meson to see the differences in the generated compile_commands.json.
> The fact that the paths start with a `/` is a serious problem because technically that is an absolute path.
> 
>     * `build/../src/blah` is a relative path with relative redirects (`..`)
> 
>     * `/build/../src/blah` is an absolute path with relative redirects (`..`)
> 
> 
> The fix I proposed above will not recognize `/build/../src/` as a relative path as you can see from the following interactive python session:
> 
> ```python
> >>> from pathlib import PurePath
> >>> PurePath("/build/../src").is_absolute()
> True
> >>> PurePath("build/../src").is_absolute()
> False
> ```

Maybe it is enough to remove the "dir/.." pattern. I got it working with the following Python code:

    In [1]: from pathlib import Path
    In [2]: Path('/foo/bar/../src').resolve()
    Out[2]: PosixPath('/foo/src')
    In [3]: Path('foo/bar/../src').resolve()
    Out[3]: PosixPath('/home/lutz/workspace/sleep-proxy/.github/workflows/foo/src')


I checked the console output of the last failing and then successful build (where I tweak the compile_commands.json so that everthing starts from the source directory).

[No annotations:](https://github.com/lurtz/sleep-proxy/actions/runs/4211802730/jobs/7310386899)

    INFO:CPP Linter:Running "clang-tidy-14 --export-fixes=clang_tidy_output.yml -p /home/runner/work/sleep-proxy/sleep-proxy/build --extra-arg= src/emulateHost.cpp"
    DEBUG:CPP Linter:Output from clang-tidy:
    ../src/emulateHost.cpp:28:4: warning: unused variable 'x' [clang-diagnostic-unused-variable]
              x
              ^
    /home/runner/work/sleep-proxy/sleep-proxy/build/../src/emulateHost.cpp:32:7: warning: redundant boolean literal in if statement condition [readability-simplify-boolean-expr]
      if (false) {
      ~~~~^~~~~~~~
    /home/runner/work/sleep-proxy/sleep-proxy/build/../src/emulateHost.cpp:33:18: warning: variable 'sptr' is not initialized [cppcoreguidelines-init-variables]
              std::string * sptr;
                            ^
    note: this fix will not be applied because it overlaps with another fix
    /home/runner/work/sleep-proxy/sleep-proxy/build/../src/emulateHost.cpp:40:15: warning: do not use pointer arithmetic [cppcoreguidelines-pro-bounds-pointer-arithmetic]
        setup_log(argv[0], 0, LOG_DAEMON);
                  ^

[With annotations:](https://github.com/lurtz/sleep-proxy/actions/runs/4213473474/jobs/7313201206)

    INFO:CPP Linter:Running "clang-tidy-14 --export-fixes=clang_tidy_output.yml -p /home/runner/work/sleep-proxy/sleep-proxy/build --extra-arg= src/emulateHost.cpp"
    DEBUG:CPP Linter:Output from clang-tidy:
    src/emulateHost.cpp:28:4: warning: unused variable 'x' [clang-diagnostic-unused-variable]
              x
              ^
    /home/runner/work/sleep-proxy/sleep-proxy/src/emulateHost.cpp:32:7: warning: redundant boolean literal in if statement condition [readability-simplify-boolean-expr]
      if (false) {
      ~~~~^~~~~~~~
    /home/runner/work/sleep-proxy/sleep-proxy/src/emulateHost.cpp:33:18: warning: variable 'sptr' is not initialized [cppcoreguidelines-init-variables]
              std::string * sptr;
                            ^
    note: this fix will not be applied because it overlaps with another fix
    /home/runner/work/sleep-proxy/sleep-proxy/src/emulateHost.cpp:40:15: warning: do not use pointer arithmetic [cppcoreguidelines-pro-bounds-pointer-arithmetic]
        setup_log(argv[0], 0, LOG_DAEMON);
                  ^

You can see in both cases absolute paths are used, but in the non working case a detour via "build/.." is taken.
> I will create a toy C++ project with CMake and Meson to see the differences in the generated compile_commands.json.

compile_commands.json with CMake and Ninja:

    [
    {
      "directory": "/home/lutz/workspace/tiny-cpp-project/build",
      "command": "/usr/bin/c++ -o CMakeFiles/hello_world.dir/src/main.cpp.o -c /home/lutz/workspace/tiny-cpp-project/src/main.cpp",
      "file": "/home/lutz/workspace/tiny-cpp-project/src/main.cpp"
    }

compile_commands.json with Meson and Ninja:

    [
      {
        "directory": "/home/lutz/workspace/tiny-cpp-project/build_meson",
        "command": "c++ -Ihello_world.p -I. -I.. -fdiagnostics-color=always -D_FILE_OFFSET_BITS=64 -Wall -Winvalid-pch -O0 -g -MD -MQ hello_world.p/src_main.cpp.o -MF hello_world.p/src_main.cpp.o.d -o hello_world.p/src_main.cpp.o -c ../src/main.cpp",
        "file": "../src/main.cpp",
        "output": "hello_world.p/src_main.cpp.o"
      }
    ]

So CMake always uses absolute paths. I do not know if Ninja is to blame, because I used that with CMake as well. Maybe CMake does not use Ninja to generate the compile_commands.json. This evening I will check how CMake and Meson are storing the paths internally.
> I do not know if Ninja is to blame, because I used that with CMake as well. Maybe CMake does not use Ninja to generate the compile_commands.json.

This is a good observation. The discrepancy then is likely in the build generator's input to the tool(s) that create the database. 

### Looking at the CMake source
It isn't immediately clear how this is done with ninja; [they set certain target properties](https://gitlab.kitware.com/cmake/cmake/-/blob/master/Source/cmNinjaTargetGenerator.cxx#L1755) when the target property `EXPORT_COMPILE_COMMANDS` is asserted.

### Looking at the Meson source
It is a little clearer how this is done, but I don't know if [the args passed to `ninja -t compdb`](https://github.com/mesonbuild/meson/blob/019fda91d0cb4308803c32e8d9dd311b85caf702/mesonbuild/backend/ninjabackend.py#L667) are the point of error. Based on my probing of `ninja -t compdb`, I believe the build.jinja file specified by Meson is using the relative redirects in certain paths.
You are right Meson is using relative paths. Look at the build.ninja file.
CMake:

    lutz@brutus:~/workspace/tiny-cpp-project/build$ grep src build.ninja 
    build CMakeFiles/hello_world.dir/src/main.cpp.o: CXX_COMPILER__hello_world_ /home/lutz/workspace/tiny-cpp-project/src/main.cpp || cmake_object_order_depends_target_hello_world

Meson:

    lutz@brutus:~/workspace/tiny-cpp-project/build_meson$ grep src build.ninja 
    build hello_world.p/src_main.cpp.o: cpp_COMPILER ../src/main.cpp

By looking at the [compile_commands.json](https://clang.llvm.org/docs/JSONCompilationDatabase.html) if find the following statement;

    directory: The working directory of the compilation. All paths specified in the command or file fields must be either absolute or relative to this directory.

I would say Meson is not to blame. What it does is within the specification. Tools which always expect absolute paths are too strict.
> Tools which always expect absolute paths are too strict.

clang-tools are famous for their strictness.
And now for the bad news...

The way I see it, it is not within this action's scope to post-process a compilation database. Thus I don't see a way that we can address this issue from our side -- not unless Meson is persuaded to use absolute paths when generating the ninja build file(s).

On a side note, it may be within the specifications to use relative paths, but absolute paths are preferred in many best practiced conventions. We also made the move to ensure the path specified to our `database` option was resolved to an absolute path (mostly because of troubles with historically using a docker container). 
You could add some documentation. E.g. meson users have to use the database option to specify the location of the file. That could be a place to tell Meson users that they need at least clang-tidy-14 and that they have to modify the compile_commands.json so that e.g. in no path repoorted by clang-tidy the pattern  "../src" appears.

I got it working with two sed commands and helpful documentation would prevent people opening issues in the future.
I still haven't tried my idea to fix the path that clang-tidy outputs when using a compilation database. But if that fails, then I agree some mentions in the README (and perhaps a link to this issue) would be helpful.
Are running the action in the repo's root folder? This is required to properly match a file path with any repo files.
Is `/__w/` the repo root?

You can try setting the [`repo-root`](https://github.com/cpp-linter/cpp-linter-action#repo-root) option to `'__w'` or whatever the repo root is.
hmm. this is odd.
the clang-tidy output shows the file name as
```
../src/modules/hyprland/workspaces.cpp
```
Our function for parsing clang-tidy's stdout resolved that path to
```
/__w/Waybar/src/modules/hyprland/workspaces.cpp
```
But I think the actual repo path (in absolute form) is checked out to
```
/__w/Waybar/Waybar/.git/
```
as reported by the step for `actions/checkout`
I think this is somehow related to how the docker image is setup. The docker command used to configure the runner is
```
/usr/bin/docker create \
--name 65d119c2c1744afaac22e68da63f8464_alexayswaybardebian_0b3155 \
--label 16341d \
--workdir /__w/Waybar/Waybar \
--network github_network_309ce9dd3cad431c89794898ff30ee61  \
-e "HOME=/github/home" \
-e GITHUB_ACTIONS=true \
-e CI=true \
-v "/var/run/docker.sock":"/var/run/docker.sock" \
-v "/home/runner/work":"/__w" \
-v "/home/runner/runners/2.311.0/externals":"/__e":ro \
-v "/home/runner/work/_temp":"/__w/_temp" \
-v "/home/runner/work/_actions":"/__w/_actions" \
-v "/opt/hostedtoolcache":"/__t" \
-v "/home/runner/work/_temp/_github_home":"/github/home" \
-v "/home/runner/work/_temp/_github_workflow":"/github/workflow" \
--entrypoint "tail" alexays/waybar:debian "-f" "/dev/null"
```
We had to migrate away from using our own docker image in v1 because clang-tidy was unable to find/read the compilation database. The paths exposed from docker containers really screw with the how file system is perceived by clang-tidy. I didn't see a robust way to detect that, so we made cpp-linter-action a composite-based action (without using a docker image for the installed clang-tidy/format).
I also still think that part of the reason why clang-tidy output the file's path as a relative path is because meson uses relative paths in their generated compilation database (which is why cpp-linter/cpp-linter#30 could not be solved on our end).
@zjeffer The solution to using mesaon's compilation database (in cpp-linter/cpp-linter#30) was to manually adjust all relative paths into absolute paths:
```yml
      - name: Massage compile_commands.json so that cpp-linter-action works
        run: |
          sed -i 's/\/build//g' ${{github.workspace}}/build/compile_commands.json
          sed -i 's/\.\.\///g' ${{github.workspace}}/build/compile_commands.json
```
This might need tweaking for your specific project structure, but I hope it helps.
I made a branch called `process-compilation-database` that crawls the compile_commands.json file (if `database` was specified and the file exists). It first tried to match relative paths found in the database as relative to the compilation unit's directory.

@zjeffer to test it with a meson build, you'll need to either run the cpp-linter package directly after installing from git source:
```
pip install git+https://github.com/cpp-linter/cpp-linter/@process-compilation-database

cpp-linter --database build
``` 
or use a fork of cpp-linter-action that changes the dependency named in requirements.txt:
```diff
-cpp-linter==1.6.4
+git+https://github.com/cpp-linter/cpp-linter/@process-compilation-database
```

---
I'm not enthusiastic about this idea because the fact that meson uses relative paths in their compile_commands.json seems like a problem with meson. However, they don't seem interested in changing that because clang-tools docs state that the database can use either relative or absolute paths... despite that it is just best practice to use absolute paths whenever possible.

Fix patch:
diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml
index b447fa89..72c12a6a 100644
--- a/.github/workflows/build-docs.yml
+++ b/.github/workflows/build-docs.yml
@@ -6,9 +6,9 @@ jobs:
   build:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
-      - uses: actions/setup-python@v4
+      - uses: actions/setup-python@v5
         with:
           python-version: 3.x
 
@@ -19,7 +19,7 @@ jobs:
         run: sphinx-build docs docs/_build/html
 
       - name: upload docs build as artifact
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: "cpp-linter_docs"
           path: ${{ github.workspace }}/docs/_build/html
diff --git a/.github/workflows/pre-commit-hooks.yml b/.github/workflows/pre-commit-hooks.yml
index e9426fc7..0df19030 100644
--- a/.github/workflows/pre-commit-hooks.yml
+++ b/.github/workflows/pre-commit-hooks.yml
@@ -9,8 +9,8 @@ jobs:
   check-source-files:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
-      - uses: actions/setup-python@v4
+      - uses: actions/checkout@v4
+      - uses: actions/setup-python@v5
         with:
           python-version: '3.x'
       - run: python3 -m pip install pre-commit
diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml
index 278717b1..49cfb957 100644
--- a/.github/workflows/publish-pypi.yml
+++ b/.github/workflows/publish-pypi.yml
@@ -23,12 +23,12 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       # use fetch --all for setuptools_scm to work
       with:
         fetch-depth: 0
     - name: Set up Python
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: '3.x'
     - name: Install dependencies
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index e2e84520..fb88820a 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -35,4 +35,4 @@ repos:
         types: [python]
         entry: mypy
         exclude: "^(docs/|setup.py$)"
-        additional_dependencies: [mypy, types-pyyaml, types-requests, rich, requests, pytest, pyyaml, '.']
+        additional_dependencies: [mypy, types-pyyaml, types-requests, rich, requests, pytest, pyyaml, meson, '.']
diff --git a/cpp_linter/clang_tidy.py b/cpp_linter/clang_tidy.py
index e5db3423..2a4a39df 100644
--- a/cpp_linter/clang_tidy.py
+++ b/cpp_linter/clang_tidy.py
@@ -2,7 +2,7 @@
 from pathlib import Path, PurePath
 from textwrap import indent
 import re
-from typing import Tuple, Union, List, cast
+from typing import Tuple, Union, List, cast, Optional, Dict
 from . import GlobalParser, CLANG_TIDY_STDOUT
 
 NOTE_HEADER = re.compile(r"^(.+):(\d+):(\d+):\s(\w+):(.*)\[([a-zA-Z\d\-\.]+)\]$")
@@ -20,6 +20,7 @@ class TidyNotification:
     def __init__(
         self,
         notification_line: Tuple[str, Union[int, str], Union[int, str], str, str, str],
+        database: Optional[List[Dict[str, str]]] = None,
     ):
         # logger.debug("Creating tidy note from line %s", notification_line)
         (
@@ -40,13 +41,31 @@ def __init__(
         #: The line number of the source file.
         self.line = int(self.line)
         self.cols = int(self.cols)
-        #: The source filename concerning the notification.
-        self.filename = (
+        rel_path = (
             Path(self.filename)
             .resolve()
             .as_posix()
             .replace(Path.cwd().as_posix() + "/", "")
         )
+        if not PurePath(self.filename).is_absolute() and database is not None:
+                # get absolute path from compilation database:
+                # This is need for meson builds as they use paths relative to
+                # the build env (or wherever the database is usually located).
+                for unit in database:
+                    if (
+                        "file" in unit
+                        and "directory" in unit
+                        and unit["file"] == self.filename
+                    ):
+                        rel_path = (
+                            Path(unit["directory"], unit["file"])
+                            .resolve()
+                            .as_posix()
+                            .replace(Path.cwd().as_posix() + "/", "")
+                        )
+                        break
+        #: The source filename concerning the notification.
+        self.filename = rel_path
         #: A `list` of lines for the code-block in the notification.
         self.fixit_lines: List[str] = []
 
@@ -93,7 +112,7 @@ def log_command(self) -> str:
         )
 
 
-def parse_tidy_output() -> None:
+def parse_tidy_output(database: Optional[List[Dict[str, str]]]) -> None:
     """Parse clang-tidy output in a file created from stdout. The results are
     saved to :attr:`~cpp_linter.GlobalParser.tidy_notes`."""
     notification = None
@@ -105,7 +124,8 @@ def parse_tidy_output() -> None:
                 cast(
                     Tuple[str, Union[int, str], Union[int, str], str, str, str],
                     match.groups(),
-                )
+                ),
+                database,
             )
             GlobalParser.tidy_notes.append(notification)
         elif notification is not None:
diff --git a/cpp_linter/run.py b/cpp_linter/run.py
index 6974f622..da17df33 100644
--- a/cpp_linter/run.py
+++ b/cpp_linter/run.py
@@ -16,7 +16,7 @@
 import json
 import urllib.parse
 import logging
-from typing import cast, List, Tuple, Optional
+from typing import cast, List, Tuple, Optional, Dict
 import requests
 from . import (
     Globals,
@@ -202,7 +202,7 @@ def filter_out_non_source_files(
         # dump altered json of changed files
         CHANGED_FILES_JSON.write_text(
             json.dumps(
-                [ f.serialize() for f in Globals.FILES ],
+                [f.serialize() for f in Globals.FILES],
                 indent=2,
             ),
             encoding="utf-8",
@@ -275,7 +275,6 @@ def run_clang_tidy(
     checks: str,
     lines_changed_only: int,
     database: str,
-    repo_root: str,
     extra_args: List[str],
 ) -> None:
     """Run clang-tidy on a certain file.
@@ -311,16 +310,15 @@ def run_clang_tidy(
         CLANG_TIDY_STDOUT.write_bytes(b"")
         return
     filename = file_obj.name.replace("/", os.sep)
-    cmds = [
-        assemble_version_exec("clang-tidy", version),
-        f"--export-fixes={str(CLANG_TIDY_YML)}",
-    ]
+    cmds = [assemble_version_exec("clang-tidy", version)]
+    if "CPP_LINTER_TEST_ALPHA_CODE" in os.environ:
+        cmds.append(f"--export-fixes={str(CLANG_TIDY_YML)}")
+        # clear yml file's content before running clang-tidy
+        CLANG_TIDY_YML.write_bytes(b"")
     if checks:
         cmds.append(f"-checks={checks}")
     if database:
         cmds.append("-p")
-        if not PurePath(database).is_absolute():
-            database = str(Path(RUNNER_WORKSPACE, repo_root, database).resolve())
         cmds.append(database)
     line_ranges = {
         "name": filename,
@@ -334,13 +332,11 @@ def run_clang_tidy(
     for extra_arg in extra_args:
         cmds.append(f"--extra-arg={extra_arg}")
     cmds.append(filename)
-    # clear yml file's content before running clang-tidy
-    CLANG_TIDY_YML.write_bytes(b"")
     logger.info('Running "%s"', " ".join(cmds))
     results = subprocess.run(cmds, capture_output=True)
     CLANG_TIDY_STDOUT.write_bytes(results.stdout)
     logger.debug("Output from clang-tidy:\n%s", results.stdout.decode())
-    if CLANG_TIDY_YML.stat().st_size:
+    if "CPP_LINTER_TEST_ALPHA_CODE" in os.environ and CLANG_TIDY_YML.stat().st_size:
         parse_tidy_suggestions_yml()  # get clang-tidy fixes from yml
     if results.stderr:
         logger.debug(
@@ -391,6 +387,7 @@ def create_comment_body(
     file_obj: FileObj,
     lines_changed_only: int,
     tidy_notes: List[TidyNotification],
+    database: Optional[List[Dict[str, str]]],
 ):
     """Create the content for a thread comment about a certain file.
     This is a helper function to `capture_clang_tools_output()`.
@@ -403,7 +400,7 @@ def create_comment_body(
     """
     ranges = file_obj.range_of_changed_lines(lines_changed_only)
     if CLANG_TIDY_STDOUT.exists() and CLANG_TIDY_STDOUT.stat().st_size:
-        parse_tidy_output()  # get clang-tidy fixes from stdout
+        parse_tidy_output(database)  # get clang-tidy fixes from stdout
         comment_output = ""
         for fix in GlobalParser.tidy_notes:
             if lines_changed_only and fix.line not in ranges:
@@ -453,6 +450,15 @@ def capture_clang_tools_output(
     :param extra_args: A list of extra arguments used by clang-tidy as compiler
         arguments.
     """
+
+    if database and not PurePath(database).is_absolute():
+        database = str(Path(RUNNER_WORKSPACE, repo_root, database).resolve())
+    db_json: Optional[List[Dict[str, str]]] = None
+    if database:
+        db_path = Path(database, "compile_commands.json")
+        if db_path.exists():
+            db_json = json.loads(db_path.read_text(encoding="utf-8"))
+
     # temporary cache of parsed notifications for use in log commands
     tidy_notes: List[TidyNotification] = []
     for file in Globals.FILES:
@@ -463,13 +469,12 @@ def capture_clang_tools_output(
             checks,
             lines_changed_only,
             database,
-            repo_root,
             extra_args,
         )
         run_clang_format(file, version, style, lines_changed_only)
         end_log_group()
 
-        create_comment_body(file, lines_changed_only, tidy_notes)
+        create_comment_body(file, lines_changed_only, tidy_notes, db_json)
 
     if Globals.FORMAT_COMMENT or Globals.TIDY_COMMENT:
         Globals.OUTPUT += ":warning:\nSome files did not pass the configured checks!\n"
diff --git a/pyproject.toml b/pyproject.toml
index 45739d5c..5fcbc3b9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -67,6 +67,9 @@ testpaths = ["tests"]
 [tool.coverage]
 [tool.coverage.run]
 dynamic_context = "test_function"
+# These options are useful if combining coverage data from multiple tested envs
+parallel = true
+relative_files = true
 omit = [
     # don't include tests in coverage
     "tests/*",
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 9b3d3673..7485c1ee 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,4 +1,5 @@
 coverage[toml]
+meson
 mypy
 pre-commit
 pytest


Test patch:
diff --git a/.github/workflows/run-dev-tests.yml b/.github/workflows/run-dev-tests.yml
index 03880548..ee07ca46 100644
--- a/.github/workflows/run-dev-tests.yml
+++ b/.github/workflows/run-dev-tests.yml
@@ -23,14 +23,14 @@ jobs:
   build:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
-      - uses: actions/setup-python@v4
+      - uses: actions/checkout@v4
+      - uses: actions/setup-python@v5
         with:
           python-version: '3.x'
       - name: Build wheel
         run: python3 -m pip wheel --no-deps -w dist .
       - name: Upload wheel as artifact
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: cpp-linter_wheel
           path: ${{ github.workspace }}/dist/*.whl
@@ -40,34 +40,20 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        py: ['3.7', '3.8', '3.9', '3.10']
+        py: ['3.8', '3.9', '3.10', '3.11']
         os: ['windows-latest', ubuntu-22.04]
-        version: ['15', '14', '13', '12', '11', '10', '9', '8', '7']
-        include:
-          # mark all matrix jobs as "need to install clang-tools"
-          - tools_dir: 'temp'
-
-          # mark certain matrix jobs as "use natively installed clang-tools"
-          - os: 'windows-latest'
-            version: '14'
-            tools_dir: 'N/A'
-          - os: 'ubuntu-22.04'
-            version: '14'
-            tools_dir: 'N/A'
-          - os: 'ubuntu-22.04'
-            version: '13'
-            tools_dir: 'N/A'
+        version: ['17', '16', '15', '14', '13', '12', '11', '10', '9', '8', '7']
 
     runs-on: ${{ matrix.os }}
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
-      - uses: actions/setup-python@v4
+      - uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.py }}
 
       - name: download wheel artifact
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: cpp-linter_wheel
           path: dist
@@ -75,30 +61,80 @@ jobs:
       - name: Install workflow deps
         # using a wildcard as filename on Windows requires a bash shell
         shell: bash
-        run: python3 -m pip install pytest coverage[toml] dist/*.whl
+        run: python3 -m pip install pytest coverage[toml] meson dist/*.whl
+
+      # https://github.com/ninja-build/ninja/wiki/Pre-built-Ninja-packages
+      - name: Install ninja (Linux)
+        if: runner.os == 'Linux'
+        run: sudo apt-get install ninja-build
+      - name: Install ninja (Windows)
+        if: runner.os == 'Windows'
+        run: choco install ninja
+
+      - name: Install Linux clang dependencies
+        if: runner.os == 'Linux'
+        shell: bash
+        run: |
+          sudo apt-get update
+          # First try installing from default Ubuntu repositories before trying LLVM script
+          if ! sudo apt-get install -y clang-format-${{ matrix.version }} clang-tidy-${{ matrix.version }}; then
+            # This LLVM script will add the relevant LLVM PPA: https://apt.llvm.org/
+            wget https://apt.llvm.org/llvm.sh -O ${{ runner.temp }}/llvm_install.sh
+            chmod +x ${{ runner.temp }}/llvm_install.sh
+            if sudo ${{ runner.temp }}/llvm_install.sh ${{ matrix.version }}; then
+              sudo apt-get install -y clang-format-${{ matrix.version }} clang-tidy-${{ matrix.version }}
+            fi
+          fi
 
       - name: Install clang-tools
-        if: matrix.tools_dir == 'temp'
         run: |
           python -m pip install clang-tools
-          clang-tools --install ${{ matrix.version }}  --directory ${{ runner.temp }}/clang-tools
+          clang-tools --install ${{ matrix.version }}
 
-      - name: Collect Coverage (native clang install)
-        if: matrix.tools_dir == 'N/A'
+      - name: Collect Coverage
         env:
           CLANG_VERSION: ${{ matrix.version }}
         run: coverage run -m pytest
 
-      - name: Collect Coverage (non-native clang install)
-        if: matrix.tools_dir == 'temp'
-        env:
-          CLANG_VERSION: ${{ runner.temp }}/clang-tools
-        run: coverage run -m pytest
+      - name: Upload coverage data
+        uses: actions/upload-artifact@v4
+        with:
+          name: coverage-data-${{ runner.os }}-py${{ matrix.py }}-${{ matrix.version }}
+          path: .coverage*
+
+  coverage-report:
+    needs: [test]
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Download all artifacts
+        uses: actions/download-artifact@v4
+        with:
+          path: ci-artifacts
+
+      - run: mv ci-artifacts/**/.coverage* ./
+
+      - name: Setup python
+        uses: actions/setup-python@v5
+        with:
+          python-version: '3.x'
+
+      - name: Create coverage report
+        run: |
+          pip3 install coverage[toml]
+          coverage combine
+          coverage html
+
+      - name: Upload comprehensive coverage HTML report
+        uses: actions/upload-artifact@v4
+        with:
+          name: coverage-report
+          path: htmlcov/
 
       - run: coverage report && coverage xml
 
       - uses: codecov/codecov-action@v3
-        if: matrix.os == 'ubuntu-22.04' && matrix.version == '12' && matrix.py == '3.10'
         with:
           token: ${{secrets.CODECOV_TOKEN}}
           files: ./coverage.xml
diff --git a/tests/capture_tools_output/test_database_path.py b/tests/capture_tools_output/test_database_path.py
index f465f1e1..793a9d21 100644
--- a/tests/capture_tools_output/test_database_path.py
+++ b/tests/capture_tools_output/test_database_path.py
@@ -4,12 +4,16 @@
 import logging
 import os
 import re
+import sys
+import shutil
 import pytest
 from cpp_linter import logger, FileObj
 import cpp_linter.run
-from cpp_linter.run import run_clang_tidy
+import cpp_linter
+from cpp_linter.run import capture_clang_tools_output, make_annotations
+from mesonbuild.mesonmain import main as meson  # type: ignore[import-untyped]
 
-CLANG_TIDY_COMMAND = re.compile(r'"clang-tidy(.*)"')
+CLANG_TIDY_COMMAND = re.compile(r'clang-tidy[^\s]*\s(.*)"')
 
 ABS_DB_PATH = str(Path("tests/demo").resolve())
 
@@ -20,7 +24,7 @@
         # implicit path to the compilation database
         ("", []),
         # explicit relative path to the compilation database
-        ("../../demo", ["-p", ABS_DB_PATH]),
+        ("demo", ["-p", ABS_DB_PATH]),
         # explicit absolute path to the compilation database
         (ABS_DB_PATH, ["-p", ABS_DB_PATH]),
     ],
@@ -37,25 +41,91 @@ def test_db_detection(
     monkeypatch.chdir(PurePath(__file__).parent.as_posix())
     cpp_linter.CACHE_PATH.mkdir(exist_ok=True)
     demo_src = "../demo/demo.cpp"
-    rel_root = str(Path(*Path(__file__).parts[-2:]))
-    cpp_linter.run.RUNNER_WORKSPACE = (
-        Path(pytestconfig.rootpath, "tests").resolve().as_posix()
+    monkeypatch.setattr(
+        cpp_linter.run,
+        "RUNNER_WORKSPACE",
+        Path(pytestconfig.rootpath, "tests").resolve().as_posix(),
     )
+    monkeypatch.setattr(cpp_linter.Globals, "FILES", [FileObj(demo_src, [], [])])
+
     caplog.set_level(logging.DEBUG, logger=logger.name)
-    run_clang_tidy(
-        file_obj=FileObj(demo_src, [], []),  # not filtering lines
-        version="",
+    capture_clang_tools_output(
+        version=os.getenv("CLANG_VERSION", "12"),
         checks="",  # let clang-tidy use a .clang-tidy config file
+        style="",  # don't invoke clang-format
         lines_changed_only=0,  # analyze complete file
         database=database,
-        repo_root=rel_root,
+        repo_root=".",
         extra_args=[],
     )
     matched_args = []
     for record in caplog.records:
+        assert "Error while trying to load a compilation database" not in record.message
         msg_match = CLANG_TIDY_COMMAND.search(record.message)
         if msg_match is not None:
-            matched_args = msg_match.group(0)[:-1].split()[2:]
-        assert "Error while trying to load a compilation database" not in record.message
-    expected_args.append(demo_src.replace("/", os.sep))
-    assert matched_args == expected_args
+            matched_args = msg_match.group(0).split()[1:]
+            break
+    else:
+        raise RuntimeError("failed to find args passed in clang-tidy in log records")
+    expected_args.append(demo_src.replace("/", os.sep) + '"')
+    assert expected_args == matched_args
+
+
+def test_ninja_database(
+    monkeypatch: pytest.MonkeyPatch, tmp_path: Path, caplog: pytest.LogCaptureFixture
+):
+    """verify that the relative paths used in a database generated (and thus clang-tidy
+    stdout) for the ninja build system are resolved accordingly."""
+    tmp_path_demo = tmp_path / "demo"
+    # generate the project
+    shutil.copytree(
+        str(Path(__file__).parent.parent / "demo"),
+        str(tmp_path_demo),
+        ignore=shutil.ignore_patterns("compile_flags.txt"),
+    )
+    (tmp_path_demo / "build").mkdir(parents=True)
+    monkeypatch.chdir(str(tmp_path_demo))
+    monkeypatch.setattr(sys, "argv", ["meson", "init"])
+    meson()
+    monkeypatch.setattr(
+        sys, "argv", ["meson", "setup", "--backend=ninja", "build", "."]
+    )
+    meson()
+
+    caplog.set_level(logging.DEBUG, logger=cpp_linter.logger.name)
+    monkeypatch.setattr(
+        cpp_linter.Globals,
+        "FILES",
+        [FileObj("demo.cpp", [], [])],
+    )
+    for attr in ["OUTPUT", "FORMAT_COMMENT", "TIDY_COMMENT"]:
+        monkeypatch.setattr(cpp_linter.Globals, attr, "")
+    for attr in ["tidy_notes", "format_advice"]:
+        monkeypatch.setattr(cpp_linter.GlobalParser, attr, [])
+    for attr in ["tidy_failed_count", "format_failed_count"]:
+        monkeypatch.setattr(cpp_linter.Globals, attr, 0)
+    monkeypatch.setattr(cpp_linter.run, "RUNNER_WORKSPACE", str(tmp_path_demo))
+
+    cpp_linter.CLANG_TIDY_STDOUT.parent.mkdir(parents=True)
+    # run clang-tidy and verify paths of project files were matched with database paths
+    capture_clang_tools_output(
+        version=os.getenv("CLANG_VERSION", "12"),
+        checks="",  # let clang-tidy use a .clang-tidy config file
+        style="",  # don't invoke clang-format
+        lines_changed_only=0,  # analyze complete file
+        database="build",  # point to generated compile_commands.txt
+        repo_root=".",  # already changed cwd
+        extra_args=[],
+    )
+    found_project_file = False
+    for note in cpp_linter.GlobalParser.tidy_notes:
+        if note.filename.endswith("demo.cpp") or note.filename.endswith("demo.hpp"):
+            assert not Path(note.filename).is_absolute()
+            found_project_file = True
+    if not found_project_file:
+        raise RuntimeError("no project files raised concerns with clang-tidy")
+    assert make_annotations("", True, 0)
+    # write step-summary for manual verification
+    Path(tmp_path, "job_summary.md").write_text(
+        cpp_linter.Globals.OUTPUT, encoding="utf-8"
+    )
diff --git a/tests/capture_tools_output/test_tools_output.py b/tests/capture_tools_output/test_tools_output.py
index 4c1901f4..ccb4db6a 100644
--- a/tests/capture_tools_output/test_tools_output.py
+++ b/tests/capture_tools_output/test_tools_output.py
@@ -352,6 +352,7 @@ def test_diff_comment(
     comments (not annotations) in the event's diff.
 
     Remember, diff comments should only focus on lines in the diff."""
+    monkeypatch.setenv("CPP_LINTER_TEST_ALPHA_CODE", "true")
     prep_tmp_dir(
         tmp_path,
         monkeypatch,