From 951288d0c65b8439b4cf0ab321326785405daeab Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 6 Dec 2025 12:20:18 -0500 Subject: [PATCH 1/4] build: add clang-tidy configuration and wrapper script with parallel processing --- .clang-tidy | 167 ++++++++++++++++++ scripts/run-clang-tidy.py | 350 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 517 insertions(+) create mode 100644 .clang-tidy create mode 100755 scripts/run-clang-tidy.py diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 00000000000..69fe82afafb --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,167 @@ +# TheSuperHackers @build JohnsterID 15/09/2025 Add clang-tidy configuration for code quality analysis +--- +# Clang-tidy configuration for GeneralsGameCode project +# This configuration is tailored for a legacy C++98/C++20 hybrid codebase +# with Windows-specific code and COM interfaces + +# Enable specific checks that are appropriate for this codebase +Checks: > + -*, + bugprone-*, + -bugprone-easily-swappable-parameters, + -bugprone-implicit-widening-of-multiplication-result, + -bugprone-narrowing-conversions, + -bugprone-signed-char-misuse, + cert-*, + -cert-dcl21-cpp, + -cert-dcl50-cpp, + -cert-dcl58-cpp, + -cert-env33-c, + -cert-err58-cpp, + clang-analyzer-*, + -clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling, + cppcoreguidelines-*, + -cppcoreguidelines-avoid-c-arrays, + -cppcoreguidelines-avoid-magic-numbers, + -cppcoreguidelines-avoid-non-const-global-variables, + -cppcoreguidelines-init-variables, + -cppcoreguidelines-macro-usage, + -cppcoreguidelines-no-malloc, + -cppcoreguidelines-owning-memory, + -cppcoreguidelines-pro-bounds-array-to-pointer-decay, + -cppcoreguidelines-pro-bounds-constant-array-index, + -cppcoreguidelines-pro-bounds-pointer-arithmetic, + -cppcoreguidelines-pro-type-cstyle-cast, + -cppcoreguidelines-pro-type-reinterpret-cast, + -cppcoreguidelines-pro-type-union-access, + -cppcoreguidelines-pro-type-vararg, + -cppcoreguidelines-special-member-functions, + google-*, + -google-build-using-namespace, + -google-explicit-constructor, + -google-readability-casting, + -google-readability-todo, + -google-runtime-int, + -google-runtime-references, + hicpp-*, + -hicpp-avoid-c-arrays, + -hicpp-explicit-conversions, + -hicpp-no-array-decay, + -hicpp-signed-bitwise, + -hicpp-special-member-functions, + -hicpp-uppercase-literal-suffix, + -hicpp-use-auto, + -hicpp-vararg, + misc-*, + -misc-const-correctness, + -misc-include-cleaner, + -misc-non-private-member-variables-in-classes, + -misc-use-anonymous-namespace, + modernize-*, + -modernize-avoid-c-arrays, + -modernize-concat-nested-namespaces, + -modernize-loop-convert, + -modernize-pass-by-value, + -modernize-raw-string-literal, + -modernize-return-braced-init-list, + -modernize-use-auto, + -modernize-use-default-member-init, + -modernize-use-nodiscard, + -modernize-use-trailing-return-type, + performance-*, + -performance-avoid-endl, + portability-*, + readability-*, + -readability-avoid-const-params-in-decls, + -readability-braces-around-statements, + -readability-convert-member-functions-to-static, + -readability-function-cognitive-complexity, + -readability-identifier-length, + -readability-implicit-bool-conversion, + -readability-isolate-declaration, + -readability-magic-numbers, + -readability-named-parameter, + -readability-redundant-access-specifiers, + -readability-uppercase-literal-suffix + +# Treat warnings as errors for CI/CD +WarningsAsErrors: false + +# Header filter to include project headers +HeaderFilterRegex: '(Core|Generals|GeneralsMD|Dependencies)/.*\.(h|hpp)$' + +# Check options for specific rules +CheckOptions: + # Naming conventions - adapted for the existing codebase style + - key: readability-identifier-naming.ClassCase + value: CamelCase + - key: readability-identifier-naming.StructCase + value: CamelCase + - key: readability-identifier-naming.FunctionCase + value: CamelCase + - key: readability-identifier-naming.MethodCase + value: CamelCase + - key: readability-identifier-naming.VariableCase + value: lower_case + - key: readability-identifier-naming.ParameterCase + value: lower_case + - key: readability-identifier-naming.MemberCase + value: lower_case + - key: readability-identifier-naming.MemberPrefix + value: m_ + - key: readability-identifier-naming.ConstantCase + value: UPPER_CASE + - key: readability-identifier-naming.EnumConstantCase + value: UPPER_CASE + - key: readability-identifier-naming.MacroDefinitionCase + value: UPPER_CASE + + # Performance settings + - key: performance-for-range-copy.WarnOnAllAutoCopies + value: true + - key: performance-unnecessary-value-param.AllowedTypes + value: 'AsciiString;UnicodeString;Utf8String;Utf16String' + + # Modernize settings - be conservative for legacy code + - key: modernize-use-nullptr.NullMacros + value: 'NULL' + - key: modernize-replace-auto-ptr.IncludeStyle + value: llvm + + # Readability settings + - key: readability-function-size.LineThreshold + value: 150 + - key: readability-function-size.StatementThreshold + value: 100 + - key: readability-function-size.BranchThreshold + value: 25 + - key: readability-function-size.ParameterThreshold + value: 8 + - key: readability-function-size.NestingThreshold + value: 6 + + # Bugprone settings + - key: bugprone-argument-comment.StrictMode + value: false + - key: bugprone-suspicious-string-compare.WarnOnImplicitComparison + value: true + - key: bugprone-suspicious-string-compare.WarnOnLogicalNotComparison + value: true + + # Google style settings + - key: google-readability-braces-around-statements.ShortStatementLines + value: 2 + - key: google-readability-function-size.StatementThreshold + value: 100 + + # CERT settings + - key: cert-dcl16-c.NewSuffixes + value: 'L;LL;LU;LLU' + - key: cert-oop54-cpp.WarnOnlyIfThisHasSuspiciousField + value: false + +# Use .clang-format for formatting suggestions +FormatStyle: file + +# Exclude certain directories and files +# Note: This is handled by HeaderFilterRegex above, but can be extended diff --git a/scripts/run-clang-tidy.py b/scripts/run-clang-tidy.py new file mode 100755 index 00000000000..43c061378fc --- /dev/null +++ b/scripts/run-clang-tidy.py @@ -0,0 +1,350 @@ +#!/usr/bin/env python3 +# TheSuperHackers @build JohnsterID 15/09/2025 Add clang-tidy runner script for code quality analysis +# TheSuperHackers @build bobtista 04/12/2025 Simplify script for PCH-free analysis builds + +""" +Clang-tidy runner script for GeneralsGameCode project. + +This is a convenience wrapper that: +- Auto-detects the clang-tidy analysis build (build/clang-tidy) +- Filters source files by include/exclude patterns +- Processes files in batches to handle Windows command-line limits +- Provides progress reporting + +For the analysis build to work correctly, it must be built WITHOUT precompiled headers. +Run this first: + cmake -B build/clang-tidy -DCMAKE_DISABLE_PRECOMPILE_HEADERS=ON -G Ninja +""" + +import argparse +import json +import multiprocessing +import subprocess +import sys +from pathlib import Path +from typing import List, Optional, Tuple + + +def find_clang_tidy() -> str: + """Find clang-tidy executable in PATH.""" + try: + result = subprocess.run( + ['clang-tidy', '--version'], + capture_output=True, + text=True, + timeout=5 + ) + if result.returncode == 0: + return 'clang-tidy' + except (FileNotFoundError, subprocess.TimeoutExpired): + pass + + raise RuntimeError( + "clang-tidy not found in PATH. Please install clang-tidy:\n" + " Windows: Install LLVM from https://llvm.org/builds/" + ) + + +def find_project_root() -> Path: + """Find the project root directory.""" + current = Path(__file__).resolve().parent + while current != current.parent: + if (current / 'CMakeLists.txt').exists(): + return current + current = current.parent + raise RuntimeError("Could not find project root (no CMakeLists.txt found)") + + +def find_compile_commands(build_dir: Optional[Path] = None) -> Path: + """Find compile_commands.json from the clang-tidy analysis build.""" + project_root = find_project_root() + + if build_dir: + if not build_dir.is_absolute(): + build_dir = project_root / build_dir + compile_commands = build_dir / "compile_commands.json" + if compile_commands.exists(): + return compile_commands + raise FileNotFoundError( + f"compile_commands.json not found in {build_dir}" + ) + + # Use the dedicated clang-tidy build (PCH-free, required for correct analysis) + clang_tidy_build = project_root / "build" / "clang-tidy" + compile_commands = clang_tidy_build / "compile_commands.json" + + if not compile_commands.exists(): + raise RuntimeError( + "Clang-tidy build not found!\n\n" + "Create the analysis build first:\n" + " cmake -B build/clang-tidy -DCMAKE_DISABLE_PRECOMPILE_HEADERS=ON -G Ninja\n\n" + "Or specify a different build with --build-dir" + ) + + return compile_commands + + +def load_compile_commands(compile_commands_path: Path) -> List[dict]: + """Load and parse compile_commands.json.""" + try: + with open(compile_commands_path, 'r') as f: + return json.load(f) + except (json.JSONDecodeError, IOError) as e: + raise RuntimeError(f"Failed to load compile_commands.json: {e}") + + +def filter_source_files(compile_commands: List[dict], + include_patterns: List[str], + exclude_patterns: List[str]) -> List[str]: + """Filter source files based on include/exclude patterns.""" + project_root = find_project_root() + source_files = set() + + for entry in compile_commands: + file_path = Path(entry['file']) + + # Convert to relative path for pattern matching + try: + rel_path = file_path.relative_to(project_root) + except ValueError: + continue # File outside project root + + rel_path_str = str(rel_path) + + if include_patterns: + if not any(pattern in rel_path_str for pattern in include_patterns): + continue + + if any(pattern in rel_path_str for pattern in exclude_patterns): + continue + + if file_path.suffix in {'.cpp', '.cxx', '.cc', '.c'}: + source_files.add(str(file_path)) + + return sorted(source_files) + + +def _run_batch(args: Tuple) -> int: + """Helper function to run clang-tidy on a batch of files (for multiprocessing).""" + batch_num, batch, compile_commands_dir, fix, extra_args, project_root, clang_tidy_exe = args + + cmd = [ + clang_tidy_exe, + f'-p={compile_commands_dir}', + ] + + if fix: + cmd.append('--fix') + + if extra_args: + cmd.extend(extra_args) + + cmd.extend(batch) + + print(f"Batch {batch_num}: Analyzing {len(batch)} file(s)...") + + try: + result = subprocess.run(cmd, cwd=project_root) + return result.returncode + except FileNotFoundError: + print("Error: clang-tidy not found. Please install LLVM/Clang.", file=sys.stderr) + return 1 + + +def run_clang_tidy(source_files: List[str], + compile_commands_path: Path, + extra_args: List[str], + fix: bool = False, + jobs: int = 1) -> int: + """Run clang-tidy on source files in batches, optionally in parallel.""" + if not source_files: + print("No source files to analyze.") + return 0 + + # Find clang-tidy executable + clang_tidy_exe = find_clang_tidy() + + # Process files in batches (Windows has ~8191 char command-line limit) + BATCH_SIZE = 50 + total_files = len(source_files) + batches = [source_files[i:i + BATCH_SIZE] for i in range(0, total_files, BATCH_SIZE)] + + project_root = find_project_root() + compile_commands_dir = compile_commands_path.parent + + if jobs > 1: + print(f"Running clang-tidy on {total_files} file(s) in {len(batches)} batch(es) with {jobs} workers...\n") + + try: + with multiprocessing.Pool(processes=jobs) as pool: + results = pool.map( + _run_batch, + [ + (idx + 1, batch, compile_commands_dir, fix, extra_args, project_root, clang_tidy_exe) + for idx, batch in enumerate(batches) + ] + ) + overall_returncode = max(results) if results else 0 + except KeyboardInterrupt: + print("\nInterrupted by user.") + return 130 + else: + print(f"Running clang-tidy on {total_files} file(s) in {len(batches)} batch(es)...\n") + + overall_returncode = 0 + for batch_num, batch in enumerate(batches, 1): + try: + returncode = _run_batch((batch_num, batch, compile_commands_dir, fix, extra_args, project_root, clang_tidy_exe)) + if returncode != 0: + overall_returncode = returncode + except KeyboardInterrupt: + print("\nInterrupted by user.") + return 130 + + return overall_returncode + + +def main(): + parser = argparse.ArgumentParser( + description="Run clang-tidy on GeneralsGameCode project", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # First-time setup: Create PCH-free analysis build + cmake -B build/clang-tidy -DCMAKE_DISABLE_PRECOMPILE_HEADERS=ON -G Ninja + + # Analyze all source files + python scripts/run-clang-tidy.py + + # Analyze specific directory + python scripts/run-clang-tidy.py --include Core/Libraries/ + + # Analyze with specific checks + python scripts/run-clang-tidy.py --include GameClient/ -- -checks="-*,modernize-use-nullptr" + + # Apply fixes (use with caution!) + python scripts/run-clang-tidy.py --fix --include Keyboard.cpp -- -checks="-*,modernize-use-nullptr" + + # Use parallel processing (recommended: --jobs 4 for 6-core CPUs) + python scripts/run-clang-tidy.py --jobs 4 -- -checks="-*,modernize-use-nullptr" + + # Use different build directory + python scripts/run-clang-tidy.py --build-dir build/win32-debug + +Note: Requires a PCH-free build. Create with: + cmake -B build/clang-tidy -DCMAKE_DISABLE_PRECOMPILE_HEADERS=ON -G Ninja + """ + ) + + parser.add_argument( + '--build-dir', '-b', + type=Path, + help='Build directory with compile_commands.json (auto-detected if omitted)' + ) + + parser.add_argument( + '--include', '-i', + action='append', + default=[], + help='Include files matching this pattern (can be used multiple times)' + ) + + parser.add_argument( + '--exclude', '-e', + action='append', + default=[], + help='Exclude files matching this pattern (can be used multiple times)' + ) + + parser.add_argument( + '--fix', + action='store_true', + help='Apply suggested fixes automatically (use with caution!)' + ) + + parser.add_argument( + '--jobs', '-j', + type=int, + default=1, + help='Number of parallel workers (default: 1). Recommended: 4 for 6-core CPUs' + ) + + parser.add_argument( + 'clang_tidy_args', + nargs='*', + help='Additional arguments to pass to clang-tidy, or specific files to analyze (if files are provided, --include/--exclude are ignored)' + ) + + args = parser.parse_args() + + try: + compile_commands_path = find_compile_commands(args.build_dir) + print(f"Using compile commands: {compile_commands_path}\n") + + # Check if any arguments look like file paths + project_root = find_project_root() + specified_files = [] + clang_tidy_args = [] + + for arg in args.clang_tidy_args: + # Check if it's a file path (exists and has source file extension) + file_path = Path(arg) + if not file_path.is_absolute(): + file_path = project_root / file_path + + if file_path.exists() and file_path.suffix in {'.cpp', '.cxx', '.cc', '.c', '.h', '.hpp'}: + specified_files.append(str(file_path.resolve())) + else: + clang_tidy_args.append(arg) + + # If specific files were provided, use them directly + if specified_files: + print(f"Analyzing {len(specified_files)} specified file(s)\n") + return run_clang_tidy( + specified_files, + compile_commands_path, + clang_tidy_args, + args.fix, + args.jobs + ) + + # Otherwise, filter from compile_commands.json + compile_commands = load_compile_commands(compile_commands_path) + + default_excludes = [ + 'Dependencies/MaxSDK', # External SDK + '_deps/', # CMake dependencies + 'build/', # Build artifacts + '.git/', # Git directory + ] + + exclude_patterns = default_excludes + args.exclude + + source_files = filter_source_files( + compile_commands, + args.include, + exclude_patterns + ) + + if not source_files: + print("No source files found matching the criteria.") + return 1 + + print(f"Found {len(source_files)} source file(s) to analyze\n") + + return run_clang_tidy( + source_files, + compile_commands_path, + clang_tidy_args, + args.fix, + args.jobs + ) + + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + return 1 + + +if __name__ == '__main__': + sys.exit(main()) + From 255cc8db4fe419e84cffdf7e2008d3d0d8ee738a Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Tue, 9 Dec 2025 08:37:57 -0500 Subject: [PATCH 2/4] fix: add CMAKE_EXPORT_COMPILE_COMMANDS to cmake commands and enable parallelization by default --- scripts/run-clang-tidy.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/run-clang-tidy.py b/scripts/run-clang-tidy.py index 43c061378fc..22f76d15051 100755 --- a/scripts/run-clang-tidy.py +++ b/scripts/run-clang-tidy.py @@ -13,7 +13,7 @@ For the analysis build to work correctly, it must be built WITHOUT precompiled headers. Run this first: - cmake -B build/clang-tidy -DCMAKE_DISABLE_PRECOMPILE_HEADERS=ON -G Ninja + cmake -B build/clang-tidy -DCMAKE_DISABLE_PRECOMPILE_HEADERS=ON -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -G Ninja """ import argparse @@ -75,9 +75,9 @@ def find_compile_commands(build_dir: Optional[Path] = None) -> Path: if not compile_commands.exists(): raise RuntimeError( - "Clang-tidy build not found!\n\n" + "compile_commands.json not found!\n\n" "Create the analysis build first:\n" - " cmake -B build/clang-tidy -DCMAKE_DISABLE_PRECOMPILE_HEADERS=ON -G Ninja\n\n" + " cmake -B build/clang-tidy -DCMAKE_DISABLE_PRECOMPILE_HEADERS=ON -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -G Ninja\n\n" "Or specify a different build with --build-dir" ) @@ -211,7 +211,7 @@ def main(): epilog=""" Examples: # First-time setup: Create PCH-free analysis build - cmake -B build/clang-tidy -DCMAKE_DISABLE_PRECOMPILE_HEADERS=ON -G Ninja + cmake -B build/clang-tidy -DCMAKE_DISABLE_PRECOMPILE_HEADERS=ON -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -G Ninja # Analyze all source files python scripts/run-clang-tidy.py @@ -232,7 +232,7 @@ def main(): python scripts/run-clang-tidy.py --build-dir build/win32-debug Note: Requires a PCH-free build. Create with: - cmake -B build/clang-tidy -DCMAKE_DISABLE_PRECOMPILE_HEADERS=ON -G Ninja + cmake -B build/clang-tidy -DCMAKE_DISABLE_PRECOMPILE_HEADERS=ON -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -G Ninja """ ) @@ -265,8 +265,8 @@ def main(): parser.add_argument( '--jobs', '-j', type=int, - default=1, - help='Number of parallel workers (default: 1). Recommended: 4 for 6-core CPUs' + default=multiprocessing.cpu_count(), + help=f'Number of parallel workers (default: {multiprocessing.cpu_count()} - auto-detected). Use 1 for serial processing' ) parser.add_argument( From e012b3a462268fb08b134027fd4f81c2e4636bb4 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Wed, 10 Dec 2025 11:46:41 -0500 Subject: [PATCH 3/4] build: reduce noisy output in clang-tidy script - only show warnings/errors by default --- scripts/run-clang-tidy.py | 147 ++++++++++++++++++++++++++++++++------ 1 file changed, 126 insertions(+), 21 deletions(-) diff --git a/scripts/run-clang-tidy.py b/scripts/run-clang-tidy.py index 22f76d15051..5c20a270eff 100755 --- a/scripts/run-clang-tidy.py +++ b/scripts/run-clang-tidy.py @@ -9,7 +9,7 @@ - Auto-detects the clang-tidy analysis build (build/clang-tidy) - Filters source files by include/exclude patterns - Processes files in batches to handle Windows command-line limits -- Provides progress reporting +- Provides quiet progress reporting (only shows warnings/errors by default) For the analysis build to work correctly, it must be built WITHOUT precompiled headers. Run this first: @@ -21,8 +21,9 @@ import multiprocessing import subprocess import sys +from collections import defaultdict from pathlib import Path -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Dict def find_clang_tidy() -> str: @@ -124,9 +125,9 @@ def filter_source_files(compile_commands: List[dict], return sorted(source_files) -def _run_batch(args: Tuple) -> int: +def _run_batch(args: Tuple) -> Tuple[int, Dict[str, List[str]]]: """Helper function to run clang-tidy on a batch of files (for multiprocessing).""" - batch_num, batch, compile_commands_dir, fix, extra_args, project_root, clang_tidy_exe = args + batch_num, batch, compile_commands_dir, fix, extra_args, project_root, clang_tidy_exe, verbose = args cmd = [ clang_tidy_exe, @@ -141,21 +142,62 @@ def _run_batch(args: Tuple) -> int: cmd.extend(batch) - print(f"Batch {batch_num}: Analyzing {len(batch)} file(s)...") - + issues_by_file = defaultdict(list) + try: - result = subprocess.run(cmd, cwd=project_root) - return result.returncode + result = subprocess.run( + cmd, + cwd=project_root, + capture_output=True, + text=True + ) + + # Parse output to extract warnings/errors + if result.stdout or result.stderr: + output = result.stdout + result.stderr + + for line in output.splitlines(): + line = line.strip() + if not line: + continue + + # clang-tidy diagnostic format: "path/to/file.cpp:line:col: level: message" + line_lower = line.lower() + is_warning_or_error = ('warning' in line_lower or 'error' in line_lower) + + # Only process lines that look like diagnostics (have colons and file paths) + if ':' in line and (is_warning_or_error or verbose): + # Try to extract file path (first part before colon) + parts = line.split(':', 1) + if parts: + file_path = parts[0].strip() + # Check if it looks like a file path + if any(file_path.endswith(ext) for ext in ['.cpp', '.cxx', '.cc', '.c', '.h', '.hpp', '.hxx']): + # Extract relative path for cleaner output + try: + rel_path = Path(file_path).relative_to(project_root) + file_key = str(rel_path) + except (ValueError, OSError): + file_key = file_path + + # Only add if it's a warning/error, or if verbose mode + if is_warning_or_error or verbose: + issues_by_file[file_key].append(line) + + return result.returncode, dict(issues_by_file) except FileNotFoundError: - print("Error: clang-tidy not found. Please install LLVM/Clang.", file=sys.stderr) - return 1 + error_msg = "Error: clang-tidy not found. Please install LLVM/Clang." + if verbose: + print(error_msg, file=sys.stderr) + return 1, {} def run_clang_tidy(source_files: List[str], compile_commands_path: Path, extra_args: List[str], fix: bool = False, - jobs: int = 1) -> int: + jobs: int = 1, + verbose: bool = False) -> int: """Run clang-tidy on source files in batches, optionally in parallel.""" if not source_files: print("No source files to analyze.") @@ -172,35 +214,85 @@ def run_clang_tidy(source_files: List[str], project_root = find_project_root() compile_commands_dir = compile_commands_path.parent + all_issues = defaultdict(list) + files_with_issues = set() + total_issues = 0 + if jobs > 1: - print(f"Running clang-tidy on {total_files} file(s) in {len(batches)} batch(es) with {jobs} workers...\n") + if verbose: + print(f"Running clang-tidy on {total_files} file(s) in {len(batches)} batch(es) with {jobs} workers...\n") + else: + print(f"Analyzing {total_files} file(s) with {jobs} workers...", end='', flush=True) try: with multiprocessing.Pool(processes=jobs) as pool: - results = pool.map( + batch_results = pool.map( _run_batch, [ - (idx + 1, batch, compile_commands_dir, fix, extra_args, project_root, clang_tidy_exe) + (idx + 1, batch, compile_commands_dir, fix, extra_args, project_root, clang_tidy_exe, verbose) for idx, batch in enumerate(batches) ] ) - overall_returncode = max(results) if results else 0 + + # Collect results + overall_returncode = 0 + for returncode, issues in batch_results: + if returncode != 0: + overall_returncode = returncode + for file_path, file_issues in issues.items(): + if file_issues: + all_issues[file_path].extend(file_issues) + files_with_issues.add(file_path) + total_issues += len(file_issues) + + if not verbose: + print(" done.") except KeyboardInterrupt: print("\nInterrupted by user.") return 130 else: - print(f"Running clang-tidy on {total_files} file(s) in {len(batches)} batch(es)...\n") + if verbose: + print(f"Running clang-tidy on {total_files} file(s) in {len(batches)} batch(es)...\n") + else: + print(f"Analyzing {total_files} file(s)...", end='', flush=True) overall_returncode = 0 for batch_num, batch in enumerate(batches, 1): try: - returncode = _run_batch((batch_num, batch, compile_commands_dir, fix, extra_args, project_root, clang_tidy_exe)) + if verbose: + print(f"Batch {batch_num}/{len(batches)}: {len(batch)} file(s)...") + + returncode, issues = _run_batch((batch_num, batch, compile_commands_dir, fix, extra_args, project_root, clang_tidy_exe, verbose)) if returncode != 0: overall_returncode = returncode + + # Collect issues + for file_path, file_issues in issues.items(): + if file_issues: + all_issues[file_path].extend(file_issues) + files_with_issues.add(file_path) + total_issues += len(file_issues) + + if not verbose and batch_num < len(batches): + print('.', end='', flush=True) except KeyboardInterrupt: print("\nInterrupted by user.") return 130 + + if not verbose: + print(" done.") + # Print summary + print(f"\nSummary: {len(files_with_issues)} file(s) with issues, {total_issues} total issue(s)") + + # Print issues (only warnings/errors, not verbose informational messages) + if all_issues: + print("\nIssues found:") + for file_path in sorted(all_issues.keys()): + print(f"\n{file_path}:") + for issue in all_issues[file_path]: + print(f" {issue}") + return overall_returncode @@ -228,6 +320,9 @@ def main(): # Use parallel processing (recommended: --jobs 4 for 6-core CPUs) python scripts/run-clang-tidy.py --jobs 4 -- -checks="-*,modernize-use-nullptr" + # Show verbose output (default: only warnings/errors) + python scripts/run-clang-tidy.py --verbose --include Core/Libraries/ + # Use different build directory python scripts/run-clang-tidy.py --build-dir build/win32-debug @@ -269,6 +364,12 @@ def main(): help=f'Number of parallel workers (default: {multiprocessing.cpu_count()} - auto-detected). Use 1 for serial processing' ) + parser.add_argument( + '--verbose', '-v', + action='store_true', + help='Show detailed output for each file (default: only show warnings/errors)' + ) + parser.add_argument( 'clang_tidy_args', nargs='*', @@ -299,13 +400,15 @@ def main(): # If specific files were provided, use them directly if specified_files: - print(f"Analyzing {len(specified_files)} specified file(s)\n") + if args.verbose: + print(f"Analyzing {len(specified_files)} specified file(s)\n") return run_clang_tidy( specified_files, compile_commands_path, clang_tidy_args, args.fix, - args.jobs + args.jobs, + args.verbose ) # Otherwise, filter from compile_commands.json @@ -330,14 +433,16 @@ def main(): print("No source files found matching the criteria.") return 1 - print(f"Found {len(source_files)} source file(s) to analyze\n") + if args.verbose: + print(f"Found {len(source_files)} source file(s) to analyze\n") return run_clang_tidy( source_files, compile_commands_path, clang_tidy_args, args.fix, - args.jobs + args.jobs, + args.verbose ) except Exception as e: From b598b6371696f7d57594ffd0d705b4b9f1a6a20a Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Wed, 10 Dec 2025 11:55:17 -0500 Subject: [PATCH 4/4] build: clean up whitespace and remove self-explanatory comments in clang-tidy script --- scripts/run-clang-tidy.py | 55 +++++++++++++-------------------------- 1 file changed, 18 insertions(+), 37 deletions(-) diff --git a/scripts/run-clang-tidy.py b/scripts/run-clang-tidy.py index 5c20a270eff..49f06b4563a 100755 --- a/scripts/run-clang-tidy.py +++ b/scripts/run-clang-tidy.py @@ -39,7 +39,7 @@ def find_clang_tidy() -> str: return 'clang-tidy' except (FileNotFoundError, subprocess.TimeoutExpired): pass - + raise RuntimeError( "clang-tidy not found in PATH. Please install clang-tidy:\n" " Windows: Install LLVM from https://llvm.org/builds/" @@ -70,7 +70,6 @@ def find_compile_commands(build_dir: Optional[Path] = None) -> Path: f"compile_commands.json not found in {build_dir}" ) - # Use the dedicated clang-tidy build (PCH-free, required for correct analysis) clang_tidy_build = project_root / "build" / "clang-tidy" compile_commands = clang_tidy_build / "compile_commands.json" @@ -104,11 +103,10 @@ def filter_source_files(compile_commands: List[dict], for entry in compile_commands: file_path = Path(entry['file']) - # Convert to relative path for pattern matching try: rel_path = file_path.relative_to(project_root) except ValueError: - continue # File outside project root + continue rel_path_str = str(rel_path) @@ -128,7 +126,7 @@ def filter_source_files(compile_commands: List[dict], def _run_batch(args: Tuple) -> Tuple[int, Dict[str, List[str]]]: """Helper function to run clang-tidy on a batch of files (for multiprocessing).""" batch_num, batch, compile_commands_dir, fix, extra_args, project_root, clang_tidy_exe, verbose = args - + cmd = [ clang_tidy_exe, f'-p={compile_commands_dir}', @@ -143,7 +141,7 @@ def _run_batch(args: Tuple) -> Tuple[int, Dict[str, List[str]]]: cmd.extend(batch) issues_by_file = defaultdict(list) - + try: result = subprocess.run( cmd, @@ -151,36 +149,29 @@ def _run_batch(args: Tuple) -> Tuple[int, Dict[str, List[str]]]: capture_output=True, text=True ) - - # Parse output to extract warnings/errors + if result.stdout or result.stderr: output = result.stdout + result.stderr - + for line in output.splitlines(): line = line.strip() if not line: continue - - # clang-tidy diagnostic format: "path/to/file.cpp:line:col: level: message" + line_lower = line.lower() is_warning_or_error = ('warning' in line_lower or 'error' in line_lower) - - # Only process lines that look like diagnostics (have colons and file paths) + if ':' in line and (is_warning_or_error or verbose): - # Try to extract file path (first part before colon) parts = line.split(':', 1) if parts: file_path = parts[0].strip() - # Check if it looks like a file path if any(file_path.endswith(ext) for ext in ['.cpp', '.cxx', '.cc', '.c', '.h', '.hpp', '.hxx']): - # Extract relative path for cleaner output try: rel_path = Path(file_path).relative_to(project_root) file_key = str(rel_path) except (ValueError, OSError): file_key = file_path - - # Only add if it's a warning/error, or if verbose mode + if is_warning_or_error or verbose: issues_by_file[file_key].append(line) @@ -203,21 +194,19 @@ def run_clang_tidy(source_files: List[str], print("No source files to analyze.") return 0 - # Find clang-tidy executable clang_tidy_exe = find_clang_tidy() - # Process files in batches (Windows has ~8191 char command-line limit) BATCH_SIZE = 50 total_files = len(source_files) batches = [source_files[i:i + BATCH_SIZE] for i in range(0, total_files, BATCH_SIZE)] project_root = find_project_root() compile_commands_dir = compile_commands_path.parent - + all_issues = defaultdict(list) files_with_issues = set() total_issues = 0 - + if jobs > 1: if verbose: print(f"Running clang-tidy on {total_files} file(s) in {len(batches)} batch(es) with {jobs} workers...\n") @@ -233,8 +222,7 @@ def run_clang_tidy(source_files: List[str], for idx, batch in enumerate(batches) ] ) - - # Collect results + overall_returncode = 0 for returncode, issues in batch_results: if returncode != 0: @@ -265,8 +253,7 @@ def run_clang_tidy(source_files: List[str], returncode, issues = _run_batch((batch_num, batch, compile_commands_dir, fix, extra_args, project_root, clang_tidy_exe, verbose)) if returncode != 0: overall_returncode = returncode - - # Collect issues + for file_path, file_issues in issues.items(): if file_issues: all_issues[file_path].extend(file_issues) @@ -278,14 +265,12 @@ def run_clang_tidy(source_files: List[str], except KeyboardInterrupt: print("\nInterrupted by user.") return 130 - + if not verbose: print(" done.") - # Print summary print(f"\nSummary: {len(files_with_issues)} file(s) with issues, {total_issues} total issue(s)") - - # Print issues (only warnings/errors, not verbose informational messages) + if all_issues: print("\nIssues found:") for file_path in sorted(all_issues.keys()): @@ -382,23 +367,20 @@ def main(): compile_commands_path = find_compile_commands(args.build_dir) print(f"Using compile commands: {compile_commands_path}\n") - # Check if any arguments look like file paths project_root = find_project_root() specified_files = [] clang_tidy_args = [] - + for arg in args.clang_tidy_args: - # Check if it's a file path (exists and has source file extension) file_path = Path(arg) if not file_path.is_absolute(): file_path = project_root / file_path - + if file_path.exists() and file_path.suffix in {'.cpp', '.cxx', '.cc', '.c', '.h', '.hpp'}: specified_files.append(str(file_path.resolve())) else: clang_tidy_args.append(arg) - - # If specific files were provided, use them directly + if specified_files: if args.verbose: print(f"Analyzing {len(specified_files)} specified file(s)\n") @@ -411,7 +393,6 @@ def main(): args.verbose ) - # Otherwise, filter from compile_commands.json compile_commands = load_compile_commands(compile_commands_path) default_excludes = [