chromium/third_party/blink/renderer/bindings/scripts/check_generated_file_list.py

# Copyright 2022 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""
Checks the file list of generated bindings defined in generated_in_*.gni
if there is no missing entry.
"""

import argparse
import itertools
import sys

import bind_gen
import web_idl

from bind_gen.path_manager import PathManager


def parse_output_reldirs(reldirs):
    required = ["core", "modules"]
    valid = required + ["extensions_chromeos", "extensions_webview"]
    result = {}
    for key_value_pair in reldirs:
        key, value = key_value_pair.split("=", 1)
        key = key.strip()
        result[key] = value

    for c in required:
        assert c in result, "missing required --output_reldir \"{0}\"".format(
            c)

    for c in result.keys():
        assert c in valid, "invalid --output_reldir \"{0}\"".format(c)

    return result


def parse_options():
    parser = argparse.ArgumentParser(
        description="Check the generated file list of Blink bindings")
    parser.add_argument("--web_idl_database",
                        required=True,
                        type=str,
                        help="filepath of the input database")
    parser.add_argument("--root_src_dir",
                        required=True,
                        type=str,
                        help="root directory of chromium project, i.e. \"//\"")
    parser.add_argument("--root_gen_dir",
                        required=True,
                        type=str,
                        help="root directory of generated code files, i.e. "
                        "\"//out/Default/gen\"")
    parser.add_argument(
        "--output_reldir",
        metavar="KEY=VALUE",
        action="append",
        help="output directory of KEY component relative to root_gen_dir.")
    parser.add_argument("--generated_file_list",
                        required=True,
                        type=str,
                        help="filepath of the generated file list")
    parser.add_argument("--output",
                        required=True,
                        type=str,
                        help="filepath of the check results")

    options = parser.parse_args()

    return options


def main():
    options = parse_options()

    output_reldirs = parse_output_reldirs(options.output_reldir)
    component_reldirs = {}
    for component, reldir in output_reldirs.items():
        component_reldirs[web_idl.Component(component)] = reldir

    bind_gen.init(web_idl_database_path=options.web_idl_database,
                  root_src_dir=options.root_src_dir,
                  root_gen_dir=options.root_gen_dir,
                  component_reldirs=component_reldirs)
    web_idl_database = bind_gen.package_initializer.package_initializer(
    ).web_idl_database()
    idl_definitions = {
        "async_iterator": [
            interface.async_iterator
            for interface in web_idl_database.interfaces
            if interface.async_iterator
        ],
        "callback_function":
        web_idl_database.callback_functions,
        "callback_interface":
        web_idl_database.callback_interfaces,
        "dictionary":
        web_idl_database.dictionaries,
        "enumeration":
        web_idl_database.enumerations,
        "interface":
        web_idl_database.interfaces,
        "namespace":
        web_idl_database.namespaces,
        "observable_array":
        web_idl_database.observable_arrays,
        "sync_iterator": [
            interface.sync_iterator
            for interface in web_idl_database.interfaces
            if interface.sync_iterator
        ],
        "union":
        web_idl_database.union_types,
    }

    error_log = []

    # Read generated_file_list into `filepaths`.
    filepaths = {}  # {kind: set([(filepath, for_testing), ...])}
    for_testing = False
    kind = None
    with open(options.generated_file_list) as input:
        for token in itertools.chain.from_iterable(line.split()
                                                   for line in input):
            if token == "--for_prod":
                for_testing = False
                continue
            if token == "--for_testing":
                for_testing = True
                continue
            if token == "--kind":
                kind = None
                continue
            if token.startswith("--"):
                raise KeyError("Unknown keyword: {}".format(token))
            if kind is None:
                kind = token
                filepaths.setdefault(kind, set())
                continue
            filepaths[kind].add((token, for_testing))

    def check_if_listed(file_set, path, for_testing, component, kind):
        try:
            file_set.remove((path, for_testing))
        except KeyError:
            error_log.append(
                "\"{path}\" is generated but not listed in the file list of "
                "\"{kind}\" in generated_in_{component}.gni.\n".format(
                    path=path, component=component, kind=kind))

    # Check whether all generated files are listed appropriately.
    for kind, file_set in filepaths.items():
        for idl_definition in idl_definitions.get(kind, []):
            if kind == "callback_function" and idl_definition.identifier in (
                    "OnErrorEventHandlerNonNull",
                    "OnBeforeUnloadEventHandlerNonNull"):
                # OnErrorEventHandlerNonNull and
                # OnBeforeUnloadEventHandlerNonNull are unified into
                # EventHandlerNonNull, and they won't be used.
                continue

            path_manager = PathManager(idl_definition)
            for_testing = idl_definition.code_generator_info.for_testing
            check_if_listed(file_set, path_manager.api_path(ext="cc"),
                            for_testing, path_manager.api_component, kind)
            check_if_listed(file_set, path_manager.api_path(ext="h"),
                            for_testing, path_manager.api_component, kind)
            if path_manager.is_cross_components:
                check_if_listed(file_set, path_manager.impl_path(ext="cc"),
                                for_testing, path_manager.impl_component, kind)
                check_if_listed(file_set, path_manager.impl_path(ext="h"),
                                for_testing, path_manager.impl_component, kind)
        for path, _ in file_set:
            error_log.append(
                "\"{path}\" is listed in the file list of \"{kind}\", but "
                "the file is not generated as {kind}.\n".format(path=path,
                                                                kind=kind))

    with open(options.output, mode="w") as output:
        for message in error_log:
            output.write(message)

    if error_log:
        sys.stderr.write(
            "Error: {} errors were detected in file listing of the generated "
            "Blink-V8 bindings files.\n\n".format(len(error_log)))
        for message in error_log:
            sys.stderr.write(message)
        sys.stderr.write("\n")
        sys.exit(1)

    if sys.stdout.isatty():
        sys.stdout.write("No error was detected.\n")


if __name__ == "__main__":
    main()