diff --git a/.devcontainer/codespace.code-workspace b/.devcontainer/codespace.code-workspace index 40408383..6e8b66b0 100644 --- a/.devcontainer/codespace.code-workspace +++ b/.devcontainer/codespace.code-workspace @@ -60,6 +60,8 @@ "python.languageServer": "Pylance", "python.terminal.activateEnvironment": false, "python.defaultInterpreterPath": "/opt/spack-environments/phlex-ci/.spack-env/view/bin/python", + "esbonio.server.pythonPath": "/opt/spack-environments/phlex-ci/.spack-env/view/bin/python", + "esbonio.logging.level": "debug", "python.analysis.typeCheckingMode": "basic", "python.analysis.diagnosticMode": "workspace", "python.analysis.exclude": [ @@ -119,5 +121,35 @@ "vscjava.vscode-java-test", "vscjava.vscode-maven" ] + }, + "tasks": { + "version": "2.0.0", + "tasks": [ + { + "label": "Generate clang-tidy Problems Log", + "type": "shell", + "command": "python3 /workspaces/phlex/scripts/clang_tidy_fixes_to_problems.py /workspaces/phlex/clang-tidy-fixes.yaml -o /workspaces/phlex/out/clang-tidy-problems.log --workspace-root /workspaces/phlex", + "problemMatcher": [] + }, + { + "label": "Load clang-tidy Problems", + "type": "shell", + "command": "python3 /workspaces/phlex/scripts/clang_tidy_fixes_to_problems.py /workspaces/phlex/clang-tidy-fixes.yaml -o /workspaces/phlex/out/clang-tidy-problems.log --workspace-root /workspaces/phlex && cat /workspaces/phlex/out/clang-tidy-problems.log", + "problemMatcher": [ + { + "owner": "clang-tidy", + "fileLocation": "absolute", + "pattern": { + "regexp": "^(.*):(\\d+):(\\d+):\\s+(warning|error|note):\\s+(.*)$", + "file": 1, + "line": 2, + "column": 3, + "severity": 4, + "message": 5 + } + } + ] + } + ] } } diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c501ce83..c82690ce 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,8 +8,8 @@ }, "workspaceFolder": "/workspaces/phlex", "remoteUser": "root", - "userEnvProbe": "none", "containerEnv": { + "CMAKE_GENERATOR": "Ninja", "GH_CONFIG_DIR": "/root/.config/gh", "DOCKER_HOST": "unix:///tmp/podman.sock", "CONTAINER_HOST": "unix:///tmp/podman.sock", @@ -20,6 +20,8 @@ "source=${localWorkspaceFolder}/../phlex-examples,target=/workspaces/phlex-examples,type=bind", "source=${localWorkspaceFolder}/../phlex-coding-guidelines,target=/workspaces/phlex-coding-guidelines,type=bind", "source=${localWorkspaceFolder}/../phlex-spack-recipes,target=/workspaces/phlex-spack-recipes,type=bind", + "source=${localEnv:HOME}/.vscode-remote-user-data,target=/root/.vscode-server-insiders/data/User,type=bind", + "source=${localEnv:HOME}/.vscode-remote-machine-data,target=/root/.vscode-server-insiders/data/Machine,type=bind", "source=${localEnv:HOME}/.podman-proxy/podman.sock,target=/tmp/podman.sock,type=bind", "source=${localEnv:HOME}/.aws,target=/root/.aws,type=bind", "source=${localEnv:HOME}/.config/gh,target=/root/.config/gh,type=bind,readonly", @@ -51,8 +53,6 @@ "/opt/spack-environments/phlex-ci/.spack-env/view/lib/python/site-packages" ], "cmake.defaultConfigurePreset": "default", - "cmake.cmakePath": "${workspaceFolder}/.devcontainer/cmake_wrapper.sh", - "cmake.ctestPath": "${workspaceFolder}/.devcontainer/ctest_wrapper.sh", "cmake.generator": "Ninja", "C_Cpp.default.cStandard": "c17", "C_Cpp.default.cppStandard": "c++23", diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index fba350ac..ac4088d8 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -137,11 +137,13 @@ When the developer provides HTTPS links in conversation: ## Workspace Environment Setup -> **Note for AI Agents**: The following environment setup instructions apply primarily when working in CI containers (`phlex-ci`, `phlex-dev`) or devcontainers. Human developers may use different local setups (e.g., native macOS, Linux with system packages, or their own Spack configurations). +> **Note for AI Agents**: The following environment setup instructions apply primarily when working outside the devcontainer (e.g., native macOS, Linux with system packages, or custom Spack configurations). In the devcontainer, `/entrypoint.sh` is sourced automatically by `/root/.bashrc`, so the full Spack environment (including `cmake`, `ninja`, `gcc`, etc.) is available in every terminal session without any additional setup. ### setup-env.sh Integration -When working in CI/container environments, always source `setup-env.sh` before executing commands that depend on the build environment: +**Devcontainer**: Do not source `setup-env.sh` or `/entrypoint.sh` in terminal commands — the environment is already configured. Run build tools directly (e.g., `cmake`, `ctest`, `ninja`). + +**Outside the devcontainer**, source the appropriate script before commands that depend on the build environment: - **Repository version**: `srcs/phlex/scripts/setup-env.sh` - Multi-mode environment setup for developers - Supports standalone repository, multi-project workspace, Spack, and system packages @@ -151,13 +153,10 @@ When working in CI/container environments, always source `setup-env.sh` before e - May exist in multi-project workspace setups - Can supplement or override repository setup-env.sh -Command execution guidelines: +Command execution guidelines (non-devcontainer only): - Use `. ./setup-env.sh && ` for terminal commands in workspaces with root-level setup-env.sh - Use `. srcs/phlex/scripts/setup-env.sh && ` when working in standalone repository -- Ensure VS Code tasks include appropriate `source` command in their definitions -- Terminal sessions should source the setup script to access build tools (gcc, cmake, ninja, etc.) -- VS Code settings should use absolute paths or `${workspaceFolder}/local` rather than environment variables for IntelliSense configuration - Always ensure that the terminal's current working directory is appropriate to the command being issued ### Source Directory Symbolic Links @@ -174,20 +173,16 @@ If the workspace root contains a `srcs/` directory, it may contain symbolic link The project uses Spack for dependency management in CI and container development environments: -- **Spack Environments**: The `scripts/setup-env.sh` script automatically activates Spack environments when available -- **Loading Additional Packages**: If you need tools or libraries not loaded by default, use `spack load ` to bring them into the environment -- **Common Use Cases**: - - `spack load cmake` - Load CMake if not in current environment - - `spack load gcc` - Load specific GCC compiler - - `spack load ninja` - Load Ninja build tool - - `spack load gcovr` - Load coverage reporting tools +- **Devcontainer**: All Spack packages (cmake, gcc, ninja, gcovr, etc.) are pre-activated via `/entrypoint.sh`; no `spack load` commands are needed. +- **Outside the devcontainer**: The `scripts/setup-env.sh` script automatically activates Spack environments when available +- **Loading Additional Packages** (non-devcontainer): If you need tools or libraries not loaded by default, use `spack load ` to bring them into the environment - **Graceful Degradation**: The build system works with system-installed packages when Spack is unavailable - **Recipe Repository**: Changes to Spack recipes should be proposed to `Framework-R-D/phlex-spack-recipes` When suggesting installation of dependencies: -- Prefer sourcing the environment setup script (`scripts/setup-env.sh` or workspace-level `setup-env.sh`) as it handles both Spack and system packages -- For manual installations, provide both Spack (`spack install/load`) and system package manager options +- In the devcontainer, all required tools are already available; no installation steps are needed +- Outside the devcontainer, prefer sourcing the environment setup script (`scripts/setup-env.sh` or workspace-level `setup-env.sh`) as it handles both Spack and system packages - Consult `scripts/README.md` and `scripts/QUICK_REFERENCE.md` for common patterns ## Text Formatting Standards @@ -290,7 +285,7 @@ All Markdown files must strictly follow these markdownlint rules: ### Build and Test -- **Environment**: Always source `setup-env.sh` before building or testing. This applies to all environments (Dev Container, local machine, HPC). +- **Environment**: Always source `setup-env.sh` before building or testing outside the devcontainer. In the devcontainer, the environment is already configured — run build tools directly. - **Configuration**: - **Presets**: Prefer `CMakePresets.json` workflows (e.g., `cmake --preset default`). - **Generator**: Prefer `Ninja` over `Makefiles` when available (`-G Ninja`). diff --git a/.github/workflows/clang-tidy-check.yaml b/.github/workflows/clang-tidy-check.yaml index 50f64ab6..ddc3b1ea 100644 --- a/.github/workflows/clang-tidy-check.yaml +++ b/.github/workflows/clang-tidy-check.yaml @@ -50,7 +50,7 @@ jobs: always() && (github.event_name == 'workflow_dispatch' || needs.setup.outputs.has_changes == 'true') runs-on: ubuntu-24.04 container: - image: ghcr.io/framework-r-d/phlex-ci:latest + image: ghcr.io/framework-r-d/phlex-ci:2026-04-06 steps: - name: Checkout code @@ -72,35 +72,65 @@ jobs: build-type: Debug source-path: ${{ needs.setup.outputs.checkout_path }} build-path: ${{ needs.setup.outputs.build_path }} - extra-options: - "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_CXX_SCAN_FOR_MODULES=OFF - -DCMAKE_CXX_CLANG_TIDY='clang-tidy;--export-fixes=clang-tidy-fixes.yaml'" + extra-options: "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_CXX_SCAN_FOR_MODULES=OFF" - - name: Run clang-tidy using CMake + - name: Run clang-tidy id: tidy shell: bash working-directory: ${{ needs.setup.outputs.build_path }} env: REPO: ${{ needs.setup.outputs.repo }} + SOURCE_PATH: ${{ needs.setup.outputs.checkout_path }} + BUILD_PATH: ${{ needs.setup.outputs.build_path }} run: | . /entrypoint.sh REPO_NAME="${REPO##*/}" + SOURCE_DIR="$GITHUB_WORKSPACE/$SOURCE_PATH" + BUILD_DIR="$GITHUB_WORKSPACE/$BUILD_PATH" - echo "➡️ Running clang-tidy checks..." - cmake_status=0 - cmake --build . -j "$(nproc)" > clang-tidy.log 2>&1 || cmake_status=$? - - if [ "$cmake_status" -ne 0 ]; then - echo "::error::clang-tidy CMake build failed (exit code $cmake_status)" - echo "::group::clang-tidy log output" - cat clang-tidy.log - echo "::endgroup::" - exit "$cmake_status" + # run-clang-tidy runs clang-tidy on every translation unit in + # compile_commands.json in parallel and merges the per-TU fix YAML files + # into a single comprehensive clang-tidy-fixes.yaml. The alternative + # (setting CMAKE_CXX_CLANG_TIDY=clang-tidy;--export-fixes=... and + # building with -j) suffers from a race condition: every parallel + # clang-tidy invocation independently overwrites the same output file, so + # only the fixes from whichever translation unit finishes last are + # retained. + # + # Path arguments are substring-matched against file paths in + # compile_commands.json; restricting to the source directory automatically + # excludes generated files (e.g. version.cpp, ROOT dictionaries) that live + # in the build directory and lack access to the .clang-tidy config. + # + # run-clang-tidy validates the check list by calling clang-tidy from its + # working directory; run from the source root so that clang-tidy discovers + # the .clang-tidy config file there, preventing a spurious "No checks + # enabled." error. + if ! command -v run-clang-tidy >/dev/null 2>&1; then + echo "::error::run-clang-tidy not found in PATH; cannot run clang-tidy checks" + exit 1 fi + echo "➡️ Running clang-tidy checks..." + cd "$SOURCE_DIR" + run-clang-tidy \ + -p "$BUILD_DIR" \ + -export-fixes "$BUILD_DIR/clang-tidy-fixes.yaml" \ + -j "$(nproc)" \ + phlex \ + form \ + plugins \ + test \ + > "$BUILD_DIR/clang-tidy.log" 2>&1 || true - if grep -qE '^/.+\.(cpp|hpp|c|h):[0-9]+:[0-9]+: (warning|error):' clang-tidy.log; then + if grep -qE '^/.+\.(cpp|hpp|c|h):[0-9]+:[0-9]+: (warning|error):' "$BUILD_DIR/clang-tidy.log"; then echo "::warning::Clang-tidy found issues in the code" echo "Comment '@${REPO_NAME}bot tidy-fix [...]' on the PR to attempt auto-fix" + elif grep -q "No checks enabled\.\|Traceback\|RuntimeError\|ModuleNotFoundError\|ImportError" "$BUILD_DIR/clang-tidy.log"; then + echo "::error::run-clang-tidy failed (see log below)" + echo "::group::clang-tidy.log (error)" + cat "$BUILD_DIR/clang-tidy.log" + echo "::endgroup::" + exit 1 else echo "✅ clang-tidy check passed" fi diff --git a/.github/workflows/clang-tidy-fix.yaml b/.github/workflows/clang-tidy-fix.yaml index 6256d949..211fddc9 100644 --- a/.github/workflows/clang-tidy-fix.yaml +++ b/.github/workflows/clang-tidy-fix.yaml @@ -73,7 +73,7 @@ jobs: if: ${{ needs.setup.result == 'success' }} container: - image: ghcr.io/framework-r-d/phlex-ci:latest + image: ghcr.io/framework-r-d/phlex-ci:2026-04-06 steps: - name: Checkout code @@ -134,20 +134,17 @@ jobs: with: build-path: ${{ needs.setup.outputs.build_path }} - - name: Prepare CMake configuration options + - name: Prepare clang-tidy options if: steps.apply_from_artifact.outputs.applied != 'true' id: prep_tidy_opts env: TIDY_CHECKS: ${{ needs.setup.outputs.tidy_checks }} run: | . /entrypoint.sh - - CLANG_TIDY_OPTS="clang-tidy;--export-fixes=clang-tidy-fixes.yaml" if [ -n "$TIDY_CHECKS" ]; then CHECKS_NORMALIZED=$(echo "$TIDY_CHECKS" | tr ' ' ',') - CLANG_TIDY_OPTS="${CLANG_TIDY_OPTS};--checks=-*,${CHECKS_NORMALIZED}" + echo "clang_tidy_checks=-*,${CHECKS_NORMALIZED}" >> "$GITHUB_OUTPUT" fi - echo "clang_tidy_opts=${CLANG_TIDY_OPTS}" >> "$GITHUB_OUTPUT" - name: Configure CMake (Debug) if: steps.apply_from_artifact.outputs.applied != 'true' @@ -156,16 +153,51 @@ jobs: build-type: Debug source-path: ${{ needs.setup.outputs.checkout_path }} build-path: ${{ needs.setup.outputs.build_path }} - extra-options: - "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_CXX_SCAN_FOR_MODULES=OFF -DCMAKE_CXX_CLANG_TIDY='${{ - steps.prep_tidy_opts.outputs.clang_tidy_opts }}'" + extra-options: "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_CXX_SCAN_FOR_MODULES=OFF" - - name: Generate clang-tidy fixes using CMake build + - name: Generate clang-tidy fixes if: steps.apply_from_artifact.outputs.applied != 'true' working-directory: ${{ needs.setup.outputs.build_path }} + env: + TIDY_CHECKS: ${{ steps.prep_tidy_opts.outputs.clang_tidy_checks }} + SOURCE_PATH: ${{ needs.setup.outputs.checkout_path }} + BUILD_PATH: ${{ needs.setup.outputs.build_path }} run: | . /entrypoint.sh - cmake --build . -j "$(nproc)" || true + SOURCE_DIR="$GITHUB_WORKSPACE/$SOURCE_PATH" + BUILD_DIR="$GITHUB_WORKSPACE/$BUILD_PATH" + + # run-clang-tidy merges per-TU fix YAML files into one comprehensive + # clang-tidy-fixes.yaml. Using CMAKE_CXX_CLANG_TIDY=..;--export-fixes= + # in a parallel build is unsound: every clang-tidy invocation races to + # overwrite the same output file, so only the last TU's fixes survive. + # Path arguments restrict analysis to source files in the checkout, + # automatically excluding build-directory generated files. + # + # run-clang-tidy validates the check list by calling clang-tidy from its + # working directory; run from the source root so that clang-tidy discovers + # the .clang-tidy config file there. + if ! command -v run-clang-tidy >/dev/null 2>&1; then + echo "::error::run-clang-tidy not found in PATH; cannot generate fixes" + exit 1 + fi + + TIDY_ARGS=() + if [ -n "$TIDY_CHECKS" ]; then + TIDY_ARGS+=("-checks=$TIDY_CHECKS") + fi + + cd "$SOURCE_DIR" + run-clang-tidy \ + -p "$BUILD_DIR" \ + -export-fixes "$BUILD_DIR/clang-tidy-fixes.yaml" \ + -j "$(nproc)" \ + ${TIDY_ARGS[@]+"${TIDY_ARGS[@]}"} \ + phlex \ + form \ + plugins \ + test \ + || true - name: Apply clang-tidy fixes if: steps.apply_from_artifact.outputs.applied != 'true' diff --git a/.github/workflows/cmake-build.yaml b/.github/workflows/cmake-build.yaml index 71cd7036..43e38208 100644 --- a/.github/workflows/cmake-build.yaml +++ b/.github/workflows/cmake-build.yaml @@ -152,7 +152,7 @@ jobs: matrix: ${{ fromJSON(needs.generate-matrix.outputs.matrix) }} container: - image: ghcr.io/framework-r-d/phlex-ci:latest + image: ghcr.io/framework-r-d/phlex-ci:2026-04-06 options: --cap-add=SYS_PTRACE steps: diff --git a/.github/workflows/codeql-analysis.yaml b/.github/workflows/codeql-analysis.yaml index aee75a66..c7b28a67 100644 --- a/.github/workflows/codeql-analysis.yaml +++ b/.github/workflows/codeql-analysis.yaml @@ -214,7 +214,7 @@ jobs: name: Analyze ${{ matrix.language }} with CodeQL runs-on: ubuntu-24.04 container: - image: ghcr.io/framework-r-d/phlex-ci:latest + image: ghcr.io/framework-r-d/phlex-ci:2026-04-06 env: local_checkout_path: ${{ needs.setup.outputs.checkout_path }} local_build_path: ${{ needs.setup.outputs.build_path }} diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index 6cb2d6ed..5ae09f9c 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -74,7 +74,7 @@ jobs: always() && (github.event_name == 'workflow_dispatch' || needs.setup.outputs.has_changes == 'true') runs-on: ubuntu-24.04 container: - image: ghcr.io/framework-r-d/phlex-ci:latest + image: ghcr.io/framework-r-d/phlex-ci:2026-04-06 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/ci/spack.yaml b/ci/spack.yaml index 41d5a84d..0b3dc5d2 100644 --- a/ci/spack.yaml +++ b/ci/spack.yaml @@ -13,6 +13,7 @@ spack: - ninja - py-gcovr - py-pytest-cov # Needed for Python coverage reports + - py-pyyaml # Needed by run-clang-tidy --export-fixes to merge per-TU fix files - | llvm@22.1.1 +zstd +llvm_dylib +link_llvm_dylib ~lldb targets=x86 diff --git a/form/.clang-tidy b/form/.clang-tidy new file mode 100644 index 00000000..af8f512d --- /dev/null +++ b/form/.clang-tidy @@ -0,0 +1,8 @@ +--- +# Clang-tidy configuration for Phlex project +# Enforces C++ Core Guidelines and modern C++23 best practices + +InheritParentConfig: true + +Checks: > + -readability-identifier-naming diff --git a/phlex.code-workspace b/phlex.code-workspace index c3b66340..36f97858 100644 --- a/phlex.code-workspace +++ b/phlex.code-workspace @@ -7,6 +7,7 @@ ], "settings": { "files.associations": { + ".yamllint": "yaml", "*.yml": "yaml", "*.yaml": "yaml" }, diff --git a/phlex/app/CMakeLists.txt b/phlex/app/CMakeLists.txt index df80dfcf..e9a4f50b 100644 --- a/phlex/app/CMakeLists.txt +++ b/phlex/app/CMakeLists.txt @@ -2,9 +2,7 @@ configure_file(version.cpp.in version.cpp @ONLY) add_library(version_obj OBJECT version.cpp) -# version.cpp is generated into the build directory; clang-tidy cannot find -# .clang-tidy by walking the build-dir path, so disable linting for this file. -set_target_properties(version_obj PROPERTIES CXX_CLANG_TIDY "" POSITION_INDEPENDENT_CODE TRUE) +set_target_properties(version_obj PROPERTIES POSITION_INDEPENDENT_CODE TRUE) target_include_directories(version_obj PRIVATE ${PROJECT_SOURCE_DIR} ${PROJECT_BINARY_DIR}/include) cet_make_library( diff --git a/scripts/clang_tidy_fixes_to_problems.py b/scripts/clang_tidy_fixes_to_problems.py new file mode 100644 index 00000000..e5494a82 --- /dev/null +++ b/scripts/clang_tidy_fixes_to_problems.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 + +"""Convert clang-tidy export-fixes YAML into compiler-style diagnostics. + +The output format is compatible with VS Code problem matchers such as "$gcc": + + /abs/path/file.cpp:line:column: warning: message [check-name] + +This script intentionally uses a lightweight line-based parser so it does not +depend on PyYAML. +""" + +from __future__ import annotations + +import argparse +import re +import sys +from dataclasses import dataclass +from pathlib import Path + + +@dataclass +class Diagnostic: + """Represents a single clang-tidy diagnostic.""" + + check: str = "clang-tidy" + message: str = "" + level: str = "warning" + file_path: str | None = None + file_offset: int | None = None + + +def _strip_yaml_string(value: str) -> str: + value = value.strip() + if len(value) >= 2 and value[0] == "'" and value[-1] == "'": + # YAML single-quoted escape sequence: '' -> ' + return value[1:-1].replace("''", "'") + return value + + +def _parse_kv(line: str) -> tuple[str, str] | None: + match = re.match(r"^\s*([^:]+):\s*(.*)$", line) + if not match: + return None + return match.group(1).strip(), match.group(2).rstrip("\n") + + +def parse_clang_tidy_fixes(text: str) -> tuple[str | None, list[Diagnostic]]: + """Parse a clang-tidy export-fixes YAML string into a list of diagnostics.""" + main_source_file: str | None = None + diagnostics: list[Diagnostic] = [] + + current: Diagnostic | None = None + in_diag_message = False + + for raw_line in text.splitlines(): + line = raw_line.rstrip("\n") + + if line.startswith("MainSourceFile:"): + kv = _parse_kv(line) + if kv: + main_source_file = _strip_yaml_string(kv[1]) + continue + + if re.match(r"^\s*-\s+DiagnosticName:\s+", line): + if current is not None: + diagnostics.append(current) + check_name = re.sub(r"^\s*-\s+DiagnosticName:\s+", "", line) + current = Diagnostic(check=_strip_yaml_string(check_name).strip()) + in_diag_message = False + continue + + if current is None: + continue + + if re.match(r"^\s*DiagnosticMessage:\s*$", line): + in_diag_message = True + continue + + if re.match(r"^\s*Notes:\s*$", line): + # Notes are supplementary and do not define the primary location. + in_diag_message = False + continue + + kv = _parse_kv(line) + if not kv: + continue + + key, value = kv + + if in_diag_message: + if key == "Message": + current.message = _strip_yaml_string(value) + elif key == "FilePath": + current.file_path = _strip_yaml_string(value) + elif key == "FileOffset": + try: + current.file_offset = int(value.strip()) + except ValueError: + current.file_offset = None + continue + + if key == "Level": + level = _strip_yaml_string(value).strip().lower() + if level: + current.level = level + + if current is not None: + diagnostics.append(current) + + return main_source_file, diagnostics + + +def apply_path_map(path: str, mappings: list[tuple[str, str]]) -> str: + """Apply the first matching prefix mapping to translate a path.""" + normalized = path + for old, new in mappings: + if normalized.startswith(old): + normalized = new + normalized[len(old) :] + break + return normalized + + +def offset_to_line_col(path: Path, offset: int) -> tuple[int, int]: + """Convert a byte offset in a file to a (line, column) pair.""" + try: + data = path.read_bytes() + except OSError: + return 1, 1 + + if not data: + return 1, 1 + + bounded = max(0, min(offset, len(data))) + line = data.count(b"\n", 0, bounded) + 1 + last_newline = data.rfind(b"\n", 0, bounded) + if last_newline < 0: + col = bounded + 1 + else: + col = bounded - last_newline + return line, max(col, 1) + + +def parse_path_map(items: list[str]) -> list[tuple[str, str]]: + """Parse a list of OLD=NEW path mapping strings into (old, new) tuples.""" + mappings: list[tuple[str, str]] = [] + for item in items: + if "=" not in item: + raise ValueError(f"Invalid --path-map value '{item}'. Expected OLD=NEW.") + old, new = item.split("=", 1) + mappings.append((old, new)) + return mappings + + +def build_arg_parser() -> argparse.ArgumentParser: + """Build and return the argument parser for this script.""" + parser = argparse.ArgumentParser( + description="Convert clang-tidy export-fixes YAML into compiler-style diagnostics." + ) + parser.add_argument("input", type=Path, help="Path to clang-tidy-fixes.yaml") + parser.add_argument( + "-o", + "--output", + type=Path, + required=True, + help="Output text file with compiler-style diagnostics", + ) + parser.add_argument( + "--workspace-root", + type=Path, + default=Path.cwd(), + help="Workspace root used by default path mapping", + ) + parser.add_argument( + "--path-map", + action="append", + default=[], + help="Path mapping in OLD=NEW form. Can be specified multiple times.", + ) + return parser + + +def main() -> int: + """Parse arguments, process the fixes YAML, and write compiler-style diagnostics.""" + args = build_arg_parser().parse_args() + + text = args.input.read_text(encoding="utf-8") + main_source, diagnostics = parse_clang_tidy_fixes(text) + + default_mappings = [ + ("/__w/phlex/phlex/phlex-src", str(args.workspace_root.resolve())), + ("/__w/phlex/phlex/phlex-build", str((args.workspace_root / "build").resolve())), + ] + extra_mappings = parse_path_map(args.path_map) + mappings = extra_mappings + default_mappings + + lines: list[str] = [] + for diag in diagnostics: + file_path = diag.file_path or main_source + if not file_path: + # Skip diagnostics with no usable location. + continue + + mapped = apply_path_map(file_path, mappings) + resolved = Path(mapped) + + offset = diag.file_offset if diag.file_offset is not None else 0 + line, col = offset_to_line_col(resolved, offset) + + message = diag.message or "clang-tidy diagnostic" + check = diag.check or "clang-tidy" + severity = diag.level if diag.level in {"error", "warning", "note"} else "warning" + lines.append(f"{resolved}:{line}:{col}: {severity}: {message} [{check}]") + + args.output.parent.mkdir(parents=True, exist_ok=True) + output_text = "\n".join(lines) + if output_text: + output_text += "\n" + args.output.write_text(output_text, encoding="utf-8") + + print(f"Wrote {len(lines)} diagnostics to {args.output}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test/form/.clang-tidy b/test/form/.clang-tidy new file mode 100644 index 00000000..af8f512d --- /dev/null +++ b/test/form/.clang-tidy @@ -0,0 +1,8 @@ +--- +# Clang-tidy configuration for Phlex project +# Enforces C++ Core Guidelines and modern C++23 best practices + +InheritParentConfig: true + +Checks: > + -readability-identifier-naming diff --git a/test/form/data_products/CMakeLists.txt b/test/form/data_products/CMakeLists.txt index ad638b8d..170830a2 100644 --- a/test/form/data_products/CMakeLists.txt +++ b/test/form/data_products/CMakeLists.txt @@ -16,7 +16,6 @@ if(FORM_USE_ROOT_STORAGE) ${FORM_DATA_PROD_LIB_NAME} dict.h SELECTION classes_def.xml ) target_link_libraries(${FORM_DATA_PROD_LIB_NAME} PRIVATE ROOT::RIO) - set_target_properties(${FORM_DATA_PROD_LIB_NAME} PROPERTIES CXX_CLANG_TIDY "") add_subdirectory(extra_member) endif() diff --git a/test/form/data_products/extra_member/CMakeLists.txt b/test/form/data_products/extra_member/CMakeLists.txt index e686494c..b580a48d 100644 --- a/test/form/data_products/extra_member/CMakeLists.txt +++ b/test/form/data_products/extra_member/CMakeLists.txt @@ -14,5 +14,4 @@ if(FORM_USE_ROOT_STORAGE) ${FORM_EXTRA_DATA_PROD_LIB_NAME} dict.h SELECTION classes_def.xml ) target_link_libraries(${FORM_EXTRA_DATA_PROD_LIB_NAME} PRIVATE ROOT::RIO) - set_target_properties(${FORM_EXTRA_DATA_PROD_LIB_NAME} PROPERTIES CXX_CLANG_TIDY "") endif() diff --git a/test/form/test_utils.hpp b/test/form/test_utils.hpp index 99e51ee6..6f9a2c78 100644 --- a/test/form/test_utils.hpp +++ b/test/form/test_utils.hpp @@ -17,8 +17,8 @@ using namespace form::detail::experimental; namespace form::test { - inline std::string const testTreeName = "FORMTestTree"; - inline std::string const testFileName = "FORMTestFile.root"; + inline constexpr char const* testTreeName = "FORMTestTree"; + inline constexpr char const* testFileName = "FORMTestFile.root"; template inline std::string getTypeName() @@ -29,7 +29,7 @@ namespace form::test { template inline std::string makeTestBranchName() { - return testTreeName + "/" + getTypeName(); + return std::string(testTreeName) + "/" + getTypeName(); } inline std::vector> doWrite( @@ -66,8 +66,8 @@ namespace form::test { template inline void write(int const technology, PRODS&... prods) { - auto file = createFile(technology, testFileName.c_str(), 'o'); - auto parent = createWriteAssociation(technology, testTreeName); + auto file = createFile(technology, std::string(testFileName), 'o'); + auto parent = createWriteAssociation(technology, std::string(testTreeName)); parent->setFile(file); parent->setupWrite(); @@ -94,7 +94,7 @@ namespace form::test { template inline std::tuple...> read(int const technology) { - auto file = createFile(technology, testFileName, 'i'); + auto file = createFile(technology, std::string(testFileName), 'i'); return std::make_tuple(doRead(file, technology)...); }