blob: 50fc227ae29d625d71ec66a05066fef3919d64d2 [file] [log] [blame]
#!/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_data = {}
# TODO(srujzs): add more if needed
# Transform to absolute paths
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(
# 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):
if not _has_compat_data(a):
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):
if not _has_status(a):
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:
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:
browser_compat_data[interface] = metadata
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
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:
# 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 and len( > 0:
interface_dict = self._BROWSER_COMPAT_DATA[interface]
id_name =
secure_context_key = 'isSecureContext'
if interface in self._compat_overrides and id_name in self._compat_overrides[
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])
# 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.
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 and len( > 0:
id_name =
self._compat_overrides[interface][id_name] = compatible