com.google.common.css.compiler.passes.MarkRemovableRulesetNodes Maven / Gradle / Ivy
Show all versions of closure-stylesheets Show documentation
/*
* 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.Suppliers;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Table;
import com.google.common.css.compiler.ast.*;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.StreamSupport;
/**
* Compiler pass that marks the ruleset nodes that should be removed from the
* tree.
*
* 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 [email protected] (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 ImmutableSet PROPERTIES_NOT_TO_BE_CHECKED =
ImmutableSet.of("display", "cursor");
/**
* The set of rules known to be referenced.
*/
private Set 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 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 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 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 propertyValues =
decl.getPropertyValue().childIterable();
return StreamSupport.stream(propertyValues.spliterator(), false).anyMatch(Predicates.instanceOf(CssPriorityNode.class)::apply);
}
/**
* 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 rules,
final CssRulesetNode ruleset) {
Supplier rulesetIsImportant = Suppliers.memoize(() -> isImportantRule(ruleset))::get;
for (String shorthand : propertyNode.getProperty().getShorthands()) {
CssRulesetNode shorthandRuleset = rules.get(selector, shorthand);
if ((shorthandRuleset != null)
&& (!rulesetIsImportant.get() || isImportantRule(shorthandRuleset))) {
return true;
}
}
return false;
}
}