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) | |
gfeher
2011/01/18 10:04:07
Nit: indentation. Please align these two lines to
Jakob Kummerow
2011/01/18 10:38:36
Done.
| |
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) | |
gfeher
2011/01/18 10:04:07
Nit:indentation.
Jakob Kummerow
2011/01/18 10:38:36
Done.
| |
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 self._CheckContains(item, 'name', str, container_name='item', | |
177 identifier=policy.get('name')) | |
gfeher
2011/01/18 10:04:07
Nit: policy['name']
Jakob Kummerow
2011/01/18 10:38:36
This (as well as the other two occurrences) is act
| |
178 | |
179 # Each item must have a value of the correct type. | |
180 self._CheckContains(item, 'value', value_type, container_name='item', | |
181 identifier=policy.get('name')) | |
gfeher
2011/01/18 10:04:07
Nit: policy['name']
| |
182 | |
183 # Each item must have a caption. | |
184 self._CheckContains(item, 'caption', str, container_name='item', | |
185 identifier=policy.get('name')) | |
gfeher
2011/01/18 10:04:07
Nit: policy['name']
| |
186 | |
187 def _CheckMessage(self, key, value): | |
188 # |key| must be a string, |value| a dict. | |
189 if not isinstance(key, str): | |
190 self._Error('Each message key must be a string.', 'message', key, key) | |
191 return | |
192 | |
193 if not isinstance(value, dict): | |
194 self._Error('Each message must be a dictionary.', 'message', key, value) | |
195 return | |
196 | |
197 # Each message must have a desc. | |
198 self._CheckContains(value, 'desc', str, parent_element='message', | |
199 identifier=key) | |
200 | |
201 # Each message must have a text. | |
202 self._CheckContains(value, 'text', str, parent_element='message', | |
203 identifier=key) | |
204 | |
205 # There should not be any unknown keys in |value|. | |
206 for vkey in value: | |
207 if vkey not in ('desc', 'text'): | |
208 self.warning_count += 1 | |
209 print 'In message %s: Warning: Unknown key: %s' % (key, vkey) | |
210 | |
211 def _CheckPlaceholder(self, placeholder): | |
212 if not isinstance(placeholder, dict): | |
213 self._Error('Each placeholder must be a dictionary.', | |
214 'placeholder', None, placeholder) | |
215 return | |
216 | |
217 # Each placeholder must have a 'key'. | |
218 key = self._CheckContains(placeholder, 'key', str, | |
219 parent_element='placeholder') | |
220 | |
221 # Each placeholder must have a 'value'. | |
222 self._CheckContains(placeholder, 'value', str, parent_element='placeholder', | |
223 identifier=key) | |
224 | |
225 # There should not be any unknown keys in |placeholder|. | |
226 for k in placeholder: | |
227 if k not in ('key', 'value'): | |
228 self.warning_count += 1 | |
229 name = str(placeholder.get('key'), placeholder) | |
230 print 'In placeholder %s: Warning: Unknown key: %s' % (name, k) | |
231 | |
232 def _LeadingWhitespace(self, line): | |
233 match = LEADING_WHITESPACE.match(line) | |
234 if match: | |
235 return match.group(1) | |
236 return '' | |
237 | |
238 def _TrailingWhitespace(self, line): | |
239 match = TRAILING_WHITESPACE.match(line) | |
240 if match: | |
241 return match.group(1) | |
242 return '' | |
243 | |
244 def _LineError(self, message, line_number): | |
245 self.error_count += 1 | |
246 print 'In line %d: Error: %s' % (line_number, message) | |
247 | |
248 def _LineWarning(self, message, line_number): | |
249 self.warning_count += 1 | |
250 print ('In line %d: Warning: Automatically fixing formatting: %s' | |
251 % (line_number, message)) | |
252 | |
253 def _CheckFormat(self, filename): | |
254 if self.options.fix: | |
255 fixed_lines = [] | |
256 with open(filename) as f: | |
257 indent = 0 | |
258 line_number = 0 | |
259 for line in f: | |
260 line_number += 1 | |
261 line = line.rstrip('\n') | |
262 # Check for trailing whitespace. | |
263 trailing_whitespace = self._TrailingWhitespace(line) | |
264 if len(trailing_whitespace) > 0: | |
265 if self.options.fix: | |
266 line = line.rstrip() | |
267 self._LineWarning('Trailing whitespace.', line_number) | |
268 else: | |
269 self._LineError('Trailing whitespace.', line_number) | |
270 if len(line) == 0: | |
271 if self.options.fix: | |
272 fixed_lines += ['\n'] | |
273 continue | |
274 if len(line) == len(trailing_whitespace): | |
275 continue | |
276 # Check for correct amount of leading whitespace. | |
277 leading_whitespace = self._LeadingWhitespace(line) | |
278 if leading_whitespace.count('\t') > 0: | |
279 if self.options.fix: | |
280 line = leading_whitespace.replace('\t', ' ') + line.lstrip() | |
281 self._LineWarning('Tab character found.', line_number) | |
282 else: | |
283 self._LineError('Tab character found.', line_number) | |
284 if line[len(leading_whitespace)] in (']', '}'): | |
285 indent -= 2 | |
286 if line[0] != '#': # Ignore 0-indented comments. | |
287 if len(leading_whitespace) != indent: | |
288 if self.options.fix: | |
289 line = ' ' * indent + line.lstrip() | |
290 self._LineWarning('Indentation should be ' + str(indent) + | |
291 ' spaces.', line_number) | |
292 else: | |
293 self._LineError('Bad indentation. Should be ' + str(indent) + | |
294 ' spaces.', line_number) | |
295 if line[-1] in ('[', '{'): | |
296 indent += 2 | |
297 if self.options.fix: | |
298 fixed_lines.append(line + '\n') | |
299 | |
300 # If --fix is specified: backup the file (deleting any existing backup), | |
301 # then write the fixed version with the old filename. | |
302 if self.options.fix: | |
303 if self.options.backup: | |
304 backupfilename = filename + '.bak' | |
305 if os.path.exists(backupfilename): | |
306 os.remove(backupfilename) | |
307 os.rename(filename, backupfilename) | |
308 with open(filename, 'w') as f: | |
309 f.writelines(fixed_lines) | |
310 | |
311 def Main(self, filename, options): | |
312 try: | |
313 with open(filename) as f: | |
314 data = eval(f.read()) | |
315 except: | |
316 import traceback | |
317 traceback.print_exc(file=sys.stdout) | |
318 self._Error('Invalid JSON syntax.') | |
319 return | |
320 if data == None: | |
321 self._Error('Invalid JSON syntax.') | |
322 return | |
323 self.options = options | |
324 | |
325 # First part: check JSON structure. | |
326 | |
327 # Check policy definitions. | |
328 policy_definitions = self._CheckContains(data, 'policy_definitions', list, | |
329 parent_element=None, | |
330 container_name='The root element', | |
331 offending=None) | |
332 if policy_definitions is not None: | |
333 for policy in policy_definitions: | |
334 self._CheckPolicy(policy, True) | |
335 | |
336 # Check (non-policy-specific) message definitions. | |
337 messages = self._CheckContains(data, 'messages', dict, | |
338 parent_element=None, | |
339 container_name='The root element', | |
340 offending=None) | |
341 if messages is not None: | |
342 for message in messages: | |
343 self._CheckMessage(message, messages[message]) | |
344 | |
345 # Check placeholders. | |
346 placeholders = self._CheckContains(data, 'placeholders', list, | |
347 parent_element=None, | |
348 container_name='The root element', | |
349 offending=None) | |
350 if placeholders is not None: | |
351 for placeholder in placeholders: | |
352 self._CheckPlaceholder(placeholder) | |
353 | |
354 # Second part: check formatting. | |
355 self._CheckFormat(filename) | |
356 | |
357 # Third part: summary and exit. | |
358 print ('Finished. %d errors, %d warnings.' % | |
359 (self.error_count, self.warning_count)) | |
360 if self.options.stats: | |
361 if self.num_groups > 0: | |
362 print ('%d policies, %d of those in %d groups (containing on ' | |
363 'average %.1f policies).' % | |
364 (self.num_policies, self.num_policies_in_groups, self.num_groups, | |
365 (1.0 * self.num_policies_in_groups / self.num_groups))) | |
366 else: | |
367 print self.num_policies, 'policies, 0 policy groups.' | |
368 if self.error_count > 0: | |
369 return 1 | |
370 return 0 | |
371 | |
372 def Run(self, argv, filename=None): | |
373 parser = optparse.OptionParser( | |
374 usage='usage: %prog [options] filename', | |
375 description='Syntax check a policy_templates.json file.') | |
376 parser.add_option('--fix', action='store_true', | |
377 help='Automatically fix formatting.') | |
378 parser.add_option('--backup', action='store_true', | |
379 help='Create backup of original file (before fixing).') | |
380 parser.add_option('--stats', action='store_true', | |
381 help='Generate statistics.') | |
382 (options, args) = parser.parse_args(argv) | |
383 if filename is None: | |
384 if len(args) != 2: | |
385 parser.print_help() | |
386 sys.exit(1) | |
387 filename = args[1] | |
388 return self.Main(filename, options) | |
389 | |
390 | |
391 if __name__ == '__main__': | |
392 checker = PolicyTemplateChecker() | |
393 sys.exit(checker.Run(sys.argv)) | |
OLD | NEW |