| #!/usr/bin/env python3 |
| # |
| # Copyright (c) 2015-2016 Apple Inc. 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 re |
| import os |
| |
| from builtins_templates import BuiltinsGeneratorTemplates as Templates |
| |
| log = logging.getLogger('global') |
| |
| _FRAMEWORK_CONFIG_MAP = { |
| "JavaScriptCore": { |
| "macro_prefix": "JSC", |
| "namespace": "JSC", |
| }, |
| "WebCore": { |
| "macro_prefix": "WEBCORE", |
| "namespace": "WebCore", |
| }, |
| } |
| |
| functionHeadRegExp = re.compile(r"(?:@[\w|=\[\] \"\.]+\s*\n)*(?:async\s+)?function\s+\w+\s*\(.*?\)", re.MULTILINE | re.DOTALL) |
| functionGlobalPrivateRegExp = re.compile(r".*^@globalPrivate", re.MULTILINE | re.DOTALL) |
| functionNakedConstructorRegExp = re.compile(r".*^@nakedConstructor", re.MULTILINE | re.DOTALL) |
| functionIntrinsicRegExp = re.compile(r".*^@intrinsic=(\w+)", re.MULTILINE | re.DOTALL) |
| functionIsConstructorRegExp = re.compile(r".*^@constructor", re.MULTILINE | re.DOTALL) |
| functionIsGetterRegExp = re.compile(r".*^@getter", re.MULTILINE | re.DOTALL) |
| functionIsAsyncRegExp = re.compile(r"(async)?\s*function", re.MULTILINE | re.DOTALL) |
| functionNameRegExp = re.compile(r"function\s+(\w+)\s*\(", re.MULTILINE | re.DOTALL) |
| functionOverriddenNameRegExp = re.compile(r".*^@overriddenName=(\".+\")$", re.MULTILINE | re.DOTALL) |
| functionParameterFinder = re.compile(r"^(?:async\s+)?function\s+(?:\w+)\s*\(((?:\s*\w+)?\s*(?:\s*,\s*\w+)*)?\s*\)", re.MULTILINE | re.DOTALL) |
| functionParametersSplitter = re.compile(r",\s*(?![^{}]*\})", re.MULTILINE | re.DOTALL) |
| |
| multilineCommentRegExp = re.compile(r"\/\*.*?\*\/", re.MULTILINE | re.DOTALL) |
| singleLineCommentRegExp = re.compile(r"\/\/.*?\n", re.MULTILINE | re.DOTALL) |
| keyValueAnnotationCommentRegExp = re.compile(r"^\/\/ @(\w+)=([^=]+?)\n", re.MULTILINE | re.DOTALL) |
| flagAnnotationCommentRegExp = re.compile(r"^\/\/ @(\w+)[^=]*?\n", re.MULTILINE | re.DOTALL) |
| lineWithOnlySingleLineCommentRegExp = re.compile(r"^\s*\/\/\n", re.MULTILINE | re.DOTALL) |
| lineWithTrailingSingleLineCommentRegExp = re.compile(r"\s*\/\/\n", re.MULTILINE | re.DOTALL) |
| leadingWhitespaceRegExp = re.compile(r"^ +", re.MULTILINE | re.DOTALL) |
| multipleEmptyLinesRegExp = re.compile(r"\n{2,}", re.MULTILINE | re.DOTALL) |
| |
| class ParseException(Exception): |
| pass |
| |
| |
| class Framework: |
| def __init__(self, name): |
| self._settings = _FRAMEWORK_CONFIG_MAP[name] |
| self.name = name |
| |
| def setting(self, key, default=''): |
| return self._settings.get(key, default) |
| |
| @staticmethod |
| def fromString(frameworkString): |
| if frameworkString == "JavaScriptCore": |
| return Frameworks.JavaScriptCore |
| |
| if frameworkString == "WebCore": |
| return Frameworks.WebCore |
| |
| raise ParseException("Unknown framework: %s" % frameworkString) |
| |
| |
| class Frameworks: |
| JavaScriptCore = Framework("JavaScriptCore") |
| WebCore = Framework("WebCore") |
| |
| |
| class BuiltinObject: |
| def __init__(self, object_name, annotations, functions): |
| self.object_name = object_name |
| self.annotations = annotations |
| self.functions = functions |
| self.collection = None # Set by the owning BuiltinsCollection |
| |
| for function in self.functions: |
| function.object = self |
| |
| |
| class BuiltinFunction: |
| def __init__(self, function_name, function_source, parameters, is_async, is_constructor, is_global_private, is_naked_constructor, intrinsic, overridden_name): |
| self.function_name = function_name |
| self.function_source = function_source |
| self.parameters = parameters |
| self.is_async = is_async |
| self.is_constructor = is_constructor |
| self.is_naked_constructor = is_naked_constructor |
| self.is_global_private = is_global_private |
| self.intrinsic = intrinsic |
| self.overridden_name = overridden_name |
| self.object = None # Set by the owning BuiltinObject |
| |
| @staticmethod |
| def fromString(function_string): |
| function_source = multilineCommentRegExp.sub("", function_string) |
| |
| intrinsic = "NoIntrinsic" |
| intrinsicMatch = functionIntrinsicRegExp.search(function_source) |
| if intrinsicMatch: |
| intrinsic = intrinsicMatch.group(1) |
| function_source = functionIntrinsicRegExp.sub("", function_source) |
| |
| overridden_name = None |
| overriddenNameMatch = functionOverriddenNameRegExp.search(function_source) |
| if overriddenNameMatch: |
| overridden_name = overriddenNameMatch.group(1) |
| function_source = functionOverriddenNameRegExp.sub("", function_source) |
| |
| if not os.getenv("CONFIGURATION", "Debug").startswith("Debug"): |
| function_source = lineWithOnlySingleLineCommentRegExp.sub("", function_source) |
| function_source = lineWithTrailingSingleLineCommentRegExp.sub("\n", function_source) |
| function_source = leadingWhitespaceRegExp.sub("", function_source) |
| function_source = multipleEmptyLinesRegExp.sub("\n", function_source) |
| |
| function_name = functionNameRegExp.findall(function_source)[0] |
| async_match = functionIsAsyncRegExp.match(function_source) |
| is_async = async_match != None and async_match.group(1) == "async" |
| is_constructor = functionIsConstructorRegExp.match(function_source) != None |
| is_getter = functionIsGetterRegExp.match(function_source) != None |
| is_global_private = functionGlobalPrivateRegExp.match(function_source) != None |
| is_naked_constructor = functionNakedConstructorRegExp.match(function_source) != None |
| if is_naked_constructor: |
| is_constructor = True |
| parameters = [s.strip() for s in functionParametersSplitter.split(functionParameterFinder.findall(function_source)[0])] |
| if len(parameters[0]) == 0: |
| parameters = [] |
| |
| if is_getter and not overridden_name: |
| overridden_name = "\"get %s\"_s" % (function_name) |
| |
| if not overridden_name: |
| overridden_name = "ASCIILiteral()" |
| |
| if overridden_name[-1] == "\"": |
| overridden_name += "_s" |
| |
| return BuiltinFunction(function_name, function_source, parameters, is_async, is_constructor, is_global_private, is_naked_constructor, intrinsic, overridden_name) |
| |
| def __str__(self): |
| interface = "%s(%s)" % (self.function_name, ', '.join(self.parameters)) |
| if self.is_constructor: |
| interface = interface + " [Constructor]" |
| |
| if self.is_async: |
| interface = "async " + interface |
| |
| return interface |
| |
| def __lt__(self, other): |
| return self.function_name < other.function_name |
| |
| |
| class BuiltinsCollection: |
| def __init__(self, framework_name): |
| self._copyright_lines = set() |
| self.objects = [] |
| self.framework = Framework.fromString(framework_name) |
| log.debug("Created new Builtins collection.") |
| |
| def parse_builtins_file(self, filename, text): |
| log.debug("Parsing builtins file: %s" % filename) |
| |
| parsed_copyrights = set(self._parse_copyright_lines(text)) |
| self._copyright_lines = self._copyright_lines.union(parsed_copyrights) |
| |
| log.debug("Found copyright lines:") |
| for line in self._copyright_lines: |
| log.debug(line) |
| log.debug("") |
| |
| object_annotations = self._parse_annotations(text) |
| |
| object_name, ext = os.path.splitext(os.path.basename(filename)) |
| log.debug("Parsing object: %s" % object_name) |
| |
| parsed_functions = self._parse_functions(text) |
| for function in parsed_functions: |
| function.object = object_name |
| |
| log.debug("Parsed functions:") |
| for func in parsed_functions: |
| log.debug(func) |
| log.debug("") |
| |
| new_object = BuiltinObject(object_name, object_annotations, parsed_functions) |
| new_object.collection = self |
| self.objects.append(new_object) |
| |
| def copyrights(self): |
| owner_to_years = dict() |
| copyrightYearRegExp = re.compile(r"(\d{4})[, ]{0,2}") |
| ownerStartRegExp = re.compile(r"[^\d, ]") |
| |
| # Returns deduplicated copyrights keyed on the owner. |
| for line in self._copyright_lines: |
| years = set(copyrightYearRegExp.findall(line)) |
| ownerIndex = ownerStartRegExp.search(line).start() |
| owner = line[ownerIndex:] |
| log.debug("Found years: %s and owner: %s" % (years, owner)) |
| if owner not in owner_to_years: |
| owner_to_years[owner] = set() |
| |
| owner_to_years[owner] = owner_to_years[owner].union(years) |
| |
| result = [] |
| |
| for owner, years in list(owner_to_years.items()): |
| sorted_years = list(years) |
| sorted_years.sort() |
| result.append("%s %s" % (', '.join(sorted_years), owner)) |
| |
| return result |
| |
| def all_functions(self): |
| result = [] |
| for object in self.objects: |
| result.extend(object.functions) |
| |
| result.sort() |
| return result |
| |
| def all_internal_functions(self): |
| result = [] |
| for object in [o for o in self.objects if 'internal' in o.annotations]: |
| result.extend(object.functions) |
| |
| result.sort() |
| return result |
| |
| # Private methods. |
| |
| def _parse_copyright_lines(self, text): |
| licenseBlock = multilineCommentRegExp.findall(text)[0] |
| licenseBlock = licenseBlock[:licenseBlock.index("Redistribution")] |
| |
| copyrightLines = [Templates.DefaultCopyright] |
| for line in licenseBlock.split("\n"): |
| line = line.replace("/*", "") |
| line = line.replace("*/", "") |
| line = line.replace("*", "") |
| line = line.replace("Copyright", "") |
| line = line.replace("copyright", "") |
| line = line.replace("(C)", "") |
| line = line.replace("(c)", "") |
| line = line.strip() |
| |
| if len(line) == 0: |
| continue |
| |
| copyrightLines.append(line) |
| |
| return copyrightLines |
| |
| def _parse_annotations(self, text): |
| annotations = {} |
| |
| for match in keyValueAnnotationCommentRegExp.finditer(text): |
| (key, value) = match.group(1, 2) |
| log.debug("Found annotation: '%s' => '%s'" % (key, value)) |
| if key in annotations: |
| raise ParseException("Duplicate annotation found: %s" % key) |
| |
| annotations[key] = value |
| |
| for match in flagAnnotationCommentRegExp.finditer(text): |
| key = match.group(1) |
| log.debug("Found annotation: '%s' => 'TRUE'" % key) |
| if key in annotations: |
| raise ParseException("Duplicate annotation found: %s" % key) |
| |
| annotations[key] = True |
| |
| return annotations |
| |
| def _parse_functions(self, text): |
| text = multilineCommentRegExp.sub("/**/", singleLineCommentRegExp.sub("//\n", text)) |
| |
| matches = [func for func in functionHeadRegExp.finditer(text)] |
| functionBounds = [] |
| start = 0 |
| end = 0 |
| |
| for match in matches: |
| start = match.start() |
| if start < end: |
| continue |
| end = match.end() |
| while text[end] != '{': |
| end += 1 |
| depth = 1 |
| isEscapingCharacter = False |
| currentStringStartCharacter = None |
| while depth > 0: |
| end += 1 |
| currentCharacter = text[end] |
| |
| if isEscapingCharacter: |
| isEscapingCharacter = False |
| continue |
| |
| if currentCharacter in "`'\"": |
| if currentStringStartCharacter is None: |
| currentStringStartCharacter = currentCharacter |
| elif currentCharacter == currentStringStartCharacter: |
| currentStringStartCharacter = None |
| continue |
| |
| if currentCharacter == '\\' and currentStringStartCharacter is not None: |
| isEscapingCharacter = True |
| continue |
| |
| # FIXME: <webkit.org/b/239817> Regular expressions containing unbalanced quotation marks (like the one |
| # found in `StringPrototype.js`'s `createHTML`) can confuse the state of tracking our being |
| # inside/outside a string. To work around this, just reset our string state at the end of each line |
| # (unless we are inside a template string). This will work unless a regular expression contains |
| # unbalanced curly brackets or a closing curly bracket we care about is on the same line as the earlier |
| # regular expression with an unbalanced quote. |
| if currentCharacter == '\n' and currentStringStartCharacter != '`': |
| currentStringStartCharacter = None |
| continue |
| |
| if currentStringStartCharacter is not None: |
| continue |
| |
| if currentCharacter == '{': |
| depth += 1 |
| elif currentCharacter == '}': |
| depth -= 1 |
| |
| end += 1 |
| functionBounds.append((start, end)) |
| |
| functionStrings = [text[start:end].strip() for (start, end) in functionBounds] |
| return list(map(BuiltinFunction.fromString, functionStrings)) |