OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/python2 |
| 2 # Copyright (c) 2011 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 |
| 20 |
| 21 class PolicyTemplateChecker(object): |
| 22 |
| 23 def __init__(self): |
| 24 self.error_count = 0 |
| 25 self.warning_count = 0 |
| 26 self.num_policies = 0 |
| 27 self.num_groups = 0 |
| 28 self.num_policies_in_groups = 0 |
| 29 self.options = None |
| 30 |
| 31 def _Error(self, message, parent_element=None, identifier=None, |
| 32 offending_snippet=None): |
| 33 self.error_count += 1 |
| 34 error = '' |
| 35 if identifier is not None and parent_element is not None: |
| 36 error += 'In %s %s: ' % (parent_element, identifier) |
| 37 print error + 'Error: ' + message |
| 38 if offending_snippet is not None: |
| 39 print ' Offending:', json.dumps(offending_snippet, indent=2) |
| 40 |
| 41 def _CheckContains(self, container, key, value_type, |
| 42 optional=False, |
| 43 parent_element='policy', |
| 44 container_name=None, |
| 45 identifier=None, |
| 46 offending='__CONTAINER__'): |
| 47 ''' |
| 48 Checks |container| for presence of |key| with value of type |value_type|. |
| 49 |
| 50 The other parameters are needed to generate, if applicable, an appropriate |
| 51 human-readable error message of the following form: |
| 52 |
| 53 In |parent_element| |identifier|: |
| 54 (if the key is not present): |
| 55 Error: |container_name| must have a |value_type| named |key|. |
| 56 Offending snippet: |offending| (if specified; defaults to |container|) |
| 57 (if the value does not have the required type): |
| 58 Error: Value of |key| must be a |value_type|. |
| 59 Offending snippet: |container[key]| |
| 60 |
| 61 Returns: |container[key]| if the key is present, None otherwise. |
| 62 ''' |
| 63 if identifier is None: |
| 64 identifier = container.get('name') |
| 65 if container_name is None: |
| 66 container_name = parent_element |
| 67 if offending == '__CONTAINER__': |
| 68 offending = container |
| 69 if key not in container: |
| 70 if optional: |
| 71 return |
| 72 else: |
| 73 self._Error('%s must have a %s "%s".' % |
| 74 (container_name.title(), value_type.__name__, key), |
| 75 container_name, identifier, offending) |
| 76 return None |
| 77 value = container[key] |
| 78 if not isinstance(value, value_type): |
| 79 self._Error('Value of "%s" must be a %s.' % |
| 80 (key, value_type.__name__), |
| 81 container_name, identifier, value) |
| 82 return value |
| 83 |
| 84 def _CheckPolicy(self, policy, may_contain_groups): |
| 85 if not isinstance(policy, dict): |
| 86 self._Error('Each policy must be a dictionary.', 'policy', None, policy) |
| 87 return |
| 88 |
| 89 # There should not be any unknown keys in |policy|. |
| 90 for key in policy: |
| 91 if key not in ('name', 'type', 'caption', 'desc', 'supported_on', |
| 92 'label', 'policies', 'items', 'example_value', 'features', |
| 93 'deprecated'): |
| 94 self.warning_count += 1 |
| 95 print ('In policy %s: Warning: Unknown key: %s' % |
| 96 (policy.get('name'), key)) |
| 97 |
| 98 # Each policy must have a name. |
| 99 self._CheckContains(policy, 'name', str) |
| 100 |
| 101 # Each policy must have a type. |
| 102 policy_type = self._CheckContains(policy, 'type', str) |
| 103 if policy_type not in ('group', 'main', 'string', 'int', 'list', 'int-enum', |
| 104 'string-enum'): |
| 105 self._Error('Policy type must be either of: group, main, string, int, ' |
| 106 'list, int-enum, string-enum', 'policy', policy, policy_type) |
| 107 return # Can't continue for unsupported type. |
| 108 |
| 109 # Each policy must have a caption message. |
| 110 self._CheckContains(policy, 'caption', str) |
| 111 |
| 112 # Each policy must have a description message. |
| 113 self._CheckContains(policy, 'desc', str) |
| 114 |
| 115 # If 'label' is present, it must be a string. |
| 116 self._CheckContains(policy, 'label', str, True) |
| 117 |
| 118 # If 'deprecated' is present, it must be a bool. |
| 119 self._CheckContains(policy, 'deprecated', bool, True) |
| 120 |
| 121 if policy_type == 'group': |
| 122 |
| 123 # Groups must not be nested. |
| 124 if not may_contain_groups: |
| 125 self._Error('Policy groups must not be nested.', 'policy', policy) |
| 126 |
| 127 # Each policy group must have a list of policies. |
| 128 policies = self._CheckContains(policy, 'policies', list) |
| 129 if policies is not None: |
| 130 for nested_policy in policies: |
| 131 self._CheckPolicy(nested_policy, False) |
| 132 |
| 133 # Statistics. |
| 134 self.num_groups += 1 |
| 135 else: # policy_type != group |
| 136 |
| 137 # Each policy must have a supported_on list. |
| 138 supported_on = self._CheckContains(policy, 'supported_on', list) |
| 139 if supported_on is not None: |
| 140 for s in supported_on: |
| 141 if not isinstance(s, str): |
| 142 self._Error('Entries in "supported_on" must be strings.', 'policy', |
| 143 policy, supported_on) |
| 144 |
| 145 # Each policy must have a 'features' dict. |
| 146 self._CheckContains(policy, 'features', dict) |
| 147 |
| 148 # Each policy must have an 'example_value' of appropriate type. |
| 149 if policy_type == 'main': |
| 150 value_type = bool |
| 151 elif policy_type in ('string', 'string-enum'): |
| 152 value_type = str |
| 153 elif policy_type in ('int', 'int-enum'): |
| 154 value_type = int |
| 155 elif policy_type == 'list': |
| 156 value_type = list |
| 157 else: |
| 158 raise NotImplementedError('Unimplemented policy type: %s' % policy_type) |
| 159 self._CheckContains(policy, 'example_value', value_type) |
| 160 |
| 161 # Statistics. |
| 162 self.num_policies += 1 |
| 163 if not may_contain_groups: |
| 164 self.num_policies_in_groups += 1 |
| 165 |
| 166 if policy_type in ('int-enum', 'string-enum'): |
| 167 |
| 168 # Enums must contain a list of items. |
| 169 items = self._CheckContains(policy, 'items', list) |
| 170 if items is not None: |
| 171 if len(items) < 1: |
| 172 self._Error('"items" must not be empty.', 'policy', policy, items) |
| 173 for item in items: |
| 174 |
| 175 # Each item must have a name. |
| 176 # Note: |policy.get('name')| is used instead of |policy['name']| |
| 177 # because it returns None rather than failing when no key called |
| 178 # 'name' exists. |
| 179 self._CheckContains(item, 'name', str, container_name='item', |
| 180 identifier=policy.get('name')) |
| 181 |
| 182 # Each item must have a value of the correct type. |
| 183 self._CheckContains(item, 'value', value_type, container_name='item', |
| 184 identifier=policy.get('name')) |
| 185 |
| 186 # Each item must have a caption. |
| 187 self._CheckContains(item, 'caption', str, container_name='item', |
| 188 identifier=policy.get('name')) |
| 189 |
| 190 def _CheckMessage(self, key, value): |
| 191 # |key| must be a string, |value| a dict. |
| 192 if not isinstance(key, str): |
| 193 self._Error('Each message key must be a string.', 'message', key, key) |
| 194 return |
| 195 |
| 196 if not isinstance(value, dict): |
| 197 self._Error('Each message must be a dictionary.', 'message', key, value) |
| 198 return |
| 199 |
| 200 # Each message must have a desc. |
| 201 self._CheckContains(value, 'desc', str, parent_element='message', |
| 202 identifier=key) |
| 203 |
| 204 # Each message must have a text. |
| 205 self._CheckContains(value, 'text', str, parent_element='message', |
| 206 identifier=key) |
| 207 |
| 208 # There should not be any unknown keys in |value|. |
| 209 for vkey in value: |
| 210 if vkey not in ('desc', 'text'): |
| 211 self.warning_count += 1 |
| 212 print 'In message %s: Warning: Unknown key: %s' % (key, vkey) |
| 213 |
| 214 def _CheckPlaceholder(self, placeholder): |
| 215 if not isinstance(placeholder, dict): |
| 216 self._Error('Each placeholder must be a dictionary.', |
| 217 'placeholder', None, placeholder) |
| 218 return |
| 219 |
| 220 # Each placeholder must have a 'key'. |
| 221 key = self._CheckContains(placeholder, 'key', str, |
| 222 parent_element='placeholder') |
| 223 |
| 224 # Each placeholder must have a 'value'. |
| 225 self._CheckContains(placeholder, 'value', str, parent_element='placeholder', |
| 226 identifier=key) |
| 227 |
| 228 # There should not be any unknown keys in |placeholder|. |
| 229 for k in placeholder: |
| 230 if k not in ('key', 'value'): |
| 231 self.warning_count += 1 |
| 232 name = str(placeholder.get('key'), placeholder) |
| 233 print 'In placeholder %s: Warning: Unknown key: %s' % (name, k) |
| 234 |
| 235 def _LeadingWhitespace(self, line): |
| 236 match = LEADING_WHITESPACE.match(line) |
| 237 if match: |
| 238 return match.group(1) |
| 239 return '' |
| 240 |
| 241 def _TrailingWhitespace(self, line): |
| 242 match = TRAILING_WHITESPACE.match(line) |
| 243 if match: |
| 244 return match.group(1) |
| 245 return '' |
| 246 |
| 247 def _LineError(self, message, line_number): |
| 248 self.error_count += 1 |
| 249 print 'In line %d: Error: %s' % (line_number, message) |
| 250 |
| 251 def _LineWarning(self, message, line_number): |
| 252 self.warning_count += 1 |
| 253 print ('In line %d: Warning: Automatically fixing formatting: %s' |
| 254 % (line_number, message)) |
| 255 |
| 256 def _CheckFormat(self, filename): |
| 257 if self.options.fix: |
| 258 fixed_lines = [] |
| 259 with open(filename) as f: |
| 260 indent = 0 |
| 261 line_number = 0 |
| 262 for line in f: |
| 263 line_number += 1 |
| 264 line = line.rstrip('\n') |
| 265 # Check for trailing whitespace. |
| 266 trailing_whitespace = self._TrailingWhitespace(line) |
| 267 if len(trailing_whitespace) > 0: |
| 268 if self.options.fix: |
| 269 line = line.rstrip() |
| 270 self._LineWarning('Trailing whitespace.', line_number) |
| 271 else: |
| 272 self._LineError('Trailing whitespace.', line_number) |
| 273 if len(line) == 0: |
| 274 if self.options.fix: |
| 275 fixed_lines += ['\n'] |
| 276 continue |
| 277 if len(line) == len(trailing_whitespace): |
| 278 continue |
| 279 # Check for correct amount of leading whitespace. |
| 280 leading_whitespace = self._LeadingWhitespace(line) |
| 281 if leading_whitespace.count('\t') > 0: |
| 282 if self.options.fix: |
| 283 line = leading_whitespace.replace('\t', ' ') + line.lstrip() |
| 284 self._LineWarning('Tab character found.', line_number) |
| 285 else: |
| 286 self._LineError('Tab character found.', line_number) |
| 287 if line[len(leading_whitespace)] in (']', '}'): |
| 288 indent -= 2 |
| 289 if line[0] != '#': # Ignore 0-indented comments. |
| 290 if len(leading_whitespace) != indent: |
| 291 if self.options.fix: |
| 292 line = ' ' * indent + line.lstrip() |
| 293 self._LineWarning('Indentation should be ' + str(indent) + |
| 294 ' spaces.', line_number) |
| 295 else: |
| 296 self._LineError('Bad indentation. Should be ' + str(indent) + |
| 297 ' spaces.', line_number) |
| 298 if line[-1] in ('[', '{'): |
| 299 indent += 2 |
| 300 if self.options.fix: |
| 301 fixed_lines.append(line + '\n') |
| 302 |
| 303 # If --fix is specified: backup the file (deleting any existing backup), |
| 304 # then write the fixed version with the old filename. |
| 305 if self.options.fix: |
| 306 if self.options.backup: |
| 307 backupfilename = filename + '.bak' |
| 308 if os.path.exists(backupfilename): |
| 309 os.remove(backupfilename) |
| 310 os.rename(filename, backupfilename) |
| 311 with open(filename, 'w') as f: |
| 312 f.writelines(fixed_lines) |
| 313 |
| 314 def Main(self, filename, options): |
| 315 try: |
| 316 with open(filename) as f: |
| 317 data = eval(f.read()) |
| 318 except: |
| 319 import traceback |
| 320 traceback.print_exc(file=sys.stdout) |
| 321 self._Error('Invalid JSON syntax.') |
| 322 return |
| 323 if data == None: |
| 324 self._Error('Invalid JSON syntax.') |
| 325 return |
| 326 self.options = options |
| 327 |
| 328 # First part: check JSON structure. |
| 329 |
| 330 # Check policy definitions. |
| 331 policy_definitions = self._CheckContains(data, 'policy_definitions', list, |
| 332 parent_element=None, |
| 333 container_name='The root element', |
| 334 offending=None) |
| 335 if policy_definitions is not None: |
| 336 for policy in policy_definitions: |
| 337 self._CheckPolicy(policy, True) |
| 338 |
| 339 # Check (non-policy-specific) message definitions. |
| 340 messages = self._CheckContains(data, 'messages', dict, |
| 341 parent_element=None, |
| 342 container_name='The root element', |
| 343 offending=None) |
| 344 if messages is not None: |
| 345 for message in messages: |
| 346 self._CheckMessage(message, messages[message]) |
| 347 |
| 348 # Check placeholders. |
| 349 placeholders = self._CheckContains(data, 'placeholders', list, |
| 350 parent_element=None, |
| 351 container_name='The root element', |
| 352 offending=None) |
| 353 if placeholders is not None: |
| 354 for placeholder in placeholders: |
| 355 self._CheckPlaceholder(placeholder) |
| 356 |
| 357 # Second part: check formatting. |
| 358 self._CheckFormat(filename) |
| 359 |
| 360 # Third part: summary and exit. |
| 361 print ('Finished. %d errors, %d warnings.' % |
| 362 (self.error_count, self.warning_count)) |
| 363 if self.options.stats: |
| 364 if self.num_groups > 0: |
| 365 print ('%d policies, %d of those in %d groups (containing on ' |
| 366 'average %.1f policies).' % |
| 367 (self.num_policies, self.num_policies_in_groups, self.num_groups, |
| 368 (1.0 * self.num_policies_in_groups / self.num_groups))) |
| 369 else: |
| 370 print self.num_policies, 'policies, 0 policy groups.' |
| 371 if self.error_count > 0: |
| 372 return 1 |
| 373 return 0 |
| 374 |
| 375 def Run(self, argv, filename=None): |
| 376 parser = optparse.OptionParser( |
| 377 usage='usage: %prog [options] filename', |
| 378 description='Syntax check a policy_templates.json file.') |
| 379 parser.add_option('--fix', action='store_true', |
| 380 help='Automatically fix formatting.') |
| 381 parser.add_option('--backup', action='store_true', |
| 382 help='Create backup of original file (before fixing).') |
| 383 parser.add_option('--stats', action='store_true', |
| 384 help='Generate statistics.') |
| 385 (options, args) = parser.parse_args(argv) |
| 386 if filename is None: |
| 387 if len(args) != 2: |
| 388 parser.print_help() |
| 389 sys.exit(1) |
| 390 filename = args[1] |
| 391 return self.Main(filename, options) |
| 392 |
| 393 |
| 394 if __name__ == '__main__': |
| 395 checker = PolicyTemplateChecker() |
| 396 sys.exit(checker.Run(sys.argv)) |
OLD | NEW |