diff --git a/.clang-tidy b/.clang-tidy index 452fbaf014..599a02fd88 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -38,4 +38,4 @@ Checks: > -cppcoreguidelines-macro-usage, -cppcoreguidelines-non-private-member-variables-in-classes, -cppcoreguidelines-avoid-non-const-global-variables, - -cppcoreguidelines-pro-* \ No newline at end of file + -cppcoreguidelines-pro-* diff --git a/.devcontainer/Dockerfile.dev b/.devcontainer/Dockerfile.dev index 60efed9723..fec4e16c6a 100644 --- a/.devcontainer/Dockerfile.dev +++ b/.devcontainer/Dockerfile.dev @@ -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 diff --git a/.github/workflows/clang-tidy.yaml b/.github/workflows/clang-tidy.yaml index 16623b4b28..127387ec7e 100644 --- a/.github/workflows/clang-tidy.yaml +++ b/.github/workflows/clang-tidy.yaml @@ -17,9 +17,13 @@ jobs: matrix: include: - cmake_options: all-options-abiv1-preview - warning_limit: 63 + warning_limit: 595 - cmake_options: all-options-abiv2-preview - warning_limit: 63 + warning_limit: 597 + env: + CC: /usr/bin/clang-18 + CXX: /usr/bin/clang++-18 + CXX_STANDARD: '14' # Run clang-tidy on the minimum supported c++ standard steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 @@ -53,52 +57,60 @@ 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)" - - name: Prepare CMake + - name: Build and run clang-tidy + id: build env: - CC: clang - CXX: clang++ + OTELCPP_CMAKE_CACHE_FILE: ${{ matrix.cmake_options }}.cmake + BUILD_DIR: build-${{ matrix.cmake_options }} run: | - echo "Running cmake..." - cmake -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 }}" + ./ci/do_ci.sh cmake.clang_tidy.test + echo "build_log=${BUILD_DIR}/opentelemetry-cpp-clang-tidy.log" >> "$GITHUB_OUTPUT" - - name: Run clang-tidy + - name: Analyze clang-tidy output + id: analyze run: | - cmake --build build-${{ matrix.cmake_options }} -- -j$(nproc) 2>&1 | tee clang-tidy-${{ matrix.cmake_options }}.log + SCRIPT_OUTPUT=$(python3 ./ci/create_clang_tidy_report.py \ + --build_log ${{ steps.build.outputs.build_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 - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + - 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 diff --git a/ci/create_clang_tidy_report.py b/ci/create_clang_tidy_report.py new file mode 100644 index 0000000000..dc8ae0a0f6 --- /dev/null +++ b/ci/create_clang_tidy_report.py @@ -0,0 +1,184 @@ +# 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.+):(?P\d+):(?P\d+): warning: (?P.+) " + r"\[(?P.+)\]$" +) +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, +): + 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 = "#### " + md.write( + f"{title} `clang-tidy` job reported {len(warnings)} warnings\n\n" + ) + + if not warnings: + return + + def write_section( + title, + data, + item_sort_key, + header, + row_fmt, + group_key, + reverse, + summary_col_name, + ): + md.write(f"
{title} - Click to expand \n\n") + sorted_groups = sorted( + data.items(), key=group_key, reverse=reverse + ) + + # Summary Table (Sorted by Count Descending) + summary_groups = sorted( + data.items(), key=lambda x: len(x[1]), reverse=True + ) + md.write("#### Summary\n\n") + md.write(f"| {summary_col_name} | Count |\n|---|---|\n") + for key, items in summary_groups: + md.write(f"| {key} | {len(items)} |\n") + md.write("\n") + + md.write("#### Details\n\n") + for key, items in sorted_groups: + md.write( + f"\n----\n\n**{key}** ({len(items)} warnings)\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") + md.write("
\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, + summary_col_name="File", + ) + + # 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, + summary_col_name="Check", + ) + + 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", + ) + args = parser.parse_args() + + warnings = parse_log(args.build_log) + generate_report(warnings, args.output) + + 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() \ No newline at end of file diff --git a/ci/do_ci.sh b/ci/do_ci.sh index ddeffe74da..eaea9e9643 100755 --- a/ci/do_ci.sh +++ b/ci/do_ci.sh @@ -106,6 +106,12 @@ if [ -n "$CMAKE_TOOLCHAIN_FILE" ]; then CMAKE_OPTIONS+=("-DCMAKE_TOOLCHAIN_FILE=$CMAKE_TOOLCHAIN_FILE") fi +if [ -n "$OTELCPP_CMAKE_CACHE_FILE" ]; then + OTELCPP_CMAKE_CACHE_FILE_PATH="${SRC_DIR}/test_common/cmake/${OTELCPP_CMAKE_CACHE_FILE}" +else + OTELCPP_CMAKE_CACHE_FILE_PATH="${SRC_DIR}/test_common/cmake/all-options-abiv1-preview.cmake" +fi + echo "CMAKE_OPTIONS:" "${CMAKE_OPTIONS[@]}" export CTEST_OUTPUT_ON_FAILURE=1 @@ -339,19 +345,26 @@ elif [[ "$1" == "cmake.legacy.test" ]]; then make test exit 0 elif [[ "$1" == "cmake.clang_tidy.test" ]]; then - cd "${BUILD_DIR}" - rm -rf * - export BUILD_ROOT="${BUILD_DIR}" - cmake -S ${SRC_DIR} \ + rm -rf "${BUILD_DIR}" + mkdir -p "${BUILD_DIR}" + clang-tidy --version + LOG_FILE="${BUILD_DIR}/opentelemetry-cpp-clang-tidy.log" + cmake "${CMAKE_OPTIONS[@]}" \ + -S ${SRC_DIR} \ -B ${BUILD_DIR} \ - -C ${SRC_DIR}/test_common/cmake/all-options-abiv2-preview.cmake \ - "${CMAKE_OPTIONS[@]}" \ + -C ${OTELCPP_CMAKE_CACHE_FILE_PATH} \ -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) - make test + -DCMAKE_CXX_CLANG_TIDY="clang-tidy;--header-filter=.*/opentelemetry-cpp/.*;--exclude-header-filter=.*(internal/absl|third_party|third-party|build.*|/usr|/opt)/.*|.*\.pb\.h;--quiet" + cmake --build "${BUILD_DIR}" -- -j $(nproc) 2>&1 | tee "$LOG_FILE" + if [ ! -s "$LOG_FILE" ]; then + echo "Error: Build log was not created at $LOG_FILE" + exit 1 + fi + echo "Build log written to: $LOG_FILE" + echo "To generate a clang-tidy report, use the following command:" + echo " python3 ./ci/create_clang_tidy_report.py --build_log $LOG_FILE" exit 0 elif [[ "$1" == "cmake.legacy.exporter.otprotocol.test" ]]; then cd "${BUILD_DIR}"