0

Check deprecation of preferences

It's a common mistake that developers just remove Register...Pref()
calls for preferences they don't need anymore. If they do this, the
preference stays in the prefs files on disk for ever. A proper approach
is to first ClearPref() the preference for some releases and then to
remove the code.

This CL introduces a presubmit warning if we detect that a
Register...Pref() call is removed from a non-unittest.

Bug: 1153014
Change-Id: If8f19933de039553ef47e0ddce12eb194df45ce1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2560205
Commit-Queue: Dominic Battré <battre@chromium.org>
Reviewed-by: Jochen Eisinger <jochen@chromium.org>
Reviewed-by: Gabriel Charette <gab@chromium.org>
Reviewed-by: Dominic Battré <battre@chromium.org>
Cr-Commit-Position: refs/heads/master@{#833730}
This commit is contained in:
Dominic Battre
2020-12-04 16:14:10 +00:00
committed by Chromium LUCI CQ
parent 2d3a3d1cbc
commit 645d4234a6
4 changed files with 285 additions and 5 deletions

@ -5214,3 +5214,115 @@ def CheckStableMojomChanges(input_api, output_api):
'in a way that is not backward-compatible.',
long_text=error)]
return []
def CheckDeprecationOfPreferences(input_api, output_api):
"""Removing a preference should come with a deprecation."""
def FilterFile(affected_file):
"""Accept only .cc files and the like."""
file_inclusion_pattern = [r'.+%s' % _IMPLEMENTATION_EXTENSIONS]
files_to_skip = (_EXCLUDED_PATHS +
_TEST_CODE_EXCLUDED_PATHS +
input_api.DEFAULT_FILES_TO_SKIP)
return input_api.FilterSourceFile(
affected_file,
files_to_check=file_inclusion_pattern,
files_to_skip=files_to_skip)
def ModifiedLines(affected_file):
"""Returns a list of tuples (line number, line text) of added and removed
lines.
Deleted lines share the same line number as the previous line.
This relies on the scm diff output describing each changed code section
with a line of the form
^@@ <old line num>,<old size> <new line num>,<new size> @@$
"""
line_num = 0
modified_lines = []
for line in affected_file.GenerateScmDiff().splitlines():
# Extract <new line num> of the patch fragment (see format above).
m = input_api.re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
if m:
line_num = int(m.groups(1)[0])
continue
if ((line.startswith('+') and not line.startswith('++')) or
(line.startswith('-') and not line.startswith('--'))):
modified_lines.append((line_num, line))
if not line.startswith('-'):
line_num += 1
return modified_lines
def FindLineWith(lines, needle):
"""Returns the line number (i.e. index + 1) in `lines` containing `needle`.
If 0 or >1 lines contain `needle`, -1 is returned.
"""
matching_line_numbers = [
# + 1 for 1-based counting of line numbers.
i + 1 for i, line
in enumerate(lines)
if needle in line]
return matching_line_numbers[0] if len(matching_line_numbers) == 1 else -1
def ModifiedPrefMigration(affected_file):
"""Returns whether the MigrateObsolete.*Pref functions were modified."""
# Determine first and last lines of MigrateObsolete.*Pref functions.
new_contents = affected_file.NewContents();
range_1 = (
FindLineWith(new_contents, 'BEGIN_MIGRATE_OBSOLETE_LOCAL_STATE_PREFS'),
FindLineWith(new_contents, 'END_MIGRATE_OBSOLETE_LOCAL_STATE_PREFS'))
range_2 = (
FindLineWith(new_contents, 'BEGIN_MIGRATE_OBSOLETE_PROFILE_PREFS'),
FindLineWith(new_contents, 'END_MIGRATE_OBSOLETE_PROFILE_PREFS'))
if (-1 in range_1 + range_2):
raise Exception(
'Broken .*MIGRATE_OBSOLETE_.*_PREFS markers in browser_prefs.cc.')
# Check whether any of the modified lines are part of the
# MigrateObsolete.*Pref functions.
for line_nr, line in ModifiedLines(affected_file):
if (range_1[0] <= line_nr <= range_1[1] or
range_2[0] <= line_nr <= range_2[1]):
return True
return False
register_pref_pattern = input_api.re.compile(r'Register.+Pref')
browser_prefs_file_pattern = input_api.re.compile(
r'chrome/browser/prefs/browser_prefs.cc')
changes = input_api.AffectedFiles(include_deletes=True,
file_filter=FilterFile)
potential_problems = []
for f in changes:
for line in f.GenerateScmDiff().splitlines():
# Check deleted lines for pref registrations.
if (line.startswith('-') and not line.startswith('--') and
register_pref_pattern.search(line)):
potential_problems.append('%s: %s' % (f.LocalPath(), line))
if browser_prefs_file_pattern.search(f.LocalPath()):
# If the developer modified the MigrateObsolete.*Prefs() functions, we
# assume that they knew that they have to deprecate preferences and don't
# warn.
try:
if ModifiedPrefMigration(f):
return []
except Exception as e:
return [output_api.PresubmitError(str(e))]
if potential_problems:
return [output_api.PresubmitPromptWarning(
'Discovered possible removal of preference registrations.\n\n'
'Please make sure to properly deprecate preferences by clearing their\n'
'value for a couple of milestones before finally removing the code.\n'
'Otherwise data may stay in the preferences files forever. See\n'
'Migrate*Prefs() in chrome/browser/prefs/browser_prefs.cc for examples.\n'
'This may be a false positive warning (e.g. if you move preference\n'
'registrations to a different place).\n',
potential_problems
)]
return []

@ -3658,5 +3658,156 @@ class MojomStabilityCheckTest(unittest.TestCase):
self.assertEqual([], errors)
class CheckDeprecationOfPreferencesTest(unittest.TestCase):
# Test that a warning is generated if a preference registration is removed
# from a random file.
def testWarning(self):
mock_input_api = MockInputApi()
mock_input_api.files = [
MockAffectedFile(
'foo.cc',
['A', 'B'],
['A', 'prefs->RegisterStringPref("foo", "default");', 'B'],
scm_diff='\n'.join([
'--- foo.cc.old 2020-12-02 20:40:54.430676385 +0100',
'+++ foo.cc.new 2020-12-02 20:41:02.086700197 +0100',
'@@ -1,3 +1,2 @@',
' A',
'-prefs->RegisterStringPref("foo", "default");',
' B']),
action='M')
]
mock_output_api = MockOutputApi()
errors = PRESUBMIT.CheckDeprecationOfPreferences(mock_input_api,
mock_output_api)
self.assertEqual(1, len(errors))
self.assertTrue(
'Discovered possible removal of preference registrations' in
errors[0].message)
# Test that a warning is inhibited if the preference registration was moved
# to the deprecation functions in browser prefs.
def testNoWarningForMigration(self):
mock_input_api = MockInputApi()
mock_input_api.files = [
# RegisterStringPref was removed from foo.cc.
MockAffectedFile(
'foo.cc',
['A', 'B'],
['A', 'prefs->RegisterStringPref("foo", "default");', 'B'],
scm_diff='\n'.join([
'--- foo.cc.old 2020-12-02 20:40:54.430676385 +0100',
'+++ foo.cc.new 2020-12-02 20:41:02.086700197 +0100',
'@@ -1,3 +1,2 @@',
' A',
'-prefs->RegisterStringPref("foo", "default");',
' B']),
action='M'),
# But the preference was properly migrated.
MockAffectedFile(
'chrome/browser/prefs/browser_prefs.cc',
[
'// BEGIN_MIGRATE_OBSOLETE_LOCAL_STATE_PREFS',
'// END_MIGRATE_OBSOLETE_LOCAL_STATE_PREFS',
'// BEGIN_MIGRATE_OBSOLETE_PROFILE_PREFS',
'prefs->RegisterStringPref("foo", "default");',
'// END_MIGRATE_OBSOLETE_PROFILE_PREFS',
],
[
'// BEGIN_MIGRATE_OBSOLETE_LOCAL_STATE_PREFS',
'// END_MIGRATE_OBSOLETE_LOCAL_STATE_PREFS',
'// BEGIN_MIGRATE_OBSOLETE_PROFILE_PREFS',
'// END_MIGRATE_OBSOLETE_PROFILE_PREFS',
],
scm_diff='\n'.join([
'--- browser_prefs.cc.old 2020-12-02 20:51:40.812686731 +0100',
'+++ browser_prefs.cc.new 2020-12-02 20:52:02.936755539 +0100',
'@@ -2,3 +2,4 @@',
' // END_MIGRATE_OBSOLETE_LOCAL_STATE_PREFS',
' // BEGIN_MIGRATE_OBSOLETE_PROFILE_PREFS',
'+prefs->RegisterStringPref("foo", "default");',
' // END_MIGRATE_OBSOLETE_PROFILE_PREFS']),
action='M'),
]
mock_output_api = MockOutputApi()
errors = PRESUBMIT.CheckDeprecationOfPreferences(mock_input_api,
mock_output_api)
self.assertEqual(0, len(errors))
# Test that a warning is NOT inhibited if the preference registration was
# moved to a place outside of the migration functions in browser_prefs.cc
def testWarningForImproperMigration(self):
mock_input_api = MockInputApi()
mock_input_api.files = [
# RegisterStringPref was removed from foo.cc.
MockAffectedFile(
'foo.cc',
['A', 'B'],
['A', 'prefs->RegisterStringPref("foo", "default");', 'B'],
scm_diff='\n'.join([
'--- foo.cc.old 2020-12-02 20:40:54.430676385 +0100',
'+++ foo.cc.new 2020-12-02 20:41:02.086700197 +0100',
'@@ -1,3 +1,2 @@',
' A',
'-prefs->RegisterStringPref("foo", "default");',
' B']),
action='M'),
# The registration call was moved to a place in browser_prefs.cc that
# is outside the migration functions.
MockAffectedFile(
'chrome/browser/prefs/browser_prefs.cc',
[
'prefs->RegisterStringPref("foo", "default");',
'// BEGIN_MIGRATE_OBSOLETE_LOCAL_STATE_PREFS',
'// END_MIGRATE_OBSOLETE_LOCAL_STATE_PREFS',
'// BEGIN_MIGRATE_OBSOLETE_PROFILE_PREFS',
'// END_MIGRATE_OBSOLETE_PROFILE_PREFS',
],
[
'// BEGIN_MIGRATE_OBSOLETE_LOCAL_STATE_PREFS',
'// END_MIGRATE_OBSOLETE_LOCAL_STATE_PREFS',
'// BEGIN_MIGRATE_OBSOLETE_PROFILE_PREFS',
'// END_MIGRATE_OBSOLETE_PROFILE_PREFS',
],
scm_diff='\n'.join([
'--- browser_prefs.cc.old 2020-12-02 20:51:40.812686731 +0100',
'+++ browser_prefs.cc.new 2020-12-02 20:52:02.936755539 +0100',
'@@ -1,2 +1,3 @@',
'+prefs->RegisterStringPref("foo", "default");',
' // BEGIN_MIGRATE_OBSOLETE_LOCAL_STATE_PREFS',
' // END_MIGRATE_OBSOLETE_LOCAL_STATE_PREFS']),
action='M'),
]
mock_output_api = MockOutputApi()
errors = PRESUBMIT.CheckDeprecationOfPreferences(mock_input_api,
mock_output_api)
self.assertEqual(1, len(errors))
self.assertTrue(
'Discovered possible removal of preference registrations' in
errors[0].message)
# Check that the presubmit fails if a marker line in brower_prefs.cc is
# deleted.
def testDeletedMarkerRaisesError(self):
mock_input_api = MockInputApi()
mock_input_api.files = [
MockAffectedFile('chrome/browser/prefs/browser_prefs.cc',
[
'// BEGIN_MIGRATE_OBSOLETE_LOCAL_STATE_PREFS',
'// END_MIGRATE_OBSOLETE_LOCAL_STATE_PREFS',
'// BEGIN_MIGRATE_OBSOLETE_PROFILE_PREFS',
# The following line is deleted for this test
# '// END_MIGRATE_OBSOLETE_PROFILE_PREFS',
])
]
mock_output_api = MockOutputApi()
errors = PRESUBMIT.CheckDeprecationOfPreferences(mock_input_api,
mock_output_api)
self.assertEqual(1, len(errors))
self.assertEqual(
'Broken .*MIGRATE_OBSOLETE_.*_PREFS markers in browser_prefs.cc.',
errors[0].message)
if __name__ == '__main__':
unittest.main()

@ -179,16 +179,21 @@ class MockFile(object):
MockInputApi for presubmit unittests.
"""
def __init__(self, local_path, new_contents, old_contents=None, action='A'):
def __init__(self, local_path, new_contents, old_contents=None, action='A',
scm_diff=None):
self._local_path = local_path
self._new_contents = new_contents
self._changed_contents = [(i + 1, l) for i, l in enumerate(new_contents)]
self._action = action
self._scm_diff = "--- /dev/null\n+++ %s\n@@ -0,0 +1,%d @@\n" % (local_path,
len(new_contents))
if scm_diff:
self._scm_diff = scm_diff
else:
self._scm_diff = (
"--- /dev/null\n+++ %s\n@@ -0,0 +1,%d @@\n" %
(local_path, len(new_contents)))
for l in new_contents:
self._scm_diff += "+%s\n" % l
self._old_contents = old_contents
for l in new_contents:
self._scm_diff += "+%s\n" % l
def Action(self):
return self._action

@ -1077,6 +1077,9 @@ void RegisterSigninProfilePrefs(user_prefs::PrefRegistrySyncable* registry) {
// This method should be periodically pruned of year+ old migrations.
void MigrateObsoleteLocalStatePrefs(PrefService* local_state) {
// BEGIN_MIGRATE_OBSOLETE_LOCAL_STATE_PREFS
// Please don't delete the preceding line. It is used by PRESUBMIT.py.
// Added 1/2020
#if defined(OS_MAC)
local_state->ClearPref(kKeyCreated);
@ -1094,10 +1097,16 @@ void MigrateObsoleteLocalStatePrefs(PrefService* local_state) {
// Added 4/2020.
local_state->ClearPref(kSupervisedUsersNextId);
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
// Please don't delete the following line. It is used by PRESUBMIT.py.
// END_MIGRATE_OBSOLETE_LOCAL_STATE_PREFS
}
// This method should be periodically pruned of year+ old migrations.
void MigrateObsoleteProfilePrefs(Profile* profile) {
// BEGIN_MIGRATE_OBSOLETE_PROFILE_PREFS
// Please don't delete the preceding line. It is used by PRESUBMIT.py.
PrefService* profile_prefs = profile->GetPrefs();
// Check MigrateDeprecatedAutofillPrefs() to see if this is safe to remove.
@ -1188,4 +1197,7 @@ void MigrateObsoleteProfilePrefs(Profile* profile) {
// Added 11/2020
profile_prefs->ClearPref(kDRMSalt);
// Please don't delete the following line. It is used by PRESUBMIT.py.
// END_MIGRATE_OBSOLETE_PROFILE_PREFS
}