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

Java tutorial

Introduction

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

Source

/*
 * Copyright 2011 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.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.css.SourceCodeLocation;
import com.google.common.css.compiler.ast.CssCompilerPass;
import com.google.common.css.compiler.ast.CssConstantReferenceNode;
import com.google.common.css.compiler.ast.CssDeclarationBlockNode;
import com.google.common.css.compiler.ast.CssDeclarationNode;
import com.google.common.css.compiler.ast.CssMixinDefinitionNode;
import com.google.common.css.compiler.ast.CssMixinNode;
import com.google.common.css.compiler.ast.CssNode;
import com.google.common.css.compiler.ast.CssValueNode;
import com.google.common.css.compiler.ast.DefaultTreeVisitor;
import com.google.common.css.compiler.ast.ErrorManager;
import com.google.common.css.compiler.ast.GssError;
import com.google.common.css.compiler.ast.MutatingVisitController;

import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Stack;

/**
 * Compiler pass that replaces mixins with the corresponding mixin definitions
 * and replaces the variables in the definitions with the values given by the
 * mixin.
 *
 * <p>In addition, this pass ensures that each mixin has a matching definition
 * and that the argument count of the use and the definition is equal.
 *
 * <p>{@link CollectMixinDefinitions} has to run before.
 * {@link ReplaceConstantReferences} has to run afterwards.
 *
 * @author fbenz@google.com (Florian Benz)
 */
public class ReplaceMixins extends DefaultTreeVisitor implements CssCompilerPass {
    @VisibleForTesting
    static final String NO_MATCHING_MIXIN_DEFINITION_ERROR_MESSAGE = "The name of the mixin matches no mixin definition name";
    @VisibleForTesting
    static final String ARGUMENT_MISMATCH_ERROR_MESSAGE = "The number of arguments of the mixin and the corresponding definition "
            + "are different";
    @VisibleForTesting
    static final String CYCLE_ERROR_MESSAGE = "A nested mixin call produces a cycle";
    @VisibleForTesting
    static final String NO_MATCHING_MIXIN_FOR_REF_ERROR_MESSAGE = "Internal compiler error: The current definition reference belongs to "
            + "a mixin but does not match the mixin on top of the stack";

    private final MutatingVisitController visitController;
    private final ErrorManager errorManager;
    private final Map<String, CssMixinDefinitionNode> definitions;

    /**
     * the current call stack of the mixins
     */
    private Stack<StackFrame> currentMixinStack;

    public ReplaceMixins(MutatingVisitController visitController, ErrorManager errorManager,
            Map<String, CssMixinDefinitionNode> definitions) {
        this.visitController = visitController;
        this.errorManager = errorManager;
        this.definitions = definitions;
        this.currentMixinStack = new Stack<StackFrame>();
    }

    @Override
    public void leaveMixin(CssMixinNode node) {
        if (!currentMixinStack.empty()) {
            currentMixinStack.peek().decreaseDeclarationCount();
        }
        // This pushes the mixin on the stack if the corresponding definition
        // contains at least one declaration.
        replaceMixin(node);
        // Goes up the stack if this is the last declaration inserted by a mixin.
        // This is done for the case where no mixin is added to the stack.
        while (!currentMixinStack.empty() && currentMixinStack.peek().isDeclarationCountZero()) {
            currentMixinStack.pop();
        }
    }

    @Override
    public void leaveDeclaration(CssDeclarationNode node) {
        // Updates the stacks if the last declaration that was added by the last
        // mixin call is reached.
        if (currentMixinStack.empty()) {
            return;
        }
        // get the number of declarations left that were added by the last
        // mixin call
        currentMixinStack.peek().decreaseDeclarationCount();
        // go up the stack if this is the last declaration inserted by a mixin
        while (!currentMixinStack.empty() && currentMixinStack.peek().isDeclarationCountZero()) {
            currentMixinStack.pop();
        }
    }

    @Override
    public void leaveDeclarationBlock(CssDeclarationBlockNode node) {
        currentMixinStack.clear();
    }

    /**
     * Replaces a variable inside the copy of the mixin definition with the
     * value given by the mixin.
     */
    @Override
    public boolean enterValueNode(CssValueNode node) {
        return replaceReference(node, false /* isArgument */);
    }

    @Override
    public boolean enterArgumentNode(CssValueNode node) {
        return replaceReference(node, true /* isArgument */);
    }

    private boolean replaceReference(CssValueNode node, boolean isArgument) {
        if (!(node instanceof CssConstantReferenceNode)) {
            return true;
        }
        List<CssValueNode> values = getValuesForReference((CssConstantReferenceNode) node, isArgument);
        if (values == null) {
            return true;
        }
        visitController.replaceCurrentBlockChildWith(values, false);
        return true;
    }

    /**
     * Replaces a mixin with the declarations of the corresponding definition.
     */
    private void replaceMixin(CssMixinNode mixin) {
        if (containsCycle(mixin)) {
            return;
        }
        CssMixinDefinitionNode currentMixinDefinition = definitions.get(mixin.getDefinitionName());
        if (currentMixinDefinition == null) {
            errorManager.report(
                    new GssError(NO_MATCHING_MIXIN_DEFINITION_ERROR_MESSAGE, mixin.getSourceCodeLocation()));
            return;
        }
        // Adds deep copies of the declarations in the definition to the current
        // declaration block. The variables are visited and replaced afterwards.
        List<CssNode> mixinDecls = currentMixinDefinition.getBlock().deepCopy().getChildren();
        visitController.replaceCurrentBlockChildWith(mixinDecls, /* visitTheReplacementNodes */ true);
        // Create a mapping so that references can easily be replaced by their
        // value.
        Map<String, List<CssValueNode>> refMap = createReferenceMapping(mixin, currentMixinDefinition);
        if (refMap == null) {
            visitController.stopVisit();
            return;
        }
        if (mixinDecls.size() == 0) {
            return;
        }
        // Add the mixin and the number of declarations to the stack
        currentMixinStack.push(new StackFrame(mixin, mixinDecls.size(), refMap));
    }

    /**
     * Returns the value of the given reference. The value is defined by the
     * mixin referring to the mixin definition the reference is in.
     */
    private List<CssValueNode> getValuesForReference(CssConstantReferenceNode ref, boolean isArgument) {
        if (!(ref.getScope() instanceof CssMixinDefinitionNode)) {
            return null;
        }

        String defName = ref.getValue();
        CssMixinDefinitionNode currentMixinDefinition = (CssMixinDefinitionNode) ref.getScope();
        CssMixinNode currentMixin = currentMixinStack.peek().getMixin();
        if (!currentMixin.getDefinitionName().equals(currentMixinDefinition.getDefinitionName())) {
            errorManager.report(new GssError(NO_MATCHING_MIXIN_FOR_REF_ERROR_MESSAGE, ref.getSourceCodeLocation()));
            return null;
        }

        List<CssValueNode> values = currentMixinStack.peek().getValuesForReference(ref.getValue());
        Preconditions.checkNotNull(values);

        // Create deep copies because the values can be inserted in several places.
        ImmutableList.Builder<CssValueNode> builder = ImmutableList.builder();
        for (CssValueNode val : values) {
            if (isArgument || !" ".equals(val.getValue())) {
                // Values only containing a whitespace are only added if they inside
                // the argument of a function as they are inserted by the parser to
                // separate values that are together seen as one argument.
                builder.add(val.deepCopy());
            }
        }
        return builder.build();
    }

    /**
     * Ensures that no cyclic mixin calls occur.
     */
    private boolean containsCycle(CssMixinNode mixin) {
        for (StackFrame frame : currentMixinStack) {
            if (mixin.getDefinitionName().equals(frame.getMixin().getDefinitionName())) {
                errorManager.report(new GssError(CYCLE_ERROR_MESSAGE, frame.getMixin().getSourceCodeLocation()));
                return true;
            }
        }
        return false;
    }

    /**
     * Creates a mapping between argument names of the mixin definition and the
     * values that are provided by the mixin.
     */
    private Map<String, List<CssValueNode>> createReferenceMapping(CssMixinNode mixin, CssMixinDefinitionNode def) {
        Map<String, List<CssValueNode>> refMap = Maps.newHashMap();
        List<CssValueNode> currentValues = Lists.newArrayList();
        Iterator<CssValueNode> definitionArgumentIterator = def.getArguments().getChildIterator();
        // Collects all values up to a comma and then adds these values to the map.
        for (CssValueNode arg : mixin.getArguments().getChildren()) {
            if (",".equals(arg.getValue())) {
                if (!addValuesToMap(refMap, definitionArgumentIterator, currentValues,
                        arg.getSourceCodeLocation())) {
                    return null;
                }
                currentValues.clear();
            } else {
                currentValues.add(arg);
            }
        }
        if (!currentValues.isEmpty()) {
            // Add values for last argument.
            if (!addValuesToMap(refMap, definitionArgumentIterator, currentValues, mixin.getSourceCodeLocation())) {
                return null;
            }
        }
        if (definitionArgumentIterator.hasNext()) {
            // The definition takes more arguments than the mixin provides.
            errorManager.report(new GssError(ARGUMENT_MISMATCH_ERROR_MESSAGE, mixin.getSourceCodeLocation()));
            return null;
        }
        return refMap;
    }

    /**
     * Adds the given values to the map that maps argument names to their values.
     */
    private boolean addValuesToMap(Map<String, List<CssValueNode>> refMap,
            Iterator<CssValueNode> definitionArgumentIterator, List<CssValueNode> values,
            SourceCodeLocation location) {
        if (values.isEmpty() || !definitionArgumentIterator.hasNext()) {
            // There is no value between two commas or the mixin provides more
            // arguments than the definition takes.
            errorManager.report(new GssError(ARGUMENT_MISMATCH_ERROR_MESSAGE, location));
            return false;
        }
        CssValueNode argument = definitionArgumentIterator.next();
        // Commas are skipped.
        if (",".equals(argument.getValue())) {
            if (values.isEmpty() || !definitionArgumentIterator.hasNext()) {
                errorManager.report(new GssError(ARGUMENT_MISMATCH_ERROR_MESSAGE, location));
                return false;
            }
            argument = definitionArgumentIterator.next();
        }
        refMap.put(argument.getValue(), ImmutableList.copyOf(values));
        return true;
    }

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

    /**
     * Helper class that is used to keep track of the called mixins inside of
     * other mixins.
     */
    private static class StackFrame {
        private CssMixinNode mixin;
        private int declarationCount;
        private final Map<String, List<CssValueNode>> valueMap;

        StackFrame(CssMixinNode mixin, int declarationCount, Map<String, List<CssValueNode>> valueMap) {
            Preconditions.checkNotNull(mixin);
            Preconditions.checkArgument(declarationCount > 0);
            Preconditions.checkNotNull(valueMap);
            this.mixin = mixin;
            this.declarationCount = declarationCount;
            this.valueMap = valueMap;
        }

        CssMixinNode getMixin() {
            return mixin;
        }

        void decreaseDeclarationCount() {
            if (declarationCount > 0) {
                declarationCount--;
            }
        }

        boolean isDeclarationCountZero() {
            return this.declarationCount == 0;
        }

        List<CssValueNode> getValuesForReference(String refName) {
            return valueMap.get(refName);
        }
    }
}