# Copyright 2011 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Presubmit script for changes affecting chrome/

See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts
for more details about the presubmit API built into depot_tools.
"""


import re

INCLUDE_CPP_FILES_ONLY = (
  r'.*\.(cc|h)$',
)

INCLUDE_SOURCE_FILES_ONLY = (
  r'.*\.(c|cc|cpp|h|m|mm)$',
)

EXCLUDE = (
  # Objective C confuses everything.
  r'.*cocoa.*',
  r'.*_mac\.(cc|h)$',
  r'.*_mac_.*',
  # All the messages files do weird multiple include trickery
  r'.*_messages.*\.h$',
  # Autogenerated window resources files are off limits
  r'.*resource.h$',
  # Header trickery
  r'.*-inl\.h$',
  # Has safe printf usage that cpplint complains about
  r'safe_browsing_util\.cc$',
)

def _CheckChangeLintsClean(input_api, output_api):
    """Makes sure that the chrome/ code is cpplint clean."""
    files_to_skip = input_api.DEFAULT_FILES_TO_SKIP + EXCLUDE
    sources = lambda x: input_api.FilterSourceFile(
        x, files_to_check=INCLUDE_CPP_FILES_ONLY, files_to_skip=files_to_skip)
    return input_api.canned_checks.CheckChangeLintsClean(
        input_api, output_api, sources)


def _CheckNoContentUnitTestsInChrome(input_api, output_api):
    """Makes sure that unit_tests does not include unit tests from content/."""
    problems = []
    for f in input_api.AffectedFiles():
        if not f.LocalPath().endswith('BUILD.gn'):
            continue

        for line_num, line in f.ChangedContents():
            m = re.search(r"'(.*\/content\/.*unittest.*)'", line)
            if m:
                problems.append(m.group(1))

    if not problems:
        return []
    return [
        output_api.PresubmitPromptWarning(
            'Unit tests located in content/ should be added to the ' +
            'content_unittests target.',
            items=problems)
    ]


def _CheckNoIsAppleBuildFlagsInChromeFile(input_api, f):
    """Check for IS_APPLE in a given file in chrome/."""
    preprocessor_statement = input_api.re.compile(r'^\s*#')
    apple_buildflag = input_api.re.compile(r'BUILDFLAG\(IS_APPLE\)')
    results = []
    for lnum, line in f.ChangedContents():
        if preprocessor_statement.search(line) and apple_buildflag.search(
                line):
            results.append('    %s:%d' % (f.LocalPath(), lnum))

    return results


def _CheckNoIsAppleBuildFlagsInChrome(input_api, output_api):
    """Check for IS_APPLE which isn't used in chrome/."""
    apple_buildflags = []

    def SourceFilter(affected_file):
        return input_api.FilterSourceFile(affected_file,
                                          INCLUDE_SOURCE_FILES_ONLY,
                                          input_api.DEFAULT_FILES_TO_SKIP)

    for f in input_api.AffectedSourceFiles(SourceFilter):
        apple_buildflags.extend(
            _CheckNoIsAppleBuildFlagsInChromeFile(input_api, f))

    if not apple_buildflags:
        return []

    return [
        output_api.PresubmitError(
            'IS_APPLE is not used in chrome/ but found in:\n',
            apple_buildflags)
    ]


def _CheckNoIsFuchsiaBuildFlagsInChromeFile(input_api, f):
    """Check for IS_FUCHSIA in a given file in chrome/."""
    preprocessor_statement = input_api.re.compile(r'^\s*#')
    fuchsia_buildflag = input_api.re.compile(r'BUILDFLAG\(IS_FUCHSIA\)')
    results = []
    for lnum, line in f.ChangedContents():
        if preprocessor_statement.search(line) and fuchsia_buildflag.search(
                line):
            results.append('    %s:%d' % (f.LocalPath(), lnum))

    return results


def _CheckNoIsFuchsiaBuildFlagsInChrome(input_api, output_api):
    """Check for IS_FUCHSIA which isn't used in chrome/."""
    fuchsia_buildflags = []

    def SourceFilter(affected_file):
        return input_api.FilterSourceFile(affected_file,
                                          INCLUDE_SOURCE_FILES_ONLY,
                                          input_api.DEFAULT_FILES_TO_SKIP)

    for f in input_api.AffectedSourceFiles(SourceFilter):
        fuchsia_buildflags.extend(
            _CheckNoIsFuchsiaBuildFlagsInChromeFile(input_api, f))

    if not fuchsia_buildflags:
        return []

    return [
        output_api.PresubmitError(
            'IS_FUCHSIA is not used in chrome/ but found in:\n',
            fuchsia_buildflags)
    ]


def _CheckNoIsIOSBuildFlagsInChromeFile(input_api, f):
    """Check for IS_IOS in a given file in chrome/."""
    preprocessor_statement = input_api.re.compile(r'^\s*#')
    ios_buildflag = input_api.re.compile(r'BUILDFLAG\(IS_IOS\)')
    results = []
    for lnum, line in f.ChangedContents():
        if preprocessor_statement.search(line) and ios_buildflag.search(line):
            results.append('    %s:%d' % (f.LocalPath(), lnum))

    return results


def _CheckNoIsIOSBuildFlagsInChrome(input_api, output_api):
    """Check for IS_IOS which isn't used in chrome/."""
    ios_buildflags = []

    def SourceFilter(affected_file):
        return input_api.FilterSourceFile(affected_file,
                                          INCLUDE_SOURCE_FILES_ONLY,
                                          input_api.DEFAULT_FILES_TO_SKIP)

    for f in input_api.AffectedSourceFiles(SourceFilter):
        ios_buildflags.extend(_CheckNoIsIOSBuildFlagsInChromeFile(
            input_api, f))

    if not ios_buildflags:
        return []

    return [
        output_api.PresubmitError(
            'IS_IOS is not used in chrome/ but found in:\n', ios_buildflags)
    ]


def _CheckBreakingInstallerVersionBumpNeeded(input_api, output_api):
    files = []
    breaking_version_installer_updated = False

    def _FilterFile(affected_file):
        return input_api.FilterSourceFile(
            affected_file,
            files_to_check=input_api.DEFAULT_FILES_TO_CHECK +
            (r'.*\.release', ))

    for f in input_api.AffectedSourceFiles(_FilterFile):
        # Normalize the local path to Linux-style path separators so that the
        # path comparisons work on Windows as well.
        local_path = f.LocalPath().replace('\\', '/')
        breaking_version_installer_updated |= (
            local_path ==
            'chrome/installer/setup/last_breaking_installer_version.cc')
        if (local_path == 'chrome/installer/mini_installer/chrome.release'
                or local_path.startswith('chrome/test/mini_installer')):
            files.append(local_path)

    if files and not breaking_version_installer_updated:
        return [
            output_api.PresubmitPromptWarning('''
Update chrome/installer/setup/last_breaking_installer_version.cc if the changes
found in the following files might break make downgrades not possible beyond
this browser's version.''',
                                              items=files)
        ]

    if not files and breaking_version_installer_updated:
        return [
            output_api.PresubmitPromptWarning('''
No installer breaking changes detected but
chrome/installer/setup/last_breaking_installer_version.cc was updated. Please
update chrome/installer/PRESUBMIT.py if more files need to be watched for
breaking installer changes.''')
        ]

    return []


def _CheckNoBaseRunLoopInChrome(input_api, output_api):
    """Check for base::RunLoop usage in chrome/ (production code only)."""
    # These are files in //chrome that currently use base::RunLoop and are
    # grandfathered in. New usages should be avoided.
    allowed_files = [
        'chrome/app_shim/app_shim_main_delegate.mm',
        'chrome/browser/app_controller_mac.mm',
        'chrome/browser/apps/app_service/app_registry_cache_waiter.h',
        'chrome/browser/apps/app_service/browser_app_launcher.cc',
        'chrome/browser/ash/accessibility/speech_monitor.cc',
        'chrome/browser/ash/arc/session/arc_session_manager.cc',
        'chrome/browser/ash/system_web_apps/system_web_app_manager.cc',
        'chrome/browser/browser_process_impl.cc',
        'chrome/browser/browser_switcher/ieem_sitelist_parser_fuzzer.cc',
        'chrome/browser/btm/btm_browser_signin_detector_factory.h',
        'chrome/browser/chrome_browser_main.cc',
        'chrome/browser/chrome_browser_main.h',
        'chrome/browser/enterprise/connectors/device_trust/key_management/installer/management_service/rotate_util.cc',
        'chrome/browser/extensions/blocked_action_waiter.h',
        'chrome/browser/extensions/load_error_waiter.h',
        'chrome/browser/extensions/startup_helper.cc',
        'chrome/browser/first_run/first_run.cc',
        'chrome/browser/first_run/first_run_internal_linux.cc',
        'chrome/browser/lifetime/browser_shutdown.cc',
        'chrome/browser/notifications/stub_notification_display_service.cc',
        'chrome/browser/performance_manager/persistence/site_data/site_data_cache_facade.cc',
        'chrome/browser/policy/chrome_browser_cloud_management_register_watcher.h',
        'chrome/browser/policy/extension_force_install_mixin.cc',
        'chrome/browser/printing/print_job.cc',
        'chrome/browser/printing/print_view_manager_base.cc',
        'chrome/browser/profiles/keep_alive/profile_keep_alive_waiter.h',
        'chrome/browser/profiles/profile_manager.cc',
        'chrome/browser/sessions/session_restore.cc',
        'chrome/browser/sessions/tab_restore_service_load_waiter.h',
        'chrome/browser/ssl/ssl_client_auth_requestor_mock.h',
        'chrome/browser/sync_file_system/local/canned_syncable_file_system.cc',
        'chrome/browser/ui/ash/login/login_display_host.h',
        'chrome/browser/ui/cocoa/first_run_dialog_cocoa.mm',
        'chrome/browser/ui/cocoa/share_menu_controller.mm',
        'chrome/browser/ui/global_error/global_error_waiter.h',
        'chrome/browser/ui/tabs/saved_tab_groups/tab_group_sync_service_initialized_observer.h',
        'chrome/browser/ui/views/accessibility/uia_accessibility_event_waiter.cc',
        'chrome/browser/ui/views/accessibility/uia_accessibility_event_waiter.h',
        'chrome/browser/ui/views/chrome_browser_main_extra_parts_views.cc',
        'chrome/browser/ui/views/first_run_dialog.cc',
        'chrome/browser/ui/views/message_box_dialog.cc',
        'chrome/browser/ui/views/process_singleton_dialog_linux.cc',
        'chrome/browser/ui/views/uninstall_view.cc',
        'chrome/browser/web_applications/os_integration/mac/app_shim_launch.mm',
        'chrome/browser/web_applications/os_integration/mac/web_app_shortcut_copier_mac.mm',
        'chrome/browser/web_applications/web_app_command_manager.cc',
        'chrome/browser/web_applications/web_app_command_manager.h',
        'chrome/browser/web_applications/web_app_provider.cc',
        'chrome/browser/webauthn/enclave_keys_waiter.h',
        'chrome/credential_provider/extension/service.cc',
        'chrome/enterprise_companion/app/app.cc',
        'chrome/tools/service_discovery_sniffer/service_discovery_sniffer.cc',
        'chrome/updater/app/app.cc',
        'chrome/updater/app/app_server.cc',
    ]

    test_patterns = [
        r'.*test.*',
        r'.*fake.*',
        r'.*mock.*',
        r'.*fuzz.*',
    ]
    test_re = input_api.re.compile('|'.join(test_patterns))

    def SourceFilter(affected_file):
        return input_api.FilterSourceFile(affected_file,
                                          INCLUDE_SOURCE_FILES_ONLY,
                                          input_api.DEFAULT_FILES_TO_SKIP)

    run_loop_re = input_api.re.compile(r'\bbase::RunLoop\b')
    problems = []
    for f in input_api.AffectedSourceFiles(SourceFilter):
        local_path = f.LocalPath().replace('\\', '/')
        if local_path in allowed_files or test_re.match(local_path):
            continue

        for line_num, line in f.ChangedContents():
            if run_loop_re.search(line):
                problems.append('    %s:%d' % (local_path, line_num))

    if not problems:
        return []

    return [
        output_api.PresubmitError(
            'base::RunLoop is disallowed in production code in //chrome. '
            'Please email chrome-directory-owners@chromium.org if you '
            'believe a RunLoop is necessary. It could be necessary for '
            'integration with OS APIs.',
            items=problems)
    ]


def _CommonChecks(input_api, output_api):
    """Checks common to both upload and commit."""
    results = []
    results.extend(_CheckNoContentUnitTestsInChrome(input_api, output_api))
    results.extend(_CheckNoIsAppleBuildFlagsInChrome(input_api, output_api))
    results.extend(_CheckNoIsFuchsiaBuildFlagsInChrome(input_api, output_api))
    results.extend(_CheckNoIsIOSBuildFlagsInChrome(input_api, output_api))
    results.extend(
        _CheckBreakingInstallerVersionBumpNeeded(input_api, output_api))
    results.extend(_CheckNoBaseRunLoopInChrome(input_api, output_api))
    return results


def CheckChangeOnUpload(input_api, output_api):
    results = []
    results.extend(_CommonChecks(input_api, output_api))
    results.extend(_CheckChangeLintsClean(input_api, output_api))
    return results


def CheckChangeOnCommit(input_api, output_api):
    results = []
    results.extend(_CommonChecks(input_api, output_api))
    return results
