Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(17)

Side by Side Diff: chrome/app/policy/syntax_check_policy_template_json.py

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

Powered by Google App Engine
This is Rietveld 408576698