blob: a8f739f7813b86f4df50e9583e6d137a4e1b67a0 [file] [log] [blame]
/*
* Copyright 2012 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.gwt.dev.js;
import com.google.gwt.dev.js.ast.JsArrayAccess;
import com.google.gwt.dev.js.ast.JsBinaryOperation;
import com.google.gwt.dev.js.ast.JsContext;
import com.google.gwt.dev.js.ast.JsExpression;
import com.google.gwt.dev.js.ast.JsFor;
import com.google.gwt.dev.js.ast.JsInvocation;
import com.google.gwt.dev.js.ast.JsModVisitor;
import com.google.gwt.dev.js.ast.JsNameRef;
import com.google.gwt.dev.js.ast.JsNew;
import com.google.gwt.dev.js.ast.JsNode;
import com.google.gwt.dev.js.ast.JsPostfixOperation;
import com.google.gwt.dev.js.ast.JsPrefixOperation;
import com.google.gwt.dev.js.ast.JsPropertyInitializer;
import com.google.gwt.dev.js.ast.JsUnaryOperator;
import com.google.gwt.dev.js.ast.JsWhile;
import com.google.gwt.thirdparty.guava.common.collect.Sets;
import java.util.Set;
/**
* A visitor that visits every location in the AST where instrumentation is
* desirable.
*/
public abstract class CoverageVisitor extends JsModVisitor {
private int lastLine = -1;
private String lastFile = "";
private Set<String> instrumentedFiles;
/**
* Nodes in this set are used in a context that expects a reference, not
* just an arbitrary expression. For example, <code>delete</code> takes a
* reference. These are tracked because it wouldn't be safe to rewrite
* <code>delete foo.bar</code> to <code>delete (line='123',foo).bar</code>.
*/
private final Set<JsNode> nodesInRefContext = Sets.newHashSet();
public CoverageVisitor(Set<String> instrumentedFiles) {
this.instrumentedFiles = instrumentedFiles;
}
@Override public void endVisit(JsArrayAccess x, JsContext ctx) {
visitExpression(x, ctx);
}
@Override public void endVisit(JsBinaryOperation x, JsContext ctx) {
visitExpression(x, ctx);
}
@Override public void endVisit(JsInvocation x, JsContext ctx) {
nodesInRefContext.remove(x.getQualifier());
visitExpression(x, ctx);
}
@Override public void endVisit(JsNameRef x, JsContext ctx) {
visitExpression(x, ctx);
}
@Override public void endVisit(JsNew x, JsContext ctx) {
visitExpression(x, ctx);
}
@Override public void endVisit(JsPostfixOperation x, JsContext ctx) {
visitExpression(x, ctx);
}
@Override public void endVisit(JsPrefixOperation x, JsContext ctx) {
visitExpression(x, ctx);
nodesInRefContext.remove(x.getArg());
}
/**
* This is essentially a hacked-up version of JsFor.traverse to account for
* flow control differing from visitation order. It resets lastFile and
* lastLine before the condition and increment expressions in the for loop
* so that location data will be recorded correctly.
*/
@Override public boolean visit(JsFor x, JsContext ctx) {
if (x.getInitExpr() != null) {
x.setInitExpr(accept(x.getInitExpr()));
} else if (x.getInitVars() != null) {
x.setInitVars(accept(x.getInitVars()));
}
if (x.getCondition() != null) {
resetPosition();
x.setCondition(accept(x.getCondition()));
}
if (x.getIncrExpr() != null) {
resetPosition();
x.setIncrExpr(accept(x.getIncrExpr()));
}
accept(x.getBody());
return false;
}
@Override public boolean visit(JsInvocation x, JsContext ctx) {
nodesInRefContext.add(x.getQualifier());
return true;
}
@Override public boolean visit(JsPropertyInitializer x, JsContext ctx) {
// Do not instrument labels.
x.setValueExpr(accept(x.getValueExpr()));
return false;
}
@Override public boolean visit(JsPrefixOperation x, JsContext ctx) {
if (x.getOperator() == JsUnaryOperator.DELETE
|| x.getOperator() == JsUnaryOperator.TYPEOF) {
nodesInRefContext.add(x.getArg());
}
return true;
}
/**
* Similar to JsFor, this resets the current location information before
* evaluating the condition.
*/
@Override public boolean visit(JsWhile x, JsContext ctx) {
resetPosition();
x.setCondition(accept(x.getCondition()));
accept(x.getBody());
return false;
}
protected abstract void endVisit(JsExpression x, JsContext ctx);
private void resetPosition() {
lastFile = "";
lastLine = -1;
}
private void visitExpression(JsExpression x, JsContext ctx) {
if (ctx.isLvalue()) {
// Assignments to comma expressions aren't legal
return;
} else if (nodesInRefContext.contains(x)) {
// Don't modify references into non-references
return;
} else if (!instrumentedFiles.contains(x.getSourceInfo().getFileName())) {
return;
} else if (x.getSourceInfo().getStartLine() == lastLine
&& (x.getSourceInfo().getFileName().equals(lastFile))) {
return;
}
lastLine = x.getSourceInfo().getStartLine();
lastFile = x.getSourceInfo().getFileName();
endVisit(x, ctx);
}
}