/*
* Copyright 2009 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.impl.ArtificialRescue;
import com.google.gwt.dev.jdt.SafeASTVisitor;
import com.google.gwt.dev.util.Empty;
import com.google.gwt.dev.util.JsniRef;
import com.google.gwt.dev.util.collect.Lists;
import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.compiler.ast.Annotation;
import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
import org.eclipse.jdt.internal.compiler.impl.BooleanConstant;
import org.eclipse.jdt.internal.compiler.impl.StringConstant;
import org.eclipse.jdt.internal.compiler.lookup.AnnotationBinding;
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.ElementValuePair;
import org.eclipse.jdt.internal.compiler.lookup.FieldBinding;
import org.eclipse.jdt.internal.compiler.lookup.MethodBinding;
import org.eclipse.jdt.internal.compiler.lookup.ProblemReasons;
import org.eclipse.jdt.internal.compiler.lookup.ProblemReferenceBinding;
import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;
import org.eclipse.jdt.internal.compiler.lookup.TypeBinding;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Checks the validity of ArtificialRescue annotations.
*
* <ul>
* <li>(1) The ArtificialRescue annotation is only used in generated code.</li>
* <li>(2) The className value names a type known to GWT.</li>
* <li>(3) The methods and fields of the type are known to GWT.</li>
* </ul>
*/
public abstract class ArtificialRescueChecker {
/**
* Represents a single
* {@link com.google.gwt.core.client.impl.ArtificialRescue.Rescue}.
* <p>
* Only public so it can be used by
* {@link com.google.gwt.dev.jjs.impl.GenerateJavaAST}.
*/
public static class RescueData {
private static final RescueData[] EMPTY_RESCUEDATA = new RescueData[0];
public static RescueData[] createFromAnnotations(Annotation[] annotations) {
RescueData[] result = EMPTY_RESCUEDATA;
for (Annotation a : annotations) {
ReferenceBinding binding = (ReferenceBinding) a.resolvedType;
String name = CharOperation.toString(binding.compoundName);
if (!name.equals(ArtificialRescue.class.getName())) {
continue;
}
return createFromArtificialRescue(a);
}
return result;
}
public static RescueData[] createFromArtificialRescue(Annotation artificialRescue) {
Object[] values = null;
RescueData[] result = EMPTY_RESCUEDATA;
for (ElementValuePair pair : artificialRescue.computeElementValuePairs()) {
if ("value".equals(String.valueOf(pair.getName()))) {
Object value = pair.getValue();
if (value instanceof AnnotationBinding) {
values = new Object[]{value};
} else {
values = (Object[]) value;
}
break;
}
}
assert values != null;
if (values.length > 0) {
result = new RescueData[values.length];
for (int i = 0; i < result.length; ++i) {
result[i] = createFromRescue((AnnotationBinding) values[i]);
}
}
return result;
}
private static RescueData createFromRescue(AnnotationBinding rescue) {
String className = null;
boolean instantiable = false;
String[] methods = Empty.STRINGS;
String[] fields = Empty.STRINGS;
for (ElementValuePair pair : rescue.getElementValuePairs()) {
String name = String.valueOf(pair.getName());
if ("className".equals(name)) {
className = ((StringConstant) pair.getValue()).stringValue();
} else if ("instantiable".equals(name)) {
BooleanConstant value = (BooleanConstant) pair.getValue();
instantiable = value.booleanValue();
} else if ("methods".equals(name)) {
methods = getValueAsStringArray(pair.getValue());
} else if ("fields".equals(name)) {
fields = getValueAsStringArray(pair.getValue());
} else {
assert false : "Unknown ArtificialRescue field";
}
}
assert className != null;
return new RescueData(className, instantiable, fields, methods);
}
private static String[] getValueAsStringArray(Object value) {
if (value instanceof StringConstant) {
return new String[]{((StringConstant) value).stringValue()};
}
Object[] values = (Object[]) value;
String[] toReturn = new String[values.length];
for (int i = 0; i < toReturn.length; ++i) {
toReturn[i] = ((StringConstant) values[i]).stringValue();
}
return toReturn;
}
private final String className;
private final String[] fields;
private final boolean instantiable;
private final String[] methods;
RescueData(String className, boolean instantiable, String[] fields, String[] methods) {
this.className = className;
this.instantiable = instantiable;
this.methods = methods;
this.fields = fields;
}
public String getClassName() {
return className;
}
public String[] getFields() {
return fields;
}
public String[] getMethods() {
return methods;
}
public boolean isInstantiable() {
return instantiable;
}
}
/**
* Checks that references are legal and resolve to a known element. Collects
* the rescued elements per type for later user.
*/
private static class Checker extends ArtificialRescueChecker {
private final Map<TypeDeclaration, Binding[]> artificialRescues;
private transient List<Binding> currentBindings = new ArrayList<Binding>();
public Checker(CompilationUnitDeclaration cud, Map<TypeDeclaration, Binding[]> artificialRescues) {
super(cud);
this.artificialRescues = artificialRescues;
}
@Override
protected void processRescue(RescueData rescue) {
String className = rescue.getClassName();
// Strip off any array-like extensions and just find base type
int arrayDims = 0;
while (className.endsWith("[]")) {
className = className.substring(0, className.length() - 2);
++arrayDims;
}
// Goal (2) The className value names a type known to GWT.
char[][] compoundName = CharOperation.splitOn('.', className.toCharArray());
TypeBinding typeBinding = cud.scope.getType(compoundName, compoundName.length);
if (typeBinding == null) {
error(notFound(className));
return;
}
if (typeBinding instanceof ProblemReferenceBinding) {
ProblemReferenceBinding problem = (ProblemReferenceBinding) typeBinding;
if (problem.problemId() == ProblemReasons.NotVisible) {
// Ignore
} else if (problem.problemId() == ProblemReasons.NotFound) {
error(notFound(className));
} else {
error(unknownProblem(className, problem));
}
return;
}
if (arrayDims > 0) {
typeBinding = cud.scope.createArrayType(typeBinding, arrayDims);
}
if (rescue.isInstantiable()) {
currentBindings.add(typeBinding);
}
if (typeBinding instanceof BaseTypeBinding || arrayDims > 0) {
// No methods or fields on primitive types or array types (3)
if (rescue.getMethods().length > 0) {
error(noMethodsAllowed());
}
if (rescue.getFields().length > 0) {
error(noFieldsAllowed());
}
return;
}
// Goal (3) The methods and fields of the type are known to GWT.
ReferenceBinding ref = (ReferenceBinding) typeBinding;
for (String field : rescue.getFields()) {
FieldBinding fieldBinding = ref.getField(field.toCharArray(), false);
if (fieldBinding == null) {
error(unknownField(field));
} else {
currentBindings.add(fieldBinding);
}
}
for (String method : rescue.getMethods()) {
if (method.contains("@")) {
error(nameAndTypesOnly());
continue;
}
// Method signatures use the same format as JSNI method refs.
JsniRef jsni = JsniRef.parse("@foo::" + method);
if (jsni == null) {
error(badMethodSignature(method));
continue;
}
MethodBinding[] methodBindings;
if (jsni.memberName().equals(String.valueOf(ref.compoundName[ref.compoundName.length - 1]))) {
// Constructor
methodBindings = ref.getMethods("<init>".toCharArray());
} else {
methodBindings = ref.getMethods(jsni.memberName().toCharArray());
}
boolean found = false;
for (MethodBinding methodBinding : methodBindings) {
if (jsni.matchesAnyOverload() || jsni.paramTypesString().equals(sig(methodBinding))) {
currentBindings.add(methodBinding);
found = true;
}
}
if (!found) {
error(noMethod(className, jsni.memberSignature()));
continue;
}
}
}
@Override
protected void processType(TypeDeclaration x) {
super.processType(x);
if (currentBindings.size() > 0) {
Binding[] result = currentBindings.toArray(new Binding[currentBindings.size()]);
artificialRescues.put(x, result);
currentBindings = new ArrayList<Binding>();
}
}
private String sig(MethodBinding methodBinding) {
StringBuilder sb = new StringBuilder();
for (TypeBinding paramType : methodBinding.parameters) {
sb.append(paramType.signature());
}
return sb.toString();
}
}
/**
* Collects only the names of the rescued types; does not report errors.
*/
private static class Collector extends ArtificialRescueChecker {
private final List<String> referencedTypes;
public Collector(CompilationUnitDeclaration cud, List<String> referencedTypes) {
super(cud);
this.referencedTypes = referencedTypes;
}
@Override
protected void processRescue(RescueData rescue) {
String className = rescue.getClassName();
while (className.endsWith("[]")) {
className = className.substring(0, className.length() - 2);
}
referencedTypes.add(className);
}
}
/**
* Records an error if artificial rescues are used at all.
*/
private static class Disallowed extends ArtificialRescueChecker {
public Disallowed(CompilationUnitDeclaration cud) {
super(cud);
}
@Override
protected void processRescue(RescueData rescue) {
// Goal (1) ArtificialRescue annotation is only used in generated code.
error(onlyGeneratedCode());
}
}
private class Visitor extends SafeASTVisitor {
@Override
public void endVisit(TypeDeclaration memberTypeDeclaration, ClassScope scope) {
processType(memberTypeDeclaration);
}
@Override
public void endVisit(TypeDeclaration typeDeclaration, CompilationUnitScope scope) {
processType(typeDeclaration);
}
@Override
public void endVisitValid(TypeDeclaration localTypeDeclaration, BlockScope scope) {
processType(localTypeDeclaration);
}
}
/**
* Check the {@link ArtificialRescue} annotations in a CompilationUnit. Errors
* are reported through {@link GWTProblem}.
*/
public static void check(CompilationUnitDeclaration cud, boolean allowArtificialRescue,
Map<TypeDeclaration, Binding[]> artificialRescues) {
if (allowArtificialRescue) {
new Checker(cud, artificialRescues).exec();
} else {
new Disallowed(cud).exec();
}
}
/**
* Report all types named in {@link ArtificialRescue} annotations in a CUD. No
* error checking is done.
*/
public static List<String> collectReferencedTypes(CompilationUnitDeclaration cud) {
ArrayList<String> result = new ArrayList<String>();
new Collector(cud, result).exec();
return Lists.normalize(result);
}
static String badMethodSignature(String method) {
return "Bad method signature " + method;
}
static String nameAndTypesOnly() {
return "Only method name and parameter types expected";
}
static String noFieldsAllowed() {
return "Cannot refer to fields on array or primitive types";
}
static String noMethod(String className, String methodSig) {
return "No method " + methodSig + " in type " + className;
}
static String noMethodsAllowed() {
return "Cannot refer to methods on array or primitive types";
}
static String notFound(String className) {
return "Could not find type " + className;
}
static String onlyGeneratedCode() {
return "The " + ArtificialRescue.class.getName()
+ " annotation may only be used in generated code and its use"
+ " by third parties is not supported.";
}
static String unknownField(String field) {
return "Unknown field " + field;
}
static String unknownProblem(String className, ProblemReferenceBinding problem) {
return "Unknown problem: " + ProblemReferenceBinding.problemReasonString(problem.problemId())
+ " " + className;
}
protected final CompilationUnitDeclaration cud;
private Annotation errorNode;
private ArtificialRescueChecker(CompilationUnitDeclaration cud) {
this.cud = cud;
}
protected final void error(String msg) {
GWTProblem.recordError(errorNode, cud, msg, null);
}
protected final void exec() {
cud.traverse(new Visitor(), cud.scope);
}
protected abstract void processRescue(RescueData rescue);
/**
* Examine a TypeDeclaration for ArtificialRescue annotations.
*/
protected void processType(TypeDeclaration x) {
if (x.annotations == null) {
return;
}
for (Annotation a : x.annotations) {
ReferenceBinding binding = (ReferenceBinding) a.resolvedType;
String name = CharOperation.toString(binding.compoundName);
if (!name.equals(ArtificialRescue.class.getName())) {
continue;
}
errorNode = a;
RescueData[] rescues = RescueData.createFromArtificialRescue(a);
for (RescueData rescue : rescues) {
processRescue(rescue);
}
break;
}
}
}