| #!/usr/bin/env python3 | |
| # Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file | |
| # for details. All rights reserved. Use of this source code is governed by a | |
| # BSD-style license that can be found in the LICENSE file. | |
| import json | |
| import os.path | |
| import re | |
| import sys | |
| _COMPAT_KEY = '__compat' | |
| _EXPERIMENTAL_KEY = 'experimental' | |
| _STATUS_KEY = 'status' | |
| _SUPPORT_KEY = 'support' | |
| _VERSION_ADDED_KEY = 'version_added' | |
| def _get_browser_compat_data(): | |
| current_dir = os.path.dirname(__file__) | |
| browser_compat_folder = os.path.abspath( | |
| os.path.join(current_dir, '..', '..', '..', 'third_party', 'mdn', | |
| 'browser-compat-data', 'src')) | |
| if not os.path.exists(browser_compat_folder): | |
| raise RuntimeError('Browser compatibility data not found at %s' % | |
| browser_compat_folder) | |
| browser_compat_data = {} | |
| INCLUDE_DIRS = [ | |
| 'api', | |
| 'html', | |
| 'svg', | |
| # TODO(srujzs): add more if needed | |
| ] | |
| # Transform to absolute paths | |
| INCLUDE_DIRS = [ | |
| os.path.join(browser_compat_folder, dir) for dir in INCLUDE_DIRS | |
| ] | |
| def process_json_dict(json_dict): | |
| # Returns a tuple of the interface name and the metadata corresponding | |
| # to it. | |
| if 'api' in json_dict: | |
| # Get the interface name | |
| api_dict = json_dict['api'] | |
| interface_name = next(iter(api_dict)) | |
| return (interface_name, api_dict[interface_name]) | |
| elif 'html' in json_dict: | |
| html_dict = json_dict['html'] | |
| if 'elements' in html_dict: | |
| elements_dict = html_dict['elements'] | |
| element_name = next(iter(elements_dict)) | |
| # Convert to WebCore name | |
| interface = str('HTML' + element_name + 'Element') | |
| return (interface, elements_dict[element_name]) | |
| elif 'svg' in json_dict: | |
| svg_dict = json_dict['svg'] | |
| if 'elements' in svg_dict: | |
| elements_dict = svg_dict['elements'] | |
| element_name = next(iter(elements_dict)) | |
| # Convert to WebCore name | |
| interface = str('SVG' + element_name + 'Element') | |
| return (interface, elements_dict[element_name]) | |
| return (None, None) | |
| # Attempts to unify two compatibility infos by taking the union of both, and | |
| # for conflicting information, taking the "stricter" of the two versions. | |
| # Updates `a` in place to represent the union of `a` and `b`. | |
| def _unify_compat(a, b): | |
| def _has_compat_data(metadata): | |
| return _COMPAT_KEY in metadata and _SUPPORT_KEY in metadata[_COMPAT_KEY] | |
| # Unifies the support statements of both metadata and updates | |
| # `support_a` in place. If either metadata do not contain simple support | |
| # statements, defaults attribute to not supported. | |
| def _unify_support(support_a, support_b): | |
| for browser in support_a.keys(): | |
| if browser in support_b: | |
| if _is_simple_support_statement(support_a[browser]) and _is_simple_support_statement(support_b[browser]): | |
| support_a[browser][_VERSION_ADDED_KEY] = _unify_versions( | |
| support_a[browser][_VERSION_ADDED_KEY], | |
| support_b[browser][_VERSION_ADDED_KEY]) | |
| else: | |
| # Only support simple statements for now. | |
| support_a[browser] = {_VERSION_ADDED_KEY: None} | |
| for browser in support_b.keys(): | |
| if not browser in support_a: | |
| support_a[browser] = support_b[browser] | |
| if not _has_compat_data(b): | |
| return | |
| if not _has_compat_data(a): | |
| a[_COMPAT_KEY] = b[_COMPAT_KEY] | |
| return | |
| support_a = a[_COMPAT_KEY][_SUPPORT_KEY] | |
| support_b = b[_COMPAT_KEY][_SUPPORT_KEY] | |
| _unify_support(support_a, support_b) | |
| # Unifies any status info in the two metadata. Modifies `a` in place to | |
| # represent the union of both `a` and `b`. | |
| def _unify_status(a, b): | |
| def _has_status(metadata): | |
| return _COMPAT_KEY in metadata and _STATUS_KEY in metadata[_COMPAT_KEY] | |
| # Modifies `status_a` in place to combine "experimental" tags. | |
| def _unify_experimental(status_a, status_b): | |
| # If either of the statuses report experimental, assume attribute is | |
| # experimental. | |
| status_a[_EXPERIMENTAL_KEY] = status_a.get( | |
| _EXPERIMENTAL_KEY, False) or status_b.get(_EXPERIMENTAL_KEY, False) | |
| if not _has_status(b): | |
| return | |
| if not _has_status(a): | |
| a[_COMPAT_KEY] = b[_COMPAT_KEY] | |
| return | |
| status_a = a[_COMPAT_KEY][_STATUS_KEY] | |
| status_b = b[_COMPAT_KEY][_STATUS_KEY] | |
| _unify_experimental(status_a, status_b) | |
| # If there exists multiple definitions of the same interface metadata e.g. | |
| # elements, this attempts to unify the compatibilities for the interface as | |
| # well as for each attribute. | |
| def _unify_metadata(a, b): | |
| # Unify the compatibility statement and status of the API or element. | |
| _unify_compat(a, b) | |
| _unify_status(a, b) | |
| # Unify the compatibility statement and status of each attribute. | |
| for attr in list(a.keys()): | |
| if attr == _COMPAT_KEY: | |
| continue | |
| if attr in b: | |
| _unify_compat(a[attr], b[attr]) | |
| _unify_status(a[attr], b[attr]) | |
| for attr in b.keys(): | |
| if not attr in a: | |
| a[attr] = b[attr] | |
| for (dir_path, dirs, files) in os.walk(browser_compat_folder): | |
| def should_process_dir(dir_path): | |
| if os.path.abspath(dir_path) == browser_compat_folder: | |
| return True | |
| for dir in INCLUDE_DIRS: | |
| if dir_path.startswith(dir): | |
| return True | |
| return False | |
| if should_process_dir(dir_path): | |
| for name in files: | |
| file_name = os.path.join(dir_path, name) | |
| (interface_path, ext) = os.path.splitext(file_name) | |
| if ext == '.json': | |
| with open(file_name) as src: | |
| json_dict = json.load(src) | |
| interface, metadata = process_json_dict(json_dict) | |
| if not interface is None: | |
| # Note: interface and member names do not | |
| # necessarily have the same capitalization as | |
| # WebCore, so we keep them all lowercase for easier | |
| # matching later. | |
| interface = interface.lower() | |
| metadata = { | |
| member.lower(): info | |
| for member, info in metadata.items() | |
| } | |
| if interface in browser_compat_data: | |
| _unify_metadata(browser_compat_data[interface], | |
| metadata) | |
| else: | |
| browser_compat_data[interface] = metadata | |
| else: | |
| dirs[:] = [] # Do not go underneath | |
| return browser_compat_data | |
| # Given two version values for a given browser, chooses the more strict version. | |
| def _unify_versions(version_a, version_b): | |
| # Given two valid version strings, compares parts of the version string | |
| # iteratively. | |
| def _greater_version(version_a, version_b): | |
| version_a_split = list(map(int, version_a.split('.'))) | |
| version_b_split = list(map(int, version_b.split('.'))) | |
| for i in range(min(len(version_a_split), len(version_b_split))): | |
| if version_a_split[i] > version_b_split[i]: | |
| return version_a | |
| elif version_a_split[i] < version_b_split[i]: | |
| return version_b | |
| return version_a if len(version_a_split) > len( | |
| version_b_split) else version_b | |
| # Validate that we can handle the given version. | |
| def _validate_version(version): | |
| if not version: | |
| return False | |
| if version is True: | |
| return True | |
| if isinstance(version, str): | |
| pattern = re.compile('^([0-9]+\.)*[0-9]+$') | |
| if not pattern.match(version): | |
| # It's possible for version strings to look like '<35'. We don't | |
| # attempt to parse the conditional logic, and just default to | |
| # potentially incompatible. | |
| return None | |
| return version | |
| else: | |
| raise ValueError( | |
| 'Type of version_a was not handled correctly! type(version) = ' | |
| + str(type(version))) | |
| version_a = _validate_version(version_a) | |
| version_b = _validate_version(version_b) | |
| # If one version reports not supported, default to not supported. | |
| if not version_a or not version_b: | |
| return False | |
| # If one version reports always supported, the other version can only be | |
| # more strict. | |
| if version_a is True: | |
| return version_b | |
| if version_b is True: | |
| return version_a | |
| return _greater_version(version_a, version_b) | |
| # At this time, we only handle simple support statements due to the complexity | |
| # and variability around support statements with multiple elements. | |
| def _is_simple_support_statement(support_statement): | |
| if isinstance(support_statement, list): # array_support_statement | |
| # TODO(srujzs): Parse this list to determine compatibility. Will | |
| # likely require parsing for 'version_removed' keys. Notes about | |
| # which browser version enabled this attribute for which | |
| # platform also complicates things. For now, we assume it's not | |
| # compatible. | |
| return False | |
| if len(support_statement.keys()) > 1: | |
| # If it's anything more complicated than 'version_added', like | |
| # 'notes' that specify platform versions, we assume it's not | |
| # compatible. | |
| return False | |
| return True | |
| class MDNReader(object): | |
| # Statically initialize and treat as constant. | |
| _BROWSER_COMPAT_DATA = _get_browser_compat_data() | |
| def __init__(self): | |
| self._compat_overrides = {} | |
| def _get_attr_compatibility(self, compat_data): | |
| # Parse schema syntax of MDN data: | |
| # https://github.com/mdn/browser-compat-data/blob/master/schemas/compat-data.schema.json | |
| # For now, we will require support for browsers since the last IDL roll. | |
| # TODO(srujzs): Determine if this is too conservative. | |
| browser_version_map = { | |
| 'chrome': '63', | |
| 'firefox': '57', | |
| 'safari': '11', | |
| # We still support the latest version of IE. | |
| 'ie': '11', | |
| 'opera': '50', | |
| } | |
| for browser in browser_version_map.keys(): | |
| support_data = compat_data[_SUPPORT_KEY] | |
| if browser not in support_data: | |
| return False | |
| support_statement = support_data[browser] | |
| if not _is_simple_support_statement(support_statement): | |
| return False | |
| version = support_statement[_VERSION_ADDED_KEY] | |
| # Compare version strings, target should be the more strict version. | |
| target = browser_version_map[browser] | |
| if _unify_versions(version, target) != target: | |
| return False | |
| # If the attribute is experimental, we assume it's not compatible. | |
| status_data = compat_data[_STATUS_KEY] | |
| if _EXPERIMENTAL_KEY in status_data and status_data[_EXPERIMENTAL_KEY]: | |
| return False | |
| return True | |
| def is_compatible(self, attribute): | |
| # Since capitalization isn't consistent across MDN and WebCore, we | |
| # compare lowercase equivalents for interface and attribute names. | |
| interface = attribute.doc_js_interface_name.lower() | |
| if interface in self._BROWSER_COMPAT_DATA and attribute.id and len( | |
| attribute.id) > 0: | |
| interface_dict = self._BROWSER_COMPAT_DATA[interface] | |
| id_name = attribute.id.lower() | |
| secure_context_key = 'isSecureContext' | |
| if interface in self._compat_overrides and id_name in self._compat_overrides[ | |
| interface]: | |
| return self._compat_overrides[interface][id_name] | |
| elif secure_context_key in interface_dict: | |
| # If the interface requires a secure context, all attributes are | |
| # implicitly incompatible. | |
| return False | |
| elif id_name in interface_dict: | |
| id_data = interface_dict[id_name] | |
| return self._get_attr_compatibility(id_data[_COMPAT_KEY]) | |
| else: | |
| # Might be an attribute that is defined in a parent interface. | |
| # We defer until attribute emitting to determine if this is the | |
| # case. Otherwise, return None. | |
| pass | |
| return None | |
| def set_compatible(self, attribute, compatible): | |
| # Override value in the MDN browser compatibility data. | |
| if not compatible in [True, False, None]: | |
| raise ValueError('Cannot set a non-boolean object for compatible') | |
| interface = attribute.doc_js_interface_name.lower() | |
| if not interface in self._compat_overrides: | |
| self._compat_overrides[interface] = {} | |
| if attribute.id and len(attribute.id) > 0: | |
| id_name = attribute.id.lower() | |
| self._compat_overrides[interface][id_name] = compatible |