com.google.common.css.compiler.passes.MarkRemovableRulesetNodes.java Source code

Java tutorial

Introduction

Here is the source code for com.google.common.css.compiler.passes.MarkRemovableRulesetNodes.java

Source

/*
 * Copyright 2009 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.common.css.compiler.passes;

import com.google.common.base.Preconditions;
import com.google.common.base.Predicates;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Table;
import com.google.common.css.compiler.ast.CssBlockNode;
import com.google.common.css.compiler.ast.CssClassSelectorNode;
import com.google.common.css.compiler.ast.CssCompilerPass;
import com.google.common.css.compiler.ast.CssDeclarationNode;
import com.google.common.css.compiler.ast.CssPriorityNode;
import com.google.common.css.compiler.ast.CssPropertyNode;
import com.google.common.css.compiler.ast.CssRefinerNode;
import com.google.common.css.compiler.ast.CssRulesetNode;
import com.google.common.css.compiler.ast.CssSelectorNode;
import com.google.common.css.compiler.ast.CssTree;
import com.google.common.css.compiler.ast.CssValueNode;
import com.google.common.css.compiler.ast.SkippingTreeVisitor;
import com.google.common.css.compiler.ast.VisitController;

import java.util.Set;

/**
 * Compiler pass that marks the ruleset nodes that should be removed from the
 * tree.
 *
 * <p>This pass assumes that each ruleset node contains exactly one declaration
 * and one selector. This pass should be preceded by SplitRulesetNodes
 * pass. This pass skips over nodes in the body that are not ruleset nodes and
 * ignores all their children including ruleset nodes. So, in particular, all
 * ruleset nodes contained within {@code @media} rules will be ignored.
 *
 * @author oana@google.com (Oana Florescu)
 */
// TODO(oana): Split this pass into two, but only after running some performance
//     tests to figure out the time it takes for a pass that does not change the
//     tree to run.
public class MarkRemovableRulesetNodes extends SkippingTreeVisitor implements CssCompilerPass {

    private final CssTree tree;
    private final VisitController visitController;

    /**
     * Property names not be to checked while traversing the tree to find
     * overridden declarations.
     */
    static final Set<String> PROPERTIES_NOT_TO_BE_CHECKED = ImmutableSet.of("display", "cursor");

    /** The set of rules known to be referenced. */
    private Set<String> referencedRules = null;

    /** The prefix of the class names. TODO(oana): This should be a namespace. */
    private String prefixOfReferencedRules = "";

    /**
     * Creates a new pass over the specified tree.
     */
    public MarkRemovableRulesetNodes(CssTree tree) {
        this(tree, false);
    }

    /**
     * Creates a new pass over the specified tree.
     *
     * @param skipping whether to skip over rulesets containing properties that
     *     might make them unsafe to modify (see {@link SkippingTreeVisitor})
     */
    public MarkRemovableRulesetNodes(CssTree tree, boolean skipping) {
        super(skipping);
        this.tree = tree;
        this.visitController = tree.getVisitController();
    }

    /**
     * Sets the reference rules.
     *
     * @param referencedRules the set of referenced rules, which, since it is
     *     aliased, is expected not to change while this pass is in progress
     * @param prefixOfReferencedRules the prefix to match when looking for
     *     unreferenced rules
     */
    public void setReferencedRules(Set<String> referencedRules, String prefixOfReferencedRules) {
        this.referencedRules = referencedRules;
        this.prefixOfReferencedRules = prefixOfReferencedRules;
    }

    @Override
    public boolean enterBlock(CssBlockNode block) {
        // All the children of the block, which are ruleset nodes, are looked at
        // in reverse order, from the last one to the first. We mark as removable
        // those nodes that we are found as overridden already.
        // Collect the already-seen pairs of selectors and property names in this
        // table, save the CssRulesetNode also.
        Table<String, String, CssRulesetNode> rules = HashBasedTable.create();

        for (int i = block.numChildren() - 1; i >= 0; i--) {
            if (block.getChildAt(i) instanceof CssRulesetNode) {
                CssRulesetNode ruleset = (CssRulesetNode) block.getChildAt(i);

                // Filter out unreferenced rules. We do this regardless of whether the
                // rule in question has a "display" property, is in the chunk currently
                // being processed or any of the other criteria that canModifyRuleset
                // checks for because there's no reason not to remove unreferenced
                // rules.
                if (referencedRules != null && !referencedRules.isEmpty()
                        && isSelectorUnreferenced(ruleset.getSelectors().getChildAt(0))) {
                    tree.getRulesetNodesToRemove().addRulesetNode(ruleset);
                    continue;
                }

                // If skipping is on and the rule contains a property from a
                // pre-defined set then we skip processing this ruleset
                // (in that case "canModifyRuleset()" will return false).
                if (canModifyRuleset(ruleset)) {
                    // Make sure the node has only one selector.
                    Preconditions.checkArgument(isSkipping() || (ruleset.getSelectors().numChildren() == 1));

                    processRuleset(rules, ruleset);
                }
            }
        }

        return false;
    }

    @Override
    public void runPass() {
        visitController.startVisit(this);
    }

    /**
     * Processes the given ruleset, deciding whether it should be kept
     * or removed by looking at the given previous rules.
     */
    private void processRuleset(Table<String, String, CssRulesetNode> rules, CssRulesetNode ruleset) {
        if ((referencedRules != null) && !referencedRules.isEmpty()) {
            // If this rule is not referenced to in the code we remove it.
            if (isSelectorUnreferenced(ruleset.getSelectors().getChildAt(0))) {
                // TODO(henrywong, dgajda): Storing the set of things to clean up
                // in the tree is pretty brittle - better would be to have the pass
                // return the rules it finds and then manually pass them in to the
                // EliminateUselessRulesets pass.
                tree.getRulesetNodesToRemove().addRulesetNode(ruleset);
                return;
            }
        }

        // Make sure the node has only one declaration.
        Preconditions.checkArgument(ruleset.getDeclarations().numChildren() == 1);
        Preconditions.checkArgument(ruleset.getDeclarations().getChildAt(0) instanceof CssDeclarationNode);
        CssDeclarationNode declaration = (CssDeclarationNode) ruleset.getDeclarations().getChildAt(0);
        CssPropertyNode propertyNode = declaration.getPropertyName();
        String propertyName = propertyNode.getPropertyName();
        if (PROPERTIES_NOT_TO_BE_CHECKED.contains(propertyName)) {
            return;
        }
        // If the declaration is star-hacked then we make the star be part of
        // the property name to ensure that we do not consider hacked
        // declarations as overridden by the non-hacked ones.
        if (declaration.hasStarHack()) {
            propertyName = "*" + propertyName;
        }

        String selector = PassUtil.printSelector(ruleset.getSelectors().getChildAt(0));

        CssRulesetNode previousRuleset = rules.get(selector, propertyName);
        if (previousRuleset != null) {
            // If the new rule is important and the saved was not, then remove the saved one.
            if (isImportantRule(ruleset) && !isImportantRule(previousRuleset)) {
                tree.getRulesetNodesToRemove().addRulesetNode(previousRuleset);
                // Replace the non-important ruleset in the map, keep the important one.
                rules.put(selector, propertyName, ruleset);
            } else {
                tree.getRulesetNodesToRemove().addRulesetNode(ruleset);
            }
        } else if (hasOverridingShorthand(propertyNode, selector, rules, ruleset)) {
            tree.getRulesetNodesToRemove().addRulesetNode(ruleset);
        } else if (PassUtil.hasAlternateAnnotation(declaration)) {
            // The declaration has @alternate, so do not let it mask other
            // declarations that precede it.  However, @alternate rules may be masked
            // by succeeding non-@alternate rules.
        } else {
            rules.put(selector, propertyName, ruleset);
        }
    }

    private boolean isSelectorUnreferenced(CssSelectorNode selector) {
        return okToRemoveSelector(selector)
                && (atLeastOneUnreferencedRefiner(selector) || unreferencedSelectorCombinator(selector));
    }

    /**
     * Returns whether it's OK to remove the specified selector, i.e. it is not a
     * tag, like: body, html, etc, as tags should not be removed unless they are
     * in combination with other CSS classes.
     */
    private boolean okToRemoveSelector(CssSelectorNode selector) {
        return (selector.getSelectorName() == null) || !selector.getRefiners().isEmpty()
                || (selector.getCombinator() != null);
    }

    /**
     * Returns whether at least one of the refiners of the specified selector is
     * not in the list of referenced rules.
     */
    private boolean atLeastOneUnreferencedRefiner(CssSelectorNode selector) {
        for (CssRefinerNode ref : selector.getRefiners().childIterable()) {
            if (ref instanceof CssClassSelectorNode) {
                String refiner = ref.getRefinerName();
                if (refiner.startsWith(prefixOfReferencedRules) && isRefinerUnreferenced(refiner)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Returns whether the combinator of the specified selector is unreferenced.
     */
    private boolean unreferencedSelectorCombinator(CssSelectorNode selector) {
        return (selector.getCombinator() != null && isSelectorUnreferenced(selector.getCombinator().getSelector()));
    }

    private boolean isRefinerUnreferenced(String refiner) {
        String[] splits = refiner.split("-");
        for (String s : splits) {
            if (!referencedRules.contains(s)) {
                return true;
            }
        }
        return false;
    }

    private boolean isImportantRule(CssRulesetNode ruleset) {
        CssDeclarationNode decl = (CssDeclarationNode) ruleset.getDeclarations().getChildAt(0);
        Iterable<CssValueNode> propertyValues = decl.getPropertyValue().childIterable();
        return Iterables.any(propertyValues, Predicates.instanceOf(CssPriorityNode.class));
    }

    /**
     * Computes whether the given rule is overridden by another rule with some
     * related shorthand property of equal or higher importance.
     *
     * @param propertyNode the property node of the rule to check
     * @param selector the printed representation of the selector of the rule
     * @param rules rulesets occurring after the ruleset to check (represented as
     *     a map from selector/property pairs to rulesets for easy searching)
     * @param ruleset the ruleset to check (assumed to contain one rule)
     * @return whether the given ruleset has an overriding ruleset which uses a
     *     related shorthand property
     */
    private boolean hasOverridingShorthand(CssPropertyNode propertyNode, String selector,
            Table<String, String, CssRulesetNode> rules, final CssRulesetNode ruleset) {

        Supplier<Boolean> rulesetIsImportant = Suppliers.memoize(new Supplier<Boolean>() {
            @Override
            public Boolean get() {
                return isImportantRule(ruleset);
            }
        });

        for (String shorthand : propertyNode.getProperty().getShorthands()) {
            CssRulesetNode shorthandRuleset = rules.get(selector, shorthand);
            if ((shorthandRuleset != null) && (!rulesetIsImportant.get() || isImportantRule(shorthandRuleset))) {
                return true;
            }
        }

        return false;
    }
}