summaryrefslogtreecommitdiffstats
path: root/chrome
diff options
context:
space:
mode:
authorjkummerow@chromium.org <jkummerow@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-01-19 10:12:55 +0000
committerjkummerow@chromium.org <jkummerow@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-01-19 10:12:55 +0000
commit989733702ea9ccd8d2aa2a8b5dc2238bcab43c87 (patch)
tree63df9c4cad8c7ce94afc864aa33cd920b7de1456 /chrome
parent00122a4dd029043dc94d6d25c4cdd9d9b3d2f99e (diff)
downloadchromium_src-989733702ea9ccd8d2aa2a8b5dc2238bcab43c87.zip
chromium_src-989733702ea9ccd8d2aa2a8b5dc2238bcab43c87.tar.gz
chromium_src-989733702ea9ccd8d2aa2a8b5dc2238bcab43c87.tar.bz2
Syntax checker for policy_templates.json
To make sure that all policy definitions match the expected format. The checker is called as a presubmit hook, and can also be invoked manually. Run it without any parameters (or with --help) to get usage information. BUG=69527 TEST=manual: checker reports no errors for correct policy definitions, checker complains for malformed policy definitions, checker is invoked as presubmit hook when policy_templates.json has been changed Review URL: http://codereview.chromium.org/6299001 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@71778 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome')
-rw-r--r--chrome/app/policy/PRESUBMIT.py35
-rw-r--r--chrome/app/policy/policy_templates.json2
-rw-r--r--chrome/app/policy/syntax_check_policy_template_json.py396
3 files changed, 432 insertions, 1 deletions
diff --git a/chrome/app/policy/PRESUBMIT.py b/chrome/app/policy/PRESUBMIT.py
new file mode 100644
index 0000000..a17f927
--- /dev/null
+++ b/chrome/app/policy/PRESUBMIT.py
@@ -0,0 +1,35 @@
+# Copyright (c) 2011 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# If this presubmit check fails or misbehaves, please complain to
+# gfeher@chromium.org or jkummerow@chromium.org.
+
+import sys
+
+
+def _CommonChecks(input_api, output_api):
+ filepath = input_api.os_path.join(input_api.PresubmitLocalPath(),
+ 'policy_templates.json')
+ if any(f.AbsoluteLocalPath() == filepath
+ for f in input_api.AffectedFiles()):
+ old_sys_path = sys.path
+ try:
+ sys.path = [input_api.PresubmitLocalPath()] + sys.path
+ # Optimization: only load this when it's needed.
+ import syntax_check_policy_template_json
+ checker = syntax_check_policy_template_json.PolicyTemplateChecker()
+ if checker.Run([], filepath) > 0:
+ return [output_api.PresubmitError('Syntax error(s) in file:',
+ [filepath])]
+ finally:
+ sys.path = old_sys_path
+ return []
+
+
+def CheckChangeOnUpload(input_api, output_api):
+ return _CommonChecks(input_api, output_api)
+
+
+def CheckChangeOnCommit(input_api, output_api):
+ return _CommonChecks(input_api, output_api)
diff --git a/chrome/app/policy/policy_templates.json b/chrome/app/policy/policy_templates.json
index d046bf9..0061797 100644
--- a/chrome/app/policy/policy_templates.json
+++ b/chrome/app/policy/policy_templates.json
@@ -81,7 +81,7 @@
# chrome.*, chrome.mac -> plist, plist_strings,doc
# everything else -> doc
#
-# Annotations:
+# Annotations:
# Additional information is specified under keys 'features' and
# 'example_value'. These are used in the generated documentation and example
# policy configuration files.
diff --git a/chrome/app/policy/syntax_check_policy_template_json.py b/chrome/app/policy/syntax_check_policy_template_json.py
new file mode 100644
index 0000000..04d69c9
--- /dev/null
+++ b/chrome/app/policy/syntax_check_policy_template_json.py
@@ -0,0 +1,396 @@
+#!/usr/bin/python2
+# Copyright (c) 2011 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''
+Checks a policy_templates.json file for conformity to its syntax specification.
+'''
+
+import json
+import optparse
+import os
+import re
+import sys
+
+
+LEADING_WHITESPACE = re.compile('^([ \t]*)')
+TRAILING_WHITESPACE = re.compile('.*?([ \t]+)$')
+
+
+class PolicyTemplateChecker(object):
+
+ def __init__(self):
+ self.error_count = 0
+ self.warning_count = 0
+ self.num_policies = 0
+ self.num_groups = 0
+ self.num_policies_in_groups = 0
+ self.options = None
+
+ def _Error(self, message, parent_element=None, identifier=None,
+ offending_snippet=None):
+ self.error_count += 1
+ error = ''
+ if identifier is not None and parent_element is not None:
+ error += 'In %s %s: ' % (parent_element, identifier)
+ print error + 'Error: ' + message
+ if offending_snippet is not None:
+ print ' Offending:', json.dumps(offending_snippet, indent=2)
+
+ def _CheckContains(self, container, key, value_type,
+ optional=False,
+ parent_element='policy',
+ container_name=None,
+ identifier=None,
+ offending='__CONTAINER__'):
+ '''
+ Checks |container| for presence of |key| with value of type |value_type|.
+
+ The other parameters are needed to generate, if applicable, an appropriate
+ human-readable error message of the following form:
+
+ In |parent_element| |identifier|:
+ (if the key is not present):
+ Error: |container_name| must have a |value_type| named |key|.
+ Offending snippet: |offending| (if specified; defaults to |container|)
+ (if the value does not have the required type):
+ Error: Value of |key| must be a |value_type|.
+ Offending snippet: |container[key]|
+
+ Returns: |container[key]| if the key is present, None otherwise.
+ '''
+ if identifier is None:
+ identifier = container.get('name')
+ if container_name is None:
+ container_name = parent_element
+ if offending == '__CONTAINER__':
+ offending = container
+ if key not in container:
+ if optional:
+ return
+ else:
+ self._Error('%s must have a %s "%s".' %
+ (container_name.title(), value_type.__name__, key),
+ container_name, identifier, offending)
+ return None
+ value = container[key]
+ if not isinstance(value, value_type):
+ self._Error('Value of "%s" must be a %s.' %
+ (key, value_type.__name__),
+ container_name, identifier, value)
+ return value
+
+ def _CheckPolicy(self, policy, may_contain_groups):
+ if not isinstance(policy, dict):
+ self._Error('Each policy must be a dictionary.', 'policy', None, policy)
+ return
+
+ # There should not be any unknown keys in |policy|.
+ for key in policy:
+ if key not in ('name', 'type', 'caption', 'desc', 'supported_on',
+ 'label', 'policies', 'items', 'example_value', 'features',
+ 'deprecated'):
+ self.warning_count += 1
+ print ('In policy %s: Warning: Unknown key: %s' %
+ (policy.get('name'), key))
+
+ # Each policy must have a name.
+ self._CheckContains(policy, 'name', str)
+
+ # Each policy must have a type.
+ policy_type = self._CheckContains(policy, 'type', str)
+ if policy_type not in ('group', 'main', 'string', 'int', 'list', 'int-enum',
+ 'string-enum'):
+ self._Error('Policy type must be either of: group, main, string, int, '
+ 'list, int-enum, string-enum', 'policy', policy, policy_type)
+ return # Can't continue for unsupported type.
+
+ # Each policy must have a caption message.
+ self._CheckContains(policy, 'caption', str)
+
+ # Each policy must have a description message.
+ self._CheckContains(policy, 'desc', str)
+
+ # If 'label' is present, it must be a string.
+ self._CheckContains(policy, 'label', str, True)
+
+ # If 'deprecated' is present, it must be a bool.
+ self._CheckContains(policy, 'deprecated', bool, True)
+
+ if policy_type == 'group':
+
+ # Groups must not be nested.
+ if not may_contain_groups:
+ self._Error('Policy groups must not be nested.', 'policy', policy)
+
+ # Each policy group must have a list of policies.
+ policies = self._CheckContains(policy, 'policies', list)
+ if policies is not None:
+ for nested_policy in policies:
+ self._CheckPolicy(nested_policy, False)
+
+ # Statistics.
+ self.num_groups += 1
+ else: # policy_type != group
+
+ # Each policy must have a supported_on list.
+ supported_on = self._CheckContains(policy, 'supported_on', list)
+ if supported_on is not None:
+ for s in supported_on:
+ if not isinstance(s, str):
+ self._Error('Entries in "supported_on" must be strings.', 'policy',
+ policy, supported_on)
+
+ # Each policy must have a 'features' dict.
+ self._CheckContains(policy, 'features', dict)
+
+ # Each policy must have an 'example_value' of appropriate type.
+ if policy_type == 'main':
+ value_type = bool
+ elif policy_type in ('string', 'string-enum'):
+ value_type = str
+ elif policy_type in ('int', 'int-enum'):
+ value_type = int
+ elif policy_type == 'list':
+ value_type = list
+ else:
+ raise NotImplementedError('Unimplemented policy type: %s' % policy_type)
+ self._CheckContains(policy, 'example_value', value_type)
+
+ # Statistics.
+ self.num_policies += 1
+ if not may_contain_groups:
+ self.num_policies_in_groups += 1
+
+ if policy_type in ('int-enum', 'string-enum'):
+
+ # Enums must contain a list of items.
+ items = self._CheckContains(policy, 'items', list)
+ if items is not None:
+ if len(items) < 1:
+ self._Error('"items" must not be empty.', 'policy', policy, items)
+ for item in items:
+
+ # Each item must have a name.
+ # Note: |policy.get('name')| is used instead of |policy['name']|
+ # because it returns None rather than failing when no key called
+ # 'name' exists.
+ self._CheckContains(item, 'name', str, container_name='item',
+ identifier=policy.get('name'))
+
+ # Each item must have a value of the correct type.
+ self._CheckContains(item, 'value', value_type, container_name='item',
+ identifier=policy.get('name'))
+
+ # Each item must have a caption.
+ self._CheckContains(item, 'caption', str, container_name='item',
+ identifier=policy.get('name'))
+
+ def _CheckMessage(self, key, value):
+ # |key| must be a string, |value| a dict.
+ if not isinstance(key, str):
+ self._Error('Each message key must be a string.', 'message', key, key)
+ return
+
+ if not isinstance(value, dict):
+ self._Error('Each message must be a dictionary.', 'message', key, value)
+ return
+
+ # Each message must have a desc.
+ self._CheckContains(value, 'desc', str, parent_element='message',
+ identifier=key)
+
+ # Each message must have a text.
+ self._CheckContains(value, 'text', str, parent_element='message',
+ identifier=key)
+
+ # There should not be any unknown keys in |value|.
+ for vkey in value:
+ if vkey not in ('desc', 'text'):
+ self.warning_count += 1
+ print 'In message %s: Warning: Unknown key: %s' % (key, vkey)
+
+ def _CheckPlaceholder(self, placeholder):
+ if not isinstance(placeholder, dict):
+ self._Error('Each placeholder must be a dictionary.',
+ 'placeholder', None, placeholder)
+ return
+
+ # Each placeholder must have a 'key'.
+ key = self._CheckContains(placeholder, 'key', str,
+ parent_element='placeholder')
+
+ # Each placeholder must have a 'value'.
+ self._CheckContains(placeholder, 'value', str, parent_element='placeholder',
+ identifier=key)
+
+ # There should not be any unknown keys in |placeholder|.
+ for k in placeholder:
+ if k not in ('key', 'value'):
+ self.warning_count += 1
+ name = str(placeholder.get('key'), placeholder)
+ print 'In placeholder %s: Warning: Unknown key: %s' % (name, k)
+
+ def _LeadingWhitespace(self, line):
+ match = LEADING_WHITESPACE.match(line)
+ if match:
+ return match.group(1)
+ return ''
+
+ def _TrailingWhitespace(self, line):
+ match = TRAILING_WHITESPACE.match(line)
+ if match:
+ return match.group(1)
+ return ''
+
+ def _LineError(self, message, line_number):
+ self.error_count += 1
+ print 'In line %d: Error: %s' % (line_number, message)
+
+ def _LineWarning(self, message, line_number):
+ self.warning_count += 1
+ print ('In line %d: Warning: Automatically fixing formatting: %s'
+ % (line_number, message))
+
+ def _CheckFormat(self, filename):
+ if self.options.fix:
+ fixed_lines = []
+ with open(filename) as f:
+ indent = 0
+ line_number = 0
+ for line in f:
+ line_number += 1
+ line = line.rstrip('\n')
+ # Check for trailing whitespace.
+ trailing_whitespace = self._TrailingWhitespace(line)
+ if len(trailing_whitespace) > 0:
+ if self.options.fix:
+ line = line.rstrip()
+ self._LineWarning('Trailing whitespace.', line_number)
+ else:
+ self._LineError('Trailing whitespace.', line_number)
+ if len(line) == 0:
+ if self.options.fix:
+ fixed_lines += ['\n']
+ continue
+ if len(line) == len(trailing_whitespace):
+ continue
+ # Check for correct amount of leading whitespace.
+ leading_whitespace = self._LeadingWhitespace(line)
+ if leading_whitespace.count('\t') > 0:
+ if self.options.fix:
+ line = leading_whitespace.replace('\t', ' ') + line.lstrip()
+ self._LineWarning('Tab character found.', line_number)
+ else:
+ self._LineError('Tab character found.', line_number)
+ if line[len(leading_whitespace)] in (']', '}'):
+ indent -= 2
+ if line[0] != '#': # Ignore 0-indented comments.
+ if len(leading_whitespace) != indent:
+ if self.options.fix:
+ line = ' ' * indent + line.lstrip()
+ self._LineWarning('Indentation should be ' + str(indent) +
+ ' spaces.', line_number)
+ else:
+ self._LineError('Bad indentation. Should be ' + str(indent) +
+ ' spaces.', line_number)
+ if line[-1] in ('[', '{'):
+ indent += 2
+ if self.options.fix:
+ fixed_lines.append(line + '\n')
+
+ # If --fix is specified: backup the file (deleting any existing backup),
+ # then write the fixed version with the old filename.
+ if self.options.fix:
+ if self.options.backup:
+ backupfilename = filename + '.bak'
+ if os.path.exists(backupfilename):
+ os.remove(backupfilename)
+ os.rename(filename, backupfilename)
+ with open(filename, 'w') as f:
+ f.writelines(fixed_lines)
+
+ def Main(self, filename, options):
+ try:
+ with open(filename) as f:
+ data = eval(f.read())
+ except:
+ import traceback
+ traceback.print_exc(file=sys.stdout)
+ self._Error('Invalid JSON syntax.')
+ return
+ if data == None:
+ self._Error('Invalid JSON syntax.')
+ return
+ self.options = options
+
+ # First part: check JSON structure.
+
+ # Check policy definitions.
+ policy_definitions = self._CheckContains(data, 'policy_definitions', list,
+ parent_element=None,
+ container_name='The root element',
+ offending=None)
+ if policy_definitions is not None:
+ for policy in policy_definitions:
+ self._CheckPolicy(policy, True)
+
+ # Check (non-policy-specific) message definitions.
+ messages = self._CheckContains(data, 'messages', dict,
+ parent_element=None,
+ container_name='The root element',
+ offending=None)
+ if messages is not None:
+ for message in messages:
+ self._CheckMessage(message, messages[message])
+
+ # Check placeholders.
+ placeholders = self._CheckContains(data, 'placeholders', list,
+ parent_element=None,
+ container_name='The root element',
+ offending=None)
+ if placeholders is not None:
+ for placeholder in placeholders:
+ self._CheckPlaceholder(placeholder)
+
+ # Second part: check formatting.
+ self._CheckFormat(filename)
+
+ # Third part: summary and exit.
+ print ('Finished. %d errors, %d warnings.' %
+ (self.error_count, self.warning_count))
+ if self.options.stats:
+ if self.num_groups > 0:
+ print ('%d policies, %d of those in %d groups (containing on '
+ 'average %.1f policies).' %
+ (self.num_policies, self.num_policies_in_groups, self.num_groups,
+ (1.0 * self.num_policies_in_groups / self.num_groups)))
+ else:
+ print self.num_policies, 'policies, 0 policy groups.'
+ if self.error_count > 0:
+ return 1
+ return 0
+
+ def Run(self, argv, filename=None):
+ parser = optparse.OptionParser(
+ usage='usage: %prog [options] filename',
+ description='Syntax check a policy_templates.json file.')
+ parser.add_option('--fix', action='store_true',
+ help='Automatically fix formatting.')
+ parser.add_option('--backup', action='store_true',
+ help='Create backup of original file (before fixing).')
+ parser.add_option('--stats', action='store_true',
+ help='Generate statistics.')
+ (options, args) = parser.parse_args(argv)
+ if filename is None:
+ if len(args) != 2:
+ parser.print_help()
+ sys.exit(1)
+ filename = args[1]
+ return self.Main(filename, options)
+
+
+if __name__ == '__main__':
+ checker = PolicyTemplateChecker()
+ sys.exit(checker.Run(sys.argv))