| /******************************************************************************* |
| * Copyright (c) 2005 BEA Systems, Inc. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * tyeung@bea.com - initial API and implementation |
| *******************************************************************************/ |
| |
| package org.eclipse.jdt.apt.core.internal.env; |
| |
| import java.io.BufferedInputStream; |
| import java.io.CharArrayWriter; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.Reader; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.regex.Pattern; |
| |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.core.resources.IMarker; |
| import org.eclipse.core.resources.IResource; |
| import org.eclipse.core.resources.IWorkspace; |
| import org.eclipse.core.resources.IWorkspaceRoot; |
| import org.eclipse.core.resources.IWorkspaceRunnable; |
| import org.eclipse.core.resources.ResourcesPlugin; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IPath; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.Path; |
| import org.eclipse.jdt.apt.core.AptPlugin; |
| import org.eclipse.jdt.apt.core.env.EclipseAnnotationProcessorEnvironment; |
| import org.eclipse.jdt.apt.core.env.Phase; |
| import org.eclipse.jdt.apt.core.internal.EclipseMirrorImpl; |
| import org.eclipse.jdt.apt.core.internal.declaration.TypeDeclarationImpl; |
| import org.eclipse.jdt.apt.core.internal.env.MessagerImpl.Severity; |
| import org.eclipse.jdt.apt.core.internal.util.Factory; |
| import org.eclipse.jdt.apt.core.internal.util.Visitors.AnnotatedNodeVisitor; |
| import org.eclipse.jdt.apt.core.internal.util.Visitors.AnnotationVisitor; |
| import org.eclipse.jdt.apt.core.util.AptConfig; |
| import org.eclipse.jdt.apt.core.util.EclipseMessager; |
| import org.eclipse.jdt.core.BindingKey; |
| import org.eclipse.jdt.core.ICompilationUnit; |
| import org.eclipse.jdt.core.IJavaProject; |
| import org.eclipse.jdt.core.JavaCore; |
| import org.eclipse.jdt.core.compiler.IProblem; |
| import org.eclipse.jdt.core.dom.AST; |
| import org.eclipse.jdt.core.dom.ASTNode; |
| import org.eclipse.jdt.core.dom.ASTParser; |
| import org.eclipse.jdt.core.dom.ASTVisitor; |
| import org.eclipse.jdt.core.dom.AbstractTypeDeclaration; |
| import org.eclipse.jdt.core.dom.Annotation; |
| import org.eclipse.jdt.core.dom.CompilationUnit; |
| import org.eclipse.jdt.core.dom.IBinding; |
| import org.eclipse.jdt.core.dom.ITypeBinding; |
| import org.eclipse.jdt.core.dom.SimpleName; |
| |
| import com.sun.mirror.apt.AnnotationProcessorListener; |
| import com.sun.mirror.apt.Filer; |
| import com.sun.mirror.declaration.AnnotationTypeDeclaration; |
| import com.sun.mirror.declaration.PackageDeclaration; |
| import com.sun.mirror.declaration.TypeDeclaration; |
| |
| public class ProcessorEnvImpl extends BaseProcessorEnv implements EclipseAnnotationProcessorEnvironment |
| { |
| private static final boolean ENABLE_GENERATED_FILE_LISTENER = false; |
| public static final String BUILD_MARKER = "org.eclipse.jdt.apt.core.marker"; //$NON-NLS-1$ |
| public static final ICompilationUnit[] NO_UNIT = new ICompilationUnit[0]; |
| /** delimiter of path variables in -A values, e.g., %ROOT%/foo */ |
| private static final char PATHVAR_DELIM = '%'; |
| /** regex to identify substituted token in path variables */ |
| private static final String PATHVAR_TOKEN = "^%[^%/\\\\ ]+%.*"; //$NON-NLS-1$ |
| /** path variable meaning "workspace root" */ |
| private static final String PATHVAR_ROOT = "%ROOT%"; //$NON-NLS-1$ |
| |
| /** |
| * The compilation unit of the file that is being processed in reconcile |
| * or in file-based mode of build. |
| */ |
| private ICompilationUnit _unit; |
| private Map<IFile, List<IProblem>> _allProblems; |
| |
| // Stores the generated java files from parent to child |
| private Map<IFile, Set<IFile>> _allGeneratedFiles = new HashMap<IFile, Set<IFile>>(); |
| private Set<IFile> _modifiedGeneratedFiles = new HashSet<IFile>(); |
| private Set<AnnotationProcessorListener> _listeners = null; |
| private final FilerImpl _filer; |
| private boolean _sourcePathChanged; |
| private boolean _isClosed = false; |
| |
| /** |
| * Set of strings that indicate new type dependencies introduced on the file |
| * each string is a fully-qualified type name. |
| */ |
| private Map<IFile, Set<String>> _typeDependencies = new HashMap<IFile, Set<String>>(); |
| |
| /** |
| * Processor options, including -A options. |
| * Set in ctor and then not changed. |
| */ |
| private Map<String, String> _options; |
| |
| /** |
| * Indicates whether we are in batch mode or not. This gets flipped only |
| * during build and could be flipped back and forth. |
| */ |
| private boolean _batchMode = false; // off by default. |
| |
| /** |
| * Holds all the files that contains annotation that are to be processed during build. |
| * If we are not in batch mode (reconcile time or file-based dispatch during build), |
| * <code>super._file</code> holds the file being processed at the time. |
| */ |
| private IFile[] _filesWithAnnotation = null; |
| |
| /** |
| * These are files that are part of a build but does not have annotations on it. |
| * During batch mode processing, these files still also need to be included. |
| */ |
| private IFile[] _additionFiles = null; |
| /** |
| * This is intialized when <code>_batchMode</code> is set to be <code>true</code> or |
| * when batch processing is expected. @see #getAllAnnotationTypes(Map) |
| */ |
| private CompilationUnit[] _astUnits = null; |
| /** |
| * <code>ICompilationUnit</code> parallel to the <code>CompilationUnit</code>s in |
| * <code>_astUnits</code> |
| */ |
| private ICompilationUnit[] _units = null; |
| private List<MarkerInfo> _markerInfos = null; |
| |
| public static ProcessorEnvImpl newReconcileEnv(ICompilationUnit compilationUnit, IJavaProject javaProj) |
| { |
| CompilationUnit domUnit = createDietAST( javaProj, compilationUnit ); |
| return new ProcessorEnvImpl( domUnit, compilationUnit, javaProj); |
| } |
| |
| /** |
| * @param filesWithAnnotation files that have annotation. |
| * @param units compilation unit associated with <code>filesWithAnnotation</code> |
| * @param javaProj |
| * @return a new processor environment. |
| */ |
| public static ProcessorEnvImpl newBuildEnvInternalRounding( |
| IFile[] filesWithAnnotation, |
| ICompilationUnit[] units, |
| IJavaProject javaProj) |
| { |
| assert filesWithAnnotation != null : "missing files"; //$NON-NLS-1$ |
| return new ProcessorEnvImpl(filesWithAnnotation, null, units, javaProj, Phase.BUILD); |
| } |
| |
| public static ProcessorEnvImpl newBuildEnv( |
| IFile[] filesWithAnnotation, |
| IFile[] additionalFiles, |
| IJavaProject javaProj ) |
| { |
| assert filesWithAnnotation != null : "missing files"; //$NON-NLS-1$ |
| |
| // note, we are not reading any files. |
| return new ProcessorEnvImpl(filesWithAnnotation, additionalFiles, null, javaProj, Phase.BUILD); |
| } |
| |
| /** |
| * Constructor for creating a processor environment used during reconcile |
| * @param astCompilationUnit |
| * @param compilationUnit |
| * @param file |
| * @param javaProj |
| * @param phase |
| */ |
| private ProcessorEnvImpl( |
| final CompilationUnit astCompilationUnit, |
| final ICompilationUnit compilationUnit, |
| final IJavaProject javaProj ) |
| { |
| super( astCompilationUnit, (IFile)compilationUnit.getResource(), javaProj, Phase.RECONCILE ); |
| |
| _unit = compilationUnit; |
| _filer = new FilerImpl(this); |
| _allProblems = new HashMap<IFile, List<IProblem>>(); |
| initOptions(javaProj); |
| } |
| |
| /** |
| * Constructor for creating a processor environment used during build. |
| * @param filesWithAnnotations |
| * @param additionalFiles |
| * @param units |
| * @param javaProj |
| * @param phase |
| */ |
| private ProcessorEnvImpl( |
| final IFile[] filesWithAnnotations, |
| final IFile[] additionalFiles, |
| final ICompilationUnit[] units, |
| final IJavaProject javaProj, |
| final Phase phase) { |
| |
| super(null, null, javaProj, phase); |
| |
| _unit = null; |
| _units = units; |
| _filer = new FilerImpl(this); |
| _filesWithAnnotation = filesWithAnnotations; |
| _additionFiles = additionalFiles; |
| _allProblems = new HashMap<IFile, List<IProblem>>(); |
| _markerInfos = new ArrayList<MarkerInfo>(); |
| initOptions(javaProj); |
| } |
| |
| |
| /** |
| * Set the _options map based on the current project/workspace settings. |
| * There is a bug in Sun's apt implementation: it parses the command line |
| * incorrectly, such that -Akey=value gets added to the options map as |
| * key "-Akey=value" and value "". In order to support processors written |
| * to run on Sun's apt as well as processors written without this bug |
| * in mind, we populate the map with two copies of every option, one the |
| * expected way ("key" / "value") and the other the Sun way |
| * ("-Akey=value" / ""). We make exceptions for the non-dash-A options |
| * that we set automatically, such as -classpath, -target, and so forth; |
| * since these wouldn't have come from a -A option we don't construct a |
| * -Akey=value variant. |
| * |
| * Called from constructor. A new Env is constructed for each build pass, |
| * so this will always be up to date with the latest settings. |
| */ |
| private void initOptions(IJavaProject jproj) { |
| Map<String, String> procOptions = AptConfig.getProcessorOptions(jproj); |
| _options = new HashMap<String, String>(procOptions.size() * 2); |
| |
| // Add configured options |
| for (Map.Entry<String, String> entry : procOptions.entrySet()) { |
| String value = resolveVarPath(entry.getValue()); |
| String key = entry.getKey(); |
| _options.put(key, value); |
| if (!AptConfig.isAutomaticProcessorOption(key)) { |
| String sunStyle; |
| if (value != null) { |
| sunStyle = "-A" + entry.getKey() + "=" + value; //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| else { |
| sunStyle = "-A" + entry.getKey(); //$NON-NLS-1$ |
| } |
| _options.put(sunStyle, ""); //$NON-NLS-1$ |
| } |
| } |
| } |
| |
| /** |
| * If the value starts with a path variable such as %ROOT%, replace it with |
| * the absolute path. |
| * @param value the value of a -Akey=value command option |
| */ |
| private String resolveVarPath(String value) { |
| if (value == null) { |
| return null; |
| } |
| // is there a token to substitute? |
| if (!Pattern.matches(PATHVAR_TOKEN, value)) { |
| return value; |
| } |
| IPath path = new Path(value); |
| String firstToken = path.segment(0); |
| // If it matches %ROOT%/project, it is a project-relative path. |
| if (PATHVAR_ROOT.equals(firstToken)) { |
| IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); |
| IResource proj = root.findMember(path.segment(1)); |
| if (proj == null) { |
| return value; |
| } |
| // all is well; do the substitution |
| IPath relativePath = path.removeFirstSegments(2); |
| IPath absoluteProjPath = proj.getLocation(); |
| IPath absoluteResPath = absoluteProjPath.append(relativePath); |
| return absoluteResPath.toOSString(); |
| } |
| |
| // otherwise it's a classpath-var-based path. |
| String cpvName = firstToken.substring(1, firstToken.length() - 1); |
| IPath cpvPath = JavaCore.getClasspathVariable(cpvName); |
| if (cpvPath != null) { |
| IPath resolved = cpvPath.append(path.removeFirstSegments(1)); |
| return resolved.toOSString(); |
| } |
| else { |
| return value; |
| } |
| } |
| |
| |
| public Filer getFiler() |
| { |
| checkValid(); |
| return _filer; |
| } |
| |
| public EclipseMessager getMessager() |
| { |
| checkValid(); |
| return new MessagerImpl(this); |
| } |
| |
| public Map<String, String> getOptions() |
| { |
| final HashMap<String, String> options = new HashMap<String, String>(_options); |
| options.put("phase", getPhase().toString()); //$NON-NLS-1$ |
| return options; |
| } |
| |
| public PackageDeclaration getPackage(String name) |
| { |
| checkValid(); |
| return super.getPackage(name); |
| } |
| |
| public TypeDeclaration getTypeDeclaration(String name) |
| { |
| checkValid(); |
| TypeDeclaration decl = null; |
| if( !_batchMode ){ |
| // we are not keeping dependencies unless we are processing on a |
| // per file basis. |
| decl = super.getTypeDeclaration(name); |
| addTypeDependency( name ); |
| } |
| else |
| decl = getTypeDeclarationInBatch(name); |
| |
| return decl; |
| } |
| |
| private TypeDeclaration getTypeDeclarationInBatch(String name) |
| { |
| if( name == null || _astUnits == null ) return null; |
| // get rid of the generics parts. |
| final int index = name.indexOf('<'); |
| if( index != -1 ) |
| name = name.substring(0, index); |
| |
| // first see if it is one of the well known types. |
| // any AST is as good as the other. |
| ITypeBinding typeBinding = null; |
| String typeKey = BindingKey.createTypeBindingKey(name); |
| if( _astUnits.length > 0 ){ |
| _astUnits[0].getAST().resolveWellKnownType(name); |
| |
| if(typeBinding == null){ |
| // then look into the current compilation units |
| ASTNode node = null; |
| for( int i=0, len=_astUnits.length; i<len; i++ ) |
| node = _astUnits[i].findDeclaringNode(typeKey); |
| if( node != null ){ |
| final int nodeType = node.getNodeType(); |
| if( nodeType == ASTNode.TYPE_DECLARATION || |
| nodeType == ASTNode.ANNOTATION_TYPE_DECLARATION || |
| nodeType == ASTNode.ENUM_DECLARATION ) |
| typeBinding = ((AbstractTypeDeclaration)node).resolveBinding(); |
| } |
| } |
| if( typeBinding != null ) |
| return Factory.createReferenceType(typeBinding, this); |
| } |
| |
| // finally go search for it in the universe. |
| typeBinding = getTypeBinding(typeKey); |
| if( typeBinding != null ){ |
| return Factory.createReferenceType(typeBinding, this); |
| } |
| |
| return null; |
| } |
| |
| public void addListener(AnnotationProcessorListener listener) |
| { |
| checkValid(); |
| if(_listeners == null ) |
| _listeners = new HashSet<AnnotationProcessorListener>(); |
| _listeners.add(listener); |
| } |
| |
| public void removeListener(AnnotationProcessorListener listener) |
| { |
| checkValid(); |
| if( _listeners == null ) return; |
| _listeners.remove(listener); |
| } |
| |
| public Set<AnnotationProcessorListener> getProcessorListeners() |
| { |
| if( _listeners == null ) |
| return Collections.emptySet(); |
| return Collections.unmodifiableSet(_listeners); |
| } |
| |
| public void addGeneratedFile( IFile f, boolean contentsChanged ) { |
| // Add first to the map of parent -> child |
| IFile parent = getFile(); |
| Set<IFile> children = _allGeneratedFiles.get(parent); |
| if (children == null) { |
| children = new HashSet<IFile>(); |
| _allGeneratedFiles.put(parent, children); |
| } |
| children.add(f); |
| |
| if (contentsChanged) |
| _modifiedGeneratedFiles.add(f); |
| } |
| |
| public ICompilationUnit getCompilationUnit(){ return _unit; } |
| |
| public Map<IFile, Set<IFile>> getAllGeneratedFiles(){ return _allGeneratedFiles; } |
| |
| public Set<IFile> getModifiedGeneratedFiles() { return _modifiedGeneratedFiles; } |
| |
| /** |
| * @return true iff source files has been generated. |
| * Always return false when this environment is closed. |
| */ |
| public boolean hasGeneratedSourceFiles(){ return !_allGeneratedFiles.isEmpty(); } |
| |
| /** |
| * @return true iff class files has been generated. |
| * Always return false when this environment is closed. |
| */ |
| public boolean hasGeneratedClassFiles(){ return _filer.hasGeneratedClassFile(); } |
| |
| /** |
| * @return true iff errors (markers with serverity == APTProblem.Severity.Error) has been posted |
| * Always return false when this environment is closed. |
| */ |
| public boolean hasRaisedErrors() |
| { |
| checkValid(); |
| for(List<IProblem> problems : _allProblems.values() ) |
| { |
| for(IProblem problem : problems ){ |
| if( problem.isError() ) |
| return true; |
| } |
| } |
| |
| if( _markerInfos != null ){ |
| for(MarkerInfo markerInfo : _markerInfos){ |
| if( markerInfo.isError() ) |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * |
| * reads a given file's contents and returns them as a char array. |
| * |
| * @param file |
| * @return |
| * @throws CoreException |
| */ |
| public static char[] getFileContents( IFile file ) |
| throws CoreException, IOException |
| { |
| Reader reader = null; |
| CharArrayWriter w = null; |
| |
| try |
| { |
| reader = getFileReader( file ); |
| w = new CharArrayWriter( 4096 ); |
| int c = -1; |
| while ( ( c = reader.read() ) > -1 ) |
| w.write( c ); |
| return w.toCharArray(); |
| } |
| finally |
| { |
| try { if ( reader != null ) reader.close(); } catch ( IOException ioe ) {}; |
| if ( w != null ) w.close(); |
| } |
| } |
| |
| public static InputStreamReader getFileReader( final IFile file ) throws IOException, CoreException { |
| return new InputStreamReader(getInputStream(file), file.getCharset()); |
| } |
| |
| public static InputStream getInputStream( final IFile file ) throws IOException, CoreException { |
| return new BufferedInputStream(file.getContents()); |
| } |
| |
| /* (non-Javadoc) |
| * Once the environment is closed the following is not allowed |
| * 1) posting messge |
| * 2) generating file |
| * 3) retrieving type or package by name |
| * 4) add or remove listeners |
| */ |
| public void close(){ |
| if( _isClosed ) |
| return; |
| postMarkers(); |
| _markerInfos = null; |
| _astRoot = null; |
| _file = null; |
| _astUnits = null; |
| _filesWithAnnotation = null; |
| _units = null; |
| _allProblems = null; |
| _modelCompUnit2astCompUnit.clear(); |
| _allGeneratedFiles = null; |
| _modifiedGeneratedFiles = null; |
| if(_listeners != null) |
| _listeners.clear(); |
| _isClosed = true; |
| } |
| |
| /* package */ void checkValid() |
| { |
| if( _isClosed ) |
| throw new IllegalStateException("Environment has expired"); //$NON-NLS-1$ |
| } |
| |
| |
| /** |
| * |
| * @param resource null to indicate current resource |
| * @param start the starting offset of the marker |
| * @param end -1 to indicate unknow ending offset. |
| * @param severity the severity of the marker |
| * @param msg the message on the marker |
| * @param line the line number of where the marker should be |
| */ |
| void addMessage(IFile resource, |
| int start, |
| int end, |
| Severity severity, |
| String msg, |
| int line, |
| String[] arguments) |
| { |
| checkValid(); |
| |
| if( resource == null ) |
| resource = getFile(); |
| |
| // not going to post any markers to resource outside of the one we are currently |
| // processing during reconcile phase. |
| if( _phase == Phase.RECONCILE && resource != null && !resource.equals( getFile() ) ) |
| return; |
| |
| // Eclipse doesn't support INFO-level IProblems, so we send them to the log instead. |
| if ( _phase != Phase.RECONCILE && severity == Severity.INFO) { |
| StringBuilder sb = new StringBuilder(); |
| sb.append("Informational message reported by annotation processor:\n"); //$NON-NLS-1$ |
| sb.append(msg); |
| sb.append("\n"); //$NON-NLS-1$ |
| if (resource != null) { |
| sb.append("Resource="); //$NON-NLS-1$ |
| sb.append(resource.getName()); |
| sb.append("; "); //$NON-NLS-1$ |
| } |
| sb.append("starting offset="); //$NON-NLS-1$ |
| sb.append(start); |
| sb.append("; ending offset="); //$NON-NLS-1$ |
| sb.append(end); |
| sb.append("; line="); //$NON-NLS-1$ |
| sb.append(line); |
| if (arguments != null) { |
| sb.append("; arguments:"); //$NON-NLS-1$ |
| for (String s : arguments) { |
| sb.append("\n"); //$NON-NLS-1$ |
| sb.append(s); |
| } |
| } |
| else { |
| sb.append("\n"); //$NON-NLS-1$ |
| } |
| IStatus status = AptPlugin.createInfoStatus(null, sb.toString()); |
| AptPlugin.log(status); |
| return; |
| } |
| |
| if( resource == null ){ |
| assert _batchMode : "not in batch mode but don't know about current resource"; //$NON-NLS-1$ |
| addMarker(start, end, severity, msg, line, arguments); |
| } |
| else |
| addProblem(resource, start, end, severity, msg, line, arguments); |
| |
| } |
| |
| private void addProblem( |
| IFile resource, |
| int start, |
| int end, |
| Severity severity, |
| String msg, |
| int line, |
| String[] arguments) |
| { |
| // end-1 since IProblem ending offsets are inclusive but DOM layer |
| // ending offsets are exclusive. |
| final APTProblem newProblem = |
| new APTProblem(msg, severity, resource, start, end-1, line, arguments); |
| List<IProblem> problems = _allProblems.get(resource); |
| if( problems == null ){ |
| problems = new ArrayList<IProblem>(4); |
| _allProblems.put(resource, problems); |
| } |
| problems.add(newProblem); |
| } |
| |
| private void addMarker( |
| int start, |
| int end, |
| Severity severity, |
| String msg, |
| int line, |
| String[] arguments) |
| { |
| // Note that the arguments are ignored -- no quick-fix for markers. |
| _markerInfos.add(new MarkerInfo(start, end, severity, msg, line)); |
| } |
| |
| public Map<IFile, List<IProblem>> getProblems() |
| { |
| checkValid(); |
| |
| updateProblemLength(); |
| return _allProblems; |
| } |
| |
| public Map<String, AnnotationTypeDeclaration> getAnnotationTypesInFile() |
| { |
| checkValid(); |
| assert _astRoot != null && _file != null && !_batchMode : |
| "operation not available under batch mode."; //$NON-NLS-1$ |
| final List<Annotation> instances = new ArrayList<Annotation>(); |
| final Map<String, AnnotationTypeDeclaration> decls = |
| new HashMap<String, AnnotationTypeDeclaration>(); |
| final AnnotationVisitor visitor = new AnnotationVisitor(instances); |
| _astRoot.accept(visitor); |
| |
| for (int instanceIndex=0, size = instances.size(); instanceIndex < size; instanceIndex++) { |
| final Annotation instance = instances.get(instanceIndex); |
| final ITypeBinding annoType = instance.resolveTypeBinding(); |
| if (annoType == null) |
| continue; |
| final TypeDeclarationImpl decl = |
| Factory.createReferenceType(annoType, this); |
| if (decl.kind() == EclipseMirrorImpl.MirrorKind.TYPE_ANNOTATION){ |
| final AnnotationTypeDeclaration annoDecl = (AnnotationTypeDeclaration)decl; |
| decls.put(annoDecl.getQualifiedName(), annoDecl); |
| } |
| } |
| |
| return decls; |
| } |
| |
| /** |
| * Return all annotations at declaration level within all compilation unit(s) |
| * associated with this environment. All the files associated with this environment will |
| * be parsed and resolved for all declaration level elements at the return of this call. |
| * |
| * @param file2Annotations populated by this method to map files to the annotation types |
| * if contains. May be null. |
| * @return the map containing all annotation types found within this environment. |
| */ |
| public Map<String, AnnotationTypeDeclaration> getAllAnnotationTypes( |
| final Map<IFile, Set<AnnotationTypeDeclaration>> file2Annotations) { |
| |
| checkValid(); |
| if( _filesWithAnnotation == null ) |
| return getAnnotationTypesInFile(); |
| createDomASTs(); |
| |
| final List<Annotation> instances = new ArrayList<Annotation>(); |
| final Map<String, AnnotationTypeDeclaration> decls = |
| new HashMap<String, AnnotationTypeDeclaration>(); |
| final AnnotationVisitor visitor = new AnnotationVisitor(instances); |
| for( int astIndex=0, len=_astUnits.length; astIndex<len; astIndex++ ){ |
| if( _astUnits == null || _astUnits[astIndex] == null ) |
| System.err.println(); |
| _astUnits[astIndex].accept(visitor); |
| final Set<AnnotationTypeDeclaration> perFileAnnos = new HashSet<AnnotationTypeDeclaration>(); |
| |
| for (int instanceIndex=0, size = instances.size(); instanceIndex < size; instanceIndex++) { |
| final Annotation instance = instances.get(instanceIndex); |
| final ITypeBinding annoType = instance.resolveTypeBinding(); |
| if (annoType == null) |
| continue; |
| final TypeDeclarationImpl decl = |
| Factory.createReferenceType(annoType, this); |
| if (decl.kind() == EclipseMirrorImpl.MirrorKind.TYPE_ANNOTATION){ |
| final AnnotationTypeDeclaration annoDecl = (AnnotationTypeDeclaration)decl; |
| decls.put(annoDecl.getQualifiedName(), annoDecl); |
| perFileAnnos.add(annoDecl); |
| } |
| } |
| if( file2Annotations != null && !perFileAnnos.isEmpty() ) |
| file2Annotations.put(_filesWithAnnotation[astIndex], perFileAnnos); |
| visitor.reset(); |
| } |
| |
| return decls; |
| } |
| |
| /** |
| * @param file |
| * @return length 3 int array with the following information. |
| * at index 0: contains the starting offset, always >= 0 |
| * at index 1: contains the ending offset, may be a negative number. |
| * at index 2: the line number |
| * |
| */ |
| private int[] getClassNameRange(final IFile file){ |
| final CompilationUnit astUnit = getAstCompilationUnit(file); |
| int[] startAndEnd = null; |
| if( astUnit != null){ |
| @SuppressWarnings({"unchecked", "nls"}) |
| final List<AbstractTypeDeclaration> topTypes = astUnit.types(); |
| if( topTypes != null && topTypes.size() > 0 ){ |
| final AbstractTypeDeclaration topType = topTypes.get(0); |
| startAndEnd = new int[3]; |
| final SimpleName typename = topType.getName(); |
| if( typename != null ){ |
| startAndEnd[0] = typename.getStartPosition(); |
| // ending offsets need to be exclusive. |
| startAndEnd[1] = startAndEnd[0] + typename.getLength() - 1; |
| startAndEnd[2] = astUnit.lineNumber(typename.getStartPosition()); |
| } |
| else{ |
| startAndEnd[0] = topType.getStartPosition(); |
| // let case 2 in updateProblemLength() kicks in. |
| startAndEnd[1] = -2; |
| startAndEnd[2] = astUnit.lineNumber(topType.getStartPosition()); |
| } |
| } |
| } |
| if( startAndEnd == null ) |
| // let case 2 in updateProblemLength() kicks in. |
| return new int[]{0, -2, 1}; |
| |
| return startAndEnd; |
| } |
| |
| /** |
| * Handling the following 2 cases |
| * 1) For IProblems that does not have a starting and ending offset, |
| * place the problem at the class name. |
| * |
| * 2) For IProblems that does not have an ending offset, place the ending |
| * offset at the end of the tightest ast node. |
| * We will only walk the ast once to determine the ending |
| * offsets of all the problems that do not have the information set. |
| */ |
| private void updateProblemLength() |
| { |
| // for those problems that doesn't have an ending offset, figure it out by |
| // traversing the ast. |
| // we do it once just before we post the marker so we only have to walk the ast |
| // once. |
| for( Map.Entry<IFile, List<IProblem>> entry : _allProblems.entrySet() ){ |
| int count = 0; |
| final IFile file = entry.getKey(); |
| int[] classNameRange = null; |
| for( IProblem problem : entry.getValue() ){ |
| if( problem.getSourceStart() < 0 ){ |
| if( classNameRange == null ) |
| classNameRange = getClassNameRange(file); |
| problem.setSourceStart(classNameRange[0]); |
| problem.setSourceEnd(classNameRange[1]); |
| problem.setSourceLineNumber(classNameRange[2]); |
| } |
| if( problem.getSourceEnd() < 0 ){ |
| count ++; |
| } |
| } |
| |
| if( count > 0 ){ |
| final CompilationUnit astUnit = getAstCompilationUnit(file); |
| if( astUnit != null ){ |
| |
| final int[] startingOffsets = new int[count]; |
| int index = 0; |
| for( IProblem problem : entry.getValue() ){ |
| if( problem.getSourceEnd() < 0 ) |
| startingOffsets[index++] = problem.getSourceStart(); |
| } |
| |
| final EndingOffsetFinder lfinder = new EndingOffsetFinder(startingOffsets); |
| |
| astUnit.accept( lfinder ); |
| |
| for(IProblem problem : entry.getValue() ){ |
| if( problem.getSourceEnd() < 0 ){ |
| int startingOffset = problem.getSourceStart(); |
| int endingOffset = lfinder.getEndingOffset(startingOffset); |
| if( endingOffset == 0 ) |
| endingOffset = startingOffset; |
| problem.setSourceEnd(endingOffset-1); |
| } |
| } |
| } |
| else{ |
| for(IProblem problem : entry.getValue() ){ |
| // set the -1 source end to be the same as the source start. |
| if( problem.getSourceEnd() < problem.getSourceStart() ) |
| problem.setSourceEnd(problem.getSourceStart()); |
| } |
| } |
| } |
| |
| } |
| } |
| |
| /** |
| * Responsible for finding the ending offset of the ast node that has the tightest match |
| * for a given offset. This ast visitor can operator on an array of offsets in one pass. |
| * @author tyeung |
| */ |
| private static class EndingOffsetFinder extends ASTVisitor |
| { |
| private final int[] _sortedStartingOffset; |
| /** |
| * parallel to <code>_sortedOffsets</code> and contains |
| * the ending offset of the ast node that has the tightest match for the |
| * corresponding starting offset. |
| */ |
| private final int[] _endingOffsets; |
| |
| /** |
| * @param offsets the array of offsets which will be sorted. |
| * @throws IllegalArgumentException if <code>offsets</code> is <code>null</code>. |
| */ |
| private EndingOffsetFinder(int[] offsets) |
| { |
| if(offsets == null) |
| throw new IllegalArgumentException("argument cannot be null."); //$NON-NLS-1$ |
| // sort the array first |
| Arrays.sort(offsets); |
| |
| // look for duplicates. |
| int count = 0; |
| for( int i=0, len=offsets.length; i<len; i++){ |
| if( i == 0 ) ; // do nothing |
| else if( offsets[i-1] == offsets[i] ) |
| continue; |
| count ++; |
| } |
| |
| if( count != offsets.length ){ |
| _sortedStartingOffset = new int[count]; |
| |
| int index = 0; |
| for( int i=0, len=offsets.length; i<len; i++){ |
| if( i != 0 && offsets[i-1] == offsets[i] ) |
| continue; |
| _sortedStartingOffset[index++] = offsets[i]; |
| } |
| } |
| else{ |
| _sortedStartingOffset = offsets; |
| } |
| |
| _endingOffsets = new int[count]; |
| for( int i=0; i<count; i++ ) |
| _endingOffsets[i] = 0; |
| } |
| |
| public void preVisit(ASTNode node) |
| { |
| final int startingOffset = node.getStartPosition(); |
| final int endingOffset = startingOffset + node.getLength(); |
| // starting offset is inclusive |
| int startIndex = Arrays.binarySearch(_sortedStartingOffset, startingOffset); |
| // ending offset is exclusive |
| int endIndex = Arrays.binarySearch(_sortedStartingOffset, endingOffset); |
| if( startIndex < 0 ) |
| startIndex = - startIndex - 1; |
| if( endIndex < 0 ) |
| endIndex = - endIndex - 1; |
| else |
| // endIndex needs to be exclusive and we want to |
| // include the 'endIndex'th entry in our computation. |
| endIndex ++; |
| if( startIndex >= _sortedStartingOffset.length ) |
| return; |
| |
| for( int i=startIndex; i<endIndex; i++ ){ |
| if( _endingOffsets[i] == 0 ) |
| _endingOffsets[i] = endingOffset; |
| else if( endingOffset < _endingOffsets[i] ) |
| _endingOffsets[i] = endingOffset; |
| } |
| } |
| |
| |
| public int getEndingOffset(final int startingOffset) |
| { |
| int index = Arrays.binarySearch(_sortedStartingOffset, startingOffset); |
| if( index == -1 ) return 0; |
| return _endingOffsets[index]; |
| } |
| } |
| |
| /** |
| * @return - the extra type dependencies for the files under compilation |
| */ |
| public Map<IFile, Set<String>> getTypeDependencies() { return _typeDependencies; } |
| |
| /** true value indicates that the source path for the project changed during this APT dispatch */ |
| public boolean getSourcePathChanged() { return _sourcePathChanged; } |
| |
| /** true value indicates that the source path for the project changed during this APT dispatch */ |
| public void setSourcePathChanged( boolean b ) { _sourcePathChanged = b; } |
| |
| |
| /** |
| * Switch to batch processing mode. |
| * Note: Call to this method will cause all files associated with this environment to be |
| * read and parsed. |
| */ |
| public void setBatchProcessing(){ |
| if( _phase != Phase.BUILD ) |
| throw new IllegalStateException("No batch processing outside build."); //$NON-NLS-1$ |
| |
| if( _batchMode ) return; |
| checkValid(); |
| createDomASTs(); |
| |
| _batchMode = true; |
| _file = null; |
| _astRoot = null; |
| } |
| |
| private void createDomASTs() |
| { |
| if( _astUnits != null || _filesWithAnnotation == null) return; |
| createICompilationUnits(); |
| _astUnits = createDietASTs(_javaProject, _units); |
| } |
| |
| public void setFileProcessing(IFile file){ |
| if( file == null ) |
| throw new IllegalStateException("missing file"); //$NON-NLS-1$ |
| _batchMode = false; |
| if( file.equals(_file) ) // this is a no-op |
| return; |
| |
| _astRoot = null; |
| _file = null; |
| _unit = null; |
| |
| // need to match up the file with the ast. |
| if( _filesWithAnnotation != null ){ |
| for( int i=0, len=_filesWithAnnotation.length; i<len; i++ ){ |
| if( file.equals(_filesWithAnnotation[i]) ){ |
| _file = file; |
| if( _astUnits != null ){ |
| _astRoot = _astUnits[i]; |
| _unit = _units[i]; |
| } |
| else{ |
| _unit = JavaCore.createCompilationUnitFrom(_filesWithAnnotation[i]); |
| _astRoot = createDietAST(_javaProject, _unit); |
| } |
| } |
| } |
| } |
| |
| if( _file == null || _astRoot == null) |
| throw new IllegalStateException( |
| "file " + //$NON-NLS-1$ |
| file.getName() + |
| " is not in the list to be processed."); //$NON-NLS-1$ |
| } |
| |
| // Implementation for EclipseAnnotationProcessorEnvironment |
| public CompilationUnit getAST() |
| { |
| if( _batchMode ) return null; |
| final ASTParser parser = ASTParser.newParser(AST.JLS3); |
| parser.setResolveBindings(false); |
| parser.setSource(_unit); |
| CompilationUnit resultUnit = (CompilationUnit)parser.createAST(null); |
| return resultUnit; |
| } |
| |
| public void addTypeDependency(final String fullyQualifiedTypeName ) |
| { |
| if(!_batchMode){ |
| Set<String> deps = _typeDependencies.get(_file); |
| if( deps == null ){ |
| deps = new HashSet<String>(4); |
| _typeDependencies.put(_file, deps); |
| } |
| deps.add( fullyQualifiedTypeName ); |
| } |
| } |
| // End of implementation for EclipseAnnotationProcessorEnvironment |
| |
| /** |
| * Include all the types from all files, files with and without annotations on it |
| * if we are in batch mode. Otherwise, just the types from the file that's currently |
| * being processed. |
| */ |
| protected List<AbstractTypeDeclaration> searchLocallyForTypeDeclarations() |
| { |
| if( !_batchMode ) |
| return super.searchLocallyForTypeDeclarations(); |
| final List<AbstractTypeDeclaration> typeDecls = new ArrayList<AbstractTypeDeclaration>(); |
| for( int i=0, len=_astUnits.length; i<len; i++ ) |
| typeDecls.addAll( _astUnits[i].types() ); |
| |
| getTypeDeclarationsFromAdditionFiles(typeDecls); |
| |
| return typeDecls; |
| } |
| |
| private void getTypeDeclarationsFromAdditionFiles(List<AbstractTypeDeclaration> typeDecls){ |
| if( _additionFiles == null || _additionFiles.length == 0 ) return; |
| |
| ICompilationUnit[] units = createICUsFrom(_additionFiles); |
| final int actualLen = units.length; |
| final int numFiles = _additionFiles.length; |
| if( actualLen == 0 ) |
| return; |
| |
| // We are simply silently dropping files that doesn't have a compilation unit. |
| // This most like means the file has been deleted. |
| if( numFiles != actualLen ){ |
| final ICompilationUnit[] newUnits = new ICompilationUnit[actualLen]; |
| int newIndex = 0; |
| for( ICompilationUnit unit : units ){ |
| if( unit != null ) |
| newUnits[newIndex ++] = unit; |
| } |
| units = newUnits; |
| } |
| final CompilationUnit[] domUnits = createDietASTs(_javaProject, units); |
| for( CompilationUnit domUnit : domUnits ){ |
| if( domUnit != null ){ |
| typeDecls.addAll( domUnit.types() ); |
| } |
| } |
| } |
| |
| protected Map<ASTNode, List<Annotation>> getASTNodesWithAnnotations() |
| { |
| if( !_batchMode ) |
| return super.getASTNodesWithAnnotations(); |
| final Map<ASTNode, List<Annotation>> astNode2Anno = new HashMap<ASTNode, List<Annotation>>(); |
| final AnnotatedNodeVisitor visitor = new AnnotatedNodeVisitor(astNode2Anno); |
| for( int i=0, len=_astUnits.length; i<len; i++ ) |
| _astUnits[i].accept( visitor ); |
| return astNode2Anno; |
| } |
| |
| protected IFile getFileForNode(final ASTNode node) |
| { |
| if( !_batchMode ) |
| return super.getFileForNode(node); |
| final CompilationUnit curRoot = (CompilationUnit)node.getRoot(); |
| for( int i=0, len=_astUnits.length; i<len; i++ ){ |
| if( _astUnits[i] == curRoot ) |
| return _filesWithAnnotation[i]; |
| } |
| throw new IllegalStateException(); |
| } |
| |
| /** |
| * Go through the list of compilation unit in this environment and looking for |
| * the declaration node of the given binding. |
| * @param binding |
| * @return the compilation unit that defines the given binding or null if no |
| * match is found. |
| */ |
| protected CompilationUnit searchLocallyForBinding(final IBinding binding) |
| { |
| if( !_batchMode ) |
| return super.searchLocallyForBinding(binding); |
| |
| for( int i=0, len=_astUnits.length; i<len; i++ ){ |
| ASTNode node = _astUnits[i].findDeclaringNode(binding); |
| if( node != null) |
| return _astUnits[i]; |
| } |
| return null; |
| } |
| |
| /** |
| * Go through the list of compilation unit in this environment and looking for |
| * the declaration node of the given binding. |
| * @param binding |
| * @return the compilation unit that defines the given binding or null if no |
| * match is found. |
| */ |
| protected IFile searchLocallyForIFile(final IBinding binding) |
| { |
| if( !_batchMode ) |
| return super.searchLocallyForIFile(binding); |
| |
| for( int i=0, len=_astUnits.length; i<len; i++ ){ |
| ASTNode node = _astUnits[i].findDeclaringNode(binding); |
| if( node != null) |
| return _filesWithAnnotation[i]; |
| } |
| return null; |
| } |
| |
| public ICompilationUnit getICompilationUnitForFile(final IFile file){ |
| if( file == null ) |
| return null; |
| else if( file.equals(_file) ) |
| return _unit; |
| else if( _units != null ){ |
| for( int i=0, len=_filesWithAnnotation.length; i<len; i++ ){ |
| if( file.equals(_filesWithAnnotation[i]) ) |
| return _units[i]; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * @param file |
| * @return the compilation unit associated with the given file. |
| * If the file is not one of those that this environment is currently processing, |
| * return null; |
| */ |
| public CompilationUnit getAstCompilationUnit(final IFile file) |
| { |
| if( file == null ) |
| return null; |
| else if( file.equals(_file) ) |
| return _astRoot; |
| else if( _astUnits != null ){ |
| for( int i=0, len=_filesWithAnnotation.length; i<len; i++ ){ |
| if( file.equals(_filesWithAnnotation[i]) ) |
| return _astUnits[i]; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * @return the current ast being processed if in per-file mode. |
| * If in batch mode, one of the asts being processed (no guarantee which |
| * one will be returned. |
| */ |
| protected AST getCurrentDietAST(){ |
| |
| if( _astRoot != null ) |
| return _astRoot.getAST(); |
| else{ |
| if( _astUnits == null ) |
| throw new IllegalStateException("no AST is available"); //$NON-NLS-1$ |
| return _astUnits[0].getAST(); |
| } |
| } |
| |
| void postMarkers() |
| { |
| if( _markerInfos == null || _markerInfos.size() == 0 ) |
| return; |
| // Posting all the markers to the workspace. Doing this in a batch process |
| // to minimize the amount of notification. |
| try{ |
| final IWorkspaceRunnable runnable = new IWorkspaceRunnable(){ |
| public void run(IProgressMonitor monitor) |
| { |
| for( MarkerInfo markerInfo : _markerInfos ){ |
| try{ |
| final IMarker marker = _javaProject.getProject().createMarker(BUILD_MARKER); |
| //final IMarker marker = _javaProject.getProject().createMarker(IJavaModelMarker.JAVA_MODEL_PROBLEM_MARKER); |
| markerInfo.copyIntoMarker(marker); |
| } |
| catch(CoreException e){ |
| AptPlugin.log(e, "Failure posting markers"); //$NON-NLS-1$ |
| } |
| } |
| }; |
| }; |
| IWorkspace ws = _javaProject.getProject().getWorkspace(); |
| ws.run(runnable, null); |
| } |
| catch(CoreException e){ |
| AptPlugin.log(e, "Failed to post markers"); //$NON-NLS-1$ |
| } |
| finally{ |
| _markerInfos.clear(); |
| } |
| } |
| |
| public IFile[] getFiles() |
| { |
| if(_filesWithAnnotation != null) |
| return _filesWithAnnotation; |
| else |
| return new IFile[]{_file}; |
| } |
| |
| private static ICompilationUnit[] createICUsFrom(final IFile[] files){ |
| final int len = files.length; |
| if( len == 0 ) |
| return NO_UNIT; |
| final ICompilationUnit[] units = new ICompilationUnit[len]; |
| for( int i=0; i<len; i++ ){ |
| units[i] = JavaCore.createCompilationUnitFrom(files[i]); |
| } |
| return units; |
| } |
| |
| /** |
| * Build <code>ICompilationUnit</code> from the files with annotations in this environment. |
| * If a compilation unit cannot be created from a file, the file will be |
| * dropped from the file list. |
| */ |
| private void createICompilationUnits(){ |
| if(_units != null) |
| return; |
| _units = createICUsFrom(_filesWithAnnotation); |
| |
| |
| } |
| } |