diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000000..41a2f6757a --- /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 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1c9c525ae1..34f9668490 100644 --- a/.gitignore +++ b/.gitignore @@ -57,4 +57,11 @@ cmake-build-*/ ## Ninja .ninja_deps .ninja_log -build.ninja \ No newline at end of file +build.ninja + +## Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python \ No newline at end of file diff --git a/TESTING.md b/TESTING.md index bcfed39142..b02c5c1e17 100644 --- a/TESTING.md +++ b/TESTING.md @@ -1,4 +1,6 @@ -# Test Replays +# Testing + +## Test Replays The GeneralsReplays folder contains replays and the required maps that are tested in CI to ensure that the game is retail compatible. @@ -11,4 +13,117 @@ START /B /W generalszh.exe -jobs 4 -headless -replay subfolder/*.rep > replay_ch echo %errorlevel% PAUSE ``` -It will run the game in the background and check that each replay is compatible. You need to use a VC6 build with optimizations and RTS_BUILD_OPTION_DEBUG = OFF, otherwise the game won't be compatible. \ No newline at end of file +It will run the game in the background and check that each replay is compatible. You need to use a VC6 build with optimizations and RTS_BUILD_OPTION_DEBUG = OFF, otherwise the game won't be compatible. + +## Code Quality Analysis with clang-tidy + +The project includes clang-tidy configuration for static code analysis to help maintain code quality and catch potential bugs. + +### Prerequisites + +1. **CMake with compile commands export**: The CMake presets already have `CMAKE_EXPORT_COMPILE_COMMANDS=ON` configured. +2. **clang-tidy**: Install clang-tidy for your platform: + - **Linux**: `apt install clang-tidy` or `yum install clang-tools-extra` + - **Windows**: Install LLVM or use the version that comes with Visual Studio + - **macOS**: `brew install llvm` or use Xcode command line tools + +### Running clang-tidy + +#### Method 1: Using the helper script (Recommended) + +The project includes a Python script that simplifies running clang-tidy: + +```bash +# Analyze all source files +python3 scripts/run-clang-tidy.py + +# Analyze only Core directory +python3 scripts/run-clang-tidy.py --include Core/ + +# Analyze GeneralsMD but exclude certain patterns +python3 scripts/run-clang-tidy.py --include GeneralsMD/ --exclude test + +# Use specific build directory +python3 scripts/run-clang-tidy.py --build-dir build/win32 + +# Apply fixes automatically (use with caution!) +python3 scripts/run-clang-tidy.py --fix --include Core/Libraries/ +``` + +#### Method 2: Direct clang-tidy usage + +First, ensure you have a `compile_commands.json` file: + +```bash +# Configure with any preset that exports compile commands +cmake --preset win32 # or vc6, unix, etc. + +# Run clang-tidy on specific files +clang-tidy -p build/win32 Core/Libraries/Source/RTS/File.cpp + +# Run on multiple files with pattern (Unix/Linux/macOS) +find Core/ -name "*.cpp" | xargs clang-tidy -p build/win32 + +# Windows Command Prompt alternative +for /r Core\ %i in (*.cpp) do clang-tidy -p build/win32 "%i" + +# Windows PowerShell alternative +Get-ChildItem -Path Core\ -Recurse -Filter "*.cpp" | ForEach-Object { clang-tidy -p build/win32 $_.FullName } +``` + +### Configuration + +The `.clang-tidy` file in the project root contains configuration tailored for this legacy C++ codebase: + +- **Enabled checks**: Focus on bug-prone patterns, performance issues, and readability +- **Disabled checks**: Overly strict modernization rules that don't fit the legacy codebase +- **Naming conventions**: Adapted to match the existing code style (CamelCase for classes, m_ prefix for members) +- **Header filtering**: Only analyzes project headers, not system/external headers + +### Integration with Development Workflow + +#### For Contributors + +Run clang-tidy on your changes before submitting PRs: + +```bash +# Analyze only files you've modified +python3 scripts/run-clang-tidy.py --include "path/to/your/changes/" +``` + +#### For Maintainers + +Consider running periodic full codebase analysis: + +```bash +# Full analysis (may take a while) +python3 scripts/run-clang-tidy.py > clang-tidy-report.txt 2>&1 +``` + +### MinGW-w64 Compatibility + +The clang-tidy configuration is designed to work with the MinGW-w64 cross-compilation setup. The project supports: + +- **MinGW-w64 headers**: For Windows API compatibility +- **ReactOS ATL**: For COM interface support (as referenced in PR #672) +- **Legacy C++98 patterns**: While encouraging modern practices where appropriate + +### Troubleshooting + +**Issue**: `compile_commands.json not found` +**Solution**: Run cmake configuration first: `cmake --preset ` + +**Issue**: PCH file not found errors (e.g., `cmake_pch.cxx.pch' not found`) +**Solution**: The script now automatically strips precompiled header flags that are incompatible with clang-tidy. If you still encounter issues, ensure you're using the Python helper script which handles this automatically. + +**Issue**: Command line too long on Windows +**Solution**: The script now processes files in batches of 50 to avoid Windows command line length limits. This happens automatically. + +**Issue**: clang-tidy reports errors in system headers +**Solution**: The configuration should filter these out, but you can also use `--system-headers=false` + +**Issue**: Too many warnings for legacy code +**Solution**: Use the `--include` flag to focus on specific directories or files you're working on + +**Issue**: Script fails with relative paths +**Solution**: This has been fixed - the script now properly resolves relative paths. Make sure you're using the latest version. \ No newline at end of file diff --git a/scripts/run-clang-tidy.py b/scripts/run-clang-tidy.py new file mode 100755 index 0000000000..dc200ed202 --- /dev/null +++ b/scripts/run-clang-tidy.py @@ -0,0 +1,344 @@ +#!/usr/bin/env python3 +# TheSuperHackers @build JohnsterID 15/09/2025 Add clang-tidy runner script for code quality analysis + +""" +Clang-tidy runner script for GeneralsGameCode project. + +This script helps run clang-tidy on the codebase with proper configuration +for the MinGW-w64 cross-compilation environment and legacy C++ code. +""" + +import argparse +import json +import os +import re +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path +from typing import List, Optional, Set + + +def find_project_root() -> Path: + """Find the project root directory by looking for CMakeLists.txt.""" + current = Path(__file__).parent.resolve() + while current != current.parent: + if (current / "CMakeLists.txt").exists(): + return current + current = current.parent + raise RuntimeError("Could not find project root (CMakeLists.txt not found)") + + +def find_compile_commands(build_dir: Optional[Path] = None) -> Path: + """Find the compile_commands.json file.""" + project_root = find_project_root() + + if build_dir: + compile_commands = build_dir / "compile_commands.json" + if compile_commands.exists(): + return compile_commands + + # Search common build directories + build_dirs = [ + project_root / "build", + project_root / "build" / "test", + project_root / "build" / "win32", + project_root / "build" / "vc6", + project_root / "build" / "unix", + ] + + for build_path in build_dirs: + compile_commands = build_path / "compile_commands.json" + if compile_commands.exists(): + return compile_commands + + raise RuntimeError( + "Could not find compile_commands.json. " + "Please run cmake with CMAKE_EXPORT_COMPILE_COMMANDS=ON first." + ) + + +def sanitize_compile_command(entry: dict) -> dict: + """Remove PCH and other incompatible flags for clang-tidy.""" + cmd = entry.get('command', '') + + # Remove MSVC precompiled header flags that clang-tidy can't handle + flags_to_remove = [ + r'/Yu[^\s]*', # MSVC: Use precompiled header + r'/Yc[^\s]*', # MSVC: Create precompiled header + r'/Fp[^\s]*', # MSVC: Precompiled header file path + r'/FI[^\s]*', # MSVC: Force include file (used for PCH) + r'-Xclang -include-pch [^\s]+', # Clang PCH + r'-include-pch [^\s]+', + ] + + for flag_pattern in flags_to_remove: + cmd = re.sub(flag_pattern, '', cmd) + + entry['command'] = cmd + return entry + + +def load_compile_commands(compile_commands_path: Path) -> List[dict]: + """Load and parse the compile_commands.json file.""" + try: + with open(compile_commands_path, 'r') as f: + commands = json.load(f) + # Sanitize commands to remove PCH flags + return [sanitize_compile_command(cmd) for cmd in commands] + 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: + # File is outside project root, skip + continue + + rel_path_str = str(rel_path) + + # Check include patterns + if include_patterns: + if not any(pattern in rel_path_str for pattern in include_patterns): + continue + + # Check exclude patterns + if any(pattern in rel_path_str for pattern in exclude_patterns): + continue + + # Only include C++ source files + if file_path.suffix in {'.cpp', '.cxx', '.cc', '.c'}: + source_files.add(str(file_path)) + + return sorted(source_files) + + +def create_sanitized_compile_commands(compile_commands: List[dict], + original_path: Path) -> Path: + """Create a temporary sanitized compile_commands.json file. + + Returns the path to the directory containing the sanitized compile_commands.json. + """ + # Create a temporary directory for the sanitized compile commands + # We need the file to be named exactly "compile_commands.json" for clang-tidy -p + temp_dir = Path(tempfile.mkdtemp( + suffix='_clang_tidy', + prefix='sanitized_', + dir=original_path.parent + )) + + temp_file = temp_dir / 'compile_commands.json' + + try: + with open(temp_file, 'w') as f: + json.dump(compile_commands, f, indent=2) + return temp_dir + except Exception as e: + # Clean up on error + try: + import shutil + shutil.rmtree(temp_dir) + except: + pass + raise RuntimeError(f"Failed to create sanitized compile commands: {e}") + + +def run_clang_tidy(source_files: List[str], + compile_commands: List[dict], + compile_commands_path: Path, + extra_args: List[str], + fix: bool = False, + jobs: int = 1) -> int: + """Run clang-tidy on the specified source files.""" + if not source_files: + print("No source files to analyze.") + return 0 + + # Create a temporary sanitized compile_commands.json + print("Creating sanitized compile commands...") + temp_compile_commands = create_sanitized_compile_commands( + compile_commands, + compile_commands_path + ) + + try: + # Process files in batches to avoid command line length limits on Windows + # Windows cmd.exe has a limit of ~8191 characters + BATCH_SIZE = 50 # Conservative batch size for Windows compatibility + total_files = len(source_files) + batches = [source_files[i:i + BATCH_SIZE] for i in range(0, total_files, BATCH_SIZE)] + + print(f"Running clang-tidy on {total_files} files in {len(batches)} batch(es)...") + if jobs > 1: + print(f"Note: Parallel execution with {jobs} jobs not implemented yet.") + + overall_returncode = 0 + for batch_num, batch in enumerate(batches, 1): + cmd = [ + 'clang-tidy', + f'-p={temp_compile_commands}', + ] + + if fix: + cmd.append('--fix') + + if extra_args: + cmd.extend(extra_args) + + # Add source files for this batch + cmd.extend(batch) + + print(f"\nBatch {batch_num}/{len(batches)}: Analyzing {len(batch)} file(s)...") + + try: + result = subprocess.run(cmd, cwd=find_project_root()) + if result.returncode != 0: + overall_returncode = result.returncode + except FileNotFoundError: + print("Error: clang-tidy not found. Please install clang-tidy.") + return 1 + except KeyboardInterrupt: + print("\nInterrupted by user.") + return 130 + + return overall_returncode + + finally: + # Clean up the temporary directory + try: + shutil.rmtree(temp_compile_commands) + print(f"Cleaned up temporary directory: {temp_compile_commands.name}") + except Exception as e: + print(f"Warning: Could not remove temporary directory {temp_compile_commands}: {e}") + + +def main(): + parser = argparse.ArgumentParser( + description="Run clang-tidy on GeneralsGameCode project", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Analyze all source files + python3 scripts/run-clang-tidy.py + + # Analyze only Core directory + python3 scripts/run-clang-tidy.py --include Core/ + + # Analyze GeneralsMD but exclude tests + python3 scripts/run-clang-tidy.py --include GeneralsMD/ --exclude test + + # Fix issues automatically (use with caution!) + python3 scripts/run-clang-tidy.py --fix --include Core/Libraries/ + + # Use specific build directory + python3 scripts/run-clang-tidy.py --build-dir build/win32 + """ + ) + + parser.add_argument( + '--build-dir', '-b', + type=Path, + help='Build directory containing compile_commands.json' + ) + + 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 jobs (not implemented yet)' + ) + + parser.add_argument( + 'clang_tidy_args', + nargs='*', + help='Additional arguments to pass to clang-tidy' + ) + + args = parser.parse_args() + + try: + # Find compile commands + compile_commands_path = find_compile_commands(args.build_dir) + print(f"Using compile commands: {compile_commands_path}") + + # Load compile commands + compile_commands = load_compile_commands(compile_commands_path) + print(f"Loaded {len(compile_commands)} compile commands") + + # Default exclude patterns for this project + default_excludes = [ + 'Dependencies/MaxSDK', # External SDK + '_deps/', # CMake dependencies + 'build/', # Build artifacts + '.git/', # Git directory + 'stlport.diff', # Patch file + ] + + exclude_patterns = default_excludes + args.exclude + + # Filter source files + 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 files to analyze") + + # Run clang-tidy + return run_clang_tidy( + source_files, + compile_commands, + compile_commands_path, + args.clang_tidy_args, + args.fix, + args.jobs + ) + + except RuntimeError as e: + print(f"Error: {e}", file=sys.stderr) + return 1 + except KeyboardInterrupt: + print("\nInterrupted by user.") + return 130 + + +if __name__ == '__main__': + sys.exit(main()) \ No newline at end of file