blob: e00e9ba8acd96f7c9c8e4cefe85266593bbd51f1 [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.javac;
import com.google.gwt.core.client.GwtScriptOnly;
import com.google.gwt.core.ext.TreeLogger.HelpInfo;
import com.google.gwt.dev.jjs.CorrelationFactory;
import com.google.gwt.dev.jjs.InternalCompilerException;
import com.google.gwt.dev.jjs.SourceInfo;
import com.google.gwt.dev.jjs.SourceOrigin;
import com.google.gwt.dev.js.JsParser;
import com.google.gwt.dev.js.JsParserException;
import com.google.gwt.dev.js.JsParserException.SourceDetail;
import com.google.gwt.dev.js.ast.JsExprStmt;
import com.google.gwt.dev.js.ast.JsFunction;
import com.google.gwt.dev.js.ast.JsParameter;
import com.google.gwt.dev.js.ast.JsScope;
import com.google.gwt.dev.js.ast.JsStatement;
import com.google.gwt.dev.util.collect.IdentityHashMap;
import com.google.gwt.dev.util.collect.IdentityMaps;
import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.compiler.CompilationResult;
import org.eclipse.jdt.internal.compiler.ast.AbstractMethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.Annotation;
import org.eclipse.jdt.internal.compiler.ast.Argument;
import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
import org.eclipse.jdt.internal.compiler.ast.MethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;
import org.eclipse.jdt.internal.compiler.problem.ProblemSeverities;
import org.eclipse.jdt.internal.compiler.util.Util;
import java.io.IOException;
import java.io.Serializable;
import java.io.StringReader;
import java.util.List;
import java.util.Map;
/**
* Adapts compilation units containing JSNI-accessible code by rewriting the
* source.
*/
public class JsniCollector {
/**
* Represents a logical interval of text.
*/
public static class Interval {
public final int end;
public final int start;
public Interval(int start, int end) {
this.start = start;
this.end = end;
}
}
private static final class JsniMethodImpl extends JsniMethod implements
Serializable {
private final JsFunction func;
private boolean isScriptOnly;
private final String name;
public JsniMethodImpl(String name, JsFunction func, boolean isScriptOnly) {
this.name = name;
this.func = func;
this.isScriptOnly = isScriptOnly;
}
@Override
public JsFunction function() {
return func;
}
@Override
public boolean isScriptOnly() {
return isScriptOnly;
}
@Override
public int line() {
return func.getSourceInfo().getStartLine();
}
@Override
public String location() {
return func.getSourceInfo().getFileName();
}
@Override
public String name() {
return name;
}
@Override
public String[] paramNames() {
List<JsParameter> params = func.getParameters();
String[] result = new String[params.size()];
for (int i = 0; i < result.length; ++i) {
result[i] = params.get(i).getName().getIdent();
}
return result;
}
@Override
public String toString() {
return func.toString();
}
}
private static class Visitor extends MethodVisitor {
private static boolean isScriptOnly(AbstractMethodDeclaration method) {
if (method.annotations == null) {
return false;
}
for (Annotation a : method.annotations) {
ReferenceBinding binding = (ReferenceBinding) a.resolvedType;
String name = CharOperation.toString(binding.compoundName);
if (name.equals(GwtScriptOnly.class.getName())) {
return true;
}
}
return false;
}
private final CorrelationFactory correlator;
private final Map<MethodDeclaration, JsniMethod> jsniMethods;
private final JsScope scope;
private final String source;
private SourceInfo cudInfo;
public Visitor(String source, JsScope scope, CorrelationFactory correlator,
Map<MethodDeclaration, JsniMethod> jsniMethods) {
this.jsniMethods = jsniMethods;
this.source = source;
this.scope = scope;
this.correlator = correlator;
}
@Override
public void collect(CompilationUnitDeclaration cud) {
cudInfo = correlator.makeSourceInfo(SourceOrigin.create(0, String
.valueOf(cud.getFileName())));
super.collect(cud);
}
@Override
protected boolean interestingMethod(AbstractMethodDeclaration method) {
return method.isNative();
}
@Override
protected void processMethod(TypeDeclaration typeDecl,
AbstractMethodDeclaration method, String enclosingType) {
JsFunction jsFunction = parseJsniFunction(method, source, enclosingType,
cudInfo, scope);
if (jsFunction != null) {
String jsniSignature = getJsniSignature(enclosingType, method);
jsniMethods.put((MethodDeclaration) method, new JsniMethodImpl(
jsniSignature, jsFunction, isScriptOnly(method)));
}
}
}
public static final String JSNI_BLOCK_END = "}-*/";
public static final String JSNI_BLOCK_START = "/*-{";
public static Map<MethodDeclaration, JsniMethod> collectJsniMethods(
CompilationUnitDeclaration cud, String source, JsScope scope,
CorrelationFactory correlator) {
Map<MethodDeclaration, JsniMethod> jsniMethods = new IdentityHashMap<MethodDeclaration, JsniMethod>();
new Visitor(source, scope, correlator, jsniMethods).collect(cud);
return IdentityMaps.normalizeUnmodifiable(jsniMethods);
}
public static JsFunction parseJsniFunction(AbstractMethodDeclaration method,
String unitSource, String enclosingType, SourceInfo baseInfo,
JsScope scope) {
CompilationResult compResult = method.compilationResult;
int[] indexes = compResult.lineSeparatorPositions;
int startLine = Util.getLineNumber(method.sourceStart, indexes, 0,
indexes.length - 1);
SourceInfo info = baseInfo.makeChild(SourceOrigin.create(
method.sourceStart, method.bodyEnd, startLine, baseInfo.getFileName()));
// Handle JSNI block
String jsniCode = unitSource
.substring(method.bodyStart, method.bodyEnd + 1);
int startPos = jsniCode.indexOf("/*-{");
int endPos = jsniCode.lastIndexOf("}-*/");
if (startPos < 0 && endPos < 0) {
reportJsniError(
info,
method,
"Native methods require a JavaScript implementation enclosed with /*-{ and }-*/");
return null;
}
if (startPos < 0) {
reportJsniError(info, method,
"Unable to find start of native block; begin your JavaScript block with: /*-{");
return null;
}
if (endPos < 0) {
reportJsniError(
info,
method,
"Unable to find end of native block; terminate your JavaScript block with: }-*/");
return null;
}
startPos += 3; // move up to open brace
endPos += 1; // move past close brace
jsniCode = jsniCode.substring(startPos, endPos);
// Here we parse it as an anonymous function, but we will give it a
// name later when we generate the JavaScript during code generation.
//
StringBuilder functionSource = new StringBuilder("function (");
boolean first = true;
if (method.arguments != null) {
for (Argument arg : method.arguments) {
if (first) {
first = false;
} else {
functionSource.append(',');
}
functionSource.append(arg.binding.name);
}
}
functionSource.append(") ");
int functionHeaderLength = functionSource.length();
functionSource.append(jsniCode);
StringReader sr = new StringReader(functionSource.toString());
// Absolute start and end position of braces in original source.
int absoluteJsStartPos = method.bodyStart + startPos;
int absoluteJsEndPos = absoluteJsStartPos + jsniCode.length();
// Adjust the points the JS parser sees to account for the synth header.
int jsStartPos = absoluteJsStartPos - functionHeaderLength;
int jsEndPos = absoluteJsEndPos - functionHeaderLength;
// To compute the start line, count lines from point to point.
int jsLine = info.getStartLine()
+ countLines(indexes, info.getStartPos(), absoluteJsStartPos);
SourceInfo jsInfo = baseInfo.makeChild(SourceOrigin.create(jsStartPos,
jsEndPos, jsLine, baseInfo.getFileName()));
try {
List<JsStatement> result = JsParser.parse(jsInfo, scope, sr);
JsExprStmt jsExprStmt = (JsExprStmt) result.get(0);
return (JsFunction) jsExprStmt.getExpression();
} catch (IOException e) {
throw new InternalCompilerException("Internal error parsing JSNI in '"
+ enclosingType + '.' + method.toString() + '\'', e);
} catch (JsParserException e) {
int problemCharPos = computeAbsoluteProblemPosition(indexes, e
.getSourceDetail());
SourceInfo errorInfo = SourceOrigin.create(problemCharPos,
problemCharPos, e.getSourceDetail().getLine(), info.getFileName());
// Strip the file/line header because reportJsniError will add that.
String msg = e.getMessage();
int pos = msg.indexOf(": ");
msg = msg.substring(pos + 2);
reportJsniError(errorInfo, method, msg);
return null;
}
}
public static void reportJsniError(SourceInfo info,
AbstractMethodDeclaration method, String msg) {
reportJsniProblem(info, method, msg, ProblemSeverities.Error);
}
public static void reportJsniWarning(SourceInfo info,
MethodDeclaration method, String msg) {
reportJsniProblem(info, method, msg, ProblemSeverities.Warning);
}
/**
* JS reports the error as a line number, to find the absolute position in the
* real source stream, we have to walk from the absolute JS start position
* until we have counted down enough lines. Then we use the column position to
* find the exact spot.
*/
private static int computeAbsoluteProblemPosition(int[] indexes,
SourceDetail detail) {
// Convert 1-based to -1 - based.
int line = detail.getLine() - 1;
if (line == 0) {
return detail.getLineOffset() - 1;
}
int result = indexes[line - 1] + detail.getLineOffset();
/*
* In other words, make sure our result is actually on this line (less than
* the start position of the next line), but make sure we don't overflow if
* this is the last line in the file.
*/
assert line >= indexes.length || result < indexes[line];
return result;
}
private static int countLines(int[] indexes, int p1, int p2) {
assert p1 >= 0;
assert p2 >= 0;
assert p1 <= p2;
int p1line = findLine(p1, indexes, 0, indexes.length);
int p2line = findLine(p2, indexes, 0, indexes.length);
return p2line - p1line;
}
private static int findLine(int pos, int[] indexes, int lo, int tooHi) {
assert (lo < tooHi);
if (lo == tooHi - 1) {
return lo;
}
int mid = lo + (tooHi - lo) / 2;
assert (lo < mid);
if (pos < indexes[mid]) {
return findLine(pos, indexes, lo, mid);
} else {
return findLine(pos, indexes, mid, tooHi);
}
}
/**
* Gets a unique name for this method and its signature (this is used to
* determine whether one method overrides another).
*/
private static String getJsniSignature(String enclosingType,
AbstractMethodDeclaration method) {
return '@' + enclosingType + "::"
+ MethodVisitor.getMemberSignature(method);
}
private static void reportJsniProblem(SourceInfo info,
AbstractMethodDeclaration methodDeclaration, String message,
int problemSeverity) {
// TODO: provide helpInfo for how to write JSNI methods?
HelpInfo jsniHelpInfo = null;
CompilationResult compResult = methodDeclaration.compilationResult();
// recalculate startColumn, because SourceInfo does not hold it
int startColumn = Util.searchColumnNumber(compResult
.getLineSeparatorPositions(), info.getStartLine(), info.getStartPos());
GWTProblem.recordProblem(info, startColumn, compResult, message,
jsniHelpInfo, problemSeverity);
}
private JsniCollector() {
}
}