| OLD | NEW |
| (Empty) |
| 1 # Copyright (c) 2016 The Chromium Authors. All rights reserved. | |
| 2 # Use of this source code is governed by a BSD-style license that can be | |
| 3 # found in the LICENSE file. | |
| 4 | |
| 5 """Presubmit script for sync | |
| 6 This checks that ModelTypeInfo entries in model_type.cc follow conventions. | |
| 7 See CheckModelTypeInfoMap or model_type.cc for more detail on the rules. | |
| 8 """ | |
| 9 | |
| 10 import os | |
| 11 | |
| 12 # Some definitions don't follow all the conventions we want to enforce. | |
| 13 # It's either difficult or impossible to fix this, so we ignore the problem(s). | |
| 14 GRANDFATHERED_MODEL_TYPES = [ | |
| 15 'UNSPECIFIED', # Doesn't have a root tag or notification type. | |
| 16 'TOP_LEVEL_FOLDER', # Doesn't have a root tag or notification type. | |
| 17 'AUTOFILL_WALLET_DATA', # Root tag and model type string lack DATA suffix. | |
| 18 'APP_SETTINGS', # Model type string has inconsistent capitalization. | |
| 19 'EXTENSION_SETTINGS', # Model type string has inconsistent capitalization. | |
| 20 'SUPERVISED_USER_SETTINGS', # Root tag and model type string replace | |
| 21 # 'Supervised' with 'Managed' | |
| 22 'SUPERVISED_USERS', # See previous. | |
| 23 'SUPERVISED_USER_WHITELISTS', # See previous. | |
| 24 'SUPERVISED_USER_SHARED_SETTINGS', # See previous. | |
| 25 'PROXY_TABS', # Doesn't have a root tag or notification type. | |
| 26 'NIGORI'] # Model type string is 'encryption keys'. | |
| 27 | |
| 28 # Number of distinct fields in a map entry; used to create | |
| 29 # sets that check for uniqueness. | |
| 30 MAP_ENTRY_FIELD_COUNT = 6 | |
| 31 | |
| 32 # String that precedes the ModelType when referencing the | |
| 33 # proto field number enum e.g. | |
| 34 # sync_pb::EntitySpecifics::kManagedUserFieldNumber. | |
| 35 # Used to map from enum references to the ModelType. | |
| 36 FIELD_NUMBER_PREFIX = 'sync_pb::EntitySpecifics::k' | |
| 37 | |
| 38 # Start and end regexes for finding the EntitySpecifics definition in | |
| 39 # sync.proto. | |
| 40 PROTO_DEFINITION_START_PATTERN = '^message EntitySpecifics' | |
| 41 PROTO_DEFINITION_END_PATTERN = '^\}' | |
| 42 | |
| 43 # Start and end regexes for finding the ModelTypeInfoMap definition | |
| 44 # in model_type.cc. | |
| 45 MODEL_TYPE_START_PATTERN = '^const ModelTypeInfo kModelTypeInfoMap' | |
| 46 MODEL_TYPE_END_PATTERN = '^\};' | |
| 47 | |
| 48 # Strings relating to files we'll need to read. | |
| 49 # model_type.cc is where the ModelTypeInfoMap is | |
| 50 # sync.proto is where the proto definitions for ModelTypes are. | |
| 51 PROTO_FILE_PATH = './protocol/sync.proto' | |
| 52 MODEL_TYPE_FILE_NAME = 'model_type.cc' | |
| 53 | |
| 54 | |
| 55 def CheckChangeOnUpload(input_api, output_api): | |
| 56 """Preupload check function required by presubmit convention. | |
| 57 See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts | |
| 58 """ | |
| 59 for f in input_api.AffectedFiles(): | |
| 60 if(f.LocalPath().endswith(MODEL_TYPE_FILE_NAME)): | |
| 61 return CheckModelTypeInfoMap(input_api, output_api, f) | |
| 62 return [] | |
| 63 | |
| 64 | |
| 65 def CheckChangeOnCommit(input_api, output_api): | |
| 66 """Precommit check function required by presubmit convention. | |
| 67 See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts | |
| 68 """ | |
| 69 for f in input_api.AffectedFiles(): | |
| 70 if f.LocalPath().endswith(MODEL_TYPE_FILE_NAME): | |
| 71 return CheckModelTypeInfoMap(input_api, output_api, f) | |
| 72 return [] | |
| 73 | |
| 74 | |
| 75 def CheckModelTypeInfoMap(input_api, output_api, model_type_file): | |
| 76 """Checks the kModelTypeInfoMap in model_type.cc follows conventions. | |
| 77 Checks that the kModelTypeInfoMap follows the below rules: | |
| 78 1) The model type string should match the model type name, but with | |
| 79 only the first letter capitalized and spaces instead of underscores. | |
| 80 2) The root tag should be the same as the model type but all lowercase. | |
| 81 3) The notification type should match the proto message name. | |
| 82 4) No duplicate data across model types. | |
| 83 Args: | |
| 84 input_api: presubmit_support InputApi instance | |
| 85 output_api: presubmit_support OutputApi instance | |
| 86 model_type_file: AffectedFile object where the ModelTypeInfoMap is | |
| 87 Returns: | |
| 88 A (potentially empty) list PresubmitError objects corresponding to | |
| 89 violations of the above rules. | |
| 90 """ | |
| 91 accumulated_problems = [] | |
| 92 map_entries = ParseModelTypeEntries( | |
| 93 input_api, model_type_file.AbsoluteLocalPath()) | |
| 94 # If any line of the map changed, we check the whole thing since | |
| 95 # definitions span multiple lines and there are rules that apply across | |
| 96 # all definitions e.g. no duplicated field values. | |
| 97 check_map = False | |
| 98 for line_num, _ in model_type_file.ChangedContents(): | |
| 99 for map_entry in map_entries: | |
| 100 if line_num in map_entry.affected_lines: | |
| 101 check_map = True | |
| 102 break | |
| 103 | |
| 104 if not check_map: | |
| 105 return [] | |
| 106 proto_field_definitions = ParseSyncProtoFieldIdentifiers( | |
| 107 input_api, os.path.abspath(PROTO_FILE_PATH)) | |
| 108 accumulated_problems.extend( | |
| 109 CheckNoDuplicatedFieldValues(output_api, map_entries)) | |
| 110 | |
| 111 for map_entry in map_entries: | |
| 112 entry_problems = [] | |
| 113 entry_problems.extend( | |
| 114 CheckNotificationTypeMatchesProtoMessageName( | |
| 115 output_api, map_entry, proto_field_definitions)) | |
| 116 | |
| 117 if map_entry.model_type not in GRANDFATHERED_MODEL_TYPES: | |
| 118 entry_problems.extend( | |
| 119 CheckModelTypeStringMatchesModelType(output_api, map_entry)) | |
| 120 entry_problems.extend( | |
| 121 CheckRootTagMatchesModelType(output_api, map_entry)) | |
| 122 | |
| 123 if len(entry_problems) > 0: | |
| 124 accumulated_problems.extend(entry_problems) | |
| 125 | |
| 126 return accumulated_problems | |
| 127 | |
| 128 | |
| 129 class ModelTypeEnumEntry(object): | |
| 130 """Class that encapsulates a ModelTypeInfo definition in model_type.cc. | |
| 131 Allows access to each of the named fields in the definition and also | |
| 132 which lines the definition spans. | |
| 133 Attributes: | |
| 134 model_type: entry's ModelType enum value | |
| 135 notification_type: model type's notification string | |
| 136 root_tag: model type's root tag | |
| 137 model_type_string: string corresponding to the ModelType | |
| 138 field_number: proto field number | |
| 139 histogram_val: value identifying ModelType in histogram | |
| 140 affected_lines: lines in model_type.cc that the definition spans | |
| 141 """ | |
| 142 def __init__(self, entry_strings, affected_lines): | |
| 143 (model_type, notification_type, root_tag, model_type_string, | |
| 144 field_number, histogram_val) = entry_strings | |
| 145 self.model_type = model_type | |
| 146 self.notification_type = notification_type | |
| 147 self.root_tag = root_tag | |
| 148 self.model_type_string = model_type_string | |
| 149 self.field_number = field_number | |
| 150 self.histogram_val = histogram_val | |
| 151 self.affected_lines = affected_lines | |
| 152 | |
| 153 | |
| 154 def ParseModelTypeEntries(input_api, model_type_cc_path): | |
| 155 """Parses model_type_cc_path for ModelTypeEnumEntries | |
| 156 Args: | |
| 157 input_api: presubmit_support InputAPI instance | |
| 158 model_type_cc_path: path to file containing the ModelTypeInfo entries | |
| 159 Returns: | |
| 160 A list of ModelTypeEnumEntry objects read from model_type.cc. | |
| 161 e.g. ('AUTOFILL_WALLET_METADATA', 'WALLET_METADATA', | |
| 162 'autofill_wallet_metadata', 'Autofill Wallet Metadata', | |
| 163 'sync_pb::EntitySpecifics::kWalletMetadataFieldNumber', '35', | |
| 164 [63, 64, 65]) | |
| 165 """ | |
| 166 file_contents = input_api.ReadFile(model_type_cc_path) | |
| 167 start_pattern = input_api.re.compile(MODEL_TYPE_START_PATTERN) | |
| 168 end_pattern = input_api.re.compile(MODEL_TYPE_END_PATTERN) | |
| 169 results, definition_strings, definition_lines = [], [], [] | |
| 170 inside_enum = False | |
| 171 current_line_number = 1 | |
| 172 for line in file_contents.splitlines(): | |
| 173 if start_pattern.match(line): | |
| 174 inside_enum = True | |
| 175 continue | |
| 176 if inside_enum: | |
| 177 if end_pattern.match(line): | |
| 178 break | |
| 179 line_entries = line.strip().strip('{},').split(',') | |
| 180 definition_strings.extend([entry.strip('" ') for entry in line_entries]) | |
| 181 definition_lines.append(current_line_number) | |
| 182 if line.endswith('},'): | |
| 183 results.append(ModelTypeEnumEntry(definition_strings, definition_lines)) | |
| 184 definition_strings = [] | |
| 185 definition_lines = [] | |
| 186 current_line_number += 1 | |
| 187 return results | |
| 188 | |
| 189 | |
| 190 def ParseSyncProtoFieldIdentifiers(input_api, sync_proto_path): | |
| 191 """Parses proto field identifiers from the EntitySpecifics definition. | |
| 192 Args: | |
| 193 input_api: presubmit_support InputAPI instance | |
| 194 proto_path: path to the file containing the proto field definitions | |
| 195 Returns: | |
| 196 A dictionary of the format {'SyncDataType': 'field_identifier'} | |
| 197 e.g. {'AutofillSpecifics': 'autofill'} | |
| 198 """ | |
| 199 proto_field_definitions = {} | |
| 200 proto_file_contents = input_api.ReadFile(sync_proto_path).splitlines() | |
| 201 start_pattern = input_api.re.compile(PROTO_DEFINITION_START_PATTERN) | |
| 202 end_pattern = input_api.re.compile(PROTO_DEFINITION_END_PATTERN) | |
| 203 in_proto_def = False | |
| 204 for line in proto_file_contents: | |
| 205 if start_pattern.match(line): | |
| 206 in_proto_def = True | |
| 207 continue | |
| 208 if in_proto_def: | |
| 209 if end_pattern.match(line): | |
| 210 break | |
| 211 line = line.strip() | |
| 212 split_proto_line = line.split(' ') | |
| 213 # ignore comments and lines that don't contain definitions. | |
| 214 if '//' in line or len(split_proto_line) < 3: | |
| 215 continue | |
| 216 | |
| 217 field_typename = split_proto_line[1] | |
| 218 field_identifier = split_proto_line[2] | |
| 219 proto_field_definitions[field_typename] = field_identifier | |
| 220 return proto_field_definitions | |
| 221 | |
| 222 | |
| 223 def StripTrailingS(string): | |
| 224 return string.rstrip('sS') | |
| 225 | |
| 226 | |
| 227 def IsTitleCased(string): | |
| 228 return reduce(lambda bool1, bool2: bool1 and bool2, | |
| 229 [s[0].isupper() for s in string.split(' ')]) | |
| 230 | |
| 231 | |
| 232 def FormatPresubmitError(output_api, message, affected_lines): | |
| 233 """ Outputs a formatted error message with filename and line number(s). | |
| 234 """ | |
| 235 if len(affected_lines) > 1: | |
| 236 message_including_lines = 'Error at lines %d-%d in model_type.cc: %s' %( | |
| 237 affected_lines[0], affected_lines[-1], message) | |
| 238 else: | |
| 239 message_including_lines = 'Error at line %d in model_type.cc: %s' %( | |
| 240 affected_lines[0], message) | |
| 241 return output_api.PresubmitError(message_including_lines) | |
| 242 | |
| 243 | |
| 244 def CheckNotificationTypeMatchesProtoMessageName( | |
| 245 output_api, map_entry, proto_field_definitions): | |
| 246 """Check that map_entry's notification type matches sync.proto. | |
| 247 Verifies that the notification_type matches the name of the field defined | |
| 248 in the sync.proto by looking it up in the proto_field_definitions map. | |
| 249 Args: | |
| 250 output_api: presubmit_support OutputApi instance | |
| 251 map_entry: ModelTypeEnumEntry instance | |
| 252 proto_field_definitions: dict of proto field types and field names | |
| 253 Returns: | |
| 254 A potentially empty list of PresubmitError objects corresponding to | |
| 255 violations of the above rule | |
| 256 """ | |
| 257 if map_entry.field_number == '-1': | |
| 258 return [] | |
| 259 proto_message_name = proto_field_definitions[ | |
| 260 FieldNumberToPrototypeString(map_entry.field_number)] | |
| 261 if map_entry.notification_type.lower() != proto_message_name: | |
| 262 return [ | |
| 263 FormatPresubmitError( | |
| 264 output_api,'notification type "%s" does not match proto message' | |
| 265 ' name defined in sync.proto: ' '"%s"' % | |
| 266 (map_entry.notification_type, proto_message_name), | |
| 267 map_entry.affected_lines)] | |
| 268 return [] | |
| 269 | |
| 270 | |
| 271 def CheckNoDuplicatedFieldValues(output_api, map_entries): | |
| 272 """Check that map_entries has no duplicated field values. | |
| 273 Verifies that every map_entry in map_entries doesn't have a field value | |
| 274 used elsewhere in map_entries, ignoring special values ("" and -1). | |
| 275 Args: | |
| 276 output_api: presubmit_support OutputApi instance | |
| 277 map_entries: list of ModelTypeEnumEntry objects to check | |
| 278 Returns: | |
| 279 A list PresubmitError objects for each duplicated field value | |
| 280 """ | |
| 281 problem_list = [] | |
| 282 field_value_sets = [set() for i in range(MAP_ENTRY_FIELD_COUNT)] | |
| 283 for map_entry in map_entries: | |
| 284 field_values = [ | |
| 285 map_entry.model_type, map_entry.notification_type, | |
| 286 map_entry.root_tag, map_entry.model_type_string, | |
| 287 map_entry.field_number, map_entry.histogram_val] | |
| 288 for i in range(MAP_ENTRY_FIELD_COUNT): | |
| 289 field_value = field_values[i] | |
| 290 field_value_set = field_value_sets[i] | |
| 291 if field_value in field_value_set: | |
| 292 problem_list.append( | |
| 293 FormatPresubmitError( | |
| 294 output_api, 'Duplicated field value "%s"' % field_value, | |
| 295 map_entry.affected_lines)) | |
| 296 elif len(field_value) > 0 and field_value != '-1': | |
| 297 field_value_set.add(field_value) | |
| 298 return problem_list | |
| 299 | |
| 300 | |
| 301 def CheckModelTypeStringMatchesModelType(output_api, map_entry): | |
| 302 """Check that map_entry's model_type_string matches ModelType. | |
| 303 Args: | |
| 304 output_api: presubmit_support OutputApi instance | |
| 305 map_entry: ModelTypeEnumEntry object to check | |
| 306 Returns: | |
| 307 A list of PresubmitError objects for each violation | |
| 308 """ | |
| 309 problem_list = [] | |
| 310 expected_model_type_string = map_entry.model_type.lower().replace('_', ' ') | |
| 311 if (StripTrailingS(expected_model_type_string) != | |
| 312 StripTrailingS(map_entry.model_type_string.lower())): | |
| 313 problem_list.append( | |
| 314 FormatPresubmitError( | |
| 315 output_api,'model type string "%s" does not match model type.' | |
| 316 ' It should be "%s"' % ( | |
| 317 map_entry.model_type_string, expected_model_type_string.title()), | |
| 318 map_entry.affected_lines)) | |
| 319 if not IsTitleCased(map_entry.model_type_string): | |
| 320 problem_list.append( | |
| 321 FormatPresubmitError( | |
| 322 output_api,'model type string "%s" should be title cased' % | |
| 323 (map_entry.model_type_string), map_entry.affected_lines)) | |
| 324 return problem_list | |
| 325 | |
| 326 | |
| 327 def CheckRootTagMatchesModelType(output_api, map_entry): | |
| 328 """Check that map_entry's root tag matches ModelType. | |
| 329 Args: | |
| 330 output_api: presubmit_support OutputAPI instance | |
| 331 map_entry: ModelTypeEnumEntry object to check | |
| 332 Returns: | |
| 333 A list of PresubmitError objects for each violation | |
| 334 """ | |
| 335 expected_root_tag = map_entry.model_type.lower() | |
| 336 if (StripTrailingS(expected_root_tag) != | |
| 337 StripTrailingS(map_entry.root_tag)): | |
| 338 return [ | |
| 339 FormatPresubmitError( | |
| 340 output_api,'root tag "%s" does not match model type. It should' | |
| 341 'be "%s"' % (map_entry.root_tag, expected_root_tag), | |
| 342 map_entry.affected_lines)] | |
| 343 return [] | |
| 344 | |
| 345 | |
| 346 def FieldNumberToPrototypeString(field_number): | |
| 347 """Converts a field number enum reference to an EntitySpecifics string. | |
| 348 Converts a reference to the field number enum to the corresponding | |
| 349 proto data type string. | |
| 350 Args: | |
| 351 field_number: string representation of a field number enum reference | |
| 352 Returns: | |
| 353 A string that is the corresponding proto field data type. e.g. | |
| 354 FieldNumberToPrototypeString('EntitySpecifics::kAppFieldNumber') | |
| 355 => 'AppSpecifics' | |
| 356 """ | |
| 357 return field_number.replace(FIELD_NUMBER_PREFIX, '').replace( | |
| 358 'FieldNumber', 'Specifics').replace( | |
| 359 'AppNotificationSpecifics', 'AppNotification') | |
| OLD | NEW |