diff options
Diffstat (limited to 'src/lombok/core')
-rw-r--r-- | src/lombok/core/AST.java | 268 | ||||
-rw-r--r-- | src/lombok/core/AnnotationValues.java | 281 | ||||
-rw-r--r-- | src/lombok/core/TypeResolver.java | 81 |
3 files changed, 630 insertions, 0 deletions
diff --git a/src/lombok/core/AST.java b/src/lombok/core/AST.java new file mode 100644 index 00000000..99902672 --- /dev/null +++ b/src/lombok/core/AST.java @@ -0,0 +1,268 @@ +package lombok.core; + +import static lombok.Lombok.sneakyThrow; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +public abstract class AST<N> { + public enum Kind { + COMPILATION_UNIT, TYPE, FIELD, INITIALIZER, METHOD, ANNOTATION, ARGUMENT, LOCAL, STATEMENT; + } + + private Node top; + private final String fileName; + private Map<N, Void> identityDetector = new IdentityHashMap<N, Void>(); + private Map<N, Node> nodeMap = new IdentityHashMap<N, Node>(); + + protected AST(String fileName) { + this.fileName = fileName == null ? "(unknown).java" : fileName; + } + + protected void setTop(Node top) { + this.top = top; + } + + public abstract String getPackageDeclaration(); + + public abstract Collection<String> getImportStatements(); + + protected <T extends Node> T putInMap(T parent) { + nodeMap.put(parent.get(), parent); + identityDetector.put(parent.get(), null); + return parent; + } + + protected Map<N, Node> getNodeMap() { + return nodeMap; + } + + protected void clearState() { + identityDetector = new IdentityHashMap<N, Void>(); + nodeMap = new IdentityHashMap<N, Node>(); + } + + protected boolean alreadyHandled(N node) { + return identityDetector.containsKey(node); + } + + protected void setAsHandled(N node) { + identityDetector.put(node, null); + } + + public String getFileName() { + return fileName; + } + + public Node top() { + return top; + } + + public Node get(N node) { + return nodeMap.get(node); + } + + public abstract class Node { + protected final Kind kind; + protected final N node; + protected final Collection<? extends Node> children; + protected Node parent; + protected boolean handled; + protected boolean isStructurallySignificant; + + protected Node(N node, Collection<? extends Node> children, Kind kind) { + this.kind = kind; + this.node = node; + this.children = children == null ? Collections.<Node>emptyList() : children; + for ( Node child : this.children ) child.parent = this; + this.isStructurallySignificant = calculateIsStructurallySignificant(); + } + + public String getPackageDeclaration() { + return AST.this.getPackageDeclaration(); + } + + public Collection<String> getImportStatements() { + return AST.this.getImportStatements(); + } + + protected abstract boolean calculateIsStructurallySignificant(); + + public N get() { + return node; + } + + public Kind getKind() { + return kind; + } + + /** + * Return the name of your type (simple name), method, field, or local variable. Return null if this + * node doesn't really have a name, such as initializers, while statements, etc. + */ + public abstract String getName(); + + /** Returns the structurally significant node that encloses this one. + * + * @see #isStructurallySignificant() + */ + public Node up() { + Node result = (Node)parent; + while ( result != null && !result.isStructurallySignificant ) result = (Node)result.parent; + return result; + } + + /** + * Returns the direct parent node in the AST tree of this node. For example, a local variable declaration's + * direct parent can be e.g. an If block, but its up() Node is the Method that contains it. + */ + public Node directUp() { + return parent; + } + + public Collection<? extends Node> down() { + return children; + } + + public boolean isHandled() { + return handled; + } + + public Node setHandled() { + this.handled = true; + return this; + } + + public Node top() { + return top; + } + + public String getFileName() { + return fileName; + } + + public abstract void addError(String message); + + public abstract void addWarning(String message); + + /** + * Structurally significant means: LocalDeclaration, TypeDeclaration, MethodDeclaration, ConstructorDeclaration, + * FieldDeclaration, Initializer, and CompilationUnitDeclaration. + * The rest is e.g. if statements, while loops, etc. + */ + public boolean isStructurallySignificant() { + return isStructurallySignificant; + } + } + + protected static class FieldAccess { + public final Field field; + public final int dim; + + FieldAccess(Field field, int dim) { + this.field = field; + this.dim = dim; + } + } + + private static Map<Class<?>, Collection<FieldAccess>> fieldsOfASTClasses = new HashMap<Class<?>, Collection<FieldAccess>>(); + protected Collection<FieldAccess> fieldsOf(Class<?> c) { + Collection<FieldAccess> fields = fieldsOfASTClasses.get(c); + if ( fields != null ) return fields; + + fields = new ArrayList<FieldAccess>(); + getFields(c, fields); + fieldsOfASTClasses.put(c, fields); + return fields; + } + + private void getFields(Class<?> c, Collection<FieldAccess> fields) { + if ( c == Object.class || c == null ) return; + for ( Field f : c.getDeclaredFields() ) { + if ( Modifier.isStatic(f.getModifiers()) ) continue; + Class<?> t = f.getType(); + int dim = 0; + + if ( t.isArray() ) { + while ( t.isArray() ) { + dim++; + t = t.getComponentType(); + } + } else if ( Collection.class.isAssignableFrom(t) ) { + while ( Collection.class.isAssignableFrom(t) ) { + dim++; + t = getComponentType(f.getGenericType()); + } + } + + for ( Class<?> statementType : getStatementTypes() ) { + if ( statementType.isAssignableFrom(t) ) { + f.setAccessible(true); + fields.add(new FieldAccess(f, dim)); + break; + } + } + } + getFields(c.getSuperclass(), fields); + } + + private Class<?> getComponentType(Type type) { + if ( type instanceof ParameterizedType ) { + Type component = ((ParameterizedType)type).getActualTypeArguments()[0]; + return component instanceof Class<?> ? (Class<?>)component : Object.class; + } else return Object.class; + } + + protected abstract Collection<Class<? extends N>> getStatementTypes(); + + protected <T extends Node> Collection<T> buildWithField(Class<T> nodeType, N statement, FieldAccess fa) { + List<T> list = new ArrayList<T>(); + buildWithField0(nodeType, statement, fa, list); + return list; + } + + private <T extends Node> void buildWithField0(Class<T> nodeType, N child, FieldAccess fa, Collection<T> list) { + try { + Object o = fa.field.get(child); + if ( o == null ) return; + if ( fa.dim == 0 ) { + Node node = buildStatement(o); + if ( node != null ) list.add(nodeType.cast(node)); + } else if ( o.getClass().isArray() ) buildWithArray(nodeType, o, list, fa.dim); + else if ( Collection.class.isInstance(o) ) buildWithCollection(nodeType, o, list, fa.dim); + } catch ( IllegalAccessException e ) { + sneakyThrow(e); + } + } + + private <T extends Node> void buildWithArray(Class<T> nodeType, Object array, Collection<T> list, int dim) { + if ( dim == 1 ) for ( Object v : (Object[])array ) { + if ( v == null ) continue; + Node node = buildStatement(v); + if ( node != null ) list.add(nodeType.cast(node)); + } else for ( Object v : (Object[])array ) { + buildWithArray(nodeType, v, list, dim-1); + } + } + + private <T extends Node> void buildWithCollection(Class<T> nodeType, Object collection, Collection<T> list, int dim) { + if ( dim == 1 ) for ( Object v : (Collection<?>)collection ) { + if ( v == null ) continue; + Node node = buildStatement(v); + if ( node != null ) list.add(nodeType.cast(node)); + } else for ( Object v : (Collection<?>)collection ) { + buildWithCollection(nodeType, v, list, dim-1); + } + } + + protected abstract Node buildStatement(Object statement); +} diff --git a/src/lombok/core/AnnotationValues.java b/src/lombok/core/AnnotationValues.java new file mode 100644 index 00000000..ec17e34f --- /dev/null +++ b/src/lombok/core/AnnotationValues.java @@ -0,0 +1,281 @@ +package lombok.core; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class AnnotationValues<A extends Annotation> { + private final Class<A> type; + private final Map<String, AnnotationValue> values; + private final AST<?>.Node ast; + + public static class AnnotationValue { + public final List<String> raws; + public final List<Object> valueGuesses; + private final AST<?>.Node node; + + /** + * 'raw' should be the exact expression, for example '5+7', 'AccessLevel.PUBLIC', or 'int.class'. + * 'valueGuess' should be a likely guess at the real value intended. + * + * For classes, supply the class name (qualified or not) as a string.<br /> + * For enums, supply the simple name part (everything after the last dot) as a string.<br /> + */ + public AnnotationValue(AST<?>.Node node, String raw, Object valueGuess) { + this.node = node; + this.raws = Collections.singletonList(raw); + this.valueGuesses = Collections.singletonList(valueGuess); + } + + /** When the value is an array type. */ + public AnnotationValue(AST<?>.Node node, List<String> raws, List<Object> valueGuesses) { + this.node = node; + this.raws = raws; + this.valueGuesses = valueGuesses; + } + + /** + * Override this if you want more specific behaviour (e.g. get the source position just right). + * + * @param message English message with the problem. + * @param valueIdx The index into the values for this annotation key that caused the problem. + * -1 for a problem that applies to all values, otherwise the 0-based index into an array of values. + * If there is no array for this value (e.g. value=1 instead of value={1,2}), then always -1 or 0. + */ + public void setError(String message, int valueIdx) { + node.addError(message); + } + } + + public AnnotationValues(Class<A> type, Map<String, AnnotationValue> values, AST<?>.Node ast) { + this.type = type; + this.values = values; + this.ast = ast; + } + + public static class AnnotationValueDecodeFail extends RuntimeException { + private static final long serialVersionUID = 1L; + + public final int idx; + public final AnnotationValue owner; + + public AnnotationValueDecodeFail(AnnotationValue owner, String msg, int idx) { + super(msg); + this.idx = idx; + this.owner = owner; + } + } + + private static AnnotationValueDecodeFail makeNoDefaultFail(AnnotationValue owner, Method method) { + return new AnnotationValueDecodeFail(owner, + "No value supplied but " + method.getName() + " has no default either.", -1); + } + + @SuppressWarnings("unchecked") + public A getInstance() throws AnnotationValueDecodeFail { + InvocationHandler invocations = new InvocationHandler() { + @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + AnnotationValue v = values.get(method.getName()); + if ( v == null ) { + Object defaultValue = method.getDefaultValue(); + if ( defaultValue != null ) return defaultValue; + throw makeNoDefaultFail(v, method); + } + + boolean isArray = false; + Class<?> expected = method.getReturnType(); + Object array = null; + if ( expected.isArray() ) { + isArray = true; + expected = expected.getComponentType(); + array = Array.newInstance(expected, 1); + } + + if ( !isArray && v.valueGuesses.size() > 1 ) { + System.out.println(v.valueGuesses.size() + ": " + v.valueGuesses); + throw new AnnotationValueDecodeFail(v, + "Expected a single value, but " + method.getName() + " has an array of values", -1); + } + + if ( v.valueGuesses.size() == 0 && !isArray ) { + Object defaultValue = method.getDefaultValue(); + if ( defaultValue == null ) throw makeNoDefaultFail(v, method); + return defaultValue; + } + + int idx = 0; + for ( Object guess : v.valueGuesses ) { + Object result = guess == null ? null : guessToType(guess, expected, v, idx); + if ( !isArray ) { + if ( result == null ) { + Object defaultValue = method.getDefaultValue(); + if ( defaultValue == null ) throw makeNoDefaultFail(v, method); + return defaultValue; + } else return result; + } else { + if ( result == null ) { + if ( v.valueGuesses.size() == 1 ) { + Object defaultValue = method.getDefaultValue(); + if ( defaultValue == null ) throw makeNoDefaultFail(v, method); + return defaultValue; + } else throw new AnnotationValueDecodeFail(v, + "I can't make sense of this annotation value. Try using a fully qualified literal.", idx); + } + Array.set(array, idx++, result); + } + } + + return array; + } + }; + + return (A) Proxy.newProxyInstance(type.getClassLoader(), new Class[] { type }, invocations); + } + + private Object guessToType(Object guess, Class<?> expected, AnnotationValue v, int pos) { + if ( expected == int.class ) { + if ( guess instanceof Integer || guess instanceof Short || guess instanceof Byte ) { + return ((Number)guess).intValue(); + } + } + + if ( expected == long.class ) { + if ( guess instanceof Long || guess instanceof Integer || guess instanceof Short || guess instanceof Byte ) { + return ((Number)guess).longValue(); + } + } + + if ( expected == short.class ) { + if ( guess instanceof Integer || guess instanceof Short || guess instanceof Byte ) { + int intVal = ((Number)guess).intValue(); + int shortVal = ((Number)guess).shortValue(); + if ( shortVal == intVal ) return shortVal; + } + } + + if ( expected == byte.class ) { + if ( guess instanceof Integer || guess instanceof Short || guess instanceof Byte ) { + int intVal = ((Number)guess).intValue(); + int byteVal = ((Number)guess).byteValue(); + if ( byteVal == intVal ) return byteVal; + } + } + + if ( expected == double.class ) { + if ( guess instanceof Number ) return ((Number)guess).doubleValue(); + } + + if ( expected == float.class ) { + if ( guess instanceof Number ) return ((Number)guess).floatValue(); + } + + if ( expected == boolean.class ) { + if ( guess instanceof Boolean ) return ((Boolean)guess).booleanValue(); + } + + if ( expected == char.class ) { + if ( guess instanceof Character ) return ((Character)guess).charValue(); + } + + if ( expected == String.class ) { + if ( guess instanceof String ) return expected; + } + + if ( Enum.class.isAssignableFrom(expected) ) { + if ( guess instanceof String ) { + for ( Object enumConstant : expected.getEnumConstants() ) { + String target = ((Enum<?>)enumConstant).name(); + if ( target.equals(guess) ) return enumConstant; + } + throw new AnnotationValueDecodeFail(v, + "Can't translate " + guess + " to an enum of type " + expected, pos); + } + } + + if ( Class.class == expected ) { + if ( guess instanceof String ) try { + return Class.forName(toFQ((String)guess)); + } catch ( ClassNotFoundException e ) { + throw new AnnotationValueDecodeFail(v, + "Can't translate " + guess + " to a class object.", pos); + } + } + + throw new AnnotationValueDecodeFail(v, + "Can't translate a " + guess.getClass() + " to the expected " + expected, pos); + } + + public List<String> getRawExpressions(String annotationMethodName) { + AnnotationValue v = values.get(annotationMethodName); + return v == null ? Collections.<String>emptyList() : v.raws; + } + + public String getRawExpression(String annotationMethodName) { + List<String> l = getRawExpressions(annotationMethodName); + return l.isEmpty() ? null : l.get(0); + } + + public List<String> getProbableFQTypes(String annotationMethodName) { + List<String> result = new ArrayList<String>(); + AnnotationValue v = values.get(annotationMethodName); + if ( v == null ) return Collections.emptyList(); + + for ( Object o : v.valueGuesses ) result.add(o == null ? null : toFQ(o.toString())); + return result; + } + + private String toFQ(String typeName) { + Class<?> c; + boolean fqn = typeName.indexOf('.') > -1; + String prefix = fqn ? typeName.substring(0, typeName.indexOf('.')) : typeName; + + for ( String im : ast.getImportStatements() ) { + int idx = im.lastIndexOf('.'); + String simple = im; + if ( idx > -1 ) simple = im.substring(idx+1); + if ( simple.equals(prefix) ) { + return im + typeName.substring(prefix.length()); + } + } + + c = tryClass(typeName); + if ( c != null ) return c.getName(); + + c = tryClass("java.lang." + typeName); + if ( c != null ) return c.getName(); + + //Try star imports + for ( String im : ast.getImportStatements() ) { + if ( im.endsWith(".*") ) { + c = tryClass(im.substring(0, im.length() -1) + typeName); + if ( c != null ) return c.getName(); + } + } + + if ( !fqn ) { + String pkg = ast.getPackageDeclaration(); + if ( pkg != null ) return pkg + "." + typeName; + } + + return null; + } + + private Class<?> tryClass(String name) { + try { + return Class.forName(name); + } catch ( ClassNotFoundException e ) { + return null; + } + } + + public String getProbableFQType(String annotationMethodName) { + List<String> l = getProbableFQTypes(annotationMethodName); + return l.isEmpty() ? null : l.get(0); + } +} diff --git a/src/lombok/core/TypeResolver.java b/src/lombok/core/TypeResolver.java new file mode 100644 index 00000000..1e356f89 --- /dev/null +++ b/src/lombok/core/TypeResolver.java @@ -0,0 +1,81 @@ +package lombok.core; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import lombok.core.AST.Kind; + +public class TypeResolver { + private final TypeLibrary library; + private Collection<String> imports; + + public TypeResolver(TypeLibrary library, String packageString, Collection<String> importStrings) { + this.library = library; + this.imports = makeImportList(packageString, importStrings); + } + + private static Collection<String> makeImportList(String packageString, Collection<String> importStrings) { + Set<String> imports = new HashSet<String>(); + if ( packageString != null ) imports.add(packageString + ".*"); + imports.addAll(importStrings == null ? Collections.<String>emptySet() : importStrings); + return imports; + } + + public Collection<String> findTypeMatches(AST<?>.Node context, String typeRef) { + Collection<String> potentialMatches = library.findCompatible(typeRef); + if ( potentialMatches.isEmpty() ) return Collections.emptyList(); + + int idx = typeRef.indexOf('.'); + if ( idx > -1 ) return potentialMatches; + String simpleName = typeRef.substring(idx+1); + + //If there's an import statement that explicitly imports a 'Getter' that isn't any of our potentials, return no matches. + if ( nameConflictInImportList(simpleName, potentialMatches) ) return Collections.emptyList(); + + //Check if any of our potentials is even imported in the first place. If not: no matches. + potentialMatches = eliminateImpossibleMatches(potentialMatches); + if ( potentialMatches.isEmpty() ) return Collections.emptyList(); + + //Find a lexically accessible type of the same simple name in the same Compilation Unit. If it exists: no matches. + AST<?>.Node n = context; + while ( n != null ) { + if ( n.getKind() == Kind.TYPE ) { + String name = n.getName(); + if ( name != null && name.equals(simpleName) ) return Collections.emptyList(); + } + n = n.up(); + } + + // The potential matches we found by comparing the import statements is our matching set. Return it. + return potentialMatches; + } + + private Collection<String> eliminateImpossibleMatches(Collection<String> potentialMatches) { + Set<String> results = new HashSet<String>(); + + for ( String importedType : imports ) { + Collection<String> reduced = new HashSet<String>(library.findCompatible(importedType)); + reduced.retainAll(potentialMatches); + results.addAll(reduced); + } + + return results; + } + + private boolean nameConflictInImportList(String simpleName, Collection<String> potentialMatches) { + for ( String importedType : imports ) { + if ( !toSimpleName(importedType).equals(simpleName) ) continue; + if ( potentialMatches.contains(importedType) ) continue; + return true; + } + + return false; + } + + private static String toSimpleName(String typeName) { + int idx = typeName.lastIndexOf('.'); + return idx == -1 ? typeName : typeName.substring(idx+1); + } +} |