blob: 4e6373092b4ff6a02482cc9e8008f21941591b9c [file] [log] [blame]
# Copyright 2020 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 src/components/autofill.
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'
import filecmp
import os
import re
import subprocess
def IsComponentsAutofillFile(f, name_suffix):
# The exact path can change. Only check the containing folder.
return (f.LocalPath().startswith('components/autofill/') and
f.LocalPath().endswith(name_suffix))
def AnyAffectedFileMatches(input_api, matcher):
return any(matcher(f) for f in input_api.change.AffectedTestableFiles())
def IsComponentsAutofillFileAffected(input_api, name_suffix):
return AnyAffectedFileMatches(
input_api, lambda f: IsComponentsAutofillFile(f, name_suffix))
def CheckNoAutofillClockTimeCalls(input_api, output_api):
"""Checks that no files call AutofillClock::Now()."""
pattern = input_api.re.compile(r'(AutofillClock::Now)\(\)')
files = []
for f in input_api.AffectedSourceFiles(input_api.FilterSourceFile):
if (f.LocalPath().startswith('components/autofill/') and
not f.LocalPath().endswith("PRESUBMIT.py")):
if any(pattern.search(line) for _, line in f.ChangedContents()):
files.append(f)
if len(files):
return [ output_api.PresubmitPromptWarning(
'Consider to not call AutofillClock::Now() but use ' +
'base::Time::Now(). AutofillClock will be deprecated and deleted soon.',
files) ]
return []
def CheckNoFieldTypeCasts(input_api, output_api):
"""Makes sure that raw integers aren't cast to FieldTypes."""
explanation = """
Do not cast raw integers to FieldType to prevent values that
have no corresponding enum constant or are deprecated. Use
ToSafeFieldType() instead.
Add "// nocheck" to the end of the line to suppress this error."""
errors = []
file_filter = lambda f: (
f.LocalPath().startswith('components/autofill/')
and f.LocalPath().endswith(('.h', '.cc', '.mm'))
)
# There may be a line break in the cast, so we test multiple patterns.
pattern_full = input_api.re.compile(r'_cast<\s*FieldType\b')
pattern_prefix = input_api.re.compile(r'_cast<\s*$')
pattern_postfix = input_api.re.compile(r'^\s*FieldType\b')
for f in input_api.AffectedSourceFiles(file_filter):
contents = f.ChangedContents()
# We look at each line and their successor to check if
# - the line contains the full `static_cast<FieldType>` or similar, or
# - the line ends with `static_cast<` and the next line begins with
# `FieldType` or similar.
for i in range(len(contents)):
line_num = contents[i][0]
line = contents[i][1]
next_line = contents[i+1][1] if i+1 < len(contents) else ''
if line.endswith("// nocheck"):
continue
if next_line.endswith("// nocheck"):
next_line = ''
line = line.split('//')[0]
next_line = next_line.split('//')[0]
if pattern_full.search(line) or (
pattern_prefix.search(line) and pattern_postfix.search(next_line)
):
errors.append(
output_api.PresubmitError(
f'{f.LocalPath()}:{line_num}: {explanation}'
)
)
return errors
def CheckFieldTypeSets(input_api, output_api):
"""Produces errors if the changed code contains DenseSet<FieldType> instead
of FieldType, and similarly for FieldTypeGroupSet and HtmlFieldTypeSet."""
bad_patterns = [
(
input_api.re.compile(r'\bDenseSet<FieldType>'),
'Use FieldTypeSet instead of DenseSet<FieldType>',
),
(
input_api.re.compile(r'\bDenseSet<FieldTypeGroup>'),
'Use FieldTypeGroupSet instead of DenseSet<FieldTypeGroup>',
),
(
input_api.re.compile(r'\bDenseSet<HtmlFieldType>'),
'Use HtmlFieldTypeSet instead of DenseSet<HtmlFieldType>',
),
(
input_api.re.compile(r'\bFieldTypeSet::all()'),
'Use kAllFieldTypes instead of FieldTypeSet::all()',
),
(
input_api.re.compile(r'\bHtmlFieldTypeSet::all()'),
'Use kAllHtmlFieldTypes instead of HtmlFieldTypeSet::all()',
),
]
warnings = []
file_filter = lambda f: (
f.LocalPath().startswith('components/autofill/')
and f.LocalPath().endswith(('.h', '.cc', '.mm'))
)
for file in input_api.AffectedSourceFiles(file_filter):
for line_num, line in file.ChangedContents():
if line.endswith("// nocheck"):
continue
line = line.split('//')[0]
for regex, explanation in bad_patterns:
if regex.search(line):
warnings.append(
output_api.PresubmitError(
f'{file.LocalPath()}:{line_num}: {explanation}. Add '
'"// nocheck" to the end of the line to suppress this error.'
)
)
return warnings
def CheckFeatureNames(input_api, output_api):
"""Checks that no features are enabled."""
pattern = input_api.re.compile(
r'\bBASE_FEATURE\s*\(\s*k(\w*)\s*,\s*"(\w*)"',
input_api.re.MULTILINE)
warnings = []
for f in input_api.AffectedSourceFiles(input_api.FilterSourceFile):
if IsComponentsAutofillFile(f, 'features.cc'):
contents = input_api.ReadFile(f)
mismatches = [(constant, feature)
for (constant, feature) in pattern.findall(contents)
if constant != feature]
if mismatches:
mismatch_strings = ['\t{} -- {}'.format(*m) for m in mismatches]
mismatch_string = format('\n').join(mismatch_strings)
warnings += [ output_api.PresubmitPromptWarning(
'Feature names should be identical to variable names:\n{}'
.format(mismatch_string),
[f]) ]
return warnings
def CheckWebViewExposedExperiments(input_api, output_api):
"""Checks that changes to autofill features are exposed to webview."""
_PRODUCTION_SUPPORT_FILE = ('android_webview/java/src/org/chromium/' +
'android_webview/common/ProductionSupportedFlagList.java')
warnings = []
if (IsComponentsAutofillFileAffected(input_api, 'features.cc') and
not AnyAffectedFileMatches(
input_api, lambda f: f.LocalPath() == _PRODUCTION_SUPPORT_FILE)):
warnings += [
output_api.PresubmitPromptWarning(
(
'You may need to modify {} instructions if your feature affects'
' WebView.'
).format(_PRODUCTION_SUPPORT_FILE)
)
]
return warnings
def CheckModificationOfLegacyRegexPatterns(input_api, output_api):
"""Reminds to update internal regex patterns when legacy ones are modified."""
if IsComponentsAutofillFileAffected(input_api, "legacy_regex_patterns.json"):
return [
output_api.PresubmitPromptWarning(
"You may need to modify the parsing patterns in src-internal. " +
"See go/autofill-internal-parsing-patterns for more details. " +
"Ideally, the legacy patterns should not be modified.")
]
return []
def CheckModificationOfFormAutofillUtil(input_api, output_api):
"""Reminds to keep form_autofill_util.cc and the iOS counterpart in sync."""
if (IsComponentsAutofillFileAffected(input_api, "fill.ts") !=
IsComponentsAutofillFileAffected(input_api, "form_autofill_util.cc")):
return [
output_api.PresubmitNotifyResult(
'Form extraction/label inference has a separate iOS ' +
'implementation in components/autofill/ios/form_util/resources/' +
'fill.ts. Try to keep it in sync with form_autofill_util.cc.')
]
return []
# Checks that whenever the regex transpiler is modified, the golden test files
# are updated to match the new output. This serves as a testing mechanism for
# the transpiler.
def CheckRegexTranspilerGoldenFiles(input_api, output_api):
if not IsComponentsAutofillFileAffected(input_api,
"transpile_regex_patterns.py"):
return []
relative_test_dir = input_api.os_path.join(
"components", "test", "data", "autofill", "regex-transpiler")
test_dir = input_api.os_path.join(
input_api.PresubmitLocalPath(), os.pardir, os.pardir, relative_test_dir)
# Transpiles `test_dir/file_name` into `output_file`.
def transpile(file_name, output_file):
transpiler = input_api.os_path.join(input_api.PresubmitLocalPath(),
"core", "browser", "form_parsing", "transpile_regex_patterns.py")
input_file = input_api.os_path.join(test_dir, file_name)
subprocess.run([input_api.python3_executable, transpiler,
"--input", input_file, "--output", output_file])
# Transpiles `test_name`.in and returns whether it matches `test_name`.out.
def run_test(test_name):
expected_output = input_api.os_path.join(test_dir, test_name + ".out")
with input_api.CreateTemporaryFile() as transpiled_output:
transpile(test_name + ".in", transpiled_output.name)
return filecmp.cmp(transpiled_output.name, expected_output, shallow=False)
tests = [name[:-3] for name in os.listdir(test_dir) if name.endswith(".in")]
if not all(run_test(test) for test in tests):
return [output_api.PresubmitError(
"Regex transpiler golden files don't match. "
"Regenerate the outputs at {}.".format(relative_test_dir))]
return []