Skip to content
Draft
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 .clang-tidy
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ Checks: >
-cppcoreguidelines-macro-usage,
-cppcoreguidelines-non-private-member-variables-in-classes,
-cppcoreguidelines-avoid-non-const-global-variables,
-cppcoreguidelines-pro-*
-cppcoreguidelines-pro-*
11 changes: 8 additions & 3 deletions .devcontainer/Dockerfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,18 @@ COPY ci /opt/ci

RUN apt update && apt install -y wget \
ninja-build \
llvm-dev \
libclang-dev \
clang-tidy \
llvm-20-dev \
libclang-20-dev \
clang-tidy-20 \
shellcheck \
sudo \
cmake

RUN update-alternatives --install /usr/bin/clang-tidy clang-tidy /usr/bin/clang-tidy-20 200 && \
update-alternatives --install /usr/bin/llvm-config llvm-config /usr/bin/llvm-config-20 200 && \
update-alternatives --config clang-tidy && \
update-alternatives --config llvm-config

RUN cd /opt/ci && bash setup_ci_environment.sh
RUN cd /opt/ci && bash install_iwyu.sh

Expand Down
67 changes: 46 additions & 21 deletions .github/workflows/clang-tidy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,11 @@ jobs:
run: |
sudo -E ./ci/install_thirdparty.sh --install-dir /usr/local --tags-file third_party_release --packages "ryml"

- name: Check clang-tidy
- name: Install clang-tidy-20
run: |
if ! command -v clang-tidy &> /dev/null; then
echo "clang-tidy could not be found"
exit 1
fi
sudo apt install -y clang-tidy-20
sudo update-alternatives --install /usr/bin/clang-tidy clang-tidy /usr/bin/clang-tidy-20 200
sudo update-alternatives --config clang-tidy
echo "Using clang-tidy version: $(clang-tidy --version)"
echo "clang-tidy installed at: $(which clang-tidy)"

Expand All @@ -68,37 +67,63 @@ jobs:
CXX: clang++
run: |
echo "Running cmake..."
cmake -B build-${{ matrix.cmake_options }} \
cmake \
-S . \
-B build-${{ matrix.cmake_options }} \
-C ./test_common/cmake/${{ matrix.cmake_options }}.cmake \
-DCMAKE_CXX_STANDARD=14 \
-DWITH_STL=CXX14 \
-DWITH_OPENTRACING=OFF \
-DCMAKE_CXX_FLAGS="-Wno-deprecated-declarations" \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DCMAKE_CXX_CLANG_TIDY="clang-tidy;--quiet;-p;build-${{ matrix.cmake_options }}"
-DCMAKE_CXX_CLANG_TIDY="clang-tidy;--header-filter=.*/opentelemetry-cpp/.*;--exclude-header-filter=.*(internal/absl|third_party|third-party)/.*;--quiet"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to put this logic (header-filter) in the top level file .clang-tidy instead ?


- name: Run clang-tidy
- name: Run clang-tidy and Build
id: build
run: |
cmake --build build-${{ matrix.cmake_options }} -- -j$(nproc) 2>&1 | tee clang-tidy-${{ matrix.cmake_options }}.log
BUILD_LOG=clang-tidy-${{ matrix.cmake_options }}.log
set -o pipefail
cmake --build build-${{ matrix.cmake_options }} -- -j$(nproc) 2>&1 | tee $BUILD_LOG
echo "build_log=$BUILD_LOG" >> "$GITHUB_OUTPUT"

- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
- name: Analyze clang-tidy build log
id: analyze
run: |
SCRIPT_OUTPUT=$(python3 ./ci/create_clang_tidy_report.py \
--job_name ${{ matrix.cmake_options }} \
--build_log clang-tidy-${{ matrix.cmake_options }}.log \
--output ./clang_tidy_report-${{ matrix.cmake_options }}.md)
export $SCRIPT_OUTPUT
echo "Found $TOTAL_WARNINGS unique warnings"
echo "clang-tidy report generated at $REPORT_PATH"
echo "warning_count=$TOTAL_WARNINGS" >> "$GITHUB_OUTPUT"
echo "report_path=$REPORT_PATH" >> "$GITHUB_OUTPUT"
cat $REPORT_PATH >> $GITHUB_STEP_SUMMARY

- name: Upload build log
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: Logs-clang-tidy-${{ matrix.cmake_options }}
path: ./clang-tidy-${{ matrix.cmake_options }}.log
path: ${{ steps.build.outputs.build_log }}

- name: Upload warning report
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: Report-clang-tidy-${{ matrix.cmake_options }}
path: ${{ steps.analyze.outputs.report_path }}

- name: Count warnings
- name: Check Warning Limits
run: |
COUNT=$(grep -c "warning:" clang-tidy-${{ matrix.cmake_options }}.log)
echo "clang-tidy reported ${COUNT} warning(s) with cmake options preset '${{ matrix.cmake_options }}'"
readonly COUNT="${{ steps.analyze.outputs.warning_count }}"
readonly LIMIT="${{ matrix.warning_limit }}"

readonly WARNING_LIMIT=${{ matrix.warning_limit }}
echo "clang-tidy reported ${COUNT} unique warning(s) with preset '${{ matrix.cmake_options }}'"
echo "Limit is ${LIMIT}"

# FAIL the build if COUNT > WARNING_LIMIT
if [ $COUNT -gt $WARNING_LIMIT ] ; then
echo "clang-tidy reported ${COUNT} warning(s) exceeding the existing warning limit of ${WARNING_LIMIT} with cmake options preset '${{ matrix.cmake_options }}'"
if [ "$COUNT" -gt "$LIMIT" ]; then
echo "::error::clang-tidy reported ${COUNT} warning(s) exceeding the limit of ${LIMIT}"
exit 1
# WARN in annotations if COUNT > 0
elif [ $COUNT -gt 0 ] ; then
echo "::warning::clang-tidy reported ${COUNT} warning(s) with cmake options preset '${{ matrix.cmake_options }}'"
elif [ "$COUNT" -gt 0 ]; then
echo "::warning::clang-tidy reported ${COUNT} warning(s) within the limit of ${LIMIT}"
fi

172 changes: 172 additions & 0 deletions ci/create_clang_tidy_report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0

import argparse
import re
import sys
from collections import defaultdict
from enum import Enum
from pathlib import Path
from typing import Dict, List, NamedTuple, Optional, Set

# --- Configuration ---
REPO_NAME = "opentelemetry-cpp"
MAX_ROWS = 1000
WARNING_RE = re.compile(
r"^(?P<file>.+):(?P<line>\d+):(?P<col>\d+): warning: (?P<msg>.+) "
r"\[(?P<check>.+)\]$"
)
ANSI_RE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")


class OutputKeys(str, Enum):
TOTAL_WARNINGS = "TOTAL_WARNINGS"
REPORT_PATH = "REPORT_PATH"


class ClangTidyWarning(NamedTuple):
file: str
line: int
col: int
msg: str
check: str


def clean_path(path: str) -> str:
"""Strip path prefix to make it relative to the repo or CWD."""
if f"{REPO_NAME}/" in path:
return path.split(f"{REPO_NAME}/", 1)[1]
try:
return str(Path(path).relative_to(Path.cwd()))
except ValueError:
return path


def parse_log(log_path: Path) -> Set[ClangTidyWarning]:
if not log_path.exists():
sys.exit(f"[ERROR] Log not found: {log_path}")
unique = set()
with log_path.open("r", encoding="utf-8", errors="replace") as f:
for line in f:
line = ANSI_RE.sub("", line.strip())
if "warning:" not in line:
continue
match = WARNING_RE.match(line)
if match:
unique.add(
ClangTidyWarning(
clean_path(match.group("file")),
int(match.group("line")),
int(match.group("col")),
match.group("msg"),
match.group("check"),
)
)
return unique


def generate_report(
warnings: Set[ClangTidyWarning],
output_path: Path,
job_name: Optional[str] = None,
):
by_check: Dict[str, List[ClangTidyWarning]] = defaultdict(list)
by_file: Dict[str, List[ClangTidyWarning]] = defaultdict(list)

for w in warnings:
by_check[w.check].append(w)
by_file[w.file].append(w)

with output_path.open("w", encoding="utf-8") as md:
title = "# "
if job_name:
title += f"{job_name}"

md.write(
f"{title} `clang-tidy` job \t[**{len(warnings)} warnings**]\n\n"
)
md.write(f"<details><summary><b>{'Warnings breakdown'}</b><i> - Click to expand</i></summary>\n\n")

def write_section(
title, data, item_sort_key, header, row_fmt, group_key, reverse
):
md.write(f"## {title}\n")
sorted_groups = sorted(
data.items(), key=group_key, reverse=reverse
)
for key, items in sorted_groups:
md.write(
f"<details><summary><b>{key} ({len(items)})</b></summary>"
f"\n\n{header}\n"
)
for i, w in enumerate(sorted(items, key=item_sort_key)):
if i >= MAX_ROWS:
remaining = len(items) - i
md.write(
f"| ... | ... | *{remaining} more omitted...* |\n"
)
break
md.write(row_fmt(w) + "\n")
md.write("\n</details>\n\n")

# Warnings by File: Sorted Alphabetically
write_section(
"Warnings by File",
by_file,
item_sort_key=lambda w: w.line,
header="| Line | Check | Message |\n|---|---|---|",
row_fmt=lambda w: f"| {w.line} | `{w.check}` | {w.msg} |",
group_key=lambda x: x[0],
reverse=False,
)

# Warnings by clang-tidy check: Sort by Warning count
write_section(
"Warnings by `clang-tidy` Check",
by_check,
item_sort_key=lambda w: (w.file, w.line),
header="| File | Line | Message |\n|---|---|---|",
row_fmt=lambda w: f"| `{w.file}` | {w.line} | {w.msg} |",
group_key=lambda x: len(x[1]),
reverse=True,
)

md.write(f"</details>\n")
md.write("\n----\n")


def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"-l",
"--build_log",
type=Path,
required=True,
help="Clang-tidy log file",
)
parser.add_argument(
"-o",
"--output",
type=Path,
default="clang_tidy_report.md",
help="Output report path",
)
parser.add_argument(
"-j",
"--job_name",
type=str,
help="Job name to include in the report title",
)
args = parser.parse_args()

warnings = parse_log(args.build_log)
generate_report(warnings, args.output, args.job_name)

sys.stdout.write(f"{OutputKeys.TOTAL_WARNINGS.value}={len(warnings)}\n")
if args.output.exists():
sys.stdout.write(f"{OutputKeys.REPORT_PATH.value}={args.output.resolve()}\n")
else:
sys.exit(f"[ERROR] Failed to write report: {args.output.resolve()}")

if __name__ == "__main__":
main()
13 changes: 10 additions & 3 deletions ci/do_ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -341,17 +341,24 @@ elif [[ "$1" == "cmake.legacy.test" ]]; then
elif [[ "$1" == "cmake.clang_tidy.test" ]]; then
cd "${BUILD_DIR}"
rm -rf *
export BUILD_ROOT="${BUILD_DIR}"
clang-tidy --version
LOG_FILE="${BUILD_DIR}/opentelemetry-cpp-clang-tidy.log"
cmake -S ${SRC_DIR} \
-B ${BUILD_DIR} \
-C ${SRC_DIR}/test_common/cmake/all-options-abiv2-preview.cmake \
"${CMAKE_OPTIONS[@]}" \
-DWITH_OPENTRACING=OFF \
-DCMAKE_CXX_FLAGS="-Wno-deprecated-declarations" \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DCMAKE_CXX_CLANG_TIDY="clang-tidy;--quiet;-p;${BUILD_DIR}"
make -j $(nproc)
-DCMAKE_CXX_CLANG_TIDY="clang-tidy;--header-filter=.*/opentelemetry-cpp/.*;--exclude-header-filter=.*(internal/absl|third_party|third-party)/.*;--quiet"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to put this logic (header-filter) in the top level file .clang-tidy instead ?

It is desirable to have all the clang-tidy configuration in the same place, independent of the workflow & makefiles.

make -j $(nproc) 2>&1 | tee "$LOG_FILE"
make test
SCRIPT_OUTPUT=$(python3 ${SRC_DIR}/ci/create_clang_tidy_report.py \
--build_log "$LOG_FILE" \
--output clang_tidy_report.md)
export $SCRIPT_OUTPUT
echo "total warnings = $TOTAL_WARNINGS"
echo "clang-tidy report generated at $REPORT_PATH"
exit 0
elif [[ "$1" == "cmake.legacy.exporter.otprotocol.test" ]]; then
cd "${BUILD_DIR}"
Expand Down
Loading