Skip to content

feat: support for clang-include-cleaner and clang-apply-replacements and switch to dynamic PyPI version resolution #185

Merged
shenxianpeng merged 14 commits into
mainfrom
feature/support-include-cleaner-apply-replacements
Jun 14, 2026
Merged

feat: support for clang-include-cleaner and clang-apply-replacements and switch to dynamic PyPI version resolution #185
shenxianpeng merged 14 commits into
mainfrom
feature/support-include-cleaner-apply-replacements

Conversation

@shenxianpeng

@shenxianpeng shenxianpeng commented Jun 14, 2026

Copy link
Copy Markdown
Member

Summary

Add clang-include-cleaner and clang-apply-replacements to the WHEEL_TOOLS set in clang-tools-pip, enabling:

clang-tools install clang-include-cleaner --wheel
clang-tools install clang-apply-replacements --wheel --version 17.0.6

Changes

clang_tools/main.py

  • Add "clang-include-cleaner" and "clang-apply-replacements" to WHEEL_TOOLS

tests/test_main.py

  • Add 3 new tests:
    • test_main_install_wheel_include_cleaner — basic wheel install
    • test_main_install_wheel_apply_replacements — basic wheel install
    • test_main_install_auto_detect_new_wheel_tools — auto-detect treats both as wheel tools

Note

These tools currently have limited version availability on PyPI:

  • clang-include-cleaner: only 22.1.7
  • clang-apply-replacements: 16.0.0, 17.0.6

Requires: cpp-linter/cpp-linter-hooks#242

Summary by CodeRabbit

  • New Features
    • Added clang-include-cleaner and clang-apply-replacements to support installation via the --wheel option.
    • Updated auto-detection so these tools are recognized as wheel-installable.
  • Bug Fixes
    • Improved wheel install diagnostics with clearer, more consistent stderr messages when wheel resolution fails.
  • Tests
    • Added integration/functional tests for the new wheel tools and updated wheel install unit tests for the new resolver behavior.
  • Chores
    • Updated the cpp-linter-hooks dependency to >=1.2.0.

…eplacements

- Add 'clang-include-cleaner' and 'clang-apply-replacements' to
  WHEEL_TOOLS set in main.py, enabling installation via
  'clang-tools install <tool> --wheel'
- Add tests for wheel install, auto-detect, and unsupported-tool
  validation covering the new tools
@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@shenxianpeng, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 20 minutes and 14 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, 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 include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 455d44e6-ef1d-487b-9f1f-a427cc16be2a

📥 Commits

Reviewing files that changed from the base of the PR and between 28d7128 and 1f4a933.

📒 Files selected for processing (5)
  • .github/workflows/test.yml
  • tests/test_install.py
  • tests/test_main.py
  • tests/test_util.py
  • tests/test_wheel_install.py

Walkthrough

A new clang_tools.wheel_install module is introduced implementing PyPI-based tool resolution and installation with version caching and fallback to locally-installed tools. The _wheel_install function in main.py is refactored to use resolve_wheel_install, which returns a (path, error) tuple for per-tool diagnostic error handling. The WHEEL_TOOLS set is expanded to include clang-include-cleaner and clang-apply-replacements. The cpp-linter-hooks dependency is bumped to >=1.2.0, and wheel-install tests are updated and extended to cover the new tools and resolver behavior.

Changes

Wheel installation system expansion

Layer / File(s) Summary
New wheel_install resolver module
clang_tools/wheel_install.py
New module implements PyPI JSON API querying with per-tool caching, version resolution with optional fallback to locally-installed tools, version detection via subprocess --version parsing, and pip-based installation. Public resolve_wheel_install(tool, version) returns (path, error) tuple where exactly one element is non-None.
Main refactor to use new resolver and expand tool support
clang_tools/main.py, pyproject.toml, clang_tools/wheel.py
WHEEL_TOOLS expanded from two tools to four by adding clang-include-cleaner and clang-apply-replacements. _wheel_install refactored to call resolve_wheel_install and handle (path, error) tuples, printing diagnostic errors to stderr and tracking overall success. cpp-linter-hooks dependency bumped to >=1.2.0. wheel.py docstring updated to note dynamic PyPI-based version resolution.
Test coverage for resolver and expanded tools
tests/test_main.py
Three new integration tests verify successful --wheel install for each new tool and auto-detection behavior. Five existing _wheel_install unit tests updated to mock clang_tools.wheel_install.resolve_wheel_install, with success cases returning (path, None) and failure cases returning (None, error_string), asserting on updated error output to stderr.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

  • cpp-linter/clang-tools-pip#179: Both PRs modify the wheel-install CLI flow in clang_tools/main.py and tests/test_main.py; this PR replaces the prior resolver with a new resolve_wheel_install function providing PyPI-based dynamic resolution and expands tool support to four tools.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 90.91% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main changes: adding support for two new LLVM tools and switching to dynamic PyPI version resolution for wheel installation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/support-include-cleaner-apply-replacements

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.

@github-actions github-actions Bot added the enhancement New feature or request label Jun 14, 2026
@codecov

codecov Bot commented Jun 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (2b4da85) to head (1f4a933).

Additional details and impacted files
@@             Coverage Diff             @@
##             main      #185      +/-   ##
===========================================
+ Coverage   99.87%   100.00%   +0.12%     
===========================================
  Files           7         9       +2     
  Lines         787      1047     +260     
===========================================
+ Hits          786      1047     +261     
+ Misses          1         0       -1     

☔ View full report in Codecov by Harness.
📢 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.

shenxianpeng and others added 3 commits June 14, 2026 23:19
- Use resolve_install_with_diagnostics() instead of resolve_install()
  to get rich error messages from PyPI API
- Update tests to mock new tuple-returning API
- Bump cpp-linter-hooks minimum version to >=1.2.0
- Update uv.lock
Remove the cpp-linter-hooks dependency. Wheel installation is now
handled by clang_tools/wheel_install.py using only the Python standard
library (urllib + json for PyPI API, subprocess for pip).

- Add clang_tools/wheel_install.py: resolve_wheel_install() with
  PyPI JSON API version resolution, LRU caching, and fallback to
  locally installed versions when PyPI is unreachable
- Remove clang_tools/wheel.py: legacy cpp-linter-hooks wrapper
- Remove cpp-linter-hooks from pyproject.toml dependencies
- Update tests to mock the new clang_tools.wheel_install module
@shenxianpeng shenxianpeng changed the title feat: add --wheel support for clang-include-cleaner and clang-apply-replacements feat: support for clang-include-cleaner and clang-apply-replacements and switch to dynamic PyPI version resolution Jun 14, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@clang_tools/wheel_install.py`:
- Around line 179-182: The current return statement always returns None as the
second element of the tuple, even when _install_tool() fails. This breaks the
resolver contract that expects a non-None error when installation or
post-install lookup fails. Modify the return statement to capture any error that
occurs when _install_tool(tool, user_version) is called and return that error as
the second element of the tuple instead of hardcoding None, ensuring the
contract is maintained and error handling in clang_tools/main.py works
correctly.
- Around line 134-142: The _is_version_installed function uses a substring check
(if version in result.stdout) which can incorrectly match when the installed
version merely contains the requested version as a substring. Replace this with
exact version parsing and equality comparison. Additionally, add the timeout and
OSError handling to the subprocess.run call that you already implemented in
_detect_installed_version to make the code consistent and more robust. Parse the
version token from the stdout output and compare it for equality rather than
substring containment.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5afa1947-4bea-4c5e-b1aa-ce0c17d03005

📥 Commits

Reviewing files that changed from the base of the PR and between 0e1340b and 28d7128.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • clang_tools/main.py
  • clang_tools/wheel.py
  • clang_tools/wheel_install.py
  • pyproject.toml
  • tests/test_main.py
💤 Files with no reviewable changes (2)
  • pyproject.toml
  • clang_tools/wheel.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • clang_tools/main.py

Comment on lines +134 to +142
def _is_version_installed(tool: str, version: str) -> Optional[Path]:
"""Return the tool path if the installed version matches, otherwise None."""
existing = shutil.which(tool)
if not existing:
return None
result = subprocess.run([existing, "--version"], capture_output=True, text=True)
if version in result.stdout:
return Path(existing)
return None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use an exact parsed version match here, not a raw substring check.

if version in result.stdout can reuse the wrong local binary for an explicit request when the installed version merely contains that string, and this path also drops the timeout/OSError handling you already added in _detect_installed_version(). Parse the version token and compare for equality instead.

Proposed fix
 def _is_version_installed(tool: str, version: str) -> Optional[Path]:
     """Return the tool path if the installed version matches, otherwise None."""
     existing = shutil.which(tool)
     if not existing:
         return None
-    result = subprocess.run([existing, "--version"], capture_output=True, text=True)
-    if version in result.stdout:
-        return Path(existing)
+    try:
+        result = subprocess.run(
+            [existing, "--version"], capture_output=True, text=True, timeout=10
+        )
+    except (OSError, subprocess.TimeoutExpired):
+        return None
+    match = re.search(r"(\d+\.\d+\.\d+(?:\.\d+)?)", result.stdout)
+    if match and match.group(1) == version:
+        return Path(existing)
     return None
🧰 Tools
🪛 ast-grep (0.43.0)

[error] 138-138: Use of unsanitized data to create processes
Context: subprocess.run([existing, "--version"], capture_output=True, text=True)
Note: [CWE-78].

(os-system-unsanitized-data)


[error] 138-138: Command coming from incoming request
Context: subprocess.run([existing, "--version"], capture_output=True, text=True)
Note: [CWE-20].

(subprocess-from-request)

🪛 Ruff (0.15.15)

[error] 139-139: subprocess call: check for execution of untrusted input

(S603)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@clang_tools/wheel_install.py` around lines 134 - 142, The
_is_version_installed function uses a substring check (if version in
result.stdout) which can incorrectly match when the installed version merely
contains the requested version as a substring. Replace this with exact version
parsing and equality comparison. Additionally, add the timeout and OSError
handling to the subprocess.run call that you already implemented in
_detect_installed_version to make the code consistent and more robust. Parse the
version token from the stdout output and compare it for equality rather than
substring containment.

Comment on lines +179 to +182
return (
_is_version_installed(tool, user_version) or _install_tool(tool, user_version),
None,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't let install failures escape as (None, None).

If _install_tool() fails, this returns (None, None), which breaks the resolver contract documented in this module and drops the per-tool error handling that clang_tools/main.py now expects. Return a non-None error whenever the install or post-install lookup fails.

Proposed fix
 def resolve_wheel_install(
     tool: str, version: Optional[str]
 ) -> Tuple[Optional[Path], Optional[str]]:
@@
     user_version, error = _resolve_version(tool, version)
     if error is not None:
         return None, error
 
-    return (
-        _is_version_installed(tool, user_version) or _install_tool(tool, user_version),
-        None,
-    )
+    installed = _is_version_installed(tool, user_version)
+    if installed is not None:
+        return installed, None
+
+    installed = _install_tool(tool, user_version)
+    if installed is not None:
+        return installed, None
+
+    return None, f"Failed to install {tool} {user_version}."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@clang_tools/wheel_install.py` around lines 179 - 182, The current return
statement always returns None as the second element of the tuple, even when
_install_tool() fails. This breaks the resolver contract that expects a non-None
error when installation or post-install lookup fails. Modify the return
statement to capture any error that occurs when _install_tool(tool,
user_version) is called and return that error as the second element of the tuple
instead of hardcoding None, ensuring the contract is maintained and error
handling in clang_tools/main.py works correctly.

shenxianpeng and others added 2 commits June 14, 2026 23:37
- Add tests/test_wheel_install.py: comprehensive tests for PyPI version
  resolution, pip installation, and all edge cases (23 tests)
- Add test for (None, None) fallback in _wheel_install
- Add OS-forcing tests for non-macos and non-linux branches
- Restructure OS-dependent test branches for full coverage
- Production code: 100% (5 modules, 365 statements)
- All tests: 151 passed
Comment thread tests/test_wheel_install.py Outdated
Co-authored-by: Xianpeng Shen <xianpeng.shen@gmail.com>
…ply-replacements

clang-include-cleaner tested for version >= 16 (introduced in LLVM 16)
clang-apply-replacements tested for version >= 13 (matching clang-tidy)
PyPI only has clang-apply-replacements wheels starting at 16.0.0
…lity

clang-include-cleaner: only v22 on PyPI
clang-apply-replacements: only v16 and v17 on PyPI
@sonarqubecloud

Copy link
Copy Markdown

@shenxianpeng shenxianpeng merged commit 34e250a into main Jun 14, 2026
72 checks passed
@shenxianpeng shenxianpeng deleted the feature/support-include-cleaner-apply-replacements branch June 14, 2026 21:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant