Roll src/third_party/WebKit 9f7fb92:f103b33 (svn 202621:202622)
[chromium-blink-merge.git] / components / policy / tools / syntax_check_policy_template_json.py
blobed1c222768dad887ea7d8a6de786d6e2405460a9
1 #!/usr/bin/env python
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
6 '''
7 Checks a policy_templates.json file for conformity to its syntax specification.
8 '''
10 import json
11 import optparse
12 import os
13 import re
14 import sys
17 LEADING_WHITESPACE = re.compile('^([ \t]*)')
18 TRAILING_WHITESPACE = re.compile('.*?([ \t]+)$')
19 # Matches all non-empty strings that contain no whitespaces.
20 NO_WHITESPACE = re.compile('[^\s]+$')
22 # Convert a 'type' to the schema types it may be converted to.
23 # The 'dict' type represents structured JSON data, and can be converted
24 # to an 'object' or an 'array'.
25 TYPE_TO_SCHEMA = {
26 'int': [ 'integer' ],
27 'list': [ 'array' ],
28 'dict': [ 'object', 'array' ],
29 'main': [ 'boolean' ],
30 'string': [ 'string' ],
31 'int-enum': [ 'integer' ],
32 'string-enum': [ 'string' ],
33 'string-enum-list': [ 'array' ],
34 'external': [ 'object' ],
37 # List of boolean policies that have been introduced with negative polarity in
38 # the past and should not trigger the negative polarity check.
39 LEGACY_INVERTED_POLARITY_WHITELIST = [
40 'DeveloperToolsDisabled',
41 'DeviceAutoUpdateDisabled',
42 'Disable3DAPIs',
43 'DisableAuthNegotiateCnameLookup',
44 'DisablePluginFinder',
45 'DisablePrintPreview',
46 'DisableSafeBrowsingProceedAnyway',
47 'DisableScreenshots',
48 'DisableSpdy',
49 'DisableSSLRecordSplitting',
50 'DriveDisabled',
51 'DriveDisabledOverCellular',
52 'ExternalStorageDisabled',
53 'SavingBrowserHistoryDisabled',
54 'SyncDisabled',
57 class PolicyTemplateChecker(object):
59 def __init__(self):
60 self.error_count = 0
61 self.warning_count = 0
62 self.num_policies = 0
63 self.num_groups = 0
64 self.num_policies_in_groups = 0
65 self.options = None
66 self.features = []
68 def _Error(self, message, parent_element=None, identifier=None,
69 offending_snippet=None):
70 self.error_count += 1
71 error = ''
72 if identifier is not None and parent_element is not None:
73 error += 'In %s %s: ' % (parent_element, identifier)
74 print error + 'Error: ' + message
75 if offending_snippet is not None:
76 print ' Offending:', json.dumps(offending_snippet, indent=2)
78 def _CheckContains(self, container, key, value_type,
79 optional=False,
80 parent_element='policy',
81 container_name=None,
82 identifier=None,
83 offending='__CONTAINER__',
84 regexp_check=None):
85 '''
86 Checks |container| for presence of |key| with value of type |value_type|.
87 If |value_type| is string and |regexp_check| is specified, then an error is
88 reported when the value does not match the regular expression object.
90 |value_type| can also be a list, if more than one type is supported.
92 The other parameters are needed to generate, if applicable, an appropriate
93 human-readable error message of the following form:
95 In |parent_element| |identifier|:
96 (if the key is not present):
97 Error: |container_name| must have a |value_type| named |key|.
98 Offending snippet: |offending| (if specified; defaults to |container|)
99 (if the value does not have the required type):
100 Error: Value of |key| must be a |value_type|.
101 Offending snippet: |container[key]|
103 Returns: |container[key]| if the key is present, None otherwise.
105 if identifier is None:
106 try:
107 identifier = container.get('name')
108 except:
109 self._Error('Cannot access container name of "%s".' % container_name)
110 return None
111 if container_name is None:
112 container_name = parent_element
113 if offending == '__CONTAINER__':
114 offending = container
115 if key not in container:
116 if optional:
117 return
118 else:
119 self._Error('%s must have a %s "%s".' %
120 (container_name.title(), value_type.__name__, key),
121 container_name, identifier, offending)
122 return None
123 value = container[key]
124 value_types = value_type if isinstance(value_type, list) else [ value_type ]
125 if not any(isinstance(value, type) for type in value_types):
126 self._Error('Value of "%s" must be one of [ %s ].' %
127 (key, ', '.join([type.__name__ for type in value_types])),
128 container_name, identifier, value)
129 if str in value_types and regexp_check and not regexp_check.match(value):
130 self._Error('Value of "%s" must match "%s".' %
131 (key, regexp_check.pattern),
132 container_name, identifier, value)
133 return value
135 def _AddPolicyID(self, id, policy_ids, policy):
137 Adds |id| to |policy_ids|. Generates an error message if the
138 |id| exists already; |policy| is needed for this message.
140 if id in policy_ids:
141 self._Error('Duplicate id', 'policy', policy.get('name'),
143 else:
144 policy_ids.add(id)
146 def _CheckPolicyIDs(self, policy_ids):
148 Checks a set of policy_ids to make sure it contains a continuous range
149 of entries (i.e. no holes).
150 Holes would not be a technical problem, but we want to ensure that nobody
151 accidentally omits IDs.
153 for i in range(len(policy_ids)):
154 if (i + 1) not in policy_ids:
155 self._Error('No policy with id: %s' % (i + 1))
157 def _CheckPolicySchema(self, policy, policy_type):
158 '''Checks that the 'schema' field matches the 'type' field.'''
159 self._CheckContains(policy, 'schema', dict)
160 if isinstance(policy.get('schema'), dict):
161 self._CheckContains(policy['schema'], 'type', str)
162 schema_type = policy['schema'].get('type')
163 if schema_type not in TYPE_TO_SCHEMA[policy_type]:
164 self._Error('Schema type must match the existing type for policy %s' %
165 policy.get('name'))
167 # Checks that boolean policies are not negated (which makes them harder to
168 # reason about).
169 if (schema_type == 'boolean' and
170 'disable' in policy.get('name').lower() and
171 policy.get('name') not in LEGACY_INVERTED_POLARITY_WHITELIST):
172 self._Error(('Boolean policy %s uses negative polarity, please make ' +
173 'new boolean policies follow the XYZEnabled pattern. ' +
174 'See also http://crbug.com/85687') % policy.get('name'))
177 def _CheckPolicy(self, policy, is_in_group, policy_ids):
178 if not isinstance(policy, dict):
179 self._Error('Each policy must be a dictionary.', 'policy', None, policy)
180 return
182 # There should not be any unknown keys in |policy|.
183 for key in policy:
184 if key not in ('name', 'type', 'caption', 'desc', 'device_only',
185 'supported_on', 'label', 'policies', 'items',
186 'example_value', 'features', 'deprecated', 'future',
187 'id', 'schema', 'max_size',
188 'default_for_enterprise_users'):
189 self.warning_count += 1
190 print ('In policy %s: Warning: Unknown key: %s' %
191 (policy.get('name'), key))
193 # Each policy must have a name.
194 self._CheckContains(policy, 'name', str, regexp_check=NO_WHITESPACE)
196 # Each policy must have a type.
197 policy_types = ('group', 'main', 'string', 'int', 'list', 'int-enum',
198 'string-enum', 'string-enum-list', 'dict', 'external')
199 policy_type = self._CheckContains(policy, 'type', str)
200 if policy_type not in policy_types:
201 self._Error('Policy type must be one of: ' + ', '.join(policy_types),
202 'policy', policy.get('name'), policy_type)
203 return # Can't continue for unsupported type.
205 # Each policy must have a caption message.
206 self._CheckContains(policy, 'caption', str)
208 # Each policy must have a description message.
209 self._CheckContains(policy, 'desc', str)
211 # If 'label' is present, it must be a string.
212 self._CheckContains(policy, 'label', str, True)
214 # If 'deprecated' is present, it must be a bool.
215 self._CheckContains(policy, 'deprecated', bool, True)
217 # If 'future' is present, it must be a bool.
218 self._CheckContains(policy, 'future', bool, True)
220 if policy_type == 'group':
221 # Groups must not be nested.
222 if is_in_group:
223 self._Error('Policy groups must not be nested.', 'policy', policy)
225 # Each policy group must have a list of policies.
226 policies = self._CheckContains(policy, 'policies', list)
228 # Check sub-policies.
229 if policies is not None:
230 for nested_policy in policies:
231 self._CheckPolicy(nested_policy, True, policy_ids)
233 # Groups must not have an |id|.
234 if 'id' in policy:
235 self._Error('Policies of type "group" must not have an "id" field.',
236 'policy', policy)
238 # Statistics.
239 self.num_groups += 1
241 else: # policy_type != group
242 # Each policy must have a protobuf ID.
243 id = self._CheckContains(policy, 'id', int)
244 self._AddPolicyID(id, policy_ids, policy)
246 # 'schema' is the new 'type'.
247 # TODO(joaodasilva): remove the 'type' checks once 'schema' is used
248 # everywhere.
249 self._CheckPolicySchema(policy, policy_type)
251 # Each policy must have a supported_on list.
252 supported_on = self._CheckContains(policy, 'supported_on', list)
253 if supported_on is not None:
254 for s in supported_on:
255 if not isinstance(s, str):
256 self._Error('Entries in "supported_on" must be strings.', 'policy',
257 policy, supported_on)
259 # Each policy must have a 'features' dict.
260 features = self._CheckContains(policy, 'features', dict)
262 # All the features must have a documenting message.
263 if features:
264 for feature in features:
265 if not feature in self.features:
266 self._Error('Unknown feature "%s". Known features must have a '
267 'documentation string in the messages dictionary.' %
268 feature, 'policy', policy.get('name', policy))
270 # All user policies must have a per_profile feature flag.
271 if (not policy.get('device_only', False) and
272 not policy.get('deprecated', False) and
273 not filter(re.compile('^chrome_frame:.*').match, supported_on)):
274 self._CheckContains(features, 'per_profile', bool,
275 container_name='features',
276 identifier=policy.get('name'))
278 # All policies must declare whether they allow changes at runtime.
279 self._CheckContains(features, 'dynamic_refresh', bool,
280 container_name='features',
281 identifier=policy.get('name'))
283 # Each policy must have an 'example_value' of appropriate type.
284 if policy_type == 'main':
285 value_type = item_type = bool
286 elif policy_type in ('string', 'string-enum'):
287 value_type = item_type = str
288 elif policy_type in ('int', 'int-enum'):
289 value_type = item_type = int
290 elif policy_type in ('list', 'string-enum-list'):
291 value_type = list
292 item_type = str
293 elif policy_type == 'external':
294 value_type = item_type = dict
295 elif policy_type == 'dict':
296 value_type = item_type = [ dict, list ]
297 else:
298 raise NotImplementedError('Unimplemented policy type: %s' % policy_type)
299 self._CheckContains(policy, 'example_value', value_type)
301 # Statistics.
302 self.num_policies += 1
303 if is_in_group:
304 self.num_policies_in_groups += 1
306 if policy_type in ('int-enum', 'string-enum', 'string-enum-list'):
307 # Enums must contain a list of items.
308 items = self._CheckContains(policy, 'items', list)
309 if items is not None:
310 if len(items) < 1:
311 self._Error('"items" must not be empty.', 'policy', policy, items)
312 for item in items:
313 # Each item must have a name.
314 # Note: |policy.get('name')| is used instead of |policy['name']|
315 # because it returns None rather than failing when no key called
316 # 'name' exists.
317 self._CheckContains(item, 'name', str, container_name='item',
318 identifier=policy.get('name'),
319 regexp_check=NO_WHITESPACE)
321 # Each item must have a value of the correct type.
322 self._CheckContains(item, 'value', item_type, container_name='item',
323 identifier=policy.get('name'))
325 # Each item must have a caption.
326 self._CheckContains(item, 'caption', str, container_name='item',
327 identifier=policy.get('name'))
329 if policy_type == 'external':
330 # Each policy referencing external data must specify a maximum data size.
331 self._CheckContains(policy, 'max_size', int)
333 def _CheckMessage(self, key, value):
334 # |key| must be a string, |value| a dict.
335 if not isinstance(key, str):
336 self._Error('Each message key must be a string.', 'message', key, key)
337 return
339 if not isinstance(value, dict):
340 self._Error('Each message must be a dictionary.', 'message', key, value)
341 return
343 # Each message must have a desc.
344 self._CheckContains(value, 'desc', str, parent_element='message',
345 identifier=key)
347 # Each message must have a text.
348 self._CheckContains(value, 'text', str, parent_element='message',
349 identifier=key)
351 # There should not be any unknown keys in |value|.
352 for vkey in value:
353 if vkey not in ('desc', 'text'):
354 self.warning_count += 1
355 print 'In message %s: Warning: Unknown key: %s' % (key, vkey)
357 def _LeadingWhitespace(self, line):
358 match = LEADING_WHITESPACE.match(line)
359 if match:
360 return match.group(1)
361 return ''
363 def _TrailingWhitespace(self, line):
364 match = TRAILING_WHITESPACE.match(line)
365 if match:
366 return match.group(1)
367 return ''
369 def _LineError(self, message, line_number):
370 self.error_count += 1
371 print 'In line %d: Error: %s' % (line_number, message)
373 def _LineWarning(self, message, line_number):
374 self.warning_count += 1
375 print ('In line %d: Warning: Automatically fixing formatting: %s'
376 % (line_number, message))
378 def _CheckFormat(self, filename):
379 if self.options.fix:
380 fixed_lines = []
381 with open(filename) as f:
382 indent = 0
383 line_number = 0
384 for line in f:
385 line_number += 1
386 line = line.rstrip('\n')
387 # Check for trailing whitespace.
388 trailing_whitespace = self._TrailingWhitespace(line)
389 if len(trailing_whitespace) > 0:
390 if self.options.fix:
391 line = line.rstrip()
392 self._LineWarning('Trailing whitespace.', line_number)
393 else:
394 self._LineError('Trailing whitespace.', line_number)
395 if self.options.fix:
396 if len(line) == 0:
397 fixed_lines += ['\n']
398 continue
399 else:
400 if line == trailing_whitespace:
401 # This also catches the case of an empty line.
402 continue
403 # Check for correct amount of leading whitespace.
404 leading_whitespace = self._LeadingWhitespace(line)
405 if leading_whitespace.count('\t') > 0:
406 if self.options.fix:
407 leading_whitespace = leading_whitespace.replace('\t', ' ')
408 line = leading_whitespace + line.lstrip()
409 self._LineWarning('Tab character found.', line_number)
410 else:
411 self._LineError('Tab character found.', line_number)
412 if line[len(leading_whitespace)] in (']', '}'):
413 indent -= 2
414 if line[0] != '#': # Ignore 0-indented comments.
415 if len(leading_whitespace) != indent:
416 if self.options.fix:
417 line = ' ' * indent + line.lstrip()
418 self._LineWarning('Indentation should be ' + str(indent) +
419 ' spaces.', line_number)
420 else:
421 self._LineError('Bad indentation. Should be ' + str(indent) +
422 ' spaces.', line_number)
423 if line[-1] in ('[', '{'):
424 indent += 2
425 if self.options.fix:
426 fixed_lines.append(line + '\n')
428 # If --fix is specified: backup the file (deleting any existing backup),
429 # then write the fixed version with the old filename.
430 if self.options.fix:
431 if self.options.backup:
432 backupfilename = filename + '.bak'
433 if os.path.exists(backupfilename):
434 os.remove(backupfilename)
435 os.rename(filename, backupfilename)
436 with open(filename, 'w') as f:
437 f.writelines(fixed_lines)
439 def Main(self, filename, options):
440 try:
441 with open(filename) as f:
442 data = eval(f.read())
443 except:
444 import traceback
445 traceback.print_exc(file=sys.stdout)
446 self._Error('Invalid Python/JSON syntax.')
447 return 1
448 if data == None:
449 self._Error('Invalid Python/JSON syntax.')
450 return 1
451 self.options = options
453 # First part: check JSON structure.
455 # Check (non-policy-specific) message definitions.
456 messages = self._CheckContains(data, 'messages', dict,
457 parent_element=None,
458 container_name='The root element',
459 offending=None)
460 if messages is not None:
461 for message in messages:
462 self._CheckMessage(message, messages[message])
463 if message.startswith('doc_feature_'):
464 self.features.append(message[12:])
466 # Check policy definitions.
467 policy_definitions = self._CheckContains(data, 'policy_definitions', list,
468 parent_element=None,
469 container_name='The root element',
470 offending=None)
471 if policy_definitions is not None:
472 policy_ids = set()
473 for policy in policy_definitions:
474 self._CheckPolicy(policy, False, policy_ids)
475 self._CheckPolicyIDs(policy_ids)
477 # Second part: check formatting.
478 self._CheckFormat(filename)
480 # Third part: summary and exit.
481 print ('Finished checking %s. %d errors, %d warnings.' %
482 (filename, self.error_count, self.warning_count))
483 if self.options.stats:
484 if self.num_groups > 0:
485 print ('%d policies, %d of those in %d groups (containing on '
486 'average %.1f policies).' %
487 (self.num_policies, self.num_policies_in_groups, self.num_groups,
488 (1.0 * self.num_policies_in_groups / self.num_groups)))
489 else:
490 print self.num_policies, 'policies, 0 policy groups.'
491 if self.error_count > 0:
492 return 1
493 return 0
495 def Run(self, argv, filename=None):
496 parser = optparse.OptionParser(
497 usage='usage: %prog [options] filename',
498 description='Syntax check a policy_templates.json file.')
499 parser.add_option('--fix', action='store_true',
500 help='Automatically fix formatting.')
501 parser.add_option('--backup', action='store_true',
502 help='Create backup of original file (before fixing).')
503 parser.add_option('--stats', action='store_true',
504 help='Generate statistics.')
505 (options, args) = parser.parse_args(argv)
506 if filename is None:
507 if len(args) != 2:
508 parser.print_help()
509 sys.exit(1)
510 filename = args[1]
511 return self.Main(filename, options)
514 if __name__ == '__main__':
515 sys.exit(PolicyTemplateChecker().Run(sys.argv))