| #!/usr/bin/env python3 |
| |
| # Copyright 2026 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """ |
| A tool to identify and remove unnecessary UNSAFE_TODO() wrappers in Chromium. |
| |
| This script automates the verification process by locating all instances of |
| UNSAFE_TODO(), determining if the wrapper is redundant, and confirming that the |
| code is compiled in the current build configuration. |
| |
| Because this process requires approximately 10k-40k compiler invocations, we |
| can't afford the latency of siso/remoteexec/autoninja for each. Thus, this tool |
| compiles locally a single file from the compile_commands.json database generated |
| by GN. This works because UNSAFE_TODO() is a compile-time check. The only |
| caveat are UNSAFE_TODO in templates instantiations, which are not verified by |
| this method. |
| |
| The script supports state persistence via a JSON file, enabling the analysis to |
| resume without losing progress. It also supports multiple builders, allowing |
| verification of the same UNSAFE_TODO() across different configurations. |
| |
| Usage: |
| ./unsafe_todo_remover.py # Run analysis |
| ./unsafe_todo_remover.py --apply # Apply fixes from out.json |
| """ |
| |
| from __future__ import annotations |
| |
| import argparse |
| import json |
| import os |
| import re |
| import shlex |
| import subprocess |
| import sys |
| from collections import defaultdict |
| from collections.abc import Sequence |
| |
| # --- Configuration --- |
| |
| # State is persisted in this file. This allows resuming the analysis |
| # without losing progress. |
| STATE_FILE = "out.json" |
| |
| # A set of builders covering most of the chromium C++ code. |
| # |
| # See infra/config/generated/builders/gn_args_locations.json for reference. |
| BUILDERS = [ |
| # Linux builders |
| ('tryserver.chromium.linux', 'linux-rel'), |
| ('tryserver.chromium.linux', 'linux_chromium_tsan_rel_ng'), |
| ('chromium.linux', 'Linux Builder (dbg)'), |
| ('chromium.memory', 'Linux ASan LSan Builder'), |
| |
| # Chromeos builders |
| ('chromium.chromiumos', 'linux-cfm-rel'), |
| ('chromium.chromiumos', 'linux-chromeos-rel'), |
| ('tryserver.chromium.chromiumos', 'chromeos-libfuzzer-asan-rel'), |
| |
| # Android builders |
| ('chromium.android', 'android-x86-rel'), |
| ('chromium', 'android-arm64-archive-rel'), |
| ('chromium.android', 'android-14-x64-rel'), |
| ('chromium.android', 'android-x86-rel'), |
| ('chromium.fuzz', 'android-arm64-libfuzzer-hwasan'), |
| ('tryserver.chromium.android', 'android-cronet-arm-dbg'), |
| ('tryserver.chromium.android', 'android-webview-13-x64-dbg'), |
| |
| # Fuchsia |
| ('chromium.fuchsia', 'Deterministic Fuchsia (dbg)'), |
| |
| # Mac |
| ('chromium.mac', 'mac-arm64-dbg'), |
| ('chromium.mac', 'mac-arm64-rel'), |
| ('tryserver.chromium.mac', 'mac-rel'), |
| ('tryserver.chromium.mac', 'mac-rel'), |
| |
| # Dawn |
| ('tryserver.chromium.dawn', 'dawn-win11-arm64-deps-rel'), |
| |
| # Windows builders |
| ('chromium.win', 'linux-win-cross-rel'), |
| ('chromium.memory', 'win-asan'), |
| ('chromium.win', 'Win Builder'), |
| ('chromium.win', 'Win x64 Builder (dbg)'), |
| ('chromium.win', 'linux-win-cross-rel'), |
| ('chromium.win', 'win-arm64-rel'), |
| ('tryserver.chromium.win', 'win-arm64-compile-dbg'), |
| ('tryserver.chromium.win', 'win-libfuzzer-asan-rel'), |
| ('tryserver.chromium.win', 'win-rel'), |
| ('tryserver.chromium.win', 'win_optional_gpu_tests_rel'), |
| |
| # Fuchsia cast |
| ('chromium.fuchsia', 'fuchsia-x64-cast-receiver-rel'), |
| |
| # Android cast |
| ('chromium.android', "android-cast-arm-dbg"), |
| ] |
| |
| EXCLUDED_DIRECTORIES = [ |
| # Non production code: |
| "agents/", |
| "tools/", |
| "build/", |
| |
| # UNSAFE_TODO in template. |
| # |
| # C++ templates can explode into many instantiations, making it impractical |
| # to verify all of them. This script only build the file that contains the |
| # UNSAFE_TODO, so template instantiations in other files are not verified. |
| # Thus, we exclude files known to have UNSAFE_TODO in templates. |
| "base/strings/string_tokenizer.h", |
| "chrome/browser/media/router/discovery/discovery_network_list_posix.cc", |
| "components/zucchini/io_utils.h", |
| "components/zucchini/patch_utils.h", |
| "components/zucchini/rel32_utils.h", |
| "device/fido/cbor_extract.h", |
| "gpu/command_buffer/common/cmd_buffer_common.h", |
| "gpu/command_buffer/common/gles2_cmd_utils.h", |
| "mojo/core/broker_messages.h", |
| "mojo/public/cpp/bindings/lib/array_internal.h", |
| "mojo/public/cpp/bindings/lib/array_serialization.h", |
| "third_party/blink/renderer/core/css/parser/css_tokenizer_input_stream.h", |
| "third_party/blink/renderer/platform/fonts/opentype/open_type_types.h", |
| "ui/gfx/x/property_cache.h", |
| |
| # UNSAFE_TODO wrapping specific macros. |
| # |
| # Those macros expands sometimes into code that triggers |
| # -WUnsafe-buffer-usage or not, depending on the platform. These variations |
| # are not supported by the script, so we exclude them. |
| "chrome/browser/media/router/discovery/discovery_network_list_posix.cc", |
| "net/base/network_interfaces_posix.cc", |
| "net/dns/address_info.cc", |
| "net/dns/loopback_only.cc", |
| "media/gpu/test/video_test_helpers.cc", |
| "net/cert/ct_objects_extractor.cc", |
| "sandbox/linux/syscall_broker/broker_simple_message.cc", |
| ] |
| |
| # List of functions that are considered unsafe and should not be unwrapped. |
| # There is not need to check the content of UNSAFE_TODO for these functions, |
| # as they are always unsafe. This is used to mark them as LIBC in the state |
| # file, which means they cannot be removed. |
| # This list is based on the Clang source code. |
| UNSAFE_LIBC_FUNCTIONS = { |
| "atof", |
| "atoi", |
| "atol", |
| "atoll", |
| "bcopy", |
| "bsearch", |
| "bzero", |
| "fgets", |
| "fgetws", |
| "fputs", |
| "fputws", |
| "fread", |
| "fwrite", |
| "gets", |
| "memccpy", |
| "memchr", |
| "memcmp", |
| "memcpy", |
| "memmove", |
| "mempcpy", |
| "memset", |
| "puts", |
| "qsort", |
| "strcasecmp", |
| "strcat", |
| "strchr", |
| "strcmp", |
| "strcoll", |
| "strcpy", |
| "strcspn", |
| "strdup", |
| "strerror_r", |
| "strerror_s", |
| "stricmp", |
| "strlcat", |
| "strlcpy", |
| "strlen", |
| "strncat", |
| "strncmp", |
| "strncpy", |
| "strndup", |
| "strnlen", |
| "strpbrk", |
| "strrchr", |
| "strspn", |
| "strstr", |
| "strtod", |
| "strtof", |
| "strtoimax", |
| "strtok", |
| "strtol", |
| "strtold", |
| "strtoll", |
| "strtoul", |
| "strtoull", |
| "strtoumax", |
| "strxfrm", |
| "wmemchr", |
| "wmemcmp", |
| "wmemcpy", |
| "wmemmove", |
| "wmemset", |
| } |
| |
| # Regex to match: optional scope (:: or std::), one of the functions, followed |
| # by word boundary. Used to check the content inside UNSAFE_TODO(...). |
| LIBC_REGEX = re.compile(r'^\s*(?:(?:std)?::)?(?:' + |
| '|'.join(re.escape(f) |
| for f in UNSAFE_LIBC_FUNCTIONS) + r')\b') |
| |
| # --- Helpers --- |
| |
| |
| class Colors: |
| CYAN = '\033[96m' |
| GREEN = '\033[92m' |
| YELLOW = '\033[93m' |
| RED = '\033[91m' |
| MAGENTA = '\033[95m' |
| BLUE = '\033[94m' |
| RESET = '\033[0m' |
| |
| |
| def log(category: str, message: str): |
| """Prints a categorized, colored message to stderr.""" |
| cat_upper = category.upper() |
| color = Colors.RESET |
| |
| if cat_upper in ("ERR", "FAIL", "ERROR"): |
| color = Colors.RED |
| elif cat_upper in ("WARN", "WARNING"): |
| color = Colors.YELLOW |
| elif cat_upper in ("OK", "SUCCESS", "PASS"): |
| color = Colors.GREEN |
| elif cat_upper in ("INFO", "SCAN", "EDIT", "FMT") or cat_upper.isdigit(): |
| color = Colors.CYAN |
| elif cat_upper in ("BUILD", "GN", "BUILDER"): |
| color = Colors.MAGENTA |
| |
| print(f"{color}[{cat_upper}]{Colors.RESET} {message}", file=sys.stderr) |
| |
| |
| def run_command(cmd: Sequence[str], |
| cwd: str | None = None, |
| capture_output: bool = True) -> subprocess.CompletedProcess: |
| """Executes a subprocess command.""" |
| try: |
| return subprocess.run(cmd, |
| cwd=cwd, |
| text=True, |
| capture_output=capture_output, |
| check=False) |
| except Exception as e: |
| log("ERR", f"Command execution failed: {cmd} -> {e}") |
| return subprocess.CompletedProcess(cmd, 1, "", str(e)) |
| |
| |
| def is_excluded(path: str) -> bool: |
| """Checks if a path should be ignored based on project rules.""" |
| |
| # 1. Only modify C++ files |
| valid_extensions = ('.cc', '.h', '.cpp', '.mm', '.hh') |
| if not path.endswith(valid_extensions): |
| return True |
| |
| # 2. Specific directory exclusions |
| if path.startswith(tuple(EXCLUDED_DIRECTORIES)): |
| return True |
| |
| # 3. Third party logic: Exclude all third_party EXCEPT third_party/blink |
| if path.startswith("third_party/"): |
| if not path.startswith("third_party/blink/"): |
| return True |
| |
| return False |
| |
| |
| # --- Core Logic --- |
| |
| |
| class StateManager: |
| """Manages the persistence of analysis results.""" |
| |
| def __init__(self, filepath: str): |
| self.filepath = filepath |
| # Structure: { file_key: { builder_name: status } } |
| self.data: dict[str, dict[str, str]] = {} |
| self.load() |
| |
| def load(self): |
| if os.path.exists(self.filepath): |
| try: |
| with open(self.filepath, 'r') as f: |
| self.data = json.load(f) |
| except json.JSONDecodeError: |
| log("WARN", f"Corrupt {self.filepath}, starting fresh.") |
| |
| def save(self): |
| with open(self.filepath, 'w') as f: |
| json.dump(self.data, f, indent=2, sort_keys=True) |
| |
| def get_builder_status(self, key: str, builder: str) -> str | None: |
| """Returns the status of a specific builder for the given key.""" |
| entry = self.data.get(key, {}) |
| # Handle legacy format where value was a string |
| if isinstance(entry, str): |
| return None |
| return entry.get(builder) |
| |
| def set_status(self, key: str, builder: str, status: str): |
| """Sets the status for a specific builder.""" |
| if key not in self.data or not isinstance(self.data[key], dict): |
| self.data[key] = {} |
| |
| self.data[key][builder] = status |
| self.save() |
| |
| def is_resolved(self, key: str) -> bool: |
| """ |
| Returns True if the UNSAFE_TODO has a definitive result |
| (SUCCESS or FAIL) from *any* builder, or is marked as LIBC. |
| """ |
| if key not in self.data: |
| return False |
| |
| entry = self.data[key] |
| if isinstance(entry, str): |
| # Legacy format support |
| return entry in ["SUCCESS", "FAIL", "LIBC"] |
| |
| vals = entry.values() |
| return "SUCCESS" in vals or "FAIL" in vals or "LIBC" in vals |
| |
| |
| class SourceLocation: |
| """Represents a specific UNSAFE_TODO instance.""" |
| |
| def __init__(self, filepath: str, line: int, col: int, content: str): |
| self.filepath = filepath |
| self.line = line |
| self.col = col |
| self.original_content = content |
| |
| # Unique key for state tracking: "path/to/file.cc:10:5" |
| self.key = f"{filepath}:{line}:{col}" |
| |
| def __repr__(self): |
| return f"{self.filepath}:{self.line}" |
| |
| |
| class BuildEnvironment: |
| """Manages the GN/Ninja build environment.""" |
| |
| def __init__(self, builder_group: str, builder_config: str): |
| self.group = builder_group |
| self.config = builder_config |
| # Sanitize directory name to avoid spaces/parens. |
| # Allow alphanumeric, underscore, dash. Replace others with underscore. |
| safe_dirname = re.sub(r'[^a-zA-Z0-9_-]', '_', builder_config) |
| self.build_dir = os.path.join("out", safe_dirname) |
| self.is_ready = False |
| self.compile_db: dict[str, str] = {} |
| |
| def ensure_ready(self): |
| """Sets up the build directory if it doesn't exist.""" |
| if self.is_ready: |
| return |
| |
| if not os.path.exists(self.build_dir): |
| log("BUILD", f"Setting up builder: {self.config}...") |
| cmd = [ |
| "./tools/mb/mb.py", "gen", "-m", self.group, "-b", self.config, |
| self.build_dir |
| ] |
| res = run_command(cmd) |
| if res.returncode != 0: |
| log("ERR", |
| f"Failed to generate build files for {self.config}: {res.stderr}") |
| raise RuntimeError("MB gen failed") |
| |
| # Always regenerate compile_commands.json to ensure it's fresh |
| log("GN", f"Generating compile_commands.json for {self.config}...") |
| res = run_command( |
| ["gn", "gen", self.build_dir, "--export-compile-commands"]) |
| if res.returncode != 0: |
| log("ERR", f"Failed to export compile commands: {res.stderr}") |
| raise RuntimeError("GN gen failed") |
| |
| # Build everything to ensure generated files (headers, etc.) are present. |
| log("BUILD", |
| f"Building all targets in {self.config} (this may take a while)...") |
| run_command(["autoninja", "-C", self.build_dir], capture_output=False) |
| |
| self._load_compile_db() |
| self.is_ready = True |
| |
| def _load_compile_db(self): |
| """Loads the compile_commands.json database.""" |
| |
| db_path = os.path.join(self.build_dir, "compile_commands.json") |
| try: |
| with open(db_path, 'r') as f: |
| data = json.load(f) |
| |
| self.compile_db = {} |
| for entry in data: |
| directory = entry.get('directory', '.') |
| filepath = entry.get('file', '') |
| |
| if not filepath: |
| continue |
| |
| abs_path = os.path.abspath(os.path.join(directory, filepath)) |
| self.compile_db[abs_path] = entry.get('command', '') |
| |
| except (IOError, json.JSONDecodeError) as e: |
| log("WARN", f"Failed to load compile_commands.json: {e}") |
| |
| def has_file(self, filepath: str) -> bool: |
| """Checks if the file exists in the compile database.""" |
| abs_path = os.path.abspath(filepath) |
| return abs_path in self.compile_db |
| |
| def find_compilable_source(self, filepath: str) -> str | None: |
| """Returns a file that can be compiled for the given filepath.""" |
| abs_path = os.path.abspath(filepath) |
| if abs_path in self.compile_db: |
| return filepath |
| |
| # Header fallback (Source/Header proxy) |
| if filepath.endswith(('.h', '.hh', '.hpp')): |
| base = os.path.splitext(filepath)[0] |
| for ext in ['.cc', '.cpp', '.mm']: |
| surrogate = base + ext |
| abs_surrogate = os.path.abspath(surrogate) |
| if abs_surrogate in self.compile_db: |
| return surrogate |
| |
| return None |
| |
| def compile_file(self, filepath: str) -> bool: |
| """ |
| Compiles a single file using the `compile_commands.json` database. |
| """ |
| compilable_source = self.find_compilable_source(filepath) |
| if not compilable_source: |
| return False |
| |
| cmd_list = shlex.split(self.compile_db[os.path.abspath(compilable_source)]) |
| res = run_command(cmd_list, cwd=self.build_dir, capture_output=False) |
| return res.returncode == 0 |
| |
| |
| class CodeModifier: |
| """Handles parsing and modifying source files.""" |
| |
| @staticmethod |
| def find_unsafe_todos() -> Sequence[SourceLocation]: |
| """Scans the codebase for UNSAFE_TODO macros.""" |
| log("SCAN", "Scanning for UNSAFE_TODO instances...") |
| cmd = [ |
| "git", "grep", "-n", "--column", "-w", "UNSAFE_TODO", "--", "*.cc", |
| "*.h", "*.cpp", "*.mm", "*.hh" |
| ] |
| res = run_command(cmd) |
| lines = res.stdout.strip().splitlines() |
| |
| locations = [] |
| for line in lines: |
| parts = line.split(":", 3) # path:line:col:content |
| if len(parts) < 4: |
| continue |
| |
| fpath = parts[0] |
| linenum = int(parts[1]) |
| colnum = int(parts[2]) |
| |
| if is_excluded(fpath): |
| continue |
| |
| locations.append(SourceLocation(fpath, linenum, colnum, "")) |
| return locations |
| |
| @staticmethod |
| def _read_file(filepath: str) -> Sequence[str]: |
| with open(filepath, 'r') as f: |
| return f.readlines() |
| |
| @staticmethod |
| def _write_file(filepath: str, lines: Sequence[str]): |
| with open(filepath, 'w') as f: |
| f.writelines(lines) |
| |
| @staticmethod |
| def replace_macro(loc: SourceLocation): |
| """ |
| Simple string replacement on the specific line. |
| We replace UNSAFE_TODO with UNSAFE_XXXX. |
| """ |
| lines = CodeModifier._read_file(loc.filepath) |
| line_idx = loc.line - 1 |
| col_idx = loc.col - 1 |
| original_line = lines[line_idx] |
| |
| lines[line_idx] = (original_line[:col_idx] + "UNSAFE_XXXX" + |
| original_line[col_idx + len("UNSAFE_TODO"):]) |
| CodeModifier._write_file(loc.filepath, lines) |
| |
| @staticmethod |
| def _remove_wrapper_from_content(content: str, line: int, col: int) -> str: |
| """ |
| Internal logic to remove one wrapper from string content. |
| |
| Note: Relying on parenthesis matching is "good" enough here, most of the |
| time. Failures will simply skip the removal. |
| """ |
| # Find the line start offset |
| lines = content.splitlines(keepends=True) |
| offset = 0 |
| for i in range(line - 1): |
| offset += len(lines[i]) |
| offset += (col - 1) |
| |
| # Verify we are at UNSAFE_TODO |
| if not content[offset:].startswith("UNSAFE_TODO"): |
| log("WARN", f"Offset mismatch at {line}:{col}. Skipping...") |
| return content |
| |
| # Find opening paren |
| open_paren = content.find("(", offset) |
| if open_paren == -1: |
| return content |
| |
| # Find matching closing paren |
| balance = 1 |
| close_paren = -1 |
| for i in range(open_paren + 1, len(content)): |
| if content[i] == "(": |
| balance += 1 |
| elif content[i] == ")": |
| balance -= 1 |
| |
| if balance == 0: |
| close_paren = i |
| break |
| |
| if close_paren == -1: |
| return content |
| |
| # Extract inner content |
| inner = content[open_paren + 1:close_paren] |
| |
| # Reconstruct |
| new_content = content[:offset] + inner + content[close_paren + 1:] |
| return new_content |
| |
| @staticmethod |
| def remove_wrapper(loc: SourceLocation): |
| """Removes UNSAFE_TODO(...) wrapper, keeping inner content.""" |
| try: |
| with open(loc.filepath, 'r') as f: |
| content = f.read() |
| |
| new_content = CodeModifier._remove_wrapper_from_content( |
| content, loc.line, loc.col) |
| |
| if content != new_content: |
| with open(loc.filepath, 'w') as f: |
| f.write(new_content) |
| except Exception as e: |
| log("ERR", f"Failed to modify {loc}: {e}") |
| |
| @staticmethod |
| def is_libc_call(loc: SourceLocation) -> bool: |
| """Checks if the UNSAFE_TODO wraps a known unsafe libc function.""" |
| try: |
| with open(loc.filepath, 'r') as f: |
| content = f.read() |
| |
| # Find start offset based on line/col |
| lines = content.splitlines(keepends=True) |
| offset = 0 |
| for i in range(loc.line - 1): |
| offset += len(lines[i]) |
| offset += (loc.col - 1) |
| |
| if not content[offset:].startswith("UNSAFE_TODO"): |
| return False |
| |
| open_paren = content.find("(", offset) |
| if open_paren == -1: return False |
| |
| balance = 1 |
| close_paren = -1 |
| for i in range(open_paren + 1, len(content)): |
| if content[i] == "(": balance += 1 |
| elif content[i] == ")": balance -= 1 |
| if balance == 0: |
| close_paren = i |
| break |
| |
| if close_paren == -1: return False |
| |
| inner = content[open_paren + 1:close_paren].strip() |
| return bool(LIBC_REGEX.match(inner)) |
| except Exception: |
| return False |
| |
| @staticmethod |
| def remove_wrappers(locs: Sequence[SourceLocation]): |
| """Removes multiple wrappers from the same file efficiently.""" |
| if not locs: |
| return |
| |
| filepath = locs[0].filepath |
| try: |
| with open(filepath, 'r') as f: |
| content = f.read() |
| |
| # Sort in reverse order (bottom-up, right-to-left) to preserve offsets |
| # of earlier modifications. |
| sorted_locs = sorted(locs, key=lambda x: (x.line, x.col), reverse=True) |
| |
| original_content = content |
| for loc in sorted_locs: |
| content = CodeModifier._remove_wrapper_from_content( |
| content, loc.line, loc.col) |
| |
| if content != original_content: |
| with open(filepath, 'w') as f: |
| f.write(content) |
| except Exception as e: |
| log("ERR", f"Failed to batch modify {filepath}: {e}") |
| |
| @staticmethod |
| def revert_file(filepath: str): |
| """Restores file using git checkout.""" |
| run_command(["git", "checkout", filepath]) |
| |
| |
| # --- Main Analysis Loop --- |
| |
| |
| def analyze_todos(args): |
| state = StateManager(STATE_FILE) |
| todos = CodeModifier.find_unsafe_todos() |
| |
| log("INFO", f"Found {len(todos)} UNSAFE_TODO candidates.") |
| |
| # Pre-scan for LIBC functions |
| log("INFO", "Checking for known LIBC functions...") |
| libc_count = 0 |
| for todo in todos: |
| if state.is_resolved(todo.key): |
| continue |
| |
| if CodeModifier.is_libc_call(todo): |
| state.set_status(todo.key, "scanner", "LIBC") |
| libc_count += 1 |
| |
| if libc_count > 0: |
| log("INFO", f"Marked {libc_count} UNSAFE_TODOs as LIBC (non-removable).") |
| |
| # Group TODOs by file |
| todos_by_file = defaultdict(list) |
| for todo in todos: |
| if not state.is_resolved(todo.key): |
| todos_by_file[todo.filepath].append(todo) |
| |
| log("INFO", f"{len(todos_by_file)} files remain unresolved.") |
| |
| # Initialize builders |
| builders = [BuildEnvironment(g, c) for g, c in BUILDERS] |
| |
| for builder in builders: |
| try: |
| builder.ensure_ready() |
| except Exception as e: |
| log("ERR", f"Skipping builder {builder.config} due to setup failure: {e}") |
| continue |
| |
| log("BUILDER", f"Processing files on builder: {builder.config}...") |
| |
| processed_files_count = 0 |
| |
| filenames = list(todos_by_file.keys()) |
| |
| for filepath in filenames: |
| file_todos = todos_by_file[filepath] |
| |
| # 1. Check if the file is already resolved |
| unresolved_todos = [t for t in file_todos if not state.is_resolved(t.key)] |
| if not unresolved_todos: |
| continue |
| |
| # 2. Check if this builder has already run for these TODOs |
| todos_to_check = [ |
| t for t in unresolved_todos |
| if not state.get_builder_status(t.key, builder.config) |
| ] |
| if not todos_to_check: |
| continue |
| |
| processed_files_count += 1 |
| log(str(processed_files_count), |
| f"Processing {filepath} ({len(todos_to_check)} items)...") |
| |
| try: |
| # --- STEP 0: Skip files that can't be compiled --- |
| if not builder.find_compilable_source(filepath): |
| log("WARN", |
| f"Cannot compile {filepath} on {builder.config}. Skipping.") |
| for todo in todos_to_check: |
| state.set_status(todo.key, builder.config, "SKIP") |
| continue |
| |
| # --- STEP 1: Individual Checks --- |
| for todo in todos_to_check: |
| # Inverted Check Logic: |
| # 1. First, check if removing the wrapper compile. |
| # 2. Second, check if the line is active under this builder. |
| |
| # 1. Remove wrapper |
| CodeModifier.remove_wrapper(todo) |
| safety_pass = builder.compile_file(filepath) |
| CodeModifier.revert_file(filepath) |
| |
| if not safety_pass: |
| # Removing the wrapper broke the build. It is required. |
| log("FAIL", f"Line {todo.line}: Unsafe (wrapper required).") |
| state.set_status(todo.key, builder.config, "FAIL") |
| continue |
| |
| # 2. Rename UNSAFE_TODO -> UNSAFE_XXXX to verify usage. |
| CodeModifier.replace_macro(todo) |
| usage_pass = builder.compile_file(filepath) |
| CodeModifier.revert_file(filepath) |
| |
| if usage_pass: |
| # UNSAFE_XXXX compiled successfully -> Macro content is dead code on |
| # this builder (compiler didn't see error). We record SKIP so other |
| # builders can try to verify it. |
| state.set_status(todo.key, builder.config, "SKIP") |
| else: |
| # UNSAFE_XXXX failed to compile -> Code is live. Since safety_pass |
| # was True (removal worked), this is a confirmed Success. |
| log("OK", f"Line {todo.line}: Safe.") |
| state.set_status(todo.key, builder.config, "SUCCESS") |
| |
| except KeyboardInterrupt: |
| log("WARN", f"Interrupted. Reverting {filepath}...") |
| CodeModifier.revert_file(filepath) |
| sys.exit(1) |
| except Exception as e: |
| log("ERR", f"Unexpected error processing {filepath}: {e}") |
| CodeModifier.revert_file(filepath) |
| |
| |
| def apply_fixes(args): |
| """Reads out.json and applies changes for SUCCESS entries.""" |
| state = StateManager(STATE_FILE) |
| if not state.data: |
| log("WARN", "No state file found or file empty. Run analysis first.") |
| return |
| |
| # Group by file to minimize IO and handle multiple edits per file |
| files_to_edit = {} |
| |
| # Iterate over all keys in state |
| for key, builder_status_map in state.data.items(): |
| # Check for legacy format |
| if not isinstance(builder_status_map, dict): |
| status = builder_status_map |
| is_success = (status == "SUCCESS") |
| else: |
| # Check if ANY builder succeeded |
| is_success = "SUCCESS" in builder_status_map.values() |
| |
| if not is_success: |
| continue |
| |
| parts = key.rsplit(":", 2) |
| fpath = parts[0] |
| line = int(parts[1]) |
| col = int(parts[2]) |
| |
| if fpath not in files_to_edit: |
| files_to_edit[fpath] = [] |
| files_to_edit[fpath].append(SourceLocation(fpath, line, col, "")) |
| |
| log("INFO", f"Applying fixes to {len(files_to_edit)} files...") |
| |
| for fpath, locs in files_to_edit.items(): |
| log("EDIT", f"Updating {fpath} ({len(locs)} fixes)...") |
| CodeModifier.remove_wrappers(locs) |
| |
| # Run git cl format on modified files |
| if files_to_edit: |
| log("FMT", "Running git cl format...") |
| run_command(["git", "cl", "format"] + list(files_to_edit.keys())) |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser(description="Cleanup UNSAFE_TODO macros") |
| parser.add_argument("--apply", |
| action="store_true", |
| help="Apply fixes based on out.json") |
| args = parser.parse_args() |
| |
| if args.apply: |
| apply_fixes(args) |
| else: |
| analyze_todos(args) |
| |
| |
| if __name__ == "__main__": |
| main() |