# 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.
"""Top-level presubmit script for build.

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

PRESUBMIT_VERSION = '2.0.0'
USE_PYTHON3 = True

DISABLED_PYLINT_WARNINGS = [
    'no-init',  # Class has no __init__ method
    'super-init-not-called',  # __init__ method from base class is not called
    'cyclic-import',
    'unused-argument',
    'import-outside-toplevel',
    'inconsistent-return-statements',
    'no-member',
    'no-value-for-parameter',
    'stop-iteration-return',
    'subprocess-run-check',
    # TODO(crbug.com/441191932): Re-enable these checks.
    # ************* Module action
    # siso/subcmd/ninja/testdata/TestBuild_CopyLocalOut/tools/action.py:21:8: W0612: Unused variable 'i' (unused-variable)
    # ************* Module clang++
    # siso/subcmd/ninja/testdata/TestBuild_Cleandead/tools/clang++.py:24:12: W0622: Redefining built-in 'input' (redefined-builtin)
    # ************* Module link
    # siso/subcmd/ninja/testdata/TestBuild_EdgeRule/tools/link.py:19:6: W0622: Redefining built-in 'input' (redefined-builtin)
    # siso/subcmd/ninja/testdata/TestBuild_EdgeRule_solibs/tools/link.py:19:6: W0622: Redefining built-in 'input' (redefined-builtin)
    # siso/subcmd/ninja/testdata/TestBuild_EdgeRule_solibs_recursive/tools/link.py:19:6: W0622: Redefining built-in 'input' (redefined-builtin)
    # siso/subcmd/ninja/testdata/TestBuild_EdgeRule_stamp_solibs/tools/link.py:20:6: W0622: Redefining built-in 'input' (redefined-builtin)
    # ************* Module ar
    # siso/subcmd/ninja/testdata/TestBuild_EdgeRule/tools/ar.py:20:6: W0622: Redefining built-in 'input' (redefined-builtin)
    # siso/subcmd/ninja/testdata/TestBuild_Local_Inputs/tools/action.py:17:6: W0622: Redefining built-in 'input' (redefined-builtin)
    # ************* Module gen
    # siso/subcmd/ninja/testdata/TestBuild_RemovedArtifact/tools/gen.py:8:0: W0622: Redefining built-in 'input' (redefined-builtin)
    # ************* Module archive
    # siso/subcmd/ninja/testdata/TestBuild_OutputDir/tools/archive.py:19:8: W0622: Redefining built-in 'input' (redefined-builtin)
    # siso/subcmd/ninja/testdata/TestBuild_Restat/tools/action.py:17:6: W0622: Redefining built-in 'input' (redefined-builtin)
    # siso/subcmd/ninja/testdata/TestBuild_RestatMultiout/tools/action.py:18:6: W0622: Redefining built-in 'input' (redefined-builtin)
    # siso/subcmd/ninja/testdata/TestBuild_Restat_RestatContent/tools/action.py:17:6: W0622: Redefining built-in 'input' (redefined-builtin)
    # ************* Module action2
    # siso/subcmd/ninja/testdata/TestBuild_RestatMultiout/tools/action2.py:17:6: W0622: Redefining built-in 'input' (redefined-builtin)
    # siso/subcmd/ninja/testdata/TestBuild_offline/tools/gen.py:9:0: W0622: Redefining built-in 'input' (redefined-builtin)
    'redefined-builtin',
    # ************* Module action
    # siso/subcmd/ninja/testdata/TestBuild_CopyLocalOut/tools/action.py:21:8: W0612: Unused variable 'i' (unused-variable)
    'unused-variable',
]

THIRD_PARTY_DIRS = [
    'siso/third_party',
]


def CheckChange(input_api, output_api):
  # Default source file filter doesn't include Go.
  # e.g. CheckChangeHasNoTabs would conflict since gofmt enforces tabs.
  source_file_filter_incl_go = lambda path: input_api.FilterSourceFile(
      path,
      files_to_check=list(input_api.DEFAULT_FILES_TO_CHECK) + [
          r'.+\.go$',
      ],
      files_to_skip=[
          r'.*pb[^/]*\.go$',
      ] + [rf'{d}/.*' for d in THIRD_PARTY_DIRS])

  results = []
  results += input_api.canned_checks.CheckDoNotSubmit(input_api, output_api)
  results += input_api.canned_checks.CheckChangeHasNoTabs(
      input_api, output_api)
  results += input_api.canned_checks.CheckPatchFormatted(
      input_api, output_api, check_clang_format=False)
  results += input_api.canned_checks.CheckChangeHasNoStrayWhitespace(
      input_api, output_api, source_file_filter=source_file_filter_incl_go)
  results += input_api.canned_checks.CheckInclusiveLanguage(
      input_api, output_api)
  results += input_api.canned_checks.CheckLicense(
      input_api, output_api, source_file_filter=source_file_filter_incl_go)

  return results


def CheckGoChanges(input_api, output_api):
  file_filter = lambda path: input_api.FilterSourceFile(
      path,
      files_to_check=[r'.*\.go$'],
      files_to_skip=THIRD_PARTY_DIRS + [r'.*\.pb\.go$', r'.*\.gen\.go$'])
  affected_files = sorted(
      [
          # TODO(b/430465030): Fix this.
          # pylint: disable=unnecessary-comprehension
          f for f in input_api.AffectedFiles(
              include_deletes=False, file_filter=file_filter)
      ],
      key=lambda source: source.AbsoluteLocalPath())
  if not affected_files:
    return []

  # Fetch dependencies from CIPD.
  # This is done in this script because we don't use gclient to manage Go
  # dependencies.
  # golangci-lint should always be fetched to ensure errors are consistent
  # between local developer machines and presubmit bots.
  cipd_root = input_api.os_path.join(input_api.change.RepositoryRoot(),
                                     '.cipd_bin')
  ensure_file_content = ('infra/3pp/tools/golangci-lint/${platform} '
                         'version:3@2.9.0.prebuilt\n')
  go = 'go'
  gofmt = 'gofmt'
  golangci_lint = input_api.os_path.join(cipd_root, 'golangci-lint')
  env = input_api.environ.copy()
  if input_api.is_committing and input_api.gerrit:
    # Go is only needed on presubmit bots.
    # This is because we use go.mod to manage the expected Go version on local
    # developer machines, and expect Go to be available on $PATH.
    ensure_file_content += ('infra/3pp/tools/go/${platform} '
                            'version:3@1.26.2\n')
    go = input_api.os_path.join(cipd_root, 'bin', 'go')
    gofmt = input_api.os_path.join(cipd_root, 'bin', 'gofmt')
    env['PATH'] = input_api.os_path.join(cipd_root, 'bin') + ':' + env['PATH']
  input_api.subprocess.check_call(
      [
          'cipd',
          'ensure',
          '-log-level',
          'warning',
          '-root',
          str(cipd_root),
          '-ensure-file',
          '-',
      ],
      stdin=ensure_file_content.encode('utf-8'),
      cwd=input_api.change.RepositoryRoot(),
  )

  # Make sure Go is available on $PATH.
  try:
    input_api.subprocess.check_call([go, 'version'],
                                    stdout=input_api.subprocess.PIPE,
                                    stderr=input_api.subprocess.PIPE)
  except input_api.subprocess.CalledProcessError as e:
    return [
        output_api.PresubmitPromptOrNotify(
            f"go isn't available on your $PATH: {e}")
    ]

  # Run `go fmt` on files individually.
  bad = []
  for f in affected_files:
    try:
      stdout, _ = input_api.subprocess.check_call_out(
          [gofmt, '-s', '-d', f.LocalPath()],
          stdout=input_api.subprocess.PIPE,
          stderr=input_api.subprocess.PIPE)
      if stdout.strip():
        bad.append(f)
    except input_api.subprocess.CalledProcessError as e:
      return [output_api.PresubmitError(f'gofmt failed to run: {e}')]
  if bad:
    return [
        output_api.PresubmitError(('Found badly formatted Go file(s). '
                                   'Run `gofmt -s -w .` to fix them.'), bad)
    ]

  # Run `golangci-lint` on folders.
  dirs = {
      input_api.os_path.dirname(f.AbsoluteLocalPath()):
          input_api.os_path.dirname(f.LocalPath()) for f in affected_files
  }
  if input_api.is_committing:
    error_type = output_api.PresubmitError
  else:
    error_type = output_api.PresubmitPromptWarning

  tests = []
  for absolute, pretty in sorted(dirs.items()):
    kwargs = {'cwd': absolute}
    if env:
      kwargs['env'] = env
    tests.append(
        input_api.Command(
            name=f'Check golangci-lint on {pretty}',
            cmd=[
                golangci_lint, 'run', '--timeout=15m',
                '--allow-parallel-runners', '.'
            ],
            kwargs=kwargs,
            message=error_type))
  return input_api.RunTests(tests)


def CheckPythonChanges(input_api, output_api):
  files_to_skip = list(input_api.DEFAULT_FILES_TO_SKIP)
  files_to_skip += [rf'{d}/.*' for d in THIRD_PARTY_DIRS]
  files_to_skip += _IgnoredPaths(input_api)
  return input_api.RunTests(
      input_api.canned_checks.GetPylint(
          input_api,
          output_api,
          files_to_skip=files_to_skip,
          disabled_warnings=DISABLED_PYLINT_WARNINGS,
          version='2.7',
      ))


def _IgnoredPaths(input_api):
  # This computes the list if repository-root-relative paths which are
  # ignored by .gitignore files. There is probably a faster way to do this.
  status_output = input_api.subprocess.check_output(
      ['git', 'status', '--porcelain', '--ignored'], text=True)
  statuses = [(line[:2], line[3:]) for line in status_output.splitlines()]
  return [
      input_api.re.escape(path)
      for (mode, path) in statuses
      if mode in ('!!', '??') and not path.endswith('.pyc')
  ]
