| #!/usr/bin/env python |
| # |
| # Copyright (c) 2014 Apple Inc. All rights reserved. |
| # Copyright (c) 2014 University of Washington. All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions |
| # are met: |
| # 1. Redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer. |
| # 2. Redistributions in binary form must reproduce the above copyright |
| # notice, this list of conditions and the following disclaimer in the |
| # documentation and/or other materials provided with the distribution. |
| # |
| # THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| # THE POSSIBILITY OF SUCH DAMAGE. |
| |
| import logging |
| import os.path |
| import re |
| from string import Template |
| |
| try: |
| from .generator_templates import GeneratorTemplates as Templates |
| from .models import PrimitiveType, ObjectType, ArrayType, EnumType, AliasedType, Frameworks, Platforms |
| except ValueError: |
| from generator_templates import GeneratorTemplates as Templates |
| from models import PrimitiveType, ObjectType, ArrayType, EnumType, AliasedType, Frameworks, Platforms |
| |
| log = logging.getLogger('global') |
| |
| |
| def ucfirst(str): |
| return str[:1].upper() + str[1:] |
| |
| _ALWAYS_SPECIALCASED_ENUM_VALUE_SUBSTRINGS = set(['2D', 'API', 'CSS', 'DOM', 'HTML', 'JIT', 'XHR', 'XML', 'IOS', 'MacOS', 'JavaScript', 'ServiceWorker']) |
| _ALWAYS_SPECIALCASED_ENUM_VALUE_LOOKUP_TABLE = dict([(s.upper(), s) for s in _ALWAYS_SPECIALCASED_ENUM_VALUE_SUBSTRINGS]) |
| |
| _ENUM_IDENTIFIER_RENAME_MAP = { |
| 'canvas-bitmaprenderer': 'CanvasBitmapRenderer', # Recording.Type.canvas-bitmaprenderer |
| 'canvas-webgl': 'CanvasWebGL', # Recording.Type.canvas-webgl |
| 'canvas-webgl2': 'CanvasWebGL2', # Recording.Type.canvas-webgl2 |
| 'webgl': 'WebGL', # Canvas.ContextType.webgl |
| 'webgl2': 'WebGL2', # Canvas.ContextType.webgl2 |
| 'webgpu': 'WebGPU', # Canvas.ContextType.gpu |
| 'bitmaprenderer': 'BitmapRenderer', # Canvas.ContextType.bitmaprenderer |
| 'webrtc': 'WebRTC', # Console.ChannelSource.webrtc |
| 'mediasource': 'MediaSource', # Console.ChannelSource.mediasource |
| 'webkit': 'WebKit', # CPUProfiler.ThreadInfo.type |
| } |
| |
| # These objects are built manually by creating and setting JSON::Value instances. |
| # Before sending these over the protocol, their shapes are checked against the specification. |
| # So, any types referenced by these types require debug-only assertions that check values. |
| # Calculating necessary assertions is annoying, and adds a lot of complexity to the generator. |
| |
| # FIXME: This should be converted into a property in JSON. |
| _TYPES_NEEDING_RUNTIME_CASTS = set([ |
| "Runtime.ObjectPreview", |
| "Runtime.RemoteObject", |
| "Runtime.PropertyDescriptor", |
| "Runtime.InternalPropertyDescriptor", |
| "Runtime.CollectionEntry", |
| "Debugger.FunctionDetails", |
| "Debugger.CallFrame", |
| "Canvas.TraceLog", |
| "Canvas.ResourceInfo", |
| "Canvas.ResourceState", |
| # This should be a temporary hack. TimelineEvent should be created via generated C++ API. |
| "Timeline.TimelineEvent", |
| # For testing purposes only. |
| "Test.TypeNeedingCast" |
| ]) |
| |
| # FIXME: This should be converted into a property in JSON. |
| _TYPES_WITH_OPEN_FIELDS = { |
| "Timeline.TimelineEvent": [], |
| # InspectorStyleSheet not only creates this property but wants to read it and modify it. |
| "CSS.CSSProperty": [], |
| # InspectorNetworkAgent needs to update mime-type. |
| "Network.Response": ["mimeType"], |
| # For testing purposes only. |
| "Test.OpenParameters": ["alpha"], |
| } |
| |
| class Generator: |
| def __init__(self, model, platform, input_filepath): |
| self._model = model |
| self._platform = platform |
| self._input_filepath = input_filepath |
| self._settings = {} |
| |
| def model(self): |
| return self._model |
| |
| def platform(self): |
| return self._platform |
| |
| def set_generator_setting(self, key, value): |
| self._settings[key] = value |
| |
| def can_generate_platform(self, model_platform): |
| return model_platform is Platforms.Generic or self._platform is Platforms.All or model_platform is self._platform |
| |
| def version_for_domain(self, domain): |
| return domain.version() |
| |
| def type_declarations_for_domain(self, domain): |
| return [type_declaration for type_declaration in domain.all_type_declarations() if self.can_generate_platform(type_declaration.platform)] |
| |
| def commands_for_domain(self, domain): |
| return [command for command in domain.all_commands() if self.can_generate_platform(command.platform)] |
| |
| def events_for_domain(self, domain): |
| return [event for event in domain.all_events() if self.can_generate_platform(event.platform)] |
| |
| # The goofy name is to disambiguate generator settings from framework settings. |
| def get_generator_setting(self, key, default=None): |
| return self._settings.get(key, default) |
| |
| def generate_license(self): |
| return Template(Templates.CopyrightBlock).substitute(None, inputFilename=os.path.basename(self._input_filepath)) |
| |
| def generate_includes_from_entries(self, entries): |
| includes = set() |
| for entry in entries: |
| (allowed_framework_names, data) = entry |
| (framework_name, header_path) = data |
| |
| allowed_frameworks = allowed_framework_names + ["Test"] |
| if self.model().framework.name not in allowed_frameworks: |
| continue |
| |
| if framework_name == "WTF": |
| includes.add("#include <%s>" % header_path) |
| elif self.model().framework.name != framework_name: |
| includes.add("#include <%s/%s>" % (framework_name, os.path.basename(header_path))) |
| else: |
| includes.add("#include \"%s\"" % os.path.basename(header_path)) |
| |
| return sorted(list(includes)) |
| |
| # These methods are overridden by subclasses. |
| def non_supplemental_domains(self): |
| return [domain for domain in self.model().domains if not domain.is_supplemental] |
| |
| def domains_to_generate(self): |
| return self.non_supplemental_domains() |
| |
| def generate_output(self): |
| pass |
| |
| def output_filename(self): |
| pass |
| |
| def encoding_for_enum_value(self, enum_value): |
| if not hasattr(self, "_assigned_enum_values"): |
| self._traverse_and_assign_enum_values() |
| |
| return self._enum_value_encodings[enum_value] |
| |
| def assigned_enum_values(self): |
| if not hasattr(self, "_assigned_enum_values"): |
| self._traverse_and_assign_enum_values() |
| |
| return self._assigned_enum_values[:] # Slice. |
| |
| @staticmethod |
| def type_needs_runtime_casts(_type): |
| return _type.qualified_name() in _TYPES_NEEDING_RUNTIME_CASTS |
| |
| @staticmethod |
| def type_has_open_fields(_type): |
| return _type.qualified_name() in _TYPES_WITH_OPEN_FIELDS |
| |
| @staticmethod |
| def open_fields(type_declaration): |
| fields = set(_TYPES_WITH_OPEN_FIELDS.get(type_declaration.type.qualified_name(), [])) |
| if not fields: |
| return type_declaration.type_members |
| return [member for member in type_declaration.type_members if member.member_name in fields] |
| |
| def type_needs_shape_assertions(self, _type): |
| if not hasattr(self, "_types_needing_shape_assertions"): |
| self.calculate_types_requiring_shape_assertions(self.model().domains) |
| |
| return _type in self._types_needing_shape_assertions |
| |
| # To restrict the domains over which we compute types needing assertions, call this method |
| # before generating any output with the desired domains parameter. The computed |
| # set of types will not be automatically regenerated on subsequent calls to |
| # Generator.types_needing_shape_assertions(). |
| def calculate_types_requiring_shape_assertions(self, domains): |
| domain_names = [domain.domain_name for domain in domains] |
| log.debug("> Calculating types that need shape assertions (eligible domains: %s)" % ", ".join(domain_names)) |
| |
| # Mutates the passed-in set; this simplifies checks to prevent infinite recursion. |
| def gather_transitively_referenced_types(_type, gathered_types): |
| if _type in gathered_types: |
| return |
| |
| if isinstance(_type, ObjectType): |
| log.debug("> Adding type %s to list of types needing shape assertions." % _type.qualified_name()) |
| gathered_types.add(_type) |
| for type_member in _type.members: |
| gather_transitively_referenced_types(type_member.type, gathered_types) |
| elif isinstance(_type, EnumType): |
| log.debug("> Adding type %s to list of types needing shape assertions." % _type.qualified_name()) |
| gathered_types.add(_type) |
| elif isinstance(_type, AliasedType): |
| gather_transitively_referenced_types(_type.aliased_type, gathered_types) |
| elif isinstance(_type, ArrayType): |
| gather_transitively_referenced_types(_type.element_type, gathered_types) |
| |
| found_types = set() |
| for domain in domains: |
| for declaration in self.type_declarations_for_domain(domain): |
| if declaration.type.qualified_name() in _TYPES_NEEDING_RUNTIME_CASTS: |
| log.debug("> Gathering types referenced by %s to generate shape assertions." % declaration.type.qualified_name()) |
| gather_transitively_referenced_types(declaration.type, found_types) |
| |
| self._types_needing_shape_assertions = found_types |
| |
| # Private helper instance methods. |
| def _traverse_and_assign_enum_values(self): |
| self._enum_value_encodings = {} |
| self._assigned_enum_values = [] |
| all_types = [] |
| |
| domains = self.non_supplemental_domains() |
| |
| for domain in domains: |
| for type_declaration in self.type_declarations_for_domain(domain): |
| all_types.append(type_declaration.type) |
| for type_member in type_declaration.type_members: |
| all_types.append(type_member.type) |
| |
| for domain in domains: |
| for event in self.events_for_domain(domain): |
| all_types.extend([parameter.type for parameter in event.event_parameters]) |
| |
| for domain in domains: |
| for command in self.commands_for_domain(domain): |
| all_types.extend([parameter.type for parameter in command.call_parameters]) |
| all_types.extend([parameter.type for parameter in command.return_parameters]) |
| |
| for _type in all_types: |
| if not isinstance(_type, EnumType): |
| continue |
| list(map(self._assign_encoding_for_enum_value, _type.enum_values())) |
| |
| def _assign_encoding_for_enum_value(self, enum_value): |
| if enum_value in self._enum_value_encodings: |
| return |
| |
| self._enum_value_encodings[enum_value] = len(self._assigned_enum_values) |
| self._assigned_enum_values.append(enum_value) |
| |
| # Miscellaneous text manipulation routines. |
| def wrap_with_guard_for_domain(self, domain, text): |
| if self.model().framework is Frameworks.WebInspector: |
| return text |
| guard = domain.feature_guard |
| if guard: |
| return Generator.wrap_with_guard(guard, text) |
| return text |
| |
| @staticmethod |
| def wrap_with_guard(guard, text): |
| return '\n'.join([ |
| '#if %s' % guard, |
| text, |
| '#endif // %s' % guard, |
| ]) |
| |
| @staticmethod |
| def stylized_name_for_enum_value(enum_value): |
| regex = '(%s)' % "|".join(_ALWAYS_SPECIALCASED_ENUM_VALUE_SUBSTRINGS) |
| |
| def replaceCallback(match): |
| return _ALWAYS_SPECIALCASED_ENUM_VALUE_LOOKUP_TABLE[match.group(1).upper()] |
| |
| # Split on hyphen, introduce camelcase, and force uppercasing of acronyms. |
| subwords = list(map(ucfirst, _ENUM_IDENTIFIER_RENAME_MAP.get(enum_value, enum_value).split('-'))) |
| return re.sub(re.compile(regex, re.IGNORECASE), replaceCallback, "".join(subwords)) |
| |
| @staticmethod |
| def js_name_for_parameter_type(_type): |
| _type = _type |
| if isinstance(_type, AliasedType): |
| _type = _type.aliased_type # Fall through. |
| if isinstance(_type, EnumType): |
| _type = _type.primitive_type # Fall through. |
| |
| if isinstance(_type, (ArrayType, ObjectType)): |
| return 'object' |
| if isinstance(_type, PrimitiveType): |
| if _type.qualified_name() in ['object', 'any']: |
| return 'object' |
| elif _type.qualified_name() in ['integer', 'number']: |
| return 'number' |
| else: |
| return _type.qualified_name() |
| |
| @staticmethod |
| def string_for_file_include(filename, file_framework, target_framework): |
| if file_framework is target_framework: |
| return '"%s"' % filename |
| else: |
| return '<%s/%s>' % (file_framework.name, filename) |