blob: aae722a30605974ef3f0f81b1a85ef998315d323 [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.UnsafeNativeLong;
import com.google.gwt.dev.javac.JSORestrictionsChecker.CheckerState;
import com.google.gwt.dev.jdt.SafeASTVisitor;
import com.google.gwt.dev.jjs.InternalCompilerException;
import com.google.gwt.dev.jjs.SourceInfo;
import com.google.gwt.dev.js.ast.JsContext;
import com.google.gwt.dev.js.ast.JsFunction;
import com.google.gwt.dev.js.ast.JsNameRef;
import com.google.gwt.dev.js.ast.JsVisitor;
import com.google.gwt.dev.util.InstalledHelpInfo;
import com.google.gwt.dev.util.JsniRef;
import com.google.gwt.dev.util.collect.Sets;
import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.compiler.ast.ASTNode;
import org.eclipse.jdt.internal.compiler.ast.Annotation;
import org.eclipse.jdt.internal.compiler.ast.Argument;
import org.eclipse.jdt.internal.compiler.ast.ArrayInitializer;
import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
import org.eclipse.jdt.internal.compiler.ast.Expression;
import org.eclipse.jdt.internal.compiler.ast.MemberValuePair;
import org.eclipse.jdt.internal.compiler.ast.MethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.StringLiteral;
import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
import org.eclipse.jdt.internal.compiler.ast.TypeReference;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
import org.eclipse.jdt.internal.compiler.impl.Constant;
import org.eclipse.jdt.internal.compiler.lookup.BaseTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.Binding;
import org.eclipse.jdt.internal.compiler.lookup.BlockScope;
import org.eclipse.jdt.internal.compiler.lookup.ClassScope;
import org.eclipse.jdt.internal.compiler.lookup.CompilationUnitScope;
import org.eclipse.jdt.internal.compiler.lookup.FieldBinding;
import org.eclipse.jdt.internal.compiler.lookup.MethodBinding;
import org.eclipse.jdt.internal.compiler.lookup.NestedTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.ProblemReferenceBinding;
import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;
import org.eclipse.jdt.internal.compiler.lookup.SyntheticArgumentBinding;
import org.eclipse.jdt.internal.compiler.lookup.TypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.TypeIds;
import org.eclipse.jdt.internal.compiler.lookup.UnresolvedReferenceBinding;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Locale;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.Stack;
/**
* Tests for access to Java from JSNI. Issues a warning for:
* <ul>
* <li>JSNI methods with a parameter or return type of long.</li>
* <li>Access from JSNI to a field whose type is long.</li>
* <li>Access from JSNI to a method with a parameter or return type of long.</li>
* <li>JSNI references to anonymous classes.</li>
* </ul>
* All tests also apply for arrays of longs, arrays of arrays of longs, etc.
*/
public class JsniChecker {
/**
* A call-back interface to resolve types.
*/
public interface TypeResolver {
ReferenceBinding resolveType(String typeName);
}
private class JsniDeclChecker extends SafeASTVisitor implements
ClassFileConstants {
@Override
public void endVisit(MethodDeclaration meth, ClassScope scope) {
if (meth.isNative()) {
boolean hasUnsafeLongsAnnotation = hasUnsafeLongsAnnotation(meth, scope);
if (!hasUnsafeLongsAnnotation) {
checkDecl(meth, scope);
}
JsniMethod jsniMethod = jsniMethods.get(meth);
if (jsniMethod != null) {
new JsniRefChecker(meth, hasUnsafeLongsAnnotation).check(jsniMethod.function());
}
}
suppressWarningsStack.pop();
}
@Override
public void endVisit(TypeDeclaration typeDeclaration, ClassScope scope) {
suppressWarningsStack.pop();
}
@Override
public void endVisit(TypeDeclaration typeDeclaration,
CompilationUnitScope scope) {
suppressWarningsStack.pop();
}
@Override
public void endVisitValid(TypeDeclaration typeDeclaration, BlockScope scope) {
suppressWarningsStack.pop();
}
@Override
public boolean visit(MethodDeclaration meth, ClassScope scope) {
suppressWarningsStack.push(getSuppressedWarnings(meth.annotations));
return true;
}
@Override
public boolean visit(TypeDeclaration typeDeclaration, ClassScope scope) {
suppressWarningsStack.push(getSuppressedWarnings(typeDeclaration.annotations));
return true;
}
@Override
public boolean visit(TypeDeclaration typeDeclaration,
CompilationUnitScope scope) {
suppressWarningsStack.push(getSuppressedWarnings(typeDeclaration.annotations));
return true;
}
@Override
public boolean visitValid(TypeDeclaration typeDeclaration, BlockScope scope) {
suppressWarningsStack.push(getSuppressedWarnings(typeDeclaration.annotations));
return true;
}
private void checkDecl(MethodDeclaration meth, ClassScope scope) {
TypeReference returnType = meth.returnType;
if (containsLong(returnType, scope)) {
longAccessError(meth, "Type '" + typeString(returnType)
+ "' may not be returned from a JSNI method");
}
if (meth.arguments != null) {
for (Argument arg : meth.arguments) {
if (containsLong(arg.type, scope)) {
longAccessError(arg, "Parameter '" + String.valueOf(arg.name)
+ "': type '" + typeString(arg.type)
+ "' is not safe to access in JSNI code");
}
}
}
}
private boolean containsLong(final TypeReference type, ClassScope scope) {
return type != null
&& JsniChecker.this.containsLong(type.resolveType(scope));
}
private String typeString(TypeReference type) {
return type.toString();
}
}
private class JsniRefChecker extends JsVisitor {
private transient SourceInfo errorInfo;
private final boolean hasUnsafeLongsAnnotation;
private final MethodDeclaration method;
public JsniRefChecker(MethodDeclaration method,
boolean hasUnsafeLongsAnnotation) {
this.method = method;
this.hasUnsafeLongsAnnotation = hasUnsafeLongsAnnotation;
}
public void check(JsFunction function) {
this.accept(function);
}
@Override
public void endVisit(JsNameRef x, JsContext ctx) {
this.errorInfo = x.getSourceInfo();
String ident = x.getIdent();
if (ident.charAt(0) == '@') {
JsniRef jsniRef = JsniRef.parse(ident);
if (jsniRef == null) {
emitError("Malformed JSNI identifier '" + ident + "'");
} else {
Binding binding = checkRef(jsniRef, x.getQualifier() != null,
ctx.isLvalue());
if (binding != null) {
jsniRefs.put(ident, binding);
}
}
}
this.errorInfo = null;
}
private FieldBinding checkFieldRef(ReferenceBinding clazz, JsniRef jsniRef,
boolean hasQualifier, boolean isLvalue) {
assert jsniRef.isField();
FieldBinding target = getField(clazz, jsniRef);
if (target == null) {
emitError("Referencing field '" + jsniRef.className() + "."
+ jsniRef.memberName() + "': unable to resolve field");
return null;
}
if (target.isDeprecated()) {
emitWarning("deprecation",
"Referencing deprecated field '" + jsniRef.className() + "."
+ jsniRef.memberName() + "'");
}
if (isLvalue && target.constant() != Constant.NotAConstant) {
emitError("Illegal assignment to compile-time constant '"
+ jsniRef.className() + "." + jsniRef.memberName() + "'");
}
if (target.isStatic() && hasQualifier) {
emitError("Unnecessary qualifier on static field '"
+ jsniRef.className() + "." + jsniRef.memberName() + "'");
} else if (!target.isStatic() && !hasQualifier) {
emitError("Missing qualifier on instance field '" + jsniRef.className()
+ "." + jsniRef.memberName() + "'");
}
if (hasUnsafeLongsAnnotation) {
return target;
}
if (containsLong(target.type)) {
emitError("Referencing field '" + jsniRef.className() + "."
+ jsniRef.memberName() + "': type '" + typeString(target.type)
+ "' is not safe to access in JSNI code");
}
return target;
}
private MethodBinding checkMethodRef(ReferenceBinding clazz,
JsniRef jsniRef, boolean hasQualifier, boolean isLvalue) {
assert jsniRef.isMethod();
MethodBinding target = getMethod(clazz, jsniRef);
if (target == null) {
emitError("Referencing method '" + jsniRef.className() + "."
+ jsniRef.memberSignature() + "': unable to resolve method");
return null;
}
if (target.isDeprecated()) {
emitWarning("deprecation",
"Referencing deprecated method '" + jsniRef.className() + "."
+ jsniRef.memberName() + "'");
}
if (isLvalue) {
emitError("Illegal assignment to method '" + jsniRef.className() + "."
+ jsniRef.memberName() + "'");
}
boolean needsQualifer = !target.isStatic() && !target.isConstructor();
if (!needsQualifer && hasQualifier) {
emitError("Unnecessary qualifier on static method '"
+ jsniRef.className() + "." + jsniRef.memberName() + "'");
} else if (needsQualifer && !hasQualifier) {
emitError("Missing qualifier on instance method '"
+ jsniRef.className() + "." + jsniRef.memberName() + "'");
}
if (!target.isStatic() && JSORestrictionsChecker.isJso(clazz)) {
emitError("Referencing method '" + jsniRef.className() + "."
+ jsniRef.memberSignature()
+ "': references to instance methods in overlay types are illegal");
}
if (checkerState.isJsoInterface(clazz)) {
String implementor = checkerState.getJsoImplementor(clazz);
emitError("Referencing interface method '" + jsniRef.className() + "."
+ jsniRef.memberSignature() + "': implemented by '" + implementor
+ "'; references to instance methods in overlay types are illegal"
+ "; use a stronger type or a Java trampoline method");
}
if (hasUnsafeLongsAnnotation) {
return target;
}
if (containsLong(target.returnType)) {
emitError("Referencing method '" + jsniRef.className() + "."
+ jsniRef.memberName() + "': return type '"
+ typeString(target.returnType)
+ "' is not safe to access in JSNI code");
}
if (target.parameters != null) {
int i = 0;
for (TypeBinding paramType : target.parameters) {
++i;
if (containsLong(paramType)) {
// It would be nice to print the parameter name, but how to find it?
emitError("Parameter " + i + " of method '" + jsniRef.className()
+ "." + jsniRef.memberName() + "': type '"
+ typeString(paramType)
+ "' may not be passed out of JSNI code");
}
}
}
return target;
}
private Binding checkRef(JsniRef jsniRef, boolean hasQualifier,
boolean isLvalue) {
String className = jsniRef.className();
if ("null".equals(className)) {
if (jsniRef.isField()) {
if (!"nullField".equals(jsniRef.memberName())) {
emitError("Referencing field '" + jsniRef.className() + "."
+ jsniRef.memberName()
+ "': 'nullField' is the only legal field reference for 'null'");
}
} else {
if (!"nullMethod()".equals(jsniRef.memberSignature())) {
emitError("Referencing method '" + jsniRef.className() + "."
+ jsniRef.memberSignature()
+ "': 'nullMethod()' is the only legal method for 'null'");
}
return null;
}
return null;
}
boolean isArray = false;
int dims = 0;
while (className.endsWith("[]")) {
++dims;
isArray = true;
className = className.substring(0, className.length() - 2);
}
boolean isPrimitive;
ReferenceBinding clazz;
TypeBinding binding = method.scope.getBaseType(className.toCharArray());
if (binding != null) {
isPrimitive = true;
clazz = null;
} else {
isPrimitive = false;
binding = clazz = findClass(className);
}
// TODO(deprecation): remove this support eventually.
if (binding == null && className.length() == 1
&& "ZBCDFIJSV".indexOf(className.charAt(0)) >= 0) {
isPrimitive = true;
binding = getTypeBinding(className.charAt(0));
assert binding != null;
JsniCollector.reportJsniWarning(
errorInfo,
method,
"Referencing primitive type '" + className
+ "': this is deprecated, use '"
+ String.valueOf(binding.sourceName()) + "' instead");
}
if ((binding == null && looksLikeAnonymousClass(jsniRef))
|| (binding != null && binding.isAnonymousType())) {
emitError("Referencing class '" + className
+ "': JSNI references to anonymous classes are illegal");
return null;
} else if (binding == null) {
emitError("Referencing class '" + className
+ "': unable to resolve class");
return null;
}
if (clazz != null && clazz.isDeprecated()) {
emitWarning("deprecation", "Referencing deprecated class '" + className
+ "'");
}
if (jsniRef.isField() && "class".equals(jsniRef.memberName())) {
if (isLvalue) {
emitError("Illegal assignment to class literal '"
+ jsniRef.className() + ".class'");
return null;
}
// Reference to the class itself.
if (isArray) {
return method.scope.createArrayType(binding, dims);
} else {
return binding;
}
}
if (isArray || isPrimitive) {
emitError("Referencing member '" + jsniRef.className() + "."
+ jsniRef.memberName()
+ "': 'class' is the only legal reference for "
+ (isArray ? "array" : "primitive") + " types");
return null;
}
assert clazz != null;
if (jsniRef.isMethod()) {
return checkMethodRef(clazz, jsniRef, hasQualifier, isLvalue);
} else {
return checkFieldRef(clazz, jsniRef, hasQualifier, isLvalue);
}
}
private void emitError(String msg) {
JsniCollector.reportJsniError(errorInfo, method, msg);
}
private void emitWarning(String category, String msg) {
for (Set<String> suppressWarnings : suppressWarningsStack) {
if (suppressWarnings.contains(category)
|| suppressWarnings.contains("all")) {
return;
}
}
JsniCollector.reportJsniWarning(errorInfo, method, msg);
}
private ReferenceBinding findClass(String className) {
ReferenceBinding binding = typeResolver.resolveType(className);
assert !(binding instanceof ProblemReferenceBinding);
assert !(binding instanceof UnresolvedReferenceBinding);
return binding;
}
private char[][] getCompoundName(JsniRef jsniRef) {
String className = jsniRef.className().replace('$', '.');
char[][] compoundName = CharOperation.splitOn('.',
className.toCharArray());
return compoundName;
}
private FieldBinding getField(ReferenceBinding clazz, JsniRef jsniRef) {
assert jsniRef.isField();
return clazz.getField(jsniRef.memberName().toCharArray(), false);
}
private MethodBinding getMethod(ReferenceBinding clazz, JsniRef jsniRef) {
assert jsniRef.isMethod();
String methodName = jsniRef.memberName();
if ("new".equals(methodName)) {
for (MethodBinding findMethod : clazz.getMethods(INIT_CTOR_CHARS)) {
StringBuilder methodSig = new StringBuilder();
if (clazz instanceof NestedTypeBinding) {
// Match synthetic args for enclosing instances.
NestedTypeBinding nestedBinding = (NestedTypeBinding) clazz;
if (nestedBinding.enclosingInstances != null) {
for (int i = 0; i < nestedBinding.enclosingInstances.length; ++i) {
SyntheticArgumentBinding arg = nestedBinding.enclosingInstances[i];
methodSig.append(arg.type.signature());
}
}
}
if (findMethod.parameters != null) {
for (TypeBinding binding : findMethod.parameters) {
methodSig.append(binding.signature());
}
}
if (methodSig.toString().equals(jsniRef.paramTypesString())) {
return findMethod;
}
}
} else {
Queue<ReferenceBinding> work = new LinkedList<ReferenceBinding>();
work.add(clazz);
while (!work.isEmpty()) {
clazz = work.remove();
for (MethodBinding findMethod : clazz.getMethods(methodName.toCharArray())) {
if (paramTypesMatch(findMethod, jsniRef)) {
return findMethod;
}
}
ReferenceBinding[] superInterfaces = clazz.superInterfaces();
if (superInterfaces != null) {
work.addAll(Arrays.asList(superInterfaces));
}
ReferenceBinding superclass = clazz.superclass();
if (superclass != null) {
work.add(superclass);
}
}
}
return null;
}
@Deprecated
private TypeBinding getTypeBinding(char c) {
switch (c) {
case 'I':
return TypeBinding.INT;
case 'Z':
return TypeBinding.BOOLEAN;
case 'V':
return TypeBinding.VOID;
case 'C':
return TypeBinding.CHAR;
case 'D':
return TypeBinding.DOUBLE;
case 'B':
return TypeBinding.BYTE;
case 'F':
return TypeBinding.FLOAT;
case 'J':
return TypeBinding.LONG;
case 'S':
return TypeBinding.SHORT;
default:
return null;
}
}
private boolean looksLikeAnonymousClass(JsniRef jsniRef) {
char[][] compoundName = getCompoundName(jsniRef);
for (char[] part : compoundName) {
if (Character.isDigit(part[0])) {
return true;
}
}
return false;
}
private boolean paramTypesMatch(MethodBinding method, JsniRef jsniRef) {
if (jsniRef.matchesAnyOverload()) {
return true;
}
StringBuilder methodSig = new StringBuilder();
if (method.parameters != null) {
for (TypeBinding binding : method.parameters) {
methodSig.append(binding.signature());
}
}
return methodSig.toString().equals(jsniRef.paramTypesString());
}
private String typeString(TypeBinding type) {
return String.valueOf(type.shortReadableName());
}
}
private static final char[] INIT_CTOR_CHARS = "<init>".toCharArray();
private static final char[][] UNSAFE_LONG_ANNOTATION_CHARS = CharOperation.splitOn(
'.', UnsafeNativeLong.class.getName().toCharArray());
/**
* Checks an entire
* {@link org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration}.
*
*/
public static void check(CompilationUnitDeclaration cud,
CheckerState checkerState,
Map<MethodDeclaration, JsniMethod> jsniMethods,
Map<String, Binding> jsniRefs, TypeResolver typeResolver) {
new JsniChecker(cud, checkerState, typeResolver, jsniMethods, jsniRefs).check();
}
static Set<String> getSuppressedWarnings(Annotation[] annotations) {
if (annotations != null) {
for (Annotation a : annotations) {
if (SuppressWarnings.class.getName().equals(
CharOperation.toString(((ReferenceBinding) a.resolvedType).compoundName))) {
for (MemberValuePair pair : a.memberValuePairs()) {
if (String.valueOf(pair.name).equals("value")) {
Expression valueExpr = pair.value;
if (valueExpr instanceof StringLiteral) {
// @SuppressWarnings("Foo")
return Sets.create(((StringLiteral) valueExpr).constant.stringValue().toLowerCase(
Locale.ENGLISH));
} else if (valueExpr instanceof ArrayInitializer) {
// @SuppressWarnings({ "Foo", "Bar"})
ArrayInitializer ai = (ArrayInitializer) valueExpr;
String[] values = new String[ai.expressions.length];
for (int i = 0, j = values.length; i < j; i++) {
values[i] = ((StringLiteral) ai.expressions[i]).constant.stringValue().toLowerCase(
Locale.ENGLISH);
}
return Sets.create(values);
} else {
throw new InternalCompilerException(
"Unable to analyze SuppressWarnings annotation");
}
}
}
}
}
}
return Sets.create();
}
private final CheckerState checkerState;
private final CompilationUnitDeclaration cud;
private final Map<MethodDeclaration, JsniMethod> jsniMethods;
private final Map<String, Binding> jsniRefs;
private final Stack<Set<String>> suppressWarningsStack = new Stack<Set<String>>();
private final TypeResolver typeResolver;
private JsniChecker(CompilationUnitDeclaration cud,
CheckerState checkerState, TypeResolver typeResolver,
Map<MethodDeclaration, JsniMethod> jsniMethods,
Map<String, Binding> jsniRefs) {
this.checkerState = checkerState;
this.cud = cud;
this.typeResolver = typeResolver;
this.jsniMethods = jsniMethods;
this.jsniRefs = jsniRefs;
}
private void check() {
// First check the declarations.
cud.traverse(new JsniDeclChecker(), cud.scope);
}
/**
* Check whether the argument type is the <code>long</code> primitive type. If
* the argument is <code>null</code>, returns <code>false</code>.
*/
private boolean containsLong(TypeBinding type) {
if (type instanceof BaseTypeBinding) {
BaseTypeBinding btb = (BaseTypeBinding) type;
if (btb.id == TypeIds.T_long) {
return true;
}
}
return false;
}
private boolean hasUnsafeLongsAnnotation(MethodDeclaration meth,
ClassScope scope) {
if (meth.annotations != null) {
for (Annotation annot : meth.annotations) {
if (isUnsafeLongAnnotation(annot, scope)) {
return true;
}
}
}
return false;
}
private boolean isUnsafeLongAnnotation(Annotation annot, ClassScope scope) {
if (annot.type != null) {
TypeBinding resolved = annot.type.resolveType(scope);
if (resolved != null) {
if (resolved instanceof ReferenceBinding) {
ReferenceBinding rb = (ReferenceBinding) resolved;
if (CharOperation.equals(rb.compoundName,
UNSAFE_LONG_ANNOTATION_CHARS)) {
return true;
}
}
}
}
return false;
}
private void longAccessError(ASTNode node, String message) {
GWTProblem.recordError(node, cud, message, new InstalledHelpInfo(
"longJsniRestriction.html"));
}
}