chromium/third_party/blink/renderer/build/scripts/core/css/css_properties.py

#!/usr/bin/env python
# Copyright 2014 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

from blinkbuild.name_style_converter import NameStyleConverter
from core.css.field_alias_expander import FieldAliasExpander
import json5_generator
from make_origin_trials import OriginTrialsWriter
from name_utilities import enum_key_for_css_property, id_for_css_property
from name_utilities import enum_key_for_css_property_alias, id_for_css_property_alias
import dataclasses
import typing
import copy

# These values are converted using CSSPrimitiveValue in the setter function,
# if applicable.
PRIMITIVE_TYPES = [
    'short', 'unsigned short', 'int', 'unsigned int', 'unsigned', 'float',
    'LineClampValue'
]


def validate_property(prop, props_by_name):
    """Perform sanity-checks on a property entry.

    prop: The property (or extra field) to perform checks on.
    props_by_name: A dict which maps properties by name. This is useful for
                   cases where 'prop' refer to other properties by name.

    Many combinations of values that are possible to specify in
    css_properties.json5 do not make sense, and/or would produce invalid
    generated code. For example, it does not make sense for longhands to
    implement the ParseShorthand function.
    """
    name = prop.name
    has_method = lambda x: x in prop.property_methods
    assert prop.is_property or prop.is_descriptor, \
        'Entry must be a property, descriptor, or both [%s]' % name
    assert not prop.interpolable or prop.is_longhand, \
        'Only longhands can be interpolable [%s]' % name
    assert not has_method('ParseSingleValue') or prop.is_longhand, \
        'Only longhands can implement ParseSingleValue [%s]' % name
    assert not has_method('ParseShorthand') or prop.is_shorthand, \
        'Only shorthands can implement ParseShorthand [%s]' % name
    assert not prop.field_template or prop.is_longhand, \
        'Only longhands can have a field_template [%s]' % name
    assert not prop.valid_for_first_letter or prop.is_longhand, \
        'Only longhands can be valid_for_first_letter [%s]' % name
    assert not prop.valid_for_first_line or prop.is_longhand, \
        'Only longhands can be valid_for_first_line [%s]' % name
    assert not prop.valid_for_cue or prop.is_longhand, \
        'Only longhands can be valid_for_cue [%s]' % name
    assert not prop.valid_for_marker or prop.is_longhand, \
        'Only longhands can be valid_for_marker [%s]' % name
    assert not prop.valid_for_highlight_legacy or prop.is_longhand, \
        'Only longhands can be valid_for_highlight_legacy [%s]' % name
    assert not prop.valid_for_highlight or prop.is_longhand, \
        'Only longhands can be valid_for_highlight [%s]' % name
    assert not prop.is_internal or prop.computable is None, \
        'Internal properties are always non-computable [%s]' % name
    if prop.supports_incremental_style:
        assert not prop.is_animation_property, \
            'Animation properties can not be applied incrementally [%s]' % name
        assert prop.idempotent, \
            'Incrementally applied properties must be idempotent [%s]' % name
        if prop.is_shorthand:
            for subprop_name in prop.longhands:
                subprop = props_by_name[subprop_name]
                assert subprop.supports_incremental_style, \
                    '%s must be incrementally applicable when its shorthand %s is' % (subprop_name, name)
    if prop.alias_for:
        assert not prop.is_internal, \
            'Internal aliases not supported [%s]' % name
    assert not prop.mutable or \
        (prop.field_template in ['derived_flag', 'monotonic_flag'] ),\
        'mutable requires field_template:derived_flag or monotonic_flag [%s]' % name
    assert not prop.in_origin_trial or prop.runtime_flag,\
        'Property participates in origin trial, but has no runtime flag'
    custom_functions = set(prop.computed_style_custom_functions)
    protected_functions = set(set(prop.computed_style_protected_functions))
    assert not custom_functions.intersection(protected_functions), \
        'Functions must be specified as either protected or custom, not both [%s]' % name
    if prop.field_template == 'derived_flag':
        assert prop.mutable, 'Derived flags must be mutable [%s]' % name
        assert not prop.field_group, 'Derived flags may not have field groups [%s]' % name
        assert prop.reset_on_new_style, 'Derived flags must have reset_on_new_style [%s]' % name
    if prop.is_logical:
        assert not prop.field_group, 'Logical properties can not have fields [%s]' % name

# Determines whether or not style builders (i.e. Apply functions)
# should be generated for the given property.
def needs_style_builders(property_):
    if not property_.is_property:
        return False
    # Shorthands do not get style builders, because shorthands are
    # expanded to longhands parse-time.
    if property_.longhands:
        return False
    # Surrogates do not get style builders, because they are replaced
    # with another target property cascade-time.
    if property_.surrogate_for:
        return False
    # Logical properties do not get style builders for the same reason
    # as surrogates.
    if property_.is_logical:
        return False
    return True


def verify_file_path(file_paths, index, expected):
    assert len(file_paths) > index and file_paths[index].endswith(expected), \
        'Unexpected file path at index %s (expected path that ends with %s, got .../%s)' \
            % (index, expected, file_paths[index])
    return file_paths[index]


class PropertyBase(object):
    """Base class for the generated 'Property' class.

    This class provides utility functions on top of 'Property', which is
    generated by 'generate_property_class'. PropertyBase is not intended to
    be instantiated directly, and expects all 'parameters' [1] to exist on
    'self'.

    [1] See 'parameters' dictionary in css_properties.json5.
    """

    def __init__(self):
        super(PropertyBase, self).__init__()

    @property
    def namespace(self):
        """The namespace for the generated CSSProperty subclass."""
        if self.is_shorthand:
            return 'css_shorthand'
        # Otherwise, 'self' is a longhand, or a descriptor (which also ends up
        # in the css_longhand namespace).
        return 'css_longhand'

    @property
    def classname(self):
        """The name of the generated CSSProperty subclass."""
        return self.name.to_upper_camel_case()

    @property
    def is_longhand(self):
        return self.is_property and not self.longhands

    @property
    def is_shorthand(self):
        return self.is_property and self.longhands

    @property
    def is_internal(self):
        return self.name.original.startswith('-internal-')

    @property
    def known_exposed(self):
        """True if the property is unconditionally web-exposed."""
        return not self.is_internal \
            and not self.runtime_flag \
            and not self.alternative

    @property
    def ultimate_property(self):
        """Returns the ultimate property, which is the final property
            in the alternative_of chain."""
        if self.alternative_of:
            return self.alternative_of.ultimate_property
        return self

    @property
    def css_sample_id(self):
        """Returns the CSSSampleId to use for this property."""
        # Alternative properties use the same use-counter as the
        # corresponding ultimate main property. In other words, alternative
        # properties are use-counted the same way as their main properties.
        return self.ultimate_property.enum_key


def generate_property_field(default):
    # Must use 'default_factory' rather than 'default' for list/dict.
    # https://docs.python.org/3/library/dataclasses.html#dataclasses.field
    if isinstance(default, list):
        return dataclasses.field(default_factory=list)
    if isinstance(default, dict):
        return dataclasses.field(default_factory=dict)
    return dataclasses.field(default=default)


def generate_property_class(parameters):
    """Generate a Property dataclass based on 'parameters' found in json5.

    See documentation about "parameters" in json5_generator.py.
    """
    # Fields and their default values, as specified in a json5-file:
    fields = [(name, spec.get('default', None))
              for name, spec in parameters.items()]

    # Additional defaults not specified in json5:
    additional = {
        'aliases': [],
        'custom_compare': False,
        'reset_on_new_style': False,
        'mutable': False,
        'name': None,
        'alternative': None,
        'visited_property': None,
    }

    fields += additional.items()

    return dataclasses.make_dataclass('Property', \
        [(name, typing.Any, generate_property_field(default)) for name, default in fields], \
        bases=(PropertyBase,))


class CSSProperties(object):
    def __init__(self, file_paths):
        assert len(
            file_paths) <= 4, 'Superfluous arguments: %s' % file_paths[4:]

        css_properties_path = verify_file_path(file_paths, 0,
                                               'css_properties.json5')
        computed_style_field_aliases_path = verify_file_path(
            file_paths, 1, 'computed_style_field_aliases.json5')
        runtime_enabled_features_path = verify_file_path(
            file_paths, 2, 'runtime_enabled_features.json5')
        # Extra fields are optional:
        computed_style_extra_fields_path = (
            len(file_paths) > 3) and verify_file_path(
                file_paths, 3, 'computed_style_extra_fields.json5')

        # computed_style_field_aliases.json5. Used to expand out parameters used
        # in the various generators for ComputedStyle.
        self._field_alias_expander = FieldAliasExpander(
            computed_style_field_aliases_path)

        # _alias_offset is updated in add_properties().
        self._alias_offset = -1
        # 0: CSSPropertyID::kInvalid
        # 1: CSSPropertyID::kVariable
        self._first_enum_value = 2
        self._last_used_enum_value = self._first_enum_value
        self._last_high_priority_property = None

        self._properties_by_id = {}
        self._aliases = []
        self._longhands = []
        self._shorthands = []
        self._properties_including_aliases = []

        # Add default data in css_properties.json5. This must be consistent
        # across instantiations of this class.
        css_properties_file = json5_generator.Json5File.load_from_files(
            [css_properties_path])
        self._default_parameters = css_properties_file.parameters

        Property = generate_property_class(self._default_parameters)

        # TODO(crbug/1031309): Refactor OriginTrialsWriter to reuse logic here.
        origin_trials_writer = OriginTrialsWriter(
            [runtime_enabled_features_path], "")
        self._origin_trial_features = {
            str(f['name'])
            for f in origin_trials_writer.origin_trial_features
        }

        properties = [
            Property(**x) for x in css_properties_file.name_dictionaries
        ]

        # Process extra fields, if any.
        self._extra_fields = []
        if computed_style_extra_fields_path:
            fields = json5_generator.Json5File.load_from_files(
                [computed_style_extra_fields_path],
                default_parameters=self._default_parameters)
            self._extra_fields = [
                Property(**x) for x in fields.name_dictionaries
            ]

        self._properties_by_name = {p.name.original: p for p in properties}

        for property_ in properties + self._extra_fields:
            self.set_derived_attributes(property_)
            validate_property(property_, self._properties_by_name)

        self.add_properties(properties)

        self._last_unresolved_property_id = max(property_.enum_value
                                                for property_ in self._aliases)

    def add_properties(self, properties):
        self._aliases = [
            property_ for property_ in properties if property_.alias_for
        ]
        self._shorthands = [
            property_ for property_ in properties if property_.longhands
        ]
        self._longhands = [
            property_ for property_ in properties
            if (not property_.alias_for and not property_.longhands)
        ]

        # Sort the properties by priority, then alphabetically. Ensure that
        # the resulting order is deterministic.
        # Sort properties by priority, then alphabetically.
        for property_ in self._longhands + self._shorthands:
            name_without_leading_dash = property_.name.original
            if name_without_leading_dash.startswith('-'):
                name_without_leading_dash = name_without_leading_dash[1:]
            property_.sorting_key = (-property_.priority,
                                     name_without_leading_dash)

        sorting_keys = {}
        for property_ in self._longhands + self._shorthands:
            key = property_.sorting_key
            assert key not in sorting_keys, \
                ('Collision detected - two properties have the same name and '
                 'priority, a potentially non-deterministic ordering can '
                 'occur: {}, {} and {}'.format(
                     key, property_.name.original, sorting_keys[key]))
            sorting_keys[key] = property_.name.original
        self._longhands.sort(key=lambda p: p.sorting_key)
        self._shorthands.sort(key=lambda p: p.sorting_key)

        # The sorted index becomes the CSSPropertyID enum value.
        for property_ in self._longhands + self._shorthands:
            property_.enum_value = self._last_used_enum_value
            self._last_used_enum_value += 1
            # Add the new property into the map of properties.
            assert property_.property_id not in self._properties_by_id, \
                ('property with ID {} appears more than once in the '
                 'properties list'.format(property_.property_id))
            self._properties_by_id[property_.property_id] = property_
            if property_.priority > 0:
                self._last_high_priority_property = property_

        self._alias_offset = self._last_used_enum_value
        self.expand_aliases()
        self._properties_including_aliases = self._longhands + \
            self._shorthands + self._aliases
        self._properties_with_alternatives = list(
            filter(lambda p: p.alternative,
                   self._properties_including_aliases))

    def get_property(self, name):
        assert name in self._properties_by_name, \
            'No property with that name [%s]' % name
        return self._properties_by_name[name]

    def set_derived_visited_attributes(self, property_):
        if not property_.visited_property_for:
            return
        visited_property_for = property_.visited_property_for
        unvisited_property = self._properties_by_name[visited_property_for]
        property_.visited = True
        # The visited property needs a link to the unvisited counterpart.
        property_.unvisited_property = unvisited_property
        # The unvisited property needs a link to the visited counterpart.
        assert not unvisited_property.visited_property, \
            'A property may not have multiple visited properties'
        unvisited_property.visited_property = property_

    def set_derived_surrogate_attributes(self, property_):
        if not property_.surrogate_for:
            return
        assert property_.surrogate_for in self._properties_by_name, \
            'surrogate_for must name a property'
        # Upgrade 'surrogate_for' to property reference.
        property_.surrogate_for = self._properties_by_name[
            property_.surrogate_for]

    def set_derived_alternative_attributes(self, property_):
        if not property_.alternative_of:
            return
        main_property = self.get_property(property_.alternative_of)
        # Upgrade 'alternative_of' to a property reference.
        property_.alternative_of = main_property
        assert not main_property.alternative, \
            'A property may not have multiple alternatives'
        main_property.alternative = property_

    def expand_aliases(self):
        for i, alias in enumerate(self._aliases):
            aliased_property = self._properties_by_id[id_for_css_property(
                alias.alias_for)]
            aliased_property.aliases.append(alias.name.original)
            updated_alias = copy.deepcopy(aliased_property)
            updated_alias.name = alias.name
            updated_alias.alias_for = alias.alias_for
            updated_alias.alternative_of = alias.alternative_of
            updated_alias.alternative = alias.alternative
            updated_alias.aliased_property = aliased_property.name.to_upper_camel_case(
            )
            updated_alias.computable = alias.computable
            updated_alias.property_id = id_for_css_property_alias(alias.name)
            updated_alias.enum_key = enum_key_for_css_property_alias(
                alias.name)
            updated_alias.enum_value = self._alias_offset + i
            updated_alias.aliased_enum_value = aliased_property.enum_value
            updated_alias.superclass = 'CSSUnresolvedProperty'
            updated_alias.namespace_group = \
                'Shorthand' if aliased_property.longhands else 'Longhand'
            self._aliases[i] = updated_alias

        updated_aliases_by_name = {a.name: a for a in self._aliases}

        # The above loop produces an "updated" alias for each (incoming) alias.
        # Any alternative_of/alternative references that point to aliases
        # must be updated to point to the respective "updated" aliases.
        def update_alternatives(properties):
            for _property in properties:
                if _property.alternative_of and _property.alternative_of.alias_for:
                    _property.alternative_of = updated_aliases_by_name[
                        _property.alternative_of.name]
                if _property.alternative and _property.alternative.alias_for:
                    _property.alternative = updated_aliases_by_name[
                        _property.alternative.name]

        update_alternatives(self.longhands)
        update_alternatives(self.shorthands)
        update_alternatives(self.aliases)

    def set_derived_attributes(self, property_):
        """Set new attributes on 'property_', based on existing attribute values
        or defaults.

        The 'Property' class (of which 'property_' is an instance) contains an
        attribute for every "parameter" [1] specified in css_properties.json5.
        However, these attributes are not always sufficient or ergonomic to use
        in template files. For example, the 'field_template' parameter is
        shortcut for specifying a bunch of other parameters, and that expansion
        happens here.

        For trivial derived attributes, such as an 'is_internal' attribute which
        just checks if the name starts with "-internal-", prefer an @property
        on PropertyBase instead.

        [1] See "parameters" dictionary in css_properties.json5.
        """

        def set_if_none(property_, key, value):
            if not getattr(property_, key, None):
                setattr(property_, key, value)

        # Basic info.
        name = property_.name
        property_.property_id = id_for_css_property(name)
        property_.enum_key = enum_key_for_css_property(name)
        method_name = property_.name_for_methods
        if not method_name:
            method_name = name.to_upper_camel_case().replace('Webkit', '')
        set_if_none(property_, 'inherited', False)

        # Initial function, Getters and Setters for ComputedStyle.
        set_if_none(property_, 'initial', 'Initial' + method_name)
        simple_type_name = str(property_.type_name).split('::')[-1]
        set_if_none(property_, 'name_for_methods', method_name)
        set_if_none(property_, 'type_name', 'E' + method_name)
        set_if_none(
            property_, 'getter', method_name
            if simple_type_name != method_name else 'Get' + method_name)
        set_if_none(property_, 'setter', 'Set' + method_name)
        if property_.inherited:
            property_.is_inherited_setter = ('Set' + method_name +
                                             'IsInherited')

        property_.is_logical = False

        if property_.logical_property_group:
            group = property_.logical_property_group
            assert 'name' in group, 'name option is required'
            assert 'resolver' in group, 'resolver option is required'
            logicals = {
                'block', 'inline', 'block-start', 'block-end', 'inline-start',
                'inline-end', 'start-start', 'start-end', 'end-start',
                'end-end'
            }
            physicals = {
                'vertical', 'horizontal', 'top', 'bottom', 'left', 'right',
                'top-left', 'top-right', 'bottom-right', 'bottom-left'
            }
            if group['resolver'] in logicals:
                group['is_logical'] = True
            elif group['resolver'] in physicals:
                group['is_logical'] = False
            else:
                assert 0, 'invalid resolver option'
            group['name'] = NameStyleConverter(group['name'])
            group['resolver_name'] = NameStyleConverter(group['resolver'])
            property_.is_logical = group['is_logical']

        property_.style_builder_declare = needs_style_builders(property_)

        # Figure out whether we should generate style builder implementations.
        for x in ['initial', 'inherit', 'value']:
            suppressed = x in property_.style_builder_custom_functions
            declared = property_.style_builder_declare
            setattr(property_, 'style_builder_generate_%s' % x,
                    (declared and not suppressed))

        # Expand StyleBuilderConverter params where necessary.
        if property_.type_name in PRIMITIVE_TYPES:
            set_if_none(property_, 'converter', 'CSSPrimitiveValue')
        else:
            set_if_none(property_, 'converter', 'CSSIdentifierValue')

        if property_.anchor_mode:
            property_.anchor_mode = NameStyleConverter(property_.anchor_mode)

        if not property_.longhands:
            property_.superclass = 'Longhand'
            property_.namespace_group = 'Longhand'
        elif property_.longhands:
            property_.superclass = 'Shorthand'
            property_.namespace_group = 'Shorthand'

        # Expand out field templates.
        if property_.field_template:
            self._field_alias_expander.expand_field_alias(property_)

            type_name = property_.type_name
            if (property_.field_template == 'keyword'
                    or property_.field_template == 'multi_keyword'
                    or property_.field_template == 'bitset_keyword'):
                default_value = (type_name + '::' + NameStyleConverter(
                    property_.default_value).to_enum_value())
            elif (property_.field_template == 'external'
                  or property_.field_template == 'primitive'
                  or property_.field_template == 'pointer'):
                default_value = property_.default_value
            elif property_.field_template == 'derived_flag':
                property_.type_name = 'unsigned'
                default_value = '0'
            else:
                assert property_.field_template == 'monotonic_flag', \
                    "Please put a valid value for field_template; got " + \
                    str(property_.field_template)
                property_.type_name = 'bool'
                default_value = 'false'
            property_.default_value = default_value

            property_.unwrapped_type_name = property_.type_name
            if property_.wrapper_pointer_name:
                assert property_.field_template in ['pointer', 'external']
                if property_.field_template == 'external':
                    property_.type_name = '{}<{}>'.format(
                        property_.wrapper_pointer_name, type_name)

        # Default values for extra parameters in computed_style_extra_fields.json5.
        set_if_none(property_, 'reset_on_new_style', False)
        set_if_none(property_, 'custom_compare', False)
        set_if_none(property_, 'mutable', False)

        property_.in_origin_trial = property_.runtime_flag and \
            property_.runtime_flag in self._origin_trial_features

        self.set_derived_visited_attributes(property_)
        self.set_derived_surrogate_attributes(property_)
        self.set_derived_alternative_attributes(property_)

    @property
    def default_parameters(self):
        return self._default_parameters

    @property
    def aliases(self):
        return self._aliases

    @property
    def computable(self):
        # Use the name of the ultimate property as the sorting key,
        # otherwise '-alternative-foo' will sort according to
        # '-alternative-...', when it will really be exposed to
        # parsing/serialization as just 'foo'.
        sorting_name = lambda p: p.ultimate_property.name.original
        is_prefixed = lambda p: sorting_name(p).startswith('-')
        is_not_prefixed = lambda p: not is_prefixed(p)

        prefixed = filter(is_prefixed, self._properties_including_aliases)
        unprefixed = filter(is_not_prefixed,
                            self._properties_including_aliases)

        def is_computable(p):
            if p.is_internal:
                return False
            if p.computable is not None:
                return p.computable
            if p.alias_for:
                return False
            if not p.is_property:
                return False
            if not p.is_longhand:
                return False
            return True

        prefixed = filter(is_computable, prefixed)
        unprefixed = filter(is_computable, unprefixed)

        return sorted(unprefixed, key=sorting_name) + \
            sorted(prefixed, key=sorting_name)

    @property
    def shorthands(self):
        return self._shorthands

    @property
    def shorthands_including_aliases(self):
        return self._shorthands + [x for x in self._aliases if x.longhands]

    @property
    def longhands(self):
        return self._longhands

    @property
    def longhands_including_aliases(self):
        return self._longhands + [x for x in self._aliases if not x.longhands]

    @property
    def properties_by_name(self):
        return self._properties_by_name

    @property
    def properties_by_id(self):
        return self._properties_by_id

    @property
    def properties_including_aliases(self):
        return self._properties_including_aliases

    @property
    def properties_with_alternatives(self):
        """Properties that have another property referencing it with 'alternative_of'."""
        return self._properties_with_alternatives

    @property
    def gperf_properties(self):
        """The CSS properties that should be passed to gperf.

        This excludes properties with 'alternative_of' set, because such properties
        have the same web-facing name as the main property.
        """
        non_alternative = lambda p: not p.alternative_of
        return list(filter(non_alternative,
                           self._properties_including_aliases))

    @property
    def first_property_id(self):
        return self._first_enum_value

    @property
    def last_property_id(self):
        return self._first_enum_value + len(self._properties_by_id) - 1

    @property
    def last_unresolved_property_id(self):
        return self._last_unresolved_property_id

    @property
    def last_high_priority_property_id(self):
        return self._last_high_priority_property.enum_key

    @property
    def property_id_bit_length(self):
        return int.bit_length(self._last_unresolved_property_id)

    @property
    def alias_offset(self):
        return self._alias_offset

    @property
    def extra_fields(self):
        return self._extra_fields

    @property
    def max_shorthand_expansion(self):
        return max(map(lambda s: len(s.longhands), self._shorthands))