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 |