910e62b5创建于 1月15日历史提交
#!/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()