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