Package com.github.timurstrekalov.saga.core.instrumentation

Source Code of com.github.timurstrekalov.saga.core.instrumentation.InstrumentingNodeVisitor

package com.github.timurstrekalov.saga.core.instrumentation;

import java.util.List;

import com.github.timurstrekalov.saga.core.cfg.Config;
import com.github.timurstrekalov.saga.core.model.ScriptData;
import com.google.common.collect.Lists;
import net.sourceforge.htmlunit.corejs.javascript.Token;
import net.sourceforge.htmlunit.corejs.javascript.ast.AstNode;
import net.sourceforge.htmlunit.corejs.javascript.ast.Block;
import net.sourceforge.htmlunit.corejs.javascript.ast.ElementGet;
import net.sourceforge.htmlunit.corejs.javascript.ast.ExpressionStatement;
import net.sourceforge.htmlunit.corejs.javascript.ast.IfStatement;
import net.sourceforge.htmlunit.corejs.javascript.ast.LabeledStatement;
import net.sourceforge.htmlunit.corejs.javascript.ast.Loop;
import net.sourceforge.htmlunit.corejs.javascript.ast.Name;
import net.sourceforge.htmlunit.corejs.javascript.ast.NodeVisitor;
import net.sourceforge.htmlunit.corejs.javascript.ast.NumberLiteral;
import net.sourceforge.htmlunit.corejs.javascript.ast.StringLiteral;
import net.sourceforge.htmlunit.corejs.javascript.ast.SwitchCase;
import net.sourceforge.htmlunit.corejs.javascript.ast.UnaryExpression;
import net.sourceforge.htmlunit.corejs.javascript.ast.VariableDeclaration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static net.sourceforge.htmlunit.corejs.javascript.Token.BLOCK;
import static net.sourceforge.htmlunit.corejs.javascript.Token.BREAK;
import static net.sourceforge.htmlunit.corejs.javascript.Token.CASE;
import static net.sourceforge.htmlunit.corejs.javascript.Token.CONTINUE;
import static net.sourceforge.htmlunit.corejs.javascript.Token.DO;
import static net.sourceforge.htmlunit.corejs.javascript.Token.EMPTY;
import static net.sourceforge.htmlunit.corejs.javascript.Token.EXPR_RESULT;
import static net.sourceforge.htmlunit.corejs.javascript.Token.EXPR_VOID;
import static net.sourceforge.htmlunit.corejs.javascript.Token.FOR;
import static net.sourceforge.htmlunit.corejs.javascript.Token.FUNCTION;
import static net.sourceforge.htmlunit.corejs.javascript.Token.IF;
import static net.sourceforge.htmlunit.corejs.javascript.Token.RETURN;
import static net.sourceforge.htmlunit.corejs.javascript.Token.SCRIPT;
import static net.sourceforge.htmlunit.corejs.javascript.Token.SWITCH;
import static net.sourceforge.htmlunit.corejs.javascript.Token.THROW;
import static net.sourceforge.htmlunit.corejs.javascript.Token.TRY;
import static net.sourceforge.htmlunit.corejs.javascript.Token.VAR;
import static net.sourceforge.htmlunit.corejs.javascript.Token.WHILE;

class InstrumentingNodeVisitor implements NodeVisitor {

    private static final Logger logger = LoggerFactory.getLogger(InstrumentingNodeVisitor.class);

    private final ScriptData data;
    private final int lineNumberOffset;

    public InstrumentingNodeVisitor(final ScriptData data, final int lineNumberOffset) {
        this.data = data;
        this.lineNumberOffset = lineNumberOffset;
    }

    @Override
    public boolean visit(final AstNode node) {
        handleNumberLiteralBug(node);

        if (isExecutableBlock(node)) {
            addInstrumentationSnippetFor(node);
        }

        return true;
    }

    /**
     * fix the fact that NumberLiteral outputs hexadecimal numbers without the '0x' part (e.g. 0xFF becomes FF
     * when toSource() is called), which results in invalid JS syntax. Using the actual number value instead
     * to fix this (shouldn't break anything)
     */
    private void handleNumberLiteralBug(final AstNode node) {
        if (node.getType() == Token.NUMBER) {
            final NumberLiteral numberLiteral = (NumberLiteral) node;
            numberLiteral.setValue(getValue(numberLiteral));
        }
    }

    private String getValue(final NumberLiteral literal) {
        if (Math.floor(literal.getNumber()) == literal.getNumber()) {
            return Long.toString((long) literal.getNumber());
        }

        return Double.toString(literal.getNumber());
    }

    private boolean isExecutableBlock(final AstNode node) {
        final AstNode parent = node.getParent();
        if (parent == null) {
            return false;
        }

        final int type = node.getType();
        final int parentType = parent.getType();

        return type == SWITCH
                || type == FOR
                || type == DO
                || type == WHILE
                || type == CONTINUE
                || type == BREAK
                || type == TRY
                || type == THROW
                || type == CASE
                || type == IF
                || type == EXPR_RESULT
                || type == EXPR_VOID
                || type == RETURN
                || (type == FUNCTION && (parentType == SCRIPT || parentType == BLOCK))
                || (type == VAR && node.getClass() == VariableDeclaration.class && parentType != FOR);
    }

    private void addInstrumentationSnippetFor(final AstNode node) {
        final AstNode parent = node.getParent();

        final int type = node.getType();
        final int parentType = parent.getType();

        if (type == WHILE || type == FOR || type == DO) {
            fixLoops((Loop) node);
        }

        if (type == IF) {
            fixIf((IfStatement) node);
        }

        if (type == CASE) {
            handleSwitchCase((SwitchCase) node);
        } else if (type == IF && parentType == IF) {
            final IfStatement elseIfStatement = (IfStatement) node;
            final IfStatement ifStatement = (IfStatement) parent;

            if (ifStatement.getElsePart() == elseIfStatement) {
                flattenElseIf(elseIfStatement, ifStatement);
                data.addExecutableLine(getActualLineNumber(node));
            }
        } else if (parentType != CASE) {
            // issue #54
            if (parent.getClass() == LabeledStatement.class) {
                return;
            }

            if (parent.hasChildren()) {
                parent.addChildBefore(newInstrumentationNode(getActualLineNumber(node)), node);
            } else {
                // if, else, while, do, for without {} around their respective 'blocks' for some reason
                // don't have children. Meh. Creating blocks to ease instrumentation.
                final Block block = newInstrumentedBlock(node);

                if (parentType == IF) {
                    final IfStatement ifStatement = (IfStatement) parent;

                    if (ifStatement.getThenPart() == node) {
                        ifStatement.setThenPart(block);
                    } else if (ifStatement.getElsePart() == node) {
                        ifStatement.setElsePart(block);
                    }
                } else if (parentType == WHILE || parentType == FOR || parentType == DO) {
                    ((Loop) parent).setBody(block);
                } else {
                    logger.warn("Cannot handle node with parent that has no children, parent class: {}, parent source:\n{}",
                            parent.getClass(), parent.toSource());
                }
            }

            data.addExecutableLine(getActualLineNumber(node));
        }
    }

    /**
     * when loops contain only ';' as body or nothing at all (happens when they are minified),
     * certain things might go horribly wrong (like the jquery 1.4.2 case)
     */
    private void fixLoops(final Loop loop) {
        if (loop.getBody().getType() == EMPTY) {
            loop.setBody(new Block());
        }
    }

    /**
     * The same as loops (the if (true); case)
     * @see #fixLoops(net.sourceforge.htmlunit.corejs.javascript.ast.Loop)
     */
    private void fixIf(final IfStatement ifStatement) {
        if (ifStatement.getThenPart().getType() == EMPTY) {
            ifStatement.setThenPart(new Block());
        }
    }

    private int getActualLineNumber(final AstNode node) {
        return node.getLineno() - lineNumberOffset;
    }

    private Block newInstrumentedBlock(final AstNode node) {
        final Block block = new Block();
        block.addChild(node);
        block.addChildBefore(newInstrumentationNode(getActualLineNumber(node)), node);
        return block;
    }

    /**
     * 'switch' statement cases are special in the sense that their children are not actually their children,
     * meaning the children have a reference to their parent, but the parent only has a List of all its
     * children, so we can't just addChildBefore() like we do for all other cases.
     *
     * They do, however, retain a list of all statements per each case, which we're using here
     */
    private void handleSwitchCase(final SwitchCase switchCase) {
        // empty case: statement
        if (switchCase.getStatements() == null) {
            return;
        }

        final List<AstNode> newStatements = Lists.newArrayList();

        for (final AstNode statement : switchCase.getStatements()) {
            final int lineNr = getActualLineNumber(statement);
            data.addExecutableLine(lineNr);

            newStatements.add(newInstrumentationNode(lineNr));
            newStatements.add(statement);
        }

        switchCase.setStatements(newStatements);
    }

    /**
     * In order to make it possible to cover else-if blocks, we're flattening the shorthand else-if
     *
     * <pre>
     * {@literal
     * if (cond1) {
     *     doIf();
     * } else if (cond2) {
     *     doElseIf();
     * } else {
     *     doElse();
     * }
     * }
     * </pre>
     *
     * into
     *
     * <pre>
     * {@literal
     * if (cond1) {
     *     doIf();
     * } else {
     *     if (cond2) {
     *         doElseIf();
     *     } else {
     *         doElse();
     *     }
     * }
     * }
     * </pre>
     */
    private void flattenElseIf(final IfStatement elseIfStatement, final IfStatement ifStatement) {
        final Block block = new Block();
        block.addChild(elseIfStatement);

        ifStatement.setElsePart(block);

        final int lineNr = getActualLineNumber(elseIfStatement);

        data.addExecutableLine(lineNr);
        block.addChildBefore(newInstrumentationNode(lineNr), elseIfStatement);
    }

    private AstNode newInstrumentationNode(final int lineNr) {
        final ExpressionStatement instrumentationNode = new ExpressionStatement();
        final UnaryExpression inc = new UnaryExpression();

        inc.setIsPostfix(true);
        inc.setOperator(Token.INC);

        final ElementGet outer = new ElementGet();
        final ElementGet inner = new ElementGet();

        outer.setTarget(inner);

        final Name covDataVar = new Name();
        covDataVar.setIdentifier(ScriptInstrumenter.COVERAGE_VARIABLE_NAME);

        inner.setTarget(covDataVar);

        final StringLiteral fileName = new StringLiteral();
        fileName.setValue(data.getSourceUriAsString());
        fileName.setQuoteCharacter('\'');

        inner.setElement(fileName);

        final NumberLiteral index = new NumberLiteral();
        index.setNumber(lineNr);
        index.setValue(Integer.toString(lineNr));

        outer.setElement(index);

        inc.setOperand(outer);

        instrumentationNode.setExpression(inc);
        instrumentationNode.setHasResult();

        return instrumentationNode;
    }

}
TOP

Related Classes of com.github.timurstrekalov.saga.core.instrumentation.InstrumentingNodeVisitor

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.