mirror of
https://github.com/dart-lang/sdk
synced 2024-10-03 08:33:32 +00:00
28b8cb34d3
browser-compat-data includes info on elements as both part of the Web API and the individual tagged elements. The scripts would default to whatever was processed last instead of trying to unify any conflicts that may come up with the different sources of truth. This CL addresses that issue by choosing the "stricter" of the two versions. Change-Id: Id3437c14276faf8af3fe41d7a6eb83defc9bd0b0 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/155844 Commit-Queue: Srujan Gaddam <srujzs@google.com> Reviewed-by: Sigmund Cherem <sigmund@google.com>
333 lines
14 KiB
Python
333 lines
14 KiB
Python
#!/usr/bin/python
|
|
# 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
|