| #!/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 = api_dict.keys()[0] |
| 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 = elements_dict.keys()[0] |
| # 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 = elements_dict.keys()[0] |
| # Convert to WebCore name |
| interface = str('SVG' + element_name + 'Element') |
| return (interface, elements_dict[element_name]) |
| return (None, None) |
| |
| def visitor(arg, dir_path, names): |
| |
| 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 names: |
| 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: |
| names[:] = [] # Do not go underneath |
| |
| # 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] |
| |
| os.path.walk(browser_compat_folder, visitor, browser_compat_folder) |
| |
| 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 = map(int, version_a.split('.')) |
| version_b_split = 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) or isinstance(version, unicode): |
| 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 |