| #!/usr/bin/env python3 |
| # Copyright 2025 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import argparse |
| import logging |
| import pathlib |
| import re |
| import subprocess |
| |
| _SRC_ROOT = pathlib.Path(__file__).parents[3] |
| |
| _GOOGLE_JAVA_FORMAT = (_SRC_ROOT / 'third_party' / 'google-java-format' / |
| 'google-java-format') |
| |
| _IMPORTS = """import org.chromium.build.annotations.NullMarked; |
| import org.chromium.build.annotations.NullMarked; |
| import org.chromium.build.annotations.Nullable; |
| import org.chromium.build.annotations.MonotonicNonNull; |
| import org.chromium.build.annotations.Contract; |
| import org.chromium.build.annotations.EnsuresNonNull; |
| import org.chromium.build.annotations.EnsuresNonNullIf; |
| import org.chromium.build.annotations.Initializer; |
| import org.chromium.build.annotations.RequiresNonNull; |
| import static org.chromium.build.NullUtil.assumeNonNull; |
| import static org.chromium.build.NullUtil.assertNonNull; |
| """ |
| |
| _MODIFIER_KEYWORDS = (r'(?:(?:' + '|'.join([ |
| 'abstract', |
| 'default', |
| 'final', |
| 'native', |
| 'private', |
| 'protected', |
| 'public', |
| 'static', |
| 'synchronized', |
| r'/\*\s*package\s*\*/', |
| ]) + r')\s+)*') |
| |
| _NULLABLE_RE = re.compile(r'(\n *)@Nullable' |
| r'(' |
| r'(?:\s*@\w+(?:\(.*?\))?)*' |
| r'\s+(?:' + _MODIFIER_KEYWORDS + r')?' + |
| r'(?:<.*?>)?' |
| r')') |
| _CLASSES_REGEX = re.compile( |
| r'(^(?:public|protected|private|/\*\s*package\s*\*/)?\s*' |
| r'(?:(?:static|abstract|final|sealed)\s+)*' |
| r'(?:class|@?interface|enum)\s+\w+)', |
| flags=re.MULTILINE) |
| |
| |
| def _mark_file(path): |
| data = path.read_text() |
| if '@NullMarked' in data: |
| logging.warning('Skipping %s. Already has @NullMarked', path) |
| return False |
| if '@NullUnmarked' in data: |
| logging.warning('Skipping %s. Already has @NullUnmarked', path) |
| return False |
| |
| data = data.replace('import androidx.annotation.Nullable;\n', '') |
| # Move @Nullable before methods to right before return type. |
| data = _NULLABLE_RE.sub(r'\1\2 @Nullable ', data) |
| # Fix up type-use position. |
| data = re.sub(r'@Nullable\s+((?:\w+\.)+)(\w+)', r'\1@Nullable \2', data) |
| data = re.sub(r'@Nullable\s+([\w<>]+)\[\]', r'\1 @Nullable[]', data) |
| |
| # Remove @NonNull |
| data = data.replace('@NonNull', '') |
| # Add imports |
| data = re.sub(r'(^package .*\n)', |
| r'\1' + _IMPORTS, |
| data, |
| flags=re.MULTILINE, |
| count=1) |
| |
| # Add @NullMarked |
| data = _CLASSES_REGEX.sub(r'@NullMarked\n\1', data, count=1) |
| |
| # Make all Void's @Nullable |
| if re.search(r'\bVoid\b', data): |
| data = re.sub(r'\bVoid\b', '@Nullable Void', data) |
| data = data.replace('@Nullable @Nullable Void', '@Nullable Void') |
| |
| # Make all Supplier<Tab> -> Supplier<@Nullable Tab> |
| if 'Supplier<Tab>' in data: |
| data = data.replace('Supplier<Tab>', 'Supplier<@Nullable Tab>') |
| |
| logging.info('Processed: %s', path) |
| path.write_text(data) |
| return True |
| |
| |
| def main(): |
| logging.basicConfig(format='%(message)s', level=logging.INFO) |
| |
| parser = argparse.ArgumentParser() |
| parser.add_argument('files', nargs='+') |
| args = parser.parse_args() |
| |
| changed_paths = [] |
| for f in args.files: |
| if _mark_file(pathlib.Path(f)): |
| changed_paths.append(f) |
| |
| if changed_paths: |
| cmd = [ |
| str(_GOOGLE_JAVA_FORMAT), '--aosp', '--skip-javadoc-formatting', |
| '--skip-removing-unused-imports', '--replace' |
| ] + changed_paths |
| logging.info('Running: %s', ' '.join(cmd)) |
| subprocess.check_call(cmd) |
| print(f'Added @NullMarked to {len(changed_paths)} path(s).') |
| |
| |
| if __name__ == '__main__': |
| main() |