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

Side by Side Diff: tools/json_schema_compiler/feature_compiler.py

Issue 2151583003: [Extensions] Add extension feature generation code (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Include fix Created 4 years, 5 months 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
OLDNEW
(Empty)
1 # Copyright 2016 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
4
5 import argparse
6 import copy
7 from datetime import datetime
8 import os
9
10 from code import Code
11 import json_parse
12
13 # The template for the header file of the generated FeatureProvider.
14 HEADER_FILE_TEMPLATE = """
15 // Copyright %(year)s The Chromium Authors. All rights reserved.
16 // Use of this source code is governed by a BSD-style license that can be
17 // found in the LICENSE file.
18
19 // GENERATED FROM THE FEATURES FILE:
20 // %(source_files)s
21 // DO NOT EDIT.
22
23 #ifndef %(header_guard)s
24 #define %(header_guard)s
25
26 #include "extensions/common/features/base_feature_provider.h"
27
28 namespace extensions {
29
30 class %(provider_class)s : public BaseFeatureProvider {
31 public:
32 %(provider_class)s();
33 ~%(provider_class)s() override;
34
35 private:
36 DISALLOW_COPY_AND_ASSIGN(%(provider_class)s);
37 };
38
39 } // namespace extensions
40
41 #endif // %(header_guard)s
42 """
43
44 # The beginning of the .cc file for the generated FeatureProvider.
45 CC_FILE_BEGIN = """
46 // Copyright %(year)s The Chromium Authors. All rights reserved.
47 // Use of this source code is governed by a BSD-style license that can be
48 // found in the LICENSE file.
49
50 // GENERATED FROM THE FEATURES FILE:
51 // %(source_files)s
52 // DO NOT EDIT.
53
54 #include "%(header_file_path)s"
55
56 #include "extensions/common/features/api_feature.h"
57 #include "extensions/common/features/complex_feature.h"
58
59 namespace extensions {
60
61 """
62
63 # The end of the .cc file for the generated FeatureProvider.
64 CC_FILE_END = """
65 %(provider_class)s::~%(provider_class)s() {}
66
67 } // namespace extensions
68 """
69
70 # A "grammar" for what is and isn't allowed in the features.json files. This
71 # grammar has to list all possible keys and the requirements for each. The
72 # format of each entry is:
73 # 'key': {
74 # allowed_type_1: optional_properties,
75 # allowed_type_2: optional_properties,
76 # }
77 # |allowed_types| are the types of values that can be used for a given key. The
78 # possible values are list, unicode, bool, and int.
79 # |optional_properties| provide more restrictions on the given type. The options
80 # are:
81 # 'subtype': Only applicable for lists. If provided, this enforces that each
82 # entry in the list is of the specified type.
83 # 'enum_map': A map of strings to C++ enums. When the compiler sees the given
84 # enum string, it will replace it with the C++ version in the
85 # compiled code. For instance, if a feature specifies
86 # 'channel': 'stable', the generated C++ will assign
87 # version_info::Channel::STABLE to channel. The keys in this map
88 # also serve as a list all of possible values.
89 # 'allow_all': Only applicable for lists. If present, this will check for
90 # a value of "all" for a list value, and will replace it with
91 # the collection of all possible values. For instance, if a
92 # feature specifies 'contexts': 'all', the generated C++ will
93 # assign the list of Feature::BLESSED_EXTENSION_CONTEXT,
94 # Feature::BLESSED_WEB_PAGE_CONTEXT et al for contexts. If not
95 # specified, defaults to false.
96 # 'values': A list of all possible allowed values for a given key.
97 # If a type definition does not have any restrictions (beyond the type itself),
98 # an empty definition ({}) is used.
99 FEATURE_GRAMMAR = (
100 {
101 'blacklist': {
102 list: {'subtype': unicode}
103 },
104 'channel': {
105 unicode: {
106 'enum_map': {
107 'trunk': 'version_info::Channel::UNKNOWN',
108 'canary': 'version_info::Channel::CANARY',
109 'dev': 'version_info::Channel::DEV',
110 'beta': 'version_info::Channel::BETA',
111 'stable': 'version_info::Channel::STABLE',
112 }
113 }
114 },
115 'command_line_switch': {
116 unicode: {}
117 },
118 'component_extensions_auto_granted': {
119 bool: {}
120 },
121 'contexts': {
122 list: {
123 'enum_map': {
124 'blessed_extension': 'Feature::BLESSED_EXTENSION_CONTEXT',
125 'blessed_web_page': 'Feature::BLESSED_WEB_PAGE_CONTEXT',
126 'content_script': 'Feature::CONTENT_SCRIPT_CONTEXT',
127 'extension_service_worker': 'Feature::SERVICE_WORKER_CONTEXT',
128 'web_page': 'Feature::WEB_PAGE_CONTEXT',
129 'webui': 'Feature::WEBUI_CONTEXT',
130 'unblessed_extension': 'Feature::UNBLESSED_EXTENSION_CONTEXT',
131 },
132 'allow_all': True
133 },
134 },
135 'default_parent': {
136 bool: {'values': [True]}
137 },
138 'dependencies': {
139 list: {'subtype': unicode}
140 },
141 'extension_types': {
142 list: {
143 'enum_map': {
144 'extension': 'Manifest::TYPE_EXTENSION',
145 'hosted_app': 'Manifest::TYPE_HOSTED_APP',
146 'legacy_packaged_app': 'Manifest::TYPE_LEGACY_PACKAGED_APP',
147 'platform_app': 'Manifest::TYPE_PLATFORM_APP',
148 'shared_module': 'Manifest::TYPE_SHARED_MODULE',
149 'theme': 'Manifest::TYPE_THEME',
150 },
151 'allow_all': True
152 },
153 },
154 'location': {
155 unicode: {
156 'enum_map': {
157 'component': 'SimpleFeature::COMPONENT_LOCATION',
158 'external_component': 'SimpleFeature::EXTERNAL_COMPONENT_LOCATION',
159 'policy': 'SimpleFeature::POLICY_LOCATION',
160 }
161 }
162 },
163 'internal': {
164 bool: {'values': [True]}
165 },
166 'matches': {
167 list: {'subtype': unicode}
168 },
169 'max_manifest_version': {
170 int: {'values': [1]}
171 },
172 'min_manifest_version': {
173 int: {'values': [2]}
174 },
175 'noparent': {
176 bool: {'values': [True]}
177 },
178 'platforms': {
179 list: {
180 'enum_map': {
181 'chromeos': 'Feature::CHROMEOS_PLATFORM',
182 'linux': 'Feature::LINUX_PLATFORM',
183 'mac': 'Feature::MACOSX_PLATFORM',
184 'win': 'Feature::WIN_PLATFORM',
185 }
186 }
187 },
188 'whitelist': {
189 list: {'subtype': unicode}
190 },
191 })
192
193 # These keys are used to find the parents of different features, but are not
194 # compiled into the features themselves.
195 IGNORED_KEYS = ['noparent', 'default_parent']
196
197 # By default, if an error is encountered, assert to stop the compilation. This
198 # can be disabled for testing.
199 ENABLE_ASSERTIONS = True
200
201 # JSON parsing returns all strings of characters as unicode types. For testing,
202 # we can enable converting all string types to unicode to avoid writing u''
203 # everywhere.
204 STRINGS_TO_UNICODE = False
205
206 class Feature(object):
207 """A representation of a single simple feature that can handle all parsing,
208 validation, and code generation.
209 """
210 def __init__(self, name):
211 self.name = name
212 self.has_parent = False
213 self.errors = []
214 self.feature_values = {}
215
216 def _GetType(self, value):
217 """Returns the type of the given value. This can be different than type() if
218 STRINGS_TO_UNICODE is enabled.
219 """
220 t = type(value)
221 if not STRINGS_TO_UNICODE:
222 return t
223 if t is str:
224 return unicode
225 return t
226
227 def _AddError(self, key, error):
228 """Adds an error to the feature. If ENABLE_ASSERTIONS is active, this will
229 also assert to stop the compilation process (since errors should never be
230 found in production).
231 """
232 error = 'Error parsing feature "%s" at key "%s": %s' % (
233 self.name, key, error)
234 self.errors.append(error)
235 if ENABLE_ASSERTIONS:
236 assert False, error
237
238 def _GetCheckedValue(self, key, expected_type, expected_values,
239 enum_map, value):
240 """Returns a string to be used in the generated C++ code for a given key's
241 python value, or None if the value is invalid. For example, if the python
242 value is True, this returns 'true', for a string foo, this returns "foo",
243 and for an enum, this looks up the C++ definition in the enum map.
244 key: The key being parsed.
245 expected_type: The expected type for this value, or None if any type is
246 allowed.
247 expected_values: The list of allowed values for this value, or None if any
248 value is allowed.
249 enum_map: The map from python value -> cpp value for all allowed values,
250 or None if no special mapping should be made.
251 value: The value to check.
252 """
253 valid = True
254 if expected_values and value not in expected_values:
255 self._AddError(key, 'Illegal value: "%s"' % value)
256 valid = False
257
258 t = self._GetType(value)
259 if expected_type and t is not expected_type:
260 self._AddError(key, 'Illegal value: "%s"' % value)
261 valid = False
262
263 if not valid:
264 return None
265
266 if enum_map:
267 return enum_map[value]
268
269 if t in [str, unicode]:
270 return '"%s"' % str(value)
271 if t is int:
272 return str(value)
273 if t is bool:
274 return 'true' if value else 'false'
275 assert False, 'Unsupported type: %s' % value
276
277 def _ParseKey(self, key, value, grammar):
278 """Parses the specific key according to the grammar rule for that key if it
279 is present in the json value.
280 key: The key to parse.
281 value: The full value for this feature.
282 grammar: The rule for the specific key.
283 """
284 if key not in value:
285 return
286 v = value[key]
287
288 is_all = False
289 if v == 'all' and list in grammar and 'allow_all' in grammar[list]:
290 v = []
291 is_all = True
292
293 value_type = self._GetType(v)
294 if value_type not in grammar:
295 self._AddError(key, 'Illegal value: "%s"' % v)
296 return
297
298 expected = grammar[value_type]
299 expected_values = None
300 enum_map = None
301 if 'values' in expected:
302 expected_values = expected['values']
303 elif 'enum_map' in expected:
304 enum_map = expected['enum_map']
305 expected_values = enum_map.keys()
306
307 if is_all:
308 v = copy.deepcopy(expected_values)
309
310 expected_type = None
311 if value_type is list and 'subtype' in expected:
312 expected_type = expected['subtype']
313
314 cpp_value = None
315 # If this value is a list, iterate over each entry and validate. Otherwise,
316 # validate the single value.
317 if value_type is list:
318 cpp_value = []
319 for sub_value in v:
320 cpp_sub_value = self._GetCheckedValue(key, expected_type,
321 expected_values, enum_map,
322 sub_value)
323 if cpp_sub_value:
324 cpp_value.append(cpp_sub_value)
325 if cpp_value:
326 cpp_value = '{' + ','.join(cpp_value) + '}'
327 else:
328 cpp_value = self._GetCheckedValue(key, expected_type, expected_values,
329 enum_map, v)
330
331 if cpp_value:
332 self.feature_values[key] = cpp_value
333
334 def SetParent(self, parent):
335 """Sets the parent of this feature, and inherits all properties from that
336 parent.
337 """
338 assert not self.feature_values, 'Parents must be set before parsing'
339 self.feature_values = copy.deepcopy(parent.feature_values)
340 self.has_parent = True
341
342 def Parse(self, parsed_json):
343 """Parses the feature from the given json value."""
344 for key in parsed_json.keys():
345 if key not in FEATURE_GRAMMAR:
346 self._AddError(key, 'Unrecognized key')
347 for key, key_grammar in FEATURE_GRAMMAR.iteritems():
348 self._ParseKey(key, parsed_json, key_grammar)
349
350 def GetCode(self, feature_class):
351 """Returns the Code object for generating this feature."""
352 c = Code()
353 c.Append('std::unique_ptr<%s> feature(new %s());' %
354 (feature_class, feature_class))
355 c.Append('feature->set_name("%s");' % self.name)
356 for key in sorted(self.feature_values.keys()):
357 if key in IGNORED_KEYS:
358 continue;
359 c.Append('feature->set_%s(%s);' % (key, self.feature_values[key]))
360 return c
361
362 class FeatureCompiler(object):
363 """A compiler to load, parse, and generate C++ code for a number of
364 features.json files."""
365 def __init__(self, chrome_root, source_files, feature_class,
366 provider_class, out_root, out_base_filename):
367 # See __main__'s ArgumentParser for documentation on these properties.
368 self._chrome_root = chrome_root
369 self._source_files = source_files
370 self._feature_class = feature_class
371 self._provider_class = provider_class
372 self._out_root = out_root
373 self._out_base_filename = out_base_filename
374
375 # The json value for the feature files.
376 self._json = {}
377 # The parsed features.
378 self._features = {}
379
380 def _Load(self):
381 """Loads and parses the source from each input file and puts the result in
382 self._json."""
383 for f in self._source_files:
384 abs_source_file = os.path.join(self._chrome_root, f)
385 try:
386 with open(abs_source_file, 'r') as f:
387 f_json = json_parse.Parse(f.read())
388 except:
389 print('FAILED: Exception encountered while loading "%s"' %
390 abs_source_file)
391 raise
392 dupes = set(f_json) & set(self._json)
393 assert not dupes, 'Duplicate keys found: %s' % list(dupes)
394 self._json.update(f_json)
395
396 def _FindParent(self, feature_name, feature_value):
397 """Checks to see if a feature has a parent. If it does, returns the
398 parent."""
399 sep = feature_name.rfind('.')
400 if sep is -1 or 'noparent' in feature_value:
401 return None
402 parent_name = feature_name[:sep]
403 if parent_name not in self._features:
404 # TODO(devlin): It'd be kind of nice to be able to assert that the
405 # deduced parent name is in our features, but some dotted features don't
406 # have parents and also don't have noparent, e.g. system.cpu. We should
407 # probably just noparent them so that we can assert this.
408 # raise KeyError('Could not find parent "%s" for feature "%s".' %
409 # (parent_name, feature_name))
410 return None
411 parent_value = self._features[parent_name]
412 parent = parent_value
413 if type(parent_value) is list:
414 for p in parent_value:
415 if 'default_parent' in p.feature_values:
416 parent = p
417 break
418 assert parent, 'No default parent found for %s' % parent_name
419 return parent
420
421 def _CompileFeature(self, feature_name, feature_value):
422 """Parses a single feature."""
423 if 'nocompile' in feature_value:
424 assert feature_value['nocompile'], (
425 'nocompile should only be true; otherwise omit this key.')
426 return
427 parent = self._FindParent(feature_name, feature_value)
428 # Handle complex features, which are lists of simple features.
429 if type(feature_value) is list:
430 feature_list = []
431 # This doesn't handle nested complex features. I think that's probably for
432 # the best.
433 for v in feature_value:
434 feature = Feature(feature_name)
435 if parent:
436 feature.SetParent(parent)
437 feature.Parse(v)
438 feature_list.append(feature)
439 self._features[feature_name] = feature_list
440 return
441
442 feature = Feature(feature_name)
443 if parent:
444 feature.SetParent(parent)
445 feature.Parse(feature_value)
446 self._features[feature_name] = feature
447
448 def Compile(self):
449 """Parses all features after loading the input files."""
450 self._Load();
451 # Iterate over in sorted order so that parents come first.
452 for k in sorted(self._json.keys()):
453 self._CompileFeature(k, self._json[k])
454
455 def Render(self):
456 """Returns the Code object for the body of the .cc file, which handles the
457 initialization of all features."""
458 c = Code()
459 c.Append('%s::%s() {' % (self._provider_class, self._provider_class))
460 c.Sblock()
461 for k in sorted(self._features.keys()):
462 c.Sblock('{')
463 feature = self._features[k]
464 if type(feature) is list:
465 c.Append('std::unique_ptr<ComplexFeature::FeatureList> features(')
466 c.Append(' new ComplexFeature::FeatureList());')
467 for f in feature:
468 c.Sblock('{')
469 c.Concat(f.GetCode(self._feature_class))
470 c.Append('features->push_back(std::move(feature));')
471 c.Eblock('}')
472 c.Append('std::unique_ptr<ComplexFeature> feature(')
473 c.Append(' new ComplexFeature(std::move(features)));')
474 c.Append('feature->set_name("%s");' % k)
475 else:
476 c.Concat(feature.GetCode(self._feature_class))
477 c.Append('features_["%s"] = std::move(feature);' % k)
478 c.Eblock('}')
479 c.Eblock('}')
480 return c
481
482 def Write(self):
483 """Writes the output."""
484 header_file_path = self._out_base_filename + '.h'
485 cc_file_path = self._out_base_filename + '.cc'
486 substitutions = ({
487 'header_file_path': header_file_path,
488 'header_guard': (header_file_path.replace('/', '_').
489 replace('.', '_').upper()),
490 'provider_class': self._provider_class,
491 'source_files': str(self._source_files),
492 'year': str(datetime.now().year)
493 })
494 if not os.path.exists(self._out_root):
495 os.makedirs(self._out_root)
496 # Write the .h file.
497 with open(os.path.join(self._out_root, header_file_path), 'w') as f:
498 header_file = Code()
499 header_file.Append(HEADER_FILE_TEMPLATE)
500 header_file.Substitute(substitutions)
501 f.write(header_file.Render().strip())
502 # Write the .cc file.
503 with open(os.path.join(self._out_root, cc_file_path), 'w') as f:
504 cc_file = Code()
505 cc_file.Append(CC_FILE_BEGIN)
506 cc_file.Substitute(substitutions)
507 cc_file.Concat(self.Render())
508 cc_end = Code()
509 cc_end.Append(CC_FILE_END)
510 cc_end.Substitute(substitutions)
511 cc_file.Concat(cc_end)
512 f.write(cc_file.Render().strip())
513
514 if __name__ == '__main__':
515 parser = argparse.ArgumentParser(description='Compile json feature files')
516 parser.add_argument('chrome_root', type=str,
517 help='The root directory of the chrome checkout')
518 parser.add_argument(
519 'feature_class', type=str,
520 help='The name of the class to use in feature generation ' +
521 '(e.g. APIFeature, PermissionFeature)')
522 parser.add_argument('provider_class', type=str,
523 help='The name of the class for the feature provider')
524 parser.add_argument('out_root', type=str,
525 help='The root directory to generate the C++ files into')
526 parser.add_argument(
527 'out_base_filename', type=str,
528 help='The base filename for the C++ files (.h and .cc will be appended)')
529 parser.add_argument('source_files', type=str, nargs='+',
530 help='The source features.json files')
531 args = parser.parse_args()
532 c = FeatureCompiler(args.chrome_root, args.source_files, args.feature_class,
533 args.provider_class, args.out_root,
534 args.out_base_filename)
535 c.Compile()
536 c.Write()
OLDNEW
« no previous file with comments | « extensions/common/features/simple_feature_unittest.cc ('k') | tools/json_schema_compiler/feature_compiler_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698