blob: 47e5763830d67dddb4e60862361cd11a9b1463ef [file] [log] [blame]
/*
* Copyright 2008 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.core.ext.soyc.Range;
import com.google.gwt.dev.jjs.JsSourceMap;
import com.google.gwt.dev.jjs.SourceInfo;
import com.google.gwt.dev.jjs.SourceOrigin;
import com.google.gwt.dev.jjs.impl.JavaToJavaScriptMap;
import com.google.gwt.dev.js.ast.JsBlock;
import com.google.gwt.dev.js.ast.JsDoWhile;
import com.google.gwt.dev.js.ast.JsExpression;
import com.google.gwt.dev.js.ast.JsName;
import com.google.gwt.dev.js.ast.JsNameRef;
import com.google.gwt.dev.js.ast.JsNode;
import com.google.gwt.dev.js.ast.JsStatement;
import com.google.gwt.dev.js.ast.JsVisitable;
import com.google.gwt.dev.util.TextOutput;
import com.google.gwt.thirdparty.guava.common.annotations.VisibleForTesting;
import com.google.gwt.thirdparty.guava.common.collect.Lists;
import java.util.List;
/**
* A variation on the standard source generation visitor that records the
* locations of SourceInfo objects in the output.
*/
public class JsReportGenerationVisitor extends
JsSourceGenerationVisitorWithSizeBreakdown {
private final List<Range> ranges = Lists.newArrayList();
private final TextOutput out;
private final boolean needSourcemapNames;
/**
* The key of the most recently added Javascript range for a descendant
* of the current node.
*/
private Range previousRange = null;
/**
* The ancestor nodes whose Range and SourceInfo will be added to the sourcemap.
*/
private List<JsNode> parentStack = Lists.newArrayList();
public JsReportGenerationVisitor(TextOutput out, JavaToJavaScriptMap map,
boolean needSourcemapNames) {
super(out, map);
this.out = out;
this.needSourcemapNames = needSourcemapNames;
}
@Override
protected <T extends JsVisitable> T generateAndBill(T node, JsName nameToBillTo) {
previousRange = null; // It's not our child because we haven't visited our children yet.
if (!(node instanceof JsNode)) {
return super.generateAndBill(node, nameToBillTo);
}
boolean willReportRange = false;
if (node instanceof JsBlock) {
willReportRange = false; // Only report the statements within the block
} else if (parentStack.isEmpty()) {
willReportRange = true;
} else if (node instanceof JsStatement) {
willReportRange = true;
} else if ((node instanceof JsNameRef) && needSourcemapNames) {
willReportRange = true;
} else {
JsNode parent = parentStack.get(parentStack.size() - 1);
if ((node instanceof JsExpression) &&
(parent instanceof JsDoWhile)) {
// Always instrument the expression because it comes at the end.
// (So we can stop there in a loop.)
willReportRange = true;
} else {
// Instrument the expression if it was inlined in Java.
SourceInfo info = ((JsNode) node).getSourceInfo();
if (!surroundsInJavaSource(parent.getSourceInfo(), info)) {
willReportRange = true;
}
}
}
// Remember the position before generating the JavaScript.
int beforePosition = out.getPosition();
int beforeLine = out.getLine();
int beforeColumn = out.getColumn();
if (willReportRange) {
parentStack.add((JsNode) node);
}
// Write some JavaScript (changing the position).
T toReturn = super.generateAndBill(node, nameToBillTo);
if (!willReportRange) {
return toReturn;
}
parentStack.remove(parentStack.size() - 1);
SourceInfo info = ((JsNode) node).getSourceInfo();
Range range = new Range(beforePosition, out.getPosition(), beforeLine, beforeColumn,
out.getLine(), out.getColumn(), info);
if (out.getPosition() <= beforePosition || beforeLine < 0 || out.getLine() < 0) {
// Skip bogus entries.
// Runtime:prototypesByTypeId is pruned here. Maybe others too?
return toReturn;
}
if (info == SourceOrigin.UNKNOWN || info.getFileName() == null || info.getStartLine() < 0) {
// Skip synthetic types (like 'true' and 'false' literals) with no Java source.
return toReturn;
}
if (previousRange != null && previousRange.contains(range)) {
// Skip duplicate and nested range.
return toReturn;
}
// There is an opportunity to do a complex "overlapping range" combination here as well. But
// it's difficult to verify. If we need more speed consider adding this transformation.
ranges.add(range);
previousRange = range;
return toReturn;
}
/**
* Returns true if the given parent's range as Java source code surrounds
* the child.
*/
@VisibleForTesting
boolean surroundsInJavaSource(SourceInfo parent, SourceInfo child) {
if (!hasValidJavaRange(parent) || !hasValidJavaRange(child)) {
return false;
}
return parent.getStartPos() <= child.getStartPos() && child.getEndPos() <= parent.getEndPos()
&& child.getFileName().equals(parent.getFileName());
}
private boolean hasValidJavaRange(SourceInfo info) {
return info != null && info.getStartPos() >= 0 && info.getEndPos() >= info.getStartPos();
}
@Override
protected void billChildToHere() {
if (previousRange != null && previousRange.getEnd() < out.getPosition()) {
// Expand overlapping range.
Range expandedRange =
previousRange.withNewEnd(out.getPosition(), out.getLine(), out.getColumn());
int lastIndex = ranges.size() - 1;
Range removedRange = ranges.set(lastIndex, expandedRange);
assert removedRange == previousRange;
previousRange = expandedRange;
}
}
@Override
public JsSourceMap getSourceInfoMap() {
return new JsSourceMap(ranges, out.getPosition(), out.getLine());
}
}