All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.squarespace.less.exec.LessEvaluator Maven / Gradle / Ivy

The newest version!
/**
 * Copyright (c) 2014 SQUARESPACE, 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.squarespace.less.exec;

import static com.squarespace.less.core.Constants.FALSE;
import static com.squarespace.less.core.ExecuteErrorMaker.mixinRecurse;
import static com.squarespace.less.core.ExecuteErrorMaker.mixinUndefined;

import java.util.List;

import com.squarespace.less.LessContext;
import com.squarespace.less.LessException;
import com.squarespace.less.LessOptions;
import com.squarespace.less.core.FlexList;
import com.squarespace.less.model.Block;
import com.squarespace.less.model.BlockDirective;
import com.squarespace.less.model.Comment;
import com.squarespace.less.model.Definition;
import com.squarespace.less.model.Directive;
import com.squarespace.less.model.GenericBlock;
import com.squarespace.less.model.Guard;
import com.squarespace.less.model.Import;
import com.squarespace.less.model.ImportMarker;
import com.squarespace.less.model.Media;
import com.squarespace.less.model.Mixin;
import com.squarespace.less.model.MixinCall;
import com.squarespace.less.model.MixinMarker;
import com.squarespace.less.model.MixinParams;
import com.squarespace.less.model.Node;
import com.squarespace.less.model.Rule;
import com.squarespace.less.model.Ruleset;
import com.squarespace.less.model.Selector;
import com.squarespace.less.model.Stylesheet;


/**
 * Given a parse tree, evaluate it. This expands all variable definitions,
 * mixins, imports, etc. This produces a tree that is ready to be rendered.
 */
public class LessEvaluator {

  /**
   * Evaluation complexity threshold at which to bail out and fast-exit.
   */
  private static final int COMPLEXITY_THRESHOLD = 2000000;

  /**
   * Limit to number of mixins which can be expanded per stylesheet.
   */
  private static final int MIXIN_THRESHOLD = 100000;

  /**
   * Limit estimated size of evaluated stylesheet.
   */
  private static final int SIZE_THRESHOLD = 15 * 1024 * 1024;

  /**
   * Context for the current compile.
   */
  protected final LessContext ctx;

  /**
   * Options for the current compile.
   */
  protected final LessOptions opts;

  /**
   * Track complexity of stylesheet as we're evaluating it.
   */
  private int complexity = 0;

  /**
   * Track number of mixins matched.
   */
  private int mixins = 0;

  /**
   * Estimated stylesheet size during evaluation.
   */
  private int size = 0;

  /**
   * Flag to indicate we're bailing out early.
   */
  private boolean fast_exit = false;

  public LessEvaluator(LessContext ctx) {
    this.ctx = ctx;
    this.opts = ctx.options();
  }

  /**
   * Return the evaluation complexity of the stylesheet. Currently this is
   * the number of rules in the stylesheet.
   */
  public int complexity() {
    return complexity;
  }

  /**
   * Evaluate the {@link Stylesheet}, producing a new instance where all variable references,
   * mixins, imports, etc have been evaluated and expanded.
   */
  public Stylesheet evaluate(Stylesheet sheet) throws LessException {
    ExecEnv env = ctx.newEnv();
    Stylesheet result = evaluateStylesheet(env, sheet);
    if (env.hasError()) {
      throw env.error();
    }
    if (fast_exit) {
      result.add(new Comment(" ERROR: Evaluation incomplete: stylesheet exceeded one or more thresholds! ", true));
    }
    if (complexity > COMPLEXITY_THRESHOLD) {
      result.add(new Comment(" EVAL: exceeded stylesheet complexity limit: " + complexity + " ", true));
    }
    if (mixins > MIXIN_THRESHOLD) {
      result.add(new Comment(" EVAL: exceeded mixin expansion limit: " + mixins + " ", true));
    }
    if (size > SIZE_THRESHOLD) {
      result.add(new Comment(" EVAL: exceeded size limit: " + size + " ", true));
    }
    return result;
  }

  /**
   * Evaluate a BLOCK_DIRECTIVE node.
   */
  protected BlockDirective evaluateBlockDirective(ExecEnv env, BlockDirective input) throws LessException {
    size += input.size();
    BlockDirective directive = input.copy();
    env.push(directive);

    Block block = directive.block();
    expandMixins(env, block);
    evaluateRules(env, block, false);

    env.pop();
    return directive;
  }

  /**
   * Evaluate a MEDIA node.
   */
  protected Media evaluateMedia(ExecEnv env, Media input) throws LessException {
    size += input.size();
    if (input.features() != null) {
      List features = input.features().features();
      if (features != null) {
        complexity += features.size();
      }
    }

    Media media = input.copy(env);
    env.push(media);

    Block block = media.block();
    expandMixins(env, block);
    evaluateRules(env, block, false);

    env.pop();
    return media;
  }

  /**
   * Evaluate a RULESET node.
   */
  protected Ruleset evaluateRuleset(ExecEnv env, Ruleset input, boolean forceImportant) throws LessException {
    if (input.block().rules().isEmpty()) {
      return input;
    }

    size += input.size();
    if (input.selectors() != null) {
      List selectors = input.selectors().selectors();
      if (selectors != null) {
        complexity += selectors.size();
      }
    }

    Ruleset original = (Ruleset)input.original();
    Ruleset ruleset = input.copy(env);

    env.push(ruleset);
    original.enter();

    Block block = ruleset.block();
    expandMixins(env, block);
    evaluateRules(env, block, forceImportant);

    original.exit();
    env.pop();
    return ruleset;
  }

  /**
   * Evaluate a STYLESHEET node.
   */
  protected Stylesheet evaluateStylesheet(ExecEnv env, Stylesheet original) throws LessException {
    Stylesheet stylesheet = original.copy();
    env.push(stylesheet);

    Block block = stylesheet.block();
    expandMixins(env, block);
    evaluateRules(env, block, false);

    env.pop();
    return stylesheet;
  }

  /**
   * Iterate over all of the rules in the block and evaluate them, replacing each rule in
   * the list with the result of the evaluation.
   */
  protected void evaluateRules(ExecEnv env, Block block, boolean forceImportant) throws LessException {
    if (complexity > COMPLEXITY_THRESHOLD) {
      fast_exit = true;
    }

    if (size > SIZE_THRESHOLD) {
      fast_exit = true;
    }

    if (fast_exit) {
      return;
    }

    FlexList rules = block.rules();
    if (rules.size() == 0) {
      return;
    }

    complexity += rules.size();

    Import currentImport = null;
    for (int i = 0; i < rules.size(); i++) {
      Node node = rules.get(i);
      if (node == null) {
        continue;
      }
      try {
        switch (node.type()) {

          case BLOCK_DIRECTIVE:
            node = evaluateBlockDirective(env, (BlockDirective)node);
            break;

          case DEFINITION:
            Definition def = (Definition)node;
            size += def.size();
            Definition newDef = def.copy(def.dereference(env));
            newDef.warnings(env.warnings());
            node = newDef;
            break;

          case DIRECTIVE:
            Directive directive = (Directive)(node.eval(env));
            size += directive.size();
            if (directive.name().equals("@charset")) {
              if (block.charset() == null) {
                block.charset(directive);
              }
            }
            node = directive;
            break;

          case IMPORT_MARKER:
            ImportMarker marker = (ImportMarker) node;
            currentImport = marker.beginning() ? marker.importStatement() : null;
            break;

          case MEDIA:
            node = evaluateMedia(env, (Media)node);
            break;

          case MIXIN:
            // Register the closure on the original MIXIN.
            Mixin mixin = (Mixin) ((Mixin)node).original();
            if (mixin.closure() == null) {
              mixin.closure(env);
            }
            break;

          case MIXIN_CALL:
            break;

          case RULESET:
            node = evaluateRuleset(env, (Ruleset)node, forceImportant);
            break;

          case RULE:
            Rule rule = (Rule) node;
            size += rule.size();
            Rule newRule = null;
            if (forceImportant && !rule.important()) {
              newRule = rule.copy(rule.value().eval(env), forceImportant);
            } else {
              newRule = (Rule)rule.eval(env);
            }
            newRule.warnings(env.warnings());
            node = newRule;
            break;

          default:
            node = node.eval(env);
            break;
        }

      } catch (LessException e) {
        if (!env.hasError()) {
          env.error(e);
        }
      }

      if (env.hasError()) {
        // If an error occurred, capture the current stack and re-throw
        LessException error = env.error();
        error.push(node);
        if (currentImport != null) {
          // Track when import boundaries are crossed
          error.push(currentImport);
        }
        return;
      }

      rules.set(i, node);
    }

  }

  /**
   * Iterate over all rules in this block and execute all of the MIXIN_CALL rules found.
   * Each successful call will produce multiple rules. We replace the call with
   * the rules it produced.
   */
  protected void expandMixins(ExecEnv env, Block block) throws LessException {
    if (!block.hasMixinCalls()) {
      return;
    }
    if (fast_exit) {
      return;
    }

    FlexList rules = block.rules();
    // Use of rules.size() intentional since the list size can change during iteration.
    for (int i = 0; i < rules.size(); i++) {
      Node node = rules.get(i);
      if (node instanceof MixinCall) {
        Block mixinResult = executeMixinCall(env, (MixinCall)node);

        // Mixin result can return null if we exceeded our mixin expansion threshold.
        if (mixinResult == null) {
          return;
        }

        // Splice the rules produced by the mixin call into the current block,
        // replacing the mixin call.
        FlexList other = mixinResult.rules();

        block.splice(i, 1, other);
        i += other.size() - 1;

        // Indicate the block has changed, new variable definitions may have
        // been added.
        block.resetVariableCache();
        block.orFlags(mixinResult);

        // All rules added by expanding a mixin add to stylesheet complexity.
        complexity += other.size();

        // Bail out of complexity threshold exceeded
        if (complexity > COMPLEXITY_THRESHOLD) {
          fast_exit = true;
          return;
        }
        if (size > SIZE_THRESHOLD) {
          fast_exit = true;
          return;
        }
      }
    }
  }

  /**
   * Execute a MIXIN_CALL. First it searches the tree for any MIXIN and RULESET nodes that
   * match the call's selector.  If no matches are found, it throws an error.  If matches
   * are found it executes each match.  In order for the call to be considered successful,
   * the call must (a) bind arguments and be called, or (b) bind arguments but evaluate
   * the mixin's guard to FALSE.
   */
  protected Block executeMixinCall(ExecEnv env, MixinCall call) throws LessException {
    if (mixins > MIXIN_THRESHOLD) {
      return null;
    }

    MixinMatcher matcher = new MixinMatcher(env, call);
    MixinResolver resolver = ctx.mixinResolver();
    resolver.reset(matcher);
    env.resolveMixins(resolver);
    List matches = resolver.matches();
    if (matches.isEmpty()) {
      LessException exc = new LessException(mixinUndefined(ctx.render(call.selector())));
      exc.push(call);
      throw exc;
    }

    Block results = new Block();
    int calls = 0;
    int size = matches.size();
    mixins += size;

    if (mixins > MIXIN_THRESHOLD) {
      return null;
    }

    for (int i = 0; i < size; i++) {
      MixinMatch match = matches.get(i);
      Node node = match.mixin();
      if (node instanceof Mixin) {
        if (executeMixin(env, results, matcher, match)) {
          calls++;
        }
      } else if (node instanceof Ruleset) {
        if (executeRulesetMixin(env, results, matcher, match)) {
          calls++;
        }
      }
    }

    if (calls == 0) {
      LessException exc = new LessException(mixinUndefined(ctx.render(call.selector())));
      exc.push(call);
      throw exc;
    }
    return results;
  }

  /**
   * Execute a MIXIN's block.  If argument binding fails, returns false, indicating the
   * call did not successfully match this mixin.  If argument binding succeeds, it sets
   * up the stack and evaluates the mixin's guard expression, if any.
   *
   * If the guard evaluates to FALSE, returns true indicating the mixin was successfully
   * matched but just not executed.
   *
   * If the guard evaluates to TRUE, we execute the mixin's block and merge the produced
   * rules into the 'collector' block.
   */
  protected boolean executeMixin(ExecEnv env, Block collector, MixinMatcher matcher, MixinMatch match)
      throws LessException {

    MixinCall call = matcher.mixinCall();
    Mixin mixin = ((Mixin)match.mixin()).copy();
    MixinParams params = (MixinParams) match.params().eval(env);

    // Attempt to bind the arguments to this mixin's parameters. If the argument binding
    // failed, this is considered a resolution failure.
    GenericBlock bindings = matcher.bind(params);
    if (bindings == null) {
      return false;
    }

    // If the closure has been set on this mixin, use it.
    env = env.copy();
    Mixin original = (Mixin) mixin.original();
    ExecEnv closureEnv = original.closure();
    if (closureEnv != null) {
      env.append(closureEnv.frames());
    }

    // Push the argument bindings onto the closure stack and create the dual stack.
    // We can resolve variables against the closure + argument scope or the scope which
    // called the mixin.
    env.push(bindings);

    // Evaluate the guard conditions. If FALSE, bail out.
    Guard guard = mixin.guard();
    if (guard != null) {
      Node result = guard.eval(env);
      if (FALSE.equals(result)) {
        return true;
      }
    }

    // Limits the overall depth if the mixin call stack.
    LessContext ctx = env.context();
    if (ctx.mixinDepth() >= opts.mixinRecursionLimit()) {
      throw new LessException(mixinRecurse(call.path(), opts.mixinRecursionLimit()));
    }

    // Enter the mixin body and execute it.
    original.enter();
    ctx.enterMixin();

    env.push(mixin);

    try {
      Block block = mixin.block();
      expandMixins(env, block);

      // Wrap the final rules generated by this mixin call.
      if (opts.tracing()) {
        MixinCall actualCall = call.copy();
        actualCall.args(matcher.mixinArgs());
        block.prependNode(new MixinMarker(actualCall, original, true));
        block.appendNode(new MixinMarker(actualCall, original, false));
      }

      evaluateRules(env, block, call.important());
      collector.appendBlock(block);

      // Check for error here and early-return. This ensures we don't decrement the
      // recursion depth and bounce between the recursion limit and limit-1.
      if (env.hasError()) {
        return true;
      }

    } catch (LessException e) {
      // If any errors occur inside a mixin call, we want to show the actual
      // arguments to the mixin call.
      MixinCall actualCall = call.copy();
      actualCall.args(matcher.mixinArgs());
      e.push(actualCall);
      throw e;
    }
    ctx.exitMixin();
    original.exit();
    return true;
  }

  /**
   * Executes a RULESET as a mixin.
   */
  protected boolean executeRulesetMixin(ExecEnv env, Block collector, MixinMatcher matcher, MixinMatch match)
      throws LessException {
    MixinCall call = matcher.mixinCall();
    Ruleset ruleset = (Ruleset)match.mixin();

    // Limits the overall depth if the mixin call stack.
    LessContext ctx = env.context();
    if (ctx.mixinDepth() >= opts.mixinRecursionLimit()) {
      throw new LessException(mixinRecurse(call.path(), opts.mixinRecursionLimit()));
    }

    ctx.enterMixin();
    Ruleset result = evaluateRuleset(env, ruleset, call.important());
    if (env.hasError()) {
      return true;
    }
    ctx.exitMixin();

    Block block = result.block();
    if (opts.tracing()) {
      block.prependNode(new MixinMarker(call, ruleset, true));
      block.appendNode(new MixinMarker(call, ruleset, false));
    }
    collector.appendBlock(block);
    return true;
  }

}






© 2015 - 2024 Weber Informatics LLC | Privacy Policy