# 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.
'''
Checks that each RuntimeFeature gated mojo method used for glic has a
corresponding '// MOJO_RUNTIME_FEATURE_GATED' comment where that method
is excluded from the public API.

Calling RuntimeFeature gated methods when the feature is not enabled
will result in a closed mojo pipe, putting Glic in an unusable state.
This check helps reduce the risk for accidentally calling such methods.
'''

import glob
import os
import re
import sys


def _GetDirAbove(dirname: str):
    """Returns the directory "above" this file containing |dirname| (which must
    also be "above" this file)."""
    path = os.path.abspath(__file__)
    while True:
        path, tail = os.path.split(path)
        if not tail:
            return None
        if tail == dirname:
            return path


def _RemoveComments(text: str):
    # Remove multi-line comments first, then single-line comments.
    text = re.sub(r'/\*.*?\*/', '', text, flags=re.DOTALL)
    text = re.sub(r'//.*', '', text)
    return text


def _FindScopeBoundaries(scope_type: str, scope_name: str, mojom_text: str):
    m = re.search(f'\\s*{scope_type}\\s+{scope_name}\\s*\\{{', mojom_text)
    if not m:
        return None
    start_index = m.end()
    brace_counter = 1
    end_index = -1
    for i in range(start_index, len(mojom_text)):
        if mojom_text[i] == '{':
            brace_counter += 1
        elif mojom_text[i] == '}':
            brace_counter -= 1
        if brace_counter == 0:
            end_index = i
            break
    if end_index == -1:
        return None

    return start_index, end_index


def _ExcludeTextWithin(scope_type: str, scope_name: str, mojom_text: str):
    boundaries = _FindScopeBoundaries(scope_type, scope_name, mojom_text)
    if boundaries:
        return mojom_text[: boundaries[0]] + mojom_text[boundaries[1] :]
    return ''


SRC_ROOT = _GetDirAbove('chrome')


def _Main():
    error = False
    # Find methods marked as checked in *_client.ts files.
    client_files_pattern = os.path.join(
        SRC_ROOT, 'chrome/browser/resources/glic/glic_api_impl/*/*_client.ts'
    )
    client_files = glob.glob(client_files_pattern)

    checked_methods = set()
    for client_file in client_files:
        with open(client_file, 'r') as f:
            client_src = f.read()
        checked_methods.update(
            re.findall(r'//\s*MOJO_RUNTIME_FEATURE_GATED\s+(\S*)', client_src)
        )

    # Find methods gated with RuntimeFeature annotations in glic.mojom.
    mojo_file_path = 'chrome/browser/glic/host/glic.mojom'
    with open(os.path.join(SRC_ROOT, mojo_file_path), 'r') as f:
        mojom_text = f.read()
    mojom_text = _RemoveComments(mojom_text)

    for excluded_interface in ['WebClient']:
        new_mojom_text = _ExcludeTextWithin(
            'interface', excluded_interface, mojom_text
        )
        if new_mojom_text:
            mojom_text = new_mojom_text
        else:
            print(
                'Error: Could not find excluded interface '
                f'"{excluded_interface}" in {mojo_file_path}.'
            )
            error = True
    if error:
        sys.exit(1)

    gated_methods = set(
        m.group(1)
        for m in re.finditer(
            r'^\s*\[[^\]]*RuntimeFeature\s*=[^\]]*]\s*(\S+)\s*\(',
            mojom_text,
            flags=re.M | re.DOTALL,
        )
    )

    # Match checked and gated methods against each other.
    unchecked = gated_methods - checked_methods
    ungated = checked_methods - gated_methods
    for method in unchecked:
        decl = f'// MOJO_RUNTIME_FEATURE_GATED {method}'
        print(
            'Error: missing feature gating code for feature'
            f' gated Mojo method {method} from {mojo_file_path}.'
            ' Please update the appropriate client file (e.g. '
            'glic_api_client.ts or actor_client.ts) with the line:\n'
            f'  {decl}'
        )
        error = True
    for method in ungated:
        decl = f'// MOJO_RUNTIME_FEATURE_GATED {method}'
        print(
            f'Error: found "{decl}" in one of the client files,'
            ' but this method was not found or is not gated by'
            f' a RuntimeFeature in {mojo_file_path}'
        )
        error = True

    if error:
        sys.exit(1)


if __name__ == '__main__':
    _Main()
