| /* |
| * 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); |
| } |
| } |