# Copyright 2024 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Chromium presubmit script for base/allocator/partition_allocator.

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

PRESUBMIT_VERSION = '2.0.0'

# This is the base path of the partition_alloc directory when stored inside the
# chromium repository. PRESUBMIT.py is executed from chromium.
_PARTITION_ALLOC_BASE_PATH = 'base/allocator/partition_allocator/src/'

# Pattern matching C/C++ source files, for use in allowlist args.
_SOURCE_FILE_PATTERN = r'.*\.(h|hpp|c|cc|cpp)$'

# Similar pattern, matching GN files.
_BUILD_FILE_PATTERN = r'.*\.(gn|gni)$'

# This is adapted from Chromium's PRESUBMIT.py. The differences are:
# - Base path: It is relative to the partition_alloc's source directory instead
#              of chromium.
# - Stricter: A single format is allowed: `PATH_ELEM_FILE_NAME_H_`.
def CheckForIncludeGuards(input_api, output_api):
    """Check that header files have proper include guards"""

    def guard_for_file(file):
        local_path = file.LocalPath()
        if input_api.is_windows:
            local_path = local_path.replace('\\', '/')
        assert local_path.startswith(_PARTITION_ALLOC_BASE_PATH)
        guard = input_api.os_path.normpath(
            local_path[len(_PARTITION_ALLOC_BASE_PATH):])
        guard = guard + '_'
        guard = guard.upper()
        guard = input_api.re.sub(r'[+\\/.-]', '_', guard)
        return guard

    def is_partition_alloc_header_file(f):
        # We only check header files.
        return f.LocalPath().endswith('.h')

    errors = []

    for f in input_api.AffectedSourceFiles(is_partition_alloc_header_file):
        expected_guard = guard_for_file(f)

        # Unlike the Chromium's top-level PRESUBMIT.py, we enforce a stricter
        # rule which accepts only `PATH_ELEM_FILE_NAME_H_` per coding style.
        guard_name_pattern = input_api.re.escape(expected_guard)
        guard_pattern = input_api.re.compile(r'#ifndef\s+(' +
                                             guard_name_pattern + ')')

        guard_name = None
        guard_line_number = None
        seen_guard_end = False
        for line_number, line in enumerate(f.NewContents()):
            if guard_name is None:
                match = guard_pattern.match(line)
                if match:
                    guard_name = match.group(1)
                    guard_line_number = line_number
                continue

            # The line after #ifndef should have a #define of the same name.
            if line_number == guard_line_number + 1:
                expected_line = '#define %s' % guard_name
                if line != expected_line:
                    errors.append(
                        output_api.PresubmitPromptWarning(
                            'Missing "%s" for include guard' % expected_line,
                            ['%s:%d' % (f.LocalPath(), line_number + 1)],
                            'Expected: %r\nGot: %r' % (expected_line, line)))

            if not seen_guard_end and line == '#endif  // %s' % guard_name:
                seen_guard_end = True
                continue

            if seen_guard_end:
                if line.strip() != '':
                    errors.append(
                        output_api.PresubmitPromptWarning(
                            'Include guard %s not covering the whole file' %
                            (guard_name), [f.LocalPath()]))
                    break  # Nothing else to check and enough to warn once.

        if guard_name is None:
            errors.append(
                output_api.PresubmitPromptWarning(
                    'Missing include guard in %s\n'
                    'Recommended name: %s\n' %
                    (f.LocalPath(), expected_guard)))

    return errors

# In .gn and .gni files, check there are no unexpected dependencies on files
# located outside of the partition_alloc repository.
#
# This is important, because partition_alloc has no CQ bots on its own, but only
# through the chromium's CQ.
#
# Only //build_overrides/ is allowed, as it provides embedders, a way to
# overrides the default build settings and forward the dependencies to
# partition_alloc.
def CheckNoExternalImportInGn(input_api, output_api):
    # Match and capture <path> from import("<path>").
    import_re = input_api.re.compile(r'^ *import\("([^"]+)"\)')

    sources = lambda affected_file: input_api.FilterSourceFile(
        affected_file,
        files_to_skip=[],
        files_to_check=[_BUILD_FILE_PATTERN])

    errors = []
    for f in input_api.AffectedSourceFiles(sources):
        for line_number, line in f.ChangedContents():
            match = import_re.search(line)
            if not match:
                continue
            import_path = match.group(1)
            if import_path.startswith('//build_overrides/'):
                continue
            if not import_path.startswith('//'):
                continue;
            errors.append(output_api.PresubmitError(
                '%s:%d\nPartitionAlloc disallow external import: %s' %
                (f.LocalPath(), line_number + 1, import_path)))
    return errors;

# partition_alloc still supports C++17, because Skia still uses C++17.
def CheckCpp17CompatibleHeaders(input_api, output_api):
    CPP_20_HEADERS = [
        "barrier",
        "bit",
        #"compare",  Three-way comparison may be used under appropriate guards.
        "format",
        "numbers",
        "ranges",
        "semaphore",
        "source_location",
        "span",
        "stop_token",
        "syncstream",
        "version",
    ]

    CPP_23_HEADERS = [
        "expected",
        "flat_map",
        "flat_set",
        "generator",
        "mdspan",
        "print",
        "spanstream",
        "stacktrace",
        "stdatomic.h",
        "stdfloat",
    ]

    sources = lambda affected_file: input_api.FilterSourceFile(
        affected_file,
        # compiler_specific.h may use these headers in guarded ways.
        files_to_skip=[
            r'.*partition_alloc_base/augmentations/compiler_specific\.h'
        ],
        files_to_check=[_SOURCE_FILE_PATTERN])

    errors = []
    for f in input_api.AffectedSourceFiles(sources):
        # for line_number, line in f.ChangedContents():
        for line_number, line in enumerate(f.NewContents()):
            for header in CPP_20_HEADERS:
                if not "#include <%s>" % header in line:
                    continue
                errors.append(
                    output_api.PresubmitError(
                        '%s:%d\nPartitionAlloc disallows C++20 headers: <%s>'
                        % (f.LocalPath(), line_number + 1, header)))
            for header in CPP_23_HEADERS:
                if not "#include <%s>" % header in line:
                    continue
                errors.append(
                    output_api.PresubmitError(
                        '%s:%d\nPartitionAlloc disallows C++23 headers: <%s>'
                        % (f.LocalPath(), line_number + 1, header)))
    return errors

def CheckCpp17CompatibleKeywords(input_api, output_api):
    CPP_20_KEYWORDS = [
        "concept",
        "consteval",
        "constinit",
        "co_await",
        "co_return",
        "co_yield",
        "requires",
        "std::hardware_",
        "std::is_constant_evaluated",
        "std::bit_cast",
        "std::midpoint",
        "std::to_array",
    ]
    # Note: C++23 doesn't introduce new keywords.

    sources = lambda affected_file: input_api.FilterSourceFile(
        affected_file,
        # compiler_specific.h may use these keywords in guarded macros.
        files_to_skip=[r'.*partition_alloc_base/compiler_specific\.h'],
        files_to_check=[_SOURCE_FILE_PATTERN])

    errors = []
    for f in input_api.AffectedSourceFiles(sources):
        for line_number, line in f.ChangedContents():
            for keyword in CPP_20_KEYWORDS:
                if not keyword in line:
                    continue
                # Skip if part of a comment
                if '//' in line and line.index('//') < line.index(keyword):
                    continue

                # Make sure there are word separators around the keyword:
                regex = r'\b%s\b' % keyword
                if not input_api.re.search(regex, line):
                    continue

                errors.append(
                    output_api.PresubmitError(
                        '%s:%d\nPartitionAlloc disallows C++20 keywords: %s'
                        % (f.LocalPath(), line_number + 1, keyword)))
    return errors

# Check `NDEBUG` is not used inside partition_alloc. We prefer to use the
# buildflags `#if PA_BUILDFLAG(IS_DEBUG)` instead.
def CheckNoNDebug(input_api, output_api):
    sources = lambda affected_file: input_api.FilterSourceFile(
        affected_file,
        files_to_skip=[],
        files_to_check=[_SOURCE_FILE_PATTERN])

    errors = []
    for f in input_api.AffectedSourceFiles(sources):
        for line_number, line in f.ChangedContents():
            if 'NDEBUG' in line:
                errors.append(output_api.PresubmitError('%s:%d\nPartitionAlloc'
                  % (f.LocalPath(), line_number + 1)
                  + 'disallows NDEBUG, use PA_BUILDFLAG(IS_DEBUG) instead'))
    return errors
