0

Enable python formatting in //tools/json_schema_compiler/

Python formatting using `git cl format` has been enabled in the codebase
for a while now by including a .style.yapf file in your folder
hierarchy. However back in 2020 an exception was added to ignore the
json schema compiler folder, as the code in there uses a lot of chained
function calls that are split across lines for readability and the
formatter was condensing them into a much less readable state.

This CL resolves this by using the line continuation character `\` for
these chained function lines, allowing us to retain the more readable
indentation and also enable the formatter by default. It also runs a
full format over all the files in the directory, to get them into a
consistent state where we can have the formatter enabled by default
going forward with low impact.

No behavior change is expected.

Bug: 40711753
Change-Id: I6e10dc5af022ce0e3557099a84773aa9cc92d2e4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5804254
Commit-Queue: Tim <tjudkins@chromium.org>
Reviewed-by: Devlin Cronin <rdevlin.cronin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1345613}
This commit is contained in:
Tim Judkins 2024-08-22 20:16:07 +00:00 committed by Chromium LUCI CQ
parent 91e2dd0507
commit 1e3d052650
43 changed files with 2267 additions and 2087 deletions

@ -5,6 +5,3 @@
third_party/blink/tools/blinkpy/third_party/*
third_party/blink/web_tests/external/wpt/*
tools/valgrind/asan/third_party/asan_symbolize.py
# TODO(crbug.com/1116155): Enable this for formatting by yapf.
tools/json_schema_compiler/*

File diff suppressed because it is too large Load Diff

@ -2,19 +2,22 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
class Code(object):
"""A convenience object for constructing code.
Logically each object should be a block of code. All methods except |Render|
and |IsEmpty| return self.
"""
def __init__(self, indent_size=2, comment_length=80):
self._code = []
self._indent_size = indent_size
self._comment_length = comment_length
self._line_prefixes = []
def Append(self, line='',
def Append(self,
line='',
substitute=True,
indent_level=None,
new_line=True,
@ -110,8 +113,11 @@ class Code(object):
self.Append(line)
return self
def Comment(self, comment, comment_prefix='// ',
wrap_indent=0, new_line=True):
def Comment(self,
comment,
comment_prefix='// ',
wrap_indent=0,
new_line=True):
"""Adds the given string as a comment.
Will split the comment if it's too long. Use mainly for variable length
@ -119,6 +125,7 @@ class Code(object):
Unaffected by code.Substitute().
"""
# Helper function to trim a comment to the maximum length, and return one
# line and the remainder of the comment.
def trim_comment(comment, max_len):
@ -196,6 +203,7 @@ class Code(object):
class Line(object):
"""A line of code.
"""
def __init__(self, value, substitute=True):
self.value = value
self.substitute = substitute

@ -6,7 +6,9 @@
from code_util import Code
import unittest
class CodeTest(unittest.TestCase):
def testAppend(self):
c = Code()
c.Append('line')
@ -14,61 +16,56 @@ class CodeTest(unittest.TestCase):
def testBlock(self):
c = Code()
(c.Append('line')
.Sblock('sblock')
.Append('inner')
.Append('moreinner')
.Sblock('moresblock')
.Append('inner')
.Eblock('out')
.Append('inner')
(c.Append('line') \
.Sblock('sblock') \
.Append('inner') \
.Append('moreinner') \
.Sblock('moresblock') \
.Append('inner') \
.Eblock('out') \
.Append('inner') \
.Eblock('out')
)
self.assertEqual(
'line\n'
'sblock\n'
' inner\n'
' moreinner\n'
' moresblock\n'
' inner\n'
' out\n'
' inner\n'
'out',
c.Render())
'line\n'
'sblock\n'
' inner\n'
' moreinner\n'
' moresblock\n'
' inner\n'
' out\n'
' inner\n'
'out', c.Render())
def testConcat(self):
b = Code()
(b.Sblock('2')
.Append('2')
(b.Sblock('2') \
.Append('2') \
.Eblock('2')
)
c = Code()
(c.Sblock('1')
.Concat(b)
.Append('1')
(c.Sblock('1') \
.Concat(b) \
.Append('1') \
.Eblock('1')
)
self.assertMultiLineEqual(
'1\n'
' 2\n'
' 2\n'
' 2\n'
' 1\n'
'1',
c.Render())
self.assertMultiLineEqual('1\n'
' 2\n'
' 2\n'
' 2\n'
' 1\n'
'1', c.Render())
d = Code()
a = Code()
a.Concat(d)
self.assertEqual('', a.Render())
a.Concat(c)
self.assertEqual(
'1\n'
' 2\n'
' 2\n'
' 2\n'
' 1\n'
'1',
a.Render())
self.assertEqual('1\n'
' 2\n'
' 2\n'
' 2\n'
' 1\n'
'1', a.Render())
def testConcatErrors(self):
c = Code()
@ -89,11 +86,9 @@ class CodeTest(unittest.TestCase):
c.Append('%(var1)s %(var2)s %(var3)s')
c.Append('%(var2)s %(var1)s %(var3)s')
c.Substitute({'var1': 'one', 'var2': 'two', 'var3': 'three'})
self.assertEqual(
'one two one\n'
'one two three\n'
'two one three',
c.Render())
self.assertEqual('one two one\n'
'one two three\n'
'two one three', c.Render())
def testSubstituteErrors(self):
# No unnamed placeholders allowed when substitute is run
@ -118,14 +113,13 @@ class CodeTest(unittest.TestCase):
def testComment(self):
long_comment = ('This comment is ninety one characters in longness, '
'that is, using a different word, length.')
'that is, using a different word, length.')
c = Code()
c.Comment(long_comment)
self.assertEqual(
'// This comment is ninety one characters '
'in longness, that is, using a different\n'
'// word, length.',
c.Render())
'// word, length.', c.Render())
c = Code()
c.Sblock('sblock')
c.Comment(long_comment)
@ -139,27 +133,26 @@ class CodeTest(unittest.TestCase):
'eblock\n'
'// This comment is ninety one characters in '
'longness, that is, using a different\n'
'// word, length.',
c.Render())
'// word, length.', c.Render())
# Words that cannot be broken up are left as too long.
long_word = 'x' * 100
c = Code()
c.Comment('xxx')
c.Comment(long_word)
c.Comment('xxx')
self.assertEqual(
'// xxx\n'
'// ' + 'x' * 100 + '\n'
'// xxx',
c.Render())
self.assertEqual('// xxx\n'
'// ' + 'x' * 100 + '\n'
'// xxx', c.Render())
c = Code(indent_size=2, comment_length=40)
c.Comment('Pretend this is a Closure Compiler style comment, which should '
'both wrap and indent', comment_prefix=' * ', wrap_indent=4)
c.Comment(
'Pretend this is a Closure Compiler style comment, which should '
'both wrap and indent',
comment_prefix=' * ',
wrap_indent=4)
self.assertEqual(
' * Pretend this is a Closure Compiler\n'
' * style comment, which should both\n'
' * wrap and indent',
c.Render())
' * wrap and indent', c.Render())
def testCommentWithSpecialCharacters(self):
c = Code()
@ -170,8 +163,7 @@ class CodeTest(unittest.TestCase):
d.Append('90')
d.Concat(c)
self.assertEqual('90\n'
'// 20% of 80%s',
d.Render())
'// 20% of 80%s', d.Render())
def testLinePrefixes(self):
c = Code()
@ -192,8 +184,7 @@ class CodeTest(unittest.TestCase):
' * x: y\n'
' * }\n'
' * }}\n'
' */',
output)
' */', output)
def testSameLineAppendConcatComment(self):
c = Code()
@ -206,12 +197,13 @@ class CodeTest(unittest.TestCase):
c = Code()
c.Append('This is a')
c.Comment(' spectacular 80-character line thingy ' +
'that fits wonderfully everywhere.',
'that fits wonderfully everywhere.',
comment_prefix='',
new_line=False)
self.assertEqual('This is a spectacular 80-character line thingy that ' +
'fits wonderfully everywhere.',
c.Render())
self.assertEqual(
'This is a spectacular 80-character line thingy that ' +
'fits wonderfully everywhere.', c.Render())
if __name__ == '__main__':
unittest.main()

@ -101,12 +101,10 @@ def GenerateSchema(
# Construct the type generator with all the namespaces in this model.
schema_dir = os.path.dirname(os.path.relpath(file_paths[0], root))
namespace_resolver = NamespaceResolver(
root, schema_dir, include_rules, cpp_namespace_pattern
)
type_generator = CppTypeGenerator(
api_model, namespace_resolver, default_namespace
)
namespace_resolver = NamespaceResolver(root, schema_dir, include_rules,
cpp_namespace_pattern)
type_generator = CppTypeGenerator(api_model, namespace_resolver,
default_namespace)
if generator_name in ('cpp-bundle-registration', 'cpp-bundle-schema'):
cpp_bundle_generator = CppBundleGenerator(
root,
@ -187,12 +185,11 @@ if __name__ == '__main__':
default='.',
help=(
'logical include root directory. Path to schema files from specified'
' dir will be the include path.'
),
)
parser.add_option(
'-d', '--destdir', help='root directory to output generated files.'
' dir will be the include path.'),
)
parser.add_option('-d',
'--destdir',
help='root directory to output generated files.')
parser.add_option(
'-n',
'--namespace',
@ -203,11 +200,9 @@ if __name__ == '__main__':
'-b',
'--bundle-name',
default='',
help=(
'A string to prepend to generated bundle class names, so that '
'multiple bundle rules can be used without conflicting. '
'Only used with one of the cpp-bundle generators.'
),
help=('A string to prepend to generated bundle class names, so that '
'multiple bundle rules can be used without conflicting. '
'Only used with one of the cpp-bundle generators.'),
)
parser.add_option(
'-g',
@ -216,9 +211,7 @@ if __name__ == '__main__':
choices=GENERATORS,
help=(
'The generator to use to build the output code. Supported values are'
' %s'
)
% GENERATORS,
' %s') % GENERATORS,
)
parser.add_option(
'-i',
@ -229,11 +222,9 @@ if __name__ == '__main__':
parser.add_option(
'-I',
'--include-rules',
help=(
'A list of paths to include when searching for referenced objects,'
" with the namespace separated by a ':'. Example: "
'/foo/bar:Foo::Bar::%(namespace)s'
),
help=('A list of paths to include when searching for referenced objects,'
" with the namespace separated by a ':'. Example: "
'/foo/bar:Foo::Bar::%(namespace)s'),
)
(opts, file_paths) = parser.parse_args()
@ -242,28 +233,23 @@ if __name__ == '__main__':
sys.exit(0) # This is OK as a no-op
# Unless in bundle mode, only one file should be specified.
if (
opts.generator not in ('cpp-bundle-registration', 'cpp-bundle-schema')
and len(file_paths) > 1
):
if (opts.generator not in ('cpp-bundle-registration', 'cpp-bundle-schema')
and len(file_paths) > 1):
# TODO(sashab): Could also just use file_paths[0] here and not complain.
raise Exception(
'Unless in bundle mode, only one file can be specified at a time.'
)
'Unless in bundle mode, only one file can be specified at a time.')
def split_path_and_namespace(path_and_namespace):
if ':' not in path_and_namespace:
raise ValueError(
'Invalid include rule "%s". Rules must be of the form path:namespace'
% path_and_namespace
)
% path_and_namespace)
return path_and_namespace.split(':', 1)
include_rules = []
if opts.include_rules:
include_rules = list(
map(split_path_and_namespace, shlex.split(opts.include_rules))
)
map(split_path_and_namespace, shlex.split(opts.include_rules)))
result = GenerateSchema(
opts.generator,

@ -26,6 +26,7 @@ def _RemoveKey(node, key, type_restriction):
for value in node:
_RemoveKey(value, key, type_restriction)
def _RemoveUnneededFields(schema):
"""Returns a copy of |schema| with fields that aren't necessary at runtime
removed.
@ -41,6 +42,7 @@ def _RemoveUnneededFields(schema):
_RemoveKey(ret, 'manifest_keys', object)
return ret
def _PrefixSchemaWithNamespace(schema):
"""Modifies |schema| in place to prefix all types and references with a
namespace, if they aren't already qualified. That is, in the tabs API, this
@ -49,10 +51,11 @@ def _PrefixSchemaWithNamespace(schema):
"""
assert isinstance(schema, dict), "Schema is unexpected type"
namespace = schema['namespace']
def prefix(obj, key, mandatory):
if not key in obj:
assert not mandatory, (
'Required key "%s" is not present in object.' % key)
assert not mandatory, ('Required key "%s" is not present in object.' %
key)
return
assert type(obj[key]) is str
if obj[key].find('.') == -1:
@ -73,6 +76,7 @@ def _PrefixSchemaWithNamespace(schema):
prefix(val, '$ref', False)
for key, sub_val in val.items():
prefix_refs(sub_val)
prefix_refs(schema)
return schema
@ -81,15 +85,8 @@ class CppBundleGenerator(object):
"""This class contains methods to generate code based on multiple schemas.
"""
def __init__(self,
root,
model,
api_defs,
cpp_type_generator,
cpp_namespace_pattern,
bundle_name,
source_file_dir,
impl_dir):
def __init__(self, root, model, api_defs, cpp_type_generator,
cpp_namespace_pattern, bundle_name, source_file_dir, impl_dir):
self._root = root
self._model = model
self._api_defs = api_defs
@ -196,9 +193,9 @@ class CppBundleGenerator(object):
if function.nocompile:
continue
namespace_types_name = JsFunctionNameToClassName(
namespace.name, type_.name)
c.Concat(self._GenerateRegistrationEntry(namespace_types_name,
function))
namespace.name, type_.name)
c.Concat(
self._GenerateRegistrationEntry(namespace_types_name, function))
if namespace_ifdefs is not None:
c.Append("#endif // %s" % namespace_ifdefs, indent_level=0)
@ -218,6 +215,7 @@ class CppBundleGenerator(object):
class _APIHGenerator(object):
"""Generates the header for API registration / declaration"""
def __init__(self, cpp_bundle):
self._bundle = cpp_bundle
@ -234,7 +232,7 @@ class _APIHGenerator(object):
self._bundle._GenerateBundleClass('GeneratedFunctionRegistry'))
c.Sblock(' public:')
c.Append('static void RegisterAll('
'ExtensionFunctionRegistry* registry);')
'ExtensionFunctionRegistry* registry);')
c.Eblock('};')
c.Append()
c.Concat(cpp_util.CloseNamespace(self._bundle._cpp_namespace))
@ -251,9 +249,8 @@ class _APICCGenerator(object):
c = code_util.Code()
c.Append(cpp_util.CHROMIUM_LICENSE)
c.Append()
c.Append('#include "%s"' % (
cpp_util.ToPosixPath(os.path.join(self._bundle._impl_dir,
'generated_api_registration.h'))))
c.Append('#include "%s"' % (cpp_util.ToPosixPath(
os.path.join(self._bundle._impl_dir, 'generated_api_registration.h'))))
c.Append()
c.Append('#include "build/build_config.h"')
c.Append('#include "build/chromeos_buildflags.h"')
@ -261,17 +258,15 @@ class _APICCGenerator(object):
for namespace in self._bundle._model.namespaces.values():
namespace_name = namespace.unix_name.replace("experimental_", "")
implementation_header = namespace.compiler_options.get(
"implemented_in",
"%s/%s/%s_api.h" % (self._bundle._impl_dir,
namespace_name,
namespace_name))
"implemented_in", "%s/%s/%s_api.h" %
(self._bundle._impl_dir, namespace_name, namespace_name))
if not os.path.exists(
os.path.join(self._bundle._root,
os.path.normpath(implementation_header))):
if "implemented_in" in namespace.compiler_options:
raise ValueError('Header file for namespace "%s" specified in '
'compiler_options not found: %s' %
(namespace.unix_name, implementation_header))
'compiler_options not found: %s' %
(namespace.unix_name, implementation_header))
continue
ifdefs = self._bundle._GetPlatformIfdefs(namespace)
if ifdefs is not None:
@ -283,7 +278,7 @@ class _APICCGenerator(object):
c.Append("#endif // %s" % ifdefs, indent_level=0)
c.Append()
c.Append('#include '
'"extensions/browser/extension_function_registry.h"')
'"extensions/browser/extension_function_registry.h"')
c.Append()
c.Concat(cpp_util.OpenNamespace(self._bundle._cpp_namespace))
c.Append()
@ -296,6 +291,7 @@ class _APICCGenerator(object):
class _SchemasHGenerator(object):
"""Generates a code_util.Code object for the generated schemas .h file"""
def __init__(self, cpp_bundle):
self._bundle = cpp_bundle
@ -322,8 +318,7 @@ class _SchemasHGenerator(object):
def _FormatNameAsConstant(name):
"""Formats a name to be a C++ constant of the form kConstantName"""
name = '%s%s' % (name[0].upper(), name[1:])
return 'k%s' % re.sub('_[a-z]',
lambda m: m.group(0)[1].upper(),
return 'k%s' % re.sub('_[a-z]', lambda m: m.group(0)[1].upper(),
name.replace('.', '_'))
@ -337,9 +332,8 @@ class _SchemasCCGenerator(object):
c = code_util.Code()
c.Append(cpp_util.CHROMIUM_LICENSE)
c.Append()
c.Append('#include "%s"' % (
cpp_util.ToPosixPath(os.path.join(self._bundle._source_file_dir,
'generated_schemas.h'))))
c.Append('#include "%s"' % (cpp_util.ToPosixPath(
os.path.join(self._bundle._source_file_dir, 'generated_schemas.h'))))
c.Append()
c.Append('#include <algorithm>')
c.Append('#include <iterator>')
@ -351,7 +345,7 @@ class _SchemasCCGenerator(object):
for api in self._bundle._api_defs:
namespace = self._bundle._model.namespaces[api.get('namespace')]
json_content = json.dumps(_PrefixSchemaWithNamespace(
_RemoveUnneededFields(api)),
_RemoveUnneededFields(api)),
separators=(',', ':'))
# This will output a valid JSON C string. Note that some schemas are
# too large to compile on windows. Split the JSON up into several
@ -381,8 +375,10 @@ class _SchemasCCGenerator(object):
c.Append('static constexpr auto kSchemas = '
'base::MakeFixedFlatMap<std::string_view, std::string_view>({')
c.Sblock()
namespaces = [self._bundle._model.namespaces[api.get('namespace')].name
for api in self._bundle._api_defs]
namespaces = [
self._bundle._model.namespaces[api.get('namespace')].name
for api in self._bundle._api_defs
]
for namespace in sorted(namespaces):
schema_constant_name = _FormatNameAsConstant(namespace)
c.Append('{"%s", %s},' % (namespace, schema_constant_name))

@ -10,26 +10,29 @@ import json_schema
import os
import unittest
def _createCppBundleGenerator(file_path):
json_object = json_schema.Load(file_path)
model = Model()
model.AddNamespace(json_object[0], file_path)
cpp_bundle_generator = CppBundleGenerator(
None, model, None, None, 'generated_api_schemas',
None, None, None)
cpp_bundle_generator = CppBundleGenerator(None, model, None, None,
'generated_api_schemas', None, None,
None)
return (cpp_bundle_generator, model)
def _getPlatformIfdefs(cpp_bundle_generator, model):
return cpp_bundle_generator._GetPlatformIfdefs(
list(list(model.namespaces.values())[0].functions.values())[0])
class CppBundleGeneratorTest(unittest.TestCase):
def testIfDefsForWinLinux(self):
cpp_bundle_generator, model = _createCppBundleGenerator(
'test/function_platform_win_linux.json')
self.assertEqual(
'BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX)',
_getPlatformIfdefs(cpp_bundle_generator, model))
self.assertEqual('BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX)',
_getPlatformIfdefs(cpp_bundle_generator, model))
def testIfDefsForAll(self):
cpp_bundle_generator, model = _createCppBundleGenerator(
@ -43,7 +46,7 @@ class CppBundleGeneratorTest(unittest.TestCase):
cpp_bundle_generator, model = _createCppBundleGenerator(
'test/function_platform_chromeos.json')
self.assertEqual('BUILDFLAG(IS_CHROMEOS_ASH)',
_getPlatformIfdefs(cpp_bundle_generator, model))
_getPlatformIfdefs(cpp_bundle_generator, model))
if __name__ == '__main__':

@ -5,7 +5,9 @@
from cc_generator import CCGenerator
from h_generator import HGenerator
class CppGenerator(object):
def __init__(self, type_generator):
self.h_generator = HGenerator(type_generator)
self.cc_generator = CCGenerator(type_generator)

@ -2,6 +2,8 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
class CppNamespaceEnvironment(object):
def __init__(self, namespace_pattern):
self.namespace_pattern = namespace_pattern

@ -8,11 +8,13 @@ import cpp_util
from json_parse import OrderedDict
import schema_util
class _TypeDependency(object):
"""Contains information about a dependency a namespace has on a type: the
type's model, and whether that dependency is "hard" meaning that it cannot be
forward declared.
"""
def __init__(self, type_, hard=False):
self.type_ = type_
self.hard = hard
@ -25,6 +27,7 @@ class CppTypeGenerator(object):
"""Manages the types of properties and provides utilities for getting the
C++ type out of a model.Property
"""
def __init__(self, model, namespace_resolver, default_namespace=None):
"""Creates a cpp_type_generator. The given root_namespace should be of the
format extensions::api::sub. The generator will generate code suitable for
@ -40,9 +43,8 @@ class CppTypeGenerator(object):
typename in an optional, for the regular case, or uses a base::expected for
when it should support string errors.
"""
return (('base::expected<{typename}, std::u16string>'
if support_errors else 'std::optional<{typename}>')
.format(typename=typename))
return (('base::expected<{typename}, std::u16string>' if support_errors else
'std::optional<{typename}>').format(typename=typename))
def GetEnumNoneValue(self, type_, full_name=True):
"""Gets the enum value in the given model. Property indicating no value has
@ -91,7 +93,7 @@ class CppTypeGenerator(object):
result = ''
for char in name:
if char in {'_', '-'}:
change_to_upper=True
change_to_upper = True
elif change_to_upper:
# Numbers must be kept separate, for better readability (e.g. kX86_64).
if char.isnumeric() and result and result[-1].isnumeric():
@ -125,9 +127,9 @@ class CppTypeGenerator(object):
prefix = '{classname}::'.format(classname=classname)
# We kCamelCase the string, also removing any _ from the name, to allow
# SHOUTY_CASE keys to be kCamelCase as well.
return '{prefix}k{name}'.format(
prefix=prefix,
name=self.FormatStringForEnumValue(enum_value.name))
return '{prefix}k{name}'.format(prefix=prefix,
name=self.FormatStringForEnumValue(
enum_value.name))
def GetCppType(self, type_, is_optional=False):
"""Translates a model.Property or model.Type into its C++ type.
@ -155,8 +157,7 @@ class CppTypeGenerator(object):
cpp_type = 'double'
elif type_.property_type == PropertyType.STRING:
cpp_type = 'std::string'
elif type_.property_type in (PropertyType.ENUM,
PropertyType.OBJECT,
elif type_.property_type in (PropertyType.ENUM, PropertyType.OBJECT,
PropertyType.CHOICES):
if self._default_namespace is type_.namespace:
cpp_type = cpp_util.Classname(type_.name)
@ -164,8 +165,7 @@ class CppTypeGenerator(object):
cpp_namespace = cpp_util.GetCppNamespace(
type_.namespace.environment.namespace_pattern,
type_.namespace.unix_name)
cpp_type = '%s::%s' % (cpp_namespace,
cpp_util.Classname(type_.name))
cpp_type = '%s::%s' % (cpp_namespace, cpp_util.Classname(type_.name))
elif type_.property_type == PropertyType.ANY:
cpp_type = 'base::Value'
elif type_.property_type == PropertyType.FUNCTION:
@ -201,10 +201,9 @@ class CppTypeGenerator(object):
return cpp_type
def IsCopyable(self, type_):
return not (self.FollowRef(type_).property_type in (PropertyType.ANY,
PropertyType.ARRAY,
PropertyType.OBJECT,
PropertyType.CHOICES))
return not (self.FollowRef(type_).property_type
in (PropertyType.ANY, PropertyType.ARRAY, PropertyType.OBJECT,
PropertyType.CHOICES))
def GenerateForwardDeclarations(self):
"""Returns the forward declarations for self._default_namespace.
@ -212,17 +211,16 @@ class CppTypeGenerator(object):
c = Code()
for namespace, deps in self._NamespaceTypeDependencies().items():
filtered_deps = [
dep for dep in deps
# Add more ways to forward declare things as necessary.
if (not dep.hard and
dep.type_.property_type in (PropertyType.CHOICES,
PropertyType.OBJECT))]
dep for dep in deps
# Add more ways to forward declare things as necessary.
if (not dep.hard and dep.type_.property_type in (PropertyType.CHOICES,
PropertyType.OBJECT))
]
if not filtered_deps:
continue
cpp_namespace = cpp_util.GetCppNamespace(
namespace.environment.namespace_pattern,
namespace.unix_name)
namespace.environment.namespace_pattern, namespace.unix_name)
c.Concat(cpp_util.OpenNamespace(cpp_namespace))
for dep in filtered_deps:
c.Append('struct %s;' % dep.type_.name)
@ -236,9 +234,9 @@ class CppTypeGenerator(object):
# The inclusion of the std::string_view header is dependent on either the
# presence of enums, or manifest keys.
include_string_view = (self._default_namespace.manifest_keys or
any(type_.property_type is PropertyType.ENUM for type_ in
self._default_namespace.types.values()))
include_string_view = (self._default_namespace.manifest_keys or any(
type_.property_type is PropertyType.ENUM
for type_ in self._default_namespace.types.values()))
if include_string_view:
c.Append('#include <string_view>')
@ -246,11 +244,10 @@ class CppTypeGenerator(object):
# The header for `base::expected` should be included whenever error messages
# are supposed to be returned, which only occurs with object, choices, or
# functions.
if (generate_error_messages and (
len(self._default_namespace.functions.values()) or
any(type_.property_type in
[PropertyType.OBJECT, PropertyType.CHOICES] for type_ in
self._default_namespace.types.values()))):
if (generate_error_messages
and (len(self._default_namespace.functions.values()) or any(
type_.property_type in [PropertyType.OBJECT, PropertyType.CHOICES]
for type_ in self._default_namespace.types.values()))):
c.Append('#include "base/types/expected.h"')
# Note: It's possible that there are multiple dependencies from the same
@ -361,11 +358,7 @@ class CppTypeGenerator(object):
cpp_value = '"%s"' % cpp_value
cpp_type = 'char'
cpp_name = '%s[]' % cpp_name
c.Append(line % {
"type": cpp_type,
"name": cpp_name,
"value": cpp_value
})
c.Append(line % {"type": cpp_type, "name": cpp_name, "value": cpp_value})
else:
has_child_code = False
c.Sblock('namespace %s {' % prop.name)

@ -12,7 +12,9 @@ import unittest
from collections import defaultdict
class _FakeSchemaLoader(object):
def __init__(self, model):
self._model = model
@ -22,13 +24,15 @@ class _FakeSchemaLoader(object):
return default if type_name in default.types else None
return self._model.namespaces[parts[0]]
class CppTypeGeneratorTest(unittest.TestCase):
def setUp(self):
self.models = defaultdict(model.Model)
forbidden_json = CachedLoad('test/forbidden.json')
self.models['forbidden'].AddNamespace(
forbidden_json[0], 'path/to/forbidden.json')
self.models['forbidden'].AddNamespace(forbidden_json[0],
'path/to/forbidden.json')
permissions_json = CachedLoad('test/permissions.json')
self.permissions = self.models['permissions'].AddNamespace(
@ -59,12 +63,13 @@ class CppTypeGeneratorTest(unittest.TestCase):
objects_movable_idl = idl_schema.Load('test/objects_movable.idl')
self.objects_movable = self.models['objects_movable'].AddNamespace(
objects_movable_idl[0], 'path/to/objects_movable.idl',
objects_movable_idl[0],
'path/to/objects_movable.idl',
include_compiler_options=True)
self.simple_api_json = CachedLoad('test/simple_api.json')
self.models['simple_api'].AddNamespace(
self.simple_api_json[0], 'path/to/simple_api.json')
self.models['simple_api'].AddNamespace(self.simple_api_json[0],
'path/to/simple_api.json')
self.crossref_enums_json = CachedLoad('test/crossref_enums.json')
self.crossref_enums = self.models['crossref_enums'].AddNamespace(
@ -73,8 +78,7 @@ class CppTypeGeneratorTest(unittest.TestCase):
self.crossref_enums_array_json = CachedLoad(
'test/crossref_enums_array.json')
self.models['crossref_enums_array'].AddNamespace(
self.crossref_enums_array_json[0],
'path/to/crossref_enums_array.json')
self.crossref_enums_array_json[0], 'path/to/crossref_enums_array.json')
def testGenerateIncludesAndForwardDeclarations(self):
m = model.Model()
@ -88,7 +92,7 @@ class CppTypeGeneratorTest(unittest.TestCase):
self.assertEqual('', manager.GenerateIncludes().Render())
self.assertEqual('#include "path/to/tabs.h"',
manager.GenerateIncludes(include_soft=True).Render())
manager.GenerateIncludes(include_soft=True).Render())
self.assertEqual(
'namespace tabs {\n'
'struct Tab;\n'
@ -96,14 +100,14 @@ class CppTypeGeneratorTest(unittest.TestCase):
manager.GenerateForwardDeclarations().Render())
m = model.Model()
m.AddNamespace(self.windows_json[0],
'path/to/windows.json',
environment=CppNamespaceEnvironment(
'foo::bar::%(namespace)s'))
m.AddNamespace(self.tabs_json[0],
'path/to/tabs.json',
environment=CppNamespaceEnvironment(
'foo::bar::%(namespace)s'))
m.AddNamespace(
self.windows_json[0],
'path/to/windows.json',
environment=CppNamespaceEnvironment('foo::bar::%(namespace)s'))
m.AddNamespace(
self.tabs_json[0],
'path/to/tabs.json',
environment=CppNamespaceEnvironment('foo::bar::%(namespace)s'))
manager = CppTypeGenerator(m, _FakeSchemaLoader(m))
self.assertEqual(
'namespace foo {\n'
@ -134,9 +138,10 @@ class CppTypeGeneratorTest(unittest.TestCase):
manager = CppTypeGenerator(m,
_FakeSchemaLoader(m),
default_namespace=dependency_tester)
self.assertEqual('#include "path/to/browser_action.h"\n'
'#include "path/to/font_settings.h"',
manager.GenerateIncludes().Render())
self.assertEqual(
'#include "path/to/browser_action.h"\n'
'#include "path/to/font_settings.h"',
manager.GenerateIncludes().Render())
self.assertEqual('', manager.GenerateForwardDeclarations().Render())
def testGetCppTypeSimple(self):
@ -167,7 +172,7 @@ class CppTypeGeneratorTest(unittest.TestCase):
def testGetCppTypeArray(self):
manager = CppTypeGenerator(self.models.get('windows'),
_FakeSchemaLoader(None))
_FakeSchemaLoader(None))
self.assertEqual(
'std::vector<Window>',
manager.GetCppType(
@ -183,9 +188,8 @@ class CppTypeGeneratorTest(unittest.TestCase):
_FakeSchemaLoader(None))
self.assertEqual(
'std::vector<MovablePod>',
manager.GetCppType(
self.objects_movable.types['MovableParent'].
properties['pods'].type_))
manager.GetCppType(self.objects_movable.types['MovableParent'].
properties['pods'].type_))
def testGetCppTypeLocalRef(self):
manager = CppTypeGenerator(self.models.get('tabs'), _FakeSchemaLoader(None))
@ -211,7 +215,8 @@ class CppTypeGeneratorTest(unittest.TestCase):
def testGetCppTypeWithPadForGeneric(self):
manager = CppTypeGenerator(self.models.get('permissions'),
_FakeSchemaLoader(None))
self.assertEqual('std::vector<std::string>',
self.assertEqual(
'std::vector<std::string>',
manager.GetCppType(
self.permissions.types['Permissions'].properties['origins'].type_))
self.assertEqual(
@ -235,7 +240,7 @@ class CppTypeGeneratorTest(unittest.TestCase):
_FakeSchemaLoader(m))
self.assertEqual('#include "path/to/simple_api.h"',
manager.GenerateIncludes().Render())
manager.GenerateIncludes().Render())
def testHardIncludesForEnumArrays(self):
"""Tests that enums in arrays generate hard includes. Note that it's
@ -253,7 +258,7 @@ class CppTypeGeneratorTest(unittest.TestCase):
_FakeSchemaLoader(m))
self.assertEqual('#include "path/to/simple_api.h"',
manager.GenerateIncludes().Render())
manager.GenerateIncludes().Render())
def testCrossNamespaceGetEnumDefaultValue(self):
m = model.Model()
@ -272,9 +277,9 @@ class CppTypeGeneratorTest(unittest.TestCase):
self.assertEqual(
'namespace1::api::simple_api::TestEnum()',
manager.GetEnumDefaultValue(
self.crossref_enums.types['CrossrefType']
.properties['testEnumOptional'].type_,
self.crossref_enums))
self.crossref_enums.types['CrossrefType'].
properties['testEnumOptional'].type_, self.crossref_enums))
if __name__ == '__main__':
unittest.main()

@ -1,7 +1,6 @@
# Copyright 2012 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Utilies and constants specific to Chromium C++ code.
"""
@ -12,11 +11,9 @@ import os
import posixpath
import re
CHROMIUM_LICENSE = (
"""// Copyright %d The Chromium Authors
CHROMIUM_LICENSE = ("""// Copyright %d The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.""" % datetime.now().year
)
// found in the LICENSE file.""" % datetime.now().year)
GENERATED_FILE_MESSAGE = """// GENERATED FROM THE API DEFINITION IN
// %s
// by tools/json_schema_compiler.
@ -33,6 +30,7 @@ GENERATED_FEATURE_MESSAGE = """// GENERATED FROM THE FEATURE DEFINITIONS IN
// DO NOT EDIT.
"""
def Classname(s):
"""Translates a namespace name or function name into something more
suited to C++.
@ -56,6 +54,7 @@ def Classname(s):
result = '_' + result
return result
def GetAsFundamentalValue(type_, src):
"""Returns the C++ code for retrieving a fundamental type from a
Value into a variable.
@ -68,9 +67,9 @@ def GetAsFundamentalValue(type_, src):
s = '%s.GetIfDouble()'
elif type_.property_type == PropertyType.INTEGER:
s = '%s.GetIfInt()'
elif (type_.property_type == PropertyType.STRING or
(type_.property_type == PropertyType.FUNCTION and
type_.is_serializable_function)):
elif (type_.property_type == PropertyType.STRING
or (type_.property_type == PropertyType.FUNCTION
and type_.is_serializable_function)):
s = '%s.GetIfString()'
else:
raise ValueError('Type %s is not a fundamental value' % type_.name)
@ -104,43 +103,49 @@ def GetValueType(type_):
raise ValueError('Invalid type: %s' % type_.name)
def ShouldUseStdOptional(type_):
"""Called to validate whether or not an optional value should be represented
with std::optional. This function is a temporary utility, while optional
fields are gradually migrated away from using std::unique_ptr.
"""
if type_.property_type in (PropertyType.ANY,
PropertyType.ARRAY,
PropertyType.BINARY,
PropertyType.BOOLEAN,
PropertyType.CHOICES,
PropertyType.DOUBLE,
PropertyType.FUNCTION,
PropertyType.INTEGER,
PropertyType.OBJECT,
PropertyType.STRING):
if type_.property_type in (
PropertyType.ANY,
PropertyType.ARRAY,
PropertyType.BINARY,
PropertyType.BOOLEAN,
PropertyType.CHOICES,
PropertyType.DOUBLE,
PropertyType.FUNCTION,
PropertyType.INTEGER,
PropertyType.OBJECT,
PropertyType.STRING,
):
return True
return False
def GetParameterDeclaration(param, type_):
"""Gets a parameter declaration of a given model.Property and its C++
type.
"""
if param.type_.property_type in (PropertyType.ANY,
PropertyType.ARRAY,
PropertyType.BINARY,
PropertyType.CHOICES,
PropertyType.OBJECT,
PropertyType.REF,
PropertyType.STRING):
if param.type_.property_type in (
PropertyType.ANY,
PropertyType.ARRAY,
PropertyType.BINARY,
PropertyType.CHOICES,
PropertyType.OBJECT,
PropertyType.REF,
PropertyType.STRING,
):
arg = 'const %(type)s& %(name)s'
else:
arg = '%(type)s %(name)s'
return arg % {
'type': type_,
'name': param.unix_name,
'type': type_,
'name': param.unix_name,
}
@ -150,10 +155,10 @@ def GenerateIfndefName(file_path):
e.g chrome/extensions/gen/file.h becomes CHROME_EXTENSIONS_GEN_FILE_H__.
"""
return (('%s__' % file_path).upper()
.replace('\\', '_')
.replace('/', '_')
.replace('-', '_')
return (('%s__' % file_path).upper() \
.replace('\\', '_') \
.replace('/', '_') \
.replace('-', '_') \
.replace('.', '_'))
@ -180,7 +185,7 @@ def FeatureNameToConstantName(feature_name):
"""Returns a kName for a feature's name.
"""
return ('k' + ''.join(word[0].upper() + word[1:]
for word in feature_name.replace('.', ' ').split()))
for word in feature_name.replace('.', ' ').split()))
def UnixNameToConstantName(unix_name):
@ -189,6 +194,7 @@ def UnixNameToConstantName(unix_name):
"""
return ('k' + ''.join(word.capitalize() for word in unix_name.split('_')))
def IsUnixName(s):
# type (str) -> bool
"""Returns true if |s| is of the type unix_name i.e. only has lower cased
@ -196,6 +202,7 @@ def IsUnixName(s):
"""
return all(x.islower() or x == '_' for x in s) and '_' in s
def ToPosixPath(path):
"""Returns |path| with separator converted to POSIX style.
@ -220,7 +227,7 @@ def GetCppNamespace(pattern, namespace):
# For some reason Windows builds escape the % characters, so unescape them.
# This means that %% can never appear legitimately within a pattern, but
# that's ok. It should never happen.
cpp_namespace = pattern.replace('%%', '%') % { 'namespace': namespace }
cpp_namespace = pattern.replace('%%', '%') % {'namespace': namespace}
assert '%' not in cpp_namespace, \
('Did not manage to fully substitute namespace "%s" into pattern "%s"'
% (namespace, pattern))

@ -5,35 +5,27 @@
import unittest
from cpp_util import (
Classname,
CloseNamespace,
GetCppNamespace,
GenerateIfndefName,
OpenNamespace
)
from cpp_util import (Classname, CloseNamespace, GetCppNamespace,
GenerateIfndefName, OpenNamespace)
class CppUtilTest(unittest.TestCase):
def testClassname(self):
self.assertEqual('Permissions', Classname('permissions'))
self.assertEqual('UpdateAllTheThings',
Classname('updateAllTheThings'))
self.assertEqual('UpdateAllTheThings', Classname('updateAllTheThings'))
self.assertEqual('Aa_Bb_Cc', Classname('aa.bb.cc'))
def testNamespaceDeclaration(self):
self.assertEqual('namespace foo {',
OpenNamespace('foo').Render())
self.assertEqual('} // namespace foo',
CloseNamespace('foo').Render())
self.assertEqual('namespace foo {', OpenNamespace('foo').Render())
self.assertEqual('} // namespace foo', CloseNamespace('foo').Render())
self.assertEqual(
'namespace extensions {\n'
'namespace foo {',
OpenNamespace('extensions::foo').Render())
self.assertEqual(
'} // namespace foo\n'
'} // namespace extensions',
CloseNamespace('extensions::foo').Render())
self.assertEqual('namespace extensions {\n'
'namespace foo {',
OpenNamespace('extensions::foo').Render())
self.assertEqual('} // namespace foo\n'
'} // namespace extensions',
CloseNamespace('extensions::foo').Render())
self.assertEqual(
'namespace extensions {\n'

@ -72,6 +72,7 @@ CC_FILE_END = """
} // namespace extensions
"""
def ToPosixPath(path):
"""Returns |path| with separator converted to POSIX style.
@ -79,11 +80,13 @@ def ToPosixPath(path):
"""
return path.replace(os.path.sep, posixpath.sep)
# Returns true if the list 'l' only contains strings that are a hex-encoded SHA1
# hashes.
def ListContainsOnlySha1Hashes(l):
return len(list(filter(lambda s: not re.match("^[A-F0-9]{40}$", s), l))) == 0
# A "grammar" for what is and isn't allowed in the features.json files. This
# grammar has to list all possible keys and the requirements for each. The
# format of each entry is:
@ -170,19 +173,25 @@ FEATURE_GRAMMAR = ({
list: {
'enum_map': {
'privileged_extension':
'mojom::ContextType::kPrivilegedExtension',
'privileged_web_page': 'mojom::ContextType::kPrivilegedWebPage',
'content_script': 'mojom::ContextType::kContentScript',
'mojom::ContextType::kPrivilegedExtension',
'privileged_web_page':
'mojom::ContextType::kPrivilegedWebPage',
'content_script':
'mojom::ContextType::kContentScript',
'lock_screen_extension':
'mojom::ContextType::kLockscreenExtension',
'mojom::ContextType::kLockscreenExtension',
'offscreen_extension':
'mojom::ContextType::kOffscreenExtension',
'user_script': 'mojom::ContextType::kUserScript',
'web_page': 'mojom::ContextType::kWebPage',
'webui': 'mojom::ContextType::kWebUi',
'webui_untrusted': 'mojom::ContextType::kUntrustedWebUi',
'mojom::ContextType::kOffscreenExtension',
'user_script':
'mojom::ContextType::kUserScript',
'web_page':
'mojom::ContextType::kWebPage',
'webui':
'mojom::ContextType::kWebUi',
'webui_untrusted':
'mojom::ContextType::kUntrustedWebUi',
'unprivileged_extension':
'mojom::ContextType::kUnprivilegedExtension',
'mojom::ContextType::kUnprivilegedExtension',
},
'allow_all': True,
'allow_empty': True
@ -210,12 +219,18 @@ FEATURE_GRAMMAR = ({
'extension_types': {
list: {
'enum_map': {
'extension': 'Manifest::TYPE_EXTENSION',
'hosted_app': 'Manifest::TYPE_HOSTED_APP',
'legacy_packaged_app': 'Manifest::TYPE_LEGACY_PACKAGED_APP',
'platform_app': 'Manifest::TYPE_PLATFORM_APP',
'shared_module': 'Manifest::TYPE_SHARED_MODULE',
'theme': 'Manifest::TYPE_THEME',
'extension':
'Manifest::TYPE_EXTENSION',
'hosted_app':
'Manifest::TYPE_HOSTED_APP',
'legacy_packaged_app':
'Manifest::TYPE_LEGACY_PACKAGED_APP',
'platform_app':
'Manifest::TYPE_PLATFORM_APP',
'shared_module':
'Manifest::TYPE_SHARED_MODULE',
'theme':
'Manifest::TYPE_THEME',
'login_screen_extension':
'Manifest::TYPE_LOGIN_SCREEN_EXTENSION',
'chromeos_system_extension':
@ -288,10 +303,12 @@ FEATURE_GRAMMAR = ({
'session_types': {
list: {
'enum_map': {
'regular': 'mojom::FeatureSessionType::kRegular',
'kiosk': 'mojom::FeatureSessionType::kKiosk',
'regular':
'mojom::FeatureSessionType::kRegular',
'kiosk':
'mojom::FeatureSessionType::kKiosk',
'kiosk.autolaunched':
'mojom::FeatureSessionType::kAutolaunchedKiosk',
'mojom::FeatureSessionType::kAutolaunchedKiosk',
}
}
},
@ -301,21 +318,27 @@ FEATURE_GRAMMAR = ({
},
})
FEATURE_TYPES = ['APIFeature', 'BehaviorFeature',
'ManifestFeature', 'PermissionFeature']
FEATURE_TYPES = [
'APIFeature', 'BehaviorFeature', 'ManifestFeature', 'PermissionFeature'
]
def HasProperty(property_name, value):
return property_name in value
def HasAtLeastOneProperty(property_names, value):
return any([HasProperty(name, value) for name in property_names])
def DoesNotHaveAllProperties(property_names, value):
return not all([HasProperty(name, value) for name in property_names])
def DoesNotHaveProperty(property_name, value):
return property_name not in value
def DoesNotHavePropertyInComplexFeature(property_name, feature, all_features):
if type(feature) is ComplexFeature:
for child_feature in feature.feature_list:
@ -323,6 +346,7 @@ def DoesNotHavePropertyInComplexFeature(property_name, feature, all_features):
return False
return True
def IsEmptyContextsAllowed(feature, all_features):
# An alias feature wouldn't have the 'contexts' feature value.
if feature.GetValue('source'):
@ -338,12 +362,13 @@ def IsEmptyContextsAllowed(feature, all_features):
assert contexts, 'contexts must have been specified for the APIFeature'
allowlisted_empty_context_namespaces = [
'manifestTypes',
'extensionsManifestTypes',
'empty_contexts' # Only added for testing.
'manifestTypes',
'extensionsManifestTypes',
'empty_contexts' # Only added for testing.
]
return (contexts != '{}' or
feature.name in allowlisted_empty_context_namespaces)
return (contexts != '{}'
or feature.name in allowlisted_empty_context_namespaces)
def IsFeatureCrossReference(property_name, reverse_property_name, feature,
all_features):
@ -374,6 +399,7 @@ def IsFeatureCrossReference(property_name, reverse_property_name, feature,
return True
return reverse_reference_value == ('"%s"' % feature.name)
# Verifies that a feature with an allowlist is not available to hosted apps,
# returning true on success.
def DoesNotHaveAllowlistForHostedApps(value):
@ -428,92 +454,90 @@ def DoesNotHaveAllowlistForHostedApps(value):
SIMPLE_FEATURE_CPP_CLASSES = ({
'APIFeature': 'SimpleFeature',
'ManifestFeature': 'ManifestFeature',
'PermissionFeature': 'PermissionFeature',
'BehaviorFeature': 'SimpleFeature',
'APIFeature': 'SimpleFeature',
'ManifestFeature': 'ManifestFeature',
'PermissionFeature': 'PermissionFeature',
'BehaviorFeature': 'SimpleFeature',
})
VALIDATION = ({
'all': [
(partial(HasAtLeastOneProperty, ['channel', 'dependencies']),
'Features must specify either a channel or dependencies'),
(DoesNotHaveAllowlistForHostedApps,
'Hosted apps are not allowed to use restricted features'),
],
'APIFeature': [
(partial(HasProperty, 'contexts'),
'APIFeatures must specify the contexts property'),
(partial(DoesNotHaveAllProperties, ['alias', 'source']),
'Features cannot specify both alias and source.')
],
'ManifestFeature': [
(partial(HasProperty, 'extension_types'),
'ManifestFeatures must specify at least one extension type'),
(partial(DoesNotHaveProperty, 'contexts'),
'ManifestFeatures do not support contexts.'),
(partial(DoesNotHaveProperty, 'alias'),
'ManifestFeatures do not support alias.'),
(partial(DoesNotHaveProperty, 'source'),
'ManifestFeatures do not support source.'),
# The `required_buildflags` field is intended to be used to toggle the
# availability of certain APIs; if we support this for feature types other
# than APIFeature, we may emit warnings that are visible to developers which
# is not desirable.
(partial(DoesNotHaveProperty, 'required_buildflags'),
'ManifestFeatures do not support required_buildflags.'),
],
'BehaviorFeature': [
(partial(DoesNotHaveProperty, 'alias'),
'BehaviorFeatures do not support alias.'),
(partial(DoesNotHaveProperty, 'source'),
'BehaviorFeatures do not support source.'),
(partial(DoesNotHaveProperty, 'required_buildflags'),
# The `required_buildflags` field is intended to be used to toggle the
# availability of certain APIs; if we support this for feature types other
# than APIFeature, we may emit warnings that are visible to developers which
# is not desirable.
'BehaviorFeatures do not support required_buildflags.'),
],
'PermissionFeature': [
(partial(HasProperty, 'extension_types'),
'PermissionFeatures must specify at least one extension type'),
(partial(DoesNotHaveProperty, 'contexts'),
'PermissionFeatures do not support contexts.'),
(partial(DoesNotHaveProperty, 'alias'),
'PermissionFeatures do not support alias.'),
(partial(DoesNotHaveProperty, 'source'),
'PermissionFeatures do not support source.'),
(partial(DoesNotHaveProperty, 'required_buildflags'),
# The `required_buildflags` field is intended to be used to toggle the
# availability of certain APIs; if we support this for feature types other
# than APIFeature, we may emit warnings that are visible to developers which
# is not desirable.
'PermissionFeatures do not support required_buildflags.'),
],
'all': [
(partial(HasAtLeastOneProperty, ['channel', 'dependencies']),
'Features must specify either a channel or dependencies'),
(DoesNotHaveAllowlistForHostedApps,
'Hosted apps are not allowed to use restricted features'),
],
'APIFeature':
[(partial(HasProperty,
'contexts'), 'APIFeatures must specify the contexts property'),
(partial(DoesNotHaveAllProperties, ['alias', 'source']),
'Features cannot specify both alias and source.')],
'ManifestFeature': [
(partial(HasProperty, 'extension_types'),
'ManifestFeatures must specify at least one extension type'),
(partial(DoesNotHaveProperty,
'contexts'), 'ManifestFeatures do not support contexts.'),
(partial(DoesNotHaveProperty,
'alias'), 'ManifestFeatures do not support alias.'),
(partial(DoesNotHaveProperty,
'source'), 'ManifestFeatures do not support source.'),
# The `required_buildflags` field is intended to be used to toggle the
# availability of certain APIs; if we support this for feature types
# other than APIFeature, we may emit warnings that are visible to
# developers which is not desirable.
(partial(DoesNotHaveProperty, 'required_buildflags'),
'ManifestFeatures do not support required_buildflags.'),
],
'BehaviorFeature': [
(partial(DoesNotHaveProperty,
'alias'), 'BehaviorFeatures do not support alias.'),
(partial(DoesNotHaveProperty,
'source'), 'BehaviorFeatures do not support source.'),
# The `required_buildflags` field is intended to be used to toggle the
# availability of certain APIs; if we support this for feature types
# other than APIFeature, we may emit warnings that are visible to
# developers which is not desirable.
(partial(DoesNotHaveProperty, 'required_buildflags'),
'BehaviorFeatures do not support required_buildflags.'),
],
'PermissionFeature': [
(partial(HasProperty, 'extension_types'),
'PermissionFeatures must specify at least one extension type'),
(partial(DoesNotHaveProperty,
'contexts'), 'PermissionFeatures do not support contexts.'),
(partial(DoesNotHaveProperty,
'alias'), 'PermissionFeatures do not support alias.'),
(partial(DoesNotHaveProperty,
'source'), 'PermissionFeatures do not support source.'),
# The `required_buildflags` field is intended to be used to toggle the
# availability of certain APIs; if we support this for feature types
# other than APIFeature, we may emit warnings that are visible to
# developers which is not desirable.
(partial(DoesNotHaveProperty, 'required_buildflags'),
'PermissionFeatures do not support required_buildflags.'),
],
})
FINAL_VALIDATION = ({
'all': [
# A complex feature requires at least one child entry at all times; with
# `required_buildflags` it becomes harder to guarantee that this holds for
# every potential combination of the provided flags.
(partial(DoesNotHavePropertyInComplexFeature, 'required_buildflags'),
'required_buildflags cannot be nested in a ComplexFeature'),
],
'APIFeature': [
(partial(IsFeatureCrossReference, 'alias', 'source'),
'A feature alias property should reference a feature whose source '
'property references it back.'),
(partial(IsFeatureCrossReference, 'source', 'alias'),
'A feature source property should reference a feature whose alias '
'property references it back.'),
(IsEmptyContextsAllowed,
'An empty contexts list is not allowed for this feature.')
],
'ManifestFeature': [],
'BehaviorFeature': [],
'PermissionFeature': []
'all': [
# A complex feature requires at least one child entry at all times; with
# `required_buildflags` it becomes harder to guarantee that this holds
# for every potential combination of the provided flags.
(partial(DoesNotHavePropertyInComplexFeature, 'required_buildflags'),
'required_buildflags cannot be nested in a ComplexFeature'),
],
'APIFeature':
[(partial(IsFeatureCrossReference, 'alias', 'source'),
'A feature alias property should reference a feature whose source '
'property references it back.'),
(partial(IsFeatureCrossReference, 'source', 'alias'),
'A feature source property should reference a feature whose alias '
'property references it back.'),
(IsEmptyContextsAllowed,
'An empty contexts list is not allowed for this feature.')],
'ManifestFeature': [],
'BehaviorFeature': [],
'PermissionFeature': []
})
# These keys can not be set on a feature and are hence ignored.
@ -523,6 +547,7 @@ IGNORED_KEYS = ['default_parent', 'required_buildflags']
# can be disabled for testing.
ENABLE_ASSERTIONS = True
def GetCodeForFeatureValues(feature_values):
""" Gets the Code object for setting feature values for this object. """
c = Code()
@ -533,10 +558,12 @@ def GetCodeForFeatureValues(feature_values):
c.Append('feature->set_%s(%s);' % (key, feature_values[key]))
return c
class Feature(object):
"""A representation of a single simple feature that can handle all parsing,
validation, and code generation.
"""
def __init__(self, name):
self.name = name
self.has_parent = False
@ -557,10 +584,10 @@ class Feature(object):
"""Adds an error relating to a particular key in the feature.
"""
self.AddError('Error parsing feature "%s" at key "%s": %s' %
(self.name, key, error))
(self.name, key, error))
def _GetCheckedValue(self, key, expected_type, expected_values,
enum_map, value):
def _GetCheckedValue(self, key, expected_type, expected_values, enum_map,
value):
"""Returns a string to be used in the generated C++ code for a given key's
python value, or None if the value is invalid. For example, if the python
value is True, this returns 'true', for a string foo, this returns "foo",
@ -737,13 +764,15 @@ class Feature(object):
return values
def GetErrors(self):
return self.errors;
return self.errors
class ComplexFeature(Feature):
""" Complex feature - feature that is comprised of list of features.
Overall complex feature is available if any of contained
feature is available.
"""
def __init__(self, name):
Feature.__init__(self, name)
self.feature_list = []
@ -779,11 +808,13 @@ class ComplexFeature(Feature):
errors.extend(feature.GetErrors())
return errors
class FeatureCompiler(object):
"""A compiler to load, parse, and generate C++ code for a number of
features.json files."""
def __init__(self, chrome_root, source_files, feature_type,
method_name, out_root, gen_dir_relpath, out_base_filename):
def __init__(self, chrome_root, source_files, feature_type, method_name,
out_root, gen_dir_relpath, out_base_filename):
# See __main__'s ArgumentParser for documentation on these properties.
self._chrome_root = chrome_root
self._source_files = source_files
@ -808,7 +839,7 @@ class FeatureCompiler(object):
f_json = json_parse.Parse(f.read())
except:
print('FAILED: Exception encountered while loading "%s"' %
abs_source_file)
abs_source_file)
raise
dupes = set(f_json) & set(self._json)
assert not dupes, 'Duplicate keys found: %s' % list(dupes)
@ -822,8 +853,8 @@ class FeatureCompiler(object):
no_parent_values = ['noparent' in v for v in feature_value]
no_parent = all(no_parent_values)
assert no_parent or not any(no_parent_values), (
'"%s:" All child features must contain the same noparent value' %
feature_name)
'"%s:" All child features must contain the same noparent value' %
feature_name)
else:
no_parent = 'noparent' in feature_value
sep = feature_name.rfind('.')
@ -889,8 +920,9 @@ class FeatureCompiler(object):
parse_and_validate(feature_name, v, parent, shared_values))
self._features[feature_name] = feature
else:
self._features[feature_name] = parse_and_validate(
feature_name, feature_value, parent, shared_values)
self._features[feature_name] = parse_and_validate(feature_name,
feature_value, parent,
shared_values)
# Apply parent shared values at the end to enable child features to
# override parent shared value - if parent shared values are added to
@ -926,7 +958,8 @@ class FeatureCompiler(object):
required_buildflags = feature.GetValue('required_buildflags')
if required_buildflags:
formatted_buildflags = [
'BUILDFLAG(%s)' % format(flag.upper()) for flag in required_buildflags
'BUILDFLAG(%s)' % format(flag.upper())
for flag in required_buildflags
]
c.Append('#if %s' % format(' && '.join(formatted_buildflags)))
c.Concat(feature.GetCode(self._feature_type))
@ -942,16 +975,20 @@ class FeatureCompiler(object):
header_file = self._out_base_filename + '.h'
cc_file = self._out_base_filename + '.cc'
include_file_root = self._out_root[len(self._gen_dir_relpath)+1:]
include_file_root = self._out_root[len(self._gen_dir_relpath) + 1:]
header_file_path = '%s/%s' % (include_file_root, header_file)
cc_file_path = '%s/%s' % (include_file_root, cc_file)
substitutions = ({
'header_file_path': header_file_path,
'header_guard': (header_file_path.replace('/', '_').
replace('.', '_').upper()),
'method_name': self._method_name,
'source_files': str([ToPosixPath(f) for f in self._source_files]),
'year': str(datetime.now().year)
'header_file_path':
header_file_path,
'header_guard':
(header_file_path.replace('/', '_').replace('.', '_').upper()),
'method_name':
self._method_name,
'source_files':
str([ToPosixPath(f) for f in self._source_files]),
'year':
str(datetime.now().year)
})
if not os.path.exists(self._out_root):
os.makedirs(self._out_root)
@ -973,25 +1010,36 @@ class FeatureCompiler(object):
cc_file.Concat(cc_end)
f.write(cc_file.Render().strip())
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Compile json feature files')
parser.add_argument('chrome_root', type=str,
parser.add_argument('chrome_root',
type=str,
help='The root directory of the chrome checkout')
parser.add_argument(
'feature_type', type=str,
'feature_type',
type=str,
help='The name of the class to use in feature generation ' +
'(e.g. APIFeature, PermissionFeature)')
parser.add_argument('method_name', type=str,
'(e.g. APIFeature, PermissionFeature)')
parser.add_argument('method_name',
type=str,
help='The name of the method to populate the provider')
parser.add_argument('out_root', type=str,
parser.add_argument('out_root',
type=str,
help='The root directory to generate the C++ files into')
parser.add_argument('gen_dir_relpath', default='gen', help='Path of the '
parser.add_argument(
'gen_dir_relpath',
default='gen',
help='Path of the '
'gen directory relative to the out/. If running in the default '
'toolchain, the path is gen, otherwise $toolchain_name/gen')
parser.add_argument(
'out_base_filename', type=str,
'out_base_filename',
type=str,
help='The base filename for the C++ files (.h and .cc will be appended)')
parser.add_argument('source_files', type=str, nargs='+',
parser.add_argument('source_files',
type=str,
nargs='+',
help='The source features.json files')
args = parser.parse_args()
if args.feature_type not in FEATURE_TYPES:

@ -7,6 +7,7 @@ import copy
import feature_compiler
import unittest
class FeatureCompilerTest(unittest.TestCase):
"""Test the FeatureCompiler. Note that we test that the expected features are
generated more thoroughly in features_generation_unittest.cc. And, of course,
@ -14,6 +15,7 @@ class FeatureCompilerTest(unittest.TestCase):
feature fails to parse, the compile fails).
These tests primarily focus on catching errors during parsing.
"""
def _parseFeature(self, value):
"""Parses a feature from the given value and returns the result."""
f = feature_compiler.Feature('alpha')
@ -22,7 +24,8 @@ class FeatureCompilerTest(unittest.TestCase):
def _createTestFeatureCompiler(self, feature_class):
return feature_compiler.FeatureCompiler('chrome_root', [], feature_class,
'provider_class', 'out_root', 'gen', 'out_base_filename')
'provider_class', 'out_root', 'gen',
'out_base_filename')
def _hasError(self, f, error):
"""Asserts that |error| is present somewhere in the given feature's
@ -37,60 +40,69 @@ class FeatureCompilerTest(unittest.TestCase):
def testFeature(self):
# Test some basic feature parsing for a sanity check.
f = self._parseFeature({
'blocklist': [
'ABCDEF0123456789ABCDEF0123456789ABCDEF01',
'10FEDCBA9876543210FEDCBA9876543210FEDCBA'
],
'channel': 'stable',
'command_line_switch': 'switch',
'component_extensions_auto_granted': False,
'contexts': [
'privileged_extension',
'privileged_web_page',
'lock_screen_extension'
],
'default_parent': True,
'dependencies': ['dependency1', 'dependency2'],
'developer_mode_only': True,
'disallow_for_service_workers': True,
'extension_types': ['extension'],
'location': 'component',
'internal': True,
'matches': ['*://*/*'],
'max_manifest_version': 1,
'requires_delegated_availability_check': True,
'noparent': True,
'platforms': ['mac', 'win'],
'session_types': ['kiosk', 'regular'],
'allowlist': [
'0123456789ABCDEF0123456789ABCDEF01234567',
'76543210FEDCBA9876543210FEDCBA9876543210'
],
'required_buildflags': [
'use_cups'
]
'blocklist': [
'ABCDEF0123456789ABCDEF0123456789ABCDEF01',
'10FEDCBA9876543210FEDCBA9876543210FEDCBA'
],
'channel':
'stable',
'command_line_switch':
'switch',
'component_extensions_auto_granted':
False,
'contexts': [
'privileged_extension', 'privileged_web_page',
'lock_screen_extension'
],
'default_parent':
True,
'dependencies': ['dependency1', 'dependency2'],
'developer_mode_only':
True,
'disallow_for_service_workers':
True,
'extension_types': ['extension'],
'location':
'component',
'internal':
True,
'matches': ['*://*/*'],
'max_manifest_version':
1,
'requires_delegated_availability_check':
True,
'noparent':
True,
'platforms': ['mac', 'win'],
'session_types': ['kiosk', 'regular'],
'allowlist': [
'0123456789ABCDEF0123456789ABCDEF01234567',
'76543210FEDCBA9876543210FEDCBA9876543210'
],
'required_buildflags': ['use_cups']
})
self.assertFalse(f.GetErrors())
def testInvalidAll(self):
f = self._parseFeature({
'channel': 'stable',
'dependencies': 'all',
'channel': 'stable',
'dependencies': 'all',
})
self._hasError(f, 'Illegal value: "all"')
def testUnknownKeyError(self):
f = self._parseFeature({
'contexts': ['privileged_extension'],
'channel': 'stable',
'unknownkey': 'unknownvalue'
'contexts': ['privileged_extension'],
'channel': 'stable',
'unknownkey': 'unknownvalue'
})
self._hasError(f, 'Unrecognized key')
def testUnknownEnumValue(self):
f = self._parseFeature({
'contexts': ['privileged_extension', 'unknown_context'],
'channel': 'stable'
'contexts': ['privileged_extension', 'unknown_context'],
'channel':
'stable'
})
self._hasError(f, 'Illegal value: "unknown_context"')
@ -116,15 +128,19 @@ class FeatureCompilerTest(unittest.TestCase):
self.assertFalse(f.GetErrors())
def testApiFeaturesNeedContexts(self):
f = self._parseFeature({'extension_types': ['extension'],
'channel': 'trunk'})
f = self._parseFeature({
'extension_types': ['extension'],
'channel': 'trunk'
})
f.Validate('APIFeature', {})
self._hasError(f, 'APIFeatures must specify the contexts property')
def testAPIFeaturesCanSpecifyEmptyContexts(self):
f = self._parseFeature({'extension_types': ['extension'],
'channel': 'trunk',
'contexts': []})
f = self._parseFeature({
'extension_types': ['extension'],
'channel': 'trunk',
'contexts': []
})
f.Validate('APIFeature', {})
self.assertFalse(f.GetErrors())
@ -135,10 +151,12 @@ class FeatureCompilerTest(unittest.TestCase):
'ManifestFeatures must specify at least one extension type')
def testManifestFeaturesCantHaveContexts(self):
f = self._parseFeature({'dependencies': 'alpha',
'channel': 'beta',
'extension_types': ['extension'],
'contexts': ['privileged_extension']})
f = self._parseFeature({
'dependencies': 'alpha',
'channel': 'beta',
'extension_types': ['extension'],
'contexts': ['privileged_extension']
})
f.Validate('ManifestFeature', {})
self._hasError(f, 'ManifestFeatures do not support contexts')
@ -149,18 +167,20 @@ class FeatureCompilerTest(unittest.TestCase):
f, 'PermissionFeatures must specify at least one extension type')
def testPermissionFeaturesCantHaveContexts(self):
f = self._parseFeature({'dependencies': 'alpha',
'channel': 'beta',
'extension_types': ['extension'],
'contexts': ['privileged_extension']})
f = self._parseFeature({
'dependencies': 'alpha',
'channel': 'beta',
'extension_types': ['extension'],
'contexts': ['privileged_extension']
})
f.Validate('PermissionFeature', {})
self._hasError(f, 'PermissionFeatures do not support contexts')
def testAllPermissionsNeedChannelOrDependencies(self):
api_feature = self._parseFeature({'contexts': ['privileged_extension']})
api_feature.Validate('APIFeature', {})
self._hasError(
api_feature, 'Features must specify either a channel or dependencies')
self._hasError(api_feature,
'Features must specify either a channel or dependencies')
permission_feature = self._parseFeature({'extension_types': ['extension']})
permission_feature.Validate('PermissionFeature', {})
self._hasError(permission_feature,
@ -169,25 +189,28 @@ class FeatureCompilerTest(unittest.TestCase):
manifest_feature.Validate('ManifestFeature', {})
self._hasError(manifest_feature,
'Features must specify either a channel or dependencies')
channel_feature = self._parseFeature({'contexts': ['privileged_extension'],
'channel': 'trunk'})
channel_feature = self._parseFeature({
'contexts': ['privileged_extension'],
'channel': 'trunk'
})
channel_feature.Validate('APIFeature', {})
self.assertFalse(channel_feature.GetErrors())
dependency_feature = self._parseFeature(
{'contexts': ['privileged_extension'],
'dependencies': ['alpha']})
dependency_feature = self._parseFeature({
'contexts': ['privileged_extension'],
'dependencies': ['alpha']
})
dependency_feature.Validate('APIFeature', {})
self.assertFalse(dependency_feature.GetErrors())
def testBothAliasAndSource(self):
compiler = self._createTestFeatureCompiler('APIFeature')
compiler._json = {
'feature_alpha': {
'channel': 'beta',
'contexts': ['privileged_extension'],
'alias': 'feature_alpha',
'source': 'feature_alpha'
}
'feature_alpha': {
'channel': 'beta',
'contexts': ['privileged_extension'],
'alias': 'feature_alpha',
'source': 'feature_alpha'
}
}
compiler.Compile()
@ -198,20 +221,20 @@ class FeatureCompilerTest(unittest.TestCase):
def testAliasOnNonApiFeature(self):
compiler = self._createTestFeatureCompiler('PermissionFeature')
compiler._json = {
'feature_alpha': {
'channel': 'beta',
'contexts': ['privileged_extension'],
'alias': 'feature_beta'
},
'feature_beta': [{
'channel': 'beta',
'contexts': ['privileged_extension'],
'source': 'feature_alpha'
},{
'channel': 'dev',
'context': ['privileged_extension']
}]
};
'feature_alpha': {
'channel': 'beta',
'contexts': ['privileged_extension'],
'alias': 'feature_beta'
},
'feature_beta': [{
'channel': 'beta',
'contexts': ['privileged_extension'],
'source': 'feature_alpha'
}, {
'channel': 'dev',
'context': ['privileged_extension']
}]
}
compiler.Compile()
feature = compiler._features.get('feature_alpha')
@ -225,17 +248,17 @@ class FeatureCompilerTest(unittest.TestCase):
def testAliasFeature(self):
compiler = self._createTestFeatureCompiler('APIFeature')
compiler._json = {
'feature_alpha': {
'channel': 'beta',
'contexts': ['privileged_extension'],
'alias': 'feature_beta'
},
'feature_beta': {
'channel': 'beta',
'contexts': ['privileged_extension'],
'source': 'feature_alpha'
}
};
'feature_alpha': {
'channel': 'beta',
'contexts': ['privileged_extension'],
'alias': 'feature_beta'
},
'feature_beta': {
'channel': 'beta',
'contexts': ['privileged_extension'],
'source': 'feature_alpha'
}
}
compiler.Compile()
feature = compiler._features.get('feature_alpha')
@ -249,40 +272,41 @@ class FeatureCompilerTest(unittest.TestCase):
def testMultipleAliasesInComplexFeature(self):
compiler = self._createTestFeatureCompiler('APIFeature')
compiler._json = {
'feature_alpha': [{
'channel': 'beta',
'contexts': ['privileged_extension'],
'alias': 'feature_beta'
}, {
'contexts': ['privileged_extension'],
'channel': 'beta',
'alias': 'feature_beta'
}]
};
'feature_alpha': [{
'channel': 'beta',
'contexts': ['privileged_extension'],
'alias': 'feature_beta'
}, {
'contexts': ['privileged_extension'],
'channel': 'beta',
'alias': 'feature_beta'
}]
}
compiler.Compile()
feature = compiler._features.get('feature_alpha')
self.assertTrue(feature)
self._hasError(feature, 'Error parsing feature "feature_alpha" at key ' +
'"alias": Key can be set at most once per feature.')
self._hasError(
feature, 'Error parsing feature "feature_alpha" at key ' +
'"alias": Key can be set at most once per feature.')
def testAliasReferenceInComplexFeature(self):
compiler = self._createTestFeatureCompiler('APIFeature')
compiler._json = {
'feature_alpha': [{
'channel': 'beta',
'contexts': ['privileged_extension'],
'alias': 'feature_beta'
}, {
'contexts': ['privileged_extension'],
'channel': 'beta',
}],
'feature_beta': {
'channel': 'beta',
'contexts': ['privileged_extension'],
'source': 'feature_alpha'
}
};
'feature_alpha': [{
'channel': 'beta',
'contexts': ['privileged_extension'],
'alias': 'feature_beta'
}, {
'contexts': ['privileged_extension'],
'channel': 'beta',
}],
'feature_beta': {
'channel': 'beta',
'contexts': ['privileged_extension'],
'source': 'feature_alpha'
}
}
compiler.Compile()
feature = compiler._features.get('feature_alpha')
@ -296,57 +320,58 @@ class FeatureCompilerTest(unittest.TestCase):
def testSourceMissingReference(self):
compiler = self._createTestFeatureCompiler('APIFeature')
compiler._json = {
'feature_alpha': {
'channel': 'beta',
'contexts': ['privileged_extension'],
'alias': 'feature_beta'
},
'feature_beta': {
'contexts': ['privileged_extension'],
'channel': 'beta',
'source': 'does_not_exist'
}
};
'feature_alpha': {
'channel': 'beta',
'contexts': ['privileged_extension'],
'alias': 'feature_beta'
},
'feature_beta': {
'contexts': ['privileged_extension'],
'channel': 'beta',
'source': 'does_not_exist'
}
}
compiler.Compile()
feature = compiler._features.get('feature_beta')
self.assertTrue(feature)
self._hasError(feature, 'A feature source property should reference a ' +
'feature whose alias property references it back.')
self._hasError(
feature, 'A feature source property should reference a ' +
'feature whose alias property references it back.')
def testAliasMissingReferenceInComplexFeature(self):
compiler = self._createTestFeatureCompiler('APIFeature')
compiler._json = {
'feature_alpha': [{
'channel': 'beta',
'contexts': ['privileged_extension'],
'alias': 'feature_beta'
}, {
'contexts': ['privileged_extension'],
'channel': 'beta'
}]
};
'feature_alpha': [{
'channel': 'beta',
'contexts': ['privileged_extension'],
'alias': 'feature_beta'
}, {
'contexts': ['privileged_extension'],
'channel': 'beta'
}]
}
compiler.Compile()
feature = compiler._features.get('feature_alpha')
self.assertTrue(feature)
self._hasError(feature, 'A feature alias property should reference a ' +
'feature whose source property references it back.')
self._hasError(
feature, 'A feature alias property should reference a ' +
'feature whose source property references it back.')
def testAliasReferenceMissingSourceInComplexFeature(self):
compiler = self._createTestFeatureCompiler('APIFeature')
compiler._json = {
'feature_alpha': {
'contexts': ['privileged_extension'],
'channel': 'beta',
},
'feature_beta': {
'channel': 'beta',
'contexts': ['privileged_extension'],
'alias': 'feature_alpha'
}
};
'feature_alpha': {
'contexts': ['privileged_extension'],
'channel': 'beta',
},
'feature_beta': {
'channel': 'beta',
'contexts': ['privileged_extension'],
'alias': 'feature_alpha'
}
}
compiler.Compile()
feature = compiler._features.get('feature_alpha')
@ -355,23 +380,23 @@ class FeatureCompilerTest(unittest.TestCase):
feature = compiler._features.get('feature_beta')
self.assertTrue(feature)
self._hasError(feature, 'A feature alias property should reference a ' +
'feature whose source property references it back.')
self._hasError(
feature, 'A feature alias property should reference a ' +
'feature whose source property references it back.')
def testComplexParentWithoutDefaultParent(self):
c = feature_compiler.FeatureCompiler(
None, None, 'APIFeature', None, None, None, None)
c._CompileFeature('bookmarks',
[{
'contexts': ['privileged_extension'],
}, {
'channel': 'stable',
'contexts': ['webui'],
}])
c = feature_compiler.FeatureCompiler(None, None, 'APIFeature', None, None,
None, None)
c._CompileFeature('bookmarks', [{
'contexts': ['privileged_extension'],
}, {
'channel': 'stable',
'contexts': ['webui'],
}])
with self.assertRaisesRegex(AssertionError,
'No default parent found for bookmarks'):
c._CompileFeature('bookmarks.export', { "allowlist": ["asdf"] })
c._CompileFeature('bookmarks.export', {"allowlist": ["asdf"]})
def testComplexFeatureWithSinglePropertyBlock(self):
compiler = self._createTestFeatureCompiler('APIFeature')
@ -383,16 +408,18 @@ class FeatureCompilerTest(unittest.TestCase):
'feature key instead of a list.')
with self.assertRaisesRegex(AssertionError, error):
compiler._CompileFeature('feature_alpha',
[{
'contexts': ['privileged_extension'],
'channel': 'stable',
}])
[{
'contexts': ['privileged_extension'],
'channel': 'stable',
}])
def testRealIdsDisallowedInAllowlist(self):
fake_id = 'a' * 32;
f = self._parseFeature({'allowlist': [fake_id],
'extension_types': ['extension'],
'channel': 'beta'})
fake_id = 'a' * 32
f = self._parseFeature({
'allowlist': [fake_id],
'extension_types': ['extension'],
'channel': 'beta'
})
f.Validate('PermissionFeature', {})
self._hasError(
f, 'list should only have hex-encoded SHA1 hashes of extension ids')
@ -401,31 +428,34 @@ class FeatureCompilerTest(unittest.TestCase):
f = self._parseFeature({
'extension_types': ['extension', 'hosted_app'],
'allowlist': ['0123456789ABCDEF0123456789ABCDEF01234567'],
'channel': 'beta',
'channel':
'beta',
})
f.Validate('PermissionFeature', {})
self._hasError(f, 'Hosted apps are not allowed to use restricted features')
def testHostedAppsCantUseAllowlistedFeatures_ComplexFeature(self):
c = feature_compiler.FeatureCompiler(
None, None, 'PermissionFeature', None, None, None, None)
c._CompileFeature('invalid_feature',
c = feature_compiler.FeatureCompiler(None, None, 'PermissionFeature', None,
None, None, None)
c._CompileFeature(
'invalid_feature',
[{
'extension_types': ['extension'],
'channel': 'beta',
'extension_types': ['extension'],
'channel': 'beta',
}, {
'channel': 'beta',
'extension_types': ['hosted_app'],
'allowlist': ['0123456789ABCDEF0123456789ABCDEF01234567'],
'channel': 'beta',
'extension_types': ['hosted_app'],
'allowlist': ['0123456789ABCDEF0123456789ABCDEF01234567'],
}])
c._CompileFeature('valid_feature',
c._CompileFeature(
'valid_feature',
[{
'extension_types': ['extension'],
'channel': 'beta',
'allowlist': ['0123456789ABCDEF0123456789ABCDEF01234567'],
'extension_types': ['extension'],
'channel': 'beta',
'allowlist': ['0123456789ABCDEF0123456789ABCDEF01234567'],
}, {
'channel': 'beta',
'extension_types': ['hosted_app'],
'channel': 'beta',
'extension_types': ['hosted_app'],
}])
valid_feature = c._features.get('valid_feature')
@ -437,20 +467,17 @@ class FeatureCompilerTest(unittest.TestCase):
self._hasError(invalid_feature,
'Hosted apps are not allowed to use restricted features')
def testHostedAppsCantUseAllowlistedFeatures_ChildFeature(self):
c = feature_compiler.FeatureCompiler(
None, None, 'PermissionFeature', None, None, None, None)
c._CompileFeature('parent',
{
'extension_types': ['hosted_app'],
'channel': 'beta',
})
c = feature_compiler.FeatureCompiler(None, None, 'PermissionFeature', None,
None, None, None)
c._CompileFeature('parent', {
'extension_types': ['hosted_app'],
'channel': 'beta',
})
c._CompileFeature('parent.child',
{
'allowlist': ['0123456789ABCDEF0123456789ABCDEF01234567']
})
c._CompileFeature(
'parent.child',
{'allowlist': ['0123456789ABCDEF0123456789ABCDEF01234567']})
feature = c._features.get('parent.child')
self.assertTrue(feature)
self._hasError(feature,
@ -459,27 +486,27 @@ class FeatureCompilerTest(unittest.TestCase):
def testEmptyContextsDisallowed(self):
compiler = self._createTestFeatureCompiler('APIFeature')
compiler._json = {
'feature_alpha': {
'channel': 'beta',
'contexts': [],
'extension_types': ['extension']
}
'feature_alpha': {
'channel': 'beta',
'contexts': [],
'extension_types': ['extension']
}
}
compiler.Compile()
feature = compiler._features.get('feature_alpha')
self.assertTrue(feature)
self._hasError(feature,
'An empty contexts list is not allowed for this feature.')
'An empty contexts list is not allowed for this feature.')
def testEmptyContextsAllowed(self):
compiler = self._createTestFeatureCompiler('APIFeature')
compiler._json = {
'empty_contexts': {
'channel': 'beta',
'contexts': [],
'extension_types': ['extension']
}
'empty_contexts': {
'channel': 'beta',
'contexts': [],
'extension_types': ['extension']
}
}
compiler.Compile()
@ -491,18 +518,19 @@ class FeatureCompilerTest(unittest.TestCase):
compiler = self._createTestFeatureCompiler('APIFeature')
compiler._json = {
'feature_cups': {
'channel': 'beta',
'contexts': ['privileged_extension'],
'extension_types': ['extension'],
'required_buildflags': ['use_cups']
}
'feature_cups': {
'channel': 'beta',
'contexts': ['privileged_extension'],
'extension_types': ['extension'],
'required_buildflags': ['use_cups']
}
}
compiler.Compile()
cc_code = compiler.Render()
# The code below is formatted correctly!
self.assertEqual(cc_code.Render(), ''' {
self.assertEqual(
cc_code.Render(), ''' {
#if BUILDFLAG(USE_CUPS)
SimpleFeature* feature = new SimpleFeature();
feature->set_name("feature_cups");
@ -513,5 +541,6 @@ class FeatureCompilerTest(unittest.TestCase):
#endif
}''')
if __name__ == '__main__':
unittest.main()

@ -9,6 +9,7 @@ import cpp_util
class CCGenerator(object):
def Generate(self, feature_defs, source_file, namespace):
return _Generator(feature_defs, source_file, namespace).Generate()
@ -16,6 +17,7 @@ class CCGenerator(object):
class _Generator(object):
"""A .cc generator for features.
"""
def __init__(self, feature_defs, source_file, namespace):
self._feature_defs = feature_defs
self._source_file = source_file
@ -27,54 +29,53 @@ class _Generator(object):
"""Generates a Code object for features.
"""
c = Code()
(c.Append(cpp_util.CHROMIUM_LICENSE)
.Append()
(c.Append(cpp_util.CHROMIUM_LICENSE) \
.Append() \
.Append(cpp_util.GENERATED_FEATURE_MESSAGE %
cpp_util.ToPosixPath(self._source_file))
.Append()
.Append('#include <string>')
.Append()
cpp_util.ToPosixPath(self._source_file)) \
.Append() \
.Append('#include <string>') \
.Append() \
.Append('#include "%s.h"' %
cpp_util.ToPosixPath(self._source_file_filename))
.Append()
.Append('#include "base/notreached.h"')
.Append()
.Concat(cpp_util.OpenNamespace(self._namespace))
cpp_util.ToPosixPath(self._source_file_filename)) \
.Append() \
.Append('#include "base/notreached.h"') \
.Append() \
.Concat(cpp_util.OpenNamespace(self._namespace)) \
.Append()
)
# Generate the constructor.
(c.Append('%s::%s() {' % (self._class_name, self._class_name))
(c.Append('%s::%s() {' % (self._class_name, self._class_name)) \
.Sblock()
)
for feature in self._feature_defs:
c.Append('features_["%s"] = %s;'
% (feature.name,
cpp_util.FeatureNameToConstantName(feature.name)))
(c.Eblock()
.Append('}')
c.Append('features_["%s"] = %s;' %
(feature.name, cpp_util.FeatureNameToConstantName(feature.name)))
(c.Eblock() \
.Append('}') \
.Append()
)
# Generate the ToString function.
(c.Append('const char* %s::ToString('
'%s::ID id) const {' % (self._class_name, self._class_name))
.Sblock()
.Append('switch (id) {')
'%s::ID id) const {' % (self._class_name, self._class_name)) \
.Sblock() \
.Append('switch (id) {') \
.Sblock()
)
for feature in self._feature_defs:
c.Append('case %s: return "%s";' %
(cpp_util.FeatureNameToConstantName(feature.name), feature.name))
(c.Append('case kUnknown: break;')
.Append('case kEnumBoundary: break;')
.Eblock()
.Append('}')
.Append('NOTREACHED_IN_MIGRATION();')
(cpp_util.FeatureNameToConstantName(feature.name), feature.name))
(c.Append('case kUnknown: break;') \
.Append('case kEnumBoundary: break;') \
.Eblock() \
.Append('}') \
.Append('NOTREACHED_IN_MIGRATION();') \
.Append('return "";')
)
(c.Eblock()
.Append('}')
(c.Eblock() \
.Append('}') \
.Append()
)
@ -82,13 +83,13 @@ class _Generator(object):
(c.Append('%s::ID %s::FromString('
'const std::string& id) const {'
% (self._class_name, self._class_name))
.Sblock()
.Append('const auto& it = features_.find(id);' % self._class_name)
.Append('return (it == features_.end()) ? kUnknown : it->second;')
.Eblock()
.Append('}')
.Append()
% (self._class_name, self._class_name)) \
.Sblock() \
.Append('const auto& it = features_.find(id);' % self._class_name) \
.Append('return (it == features_.end()) ? kUnknown : it->second;') \
.Eblock() \
.Append('}') \
.Append() \
.Cblock(cpp_util.CloseNamespace(self._namespace))
)

@ -23,9 +23,7 @@ def _GenerateSchema(filename, root, destdir, namespace):
# Load in the feature permissions from the JSON file.
schema = os.path.normpath(filename)
schema_loader = SchemaLoader(os.path.dirname(os.path.relpath(schema, root)),
os.path.dirname(schema),
[],
None)
os.path.dirname(schema), [], None)
schema_filename = os.path.splitext(schema)[0]
feature_defs = schema_loader.LoadSchema(schema)
@ -38,10 +36,8 @@ def _GenerateSchema(filename, root, destdir, namespace):
relpath = os.path.relpath(os.path.normpath(source_file_dir), root)
full_path = os.path.join(relpath, schema)
generators = [
('%s.cc' % schema_filename, CCGenerator()),
('%s.h' % schema_filename, HGenerator())
]
generators = [('%s.cc' % schema_filename, CCGenerator()),
('%s.h' % schema_filename, HGenerator())]
# Generate and output the code for all features.
output_code = []
@ -59,12 +55,19 @@ if __name__ == '__main__':
parser = optparse.OptionParser(
description='Generates a C++ features model from JSON schema',
usage='usage: %prog [option]... schema')
parser.add_option('-r', '--root', default='.',
parser.add_option(
'-r',
'--root',
default='.',
help='logical include root directory. Path to schema files from '
'specified dir will be the include path.')
parser.add_option('-d', '--destdir',
help='root directory to output generated files.')
parser.add_option('-n', '--namespace', default='generated_features',
'specified dir will be the include path.')
parser.add_option('-d',
'--destdir',
help='root directory to output generated files.')
parser.add_option(
'-n',
'--namespace',
default='generated_features',
help='C++ namespace for generated files. e.g extensions::api.')
(opts, filenames) = parser.parse_args()

@ -9,6 +9,7 @@ import cpp_util
class HGenerator(object):
def Generate(self, features, source_file, namespace):
return _Generator(features, source_file, namespace).Generate()
@ -16,6 +17,7 @@ class HGenerator(object):
class _Generator(object):
"""A .cc generator for features.
"""
def __init__(self, features, source_file, namespace):
self._feature_defs = features
self._source_file = source_file
@ -27,10 +29,10 @@ class _Generator(object):
"""Generates a Code object for features.
"""
c = Code()
(c.Append(cpp_util.CHROMIUM_LICENSE)
.Append()
(c.Append(cpp_util.CHROMIUM_LICENSE) \
.Append() \
.Append(cpp_util.GENERATED_FEATURE_MESSAGE %
cpp_util.ToPosixPath(self._source_file))
cpp_util.ToPosixPath(self._source_file)) \
.Append()
)
@ -39,31 +41,31 @@ class _Generator(object):
output_file = os.path.splitext(self._namespace.source_file)[0] + '.h'
ifndef_name = cpp_util.GenerateIfndefName(output_file)
(c.Append('#ifndef %s' % ifndef_name)
.Append('#define %s' % ifndef_name)
(c.Append('#ifndef %s' % ifndef_name) \
.Append('#define %s' % ifndef_name) \
.Append()
)
(c.Append('#include <map>')
.Append('#include <string>')
.Append()
.Concat(cpp_util.OpenNamespace(self._namespace))
(c.Append('#include <map>') \
.Append('#include <string>') \
.Append() \
.Concat(cpp_util.OpenNamespace(self._namespace)) \
.Append()
)
(c.Append('class %s {' % self._class_name)
.Append(' public:')
.Sblock()
.Concat(self._GeneratePublicBody())
.Eblock()
.Append(' private:')
.Sblock()
.Concat(self._GeneratePrivateBody())
.Eblock('};')
.Append()
(c.Append('class %s {' % self._class_name) \
.Append(' public:') \
.Sblock() \
.Concat(self._GeneratePublicBody()) \
.Eblock() \
.Append(' private:') \
.Sblock() \
.Concat(self._GeneratePrivateBody()) \
.Eblock('};') \
.Append() \
.Cblock(cpp_util.CloseNamespace(self._namespace))
)
(c.Append('#endif // %s' % ifndef_name)
(c.Append('#endif // %s' % ifndef_name) \
.Append()
)
return c
@ -71,14 +73,14 @@ class _Generator(object):
def _GeneratePublicBody(self):
c = Code()
(c.Append('%s();' % self._class_name)
.Append()
.Append('enum ID {')
.Concat(self._GenerateEnumConstants())
.Eblock('};')
.Append()
.Append('const char* ToString(ID id) const;')
.Append('ID FromString(const std::string& id) const;')
(c.Append('%s();' % self._class_name) \
.Append() \
.Append('enum ID {') \
.Concat(self._GenerateEnumConstants()) \
.Eblock('};') \
.Append() \
.Append('const char* ToString(ID id) const;') \
.Append('ID FromString(const std::string& id) const;') \
.Append()
)
return c
@ -90,7 +92,7 @@ class _Generator(object):
def _GenerateEnumConstants(self):
c = Code()
(c.Sblock()
(c.Sblock() \
.Append('kUnknown,')
)
for feature in self._feature_defs:

@ -2,7 +2,6 @@
# Copyright 2022 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Helper for quickly generating all known JS externs."""
import argparse
@ -31,6 +30,7 @@ REPO_ROOT = os.path.dirname(os.path.dirname(DIR))
# Import the helper module.
sys.path.insert(0, os.path.join(REPO_ROOT, 'extensions', 'common', 'api'))
from externs_checker import ExternsChecker
sys.path.pop(0)
@ -72,6 +72,7 @@ class FakeOutputApi:
"""
class PresubmitResult:
def __init__(self, msg, long_text=None):
self.msg = msg
self.long_text = long_text
@ -112,7 +113,7 @@ def Generate(input_api, output_api, force=False, dryrun=False):
externs_relpath = input_api.os_path.relpath(externs, src_root)
print('\r' + ' ' * msg_len, end='\r')
msg = 'Checking %s ...' % (source_relpath,)
msg = 'Checking %s ...' % (source_relpath, )
msg_len = len(msg)
print(msg, end='')
sys.stdout.flush()
@ -123,9 +124,9 @@ def Generate(input_api, output_api, force=False, dryrun=False):
if not dryrun:
print('\n%s: %s' % (source_relpath, e))
ret.append(
output_api.PresubmitResult(
'%s: unable to generate' % (source_relpath,),
long_text=str(e)))
output_api.PresubmitResult('%s: unable to generate' %
(source_relpath, ),
long_text=str(e)))
continue
# Ignore the first line (copyright) to avoid yearly thrashing.
@ -148,7 +149,7 @@ def Generate(input_api, output_api, force=False, dryrun=False):
if not dryrun:
print('\r' + ' ' * msg_len, end='\r')
msg_len = 0
print('Updating %s' % (externs_relpath,))
print('Updating %s' % (externs_relpath, ))
with open(externs, 'w', encoding='utf-8') as fp:
fp.write(copyright + '\n')
fp.write(new_data)
@ -161,11 +162,16 @@ def Generate(input_api, output_api, force=False, dryrun=False):
def get_parser():
"""Get CLI parser."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('-n', '--dry-run', dest='dryrun', action='store_true',
parser.add_argument('-n',
'--dry-run',
dest='dryrun',
action='store_true',
help="Don't make changes; only show changed files")
parser.add_argument('-f', '--force', action='store_true',
parser.add_argument('-f',
'--force',
action='store_true',
help='Regenerate files even if they have a TODO '
'disabling generation')
'disabling generation')
return parser
@ -174,7 +180,9 @@ def main(argv):
parser = get_parser()
opts = parser.parse_args(argv)
results = Generate(FakeInputApi(), FakeOutputApi(), force=opts.force,
results = Generate(FakeInputApi(),
FakeOutputApi(),
force=opts.force,
dryrun=opts.dryrun)
if opts.dryrun and results:
for result in results:

@ -9,7 +9,9 @@ from model import PropertyType, Type, Property
import cpp_util
import schema_util
class HGenerator(object):
def __init__(self, type_generator):
self._type_generator = type_generator
@ -20,6 +22,7 @@ class HGenerator(object):
class _Generator(object):
"""A .h generator for a namespace.
"""
def __init__(self, namespace, cpp_type_generator):
self._namespace = namespace
self._type_helper = cpp_type_generator
@ -30,10 +33,10 @@ class _Generator(object):
"""Generates a Code object with the .h for a single namespace.
"""
c = Code()
(c.Append(cpp_util.CHROMIUM_LICENSE)
.Append()
(c.Append(cpp_util.CHROMIUM_LICENSE) \
.Append() \
.Append(cpp_util.GENERATED_FILE_MESSAGE %
cpp_util.ToPosixPath(self._namespace.source_file))
cpp_util.ToPosixPath(self._namespace.source_file)) \
.Append()
)
@ -48,21 +51,21 @@ class _Generator(object):
# non-optional types from other namespaces.
include_soft = self._namespace.name not in ('tabs', 'windows')
(c.Append('#ifndef %s' % ifndef_name)
.Append('#define %s' % ifndef_name)
.Append()
.Append('#include <stdint.h>')
.Append()
.Append('#include <map>')
.Append('#include <memory>')
.Append('#include <optional>')
.Append('#include <string>')
.Append('#include <vector>')
.Append()
.Append('#include "base/values.h"')
(c.Append('#ifndef %s' % ifndef_name) \
.Append('#define %s' % ifndef_name) \
.Append() \
.Append('#include <stdint.h>') \
.Append() \
.Append('#include <map>') \
.Append('#include <memory>') \
.Append('#include <optional>') \
.Append('#include <string>') \
.Append('#include <vector>') \
.Append() \
.Append('#include "base/values.h"') \
.Cblock(self._type_helper.GenerateIncludes(
include_soft=include_soft,
generate_error_messages=self._generate_error_messages))
generate_error_messages=self._generate_error_messages)) \
.Append()
)
@ -77,22 +80,21 @@ class _Generator(object):
c.Concat(cpp_util.OpenNamespace(cpp_namespace))
c.Append()
if self._namespace.properties:
(c.Append('//')
.Append('// Properties')
.Append('//')
(c.Append('//') \
.Append('// Properties') \
.Append('//') \
.Append()
)
for prop in self._namespace.properties.values():
property_code = self._type_helper.GeneratePropertyValues(
prop,
'extern const %(type)s %(name)s;')
prop, 'extern const %(type)s %(name)s;')
if property_code:
c.Cblock(property_code)
if self._namespace.types:
(c.Append('//')
.Append('// Types')
.Append('//')
.Append()
(c.Append('//') \
.Append('// Types') \
.Append('//') \
.Append() \
.Cblock(self._GenerateTypes(self._FieldDependencyOrder(),
is_toplevel=True,
generate_typedefs=True))
@ -104,24 +106,24 @@ class _Generator(object):
c.Append()
c.Cblock(self._GenerateManifestKeys())
if self._namespace.functions:
(c.Append('//')
.Append('// Functions')
.Append('//')
(c.Append('//') \
.Append('// Functions') \
.Append('//') \
.Append()
)
for function in self._namespace.functions.values():
c.Cblock(self._GenerateFunction(function))
if self._namespace.events:
(c.Append('//')
.Append('// Events')
.Append('//')
(c.Append('//') \
.Append('// Events') \
.Append('//') \
.Append()
)
for event in self._namespace.events.values():
c.Cblock(self._GenerateEvent(event))
(c.Concat(cpp_util.CloseNamespace(cpp_namespace))
.Append()
.Append('#endif // %s' % ifndef_name)
(c.Concat(cpp_util.CloseNamespace(cpp_namespace)) \
.Append() \
.Append('#endif // %s' % ifndef_name) \
.Append()
)
return c
@ -137,8 +139,8 @@ class _Generator(object):
raise ValueError("Illegal circular dependency via cycle " +
", ".join(map(lambda x: x.name, path + [type_])))
for prop in type_.properties.values():
if (prop.type_ == PropertyType.REF and
schema_util.GetNamespace(prop.ref_type) == self._namespace.name):
if (prop.type_ == PropertyType.REF and schema_util.GetNamespace(
prop.ref_type) == self._namespace.name):
ExpandType(path + [type_], self._namespace.types[prop.ref_type])
if not type_ in dependency_order:
dependency_order.append(type_)
@ -151,23 +153,23 @@ class _Generator(object):
"""Generate a code object with the declaration of a C++ enum.
"""
c = Code()
c.Sblock('enum class {name} {{'.format(
name=enum_name))
c.Sblock('enum class {name} {{'.format(name=enum_name))
# Explicitly initialize kNone to 0, since we rely on default initialization
# for enum members. Otherwise, default initialization will always set a
# value to 0, even if it's not a valid enum entry.
c.Append(
self._type_helper.GetEnumNoneValue(type_, full_name=False) + ' = 0,')
self._type_helper.GetEnumNoneValue(type_, full_name=False) + ' = 0,')
for value in type_.enum_values:
current_enum_string = (
self._type_helper.GetEnumValue(type_, value, full_name=False))
current_enum_string = (self._type_helper.GetEnumValue(type_,
value,
full_name=False))
c.Append(current_enum_string + ',')
# Adding kMaxValue, which is friendly to enumaration histogram macros.
c.Append('kMaxValue = {last_key_value},'.format(
last_key_value=current_enum_string))
last_key_value=current_enum_string))
c.Eblock('};')
return c
@ -183,10 +185,8 @@ class _Generator(object):
needs_blank_line = True
if prop.description:
c.Comment(prop.description)
(c.Append('%s %s;' % (
self._type_helper.GetCppType(prop.type_, is_optional=prop.optional),
prop.unix_name))
)
(c.Append('%s %s;' % (self._type_helper.GetCppType(
prop.type_, is_optional=prop.optional), prop.unix_name)))
return c
def _GenerateType(self, type_, is_toplevel=False, generate_typedefs=False):
@ -204,7 +204,7 @@ class _Generator(object):
if type_.functions:
# Wrap functions within types in the type's namespace.
(c.Append('namespace %s {' % classname)
(c.Append('namespace %s {' % classname) \
.Append()
)
for function in type_.functions.values():
@ -217,10 +217,8 @@ class _Generator(object):
if generate_typedefs:
item_cpp_type = self._type_helper.GetCppType(type_.item_type)
if item_cpp_type != 'base::Value':
(c.Append('using %s = std::vector<%s >;' % (
classname,
item_cpp_type))
)
(c.Append('using %s = std::vector<%s >;' %
(classname, item_cpp_type)))
else:
c.Append('using %s = base::Value::List;' % classname)
elif type_.property_type == PropertyType.STRING:
@ -235,26 +233,25 @@ class _Generator(object):
# Top level enums are in a namespace scope so the methods shouldn't be
# static. On the other hand, those declared inline (e.g. in an object) do.
maybe_static = '' if is_toplevel else 'static '
(c.Append()
(c.Append() \
.Append('%sconst char* ToString(%s as_enum);' %
(maybe_static, classname))
(maybe_static, classname)) \
.Append('%s%s Parse%s(std::string_view as_string);' %
(maybe_static, classname, classname))
(maybe_static, classname, classname)) \
.Append(
'%sstd::u16string Get%sParseError(std::string_view as_string);' %
(maybe_static, classname))
)
elif type_.property_type in (PropertyType.CHOICES,
PropertyType.OBJECT):
elif type_.property_type in (PropertyType.CHOICES, PropertyType.OBJECT):
if type_.description:
c.Comment(type_.description)
(c.Sblock('struct %(classname)s {')
.Append('%(classname)s();')
.Append('~%(classname)s();')
.Append('%(classname)s(const %(classname)s&) = delete;')
.Append('%(classname)s& operator=(const %(classname)s&) = delete;')
.Append('%(classname)s(%(classname)s&& rhs) noexcept;')
(c.Sblock('struct %(classname)s {') \
.Append('%(classname)s();') \
.Append('~%(classname)s();') \
.Append('%(classname)s(const %(classname)s&) = delete;') \
.Append('%(classname)s& operator=(const %(classname)s&) = delete;') \
.Append('%(classname)s(%(classname)s&& rhs) noexcept;') \
.Append('%(classname)s& operator=(%(classname)s&& rhs) noexcept;')
)
@ -263,28 +260,27 @@ class _Generator(object):
c.Comment('Manifest key constants.')
c.Concat(self._GenerateManifestKeyConstants(type_.properties.values()))
value_type = ('base::Value'
if type_.property_type is PropertyType.CHOICES else
'base::Value::Dict')
value_type = ('base::Value' if type_.property_type is PropertyType.CHOICES
else 'base::Value::Dict')
if (type_.origin.from_json or
(type_.origin.from_manifest_keys and
type_.property_type is PropertyType.CHOICES)):
(c.Append()
if (type_.origin.from_json
or (type_.origin.from_manifest_keys
and type_.property_type is PropertyType.CHOICES)):
(c.Append() \
.Comment('Populates a %s object from a base::Value& instance. Returns'
' whether |out| was successfully populated.' % classname)
' whether |out| was successfully populated.' % classname) \
.Append('static bool Populate(%s);' % self._GenerateParams(
('const base::Value& value', '%s& out' % classname)))
)
if type_.property_type is not PropertyType.CHOICES:
(c.Append()
(c.Append() \
.Comment('Populates a %s object from a Dict& instance. Returns'
' whether |out| was successfully populated.' % classname)
' whether |out| was successfully populated.' % classname) \
.Append('static bool Populate(%s);' % self._GenerateParams(
('const base::Value::Dict& value', '%s& out' % classname)))
)
(c.Append()
.Comment('Creates a deep copy of %s.' % classname)
(c.Append() \
.Comment('Creates a deep copy of %s.' % classname) \
.Append('%s Clone() const;' % classname)
)
@ -292,32 +288,32 @@ class _Generator(object):
classname, support_errors=self._generate_error_messages)
if type_.property_type is not PropertyType.CHOICES:
(c.Append()
(c.Append() \
.Comment('Creates a {classname} object from a base::Value::Dict,'
' or {failure} on failure.'.format(
classname=classname,
failure=('unexpected'
if self._generate_error_messages else 'nullopt')))
if self._generate_error_messages else 'nullopt'))) \
.Append('static {return_type} '
'FromValue(const base::Value::Dict& value);'.format(
return_type=return_type))
)
(c.Append()
(c.Append() \
.Comment('Creates a {classname} object from a base::Value,'
' or {failure} on failure.'.format(
classname=classname,
failure=('unexpected'
if self._generate_error_messages else 'nullopt')))
if self._generate_error_messages else 'nullopt'))) \
.Append('static {return_type} '
'FromValue(const base::Value& value);'.format(
return_type=return_type))
)
if type_.origin.from_client:
(c.Append()
(c.Append() \
.Comment('Returns a new %s representing the serialized form of this'
'%s object.' % (value_type, classname))
'%s object.' % (value_type, classname)) \
.Append('%s ToValue() const;' % value_type)
)
@ -330,13 +326,12 @@ class _Generator(object):
c.Cblock(self._GenerateTypes(type_.choices))
c.Append('// Choices:')
for choice_type in type_.choices:
c.Append('%s as_%s;' % (
self._type_helper.GetCppType(choice_type, is_optional=True),
choice_type.unix_name))
c.Append('%s as_%s;' % (self._type_helper.GetCppType(
choice_type, is_optional=True), choice_type.unix_name))
else:
properties = type_.properties.values()
(c.Append()
.Cblock(self._GenerateTypes(p.type_ for p in properties))
(c.Append() \
.Cblock(self._GenerateTypes(p.type_ for p in properties)) \
.Cblock(self._GenerateFields(properties)))
if type_.additional_properties is not None:
# Most additionalProperties actually have type "any", which is better
@ -344,7 +339,7 @@ class _Generator(object):
if type_.additional_properties.property_type == PropertyType.ANY:
c.Append('base::Value::Dict additional_properties;')
else:
(c.Cblock(self._GenerateType(type_.additional_properties))
(c.Cblock(self._GenerateType(type_.additional_properties)) \
.Append('std::map<std::string, %s> additional_properties;' %
self._type_helper.GetCppType(type_.additional_properties))
)
@ -357,10 +352,10 @@ class _Generator(object):
c = Code()
# TODO(kalman): use event.unix_name not Classname.
event_namespace = cpp_util.Classname(event.name)
(c.Append('namespace %s {' % event_namespace)
.Append()
.Concat(self._GenerateEventNameConstant(event))
.Concat(self._GenerateAsyncResponseArguments(event.params))
(c.Append('namespace %s {' % event_namespace) \
.Append() \
.Concat(self._GenerateEventNameConstant(event)) \
.Concat(self._GenerateAsyncResponseArguments(event.params)) \
.Append('} // namespace %s' % event_namespace)
)
return c
@ -375,8 +370,8 @@ class _Generator(object):
# to not use the name.
if function_namespace == 'SendMessage':
function_namespace = 'PassMessage'
(c.Append('namespace %s {' % function_namespace)
.Append()
(c.Append('namespace %s {' % function_namespace) \
.Append() \
.Cblock(self._GenerateFunctionParams(function))
)
if function.returns_async:
@ -394,26 +389,26 @@ class _Generator(object):
(c.Sblock('struct Params {'))
if self._generate_error_messages:
(c.Append('static base::expected<Params, std::u16string> '
'Create(const base::Value::List& args);')
'Create(const base::Value::List& args);') \
.Comment('DEPRECATED: prefer the variant of this function '
'returning errors with `base::expected`.')
)
(c.Append('static std::optional<Params> Create(%s);' %
self._GenerateParams(
('const base::Value::List& args',)))
.Append('Params(const Params&) = delete;')
.Append('Params& operator=(const Params&) = delete;')
.Append('Params(Params&& rhs) noexcept;')
.Append('Params& operator=(Params&& rhs) noexcept;')
.Append('~Params();')
.Append()
.Cblock(self._GenerateTypes(p.type_ for p in function.params))
.Cblock(self._GenerateFields(function.params))
.Eblock()
.Append()
.Sblock(' private:')
.Append('Params();')
('const base::Value::List& args',))) \
.Append('Params(const Params&) = delete;') \
.Append('Params& operator=(const Params&) = delete;') \
.Append('Params(Params&& rhs) noexcept;') \
.Append('Params& operator=(Params&& rhs) noexcept;') \
.Append('~Params();') \
.Append() \
.Cblock(self._GenerateTypes(p.type_ for p in function.params)) \
.Cblock(self._GenerateFields(function.params)) \
.Eblock() \
.Append() \
.Sblock(' private:') \
.Append('Params();') \
.Eblock('};')
)
return c
@ -424,9 +419,10 @@ class _Generator(object):
"""
c = Code()
for type_ in types:
c.Cblock(self._GenerateType(type_,
is_toplevel=is_toplevel,
generate_typedefs=generate_typedefs))
c.Cblock(
self._GenerateType(type_,
is_toplevel=is_toplevel,
generate_typedefs=generate_typedefs))
return c
def _GenerateManifestKeys(self):
@ -446,26 +442,23 @@ class _Generator(object):
# manifest types.
if type_.IsRootManifestKeyType():
params = [
'const base::Value::Dict& root_dict',
'%s& out' % classname,
'std::u16string& error'
'const base::Value::Dict& root_dict',
'%s& out' % classname, 'std::u16string& error'
]
comment = (
'Parses manifest keys for this namespace. Any keys not available to the'
' manifest will be ignored. On a parsing error, false is returned and '
'|error| is populated.')
'Parses manifest keys for this namespace. Any keys not available to'
' the manifest will be ignored. On a parsing error, false is returned'
' and |error| is populated.')
else:
params = [
'const base::Value::Dict& root_dict',
'std::string_view key',
'%s& out' % classname,
'std::u16string& error',
'std::vector<std::string_view>& error_path_reversed'
'const base::Value::Dict& root_dict', 'std::string_view key',
'%s& out' % classname, 'std::u16string& error',
'std::vector<std::string_view>& error_path_reversed'
]
comment = (
'Parses the given |key| from |root_dict|. Any keys not available to the'
' manifest will be ignored. On a parsing error, false is returned and '
'|error| and |error_path_reversed| are populated.')
'Parses the given |key| from |root_dict|. Any keys not available to'
' the manifest will be ignored. On a parsing error, false is returned'
' and |error| and |error_path_reversed| are populated.')
c = Code()
c.Append().Comment(comment)
@ -499,18 +492,18 @@ class _Generator(object):
for param in params:
if param.description:
c.Comment(param.description)
declaration_list.append(cpp_util.GetParameterDeclaration(
param, self._type_helper.GetCppType(param.type_)))
c.Append('base::Value::List Create(%s);' %
', '.join(declaration_list))
declaration_list.append(
cpp_util.GetParameterDeclaration(
param, self._type_helper.GetCppType(param.type_)))
c.Append('base::Value::List Create(%s);' % ', '.join(declaration_list))
return c
def _GenerateEventNameConstant(self, event):
"""Generates a constant string array for the event name.
"""
c = Code()
c.Append('extern const char kEventName[]; // "%s.%s"' % (
self._namespace.name, event.name))
c.Append('extern const char kEventName[]; // "%s.%s"' %
(self._namespace.name, event.name))
c.Append()
return c
@ -518,15 +511,14 @@ class _Generator(object):
"""Generates namespace for passing a function's result back.
"""
c = Code()
(c.Append('namespace Results {')
.Append()
.Concat(self._GenerateAsyncResponseArguments(returns_async.params))
(c.Append('namespace Results {') \
.Append() \
.Concat(self._GenerateAsyncResponseArguments(returns_async.params)) \
.Append('} // namespace Results')
)
return c
def _GenerateParams(
self, params, generate_error_messages=None):
def _GenerateParams(self, params, generate_error_messages=None):
"""Builds the parameter list for a function, given an array of parameters.
If |generate_error_messages| is specified, it overrides
|self._generate_error_messages|.
@ -539,5 +531,5 @@ class _Generator(object):
if generate_error_messages is None:
generate_error_messages = self._generate_error_messages
if generate_error_messages:
params += ('std::u16string& error',)
params += ('std::u16string& error', )
return ', '.join(str(p) for p in params)

@ -32,6 +32,7 @@ else:
finally:
sys.path.pop(0)
def ProcessComment(comment):
'''
Convert a comment into a parent comment and a list of parameter comments.
@ -57,6 +58,7 @@ def ProcessComment(comment):
}
)
'''
def add_paragraphs(content):
paragraphs = content.split('\n\n')
if len(paragraphs) < 2:
@ -69,8 +71,8 @@ def ProcessComment(comment):
# Get the parent comment (everything before the first parameter comment.
first_parameter_location = (parameter_starts[0].start()
if parameter_starts else len(comment))
parent_comment = (add_paragraphs(comment[:first_parameter_location].strip())
.replace('\n', ''))
parent_comment = (add_paragraphs(
comment[:first_parameter_location].strip()).replace('\n', ''))
params = OrderedDict()
for (cur_param, next_param) in itertools.zip_longest(parameter_starts,
@ -81,9 +83,9 @@ def ProcessComment(comment):
# beginning of the next parameter's introduction.
param_comment_start = cur_param.end()
param_comment_end = next_param.start() if next_param else len(comment)
params[param_name] = (
add_paragraphs(comment[param_comment_start:param_comment_end].strip())
.replace('\n', ''))
params[param_name] = (add_paragraphs(
comment[param_comment_start:param_comment_end].strip()).replace(
'\n', ''))
return (parent_comment, params)
@ -94,6 +96,7 @@ class Callspec(object):
a tuple:
(name, list of function parameters, return type, async return)
'''
def __init__(self, callspec_node, comment):
self.node = callspec_node
self.comment = comment
@ -103,9 +106,10 @@ class Callspec(object):
return_type = None
returns_async = None
if self.node.GetProperty('TYPEREF') not in ('void', None):
return_type = Typeref(self.node.GetProperty('TYPEREF'),
self.node.parent,
{'name': self.node.GetName()}).process(callbacks)
return_type = Typeref(self.node.GetProperty('TYPEREF'), self.node.parent,
{
'name': self.node.GetName()
}).process(callbacks)
# The IDL parser doesn't allow specifying return types as optional.
# Instead we infer any object return values to be optional.
# TODO(asargent): fix the IDL parser to support optional return types.
@ -127,32 +131,25 @@ class Callspec(object):
# trailingCallbackIsFunctionParameter extended attribute).
# TODO(tjudkins): Once IDL definitions are changed to describe returning
# promises, we can condition on that instead.
if (
use_returns_async
if (use_returns_async
and not self.node.GetProperty('trailingCallbackIsFunctionParameter')
and len(parameters) > 0
and parameters[-1].get('type') == 'function'
):
and len(parameters) > 0 and parameters[-1].get('type') == 'function'):
returns_async = parameters.pop()
# The returns_async field is inherently a function, so doesn't need type
# specified on it.
returns_async.pop('type')
does_not_support_promises = self.node.GetProperty(
'doesNotSupportPromises'
)
'doesNotSupportPromises')
if does_not_support_promises is not None:
returns_async['does_not_support_promises'] = does_not_support_promises
else:
assert return_type is None, (
'Function "%s" cannot support promises and also have a '
'return value.' % self.node.GetName()
)
'return value.' % self.node.GetName())
else:
assert not self.node.GetProperty('doesNotSupportPromises'), (
'Callspec "%s" does not need to specify [doesNotSupportPromises] if '
'it does not have a trailing callback'
% self.node.GetName()
)
'it does not have a trailing callback' % self.node.GetName())
return (self.node.GetName(), parameters, return_type, returns_async)
@ -162,13 +159,14 @@ class Param(object):
Given a Param node representing a function parameter, converts into a Python
dictionary that the JSON schema compiler expects to see.
'''
def __init__(self, param_node):
self.node = param_node
def process(self, callbacks):
return Typeref(self.node.GetProperty('TYPEREF'),
self.node,
{'name': self.node.GetName()}).process(callbacks)
return Typeref(self.node.GetProperty('TYPEREF'), self.node, {
'name': self.node.GetName()
}).process(callbacks)
class Dictionary(object):
@ -176,6 +174,7 @@ class Dictionary(object):
Given an IDL Dictionary node, converts into a Python dictionary that the JSON
schema compiler expects to see.
'''
def __init__(self, dictionary_node):
self.node = dictionary_node
@ -185,9 +184,11 @@ class Dictionary(object):
if node.cls == 'Member':
k, v = Member(node).process(callbacks)
properties[k] = v
result = {'id': self.node.GetName(),
'properties': properties,
'type': 'object'}
result = {
'id': self.node.GetName(),
'properties': properties,
'type': 'object'
}
# If this has the `ignoreAdditionalProperties` extended attribute, copy it
# into the resulting object with a value of True.
if self.node.GetProperty('ignoreAdditionalProperties'):
@ -208,19 +209,22 @@ class Member(object):
where the value is a Python dictionary that the JSON schema compiler expects
to see.
'''
def __init__(self, member_node):
self.node = member_node
def process(
self, callbacks, functions_are_properties=False, use_returns_async=False
):
def process(self,
callbacks,
functions_are_properties=False,
use_returns_async=False):
properties = OrderedDict()
name = self.node.GetName()
if self.node.GetProperty('deprecated'):
properties['deprecated'] = self.node.GetProperty('deprecated')
for property_name in ['nodoc', 'nocompile', 'nodart',
'serializableFunction']:
for property_name in [
'nodoc', 'nocompile', 'nodart', 'serializableFunction'
]:
if self.node.GetProperty(property_name):
properties[property_name] = True
@ -230,27 +234,24 @@ class Member(object):
if self.node.GetProperty('platforms'):
properties['platforms'] = list(self.node.GetProperty('platforms'))
for option_name, sanitizer in [
('maxListeners', int),
('supportsFilters', lambda s: s == 'true'),
('supportsListeners', lambda s: s == 'true'),
('supportsRules', lambda s: s == 'true')]:
for option_name, sanitizer in [('maxListeners', int),
('supportsFilters', lambda s: s == 'true'),
('supportsListeners', lambda s: s == 'true'),
('supportsRules', lambda s: s == 'true')]:
if self.node.GetProperty(option_name):
if 'options' not in properties:
properties['options'] = {}
properties['options'][option_name] = sanitizer(self.node.GetProperty(
option_name))
properties['options'][option_name] = sanitizer(
self.node.GetProperty(option_name))
type_override = None
parameter_comments = OrderedDict()
for node in self.node.GetChildren():
if node.cls == 'Comment':
(parent_comment, parameter_comments) = ProcessComment(
node.GetName())
(parent_comment, parameter_comments) = ProcessComment(node.GetName())
properties['description'] = parent_comment
elif node.cls == 'Callspec':
name, parameters, return_type, returns_async = Callspec(
node, parameter_comments
).process(use_returns_async, callbacks)
node, parameter_comments).process(use_returns_async, callbacks)
if functions_are_properties:
# If functions are treated as properties (which will happen if the
# interface is named Properties) then this isn't a function, it's a
@ -258,14 +259,14 @@ class Member(object):
# property type is the return type. This is an egregious hack in lieu
# of the IDL parser supporting 'const'.
assert parameters == [], (
'Property "%s" must be no-argument functions '
'with a non-void return type' % name)
'Property "%s" must be no-argument functions '
'with a non-void return type' % name)
assert return_type is not None, (
'Property "%s" must be no-argument functions '
'with a non-void return type' % name)
'Property "%s" must be no-argument functions '
'with a non-void return type' % name)
assert 'type' in return_type, (
'Property return type "%s" from "%s" must specify a '
'fundamental IDL type.' % (pprint.pformat(return_type), name))
'Property return type "%s" from "%s" must specify a '
'fundamental IDL type.' % (pprint.pformat(return_type), name))
type_override = return_type['type']
else:
type_override = 'function'
@ -279,8 +280,8 @@ class Member(object):
if type_override is not None:
properties['type'] = type_override
else:
properties = Typeref(self.node.GetProperty('TYPEREF'),
self.node, properties).process(callbacks)
properties = Typeref(self.node.GetProperty('TYPEREF'), self.node,
properties).process(callbacks)
value = self.node.GetProperty('value')
if value is not None:
# IDL always returns values as strings, so cast to their real type.
@ -298,9 +299,9 @@ class Member(object):
return float(string_value)
# Add more as necessary.
assert json_type == 'string', (
'No rule exists to cast JSON Schema type "%s" to its equivalent '
'Python type for value "%s". You must add a new rule here.' %
(json_type, string_value))
'No rule exists to cast JSON Schema type "%s" to its equivalent '
'Python type for value "%s". You must add a new rule here.' %
(json_type, string_value))
return string_value
@ -310,6 +311,7 @@ class Typeref(object):
function parameter, converts into a Python dictionary that the JSON schema
compiler expects to see.
'''
def __init__(self, typeref, parent, additional_properties):
self.typeref = typeref
self.parent = parent
@ -363,11 +365,11 @@ class Typeref(object):
properties['additionalProperties'] = OrderedDict()
properties['additionalProperties']['type'] = 'any'
elif self.parent.GetPropertyLocal('Union'):
properties['choices'] = [Typeref(node.GetProperty('TYPEREF'),
node,
OrderedDict()).process(callbacks)
for node in self.parent.GetChildren()
if node.cls == 'Option']
properties['choices'] = [
Typeref(node.GetProperty('TYPEREF'), node,
OrderedDict()).process(callbacks)
for node in self.parent.GetChildren() if node.cls == 'Option'
]
elif self.typeref is None:
properties['type'] = 'function'
else:
@ -390,6 +392,7 @@ class Enum(object):
Given an IDL Enum node, converts into a Python dictionary that the JSON
schema compiler expects to see.
'''
def __init__(self, enum_node):
self.node = enum_node
self.description = ''
@ -411,12 +414,15 @@ class Enum(object):
self.description = ProcessComment(node.GetName())[0]
else:
sys.exit('Did not process %s %s' % (node.cls, node))
result = {'id' : self.node.GetName(),
'description': self.description,
'type': 'string',
'enum': enum}
for property_name in ['cpp_enum_prefix_override', 'inline_doc',
'noinline_doc', 'nodoc']:
result = {
'id': self.node.GetName(),
'description': self.description,
'type': 'string',
'enum': enum
}
for property_name in [
'cpp_enum_prefix_override', 'inline_doc', 'noinline_doc', 'nodoc'
]:
if self.node.GetProperty(property_name):
result[property_name] = self.node.GetProperty(property_name)
if self.node.GetProperty('deprecated'):
@ -485,19 +491,19 @@ class Namespace(object):
compiler_options = self.compiler_options or {}
documentation_options = self.documentation_options or {}
return {
'namespace': self.namespace.GetName(),
'description': self.description,
'nodoc': self.nodoc,
'types': self.types,
'functions': self.functions,
'properties': self.properties,
'manifest_keys': self.manifest_keys,
'internal': self.internal,
'events': self.events,
'platforms': self.platforms,
'compiler_options': compiler_options,
'deprecated': self.deprecated,
'documentation_options': documentation_options
'namespace': self.namespace.GetName(),
'description': self.description,
'nodoc': self.nodoc,
'types': self.types,
'functions': self.functions,
'properties': self.properties,
'manifest_keys': self.manifest_keys,
'internal': self.internal,
'events': self.events,
'platforms': self.platforms,
'compiler_options': compiler_options,
'deprecated': self.deprecated,
'documentation_options': documentation_options
}
def process_interface(self, node, functions_are_properties=False):
@ -542,9 +548,12 @@ class IDLSchema(object):
if not description:
# TODO(kalman): Go back to throwing an error here.
print('%s must have a namespace-level comment. This will '
'appear on the API summary page.' % node.GetName())
'appear on the API summary page.' % node.GetName())
description = ''
namespace = Namespace(node, description, nodoc, internal,
namespace = Namespace(node,
description,
nodoc,
internal,
platforms=platforms,
compiler_options=compiler_options or None,
deprecated=deprecated,
@ -621,8 +630,9 @@ def Main():
contents = sys.stdin.read()
for i, char in enumerate(contents):
if not char.isascii():
raise Exception('Non-ascii character "%s" (ord %d) found at offset %d.'
% (char, ord(char), i))
raise Exception(
'Non-ascii character "%s" (ord %d) found at offset %d.' %
(char, ord(char), i))
idl = idl_parser.IDLParser().ParseData(contents, '<stdin>')
schema = IDLSchema(idl).process()
print(json.dumps(schema, indent=2))

@ -8,6 +8,7 @@ import unittest
from json_parse import OrderedDict
def getFunction(schema, name):
for item in schema['functions']:
if item['name'] == name:
@ -19,10 +20,12 @@ def getParams(schema, name):
function = getFunction(schema, name)
return function['parameters']
def getReturnsAsync(schema, name):
function = getFunction(schema, name)
return function.get('returns_async', False)
def getReturns(schema, name):
function = getFunction(schema, name)
return function['returns']
@ -35,6 +38,7 @@ def getType(schema, id):
class IdlSchemaTest(unittest.TestCase):
def setUp(self):
loaded = idl_schema.Load('test/idl_basics.idl')
self.assertEqual(1, len(loaded))
@ -44,66 +48,123 @@ class IdlSchemaTest(unittest.TestCase):
def testSimpleCallbacks(self):
schema = self.idl_basics
expected = {'name': 'cb', 'parameters':[]}
expected = {'name': 'cb', 'parameters': []}
self.assertEqual(expected, getReturnsAsync(schema, 'function4'))
expected = {'name': 'cb',
'parameters':[{'name': 'x', 'type': 'integer'}]}
expected = {'name': 'cb', 'parameters': [{'name': 'x', 'type': 'integer'}]}
self.assertEqual(expected, getReturnsAsync(schema, 'function5'))
expected = {'name': 'cb',
'parameters':[{'name': 'arg', '$ref': 'MyType1'}]}
expected = {
'name': 'cb',
'parameters': [{
'name': 'arg',
'$ref': 'MyType1'
}]
}
self.assertEqual(expected, getReturnsAsync(schema, 'function6'))
def testCallbackWithArrayArgument(self):
schema = self.idl_basics
expected = {'name': 'cb',
'parameters':[{'name': 'arg', 'type': 'array',
'items':{'$ref': 'MyType2'}}]}
expected = {
'name':
'cb',
'parameters': [{
'name': 'arg',
'type': 'array',
'items': {
'$ref': 'MyType2'
}
}]
}
self.assertEqual(expected, getReturnsAsync(schema, 'function12'))
def testArrayOfCallbacks(self):
schema = idl_schema.Load('test/idl_function_types.idl')[0]
expected = [{'type': 'array', 'name': 'callbacks',
'items':{'type': 'function', 'name': 'MyCallback',
'parameters':[{'type': 'integer', 'name': 'x'}]}}]
expected = [{
'type': 'array',
'name': 'callbacks',
'items': {
'type': 'function',
'name': 'MyCallback',
'parameters': [{
'type': 'integer',
'name': 'x'
}]
}
}]
self.assertEqual(expected, getParams(schema, 'whatever'))
def testProperties(self):
self.assertEqual({
'x': {'name': 'x', 'type': 'integer',
'description': 'This comment tests "double-quotes".'},
'y': {'name': 'y', 'type': 'string'},
'z': {'name': 'z', 'type': 'string'},
'a': {'name': 'a', 'type': 'string'},
'b': {'name': 'b', 'type': 'string'},
'c': {'name': 'c', 'type': 'string'}},
getType(self.idl_basics, 'MyType1')['properties'])
self.assertEqual(
{
'x': {
'name': 'x',
'type': 'integer',
'description': 'This comment tests "double-quotes".'
},
'y': {
'name': 'y',
'type': 'string'
},
'z': {
'name': 'z',
'type': 'string'
},
'a': {
'name': 'a',
'type': 'string'
},
'b': {
'name': 'b',
'type': 'string'
},
'c': {
'name': 'c',
'type': 'string'
}
},
getType(self.idl_basics, 'MyType1')['properties'])
def testMemberOrdering(self):
self.assertEqual(
['x', 'y', 'z', 'a', 'b', 'c'],
list(getType(self.idl_basics, 'MyType1')['properties'].keys()))
self.assertEqual(['x', 'y', 'z', 'a', 'b', 'c'],
list(
getType(self.idl_basics,
'MyType1')['properties'].keys()))
def testEnum(self):
schema = self.idl_basics
expected = {'enum': [{'name': 'name1', 'description': 'comment1'},
{'name': 'name2'}],
'description': 'Enum description',
'type': 'string', 'id': 'EnumType'}
expected = {
'enum': [{
'name': 'name1',
'description': 'comment1'
}, {
'name': 'name2'
}],
'description': 'Enum description',
'type': 'string',
'id': 'EnumType'
}
self.assertEqual(expected, getType(schema, expected['id']))
expected_params = [{'name': 'type', '$ref': 'EnumType'}]
expected_returns_async = {
'name': 'cb',
'parameters':[{'name': 'type', '$ref': 'EnumType'}]}
'parameters': [{
'name': 'type',
'$ref': 'EnumType'
}]
}
self.assertEqual(expected_params, getParams(schema, 'function13'))
self.assertEqual(
expected_returns_async, getReturnsAsync(schema, 'function13')
)
self.assertEqual(expected_returns_async,
getReturnsAsync(schema, 'function13'))
expected = [{'items': {'$ref': 'EnumType'}, 'name': 'types',
'type': 'array'}]
expected = [{
'items': {
'$ref': 'EnumType'
},
'name': 'types',
'type': 'array'
}]
self.assertEqual(expected, getParams(schema, 'function14'))
def testScopedArguments(self):
@ -111,19 +172,29 @@ class IdlSchemaTest(unittest.TestCase):
expected = [{'name': 'value', '$ref': 'idl_other_namespace.SomeType'}]
self.assertEqual(expected, getParams(schema, 'function20'))
expected = [{'items': {'$ref': 'idl_other_namespace.SomeType'},
'name': 'values',
'type': 'array'}]
expected = [{
'items': {
'$ref': 'idl_other_namespace.SomeType'
},
'name': 'values',
'type': 'array'
}]
self.assertEqual(expected, getParams(schema, 'function21'))
expected = [{'name': 'value',
'$ref': 'idl_other_namespace.sub_namespace.AnotherType'}]
expected = [{
'name': 'value',
'$ref': 'idl_other_namespace.sub_namespace.AnotherType'
}]
self.assertEqual(expected, getParams(schema, 'function22'))
expected = [{'items': {'$ref': 'idl_other_namespace.sub_namespace.'
'AnotherType'},
'name': 'values',
'type': 'array'}]
expected = [{
'items': {
'$ref': 'idl_other_namespace.sub_namespace.'
'AnotherType'
},
'name': 'values',
'type': 'array'
}]
self.assertEqual(expected, getParams(schema, 'function23'))
def testNoCompile(self):
@ -151,45 +222,73 @@ class IdlSchemaTest(unittest.TestCase):
'name': 'name3',
'description': 'comment3'
}],
'type': 'string',
'id': 'EnumTypeWithNoDocValue',
'description': ''
'type':
'string',
'id':
'EnumTypeWithNoDocValue',
'description':
''
}
self.assertEqual(expected, getType(schema, expected['id']))
def testInternalNamespace(self):
idl_basics = self.idl_basics
idl_basics = self.idl_basics
self.assertEqual('idl_basics', idl_basics['namespace'])
self.assertTrue(idl_basics['internal'])
self.assertFalse(idl_basics['nodoc'])
def testReturnTypes(self):
schema = self.idl_basics
self.assertEqual({'name': 'function24', 'type': 'integer'},
getReturns(schema, 'function24'))
self.assertEqual({'name': 'function25', '$ref': 'MyType1',
'optional': True},
getReturns(schema, 'function25'))
self.assertEqual({'name': 'function26', 'type': 'array',
'items': {'$ref': 'MyType1'}},
getReturns(schema, 'function26'))
self.assertEqual({'name': 'function27', '$ref': 'EnumType',
'optional': True},
getReturns(schema, 'function27'))
self.assertEqual({'name': 'function28', 'type': 'array',
'items': {'$ref': 'EnumType'}},
getReturns(schema, 'function28'))
self.assertEqual({'name': 'function29', '$ref':
'idl_other_namespace.SomeType',
'optional': True},
getReturns(schema, 'function29'))
self.assertEqual({'name': 'function30', 'type': 'array',
'items': {'$ref': 'idl_other_namespace.SomeType'}},
getReturns(schema, 'function30'))
self.assertEqual({
'name': 'function24',
'type': 'integer'
}, getReturns(schema, 'function24'))
self.assertEqual({
'name': 'function25',
'$ref': 'MyType1',
'optional': True
}, getReturns(schema, 'function25'))
self.assertEqual(
{
'name': 'function26',
'type': 'array',
'items': {
'$ref': 'MyType1'
}
}, getReturns(schema, 'function26'))
self.assertEqual(
{
'name': 'function27',
'$ref': 'EnumType',
'optional': True
}, getReturns(schema, 'function27'))
self.assertEqual(
{
'name': 'function28',
'type': 'array',
'items': {
'$ref': 'EnumType'
}
}, getReturns(schema, 'function28'))
self.assertEqual(
{
'name': 'function29',
'$ref': 'idl_other_namespace.SomeType',
'optional': True
}, getReturns(schema, 'function29'))
self.assertEqual(
{
'name': 'function30',
'type': 'array',
'items': {
'$ref': 'idl_other_namespace.SomeType'
}
}, getReturns(schema, 'function30'))
def testIgnoresAdditionalPropertiesOnType(self):
self.assertTrue(getType(self.idl_basics, 'IgnoreAdditionalPropertiesType')
['ignoreAdditionalProperties'])
self.assertTrue(
getType(self.idl_basics,
'IgnoreAdditionalPropertiesType')['ignoreAdditionalProperties'])
def testChromeOSPlatformsNamespace(self):
schema = idl_schema.Load('test/idl_namespace_chromeos.idl')[0]
@ -206,7 +305,7 @@ class IdlSchemaTest(unittest.TestCase):
def testNonSpecificPlatformsNamespace(self):
schema = idl_schema.Load('test/idl_namespace_non_specific_platforms.idl')[0]
self.assertEqual('idl_namespace_non_specific_platforms',
schema['namespace'])
schema['namespace'])
expected = None
self.assertEqual(expected, schema['platforms'])
@ -214,17 +313,16 @@ class IdlSchemaTest(unittest.TestCase):
schema = idl_schema.Load('test/idl_generate_error_messages.idl')[0]
self.assertEqual('idl_generate_error_messages', schema['namespace'])
self.assertTrue(schema['compiler_options'].get('generate_error_messages',
False))
False))
schema = idl_schema.Load('test/idl_basics.idl')[0]
self.assertEqual('idl_basics', schema['namespace'])
self.assertFalse(schema['compiler_options'].get('generate_error_messages',
False))
False))
def testSpecificImplementNamespace(self):
schema = idl_schema.Load('test/idl_namespace_specific_implement.idl')[0]
self.assertEqual('idl_namespace_specific_implement',
schema['namespace'])
self.assertEqual('idl_namespace_specific_implement', schema['namespace'])
expected = 'idl_namespace_specific_implement.idl'
self.assertEqual(expected, schema['compiler_options']['implemented_in'])
@ -232,39 +330,39 @@ class IdlSchemaTest(unittest.TestCase):
schema = idl_schema.Load(
'test/idl_namespace_specific_implement_chromeos.idl')[0]
self.assertEqual('idl_namespace_specific_implement_chromeos',
schema['namespace'])
schema['namespace'])
expected_implemented_path = 'idl_namespace_specific_implement_chromeos.idl'
expected_platform = ['chromeos']
self.assertEqual(expected_implemented_path,
schema['compiler_options']['implemented_in'])
schema['compiler_options']['implemented_in'])
self.assertEqual(expected_platform, schema['platforms'])
def testCallbackComment(self):
schema = self.idl_basics
self.assertEqual('A comment on a callback.',
getReturnsAsync(schema, 'function16')['description'])
getReturnsAsync(schema, 'function16')['description'])
self.assertEqual(
'A parameter.',
getReturnsAsync(schema, 'function16')['parameters'][0]['description'])
self.assertEqual(
'Just a parameter comment, with no comment on the callback.',
getReturnsAsync(schema, 'function17')['parameters'][0]['description'])
self.assertEqual(
'Override callback comment.',
getReturnsAsync(schema, 'function18')['description'])
self.assertEqual('Override callback comment.',
getReturnsAsync(schema, 'function18')['description'])
def testFunctionComment(self):
schema = self.idl_basics
func = getFunction(schema, 'function3')
self.assertEqual(('This comment should appear in the documentation, '
'despite occupying multiple lines.'),
func['description'])
self.assertEqual(
[{'description': ('So should this comment about the argument. '
'<em>HTML</em> is fine too.'),
'name': 'arg',
'$ref': 'MyType1'}],
func['parameters'])
'despite occupying multiple lines.'), func['description'])
self.assertEqual([{
'description': ('So should this comment about the argument. '
'<em>HTML</em> is fine too.'),
'name':
'arg',
'$ref':
'MyType1'
}], func['parameters'])
func = getFunction(schema, 'function4')
self.assertEqual(
'<p>This tests if "double-quotes" are escaped correctly.</p>'
@ -275,12 +373,18 @@ class IdlSchemaTest(unittest.TestCase):
schema = idl_schema.Load('test/idl_reserved_words.idl')[0]
foo_type = getType(schema, 'Foo')
self.assertEqual([{'name': 'float'}, {'name': 'DOMString'}],
foo_type['enum'])
self.assertEqual([{
'name': 'float'
}, {
'name': 'DOMString'
}], foo_type['enum'])
enum_type = getType(schema, 'enum')
self.assertEqual([{'name': 'callback'}, {'name': 'namespace'}],
enum_type['enum'])
self.assertEqual([{
'name': 'callback'
}, {
'name': 'namespace'
}], enum_type['enum'])
dictionary = getType(schema, 'dictionary')
self.assertEqual('integer', dictionary['properties']['long']['type'])
@ -300,12 +404,10 @@ class IdlSchemaTest(unittest.TestCase):
self.assertEqual('integer', foo_type['properties']['x']['type'])
self.assertEqual('object', foo_type['properties']['y']['type'])
self.assertEqual(
'any',
foo_type['properties']['y']['additionalProperties']['type'])
'any', foo_type['properties']['y']['additionalProperties']['type'])
self.assertEqual('object', foo_type['properties']['z']['type'])
self.assertEqual(
'any',
foo_type['properties']['z']['additionalProperties']['type'])
'any', foo_type['properties']['z']['additionalProperties']['type'])
self.assertEqual('Window', foo_type['properties']['z']['isInstanceOf'])
bar_type = getType(schema, 'BarType')
@ -337,35 +439,48 @@ class IdlSchemaTest(unittest.TestCase):
union_type = getType(schema, 'UnionType')
expected = {
'type': 'object',
'id': 'UnionType',
'properties': {
'x': {
'name': 'x',
'optional': True,
'choices': [
{'type': 'integer'},
{'$ref': 'FooType'},
]
},
'y': {
'name': 'y',
'choices': [
{'type': 'string'},
{'type': 'object',
'additionalProperties': {'type': 'any'}}
]
},
'z': {
'name': 'z',
'choices': [
{'type': 'object', 'isInstanceOf': 'ImageData',
'additionalProperties': {'type': 'any'}},
{'type': 'integer'}
]
}
},
}
'type': 'object',
'id': 'UnionType',
'properties': {
'x': {
'name': 'x',
'optional': True,
'choices': [
{
'type': 'integer'
},
{
'$ref': 'FooType'
},
]
},
'y': {
'name':
'y',
'choices': [{
'type': 'string'
}, {
'type': 'object',
'additionalProperties': {
'type': 'any'
}
}]
},
'z': {
'name':
'z',
'choices': [{
'type': 'object',
'isInstanceOf': 'ImageData',
'additionalProperties': {
'type': 'any'
}
}, {
'type': 'integer'
}]
}
},
}
self.assertEqual(expected, union_type)
@ -374,19 +489,20 @@ class IdlSchemaTest(unittest.TestCase):
union_type = getType(schema, 'ModifiedUnionType')
expected = {
'type': 'object',
'id': 'ModifiedUnionType',
'properties': {
'x': {
'name': 'x',
'nodoc': True,
'choices': [
{'type': 'integer'},
{'type': 'string'}
]
}
}
}
'type': 'object',
'id': 'ModifiedUnionType',
'properties': {
'x': {
'name': 'x',
'nodoc': True,
'choices': [{
'type': 'integer'
}, {
'type': 'string'
}]
}
}
}
self.assertEqual(expected, union_type)
@ -394,17 +510,17 @@ class IdlSchemaTest(unittest.TestCase):
schema = idl_schema.Load('test/idl_object_types.idl')[0]
object_type = getType(schema, 'SerializableFunctionObject')
expected = {
'type': 'object',
'id': 'SerializableFunctionObject',
'properties': {
'func': {
'name': 'func',
'serializableFunction': True,
'type': 'function',
'parameters': []
}
}
}
'type': 'object',
'id': 'SerializableFunctionObject',
'properties': {
'func': {
'name': 'func',
'serializableFunction': True,
'type': 'function',
'parameters': []
}
}
}
self.assertEqual(expected, object_type)
def testUnionsWithFunctions(self):
@ -412,12 +528,13 @@ class IdlSchemaTest(unittest.TestCase):
union_params = getParams(schema, 'union_params')
expected = [{
'name': 'x',
'choices': [
{'type': 'integer'},
{'type': 'string'}
]
}]
'name': 'x',
'choices': [{
'type': 'integer'
}, {
'type': 'string'
}]
}]
self.assertEqual(expected, union_params)
@ -426,24 +543,32 @@ class IdlSchemaTest(unittest.TestCase):
blah_params = getReturnsAsync(schema, 'blah')
expected = {
'name': 'callback', 'parameters': [{
'name': 'x',
'choices': [
{'type': 'integer'},
{'type': 'string'}
]}
]
}
'name':
'callback',
'parameters': [{
'name': 'x',
'choices': [{
'type': 'integer'
}, {
'type': 'string'
}]
}]
}
badabish_params = getReturnsAsync(schema, 'badabish')
expected = {
'name': 'callback', 'parameters': [{
'name': 'x', 'optional': True, 'choices': [
{'type': 'integer'},
{'type': 'string'}
]
}]
}
'name':
'callback',
'parameters': [{
'name': 'x',
'optional': True,
'choices': [{
'type': 'integer'
}, {
'type': 'string'
}]
}]
}
self.assertEqual(expected, badabish_params)
@ -453,7 +578,10 @@ class IdlSchemaTest(unittest.TestCase):
expected_params = []
expected_returns_async = {
'name': 'callback',
'parameters': [{'name': 'x', 'type': 'integer'}],
'parameters': [{
'name': 'x',
'type': 'integer'
}],
'does_not_support_promises': 'Test'
}
params = getParams(schema, 'non_promise_supporting')
@ -465,46 +593,60 @@ class IdlSchemaTest(unittest.TestCase):
def testFunctionWithoutPromiseSupportAndParams(self):
schema = idl_schema.Load('test/idl_function_types.idl')[0]
expected_params = [
{'name': 'z', 'type': 'integer'},
{'name': 'y', 'choices': [{'type': 'integer'}, {'type': 'string'}]}
]
expected_params = [{
'name': 'z',
'type': 'integer'
}, {
'name': 'y',
'choices': [{
'type': 'integer'
}, {
'type': 'string'
}]
}]
expected_returns_async = {
'name': 'callback',
'parameters': [{'name': 'x', 'type': 'integer'}],
'parameters': [{
'name': 'x',
'type': 'integer'
}],
'does_not_support_promises': 'Test'
}
params = getParams(schema, 'non_promise_supporting_with_params')
returns_async = getReturnsAsync(
schema, 'non_promise_supporting_with_params'
)
returns_async = getReturnsAsync(schema,
'non_promise_supporting_with_params')
self.assertEqual(expected_params, params)
self.assertEqual(expected_returns_async, returns_async)
def testProperties(self):
schema = idl_schema.Load('test/idl_properties.idl')[0]
self.assertEqual(OrderedDict([
('first', OrderedDict([
('description', 'Integer property.'),
('type', 'integer'),
('value', 42),
])),
('second', OrderedDict([
('description', 'Double property.'),
('type', 'number'),
('value', 42.1),
])),
('third', OrderedDict([
('description', 'String property.'),
('type', 'string'),
('value', 'hello world'),
])),
('fourth', OrderedDict([
('description', 'Unvalued property.'),
('type', 'integer'),
])),
]), schema.get('properties'))
self.assertEqual(
OrderedDict([
('first',
OrderedDict([
('description', 'Integer property.'),
('type', 'integer'),
('value', 42),
])),
('second',
OrderedDict([
('description', 'Double property.'),
('type', 'number'),
('value', 42.1),
])),
('third',
OrderedDict([
('description', 'String property.'),
('type', 'string'),
('value', 'hello world'),
])),
('fourth',
OrderedDict([
('description', 'Unvalued property.'),
('type', 'integer'),
])),
]), schema.get('properties'))
def testManifestKeys(self):
schema = self.idl_basics
@ -512,19 +654,20 @@ class IdlSchemaTest(unittest.TestCase):
# exhaustive so we don't have to update it each time we add a new key in the
# test file.
manifest_keys = schema.get('manifest_keys')
self.assertEqual(manifest_keys['key_str'],
OrderedDict([('description', 'String manifest key.'),
('name', 'key_str'),
('type', 'string')]))
self.assertEqual(
manifest_keys['key_str'],
OrderedDict([('description', 'String manifest key.'),
('name', 'key_str'), ('type', 'string')]))
self.assertEqual(manifest_keys['key_ref'],
OrderedDict([('name', 'key_ref'),
('$ref', 'MyType2')])),
self.assertEqual(manifest_keys['choice_with_arrays'],
OrderedDict([('name', 'choice_with_arrays'),
('$ref', 'ChoiceWithArraysType')])),
self.assertEqual(manifest_keys['choice_with_optional'],
OrderedDict([('name', 'choice_with_optional'),
('$ref', 'ChoiceWithOptionalType')]))
OrderedDict([('name', 'key_ref'), ('$ref', 'MyType2')])),
self.assertEqual(
manifest_keys['choice_with_arrays'],
OrderedDict([('name', 'choice_with_arrays'),
('$ref', 'ChoiceWithArraysType')])),
self.assertEqual(
manifest_keys['choice_with_optional'],
OrderedDict([('name', 'choice_with_optional'),
('$ref', 'ChoiceWithOptionalType')]))
def testNoManifestKeys(self):
schema = idl_schema.Load('test/idl_properties.idl')[0]

@ -23,11 +23,15 @@ NOTE = """// NOTE: The format of types has changed. 'FooType' is now
// See https://chromium.googlesource.com/chromium/src/+/main/docs/closure_compilation.md
"""
class JsExternsGenerator(object):
def Generate(self, namespace):
return _Generator(namespace).Generate()
class _Generator(object):
def __init__(self, namespace):
self._namespace = namespace
self._class_name = None
@ -45,7 +49,7 @@ class _Generator(object):
src_to_script = os.path.relpath(script_dir, src_root)
# tools/json_schema_compiler/compiler.py
compiler_path = os.path.join(src_to_script, 'compiler.py')
(c.Append(self._GetHeader(compiler_path, self._namespace.name))
(c.Append(self._GetHeader(compiler_path, self._namespace.name)) \
.Append())
self._AppendNamespaceObject(c)
@ -69,13 +73,10 @@ class _Generator(object):
def _GetHeader(self, tool, namespace):
"""Returns the file header text.
"""
return (self._js_util.GetLicense() + '\n' +
self._js_util.GetInfo(tool) + (NOTE % namespace) + '\n' +
'/**\n' +
return (self._js_util.GetLicense() + '\n' + self._js_util.GetInfo(tool) +
(NOTE % namespace) + '\n' + '/**\n' +
(' * @fileoverview Externs generated from namespace: %s\n' %
namespace) +
' * @externs\n' +
' */')
namespace) + ' * @externs\n' + ' */')
def _AppendType(self, c, js_type):
"""Given a Type object, generates the Code for this type's definition.
@ -95,9 +96,10 @@ class _Generator(object):
js_type.simple_name)
c.Eblock(' */')
c.Append('%s.%s = {' % (self._GetNamespace(), js_type.name))
c.Append('\n'.join(
[" %s: '%s'," % (self._js_util.GetPropertyName(v.name), v.name)
for v in js_type.enum_values]))
c.Append('\n'.join([
" %s: '%s'," % (self._js_util.GetPropertyName(v.name), v.name)
for v in js_type.enum_values
]))
c.Append('};')
def _IsTypeConstructor(self, js_type):
@ -120,8 +122,8 @@ class _Generator(object):
if js_type.property_type is not PropertyType.OBJECT:
self._js_util.AppendTypeJsDoc(c, self._namespace.name, js_type, optional)
elif is_constructor:
c.Comment('@constructor', comment_prefix = '', wrap_indent=4)
c.Comment('@private', comment_prefix = '', wrap_indent=4)
c.Comment('@constructor', comment_prefix='', wrap_indent=4)
c.Comment('@private', comment_prefix='', wrap_indent=4)
else:
self._AppendTypedef(c, js_type.properties)
@ -151,8 +153,10 @@ class _Generator(object):
c.Append('@typedef {')
if properties:
self._js_util.AppendObjectDefinition(
c, self._namespace.name, properties, new_line=False)
self._js_util.AppendObjectDefinition(c,
self._namespace.name,
properties,
new_line=False)
else:
c.Append('Object', new_line=False)
c.Append('}', new_line=False)
@ -178,8 +182,8 @@ class _Generator(object):
"""
self._js_util.AppendFunctionJsDoc(c, self._namespace.name, function)
params = self._GetFunctionParams(function)
c.Append('%s.%s = function(%s) {};' % (self._GetNamespace(),
function.name, params))
c.Append('%s.%s = function(%s) {};' %
(self._GetNamespace(), function.name, params))
c.Append()
def _AppendEvent(self, c, event):

@ -496,15 +496,16 @@ chrome.fakeJson.funcWithReturnsAsync = function(someNumber, callback) {};""" % (
class JsExternGeneratorTest(unittest.TestCase):
def _GetNamespace(self, fake_content, filename, is_idl):
"""Returns a namespace object for the given content"""
api_def = (idl_schema.Process(fake_content, filename) if is_idl
else json_parse.Parse(fake_content))
api_def = (idl_schema.Process(fake_content, filename)
if is_idl else json_parse.Parse(fake_content))
m = model.Model()
return m.AddNamespace(api_def[0], filename)
def setUp(self):
self.maxDiff = None # Lets us see the full diff when inequal.
self.maxDiff = None # Lets us see the full diff when inequal.
def testBasic(self):
namespace = self._GetNamespace(fake_idl, 'fake_api.idl', True)

@ -15,11 +15,15 @@ import os
import sys
import re
class JsInterfaceGenerator(object):
def Generate(self, namespace):
return _Generator(namespace).Generate()
class _Generator(object):
def __init__(self, namespace):
self._namespace = namespace
first = namespace.name[0].upper()
@ -31,7 +35,7 @@ class _Generator(object):
"""Generates a Code object with the schema for the entire namespace.
"""
c = Code()
(c.Append(self._GetHeader(sys.argv[0], self._namespace.name))
(c.Append(self._GetHeader(sys.argv[0], self._namespace.name)) \
.Append())
self._AppendInterfaceObject(c)
@ -56,10 +60,10 @@ class _Generator(object):
def _GetHeader(self, tool, namespace):
"""Returns the file header text.
"""
return (
self._js_util.GetLicense() + '\n' + self._js_util.GetInfo(tool) + '\n' +
('/** @fileoverview Interface for %s that can be overriden. */' %
namespace))
return (self._js_util.GetLicense() + '\n' + self._js_util.GetInfo(tool) +
'\n' +
('/** @fileoverview Interface for %s that can be overriden. */' %
namespace))
def _AppendInterfaceObject(self, c):
"""Appends the code creating the interface object.
@ -67,7 +71,7 @@ class _Generator(object):
/** @interface */
function SettingsPrivate() {}
"""
(c.Append('/** @interface */')
(c.Append('/** @interface */') \
.Append('function %s() {}' % self._interface))
def _AppendFunction(self, c, function):
@ -88,8 +92,8 @@ class _Generator(object):
self._js_util.AppendFunctionJsDoc(c, self._namespace.name, function)
c.Append('%s: function(%s) {},' % (function.name, ', '.join(
getParamNames(function))))
c.Append('%s: function(%s) {},' %
(function.name, ', '.join(getParamNames(function))))
c.Append()
def _AppendEvent(self, c, event):

@ -110,10 +110,12 @@ FakeApi.prototype = {
* @type {!ChromeEvent}
* @see https://developer.chrome.com/extensions/fakeApi#event-onTrapDetected
*/
FakeApi.prototype.onTrapDetected;""" % (datetime.now().year,
sys.argv[0].replace('\\', '/')))
FakeApi.prototype.onTrapDetected;""" %
(datetime.now().year, sys.argv[0].replace('\\', '/')))
class JsExternGeneratorTest(unittest.TestCase):
def _GetNamespace(self, fake_content, filename):
"""Returns a namespace object for the given content"""
api_def = idl_schema.Process(fake_content, filename)
@ -121,7 +123,7 @@ class JsExternGeneratorTest(unittest.TestCase):
return m.AddNamespace(api_def[0], filename)
def setUp(self):
self.maxDiff = None # Lets us see the full diff when inequal.
self.maxDiff = None # Lets us see the full diff when inequal.
def testBasic(self):
namespace = self._GetNamespace(fake_idl, 'fake_api.idl')

@ -21,6 +21,7 @@ INFO = """// This file was generated by:
class JsUtil(object):
"""A helper class for generating JS Code.
"""
def GetLicense(self):
"""Returns the license text for JS extern and interface files.
"""
@ -31,7 +32,7 @@ class JsUtil(object):
"""
return (INFO % tool.replace('\\', '/'))
def GetPropertyName(self,e):
def GetPropertyName(self, e):
# Enum properties are normified to be in ALL_CAPS_STYLE.
# Assume enum '1ring-rulesThemAll'.
# Transform to '1ring-rules_Them_All'.
@ -43,8 +44,11 @@ class JsUtil(object):
# Transform to '_1RING_RULES_THEM_ALL'.
return e.upper()
def AppendObjectDefinition(self, c, namespace_name, properties,
new_line=True):
def AppendObjectDefinition(self,
c,
namespace_name,
properties,
new_line=True):
"""Given an OrderedDict of properties, returns a Code containing the
description of an object.
"""
@ -62,8 +66,8 @@ class JsUtil(object):
first = False
js_type = self._TypeToJsType(namespace_name, prop.type_)
if prop.optional:
js_type = (Code().Append('(')
.Concat(js_type, new_line=False)
js_type = (Code().Append('(') \
.Concat(js_type, new_line=False) \
.Append('|undefined)', new_line=False))
c.Append('%s: ' % field, strip_right=False)
c.Concat(js_type, new_line=False)
@ -88,16 +92,18 @@ class JsUtil(object):
c.Append('}', new_line=False)
c.Comment(' %s' % name, comment_prefix='', wrap_indent=4, new_line=False)
if description:
c.Comment(' %s' % description, comment_prefix='',
wrap_indent=4, new_line=False)
c.Comment(' %s' % description,
comment_prefix='',
wrap_indent=4,
new_line=False)
for i, param in enumerate(function.params):
# Mark the parameter as optional, *only if* all following parameters are
# also optional, to avoid JSC_OPTIONAL_ARG_AT_END errors thrown by Closure
# Compiler.
optional = (
all(p.optional for p in function.params[i:]) and
(function.returns_async is None or function.returns_async.optional))
optional = (all(p.optional for p in function.params[i:])
and (function.returns_async is None
or function.returns_async.optional))
js_type = self._TypeToJsType(namespace_name, param.type_)
# If the parameter was originally optional, but was followed by
@ -123,8 +129,8 @@ class JsUtil(object):
if function.returns:
append_field(c, 'return',
self._TypeToJsType(namespace_name, function.returns),
'', False, function.returns.description)
self._TypeToJsType(namespace_name, function.returns), '',
False, function.returns.description)
if function.deprecated:
c.Append('@deprecated %s' % function.deprecated)
@ -149,9 +155,8 @@ class JsUtil(object):
"""Converts a model.Function to a JS type (i.e., function([params])...)"""
c = Code()
c.Append('function(')
c.Concat(
self._FunctionParamsToJsParams(namespace_name, function.params),
new_line=False)
c.Concat(self._FunctionParamsToJsParams(namespace_name, function.params),
new_line=False)
c.Append('): ', new_line=False, strip_right=False)
if function.returns:
@ -169,9 +174,9 @@ class JsUtil(object):
# appended to the params as a callback.
c = Code()
c.Append('function(')
c.Concat(
self._FunctionParamsToJsParams(namespace_name, returns_async.params),
new_line=False)
c.Concat(self._FunctionParamsToJsParams(namespace_name,
returns_async.params),
new_line=False)
c.Append('): ', new_line=False, strip_right=False)
c.Append('void', new_line=False)
@ -211,10 +216,9 @@ class JsUtil(object):
return Code().Append(js_type.instance_of)
return Code().Append('Object')
if js_type.property_type is PropertyType.ARRAY:
return (Code().Append('!Array<').
Concat(self._TypeToJsType(namespace_name, js_type.item_type),
new_line=False).
Append('>', new_line=False))
return (Code().Append('!Array<').Concat(
self._TypeToJsType(namespace_name, js_type.item_type),
new_line=False).Append('>', new_line=False))
if js_type.property_type is PropertyType.REF:
ref_type = '!chrome.%s.%s' % (namespace_name, js_type.ref_type)
return Code().Append(ref_type)
@ -235,7 +239,7 @@ class JsUtil(object):
return Code().Append('*')
if js_type.property_type.is_fundamental:
return Code().Append(js_type.property_type.name)
return Code().Append('?') # TODO(tbreisacher): Make this more specific.
return Code().Append('?') # TODO(tbreisacher): Make this more specific.
def AppendSeeLink(self, c, namespace_name, object_type, object_name):
"""Appends a @see link for a given API 'object' (type, method, or event).

@ -9,8 +9,8 @@ import sys
_FILE_PATH = os.path.dirname(os.path.realpath(__file__))
_SYS_PATH = sys.path[:]
try:
_COMMENT_EATER_PATH = os.path.join(
_FILE_PATH, os.pardir, 'json_comment_eater')
_COMMENT_EATER_PATH = os.path.join(_FILE_PATH, os.pardir,
'json_comment_eater')
sys.path.insert(0, _COMMENT_EATER_PATH)
import json_comment_eater
finally:
@ -36,17 +36,14 @@ except ImportError:
_SYS_PATH = sys.path[:]
try:
_SIMPLE_JSON_PATH = os.path.join(_FILE_PATH,
os.pardir,
os.pardir,
_SIMPLE_JSON_PATH = os.path.join(_FILE_PATH, os.pardir, os.pardir,
'third_party')
sys.path.insert(0, _SIMPLE_JSON_PATH)
# Add this path in case this is being used in the docs server.
sys.path.insert(0, os.path.join(_FILE_PATH,
os.pardir,
os.pardir,
'third_party',
'json_schema_compiler'))
sys.path.insert(
0,
os.path.join(_FILE_PATH, os.pardir, os.pardir, 'third_party',
'json_schema_compiler'))
import simplejson
from simplejson import OrderedDict
finally:

@ -17,8 +17,8 @@ def DeleteNodes(item, delete_key=None, matcher=None):
def ShouldDelete(thing):
return json_parse.IsDict(thing) and (
delete_key is not None and delete_key in thing or
matcher is not None and matcher(thing))
delete_key is not None and delete_key in thing
or matcher is not None and matcher(thing))
if json_parse.IsDict(item):
toDelete = []
@ -30,8 +30,10 @@ def DeleteNodes(item, delete_key=None, matcher=None):
for key in toDelete:
del item[key]
elif type(item) == list:
item[:] = [DeleteNodes(thing, delete_key, matcher)
for thing in item if not ShouldDelete(thing)]
item[:] = [
DeleteNodes(thing, delete_key, matcher) for thing in item
if not ShouldDelete(thing)
]
return item

@ -6,93 +6,85 @@
import json_schema
import unittest
class JsonSchemaUnittest(unittest.TestCase):
def testNocompile(self):
compiled = [
{
compiled = [{
"namespace": "compile",
"description": "The compile API.",
"functions": [],
"types": {}
},
{
"types": {}
}, {
"namespace": "functions",
"description": "The functions API.",
"functions": [
{
"functions": [{
"id": "two"
},
{
}, {
"id": "four"
}
],
}],
"types": {
"one": { "key": "value" }
"one": {
"key": "value"
}
}
},
{
}, {
"namespace": "types",
"description": "The types API.",
"functions": [
{ "id": "one" }
],
"functions": [{
"id": "one"
}],
"types": {
"two": {
"key": "value"
},
"four": {
"key": "value"
}
"two": {
"key": "value"
},
"four": {
"key": "value"
}
}
},
{
}, {
"namespace": "nested",
"description": "The nested API.",
"properties": {
"sync": {
"functions": [
{
"id": "two"
},
{
"id": "four"
}
],
"types": {
"two": {
"key": "value"
},
"four": {
"key": "value"
}
"sync": {
"functions": [{
"id": "two"
}, {
"id": "four"
}],
"types": {
"two": {
"key": "value"
},
"four": {
"key": "value"
}
}
}
}
}
}
]
}]
schema = json_schema.CachedLoad('test/json_schema_test.json')
self.assertEqual(compiled, json_schema.DeleteNodes(schema, 'nocompile'))
def should_delete(value):
return isinstance(value, dict) and not value.get('valid', True)
expected = [
{'one': {'test': 'test'}},
{'valid': True},
{}
]
given = [
{'one': {'test': 'test'}, 'two': {'valid': False}},
{'valid': True},
{},
{'valid': False}
]
self.assertEqual(
expected, json_schema.DeleteNodes(given, matcher=should_delete))
expected = [{'one': {'test': 'test'}}, {'valid': True}, {}]
given = [{
'one': {
'test': 'test'
},
'two': {
'valid': False
}
}, {
'valid': True
}, {}, {
'valid': False
}]
self.assertEqual(expected,
json_schema.DeleteNodes(given, matcher=should_delete))
if __name__ == '__main__':

@ -2,13 +2,16 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
def memoize(fn):
'''Decorates |fn| to memoize.
'''
memory = {}
def impl(*args, **optargs):
full_args = args + tuple(optargs.items())
if full_args not in memory:
memory[full_args] = fn(*args, **optargs)
return memory[full_args]
return impl

@ -17,14 +17,16 @@ def _IsTypeFromManifestKeys(namespace, typename, fallback):
return fallback
class ParseException(Exception):
"""Thrown when data in the model is invalid.
"""
def __init__(self, parent, message):
hierarchy = _GetModelHierarchy(parent)
hierarchy.append(message)
Exception.__init__(
self, 'Model parse exception at:\n' + '\n'.join(hierarchy))
Exception.__init__(self,
'Model parse exception at:\n' + '\n'.join(hierarchy))
class Model(object):
@ -33,6 +35,7 @@ class Model(object):
Properties:
- |namespaces| a map of a namespace name to its model.Namespace
"""
def __init__(self, allow_inline_enums=True):
self._allow_inline_enums = allow_inline_enums
self.namespaces = {}
@ -67,11 +70,13 @@ class ComplexFeature(object):
- |unix_name| the unix_name of the feature
- |feature_list| a list of simple features which make up the feature
"""
def __init__(self, feature_name, features):
self.name = feature_name
self.unix_name = UnixName(self.name)
self.feature_list = features
class SimpleFeature(object):
"""A simple feature, which can make up a complex feature, as specified in
files such as chrome/common/extensions/api/_permission_features.json.
@ -83,6 +88,7 @@ class SimpleFeature(object):
- |extension_types| the types which can use the feature
- |allowlist| a list of extensions allowed to use the feature
"""
def __init__(self, feature_name, feature_def):
self.name = feature_name
self.unix_name = UnixName(self.name)
@ -112,6 +118,7 @@ class Namespace(object):
|include_compiler_options| is True
- |manifest_keys| is a Type representing the manifest keys for this namespace.
"""
def __init__(self,
json,
source_file,
@ -122,7 +129,7 @@ class Namespace(object):
if 'description' not in json:
# TODO(kalman): Go back to throwing an error here.
print('%s must have a "description" field. This will appear '
'on the API summary page.' % self.name)
'on the API summary page.' % self.name)
json['description'] = ''
self.description = json['description']
self.nodoc = json.get('nodoc', False)
@ -198,12 +205,8 @@ class Type(object):
- |additional_properties| the type of the additional properties, if any is
specified
"""
def __init__(self,
parent,
name,
json,
namespace,
input_origin):
def __init__(self, parent, name, json, namespace, input_origin):
self.name = name
# The typename "ManifestKeys" is reserved.
@ -221,8 +224,9 @@ class Type(object):
# We need to do this to ensure types reference by manifest types have the
# correct value for |origin.from_manifest_keys|.
self.origin = Origin(
input_origin.from_client, input_origin.from_json,
_IsTypeFromManifestKeys(namespace, name, input_origin.from_manifest_keys))
input_origin.from_client, input_origin.from_json,
_IsTypeFromManifestKeys(namespace, name,
input_origin.from_manifest_keys))
self.parent = parent
self.instance_of = json.get('isInstanceOf', None)
@ -252,7 +256,7 @@ class Type(object):
raise ParseException(
self,
'Inline enum "%s" found in namespace "%s". These are not allowed. '
'See crbug.com/472279' % (name, namespace.name))
'See crbug.com/472279' % (name, namespace.name))
self.property_type = PropertyType.ENUM
self.enum_values = [EnumValue(value, namespace) for value in json['enum']]
self.cpp_enum_prefix_override = json.get('cpp_enum_prefix_override', None)
@ -264,13 +268,13 @@ class Type(object):
self.property_type = PropertyType.BOOLEAN
elif json_type == 'integer':
self.property_type = PropertyType.INTEGER
elif (json_type == 'double' or
json_type == 'number'):
elif (json_type == 'double' or json_type == 'number'):
self.property_type = PropertyType.DOUBLE
elif json_type == 'string':
self.property_type = PropertyType.STRING
elif 'choices' in json:
self.property_type = PropertyType.CHOICES
def generate_type_name(type_json):
if 'items' in type_json:
return '%ss' % generate_type_name(type_json['items'])
@ -279,28 +283,22 @@ class Type(object):
if 'type' in type_json:
return type_json['type']
return None
self.choices = [
Type(self,
generate_type_name(choice) or 'choice%s' % i,
choice,
namespace,
self.origin)
for i, choice in enumerate(json['choices'])]
generate_type_name(choice) or 'choice%s' % i, choice, namespace,
self.origin) for i, choice in enumerate(json['choices'])
]
elif json_type == 'object':
if not (
'isInstanceOf' in json or
'properties' in json or
'additionalProperties' in json or
'functions' in json or
'events' in json):
if not ('isInstanceOf' in json or 'properties' in json
or 'additionalProperties' in json or 'functions' in json
or 'events' in json):
raise ParseException(self, name + " has no properties or functions")
self.property_type = PropertyType.OBJECT
additional_properties_json = json.get('additionalProperties', None)
if additional_properties_json is not None:
self.additional_properties = Type(self,
'additionalProperties',
additional_properties_json,
namespace,
self.additional_properties = Type(self, 'additionalProperties',
additional_properties_json, namespace,
self.origin)
else:
self.additional_properties = None
@ -309,8 +307,8 @@ class Type(object):
# Sometimes we might have an unnamed function, e.g. if it's a property
# of an object. Use the name of the property in that case.
function_name = json.get('name', name)
self.function = Function(
self, function_name, json, namespace, self.origin)
self.function = Function(self, function_name, json, namespace,
self.origin)
else:
raise ParseException(self, 'Unsupported JSON type %s' % json_type)
@ -321,6 +319,7 @@ class Type(object):
'''
return self.name == 'ManifestKeys'
class Function(object):
"""A Function defined in the API.
@ -340,12 +339,8 @@ class Function(object):
- |returns| the return type of the function; None if the function does not
return a value
"""
def __init__(self,
parent,
name,
json,
namespace,
origin):
def __init__(self, parent, name, json, namespace, origin):
self.name = name
self.simple_name = _StripNamespace(self.name, namespace)
self.platforms = _GetPlatforms(json)
@ -367,8 +362,10 @@ class Function(object):
def GeneratePropertyFromParam(p):
return Property(self, p['name'], p, namespace, origin)
self.filters = [GeneratePropertyFromParam(filter_instance)
for filter_instance in json.get('filters', [])]
self.filters = [
GeneratePropertyFromParam(filter_instance)
for filter_instance in json.get('filters', [])
]
# Any asynchronous return should be defined using the returns_async field.
returns_async = json.get('returns_async', None)
@ -383,14 +380,11 @@ class Function(object):
# incompatible with returning a promise. There are APIs that specify this,
# though, so we make sure they have specified does_not_support_promises if
# they do.
if (
json.get('returns') is not None
and self.returns_async.can_return_promise
):
if (json.get('returns') is not None
and self.returns_async.can_return_promise):
raise ValueError(
'Cannot specify both returns and returns_async on a function '
'which supports promies: %s.%s' % (namespace.name, name)
)
'which supports promies: %s.%s' % (namespace.name, name))
params = json.get('parameters', [])
for i, param in enumerate(params):
@ -398,11 +392,8 @@ class Function(object):
self.returns = None
if 'returns' in json:
self.returns = Type(self,
'%sReturnType' % name,
json['returns'],
namespace,
origin)
self.returns = Type(self, '%sReturnType' % name, json['returns'],
namespace, origin)
class ReturnsAsync(object):
@ -423,6 +414,7 @@ class ReturnsAsync(object):
callback. Currently only consumed for documentation
purposes
"""
def __init__(self, parent, json, namespace, origin):
self.name = json.get('name')
self.simple_name = _StripNamespace(self.name, namespace)
@ -434,25 +426,21 @@ class ReturnsAsync(object):
if json.get('returns') is not None:
raise ValueError(
'Cannot return a value from an asynchronous return: %s.%s in %s'
% (namespace.name, parent.name, namespace.source_file)
)
'Cannot return a value from an asynchronous return: %s.%s in %s' %
(namespace.name, parent.name, namespace.source_file))
if json.get('deprecated') is not None:
raise ValueError(
'Cannot specify deprecated on an asynchronous return: %s.%s in %s'
% (namespace.name, parent.name, namespace.source_file)
)
'Cannot specify deprecated on an asynchronous return: %s.%s in %s' %
(namespace.name, parent.name, namespace.source_file))
if json.get('parameters') is None:
raise ValueError(
'parameters key not specified on returns_async: %s.%s in %s'
% (namespace.name, parent.name, namespace.source_file)
)
'parameters key not specified on returns_async: %s.%s in %s' %
(namespace.name, parent.name, namespace.source_file))
if len(json.get('parameters')) > 1 and self.can_return_promise:
raise ValueError(
'Only a single parameter can be specific on a returns_async which'
' supports promises: %s.%s in %s'
% (namespace.name, parent.name, namespace.source_file)
)
' supports promises: %s.%s in %s' %
(namespace.name, parent.name, namespace.source_file))
def GeneratePropertyFromParam(p):
return Property(self, p['name'], p, namespace, origin)
@ -475,6 +463,7 @@ class Property(object):
- |simple_name| the name of this Property without a namespace
- |deprecated| a reason and possible alternative for a deprecated property
"""
def __init__(self, parent, name, json, namespace, origin):
"""Creates a Property from JSON.
"""
@ -491,11 +480,10 @@ class Property(object):
self.nodoc = json.get('nodoc', False)
# HACK: only support very specific value types.
is_allowed_value = (
'$ref' not in json and
('type' not in json or json['type'] == 'integer'
or json['type'] == 'number'
or json['type'] == 'string'))
is_allowed_value = ('$ref' not in json
and ('type' not in json or json['type'] == 'integer'
or json['type'] == 'number'
or json['type'] == 'string'))
self.value = None
if 'value' in json and is_allowed_value:
@ -532,20 +520,21 @@ class Property(object):
if unix_name == self._unix_name:
return
if self._unix_name_used:
raise AttributeError(
'Cannot set the unix_name on %s; '
'it is already used elsewhere as %s' %
(self.name, self._unix_name))
raise AttributeError('Cannot set the unix_name on %s; '
'it is already used elsewhere as %s' %
(self.name, self._unix_name))
self._unix_name = unix_name
unix_name = property(GetUnixName, SetUnixName)
class EnumValue(object):
"""A single value from an enum.
Properties:
- |name| name of the property as in the json.
- |description| a description of the property (if provided)
"""
def __init__(self, json, namespace):
if isinstance(json, dict):
self.name = json['name']
@ -556,17 +545,18 @@ class EnumValue(object):
# Using empty string values as enum key is only allowed in a few namespaces,
# as an exception to the rule, and we should not add more.
if (not self.name and
namespace.name not in ['enums', 'webstorePrivate']):
if (not self.name and namespace.name not in ['enums', 'webstorePrivate']):
raise ValueError('Enum value cannot be an empty string')
def CamelName(self):
return CamelName(self.name)
class _Enum(object):
"""Superclass for enum types with a "name" field, setting up repr/eq/ne.
Enums need to do this so that equality/non-equality work over pickling.
"""
@staticmethod
def GetAll(cls):
"""Yields all _Enum objects declared in |cls|.
@ -581,6 +571,7 @@ class _Enum(object):
def __eq__(self, other):
return type(other) == type(self) and other.name == self.name
def __ne__(self, other):
return not (self == other)
@ -595,6 +586,7 @@ class _Enum(object):
class _PropertyTypeInfo(_Enum):
def __init__(self, is_fundamental, name):
_Enum.__init__(self, name)
self.is_fundamental = is_fundamental
@ -602,6 +594,7 @@ class _PropertyTypeInfo(_Enum):
def __repr__(self):
return self.name
class PropertyType(object):
"""Enum of different types of properties/parameters.
"""
@ -619,111 +612,113 @@ class PropertyType(object):
REF = _PropertyTypeInfo(False, "ref")
STRING = _PropertyTypeInfo(True, "string")
def IsCPlusPlusKeyword(name):
"""Returns true if `name` is a C++ reserved keyword.
"""
# Obtained from https://en.cppreference.com/w/cpp/keyword.
keywords = {
"alignas",
"alignof",
"and",
"and_eq",
"asm",
"atomic_cancel",
"atomic_commit",
"atomic_noexcept",
"auto",
"bitand",
"bitor",
"bool",
"break",
"case",
"catch",
"char",
"char8_t",
"char16_t",
"char32_t",
"class",
"compl",
"concept",
"const",
"consteval",
"constexpr",
"constinit",
"const_cast",
"continue",
"co_await",
"co_return",
"co_yield",
"decltype",
"default",
"delete",
"do",
"double",
"dynamic_cast",
"else",
"enum",
"explicit",
"export",
"extern",
"false",
"float",
"for",
"friend",
"goto",
"if",
"inline",
"int",
"long",
"mutable",
"namespace",
"new",
"noexcept",
"not",
"not_eq",
"nullptr",
"operator",
"or",
"or_eq",
"private",
"protected",
"public",
"reflexpr",
"register",
"reinterpret_cast",
"requires",
"return",
"short",
"signed",
"sizeof",
"static",
"static_assert",
"static_cast",
"struct",
"switch",
"synchronized",
"template",
"this",
"thread_local",
"throw",
"true",
"try",
"typedef",
"typeid",
"typename",
"union",
"unsigned",
"using",
"virtual",
"void",
"volatile",
"wchar_t",
"while",
"xor",
"xor_eq"
"alignas",
"alignof",
"and",
"and_eq",
"asm",
"atomic_cancel",
"atomic_commit",
"atomic_noexcept",
"auto",
"bitand",
"bitor",
"bool",
"break",
"case",
"catch",
"char",
"char8_t",
"char16_t",
"char32_t",
"class",
"compl",
"concept",
"const",
"consteval",
"constexpr",
"constinit",
"const_cast",
"continue",
"co_await",
"co_return",
"co_yield",
"decltype",
"default",
"delete",
"do",
"double",
"dynamic_cast",
"else",
"enum",
"explicit",
"export",
"extern",
"false",
"float",
"for",
"friend",
"goto",
"if",
"inline",
"int",
"long",
"mutable",
"namespace",
"new",
"noexcept",
"not",
"not_eq",
"nullptr",
"operator",
"or",
"or_eq",
"private",
"protected",
"public",
"reflexpr",
"register",
"reinterpret_cast",
"requires",
"return",
"short",
"signed",
"sizeof",
"static",
"static_assert",
"static_cast",
"struct",
"switch",
"synchronized",
"template",
"this",
"thread_local",
"throw",
"true",
"try",
"typedef",
"typeid",
"typename",
"union",
"unsigned",
"using",
"virtual",
"void",
"volatile",
"wchar_t",
"while",
"xor",
"xor_eq",
}
return name in keywords
@memoize
def UnixName(name):
'''Returns the unix_style name for a given lowerCamelCase string.
@ -740,7 +735,7 @@ def UnixName(name):
# Prepend an extra underscore to the |name|'s start if it doesn't start with a
# letter or underscore to ensure the generated unix name follows C++
# identifier rules.
assert(name)
assert (name)
if name[0].isdigit():
name = '_' + name
@ -811,10 +806,7 @@ def _GetFunctions(parent, json, namespace):
"""
functions = OrderedDict()
for function_json in json.get('functions', []):
function = Function(parent,
function_json['name'],
function_json,
namespace,
function = Function(parent, function_json['name'], function_json, namespace,
Origin(from_json=True))
functions[function.name] = function
return functions
@ -825,10 +817,7 @@ def _GetEvents(parent, json, namespace):
"""
events = OrderedDict()
for event_json in json.get('events', []):
event = Function(parent,
event_json['name'],
event_json,
namespace,
event = Function(parent, event_json['name'], event_json, namespace,
Origin(from_client=True))
events[event.name] = event
return events
@ -853,12 +842,13 @@ def _GetManifestKeysType(self, json):
# Create a dummy object to parse "manifest_keys" as a type.
manifest_keys_type = {
'type': 'object',
'properties': json['manifest_keys'],
'type': 'object',
'properties': json['manifest_keys'],
}
return Type(self, 'ManifestKeys', manifest_keys_type, self,
Origin(from_manifest_keys=True))
def _GetWithDefaultChecked(self, json, key, default):
if json.get(key) == default:
raise ParseException(
@ -867,7 +857,9 @@ def _GetWithDefaultChecked(self, json, key, default):
% (key, default))
return json.get(key, default)
class _PlatformInfo(_Enum):
def __init__(self, name):
_Enum.__init__(self, name)

@ -9,63 +9,62 @@ from model import Platforms
import model
import unittest
class ModelTest(unittest.TestCase):
def setUp(self):
self.model = model.Model()
self.permissions_json = CachedLoad('test/permissions.json')
self.model.AddNamespace(self.permissions_json[0],
'path/to/permissions.json')
'path/to/permissions.json')
self.permissions = self.model.namespaces.get('permissions')
self.windows_json = CachedLoad('test/windows.json')
self.model.AddNamespace(self.windows_json[0],
'path/to/window.json')
self.model.AddNamespace(self.windows_json[0], 'path/to/window.json')
self.windows = self.model.namespaces.get('windows')
self.tabs_json = CachedLoad('test/tabs.json')
self.model.AddNamespace(self.tabs_json[0],
'path/to/tabs.json')
self.model.AddNamespace(self.tabs_json[0], 'path/to/tabs.json')
self.tabs = self.model.namespaces.get('tabs')
self.idl_chromeos = Load('test/idl_namespace_chromeos.idl')
self.model.AddNamespace(self.idl_chromeos[0],
'path/to/idl_namespace_chromeos.idl')
'path/to/idl_namespace_chromeos.idl')
self.idl_namespace_chromeos = self.model.namespaces.get(
'idl_namespace_chromeos')
self.idl_all_platforms = Load('test/idl_namespace_all_platforms.idl')
self.model.AddNamespace(self.idl_all_platforms[0],
'path/to/idl_namespace_all_platforms.idl')
'path/to/idl_namespace_all_platforms.idl')
self.idl_namespace_all_platforms = self.model.namespaces.get(
'idl_namespace_all_platforms')
self.idl_non_specific_platforms = Load(
'test/idl_namespace_non_specific_platforms.idl')
self.model.AddNamespace(self.idl_non_specific_platforms[0],
'path/to/idl_namespace_non_specific_platforms.idl')
'path/to/idl_namespace_non_specific_platforms.idl')
self.idl_namespace_non_specific_platforms = self.model.namespaces.get(
'idl_namespace_non_specific_platforms')
self.returns_async_json = CachedLoad('test/returns_async.json')
self.model.AddNamespace(self.returns_async_json[0],
'path/to/returns_async.json')
'path/to/returns_async.json')
self.returns_async = self.model.namespaces.get('returns_async')
self.idl_returns_async_idl = Load('test/idl_returns_async.idl')
self.model.AddNamespace(self.idl_returns_async_idl[0],
'path/to/idl_returns_async.idl')
'path/to/idl_returns_async.idl')
self.idl_returns_async = self.model.namespaces.get('idl_returns_async')
self.nodoc_json = CachedLoad('test/namespace_nodoc.json')
self.model.AddNamespace(self.nodoc_json[0],
'path/to/namespace_nodoc.json')
self.model.AddNamespace(self.nodoc_json[0], 'path/to/namespace_nodoc.json')
self.nodoc = self.model.namespaces.get('nodoc')
self.fakeapi_json = CachedLoad('test/namespace_fakeapi.json')
self.model.AddNamespace(self.fakeapi_json[0],
'path/to/namespace_fakeapi.json')
'path/to/namespace_fakeapi.json')
self.fakeapi = self.model.namespaces.get('fakeapi')
self.function_platforms_idl = Load('test/function_platforms.idl')
self.model.AddNamespace(self.function_platforms_idl[0],
'/path/to/function_platforms.idl')
'/path/to/function_platforms.idl')
self.function_platforms = self.model.namespaces.get('function_platforms')
self.function_platform_win_linux_json = CachedLoad(
'test/function_platform_win_linux.json')
self.model.AddNamespace(self.function_platform_win_linux_json[0],
'path/to/function_platform_win_linux.json')
'path/to/function_platform_win_linux.json')
self.function_platform_win_linux = self.model.namespaces.get(
'function_platform_win_linux')
@ -75,7 +74,7 @@ class ModelTest(unittest.TestCase):
def testHasFunctions(self):
self.assertEqual(["contains", "getAll", "remove", "request"],
sorted(self.permissions.functions.keys()))
sorted(self.permissions.functions.keys()))
def testHasTypes(self):
self.assertEqual(['Tab'], list(self.tabs.types.keys()))
@ -83,41 +82,38 @@ class ModelTest(unittest.TestCase):
self.assertEqual(['Window'], list(self.windows.types.keys()))
def testHasProperties(self):
self.assertEqual(["active", "favIconUrl", "highlighted", "id",
"incognito", "index", "pinned", "selected", "status", "title", "url",
"windowId"],
sorted(self.tabs.types['Tab'].properties.keys()))
self.assertEqual([
"active", "favIconUrl", "highlighted", "id", "incognito", "index",
"pinned", "selected", "status", "title", "url", "windowId"
], sorted(self.tabs.types['Tab'].properties.keys()))
def testProperties(self):
string_prop = self.tabs.types['Tab'].properties['status']
self.assertEqual(model.PropertyType.STRING,
string_prop.type_.property_type)
self.assertEqual(model.PropertyType.STRING, string_prop.type_.property_type)
integer_prop = self.tabs.types['Tab'].properties['id']
self.assertEqual(model.PropertyType.INTEGER,
integer_prop.type_.property_type)
integer_prop.type_.property_type)
array_prop = self.windows.types['Window'].properties['tabs']
self.assertEqual(model.PropertyType.ARRAY,
array_prop.type_.property_type)
self.assertEqual(model.PropertyType.ARRAY, array_prop.type_.property_type)
self.assertEqual(model.PropertyType.REF,
array_prop.type_.item_type.property_type)
array_prop.type_.item_type.property_type)
self.assertEqual('tabs.Tab', array_prop.type_.item_type.ref_type)
object_prop = self.tabs.functions['query'].params[0]
self.assertEqual(model.PropertyType.OBJECT,
object_prop.type_.property_type)
self.assertEqual(
["active", "highlighted", "pinned", "status", "title", "url",
"windowId", "windowType"],
sorted(object_prop.type_.properties.keys()))
self.assertEqual(model.PropertyType.OBJECT, object_prop.type_.property_type)
self.assertEqual([
"active", "highlighted", "pinned", "status", "title", "url", "windowId",
"windowType"
], sorted(object_prop.type_.properties.keys()))
def testChoices(self):
self.assertEqual(model.PropertyType.CHOICES,
self.tabs.functions['move'].params[0].type_.property_type)
self.tabs.functions['move'].params[0].type_.property_type)
def testPropertyNotImplemented(self):
(self.permissions_json[0]['types'][0]
['properties']['permissions']['type']) = 'something'
(self.permissions_json[0]['types'][0]['properties']['permissions']['type']
) = 'something'
self.assertRaises(model.ParseException, self.model.AddNamespace,
self.permissions_json[0], 'path/to/something.json')
self.permissions_json[0], 'path/to/something.json')
def testDefaultSpecifiedRedundantly(self):
test_json = CachedLoad('test/redundant_default_attribute.json')
@ -127,20 +123,16 @@ class ModelTest(unittest.TestCase):
' in path/to/redundant_default_attribute.json\n'
'The attribute "optional" is specified as "False", but this is the '
'default value if the attribute is not included\. It should be '
'removed\.',
self.model.AddNamespace,
test_json[0],
'removed\.', self.model.AddNamespace, test_json[0],
'path/to/redundant_default_attribute.json')
def testReturnsAsyncMissingParametersKey(self):
test_json = CachedLoad('test/returns_async_missing_parameters_key.json')
self.assertRaisesRegex(
ValueError,
'parameters key not specified on returns_async: '
ValueError, 'parameters key not specified on returns_async: '
'returnsAsyncMissingParametersKey.asyncNoParametersKey in '
'path/to/returns_async_missing_parameters_key.json',
self.model.AddNamespace,
test_json[0],
self.model.AddNamespace, test_json[0],
'path/to/returns_async_missing_parameters_key.json')
def testDescription(self):
@ -168,45 +160,44 @@ class ModelTest(unittest.TestCase):
def testUnixName(self):
expectations = {
'foo': 'foo',
'fooBar': 'foo_bar',
'fooBarBaz': 'foo_bar_baz',
'fooBARBaz': 'foo_bar_baz',
'fooBAR': 'foo_bar',
'FOO': 'foo',
'FOOBar': 'foo_bar',
'foo.bar': 'foo_bar',
'foo.BAR': 'foo_bar',
'foo.barBAZ': 'foo_bar_baz',
'foo_Bar_Baz_box': 'foo_bar_baz_box',
}
'foo': 'foo',
'fooBar': 'foo_bar',
'fooBarBaz': 'foo_bar_baz',
'fooBARBaz': 'foo_bar_baz',
'fooBAR': 'foo_bar',
'FOO': 'foo',
'FOOBar': 'foo_bar',
'foo.bar': 'foo_bar',
'foo.BAR': 'foo_bar',
'foo.barBAZ': 'foo_bar_baz',
'foo_Bar_Baz_box': 'foo_bar_baz_box',
}
for name in expectations:
self.assertEqual(expectations[name], model.UnixName(name))
def testCamelName(self):
expectations = {
'foo': 'foo',
'fooBar': 'fooBar',
'foo_bar_baz': 'fooBarBaz',
'FOO_BAR': 'FOOBar',
'FOO_bar': 'FOOBar',
'_bar': 'Bar',
'_bar_baz': 'BarBaz',
'bar_': 'bar',
'bar_baz_': 'barBaz',
}
'foo': 'foo',
'fooBar': 'fooBar',
'foo_bar_baz': 'fooBarBaz',
'FOO_BAR': 'FOOBar',
'FOO_bar': 'FOOBar',
'_bar': 'Bar',
'_bar_baz': 'BarBaz',
'bar_': 'bar',
'bar_baz_': 'barBaz',
}
for testcase, expected in expectations.items():
self.assertEqual(expected, model.CamelName(testcase))
def testPlatforms(self):
self.assertEqual([Platforms.CHROMEOS],
self.idl_namespace_chromeos.platforms)
self.assertEqual(
[Platforms.CHROMEOS, Platforms.FUCHSIA, Platforms.LINUX, Platforms.MAC,
Platforms.WIN],
self.idl_namespace_all_platforms.platforms)
self.assertEqual(None,
self.idl_namespace_non_specific_platforms.platforms)
self.assertEqual([
Platforms.CHROMEOS, Platforms.FUCHSIA, Platforms.LINUX, Platforms.MAC,
Platforms.WIN
], self.idl_namespace_all_platforms.platforms)
self.assertEqual(None, self.idl_namespace_non_specific_platforms.platforms)
def testInvalidNamespacePlatform(self):
invalid_namespace_platform = Load('test/invalid_platform_namespace.idl')
@ -225,7 +216,7 @@ class ModelTest(unittest.TestCase):
def testPlatformsOnFunctionsIDL(self):
function_win_linux = self.function_platforms.functions['function_win_linux']
self.assertEqual([Platforms.WIN, Platforms.LINUX],
function_win_linux.platforms)
function_win_linux.platforms)
function_all = self.function_platforms.functions['function_all']
self.assertIsNone(function_all.platforms)
@ -269,5 +260,6 @@ class ModelTest(unittest.TestCase):
self.assertIn('Enum value cannot be an empty string',
str(context.exception))
if __name__ == '__main__':
unittest.main()

@ -18,7 +18,7 @@ def _GenerateFilenames(full_namespace):
# 4. sub_name_space.idl,
# 5. etc.
sub_namespaces = full_namespace.split('.')
filenames = [ ]
filenames = []
basename = None
for namespace in reversed(sub_namespaces):
if basename is not None:
@ -39,6 +39,7 @@ class NamespaceResolver(object):
used when searching for types.
- |cpp_namespace_pattern| Default namespace pattern
'''
def __init__(self, root, path, include_rules, cpp_namespace_pattern):
self._root = root
self._include_rules = [(path, cpp_namespace_pattern)] + include_rules
@ -53,13 +54,12 @@ class NamespaceResolver(object):
if cpp_namespace:
cpp_namespace_environment = CppNamespaceEnvironment(cpp_namespace)
for filename in reversed(filenames):
filepath = os.path.join(path, filename);
filepath = os.path.join(path, filename)
if os.path.exists(os.path.join(self._root, filepath)):
schema = SchemaLoader(self._root).LoadSchema(filepath)[0]
return Model().AddNamespace(
schema,
filepath,
environment=cpp_namespace_environment)
return Model().AddNamespace(schema,
filepath,
environment=cpp_namespace_environment)
return None
def ResolveType(self, full_name, default_namespace):

@ -17,8 +17,8 @@ import optparse
import os
import shlex
import urlparse
from highlighters import (
pygments_highlighter, none_highlighter, hilite_me_highlighter)
from highlighters import (pygments_highlighter, none_highlighter,
hilite_me_highlighter)
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
from cpp_namespace_environment import CppNamespaceEnvironment
from namespace_resolver import NamespaceResolver
@ -27,6 +27,7 @@ from namespace_resolver import NamespaceResolver
class CompilerHandler(BaseHTTPRequestHandler):
"""A HTTPRequestHandler that outputs the result of tools/json_schema_compiler.
"""
def do_GET(self):
parsed_url = urlparse.urlparse(self.path)
request_path = self._GetRequestPath(parsed_url)
@ -64,38 +65,36 @@ class CompilerHandler(BaseHTTPRequestHandler):
Code panes are populated via XHR after links in the nav pane are clicked.
"""
(head.Append('<style>')
.Append('body {')
.Append(' margin: 0;')
.Append('}')
.Append('.pane {')
.Append(' height: 100%;')
.Append(' overflow-x: auto;')
.Append(' overflow-y: scroll;')
.Append(' display: inline-block;')
.Append('}')
.Append('#nav_pane {')
.Append(' width: 20%;')
.Append('}')
.Append('#nav_pane ul {')
.Append(' list-style-type: none;')
.Append(' padding: 0 0 0 1em;')
.Append('}')
.Append('#cc_pane {')
.Append(' width: 40%;')
.Append('}')
.Append('#h_pane {')
.Append(' width: 40%;')
.Append('}')
(head.Append('<style>') \
.Append('body {') \
.Append(' margin: 0;') \
.Append('}') \
.Append('.pane {') \
.Append(' height: 100%;') \
.Append(' overflow-x: auto;') \
.Append(' overflow-y: scroll;') \
.Append(' display: inline-block;') \
.Append('}') \
.Append('#nav_pane {') \
.Append(' width: 20%;') \
.Append('}') \
.Append('#nav_pane ul {') \
.Append(' list-style-type: none;') \
.Append(' padding: 0 0 0 1em;') \
.Append('}') \
.Append('#cc_pane {') \
.Append(' width: 40%;') \
.Append('}') \
.Append('#h_pane {') \
.Append(' width: 40%;') \
.Append('}') \
.Append('</style>')
)
body.Append(
'<div class="pane" id="nav_pane">%s</div>'
'<div class="pane" id="h_pane"></div>'
'<div class="pane" id="cc_pane"></div>' %
self._RenderNavPane(parsed_url.path[1:])
)
body.Append('<div class="pane" id="nav_pane">%s</div>'
'<div class="pane" id="h_pane"></div>'
'<div class="pane" id="cc_pane"></div>' %
self._RenderNavPane(parsed_url.path[1:]))
# The Javascript that interacts with the nav pane and panes to show the
# compiled files as the URL or highlighting options change.
@ -187,41 +186,39 @@ updateEverything();
(file_root, file_ext) = os.path.splitext(request_path)
(filedir, filename) = os.path.split(file_root)
namespace_resolver = NamespaceResolver("./",
filedir,
namespace_resolver = NamespaceResolver("./", filedir,
self.server.include_rules,
self.server.cpp_namespace_pattern)
try:
# Get main file.
namespace = namespace_resolver.ResolveNamespace(filename)
type_generator = cpp_type_generator.CppTypeGenerator(
api_model,
namespace_resolver,
namespace)
api_model, namespace_resolver, namespace)
# Generate code
if file_ext == '.h':
cpp_code = (h_generator.HGenerator(type_generator)
.Generate(namespace).Render())
cpp_code = (
h_generator.HGenerator(type_generator).Generate(namespace).Render())
elif file_ext == '.cc':
cpp_code = (cc_generator.CCGenerator(type_generator)
.Generate(namespace).Render())
cpp_code = (cc_generator.CCGenerator(type_generator).Generate(
namespace).Render())
else:
self.send_error(404, "File not found: %s" % request_path)
return
# Do highlighting on the generated code
(highlighter_param, style_param) = self._GetHighlighterParams(parsed_url)
head.Append('<style>' +
head.Append(
'<style>' +
self.server.highlighters[highlighter_param].GetCSS(style_param) +
'</style>')
body.Append(self.server.highlighters[highlighter_param]
.GetCodeElement(cpp_code, style_param))
body.Append(self.server.highlighters[highlighter_param].GetCodeElement(
cpp_code, style_param))
except IOError:
self.send_error(404, "File not found: %s" % request_path)
return
except (TypeError, KeyError, AttributeError,
AssertionError, NotImplementedError) as error:
except (TypeError, KeyError, AttributeError, AssertionError,
NotImplementedError) as error:
body.Append('<pre>')
body.Append('compiler error: %s' % error)
body.Append('Check server log for more details')
@ -233,7 +230,7 @@ updateEverything();
"""
query_dict = urlparse.parse_qs(parsed_url.query)
return (query_dict.get('highlighter', ['pygments'])[0],
query_dict.get('style', ['colorful'])[0])
query_dict.get('style', ['colorful'])[0])
def _RenderNavPane(self, path):
"""Renders an HTML nav pane.
@ -248,7 +245,7 @@ updateEverything();
html.Append('<select id="highlighters" onChange="updateEverything()">')
for name, highlighter in self.server.highlighters.items():
html.Append('<option value="%s">%s</option>' %
(name, highlighter.DisplayName()))
(name, highlighter.DisplayName()))
html.Append('</select>')
html.Append('<br/>')
@ -289,8 +286,7 @@ updateEverything();
html.Append('<li><a href="/%s/">%s/</a>' % (full_path, filename))
elif file_ext in ['.json', '.idl']:
# cc/h panes will automatically update via the hash change event.
html.Append('<li><a href="#%s">%s</a>' %
(filename, filename))
html.Append('<li><a href="#%s">%s</a>' % (filename, filename))
html.Append('</ul>')
@ -298,11 +294,8 @@ updateEverything();
class PreviewHTTPServer(HTTPServer, object):
def __init__(self,
server_address,
handler,
highlighters,
include_rules,
def __init__(self, server_address, handler, highlighters, include_rules,
cpp_namespace_pattern):
super(PreviewHTTPServer, self).__init__(server_address, handler)
self.highlighters = highlighters
@ -314,11 +307,18 @@ if __name__ == '__main__':
parser = optparse.OptionParser(
description='Runs a server to preview the json_schema_compiler output.',
usage='usage: %prog [option]...')
parser.add_option('-p', '--port', default='8000',
help='port to run the server on')
parser.add_option('-n', '--namespace', default='generated_api_schemas',
parser.add_option('-p',
'--port',
default='8000',
help='port to run the server on')
parser.add_option(
'-n',
'--namespace',
default='generated_api_schemas',
help='C++ namespace for generated files. e.g extensions::api.')
parser.add_option('-I', '--include-rules',
parser.add_option(
'-I',
'--include-rules',
help='A list of paths to include when searching for referenced objects,'
' with the namespace separated by a \':\'. Example: '
'/foo/bar:Foo::Bar::%(namespace)s')
@ -344,19 +344,16 @@ if __name__ == '__main__':
print('')
highlighters = {
'hilite': hilite_me_highlighter.HiliteMeHighlighter(),
'none': none_highlighter.NoneHighlighter()
'hilite': hilite_me_highlighter.HiliteMeHighlighter(),
'none': none_highlighter.NoneHighlighter()
}
try:
highlighters['pygments'] = pygments_highlighter.PygmentsHighlighter()
except ImportError as e:
pass
server = PreviewHTTPServer(('', int(opts.port)),
CompilerHandler,
highlighters,
include_rules,
opts.namespace)
server = PreviewHTTPServer(('', int(opts.port)), CompilerHandler,
highlighters, include_rules, opts.namespace)
server.serve_forever()
except KeyboardInterrupt:
server.socket.close()

@ -8,10 +8,12 @@ import sys
import idl_schema
import json_schema
class SchemaLoader(object):
'''Loads a schema from a provided filename.
|root|: path to the root directory.
'''
def __init__(self, root):
self._root = root

@ -4,6 +4,7 @@
"""Utilies for the processing of schema python structures.
"""
def CapitalizeFirstLetter(value):
return value[0].capitalize() + value[1:]

@ -7,7 +7,9 @@ from schema_util import JsFunctionNameToClassName
from schema_util import StripNamespace
import unittest
class SchemaUtilTest(unittest.TestCase):
def testStripNamespace(self):
self.assertEqual('Bar', StripNamespace('foo.Bar'))
self.assertEqual('Baz', StripNamespace('Baz'))
@ -15,11 +17,11 @@ class SchemaUtilTest(unittest.TestCase):
def testJsFunctionNameToClassName(self):
self.assertEqual('FooBar', JsFunctionNameToClassName('foo', 'bar'))
self.assertEqual('FooBar',
JsFunctionNameToClassName('experimental.foo', 'bar'))
JsFunctionNameToClassName('experimental.foo', 'bar'))
self.assertEqual('FooBarBaz', JsFunctionNameToClassName('foo.bar', 'baz'))
self.assertEqual('FooBarBaz',
JsFunctionNameToClassName('foo.bar', 'baz'))
self.assertEqual('FooBarBaz',
JsFunctionNameToClassName('experimental.foo.bar', 'baz'))
JsFunctionNameToClassName('experimental.foo.bar', 'baz'))
if __name__ == '__main__':
unittest.main()

@ -1,7 +1,6 @@
# Copyright 2023 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Generator that produces a definition file for typescript.
Note: This is a work in progress, and generated definitions may need tweaking.
@ -19,7 +18,6 @@ from js_util import JsUtil
from model import *
from schema_util import *
CHROMIUM_SRC = os.path.abspath(
os.path.join(os.path.dirname(__file__), "..", ".."))
@ -59,8 +57,7 @@ class _Generator(object):
# If events are needed, add the import.
if self._events_required:
main_code.Substitute(
{"imports": "import {ChromeEvent} from './chrome_event.js';"}
)
{"imports": "import {ChromeEvent} from './chrome_event.js';"})
else:
main_code.Substitute({"imports": ""})
main_code = self._ClangFormat(main_code)
@ -111,10 +108,8 @@ class _Generator(object):
type_name = self._ExtractType(prop.type_)
# If the ref type has additional properties, do a namespace merge.
prop_type: Type = prop.type_
if (
len(prop_type.properties) > 0
and prop_type.property_type == PropertyType.REF
):
if (len(prop_type.properties) > 0
and prop_type.property_type == PropertyType.REF):
type_name = self._AppendInterfaceForProperty(c, prop, type_name)
c.Append(f"export const {prop.name}: {type_name};")
c.Append()
@ -143,9 +138,8 @@ class _Generator(object):
# This appends an local only interface to allow for additional
# properties on an already defined type.
def _AppendInterfaceForProperty(
self, c: Code, prop: Property, prop_type_name
):
def _AppendInterfaceForProperty(self, c: Code, prop: Property,
prop_type_name):
if prop.deprecated:
return
prop_type = prop.type_
@ -177,16 +171,15 @@ class _Generator(object):
# Type alias
c.Append(f"export type {type.name} = {type.property_type.name};")
c.Append()
elif (type.property_type is PropertyType.ARRAY or
type.property_type is PropertyType.CHOICES) :
elif (type.property_type is PropertyType.ARRAY
or type.property_type is PropertyType.CHOICES):
ts_type = self._ExtractType(type)
c.Append(f"export type {type.name} = {ts_type};")
c.Append()
else:
# Adding this for things we may not have accounted for here.
c.Append(
f"// TODO({os.getlogin()}) -- {type.name}: {type.property_type.name}"
)
f"// TODO({os.getlogin()}) -- {type.name}: {type.property_type.name}")
def _AppendInterface(self, c: Code, interface: Type):
c.Sblock(f"export interface {interface.name} {{")
@ -231,8 +224,8 @@ class _Generator(object):
ret_type = "void"
if func.returns is not None:
ret_type = self._ExtractType(func.returns)
elif (func.returns_async is not None and
func.returns_async.can_return_promise):
elif (func.returns_async is not None
and func.returns_async.can_return_promise):
ret_type = f"Promise<{self._ExtractPromiseType(func.returns_async)}>"
return ret_type
@ -328,9 +321,8 @@ class _Generator(object):
# When the return async isn't a promise, we append it as a return callback
# at the end of the parameters.
use_callback = (
func.returns_async and not func.returns_async.can_return_promise
)
use_callback = (func.returns_async
and not func.returns_async.can_return_promise)
if use_callback:
callback_params = self._ExtractParams(func.returns_async.params)
if param_str:
@ -378,24 +370,21 @@ class _Generator(object):
def _ClangFormat(self, c: Code, level=0):
# temp = tempfile.NamedTemporaryFile("w", encoding="utf-8", suffix=".js")
# f_name = temp.name
with tempfile.NamedTemporaryFile(
"w", encoding="utf-8", suffix=".js", delete=False
) as f:
with tempfile.NamedTemporaryFile("w",
encoding="utf-8",
suffix=".js",
delete=False) as f:
f.write(c.Render())
f_name = f.name
script_path = self._GetChromiumClangFormatScriptPath()
style_path = self._GetChromiumClangFormatStylePath()
cmd = (
f'python3 {script_path} --fallback-style=none '
f'--style=file:{style_path} "{f_name}"'
)
p = subprocess.Popen(
cmd,
cwd=CHROMIUM_SRC,
encoding="utf-8",
shell=True,
stdout=subprocess.PIPE
)
cmd = (f'python3 {script_path} --fallback-style=none '
f'--style=file:{style_path} "{f_name}"')
p = subprocess.Popen(cmd,
cwd=CHROMIUM_SRC,
encoding="utf-8",
shell=True,
stdout=subprocess.PIPE)
out = p.communicate()[0]
out_code = Code()
out_code.Append(out)

@ -17,11 +17,8 @@ class TsDefinitionGeneratorTest(unittest.TestCase):
def _GetNamespace(self, fake_content, filename):
"""Returns a namespace object for the given content"""
is_idl = filename.endswith('.idl')
api_def = (
idl_schema.Process(fake_content, filename)
if is_idl
else json_parse.Parse(fake_content)
)
api_def = (idl_schema.Process(fake_content, filename)
if is_idl else json_parse.Parse(fake_content))
m = model.Model()
return m.AddNamespace(api_def[0], filename)

@ -9,14 +9,15 @@ class UtilCCHelper(object):
"""A util class that generates code that uses
tools/json_schema_compiler/util.cc.
"""
def __init__(self, type_manager):
self._type_manager = type_manager
def PopulateArrayFromListFunction(self, optional):
"""Returns the function to turn a list into a vector.
"""
populate_list_fn = ('PopulateOptionalArrayFromList' if optional
else 'PopulateArrayFromList')
populate_list_fn = ('PopulateOptionalArrayFromList'
if optional else 'PopulateArrayFromList')
return ('%s::%s') % (_API_UTIL_NAMESPACE, populate_list_fn)
def CreateValueFromArray(self, src):
@ -29,8 +30,8 @@ class UtilCCHelper(object):
def AppendToContainer(self, container, value):
"""Appends |value| to |container|.
"""
return '%s::AppendToContainer(%s, %s);' % (
_API_UTIL_NAMESPACE, container, value)
return '%s::AppendToContainer(%s, %s);' % (_API_UTIL_NAMESPACE, container,
value)
def GetIncludePath(self):
return '#include "tools/json_schema_compiler/util.h"'

@ -21,9 +21,8 @@ from json_parse import OrderedDict
# idl_parser expects to be able to import certain files in its directory,
# so let's set things up the way it wants.
_idl_generators_path = os.path.join(
os.path.dirname(os.path.realpath(__file__)), os.pardir, os.pardir, 'tools'
)
_idl_generators_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
os.pardir, os.pardir, 'tools')
if _idl_generators_path in sys.path:
from idl_parser import idl_parser, idl_lexer, idl_node
else:
@ -40,8 +39,7 @@ class SchemaCompilerError(Exception):
def __init__(self, message: str, node: IDLNode):
super().__init__(
node.GetLogLine(f'Error processing node {node}: {message}')
)
node.GetLogLine(f'Error processing node {node}: {message}'))
def GetChildWithName(node: IDLNode, name: str) -> Optional[IDLNode]:
@ -56,8 +54,7 @@ def GetChildWithName(node: IDLNode, name: str) -> Optional[IDLNode]:
name was not found.
"""
return next(
(child for child in node.GetChildren() if child.GetName() == name), None
)
(child for child in node.GetChildren() if child.GetName() == name), None)
def GetTypeName(node: IDLNode) -> str:
@ -76,8 +73,7 @@ def GetTypeName(node: IDLNode) -> str:
if child_node.GetClass() == 'Type':
return child_node.GetOneOf('Typeref').GetName()
raise SchemaCompilerError(
'Could not find Type node when looking for Typeref name.', node
)
'Could not find Type node when looking for Typeref name.', node)
def GetExtendedAttributes(node: IDLNode) -> Optional[List[IDLNode]]:
@ -111,9 +107,8 @@ class Type:
def __init__(self, node: IDLNode, additional_properties: dict) -> None:
assert node.GetClass() == 'Type', node.GetLogLine(
'Attempted to process a "Type" node, but was passed a "%s" node.'
% (node.GetClass())
)
'Attempted to process a "Type" node, but was passed a "%s" node.' %
(node.GetClass()))
self.node = node
self.additional_properties = additional_properties
@ -136,13 +131,11 @@ class Type:
properties['type'] = 'string'
else:
raise SchemaCompilerError(
'Unsupported basic type found when processing type.', basic_type
)
'Unsupported basic type found when processing type.', basic_type)
else:
unknown_child = self.node.GetChildren()[0]
raise SchemaCompilerError(
'Unsupported type class when processing type.', unknown_child
)
raise SchemaCompilerError('Unsupported type class when processing type.',
unknown_child)
return properties
@ -241,8 +234,7 @@ class IDLSchema:
browser_node = GetChildWithName(self.idl, 'Browser')
if browser_node is None or browser_node.GetClass() != 'Interface':
raise SchemaCompilerError(
'Required partial Browser interface not found in schema.', self.idl
)
'Required partial Browser interface not found in schema.', self.idl)
# The 'Browser' Interface has one attribute describing the name this API is
# exposed on.
@ -298,9 +290,8 @@ def Main():
for i, char in enumerate(contents):
if not char.isascii():
raise Exception(
'Non-ascii character "%s" (ord %d) found at offset %d.'
% (char, ord(char), i)
)
'Non-ascii character "%s" (ord %d) found at offset %d.' %
(char, ord(char), i))
idl = idl_parser.IDLParser().ParseData(contents, '<stdin>')
schema = IDLSchema(idl).process()
print(json.dumps(schema, indent=2))

@ -36,19 +36,31 @@ class WebIdlSchemaTest(unittest.TestCase):
getReturns(schema, 'returnsVoid'),
)
self.assertEqual(
{'name': 'returnsBoolean', 'type': 'boolean'},
{
'name': 'returnsBoolean',
'type': 'boolean'
},
getReturns(schema, 'returnsBoolean'),
)
self.assertEqual(
{'name': 'returnsDouble', 'type': 'number'},
{
'name': 'returnsDouble',
'type': 'number'
},
getReturns(schema, 'returnsDouble'),
)
self.assertEqual(
{'name': 'returnsLong', 'type': 'integer'},
{
'name': 'returnsLong',
'type': 'integer'
},
getReturns(schema, 'returnsLong'),
)
self.assertEqual(
{'name': 'returnsDOMString', 'type': 'string'},
{
'name': 'returnsDOMString',
'type': 'string'
},
getReturns(schema, 'returnsDOMString'),
)
@ -62,8 +74,7 @@ class WebIdlSchemaTest(unittest.TestCase):
def testMissingBrowserInterface(self):
expected_error_regex = (
'.* File\(test\/web_idl\/missing_browser_interface.idl\): Required'
' partial Browser interface not found in schema\.'
)
' partial Browser interface not found in schema\.')
self.assertRaisesRegex(
SchemaCompilerError,
expected_error_regex,
@ -76,8 +87,7 @@ class WebIdlSchemaTest(unittest.TestCase):
def testMissingAttributeOnBrowser(self):
expected_error_regex = (
'.* Interface\(Browser\): The partial Browser interface should have'
' exactly one attribute for the name the API will be exposed under\.'
)
' exactly one attribute for the name the API will be exposed under\.')
self.assertRaisesRegex(
Exception,
expected_error_regex,
@ -90,8 +100,7 @@ class WebIdlSchemaTest(unittest.TestCase):
def testUnsupportedBasicType(self):
expected_error_regex = (
'.* PrimitiveType\(float\): Unsupported basic type found when'
' processing type\.'
)
' processing type\.')
self.assertRaisesRegex(
SchemaCompilerError,
expected_error_regex,
@ -103,8 +112,7 @@ class WebIdlSchemaTest(unittest.TestCase):
# doesn't support yet throws an error.
def testUnsupportedTypeClass(self):
expected_error_regex = (
'.* Any\(\): Unsupported type class when processing type\.'
)
'.* Any\(\): Unsupported type class when processing type\.')
self.assertRaisesRegex(
SchemaCompilerError,
expected_error_regex,