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é <[email protected]>
Reviewed-by: Jochen Eisinger <[email protected]>
Reviewed-by: Gabriel Charette <[email protected]>
Reviewed-by: Dominic Battré <[email protected]>
Cr-Commit-Position: refs/heads/master@{#833730}
diff --git a/PRESUBMIT.py b/PRESUBMIT.py
index a81d5c4..4f3456c 100644
--- a/PRESUBMIT.py
+++ b/PRESUBMIT.py
@@ -5214,3 +5214,115 @@
'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 []
diff --git a/PRESUBMIT_test.py b/PRESUBMIT_test.py
index 2dfa78a..3dbbb77b 100755
--- a/PRESUBMIT_test.py
+++ b/PRESUBMIT_test.py
@@ -3658,5 +3658,156 @@
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()
diff --git a/PRESUBMIT_test_mocks.py b/PRESUBMIT_test_mocks.py
index 0a9e5a5..f8143ae 100644
--- a/PRESUBMIT_test_mocks.py
+++ b/PRESUBMIT_test_mocks.py
@@ -179,16 +179,21 @@
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
diff --git a/chrome/browser/prefs/browser_prefs.cc b/chrome/browser/prefs/browser_prefs.cc
index 326a5aa..1a181d49 100644
--- a/chrome/browser/prefs/browser_prefs.cc
+++ b/chrome/browser/prefs/browser_prefs.cc
@@ -1077,6 +1077,9 @@
// 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 @@
// 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 @@
// 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
}