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