- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / app / policy / syntax_check_policy_template_json.py
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))