chromium/chrome/browser/PRESUBMIT.py

# Copyright 2014 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 Chromium browser code."""


import re

# Checks whether an autofill-related browsertest fixture class inherits from
# either InProcessBrowserTest or AndroidBrowserTest without having a member of
# type `autofill::test::AutofillBrowserTestEnvironment`. In that case, the
# functions registers a presubmit warning.
def _CheckNoAutofillBrowserTestsWithoutAutofillBrowserTestEnvironment(
        input_api, output_api):
  autofill_files_pattern = re.compile(
      r'(autofill|password_manager).*\.(mm|cc|h)')
  concerned_files = [(f, input_api.ReadFile(f))
                     for f in input_api.AffectedFiles(include_deletes=False)
                     if autofill_files_pattern.search(f.LocalPath())]

  warning_files = []
  class_name = r'^( *)(class|struct)\s+\w+\s*:\s*'
  target_base = r'[^\{]*\bpublic\s+(InProcess|Android)BrowserTest[^\{]*\{'
  class_declaration_pattern = re.compile(
      class_name + target_base, re.MULTILINE)
  for autofill_file, file_content in concerned_files:
    for class_match in re.finditer(class_declaration_pattern, file_content):
      indentation = class_match.group(1)
      class_end_pattern = re.compile(
          r'^' + indentation + r'\};$', re.MULTILINE)
      class_end = class_end_pattern.search(file_content[class_match.start():])

      corresponding_subclass = (
          '' if class_end is None else
          file_content[
              class_match.start():
              class_match.start() + class_end.end()])

      required_member_pattern = re.compile(
          r'^' + indentation +
          r'  (::)?(autofill::)?test::AutofillBrowserTestEnvironment\s+\w+_;',
          re.MULTILINE)
      if not required_member_pattern.search(corresponding_subclass):
        warning_files.append(autofill_file)

  return [output_api.PresubmitPromptWarning(
      'Consider adding a member autofill::test::AutofillBrowserTestEnvironment '
      'to the test fixtures that derive from InProcessBrowserTest or '
      'AndroidBrowserTest in order to disable kAutofillServerCommunication in '
      'browser tests.',
      warning_files)] if len(warning_files) else []

def _RunHistogramChecks(input_api, output_api, histogram_name):
  try:
    # Setup sys.path so that we can call histograms code.
    import sys
    original_sys_path = sys.path
    sys.path = sys.path + [input_api.os_path.join(
        input_api.change.RepositoryRoot(),
        'tools', 'metrics', 'histograms')]

    results = []

    import presubmit_bad_message_reasons
    results.extend(presubmit_bad_message_reasons.PrecheckBadMessage(input_api,
        output_api, histogram_name))

    return results
  except:
    return [output_api.PresubmitError('Could not verify histogram!')]
  finally:
    sys.path = original_sys_path


def _CheckUnwantedDependencies(input_api, output_api):
  problems = []
  for f in input_api.AffectedFiles():
    if not f.LocalPath().endswith('DEPS'):
      continue

    for line_num, line in f.ChangedContents():
      if not line.strip().startswith('#'):
        m = re.search(r".*\/blink\/public\/web.*", line)
        if m:
          problems.append(m.group(0))

  if not problems:
    return []
  return [output_api.PresubmitPromptWarning(
      'chrome/browser cannot depend on blink/public/web interfaces. ' +
      'Use blink/public/common instead.',
      items=problems)]


def _CheckNoInteractiveUiTestLibInNonInteractiveUiTest(input_api, output_api):
  """Makes sure that ui_controls related API are used only in
  interactive_in_tests.
  """
  problems = []
  # There are interactive tests whose name ends with `_browsertest.cc`
  # or `_browser_test.cc`.
  files_to_skip = ((r'.*interactive_.*test\.cc',) +
                   input_api.DEFAULT_FILES_TO_SKIP)
  def FileFilter(affected_file):
    """Check non interactive_uitests only."""
    return input_api.FilterSourceFile(
        affected_file,
        files_to_check=(
            r'.*browsertest\.cc',
            r'.*unittest\.cc'),
        files_to_skip=files_to_skip)

  ui_controls_includes =(
    input_api.re.compile(
        r'#include.*/(ui_controls.*h|interactive_test_utils.h)"'))

  for f in input_api.AffectedFiles(include_deletes=False,
                                   file_filter=FileFilter):
    for line_num, line in f.ChangedContents():
      m = re.search(ui_controls_includes, line)
      if m:
        problems.append('  %s:%d:%s' % (f.LocalPath(), line_num, m.group(0)))

  if not problems:
    return []

  WARNING_MSG ="""
  ui_controls API can be used only in interactive_ui_tests.
  If the test is in the interactive_ui_tests, please consider renaming
  to xxx_interactive_uitest.cc"""
  return [output_api.PresubmitPromptWarning(WARNING_MSG, items=problems)]


def _CheckForUselessExterns(input_api, output_api):
  """Makes sure developers don't copy "extern const char kFoo[]" from
  foo.h to foo.cc.
  """
  problems = []
  BAD_PATTERN = input_api.re.compile(r'^extern const')

  def FileFilter(affected_file):
    """Check only a particular list of files"""
    return input_api.FilterSourceFile(
        affected_file,
        files_to_check=[r'chrome[/\\]browser[/\\]flag_descriptions\.cc']);

  for f in input_api.AffectedFiles(include_deletes=False,
                                   file_filter=FileFilter):
    for _, line in f.ChangedContents():
      if BAD_PATTERN.search(line):
        problems.append(f)

  if not problems:
    return []

  WARNING_MSG ="""Do not write "extern const char" in these .cc files:"""
  return [output_api.PresubmitPromptWarning(WARNING_MSG, items=problems)]


def _CheckBuildFilesForIndirectAshSources(input_api, output_api):
  """Warn when indirect paths are added to an ash target's "sources".

  Indirect paths are paths containing a slash, e.g. "foo/bar.h" or "../foo.cc".
  """

  MSG = ("It appears that sources were added to the above BUILD.gn file but "
         "their paths contain a slash, indicating that the files are from a "
         "different directory (e.g. a subdirectory). As a general rule, Ash "
         "sources should live in the same directory as the BUILD.gn file "
         "listing them. There may be cases where this is not feasible or "
         "doesn't make sense, hence this is only a warning. If in doubt, "
         "please contact [email protected].")

  os_path = input_api.os_path

  # Any BUILD.gn in or under one of these directories will be checked.
  monitored_dirs = [
      os_path.join("chrome", "browser", "ash"),
      os_path.join("chrome", "browser", "chromeos"),
      os_path.join("chrome", "browser", "ui", "ash"),
      os_path.join("chrome", "browser", "ui", "chromeos"),
      os_path.join("chrome", "browser", "ui", "webui", "ash"),
  ]
  def should_check_path(affected_path):
    if os_path.basename(affected_path) != 'BUILD.gn':
      return False
    ad = os_path.dirname(affected_path)
    for md in monitored_dirs:
      if os_path.commonpath([ad, md]) == md:
        return True
    return False

  # Simplifying assumption: 'sources' keyword always appears at the beginning of
  # a line (optionally preceded by whitespace).
  sep = r'(?m:\s*#.*$)*\s*'  # whitespace and/or comments, possibly empty
  sources_re = re.compile(
      fr'(?m:^\s*sources{sep}\+?={sep}\[((?:{sep}"[^"]*"{sep},?{sep})*)\])')
  source_re = re.compile(fr'{sep}"([^"]*)"')

  def find_indirect_sources(contents):
    result = []
    for sources_m in sources_re.finditer(contents):
      for source_m in source_re.finditer(sources_m.group(1)):
        source = source_m.group(1)
        if '/' in source:
          result.append(source)
    return result

  results = []
  for f in input_api.AffectedTestableFiles():
    if not should_check_path(f.LocalPath()):
      continue

    indirect_sources_new = find_indirect_sources('\n'.join(f.NewContents()))
    if not indirect_sources_new:
      continue

    indirect_sources_old = find_indirect_sources('\n'.join(f.OldContents()))
    added_indirect_sources = (
        set(indirect_sources_new) - set(indirect_sources_old))

    if added_indirect_sources:
      results.append(output_api.PresubmitPromptWarning(
          "Indirect sources detected.",
          [f.LocalPath()],
          f"{MSG}\n  " + "\n  ".join(sorted(added_indirect_sources))))
  return results


def _CommonChecks(input_api, output_api):
  """Checks common to both upload and commit."""
  results = []
  results.extend(
    _CheckNoAutofillBrowserTestsWithoutAutofillBrowserTestEnvironment(
        input_api, output_api))
  results.extend(_CheckUnwantedDependencies(input_api, output_api))
  results.extend(_RunHistogramChecks(input_api, output_api,
                 "BadMessageReasonChrome"))
  results.extend(_CheckNoInteractiveUiTestLibInNonInteractiveUiTest(
      input_api, output_api))
  results.extend(_CheckForUselessExterns(input_api, output_api))
  results.extend(_CheckBuildFilesForIndirectAshSources(input_api, output_api))
  return results

def CheckChangeOnUpload(input_api, output_api):
  return _CommonChecks(input_api, output_api)

def CheckChangeOnCommit(input_api, output_api):
  return _CommonChecks(input_api, output_api)