Skip to content

Conversation

@shenxianpeng
Copy link
Collaborator

@shenxianpeng shenxianpeng commented Sep 14, 2025

closes #112 and #95

Summary by CodeRabbit

  • Documentation

    • Updated README with version bumps, a new tip on default vs pinned tool versions, examples, and removal of the old migration note.
  • Chores

    • Pre-commit hooks now select C/C++ files by language type.
    • Build system now lists clang-format and clang-tidy as requirements.
    • Tools are installed when a specific version is requested.
  • Refactor

    • Simplified tool invocation and clearer verbose/missing-tool reporting.
  • Tests

    • Test suites simplified to match the reduced installation/checking surface.

@shenxianpeng shenxianpeng added the bug Something isn't working label Sep 14, 2025
@github-actions github-actions bot added the documentation Improvements or additions to documentation label Sep 14, 2025
@coderabbitai
Copy link

coderabbitai bot commented Sep 14, 2025

Warning

Rate limit exceeded

@github-actions[bot] has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 24 minutes and 28 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between e24aa3a and f018c1f.

📒 Files selected for processing (4)
  • README.md (4 hunks)
  • cpp_linter_hooks/util.py (2 hunks)
  • testing/benchmark_hook_1.yaml (1 hunks)
  • tests/test_util.py (5 hunks)

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Switched pre-commit file selection from path-based regexes to language-type filters; simplified installation utilities by removing public install/query helpers and always invoking installs for side effects; updated clang wrapper modules to call literal clang-format/clang-tidy; added tools to build-system requirements and updated docs and tests accordingly.

Changes

Cohort / File(s) Summary of changes
Pre-commit config
.pre-commit-hooks.yaml
Replaced per-extension files regex filters with types_or: [c++, c] for clang-format and clang-tidy.
Documentation
README.md
Bumped hook version refs to v1.1.2, added a TIP about default installs and how to pin clang tool versions via --version, and removed the NOTE about migration to Python wheels.
Clang tool wrappers
cpp_linter_hooks/clang_format.py, cpp_linter_hooks/clang_tidy.py
Stop using installer-returned executable paths; call _resolve_install(...) for side effects when a version is provided and construct commands as ["clang-format", ...] / ["clang-tidy", ...]; added verbose output helper and FileNotFoundError/encoding handling.
Install/version utilities
cpp_linter_hooks/util.py
Removed ensure_installed, is_installed, and _get_runtime_version; simplified _resolve_install to resolve version and call _install_tool without path/runtime checks; get_version_from_dependency now reads build-system.requires; _install_tool suppresses pip output on failure.
Build requirements
pyproject.toml
Added clang-format and clang-tidy to [build-system].requires and removed them from optional-dependencies.
Tests
tests/test_util.py
Removed/updated tests referencing deleted helpers (ensure_installed, is_installed, _get_runtime_version); updated tests to reflect build-system.requires parsing and simplified install behavior.
Testing config
testing/benchmark_hook_1.yaml
Updated benchmark hook repo rev from v1.1.0 to v1.1.2.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

major

Pre-merge checks

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning The changes include several items that appear beyond the narrow objective of fixing repeat installs: removal of exported functions (ensure_installed, is_installed, _get_runtime_version) which is a breaking API change, edits to pyproject.toml build-system requirements and optional-dependencies, .pre-commit-hooks.yaml filter changes, and expanded README documentation. Removing exported/public helpers is a high-impact change that does not seem required to address the installation-repeat bug and therefore increases scope and release risk. These broader modifications should be isolated or explicitly justified in this PR. Split this work: keep the installation-fix changes in a focused PR and move API-breaking removals, pyproject/build-system edits, pre-commit filter changes, and documentation updates into separate PRs; if the public-API removals are intentional, document the breaking change, provide migration guidance, and ensure versioning and release notes reflect the impact, and add targeted tests that validate the installation fix before merging.
Linked Issues Check ❓ Inconclusive The PR changes the installation flow: util.py removes ensure_installed/is_installed and pre-checks, _resolve_install now calls _install_tool directly, and clang_format/clang_tidy invoke _resolve_install as a side effect and then call the bare "clang-format"/"clang-tidy" executables; tests were adjusted accordingly. These modifications could affect the repeat-install behavior, but the provided summaries do not show an explicit guard against redundant pip installs nor a post-install verification that the expected binary file exists (the core symptom in #112). I do not see an integration test reproducing the missing-file + repeated-install sequence from #112, so from the available information I cannot confirm the specific failure is resolved. Therefore compliance with the linked-issue objective is inconclusive based on the provided diff summary. Add a focused integration test that reproduces the failure described in #112 and asserts only one install attempt occurs; modify _resolve_install/_install_tool to detect an existing executable (for example via shutil.which or by returning the installed path) and verify the expected binary file exists before reporting success; if needed, reintroduce a non-breaking ensure_installed-like helper to encapsulate path resolution and avoid redundant installs.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Title Check ✅ Passed The title "fix: refactor installation mechanism and simplify tool invocation process" accurately and concisely describes the primary changes in the diff: the installation flow in cpp_linter_hooks.util was simplified and callers (clang_format.py, clang_tidy.py) now invoke tools by name rather than resolved paths, which aligns with the PR objective to fix repeated clang-tool installations. It is specific to the main implementation change and is clear enough for a reviewer scanning history.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
cpp_linter_hooks/util.py (4)

214-221: Bug: unconditional warning + breaking return type; restore return value and fix logging.

LOG.warning executes even when the tool is found, and changing the return type to None breaks callers/tests.

-def ensure_installed(tool: str, version: Optional[str] = None) -> None:
-    """Ensure a tool is installed, resolving its version if necessary."""
-    LOG.info("Ensuring %s is installed", tool)
-    tool_path = _resolve_install(tool, version)
-    if tool_path:
-        LOG.info("%s available at %s", tool, tool_path)
-    LOG.warning("%s not found and could not be installed", tool)
+def ensure_installed(tool: str, version: Optional[str] = None) -> Optional[Path]:
+    """Ensure a tool is installed, resolving its version if necessary. Returns the resolved executable path or None."""
+    LOG.info("Ensuring %s is installed (requested=%s)", tool, version or "auto")
+    tool_path = _resolve_install(tool, version)
+    if tool_path:
+        LOG.info("%s available at %s", tool, tool_path)
+        return tool_path
+    LOG.error("%s not found and could not be installed", tool)
+    return None

177-204: Avoid repeat installs, handle None versions safely, and prefer existing tools unless explicitly pinned.

When version is not provided, we should not reinstall; also choose a sane install target when the tool is missing. This addresses the repeat‑install issue and the pipeline failures with version=None or '20'.

 def _resolve_install(tool: str, version: Optional[str]) -> Optional[Path]:
-    """Resolve the installation of a tool, checking for version and installing if necessary."""
-    user_version = _resolve_version(
-        CLANG_FORMAT_VERSIONS if tool == "clang-format" else CLANG_TIDY_VERSIONS,
-        version,
-    )
-    if user_version is None:
-        user_version = (
-            DEFAULT_CLANG_FORMAT_VERSION
-            if tool == "clang-format"
-            else DEFAULT_CLANG_TIDY_VERSION
-        )
-
-    path = shutil.which(tool)
-    if path:
-        runtime_version = _get_runtime_version(tool)
-        if runtime_version and user_version not in runtime_version:
-            LOG.info(
-                "%s version mismatch (%s != %s), reinstalling...",
-                tool,
-                runtime_version,
-                user_version,
-            )
-            return _install_tool(tool, user_version)
-        return Path(path)
-
-    return _install_tool(tool, user_version)
+    """Resolve installation, only reinstalling when an explicit version is requested."""
+    versions = CLANG_FORMAT_VERSIONS if tool == "clang-format" else CLANG_TIDY_VERSIONS
+    requested_version = _resolve_version(versions, version) if version is not None else None
+    default_version = (
+        (DEFAULT_CLANG_FORMAT_VERSION if tool == "clang-format" else DEFAULT_CLANG_TIDY_VERSION)
+        or (versions[-1] if versions else None)
+    )
+
+    path = shutil.which(tool)
+    if path:
+        # No explicit version requested: trust the existing tool to avoid repeated installs.
+        if requested_version is None:
+            return Path(path)
+        runtime_version = _get_runtime_version(tool)
+        if runtime_version and requested_version not in runtime_version:
+            LOG.info("%s version mismatch (%s != %s), reinstalling...", tool, runtime_version, requested_version)
+            return _install_tool(tool, requested_version)
+        # If we can't detect the runtime version, assume OK and avoid reinstall.
+        return Path(path)
+
+    # Tool not found: choose a version to install.
+    install_version = requested_version or default_version
+    if not install_version:
+        LOG.error("No candidate version available to install for %s", tool)
+        return None
+    return _install_tool(tool, install_version)

165-175: After pip install, resolve the binary robustly; PATH lookup can fail immediately.

shutil.which(tool) can return None right after install in some CI shells. Add a fallback to search next to sys.executable.

 def _install_tool(tool: str, version: str) -> Optional[Path]:
     """Install a tool using pip."""
     try:
-        subprocess.check_call(
-            [sys.executable, "-m", "pip", "install", f"{tool}=={version}"]
-        )
-        return shutil.which(tool)
+        subprocess.check_call(
+            [sys.executable, "-m", "pip", "install", "--disable-pip-version-check", f"{tool}=={version}"]
+        )
+        path = shutil.which(tool)
+        if path:
+            return Path(path)
+        # Fallback: look next to the current interpreter (handles PATH quirks in CI)
+        bin_dir = Path(sys.executable).parent
+        candidates = [bin_dir / tool]
+        if sys.platform.startswith("win"):
+            candidates.insert(0, bin_dir / f"{tool}.exe")
+        for p in candidates:
+            if p.exists():
+                return p
+        return None
     except subprocess.CalledProcessError:
         LOG.error("Failed to install %s==%s", tool, version)
         return None

151-161: Runtime version parsing is brittle; parse semver with a regex for both tools.

Current tidy parsing assumes a second line; format parsing uses the last token. Use a single regex to extract \d+(\.\d+)+.

Example helper (apply outside this hunk):

import re
_VERSION_RE = re.compile(r"\b(\d+(?:\.\d+)+)\b")
def _get_runtime_version(tool: str) -> Optional[str]:
    try:
        out = subprocess.check_output([tool, "--version"], text=True)
        m = _VERSION_RE.search(out)
        return m.group(1) if m else None
    except Exception:
        return None
README.md (1)

177-187: Use --verbose (remove -v) — clang-format has no -v short alias.
Update README.md (lines 177–187): remove the -v suggestion and recommend --verbose only.

🧹 Nitpick comments (2)
README.md (1)

57-59: Clarify how version resolution works.

Minor wording tweak to set expectations for major-only pins resolving to the latest patch.

> [!TIP]
- > Install the latest version of `clang-format` and `clang-tidy` if not specified. You can specify the version using the `--version` argument in the `args` list as shown below.
+ > Install the latest version of `clang-format` and `clang-tidy` if not specified. You can pin versions via `--version`. A major-only value (e.g., `--version=21`) resolves to the latest `21.x.y`; use a full version (e.g., `21.1.0`) for reproducible builds.
cpp_linter_hooks/util.py (1)

38-40: Default version discovery via pyproject may be absent in wheels.

When running inside a packaged environment, pyproject.toml often isn't shipped. Ensure you always have a fallback (you now do via the proposed _resolve_install change), or consider defaulting DEFAULT_* to the last known version at import time.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c42c695 and 5bccc50.

📒 Files selected for processing (5)
  • .pre-commit-hooks.yaml (1 hunks)
  • README.md (1 hunks)
  • cpp_linter_hooks/clang_format.py (1 hunks)
  • cpp_linter_hooks/clang_tidy.py (1 hunks)
  • cpp_linter_hooks/util.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
cpp_linter_hooks/clang_format.py (1)
cpp_linter_hooks/util.py (1)
  • ensure_installed (214-220)
cpp_linter_hooks/clang_tidy.py (1)
cpp_linter_hooks/util.py (1)
  • ensure_installed (214-220)
🪛 Ruff (0.12.2)
cpp_linter_hooks/clang_tidy.py

15-15: Consider ["clang-tidy", *other_args] instead of concatenation

Replace with ["clang-tidy", *other_args]

(RUF005)

🪛 GitHub Actions: Test
cpp_linter_hooks/util.py

[error] 216-220: cpp_linter_hooks.util: ensure_installed() returned None for clang-format when no version was specified (version=None).


[error] 216-220: cpp_linter_hooks.util: ensure_installed() returned None for clang-format with version '20' (version=20).


[error] 216-220: cpp_linter_hooks.util: ensure_installed() returned None for clang-tidy when no version was specified (version=None).


[error] 216-220: cpp_linter_hooks.util: ensure_installed() returned None for clang-tidy with version '20' (version=20).


[error] 216-220: cpp_linter_hooks.util: ensure_installed() returned None when tool not found (tool='clang-format').


[error] 216-220: cpp_linter_hooks.util: version mismatch handling for clang-format; runtime=18.1.8 vs requested=20.1.7; installation attempted but tool name not returned.


[error] 216-220: cpp_linter_hooks.util: cannot determine runtime version for clang-format; ensure_installed('clang-format', '20.1.7') returned None (runtime_version=None).

🔇 Additional comments (2)
.pre-commit-hooks.yaml (1)

6-6: Switch to types_or looks good; header extensions should still match.

types_or: [c++, c] is appropriate and typically covers headers (.h, .hpp, etc.) via identify tags.

If you have Objective‑C(++) or CUDA/Proto in this repo, consider extending types: types_or: [c, c++, objective-c, objective-c++, cuda, proto].

Also applies to: 14-14

README.md (1)

75-76: Verify migration notes link exists in the published package.

At runtime (inside a pre‑commit venv), docs/migration-notes.md may not be present if it isn’t packaged. Ensure the link is valid on GitHub or include it in the sdist/wheel.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5bccc50 and f41e42c.

📒 Files selected for processing (1)
  • pyproject.toml (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run benchmarks
🔇 Additional comments (2)
pyproject.toml (2)

36-36: Verify necessity of setuptools as a runtime dependency.

No pkg_resources matches were found — confirm whether setuptools is required at runtime. Run these checks locally:

  • git grep -n --heading -e '\bpkg_resources\b' || true
  • git grep -n -e 'importlib.resources|importlib.metadata' || true

If pkg_resources is only used to access package data/metadata, migrate to importlib.resources / importlib.metadata and remove setuptools from runtime deps.


49-53: Relax exact pins for the tools extra to a stable major range.

Exact == pins cause churn and redundant reinstalls; prefer major-locked ranges to get patch fixes without breaking changes.

 [project.optional-dependencies]
 # only clang tools can added to this section to make hooks work
 tools = [
-    "clang-format==21.1.0",
-    "clang-tidy==21.1.0",
+    "clang-format>=21.1,<22",
+    "clang-tidy>=21.1,<22",
 ]

Confirm CI matrix and README/examples still behave with the relaxed pins (check .github/workflows/test.yml and .github/workflows/codspeed.yml); also verify tests/docs that reference pinned wheels (tests/test_util.py, docs/migration-notes.md).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
cpp_linter_hooks/util.py (2)

165-175: _install_tool returns str but is annotated as Optional[Path].

Type inconsistency can propagate subtle bugs. Return a Path consistently.

 def _install_tool(tool: str, version: str) -> Optional[Path]:
@@
-        subprocess.check_call(
-            [sys.executable, "-m", "pip", "install", f"{tool}=={version}"]
-        )
-        return shutil.which(tool)
+        subprocess.check_call([sys.executable, "-m", "pip", "install", f"{tool}=={version}"])
+        path = shutil.which(tool)
+        return Path(path) if path else None

190-201: Normalize runtime/user versions and compare exact normalized versions (avoid substring containment).

Replace the check in cpp_linter_hooks/util.py (lines 190–201) — currently if runtime_version and user_version not in runtime_version: — with a normalized equality check (e.g., strip build metadata after '-' or '(', or parse into integer tuples) and compare normalized values; update tests that assume substring behavior.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f41e42c and 1953843.

📒 Files selected for processing (1)
  • cpp_linter_hooks/util.py (1 hunks)
🧰 Additional context used
🪛 GitHub Actions: pre-commit
cpp_linter_hooks/util.py

[error] 215-218: End-of-file-fixer hook (pre-commit) modified the file; pre-commit exited with code 1. Re-run 'pre-commit run --all-files' to apply fixes.

🪛 GitHub Actions: Test
cpp_linter_hooks/util.py

[error] 1-1: During pytest run (coverage run --source=tests,cpp_linter_hooks -m pytest -vv), ensure_installed() returned None instead of the tool name for clang-format/clang-tidy, causing 7 parametrized tests to fail.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run benchmarks
🔇 Additional comments (1)
cpp_linter_hooks/util.py (1)

215-218: Run pre-commit or commit formatting for cpp_linter_hooks/util.py (lines 215–218)
End-of-file-fixer changed this hunk — likely trailing whitespace or missing final newline. Install and run pre-commit locally and commit the formatting change (pip install pre-commit && pre-commit run --all-files) or ensure the file ends with a single final newline.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
cpp_linter_hooks/util.py (2)

145-154: Type mismatch: returning str where Optional[Path] is annotated.

shutil.which returns Optional[str], not Path. This causes typing drift and confuses callers.

Apply this diff:

-def _install_tool(tool: str, version: str) -> Optional[Path]:
+def _install_tool(tool: str, version: str) -> Optional[Path]:
@@
-        subprocess.check_call(
-            [sys.executable, "-m", "pip", "install", f"{tool}=={version}"]
-        )
-        return shutil.which(tool)
+        subprocess.check_call(
+            [sys.executable, "-m", "pip", "install", f"{tool}=={version}"]
+        )
+        which = shutil.which(tool)
+        return Path(which) if which else None

157-170: None version can reach pip; add idempotency and clear failure.

If DEFAULT_* is None, _install_tool is called with None (TypeError). Also, every call triggers a pip invocation, which risks the repeat-install loop described in #112.

Apply this diff to (a) guard against None, (b) skip re-install when already installed and unchanged within the same process, and (c) fail fast with a clear error:

+_LAST_INSTALLED: dict[str, Optional[str]] = {}
+
 def _resolve_install(tool: str, version: Optional[str]) -> Optional[Path]:
@@
-    if user_version is None:
-        user_version = (
-            DEFAULT_CLANG_FORMAT_VERSION
-            if tool == "clang-format"
-            else DEFAULT_CLANG_TIDY_VERSION
-        )
+    if user_version is None:
+        user_version = (
+            DEFAULT_CLANG_FORMAT_VERSION
+            if tool == "clang-format"
+            else DEFAULT_CLANG_TIDY_VERSION
+        )
+    if user_version is None:
+        raise RuntimeError(
+            f"No version available for {tool}. "
+            "Set --version or pin it in build-system.requires."
+        )
+
+    # Idempotency within a single process to avoid repeat installs
+    if _LAST_INSTALLED.get(tool) == user_version:
+        which = shutil.which(tool)
+        return Path(which) if which else None
@@
-    return _install_tool(tool, user_version)
+    installed = _install_tool(tool, user_version)
+    if installed:
+        _LAST_INSTALLED[tool] = user_version
+    return installed
cpp_linter_hooks/clang_tidy.py (1)

21-29: Capture stderr too; clearer error reporting.

clang-tidy may emit diagnostics on stderr. Combine streams for consistent detection and messaging.

Apply this diff:

-    try:
-        sp = subprocess.run(command, stdout=subprocess.PIPE, encoding="utf-8")
+    try:
+        sp = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8")
         retval = sp.returncode
-        output = sp.stdout
+        output = (sp.stdout or "") + (sp.stderr or "")
         if "warning:" in output or "error:" in output:
             retval = 1
         return retval, output
     except FileNotFoundError as stderr:
         retval = 1
         return retval, str(stderr)
♻️ Duplicate comments (2)
cpp_linter_hooks/util.py (1)

170-170: Restore public ensure_installed for backward compatibility (fixes CI).

Tests (and likely users) import ensure_installed; its removal causes ImportError in the pipeline. Reintroduce it as a thin wrapper and mark as deprecated.

Apply this diff below the existing functions:

+def ensure_installed(tool: str, version: Optional[str] = None) -> str:
+    """
+    Ensure `tool` is installed at the requested version.
+    Returns the executable path (str). Raises on failure.
+    Note: kept for backward compatibility; prefer _resolve_install or future public API.
+    """
+    LOG.info("Ensuring %s is installed", tool)
+    installed = _resolve_install(tool, version)
+    if installed:
+        return str(installed)
+    which = shutil.which(tool)
+    if which:
+        return which
+    raise RuntimeError(f"Failed to install {tool}=={version or 'unspecified'}")

Run to confirm there are no remaining imports of removed symbols and that tests pass collection:

#!/bin/bash
rg -nP '\bfrom\s+cpp_linter_hooks\.util\s+import\b.*\bensure_installed\b' -C2
rg -nP '\bimport\s+cpp_linter_hooks\.util\b' -C2
pytest -q --maxfail=1 -k util
cpp_linter_hooks/clang_tidy.py (1)

14-16: Fail fast on install errors; use splat args.

Check installation result and adopt Ruff’s splat suggestion.

Apply this diff:

-    if hook_args.version:
-        _resolve_install("clang-tidy", hook_args.version)
-    command = ["clang-tidy"] + other_args
+    tool_path = "clang-tidy"
+    if hook_args.version:
+        tool_path = ensure_installed("clang-tidy", hook_args.version)
+    command = [tool_path, *other_args]
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9cac9d1 and 0db3fea.

📒 Files selected for processing (4)
  • cpp_linter_hooks/clang_format.py (2 hunks)
  • cpp_linter_hooks/clang_tidy.py (2 hunks)
  • cpp_linter_hooks/util.py (1 hunks)
  • pyproject.toml (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • pyproject.toml
  • cpp_linter_hooks/clang_format.py
🧰 Additional context used
🧬 Code graph analysis (1)
cpp_linter_hooks/clang_tidy.py (1)
cpp_linter_hooks/util.py (1)
  • _resolve_install (157-170)
🪛 GitHub Actions: Test
cpp_linter_hooks/util.py

[error] 1-1: Missing symbol 'ensure_installed' in cpp_linter_hooks.util (ImportError during test collection).

🪛 Ruff (0.12.2)
cpp_linter_hooks/clang_tidy.py

16-16: Consider ["clang-tidy", *other_args] instead of concatenation

Replace with ["clang-tidy", *other_args]

(RUF005)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run benchmarks

@codecov
Copy link

codecov bot commented Sep 14, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.44%. Comparing base (c42c695) to head (f018c1f).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #113      +/-   ##
==========================================
- Coverage   94.48%   94.44%   -0.04%     
==========================================
  Files           3        3              
  Lines         145      108      -37     
==========================================
- Hits          137      102      -35     
+ Misses          8        6       -2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR refactors the clang tools installation mechanism to prevent repeated installations and simplifies the tool invocation process. The changes move clang-format and clang-tidy declarations from optional dependencies to build-system requirements and streamline the installation logic.

  • Moved clang tool declarations from project.optional-dependencies.tools to build-system.requires in pyproject.toml
  • Simplified tool installation by removing version checking and only installing when a specific version is requested
  • Removed complex ensure_installed and is_installed functions in favor of direct tool execution with conditional installation

Reviewed Changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pyproject.toml Moves clang-format and clang-tidy from optional dependencies to build-system requirements
cpp_linter_hooks/util.py Removes version checking logic and simplifies installation to only occur when specific versions are requested
cpp_linter_hooks/clang_format.py Updates to use simplified installation logic and direct tool execution
cpp_linter_hooks/clang_tidy.py Updates to use simplified installation logic and direct tool execution
tests/test_util.py Removes tests for deleted functions and updates remaining tests for new logic
.pre-commit-hooks.yaml Changes file selection from regex patterns to language types
README.md Adds tip about tool installation behavior

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

):
result = _resolve_install("clang-format", "20.1.7")
assert result == Path(mock_path)
assert Path(result) == Path(mock_path)
Copy link

Copilot AI Sep 14, 2025

Choose a reason for hiding this comment

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

The test expects a Path object but _resolve_install now returns the result of _install_tool, which returns Optional[Path] from shutil.which. This assertion will fail if shutil.which returns a string instead of a Path object.

Copilot uses AI. Check for mistakes.
shenxianpeng and others added 2 commits September 15, 2025 00:08
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@codspeed-hq
Copy link

codspeed-hq bot commented Sep 14, 2025

CodSpeed Performance Report

Merging #113 will improve performances by ×2.3

Comparing fix/repeat-install (f018c1f) with main (4346890)1

Summary

⚡ 34 improvements
✅ 26 untouched
⏩ 13 skipped2

Benchmarks breakdown

Benchmark BASE HEAD Change
test_run_clang_format_dry_run[args0-1] 833.7 µs 701 µs +18.92%
test_run_clang_format_invalid[args0-1] 838.7 µs 703.3 µs +19.25%
test_run_clang_format_invalid[args1-1] 985.4 µs 752.3 µs +30.99%
test_run_clang_format_invalid[args2-1] 978.3 µs 750.7 µs +30.33%
test_run_clang_format_invalid[args3-1] 893 µs 759 µs +17.66%
test_run_clang_format_invalid[args4-1] 889.6 µs 755.1 µs +17.81%
test_run_clang_format_invalid[args5-1] 885.5 µs 753 µs +17.6%
test_run_clang_format_invalid[args6-1] 862.1 µs 730.1 µs +18.07%
test_run_clang_format_valid[args0-expected_retval0] 1,014.2 µs 896.5 µs +13.13%
test_run_clang_format_valid[args1-expected_retval1] 1,120.5 µs 889.9 µs +25.91%
test_run_clang_format_valid[args2-expected_retval2] 1,115.3 µs 884.8 µs +26.05%
test_run_clang_format_valid[args3-expected_retval3] 1,025 µs 893.3 µs +14.74%
test_run_clang_format_valid[args4-expected_retval4] 1,026.3 µs 888.4 µs +15.52%
test_run_clang_format_valid[args5-expected_retval5] 1,023.9 µs 891.4 µs +14.87%
test_run_clang_format_valid[args6-expected_retval6] 1,001.9 µs 869.3 µs +15.25%
test_run_clang_format_verbose 1,013.1 µs 886.3 µs +14.31%
test_run_clang_format_verbose_error 947.9 µs 811.5 µs +16.81%
test_run_clang_tidy_invalid[args0-1] 738.6 µs 606.8 µs +21.71%
test_run_clang_tidy_invalid[args1-1] 762.9 µs 633.2 µs +20.47%
test_run_clang_tidy_invalid[args2-1] 761.1 µs 627.7 µs +21.25%
... ... ... ... ...

ℹ️ Only the first 20 benchmarks are displayed. Go to the app to view all benchmarks.

Footnotes

  1. No successful run was found on main (c42c695) during the generation of this report, so 4346890 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

  2. 13 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
cpp_linter_hooks/util.py (1)

145-156: Invalid requirement when version is None + return type mismatch.

  • When version is None, f"{tool}=={version}" becomes "tool==None" (pip error).
  • Function advertises Optional[Path] but returns Optional[str] from shutil.which.

Apply this diff to handle None safely and align types:

-def _install_tool(tool: str, version: str) -> Optional[Path]:
-    """Install a tool using pip, suppressing output."""
+def _install_tool(tool: str, version: Optional[str]) -> Optional[str]:
+    """Install a tool using pip, suppressing output."""
     try:
-        subprocess.check_call(
-            [sys.executable, "-m", "pip", "install", f"{tool}=={version}"],
-            stdout=subprocess.DEVNULL,
-            stderr=subprocess.DEVNULL,
-        )
-        return shutil.which(tool)
+        req = f"{tool}=={version}" if version else tool
+        subprocess.check_call(
+            [sys.executable, "-m", "pip", "install", req, "--disable-pip-version-check", "--no-input"],
+            stdout=subprocess.DEVNULL,
+            stderr=subprocess.DEVNULL,
+        )
+        return shutil.which(tool)
     except subprocess.CalledProcessError:
         LOG.error("Failed to install %s==%s", tool, version)
         return None
tests/test_util.py (1)

135-137: Fix failing assertion to match suppressed pip output.

The implementation redirects stdout/stderr to DEVNULL; assert accordingly (this is the current CI failure).

Apply this diff:

-        mock_check_call.assert_called_once_with(
-            [sys.executable, "-m", "pip", "install", "clang-format==20.1.7"]
-        )
+        mock_check_call.assert_called_once_with(
+            [sys.executable, "-m", "pip", "install", "clang-format==20.1.7"],
+            stdout=subprocess.DEVNULL,
+            stderr=subprocess.DEVNULL,
+        )
♻️ Duplicate comments (2)
cpp_linter_hooks/util.py (2)

159-172: Always re-installing causes repeat installs; short-circuit when possible.

Currently this unconditionally installs, re-triggering pip even when the tool is already available, which contradicts the PR goal. Short-circuit when the tool is already on PATH and no explicit version is requested; also update the signature type to match _install_tool.

Apply this diff:

-def _resolve_install(tool: str, version: Optional[str]) -> Optional[Path]:
+def _resolve_install(tool: str, version: Optional[str]) -> Optional[str]:
     """Resolve the installation of a tool, checking for version and installing if necessary."""
+    # Fast path: if no version requested and tool already exists, avoid pip
+    existing = shutil.which(tool)
+    if version is None and existing:
+        return existing
@@
-    return _install_tool(tool, user_version)
+    return _install_tool(tool, user_version)

Optional (nice-to-have): keep a per-process dedupe cache to avoid repeated installs of the same (tool, version) tuple when multiple callers invoke this in a single run. I can provide a patch if you want it.


23-29: Fragile default version sourcing; add safe fallback path.

Reading versions from build-system.requires in pyproject.toml at runtime is brittle (pyproject.toml may not be packaged in wheels), and returning None cascades into an invalid pip requirement later. Add a fallback to the latest known supported version when no pinned default is discoverable.

Apply this diff (pairs with the _resolve_install change below):

 def _resolve_install(tool: str, version: Optional[str]) -> Optional[Path]:
@@
-    if user_version is None:
-        user_version = (
-            DEFAULT_CLANG_FORMAT_VERSION
-            if tool == "clang-format"
-            else DEFAULT_CLANG_TIDY_VERSION
-        )
+    if user_version is None:
+        user_version = (
+            DEFAULT_CLANG_FORMAT_VERSION
+            if tool == "clang-format"
+            else DEFAULT_CLANG_TIDY_VERSION
+        )
+    # Fallback to the latest known supported version when no default is pinned
+    if user_version is None:
+        user_version = (
+            CLANG_FORMAT_VERSIONS[-1]
+            if tool == "clang-format"
+            else CLANG_TIDY_VERSIONS[-1]
+        )
🧹 Nitpick comments (2)
cpp_linter_hooks/util.py (1)

148-152: Ruff S603 (subprocess) — risk is low but document the guardrails.

Inputs are constrained (no shell, versions resolved from curated lists or pinned defaults). Add a brief comment to silence future concerns.

-        subprocess.check_call(
+        # Safe: shell=False, args fully controlled; version comes from curated list/defaults.
+        subprocess.check_call(
tests/test_util.py (1)

168-177: Optional: assert we don’t hit pip when tool is already present and no version is requested.

Once the short-circuit in _resolve_install lands, consider adding a variant of this test with version=None and asserting subprocess.check_call is not invoked.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0db3fea and e24aa3a.

📒 Files selected for processing (2)
  • cpp_linter_hooks/util.py (2 hunks)
  • tests/test_util.py (3 hunks)
🧰 Additional context used
🪛 GitHub Actions: Test
tests/test_util.py

[error] 135-135: test_install_tool_success failed: mocked subprocess.check_call invoked with stdout=DEVNULL and stderr=DEVNULL; expected call did not include these arguments. Command: 'coverage run --source=tests,cpp_linter_hooks -m pytest -vv'.

🪛 Ruff (0.12.2)
cpp_linter_hooks/util.py

148-148: subprocess call: check for execution of untrusted input

(S603)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run benchmarks
🔇 Additional comments (2)
tests/test_util.py (2)

28-35: LGTM: tests now target build-system.requires.


60-60: LGTM: missing dependency returns None as expected.

@shenxianpeng shenxianpeng changed the title refactor: fix repeat install clang tools fix: refactor installation mechanism and simplify tool invocation process Sep 14, 2025
@shenxianpeng shenxianpeng added minor A minor version bump enhancement New feature or request and removed documentation Improvements or additions to documentation bug Something isn't working labels Sep 14, 2025
@github-actions github-actions bot added bug Something isn't working documentation Improvements or additions to documentation labels Sep 14, 2025
@shenxianpeng shenxianpeng removed documentation Improvements or additions to documentation enhancement New feature or request minor A minor version bump labels Sep 14, 2025
@github-actions github-actions bot added the documentation Improvements or additions to documentation label Sep 14, 2025
@sonarqubecloud
Copy link

@shenxianpeng shenxianpeng changed the title fix: refactor installation mechanism and simplify tool invocation process refactor: simplify tool installation and invocation Sep 14, 2025
@shenxianpeng shenxianpeng removed the documentation Improvements or additions to documentation label Sep 14, 2025
@shenxianpeng shenxianpeng merged commit 58f6953 into main Sep 14, 2025
16 of 17 checks passed
@shenxianpeng shenxianpeng deleted the fix/repeat-install branch September 14, 2025 21:52
@shenxianpeng shenxianpeng changed the title refactor: simplify tool installation and invocation fix: simplify tool installation and invocation Sep 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Repeat install clang-format wheels

2 participants