/* * Copyright (C) 2013 The Android Open Source Project * * 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.android.tools.lint; import static com.android.SdkConstants.INT_DEF_ANNOTATION; import static com.android.SdkConstants.STRING_DEF_ANNOTATION; import static com.android.SdkConstants.UTF_8; import com.android.SdkConstants; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.annotations.VisibleForTesting; import com.android.sdklib.IAndroidTarget; import com.android.tools.lint.client.api.JavaParser; import com.android.tools.lint.client.api.LintClient; import com.android.tools.lint.detector.api.JavaContext; import com.android.tools.lint.detector.api.Location; import com.android.tools.lint.detector.api.Project; import com.android.tools.lint.detector.api.Scope; import com.google.common.base.Splitter; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import org.eclipse.jdt.core.compiler.CategorizedProblem; import org.eclipse.jdt.core.compiler.IProblem; import org.eclipse.jdt.internal.compiler.CompilationResult; import org.eclipse.jdt.internal.compiler.Compiler; import org.eclipse.jdt.internal.compiler.DefaultErrorHandlingPolicies; import org.eclipse.jdt.internal.compiler.ICompilerRequestor; import org.eclipse.jdt.internal.compiler.IErrorHandlingPolicy; import org.eclipse.jdt.internal.compiler.IProblemFactory; import org.eclipse.jdt.internal.compiler.ast.AbstractMethodDeclaration; import org.eclipse.jdt.internal.compiler.ast.AbstractVariableDeclaration; import org.eclipse.jdt.internal.compiler.ast.AllocationExpression; import org.eclipse.jdt.internal.compiler.ast.Annotation; import org.eclipse.jdt.internal.compiler.ast.ArrayInitializer; import org.eclipse.jdt.internal.compiler.ast.CharLiteral; import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration; import org.eclipse.jdt.internal.compiler.ast.DoubleLiteral; import org.eclipse.jdt.internal.compiler.ast.ExplicitConstructorCall; import org.eclipse.jdt.internal.compiler.ast.Expression; import org.eclipse.jdt.internal.compiler.ast.FalseLiteral; import org.eclipse.jdt.internal.compiler.ast.FieldDeclaration; import org.eclipse.jdt.internal.compiler.ast.FloatLiteral; import org.eclipse.jdt.internal.compiler.ast.IntLiteral; import org.eclipse.jdt.internal.compiler.ast.Literal; import org.eclipse.jdt.internal.compiler.ast.LocalDeclaration; import org.eclipse.jdt.internal.compiler.ast.LongLiteral; import org.eclipse.jdt.internal.compiler.ast.MagicLiteral; import org.eclipse.jdt.internal.compiler.ast.MemberValuePair; import org.eclipse.jdt.internal.compiler.ast.MessageSend; import org.eclipse.jdt.internal.compiler.ast.NameReference; import org.eclipse.jdt.internal.compiler.ast.NullLiteral; import org.eclipse.jdt.internal.compiler.ast.NumberLiteral; import org.eclipse.jdt.internal.compiler.ast.StringLiteral; import org.eclipse.jdt.internal.compiler.ast.TrueLiteral; import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration; import org.eclipse.jdt.internal.compiler.ast.TypeReference; import org.eclipse.jdt.internal.compiler.batch.CompilationUnit; import org.eclipse.jdt.internal.compiler.batch.FileSystem; import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants; import org.eclipse.jdt.internal.compiler.env.ICompilationUnit; import org.eclipse.jdt.internal.compiler.env.INameEnvironment; import org.eclipse.jdt.internal.compiler.impl.BooleanConstant; import org.eclipse.jdt.internal.compiler.impl.ByteConstant; import org.eclipse.jdt.internal.compiler.impl.CharConstant; import org.eclipse.jdt.internal.compiler.impl.CompilerOptions; import org.eclipse.jdt.internal.compiler.impl.Constant; import org.eclipse.jdt.internal.compiler.impl.DoubleConstant; import org.eclipse.jdt.internal.compiler.impl.FloatConstant; import org.eclipse.jdt.internal.compiler.impl.IntConstant; import org.eclipse.jdt.internal.compiler.impl.LongConstant; import org.eclipse.jdt.internal.compiler.impl.ShortConstant; import org.eclipse.jdt.internal.compiler.impl.StringConstant; import org.eclipse.jdt.internal.compiler.lookup.AnnotationBinding; import org.eclipse.jdt.internal.compiler.lookup.Binding; import org.eclipse.jdt.internal.compiler.lookup.ElementValuePair; import org.eclipse.jdt.internal.compiler.lookup.FieldBinding; import org.eclipse.jdt.internal.compiler.lookup.LocalVariableBinding; import org.eclipse.jdt.internal.compiler.lookup.MethodBinding; import org.eclipse.jdt.internal.compiler.lookup.NestedTypeBinding; import org.eclipse.jdt.internal.compiler.lookup.PackageBinding; import org.eclipse.jdt.internal.compiler.lookup.ProblemBinding; import org.eclipse.jdt.internal.compiler.lookup.ProblemFieldBinding; import org.eclipse.jdt.internal.compiler.lookup.ProblemMethodBinding; 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 org.eclipse.jdt.internal.compiler.lookup.TypeConstants; import org.eclipse.jdt.internal.compiler.parser.Parser; import org.eclipse.jdt.internal.compiler.problem.AbortCompilation; import org.eclipse.jdt.internal.compiler.problem.DefaultProblemFactory; import org.eclipse.jdt.internal.compiler.problem.ProblemReporter; import java.io.File; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import lombok.ast.Node; import lombok.ast.VariableDeclaration; import lombok.ast.VariableDefinition; import lombok.ast.VariableDefinitionEntry; import lombok.ast.ecj.EcjTreeConverter; /** * Java parser which uses ECJ for parsing and type attribution */ public class EcjParser extends JavaParser { private static final boolean DEBUG_DUMP_PARSE_ERRORS = false; private final LintClient mClient; private final Project mProject; private Map<File, ICompilationUnit> mSourceUnits; private Map<ICompilationUnit, CompilationUnitDeclaration> mCompiled; private Map<String, TypeDeclaration> mTypeUnits; private Parser mParser; private INameEnvironment mEnvironment; public EcjParser(@NonNull LintCliClient client, @Nullable Project project) { mClient = client; mProject = project; mParser = getParser(); } /** * Create the default compiler options */ public static CompilerOptions createCompilerOptions() { CompilerOptions options = new CompilerOptions(); // Always using JDK 7 rather than basing it on project metadata since we // don't do compilation error validation in lint (we leave that to the IDE's // error parser or the command line build's compilation step); we want an // AST that is as tolerant as possible. long languageLevel = ClassFileConstants.JDK1_7; options.complianceLevel = languageLevel; options.sourceLevel = languageLevel; options.targetJDK = languageLevel; options.originalComplianceLevel = languageLevel; options.originalSourceLevel = languageLevel; options.inlineJsrBytecode = true; // >1.5 options.parseLiteralExpressionsAsConstants = true; options.analyseResourceLeaks = false; options.docCommentSupport = false; options.defaultEncoding = UTF_8; options.suppressOptionalErrors = true; options.generateClassFiles = false; options.isAnnotationBasedNullAnalysisEnabled = false; options.reportUnusedDeclaredThrownExceptionExemptExceptionAndThrowable = false; options.reportUnusedDeclaredThrownExceptionIncludeDocCommentReference = false; options.reportUnusedDeclaredThrownExceptionWhenOverriding = false; options.reportUnusedParameterIncludeDocCommentReference = false; options.reportUnusedParameterWhenImplementingAbstract = false; options.reportUnusedParameterWhenOverridingConcrete = false; options.suppressWarnings = true; options.processAnnotations = true; options.storeAnnotations = true; options.verbose = false; return options; } public static long getLanguageLevel(int major, int minor) { assert major == 1; switch (minor) { case 5: return ClassFileConstants.JDK1_5; case 6: return ClassFileConstants.JDK1_6; case 7: default: return ClassFileConstants.JDK1_7; } } private Parser getParser() { if (mParser == null) { CompilerOptions options = createCompilerOptions(); ProblemReporter problemReporter = new ProblemReporter( DefaultErrorHandlingPolicies.exitOnFirstError(), options, new DefaultProblemFactory()); mParser = new Parser(problemReporter, options.parseLiteralExpressionsAsConstants); mParser.javadocParser.checkDocComment = false; } return mParser; } @Override public void prepareJavaParse(@NonNull final List<JavaContext> contexts) { if (mProject == null || contexts.isEmpty()) { return; } List<ICompilationUnit> sources = Lists.newArrayListWithExpectedSize(contexts.size()); mSourceUnits = Maps.newHashMapWithExpectedSize(sources.size()); for (JavaContext context : contexts) { String contents = context.getContents(); if (contents == null) { continue; } File file = context.file; CompilationUnit unit = new CompilationUnit(contents.toCharArray(), file.getPath(), UTF_8); sources.add(unit); mSourceUnits.put(file, unit); } List<String> classPath = computeClassPath(contexts); mCompiled = Maps.newHashMapWithExpectedSize(mSourceUnits.size()); try { mEnvironment = parse(createCompilerOptions(), sources, classPath, mCompiled, mClient); } catch (Throwable t) { mClient.log(t, "ECJ compiler crashed"); } if (DEBUG_DUMP_PARSE_ERRORS) { for (CompilationUnitDeclaration unit : mCompiled.values()) { // so maybe I don't need my map!! CategorizedProblem[] problems = unit.compilationResult() .getAllProblems(); if (problems != null) { for (IProblem problem : problems) { if (problem == null || !problem.isError()) { continue; } System.out.println( new String(problem.getOriginatingFileName()) + ":" + (problem.isError() ? "Error" : "Warning") + ": " + problem.getSourceLineNumber() + ": " + problem.getMessage()); } } } } } /** Parse the given source units and class path and store it into the given output map */ public static INameEnvironment parse( CompilerOptions options, @NonNull List<ICompilationUnit> sourceUnits, @NonNull List<String> classPath, @NonNull Map<ICompilationUnit, CompilationUnitDeclaration> outputMap, @Nullable LintClient client) { INameEnvironment environment = new FileSystem( classPath.toArray(new String[classPath.size()]), new String[0], options.defaultEncoding); IErrorHandlingPolicy policy = DefaultErrorHandlingPolicies.proceedWithAllProblems(); IProblemFactory problemFactory = new DefaultProblemFactory(Locale.getDefault()); ICompilerRequestor requestor = new ICompilerRequestor() { @Override public void acceptResult(CompilationResult result) { // Not used; we need the corresponding CompilationUnitDeclaration for the source // units (the AST parsed from source) which we don't get access to here, so we // instead subclass AST to get our hands on them. } }; NonGeneratingCompiler compiler = new NonGeneratingCompiler(environment, policy, options, requestor, problemFactory, outputMap); try { compiler.compile(sourceUnits.toArray(new ICompilationUnit[sourceUnits.size()])); } catch (OutOfMemoryError e) { environment.cleanup(); // Since we're running out of memory, if it's all still held we could potentially // fail attempting to log the failure. Actively get rid of the large ECJ data // structure references first so minimize the chance of that //noinspection UnusedAssignment compiler = null; //noinspection UnusedAssignment environment = null; //noinspection UnusedAssignment requestor = null; //noinspection UnusedAssignment problemFactory = null; //noinspection UnusedAssignment policy = null; String msg = "Ran out of memory analyzing .java sources with ECJ: Some lint checks " + "may not be accurate (missing type information from the compiler)"; if (client != null) { // Don't log exception too; this isn't a compiler error per se where we // need to pin point the exact unlucky code that asked for memory when it // had already run out client.log(null, msg); } else { System.out.println(msg); } } catch (Throwable t) { if (client != null) { CompilationUnitDeclaration currentUnit = compiler.getCurrentUnit(); if (currentUnit == null || currentUnit.getFileName() == null) { client.log(t, "ECJ compiler crashed"); } else { client.log(t, "ECJ compiler crashed processing %1$s", new String(currentUnit.getFileName())); } } else { t.printStackTrace(); } environment.cleanup(); environment = null; } return environment; } @NonNull private List<String> computeClassPath(@NonNull List<JavaContext> contexts) { assert mProject != null; List<String> classPath = Lists.newArrayList(); IAndroidTarget compileTarget = mProject.getBuildTarget(); if (compileTarget != null) { String androidJar = compileTarget.getPath(IAndroidTarget.ANDROID_JAR); if (androidJar != null && new File(androidJar).exists()) { classPath.add(androidJar); } } Set<File> libraries = Sets.newHashSet(); Set<String> names = Sets.newHashSet(); for (File library : mProject.getJavaLibraries()) { libraries.add(library); names.add(getLibraryName(library)); } for (Project project : mProject.getAllLibraries()) { for (File library : project.getJavaLibraries()) { String name = getLibraryName(library); // Avoid pulling in android-support-v4.jar from libraries etc // since we're pointing to the local copies rather than the real // maven/gradle source copies if (!names.contains(name)) { libraries.add(library); names.add(name); } } } for (File file : libraries) { if (file.exists()) { classPath.add(file.getPath()); } } // In incremental mode we may need to point to other sources in the project // for type resolution EnumSet<Scope> scope = contexts.get(0).getScope(); if (!scope.contains(Scope.ALL_JAVA_FILES)) { // May need other compiled classes too for (File dir : mProject.getJavaClassFolders()) { if (dir.exists()) { classPath.add(dir.getPath()); } } } return classPath; } @NonNull private static String getLibraryName(@NonNull File library) { String name = library.getName(); if (name.equals(SdkConstants.FN_CLASSES_JAR)) { // For AAR artifacts they'll all clash with "classes.jar"; include more unique // context String path = library.getPath(); int index = path.indexOf("exploded-aar"); if (index != -1) { return path.substring(index); } else { index = path.indexOf("exploded-bundles"); if (index != -1) { return path.substring(index); } } File parent = library.getParentFile(); if (parent != null) { return parent.getName() + File.separatorChar + name; } } return name; } @Override public Node parseJava(@NonNull JavaContext context) { String code = context.getContents(); if (code == null) { return null; } CompilationUnitDeclaration unit = getParsedUnit(context, code); try { EcjTreeConverter converter = new EcjTreeConverter(); converter.visit(code, unit); List<? extends Node> nodes = converter.getAll(); if (nodes != null) { // There could be more than one node when there are errors; pick out the // compilation unit node for (Node node : nodes) { if (node instanceof lombok.ast.CompilationUnit) { return node; } } } return null; } catch (Throwable t) { mClient.log(t, "Failed converting ECJ parse tree to Lombok for file %1$s", context.file.getPath()); return null; } } @Nullable private CompilationUnitDeclaration getParsedUnit( @NonNull JavaContext context, @NonNull String code) { ICompilationUnit sourceUnit = null; if (mSourceUnits != null && mCompiled != null) { sourceUnit = mSourceUnits.get(context.file); if (sourceUnit != null) { CompilationUnitDeclaration unit = mCompiled.get(sourceUnit); if (unit != null) { return unit; } } } if (sourceUnit == null) { sourceUnit = new CompilationUnit(code.toCharArray(), context.file.getName(), UTF_8); } try { CompilationResult compilationResult = new CompilationResult(sourceUnit, 0, 0, 0); return getParser().parse(sourceUnit, compilationResult); } catch (AbortCompilation e) { // No need to report Java parsing errors while running in Eclipse. // Eclipse itself will already provide problem markers for these files, // so all this achieves is creating "multiple annotations on this line" // tooltips instead. return null; } } @NonNull @Override public Location getLocation(@NonNull JavaContext context, @NonNull Node node) { lombok.ast.Position position = node.getPosition(); return Location.create(context.file, context.getContents(), position.getStart(), position.getEnd()); } @NonNull @Override public Location.Handle createLocationHandle(@NonNull JavaContext context, @NonNull Node node) { return new LocationHandle(context.file, node); } @Override public void dispose(@NonNull JavaContext context, @NonNull Node compilationUnit) { if (mSourceUnits != null && mCompiled != null) { ICompilationUnit sourceUnit = mSourceUnits.get(context.file); if (sourceUnit != null) { mSourceUnits.remove(context.file); mCompiled.remove(sourceUnit); } } } @Override public void dispose() { if (mEnvironment != null) { mEnvironment.cleanup(); mEnvironment = null; } } @Nullable private static Object getNativeNode(@NonNull Node node) { Object nativeNode = node.getNativeNode(); if (nativeNode != null) { return nativeNode; } Node parent = node.getParent(); // The ECJ native nodes are sometimes spotty; for example, for a // MethodInvocation node we can have a null native node, but its // parent expression statement will point to the real MessageSend node if (parent != null) { nativeNode = parent.getNativeNode(); if (nativeNode != null) { return nativeNode; } } if (node instanceof VariableDefinitionEntry) { node = node.getParent().getParent(); } if (node instanceof VariableDeclaration) { VariableDeclaration declaration = (VariableDeclaration) node; VariableDefinition definition = declaration.astDefinition(); if (definition != null) { lombok.ast.TypeReference typeReference = definition.astTypeReference(); if (typeReference != null) { return typeReference.getNativeNode(); } } } return null; } @Override @Nullable public ResolvedNode resolve(@NonNull JavaContext context, @NonNull Node node) { Object nativeNode = getNativeNode(node); if (nativeNode == null) { return null; } if (nativeNode instanceof NameReference) { return resolve(((NameReference) nativeNode).binding); } else if (nativeNode instanceof TypeReference) { return resolve(((TypeReference) nativeNode).resolvedType); } else if (nativeNode instanceof MessageSend) { return resolve(((MessageSend) nativeNode).binding); } else if (nativeNode instanceof AllocationExpression) { return resolve(((AllocationExpression) nativeNode).binding); } else if (nativeNode instanceof TypeDeclaration) { return resolve(((TypeDeclaration) nativeNode).binding); } else if (nativeNode instanceof ExplicitConstructorCall) { return resolve(((ExplicitConstructorCall) nativeNode).binding); } else if (nativeNode instanceof Annotation) { AnnotationBinding compilerAnnotation = ((Annotation) nativeNode).getCompilerAnnotation(); if (compilerAnnotation != null) { return new EcjResolvedAnnotation(compilerAnnotation); } return resolve(((Annotation) nativeNode).resolvedType); } else if (nativeNode instanceof AbstractMethodDeclaration) { return resolve(((AbstractMethodDeclaration) nativeNode).binding); } else if (nativeNode instanceof AbstractVariableDeclaration) { if (nativeNode instanceof LocalDeclaration) { return resolve(((LocalDeclaration) nativeNode).binding); } else if (nativeNode instanceof FieldDeclaration) { FieldDeclaration fieldDeclaration = (FieldDeclaration) nativeNode; if (fieldDeclaration.initialization instanceof AllocationExpression) { AllocationExpression allocation = (AllocationExpression)fieldDeclaration.initialization; if (allocation.binding != null) { // Field constructor call: this is an enum constant. return new EcjResolvedMethod(allocation.binding); } } return resolve(fieldDeclaration.binding); } } // TODO: Handle org.eclipse.jdt.internal.compiler.ast.SuperReference. It // doesn't contain an actual method binding; the parent node call should contain // it, but is missing a native node reference; investigate the ECJ bridge's super // handling. return null; } private ResolvedNode resolve(@Nullable Binding binding) { if (binding == null || binding instanceof ProblemBinding) { return null; } if (binding instanceof TypeBinding) { TypeBinding tb = (TypeBinding) binding; return new EcjResolvedClass(tb); } else if (binding instanceof MethodBinding) { MethodBinding mb = (MethodBinding) binding; if (mb instanceof ProblemMethodBinding) { return null; } //noinspection VariableNotUsedInsideIf if (mb.declaringClass != null) { return new EcjResolvedMethod(mb); } } else if (binding instanceof LocalVariableBinding) { LocalVariableBinding lvb = (LocalVariableBinding) binding; //noinspection VariableNotUsedInsideIf if (lvb.type != null) { return new EcjResolvedVariable(lvb); } } else if (binding instanceof FieldBinding) { FieldBinding fb = (FieldBinding) binding; if (fb instanceof ProblemFieldBinding) { return null; } if (fb.type != null && fb.declaringClass != null) { return new EcjResolvedField(fb); } } return null; } private TypeDeclaration findTypeDeclaration(@NonNull String signature) { if (mTypeUnits == null) { mTypeUnits = Maps.newHashMapWithExpectedSize(mCompiled.size()); for (CompilationUnitDeclaration unit : mCompiled.values()) { if (unit.types != null) { for (TypeDeclaration typeDeclaration : unit.types) { addTypeDeclaration(typeDeclaration); } } } } return mTypeUnits.get(signature); } private void addTypeDeclaration(TypeDeclaration typeDeclaration) { String type = new String(typeDeclaration.binding.readableName()); mTypeUnits.put(type, typeDeclaration); // Recurse on member types if (typeDeclaration.memberTypes != null) { for (TypeDeclaration member : typeDeclaration.memberTypes) { addTypeDeclaration(member); } } } @Override @Nullable public TypeDescriptor getType(@NonNull JavaContext context, @NonNull Node node) { Object nativeNode = getNativeNode(node); if (nativeNode == null) { return null; } if (nativeNode instanceof MessageSend) { nativeNode = ((MessageSend)nativeNode).binding; } else if (nativeNode instanceof AllocationExpression) { nativeNode = ((AllocationExpression)nativeNode).resolvedType; } else if (nativeNode instanceof NameReference) { nativeNode = ((NameReference)nativeNode).resolvedType; } else if (nativeNode instanceof Expression) { if (nativeNode instanceof Literal) { if (nativeNode instanceof StringLiteral) { return getTypeDescriptor(TYPE_STRING); } else if (nativeNode instanceof NumberLiteral) { if (nativeNode instanceof IntLiteral) { return getTypeDescriptor(TYPE_INT); } else if (nativeNode instanceof LongLiteral) { return getTypeDescriptor(TYPE_LONG); } else if (nativeNode instanceof CharLiteral) { return getTypeDescriptor(TYPE_CHAR); } else if (nativeNode instanceof FloatLiteral) { return getTypeDescriptor(TYPE_FLOAT); } else if (nativeNode instanceof DoubleLiteral) { return getTypeDescriptor(TYPE_DOUBLE); } } else if (nativeNode instanceof MagicLiteral) { if (nativeNode instanceof TrueLiteral || nativeNode instanceof FalseLiteral) { return getTypeDescriptor(TYPE_BOOLEAN); } else if (nativeNode instanceof NullLiteral) { return getTypeDescriptor(TYPE_NULL); } } } nativeNode = ((Expression)nativeNode).resolvedType; } else if (nativeNode instanceof TypeDeclaration) { nativeNode = ((TypeDeclaration) nativeNode).binding; } else if (nativeNode instanceof AbstractMethodDeclaration) { nativeNode = ((AbstractMethodDeclaration) nativeNode).binding; } if (nativeNode instanceof Binding) { Binding binding = (Binding) nativeNode; if (binding instanceof TypeBinding) { TypeBinding tb = (TypeBinding) binding; return getTypeDescriptor(tb); } else if (binding instanceof LocalVariableBinding) { LocalVariableBinding lvb = (LocalVariableBinding) binding; if (lvb.type != null) { return getTypeDescriptor(lvb.type); } } else if (binding instanceof FieldBinding) { FieldBinding fb = (FieldBinding) binding; if (fb.type != null) { return getTypeDescriptor(fb.type); } } else if (binding instanceof MethodBinding) { return getTypeDescriptor(((MethodBinding) binding).returnType); } else if (binding instanceof ProblemBinding) { // Unresolved type. We just don't know. return null; } } return null; } @Nullable @Override public ResolvedClass findClass(@NonNull JavaContext context, @NonNull String fullyQualifiedName) { Node compilationUnit = context.getCompilationUnit(); if (compilationUnit == null) { return null; } Object nativeObj = getNativeNode(compilationUnit); if (!(nativeObj instanceof CompilationUnitDeclaration)) { return null; } CompilationUnitDeclaration ecjUnit = (CompilationUnitDeclaration) nativeObj; // Convert "foo.bar.Baz" into char[][] 'foo','bar','Baz' as required for // ECJ name lookup List<char[]> arrays = Lists.newArrayList(); for (String segment : Splitter.on('.').split(fullyQualifiedName)) { arrays.add(segment.toCharArray()); } char[][] compoundName = new char[arrays.size()][]; for (int i = 0, n = arrays.size(); i < n; i++) { compoundName[i] = arrays.get(i); } Binding typeOrPackage = ecjUnit.scope.getTypeOrPackage(compoundName); if (typeOrPackage instanceof TypeBinding && !(typeOrPackage instanceof ProblemReferenceBinding)) { return new EcjResolvedClass((TypeBinding)typeOrPackage); } return null; } @Nullable private TypeDescriptor getTypeDescriptor(@Nullable TypeBinding resolvedType) { if (resolvedType == null) { return null; } return new EcjTypeDescriptor(resolvedType); } private static TypeDescriptor getTypeDescriptor(String fqn) { return new DefaultTypeDescriptor(fqn); } /** Computes the super method, if any, given a method binding */ private static MethodBinding findSuperMethodBinding(@NonNull MethodBinding binding) { try { ReferenceBinding superclass = binding.declaringClass.superclass(); while (superclass != null) { MethodBinding[] methods = superclass.getMethods(binding.selector, binding.parameters.length); for (MethodBinding method : methods) { if (method.areParameterErasuresEqual(binding)) { return method; } } superclass = superclass.superclass(); } } catch (Exception ignore) { // Work around ECJ bugs; see https://code.google.com/p/android/issues/detail?id=172268 } return null; } @NonNull private static Collection<ResolvedAnnotation> merge( @Nullable Collection<ResolvedAnnotation> first, @Nullable Collection<ResolvedAnnotation> second) { if (first == null || first.isEmpty()) { if (second == null) { return Collections.emptyList(); } else { return second; } } else if (second == null || second.isEmpty()) { return first; } else { int size = first.size() + second.size(); List<ResolvedAnnotation> merged = Lists.newArrayListWithExpectedSize(size); merged.addAll(first); merged.addAll(second); return merged; } } /* Handle for creating positions cheaply and returning full fledged locations later */ private static class LocationHandle implements Location.Handle { private File mFile; private Node mNode; private Object mClientData; public LocationHandle(File file, Node node) { mFile = file; mNode = node; } @NonNull @Override public Location resolve() { lombok.ast.Position pos = mNode.getPosition(); return Location.create(mFile, null /*contents*/, pos.getStart(), pos.getEnd()); } @Override public void setClientData(@Nullable Object clientData) { mClientData = clientData; } @Override @Nullable public Object getClientData() { return mClientData; } } // Custom version of the compiler which skips code generation and records source units private static class NonGeneratingCompiler extends Compiler { private Map<ICompilationUnit, CompilationUnitDeclaration> mUnits; private CompilationUnitDeclaration mCurrentUnit; public NonGeneratingCompiler(INameEnvironment environment, IErrorHandlingPolicy policy, CompilerOptions options, ICompilerRequestor requestor, IProblemFactory problemFactory, Map<ICompilationUnit, CompilationUnitDeclaration> units) { super(environment, policy, options, requestor, problemFactory, null, null); mUnits = units; } @Nullable CompilationUnitDeclaration getCurrentUnit() { // Can't use lookupEnvironment.unitBeingCompleted directly; it gets nulled out // as part of the exception catch handling in the compiler before this method // is called from lint -- therefore we stash a copy in our own mCurrentUnit field return mCurrentUnit; } @Override protected synchronized void addCompilationUnit(ICompilationUnit sourceUnit, CompilationUnitDeclaration parsedUnit) { super.addCompilationUnit(sourceUnit, parsedUnit); mUnits.put(sourceUnit, parsedUnit); } @Override public void process(CompilationUnitDeclaration unit, int unitNumber) { mCurrentUnit = lookupEnvironment.unitBeingCompleted = unit; parser.getMethodBodies(unit); if (unit.scope != null) { unit.scope.faultInTypes(); unit.scope.verifyMethods(lookupEnvironment.methodVerifier()); } unit.resolve(); unit.analyseCode(); // This is where we differ from super: DON'T call generateCode(). // Sadly we can't just set ignoreMethodBodies=true to have the same effect, // since that would also skip the analyseCode call, which we DO, want: // unit.generateCode(); if (options.produceReferenceInfo && unit.scope != null) { unit.scope.storeDependencyInfo(); } unit.finalizeProblems(); unit.compilationResult.totalUnitsKnown = totalUnits; lookupEnvironment.unitBeingCompleted = null; } } private class EcjTypeDescriptor extends TypeDescriptor { private final TypeBinding mBinding; private EcjTypeDescriptor(@NonNull TypeBinding binding) { mBinding = binding; } @NonNull @Override public String getName() { return new String(mBinding.readableName()); } @Override public boolean matchesName(@NonNull String name) { return sameChars(name, mBinding.readableName()); } @Override public boolean matchesSignature(@NonNull String signature) { return sameChars(signature, mBinding.readableName()); } @NonNull @Override public String getSignature() { return getName(); } @Override @Nullable public ResolvedClass getTypeClass() { if (!mBinding.isPrimitiveType()) { return new EcjResolvedClass(mBinding); } return null; } @SuppressWarnings("RedundantIfStatement") @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } EcjTypeDescriptor that = (EcjTypeDescriptor) o; if (!mBinding.equals(that.mBinding)) { return false; } return true; } @Override public int hashCode() { return mBinding.hashCode(); } } private class EcjResolvedMethod extends ResolvedMethod { private MethodBinding mBinding; private EcjResolvedMethod(MethodBinding binding) { mBinding = binding; assert mBinding.declaringClass != null; } @NonNull @Override public String getName() { char[] c = isConstructor() ? mBinding.declaringClass.readableName() : mBinding.selector; return new String(c); } @Override public boolean matches(@NonNull String name) { char[] c = isConstructor() ? mBinding.declaringClass.readableName() : mBinding.selector; return sameChars(name, c); } @NonNull @Override public ResolvedClass getContainingClass() { return new EcjResolvedClass(mBinding.declaringClass); } @Override public int getArgumentCount() { return mBinding.parameters != null ? mBinding.parameters.length : 0; } @NonNull @Override public TypeDescriptor getArgumentType(int index) { TypeBinding parameterType = mBinding.parameters[index]; TypeDescriptor typeDescriptor = getTypeDescriptor(parameterType); assert typeDescriptor != null; // because parameter is not null return typeDescriptor; } @Override public boolean argumentMatchesType(int index, @NonNull String signature) { return sameChars(signature, mBinding.parameters[index].readableName()); } @Nullable @Override public TypeDescriptor getReturnType() { return isConstructor() ? null : getTypeDescriptor(mBinding.returnType); } @Override public boolean isConstructor() { return mBinding.isConstructor(); } @Override @Nullable public ResolvedMethod getSuperMethod() { MethodBinding superBinding = findSuperMethodBinding(mBinding); if (superBinding != null) { return new EcjResolvedMethod(superBinding); } return null; } @NonNull @Override public Iterable<ResolvedAnnotation> getAnnotations() { List<ResolvedAnnotation> all = Lists.newArrayListWithExpectedSize(4); ExternalAnnotationRepository manager = ExternalAnnotationRepository.get(mClient); MethodBinding binding = this.mBinding; while (binding != null) { AnnotationBinding[] annotations = binding.getAnnotations(); int count = annotations.length; if (count > 0) { for (AnnotationBinding annotation : annotations) { if (annotation != null) { all.add(new EcjResolvedAnnotation(annotation)); } } } // Look for external annotations Collection<ResolvedAnnotation> external = manager.getAnnotations( new EcjResolvedMethod(binding)); if (external != null) { all.addAll(external); } binding = findSuperMethodBinding(binding); } return all; } @NonNull @Override public Iterable<ResolvedAnnotation> getParameterAnnotations(int index) { List<ResolvedAnnotation> all = Lists.newArrayListWithExpectedSize(4); ExternalAnnotationRepository manager = ExternalAnnotationRepository.get(mClient); MethodBinding binding = this.mBinding; while (binding != null) { AnnotationBinding[][] parameterAnnotations = binding.getParameterAnnotations(); if (parameterAnnotations != null && index >= 0 && index < parameterAnnotations.length) { AnnotationBinding[] annotations = parameterAnnotations[index]; int count = annotations.length; if (count > 0) { for (AnnotationBinding annotation : annotations) { if (annotation != null) { all.add(new EcjResolvedAnnotation(annotation)); } } } } // Look for external annotations Collection<ResolvedAnnotation> external = manager.getAnnotations( new EcjResolvedMethod(binding), index); if (external != null) { all.addAll(external); } binding = findSuperMethodBinding(binding); } return all; } @Override public int getModifiers() { return mBinding.getAccessFlags(); } @Override public String getSignature() { return mBinding.toString(); } @Override public boolean isInPackage(@NonNull String pkgName, boolean includeSubPackages) { PackageBinding pkg = mBinding.declaringClass.getPackage(); if (pkg != null) { return includeSubPackages ? startsWithCompound(pkgName, pkg.compoundName) : equalsCompound(pkgName, pkg.compoundName); } return false; } @SuppressWarnings("RedundantIfStatement") @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } EcjResolvedMethod that = (EcjResolvedMethod) o; if (mBinding != null ? !mBinding.equals(that.mBinding) : that.mBinding != null) { return false; } return true; } @Override public int hashCode() { return mBinding != null ? mBinding.hashCode() : 0; } } private class EcjResolvedClass extends ResolvedClass { protected final TypeBinding mBinding; private EcjResolvedClass(TypeBinding binding) { mBinding = binding; } @NonNull @Override public String getName() { String name = new String(mBinding.readableName()); if (name.indexOf('.') == -1 && mBinding.enclosingType() != null) { return new String(mBinding.enclosingType().readableName()) + '.' + name; } return name; } @NonNull @Override public String getSimpleName() { return new String(mBinding.shortReadableName()); } @Override public boolean matches(@NonNull String name) { return sameChars(name, mBinding.readableName()); } @Nullable @Override public ResolvedClass getSuperClass() { if (mBinding instanceof ReferenceBinding) { ReferenceBinding refBinding = (ReferenceBinding) mBinding; ReferenceBinding superClass = refBinding.superclass(); if (superClass != null) { return new EcjResolvedClass(superClass); } } return null; } @Nullable @Override public ResolvedClass getContainingClass() { if (mBinding instanceof NestedTypeBinding) { NestedTypeBinding ntb = (NestedTypeBinding) mBinding; if (ntb.enclosingType != null) { return new EcjResolvedClass(ntb.enclosingType); } } return null; } @Override public boolean isSubclassOf(@NonNull String name, boolean strict) { if (mBinding instanceof ReferenceBinding) { ReferenceBinding cls = (ReferenceBinding) mBinding; if (strict) { cls = cls.superclass(); } for (; cls != null; cls = cls.superclass()) { if (sameChars(name, cls.readableName())) { return true; } } } return false; } @Override @NonNull public Iterable<ResolvedMethod> getConstructors() { if (mBinding instanceof ReferenceBinding) { ReferenceBinding cls = (ReferenceBinding) mBinding; MethodBinding[] methods = cls.getMethods(TypeConstants.INIT); if (methods != null) { int count = methods.length; List<ResolvedMethod> result = Lists.newArrayListWithExpectedSize(count); for (MethodBinding method : methods) { if (method.isConstructor()) { result.add(new EcjResolvedMethod(method)); } } return result; } } return Collections.emptyList(); } @Override @NonNull public Iterable<ResolvedMethod> getMethods(@NonNull String name, boolean includeInherited) { return findMethods(name, includeInherited); } @Override @NonNull public Iterable<ResolvedMethod> getMethods(boolean includeInherited) { return findMethods(null, includeInherited); } @NonNull private Iterable<ResolvedMethod> findMethods(@Nullable String name, boolean includeInherited) { if (mBinding instanceof ReferenceBinding) { ReferenceBinding cls = (ReferenceBinding) mBinding; if (includeInherited) { List<ResolvedMethod> result = null; while (cls != null) { MethodBinding[] methods = name != null ? cls.getMethods(name.toCharArray()) : cls.methods(); if (methods != null) { int count = methods.length; if (count > 0) { if (result == null) { result = Lists.newArrayListWithExpectedSize(count); } for (MethodBinding method : methods) { if (!method.isConstructor()) { // See if this method looks like it's masked boolean masked = false; for (ResolvedMethod m : result) { MethodBinding mb = ((EcjResolvedMethod) m).mBinding; if (mb.areParameterErasuresEqual(method)) { masked = true; break; } } if (masked) { continue; } result.add(new EcjResolvedMethod(method)); } } } } cls = cls.superclass(); } return result != null ? result : Collections.<ResolvedMethod>emptyList(); } else { MethodBinding[] methods = name != null ? cls.getMethods(name.toCharArray()) : cls.methods(); if (methods != null) { int count = methods.length; List<ResolvedMethod> result = Lists.newArrayListWithExpectedSize(count); for (MethodBinding method : methods) { if (!method.isConstructor()) { result.add(new EcjResolvedMethod(method)); } } return result; } } } return Collections.emptyList(); } @NonNull @Override public Iterable<ResolvedAnnotation> getAnnotations() { List<ResolvedAnnotation> all = Lists.newArrayListWithExpectedSize(2); ExternalAnnotationRepository manager = ExternalAnnotationRepository.get(mClient); if (mBinding instanceof ReferenceBinding) { ReferenceBinding cls = (ReferenceBinding) mBinding; while (cls != null) { AnnotationBinding[] annotations = cls.getAnnotations(); int count = annotations.length; if (count > 0) { all = Lists.newArrayListWithExpectedSize(count); for (AnnotationBinding annotation : annotations) { if (annotation != null) { all.add(new EcjResolvedAnnotation(annotation)); } } } // Look for external annotations Collection<ResolvedAnnotation> external = manager.getAnnotations( new EcjResolvedClass(cls)); if (external != null) { all.addAll(external); } cls = cls.superclass(); } } else { Collection<ResolvedAnnotation> external = manager.getAnnotations(this); if (external != null) { all.addAll(external); } } return all; } @NonNull @Override public Iterable<ResolvedField> getFields(boolean includeInherited) { if (mBinding instanceof ReferenceBinding) { ReferenceBinding cls = (ReferenceBinding) mBinding; if (includeInherited) { List<ResolvedField> result = null; while (cls != null) { FieldBinding[] fields = cls.fields(); if (fields != null) { int count = fields.length; if (count > 0) { if (result == null) { result = Lists.newArrayListWithExpectedSize(count); } for (FieldBinding field : fields) { // See if this field looks like it's masked boolean masked = false; for (ResolvedField f : result) { FieldBinding mb = ((EcjResolvedField) f).mBinding; if (Arrays.equals(mb.readableName(), field.readableName())) { masked = true; break; } } if (masked) { continue; } result.add(new EcjResolvedField(field)); } } } cls = cls.superclass(); } return result != null ? result : Collections.<ResolvedField>emptyList(); } else { FieldBinding[] fields = cls.fields(); if (fields != null) { int count = fields.length; List<ResolvedField> result = Lists.newArrayListWithExpectedSize(count); for (FieldBinding field : fields) { result.add(new EcjResolvedField(field)); } return result; } } } return Collections.emptyList(); } @Override @Nullable public ResolvedField getField(@NonNull String name, boolean includeInherited) { if (mBinding instanceof ReferenceBinding) { ReferenceBinding cls = (ReferenceBinding) mBinding; while (cls != null) { FieldBinding[] fields = cls.fields(); if (fields != null) { for (FieldBinding field : fields) { if (sameChars(name, field.name)) { return new EcjResolvedField(field); } } } if (includeInherited) { cls = cls.superclass(); } else { break; } } } return null; } @Nullable @Override public ResolvedPackage getPackage() { return new EcjResolvedPackage(mBinding.getPackage()); } @Override public int getModifiers() { if (mBinding instanceof ReferenceBinding) { ReferenceBinding cls = (ReferenceBinding) mBinding; // These constants from ClassFileConstants luckily agree with the Modifier // constants in the low bits we care about (public, abstract, static, etc) return cls.getAccessFlags(); } return 0; } @Override public String getSignature() { return getName(); } @Override public boolean isInPackage(@NonNull String pkgName, boolean includeSubPackages) { PackageBinding pkg = mBinding.getPackage(); if (pkg != null) { return includeSubPackages ? startsWithCompound(pkgName, pkg.compoundName) : equalsCompound(pkgName, pkg.compoundName); } return false; } @SuppressWarnings("RedundantIfStatement") @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } EcjResolvedClass that = (EcjResolvedClass) o; if (mBinding != null ? !mBinding.equals(that.mBinding) : that.mBinding != null) { return false; } return true; } @Override public int hashCode() { return mBinding != null ? mBinding.hashCode() : 0; } } // "package-info" as a char private static final char[] PACKAGE_INFO_CHARS = new char[] { 'p', 'a', 'c', 'k', 'a', 'g', 'e', '-', 'i', 'n', 'f', 'o' }; private class EcjResolvedPackage extends ResolvedPackage { private final PackageBinding mBinding; public EcjResolvedPackage(PackageBinding binding) { mBinding = binding; } @NonNull @Override public String getName() { return new String(mBinding.readableName()); } @Override public String getSignature() { return getName(); } @NonNull @Override public Iterable<ResolvedAnnotation> getAnnotations() { List<ResolvedAnnotation> all = Lists.newArrayListWithExpectedSize(2); AnnotationBinding[] annotations = mBinding.getAnnotations(); int count = annotations.length; if (count == 0) { Binding pkgInfo = mBinding.getTypeOrPackage(PACKAGE_INFO_CHARS); if (pkgInfo != null) { annotations = pkgInfo.getAnnotations(); } count = annotations.length; } if (count > 0) { for (AnnotationBinding annotation : annotations) { if (annotation != null) { all.add(new EcjResolvedAnnotation(annotation)); } } } // Merge external annotations ExternalAnnotationRepository manager = ExternalAnnotationRepository.get(mClient); Collection<ResolvedAnnotation> external = manager.getAnnotations(this); if (external != null) { all.addAll(external); } return all; } @Override public int getModifiers() { return 0; } } private class EcjResolvedField extends ResolvedField { private FieldBinding mBinding; private EcjResolvedField(FieldBinding binding) { mBinding = binding; } @NonNull @Override public String getName() { return new String(mBinding.readableName()); } @Override public boolean matches(@NonNull String name) { return sameChars(name, mBinding.readableName()); } @NonNull @Override public TypeDescriptor getType() { TypeDescriptor typeDescriptor = getTypeDescriptor(mBinding.type); assert typeDescriptor != null; // because mBinding.type is known not to be null return typeDescriptor; } @NonNull @Override public ResolvedClass getContainingClass() { return new EcjResolvedClass(mBinding.declaringClass); } @Nullable @Override public Object getValue() { return getConstantValue(mBinding.constant()); } @NonNull @Override public Iterable<ResolvedAnnotation> getAnnotations() { List<ResolvedAnnotation> compiled = null; AnnotationBinding[] annotations = mBinding.getAnnotations(); int count = annotations.length; if (count > 0) { compiled = Lists.newArrayListWithExpectedSize(count); for (AnnotationBinding annotation : annotations) { if (annotation != null) { compiled.add(new EcjResolvedAnnotation(annotation)); } } } // Look for external annotations ExternalAnnotationRepository manager = ExternalAnnotationRepository.get(mClient); Collection<ResolvedAnnotation> external = manager.getAnnotations(this); return merge(compiled, external); } @Override public int getModifiers() { return mBinding.getAccessFlags(); } @Override public String getSignature() { return mBinding.toString(); } @Override public boolean isInPackage(@NonNull String pkgName, boolean includeSubPackages) { PackageBinding pkg = mBinding.declaringClass.getPackage(); if (pkg != null) { return includeSubPackages ? startsWithCompound(pkgName, pkg.compoundName) : equalsCompound(pkgName, pkg.compoundName); } return false; } @SuppressWarnings("RedundantIfStatement") @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } EcjResolvedField that = (EcjResolvedField) o; if (mBinding != null ? !mBinding.equals(that.mBinding) : that.mBinding != null) { return false; } return true; } @Override public int hashCode() { return mBinding != null ? mBinding.hashCode() : 0; } } private class EcjResolvedVariable extends ResolvedVariable { private LocalVariableBinding mBinding; private EcjResolvedVariable(LocalVariableBinding binding) { mBinding = binding; } @NonNull @Override public String getName() { return new String(mBinding.readableName()); } @Override public boolean matches(@NonNull String name) { return sameChars(name, mBinding.readableName()); } @NonNull @Override public TypeDescriptor getType() { TypeDescriptor typeDescriptor = getTypeDescriptor(mBinding.type); assert typeDescriptor != null; // because mBinding.type is known not to be null return typeDescriptor; } @Override public int getModifiers() { return mBinding.modifiers; } @NonNull @Override public Iterable<ResolvedAnnotation> getAnnotations() { AnnotationBinding[] annotations = mBinding.getAnnotations(); int count = annotations.length; if (count > 0) { List<ResolvedAnnotation> result = Lists.newArrayListWithExpectedSize(count); for (AnnotationBinding annotation : annotations) { if (annotation != null) { result.add(new EcjResolvedAnnotation(annotation)); } } return result; } // No external annotations for variables return Collections.emptyList(); } @Override public String getSignature() { return mBinding.toString(); } @SuppressWarnings("RedundantIfStatement") @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } EcjResolvedVariable that = (EcjResolvedVariable) o; if (mBinding != null ? !mBinding.equals(that.mBinding) : that.mBinding != null) { return false; } return true; } @Override public int hashCode() { return mBinding != null ? mBinding.hashCode() : 0; } } private class EcjResolvedAnnotation extends ResolvedAnnotation { private AnnotationBinding mBinding; private EcjResolvedAnnotation(@NonNull final AnnotationBinding binding) { mBinding = binding; } @NonNull @Override public String getName() { return new String(mBinding.getAnnotationType().readableName()); } @Override public boolean matches(@NonNull String name) { return sameChars(name, mBinding.getAnnotationType().readableName()); } @NonNull @Override public TypeDescriptor getType() { TypeDescriptor typeDescriptor = getTypeDescriptor(mBinding.getAnnotationType()); assert typeDescriptor != null; // because mBinding.type is known not to be null return typeDescriptor; } @Override public ResolvedClass getClassType() { ReferenceBinding annotationType = mBinding.getAnnotationType(); return new EcjResolvedClass(annotationType) { @NonNull @Override public Iterable<ResolvedAnnotation> getAnnotations() { AnnotationBinding[] annotations = mBinding.getAnnotations(); int count = annotations.length; if (count > 0) { List<ResolvedAnnotation> result = Lists.newArrayListWithExpectedSize(count); for (AnnotationBinding annotation : annotations) { if (annotation != null) { // Special case: If you look up the annotations *on* annotations, // you're probably working with the typedef annotations, @IntDef // and @StringDef. For these, we can't use the normal annotation // handling, because the compiler only keeps the values of the // constants, not the references to the constants which is what we // care about for those annotations. So in this case, construct // a special subclass of ResolvedAnnotation: EcjAstAnnotation, where // we keep the AST node for the annotation definition such that // we can look up the constant references themselves when queries // via the annotation's getValue() lookup methods. char[] readableName = annotation.getAnnotationType().readableName(); if (sameChars(INT_DEF_ANNOTATION, readableName) || sameChars(STRING_DEF_ANNOTATION, readableName)) { TypeDeclaration typeDeclaration = findTypeDeclaration(getName()); if (typeDeclaration != null && typeDeclaration.annotations != null) { Annotation astAnnotation = null; for (Annotation a : typeDeclaration.annotations) { if (a.resolvedType != null && (sameChars(INT_DEF_ANNOTATION, a.resolvedType.readableName()) || sameChars(STRING_DEF_ANNOTATION, a.resolvedType.readableName()))) { astAnnotation = a; break; } } if (astAnnotation != null) { result.add(new EcjAstAnnotation(annotation, astAnnotation)); continue; } } } result.add(new EcjResolvedAnnotation(annotation)); } } return result; } return Collections.emptyList(); } }; } @NonNull @Override public List<Value> getValues() { ElementValuePair[] pairs = mBinding.getElementValuePairs(); if (pairs != null && pairs.length > 0) { List<Value> values = Lists.newArrayListWithExpectedSize(pairs.length); for (ElementValuePair pair : pairs) { values.add(new Value(new String(pair.getName()), getPairValue(pair))); } } return Collections.emptyList(); } @Nullable @Override public Object getValue(@NonNull String name) { ElementValuePair[] pairs = mBinding.getElementValuePairs(); if (pairs != null) { for (ElementValuePair pair : pairs) { if (sameChars(name, pair.getName())) { return getPairValue(pair); } } } return null; } private Object getPairValue(ElementValuePair pair) { return getConstantValue(pair.getValue()); } @Override public String getSignature() { return new String(mBinding.getAnnotationType().readableName()); } @Override public int getModifiers() { // Not applicable; move from ResolvedNode into ones that matter? return 0; } @NonNull @Override public Iterable<ResolvedAnnotation> getAnnotations() { List<ResolvedAnnotation> compiled = null; AnnotationBinding[] annotations = mBinding.getAnnotationType().getAnnotations(); int count = annotations.length; if (count > 0) { compiled = Lists.newArrayListWithExpectedSize(count); for (AnnotationBinding annotation : annotations) { if (annotation != null) { compiled.add(new EcjResolvedAnnotation(annotation)); } } } // Look for external annotations ExternalAnnotationRepository manager = ExternalAnnotationRepository.get(mClient); Collection<ResolvedAnnotation> external = manager.getAnnotations(this); return merge(compiled, external); } private class EcjAstAnnotation extends EcjResolvedAnnotation { private final Annotation mAstAnnotation; private List<Value> mValues; public EcjAstAnnotation( @NonNull AnnotationBinding binding, @NonNull Annotation astAnnotation) { super(binding); mAstAnnotation = astAnnotation; } @NonNull @Override public List<Value> getValues() { if (mValues == null) { MemberValuePair[] memberValuePairs = mAstAnnotation.memberValuePairs(); List<Value> result = Lists .newArrayListWithExpectedSize(memberValuePairs.length); for (MemberValuePair pair : memberValuePairs) { // String n = new String(pair.name); Expression expression = pair.value; Object value = null; if (expression instanceof ArrayInitializer) { ArrayInitializer initializer = (ArrayInitializer) expression; Expression[] expressions = initializer.expressions; List<Object> values = Lists.newArrayList(); for (Expression e : expressions) { if (e instanceof NameReference) { ResolvedNode resolved = resolve(((NameReference) e).binding); if (resolved != null) { values.add(resolved); } } else if (e instanceof IntLiteral) { values.add(((IntLiteral) e).value); } else if (e instanceof StringLiteral) { values.add(String.valueOf(((StringLiteral) e).source())); } else { values.add(e.toString()); } } value = values.toArray(); } else if (expression instanceof IntLiteral) { IntLiteral intLiteral = (IntLiteral) expression; value = intLiteral.value; } else if (expression instanceof TrueLiteral) { value = true; } else if (expression instanceof FalseLiteral) { value = false; } else if (expression instanceof StringLiteral) { value = String.valueOf(((StringLiteral) expression).source()); } // Unfortunately, FloatLiteral, LongLiteral etc do not // expose the value field as public. Luckily, we don't need that // for our current annotations. result.add(new Value(new String(pair.name), value)); } mValues = result; } return mValues; } @Nullable @Override public Object getValue(@NonNull String name) { for (Value value : getValues()) { if (name.equals(value.name)) { return value.value; } } return null; } } @SuppressWarnings("RedundantIfStatement") @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } EcjResolvedAnnotation that = (EcjResolvedAnnotation) o; if (mBinding != null ? !mBinding.equals(that.mBinding) : that.mBinding != null) { return false; } return true; } @Override public int hashCode() { return mBinding != null ? mBinding.hashCode() : 0; } } @Nullable private Object getConstantValue(@Nullable Object value) { if (value instanceof Constant) { if (value == Constant.NotAConstant) { return null; } if (value instanceof StringConstant) { return ((StringConstant) value).stringValue(); } else if (value instanceof IntConstant) { return ((IntConstant) value).intValue(); } else if (value instanceof BooleanConstant) { return ((BooleanConstant) value).booleanValue(); } else if (value instanceof FloatConstant) { return ((FloatConstant) value).floatValue(); } else if (value instanceof LongConstant) { return ((LongConstant) value).longValue(); } else if (value instanceof DoubleConstant) { return ((DoubleConstant) value).doubleValue(); } else if (value instanceof ShortConstant) { return ((ShortConstant) value).shortValue(); } else if (value instanceof CharConstant) { return ((CharConstant) value).charValue(); } else if (value instanceof ByteConstant) { return ((ByteConstant) value).byteValue(); } } else if (value instanceof Object[]) { Object[] array = (Object[]) value; if (array.length > 0) { List<Object> list = Lists.newArrayListWithExpectedSize(array.length); for (Object element : array) { list.add(getConstantValue(element)); } // Pick type of array. Annotations are limited to Strings, Classes // and Annotations if (!list.isEmpty()) { Object first = list.get(0); if (first instanceof String) { //noinspection SuspiciousToArrayCall return list.toArray(new String[list.size()]); } else if (first instanceof java.lang.annotation.Annotation) { //noinspection SuspiciousToArrayCall return list.toArray(new Annotation[list.size()]); } else if (first instanceof Class) { //noinspection SuspiciousToArrayCall return list.toArray(new Class[list.size()]); } } return list.toArray(); } } else if (value instanceof AnnotationBinding) { return new EcjResolvedAnnotation((AnnotationBinding) value); } return value; } private static boolean sameChars(String str, char[] chars) { int length = str.length(); if (chars.length != length) { return false; } for (int i = 0; i < length; i++) { if (chars[i] != str.charAt(i)) { return false; } } return true; } /** * Does the given compound name match the given string? * <p> * TODO: Check if ECJ already has this as a utility somewhere */ @VisibleForTesting static boolean startsWithCompound(@NonNull String name, @NonNull char[][] compoundName) { int length = name.length(); if (length == 0) { return false; } int index = 0; for (int i = 0, n = compoundName.length; i < n; i++) { char[] o = compoundName[i]; for (int j = 0, m = o.length; j < m; j++) { if (index == length) { return false; // Don't allow prefix in a compound name } if (name.charAt(index) != o[j]) { return false; } index++; } if (i < n - 1) { if (index == length) { return true; } if (name.charAt(index) != '.') { return false; } index++; if (index == length) { return true; } } } return index == length; } @VisibleForTesting static boolean equalsCompound(@NonNull String name, @NonNull char[][] compoundName) { int length = name.length(); if (length == 0) { return false; } int index = 0; for (int i = 0, n = compoundName.length; i < n; i++) { char[] o = compoundName[i]; for (int j = 0, m = o.length; j < m; j++) { if (index == length) { return false; // Don't allow prefix in a compound name } if (name.charAt(index) != o[j]) { return false; } index++; } if (i < n - 1) { if (index == length) { return false; } if (name.charAt(index) != '.') { return false; } index++; if (index == length) { return false; } } } return index == length; } }