/* * Copyright (c) 2013, the Dart project authors. * * Licensed under the Eclipse Public License v1.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.eclipse.org/legal/epl-v10.html * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.google.dart.engine.internal.hint; import com.google.dart.engine.ast.Annotation; import com.google.dart.engine.ast.CompilationUnit; import com.google.dart.engine.ast.Directive; import com.google.dart.engine.ast.ExportDirective; import com.google.dart.engine.ast.ImportDirective; import com.google.dart.engine.ast.LibraryDirective; import com.google.dart.engine.ast.NodeList; import com.google.dart.engine.ast.PrefixedIdentifier; import com.google.dart.engine.ast.SimpleIdentifier; import com.google.dart.engine.ast.visitor.RecursiveAstVisitor; import com.google.dart.engine.element.CompilationUnitElement; import com.google.dart.engine.element.Element; import com.google.dart.engine.element.ImportElement; import com.google.dart.engine.element.LibraryElement; import com.google.dart.engine.element.MultiplyDefinedElement; import com.google.dart.engine.element.PrefixElement; import com.google.dart.engine.error.HintCode; import com.google.dart.engine.internal.error.ErrorReporter; import com.google.dart.engine.internal.resolver.ElementResolver; import com.google.dart.engine.internal.scope.Namespace; import com.google.dart.engine.internal.scope.NamespaceBuilder; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; /** * Instances of the class {@code ImportsVerifier} visit all of the referenced libraries in the * source code verifying that all of the imports are used, otherwise a * {@link HintCode#UNUSED_IMPORT} is generated with * {@link #generateUnusedImportHints(ErrorReporter)}. * <p> * While this class does not yet have support for an "Organize Imports" action, this logic built up * in this class could be used for such an action in the future. * * @coverage dart.engine.resolver */ public class ImportsVerifier extends RecursiveAstVisitor<Void> { /** * This is set to {@code true} if the current compilation unit which is being visited is the * defining compilation unit for the library, its value can be set with * {@link #setInDefiningCompilationUnit(boolean)}. */ private boolean inDefiningCompilationUnit = false; /** * The current library. */ private final LibraryElement currentLibrary; /** * A list of {@link ImportDirective}s that the current library imports, as identifiers are visited * by this visitor and an import has been identified as being used by the library, the * {@link ImportDirective} is removed from this list. After all the sources in the library have * been evaluated, this list represents the set of unused imports. * * @see ImportsVerifier#generateUnusedImportErrors(ErrorReporter) */ private final ArrayList<ImportDirective> unusedImports; /** * After the list of {@link #unusedImports} has been computed, this list is a proper subset of the * unused imports that are listed more than once. */ private final ArrayList<ImportDirective> duplicateImports; /** * This is a map between the set of {@link LibraryElement}s that the current library imports, and * a list of {@link ImportDirective}s that imports the library. In cases where the current library * imports a library with a single directive (such as {@code import lib1.dart;}), the library * element will map to a list of one {@link ImportDirective}, which will then be removed from the * {@link #unusedImports} list. In cases where the current library imports a library with multiple * directives (such as {@code import lib1.dart; import lib1.dart show C;}), the * {@link LibraryElement} will be mapped to a list of the import directives, and the namespace * will need to be used to compute the correct {@link ImportDirective} being used, see * {@link #namespaceMap}. */ private final HashMap<LibraryElement, ArrayList<ImportDirective>> libraryMap; /** * In cases where there is more than one import directive per library element, this mapping is * used to determine which of the multiple import directives are used by generating a * {@link Namespace} for each of the imports to do lookups in the same way that they are done from * the {@link ElementResolver}. */ private final HashMap<ImportDirective, Namespace> namespaceMap; /** * This is a map between prefix elements and the import directives from which they are derived. In * cases where a type is referenced via a prefix element, the import directive can be marked as * used (removed from the unusedImports) by looking at the resolved {@code lib} in {@code lib.X}, * instead of looking at which library the {@code lib.X} resolves. * <p> * TODO (jwren) Since multiple {@link ImportDirective}s can share the same {@link PrefixElement}, * it is possible to have an unreported unused import in situations where two imports use the same * prefix and at least one import directive is used. */ private final HashMap<PrefixElement, ArrayList<ImportDirective>> prefixElementMap; /** * Create a new instance of the {@link ImportsVerifier}. * * @param errorReporter the error reporter */ public ImportsVerifier(LibraryElement library) { this.currentLibrary = library; this.unusedImports = new ArrayList<ImportDirective>(); this.duplicateImports = new ArrayList<ImportDirective>(); this.libraryMap = new HashMap<LibraryElement, ArrayList<ImportDirective>>(); this.namespaceMap = new HashMap<ImportDirective, Namespace>(); this.prefixElementMap = new HashMap<PrefixElement, ArrayList<ImportDirective>>(); } /** * Any time after the defining compilation unit has been visited by this visitor, this method can * be called to report an {@link HintCode#DUPLICATE_IMPORT} hint for each of the import directives * in the {@link #duplicateImports} list. * * @param errorReporter the error reporter to report the set of {@link HintCode#DUPLICATE_IMPORT} * hints to */ public void generateDuplicateImportHints(ErrorReporter errorReporter) { for (ImportDirective duplicateImport : duplicateImports) { errorReporter.reportErrorForNode(HintCode.DUPLICATE_IMPORT, duplicateImport.getUri()); } } /** * After all of the compilation units have been visited by this visitor, this method can be called * to report an {@link HintCode#UNUSED_IMPORT} hint for each of the import directives in the * {@link #unusedImports} list. * * @param errorReporter the error reporter to report the set of {@link HintCode#UNUSED_IMPORT} * hints to */ public void generateUnusedImportHints(ErrorReporter errorReporter) { for (ImportDirective unusedImport : unusedImports) { // Check that the import isn't dart:core ImportElement importElement = unusedImport.getElement(); if (importElement != null) { LibraryElement libraryElement = importElement.getImportedLibrary(); if (libraryElement != null && libraryElement.isDartCore()) { continue; } } errorReporter.reportErrorForNode(HintCode.UNUSED_IMPORT, unusedImport.getUri()); } } /* * Should we mark imports which are only used by comments as unused? While the VM and dart2js * don't care, the analyzer (and thus the hyperlink in the Editor) and the link in dartdoc is * lost- thus we have made the decision for the time being to not mark them as unused. */ // @Override // public Void visitComment(Comment node) { // return null; // } @Override public Void visitCompilationUnit(CompilationUnit node) { if (inDefiningCompilationUnit) { NodeList<Directive> directives = node.getDirectives(); for (Directive directive : directives) { if (directive instanceof ImportDirective) { ImportDirective importDirective = (ImportDirective) directive; LibraryElement libraryElement = importDirective.getUriElement(); if (libraryElement != null) { unusedImports.add(importDirective); // // Initialize prefixElementMap // if (importDirective.getAsToken() != null) { SimpleIdentifier prefixIdentifier = importDirective.getPrefix(); if (prefixIdentifier != null) { Element element = prefixIdentifier.getStaticElement(); if (element instanceof PrefixElement) { PrefixElement prefixElementKey = (PrefixElement) element; ArrayList<ImportDirective> list = prefixElementMap.get(prefixElementKey); if (list == null) { list = new ArrayList<ImportDirective>(1); prefixElementMap.put(prefixElementKey, list); } list.add(importDirective); } // TODO (jwren) Can the element ever not be a PrefixElement? } } // // Initialize libraryMap: libraryElement -> importDirective // putIntoLibraryMap(libraryElement, importDirective); // // For this new addition to the libraryMap, also recursively add any exports from the // libraryElement // addAdditionalLibrariesForExports( libraryElement, importDirective, new ArrayList<LibraryElement>()); } } } } // If there are no imports in this library, don't visit the identifiers in the library- there // can be no unused imports. if (unusedImports.isEmpty()) { return null; } if (unusedImports.size() > 1) { // order the list of unusedImports to find duplicates in faster than O(n^2) time ImportDirective[] importDirectiveArray = unusedImports.toArray(new ImportDirective[unusedImports.size()]); Arrays.sort(importDirectiveArray, ImportDirective.COMPARATOR); ImportDirective currentDirective = importDirectiveArray[0]; for (int i = 1; i < importDirectiveArray.length; i++) { ImportDirective nextDirective = importDirectiveArray[i]; if (ImportDirective.COMPARATOR.compare(currentDirective, nextDirective) == 0) { // Add either the currentDirective or nextDirective depending on which comes second, this // guarantees that the first of the duplicates won't be highlighted. if (currentDirective.getOffset() < nextDirective.getOffset()) { duplicateImports.add(nextDirective); } else { duplicateImports.add(currentDirective); } } currentDirective = nextDirective; } } return super.visitCompilationUnit(node); } @Override public Void visitExportDirective(ExportDirective node) { visitMetadata(node.getMetadata()); return null; } @Override public Void visitImportDirective(ImportDirective node) { visitMetadata(node.getMetadata()); return null; } @Override public Void visitLibraryDirective(LibraryDirective node) { visitMetadata(node.getMetadata()); return null; } @Override public Void visitPrefixedIdentifier(PrefixedIdentifier node) { if (unusedImports.isEmpty()) { return null; } // If the prefixed identifier references some A.B, where A is a library prefix, then we can // lookup the associated ImportDirective in prefixElementMap and remove it from the // unusedImports list. SimpleIdentifier prefixIdentifier = node.getPrefix(); Element element = prefixIdentifier.getStaticElement(); if (element instanceof PrefixElement) { ArrayList<ImportDirective> importDirectives = prefixElementMap.get(element); if (importDirectives != null) { for (ImportDirective importDirective : importDirectives) { unusedImports.remove(importDirective); } } return null; } // Otherwise, pass the prefixed identifier element and name onto visitIdentifier. return visitIdentifier(element, prefixIdentifier.getName()); } @Override public Void visitSimpleIdentifier(SimpleIdentifier node) { if (unusedImports.isEmpty()) { return null; } return visitIdentifier(node.getStaticElement(), node.getName()); } void setInDefiningCompilationUnit(boolean inDefiningCompilationUnit) { this.inDefiningCompilationUnit = inDefiningCompilationUnit; } /** * Recursively add any exported library elements into the {@link #libraryMap}. */ private void addAdditionalLibrariesForExports(LibraryElement library, ImportDirective importDirective, ArrayList<LibraryElement> exportPath) { if (exportPath.contains(library)) { return; } exportPath.add(library); for (LibraryElement exportedLibraryElt : library.getExportedLibraries()) { putIntoLibraryMap(exportedLibraryElt, importDirective); addAdditionalLibrariesForExports(exportedLibraryElt, importDirective, exportPath); } } /** * Lookup and return the {@link Namespace} from the {@link #namespaceMap}, if the map does not * have the computed namespace, compute it and cache it in the map. If the import directive is not * resolved or is not resolvable, {@code null} is returned. * * @param importDirective the import directive used to compute the returned namespace * @return the computed or looked up {@link Namespace} */ private Namespace computeNamespace(ImportDirective importDirective) { Namespace namespace = namespaceMap.get(importDirective); if (namespace == null) { // If the namespace isn't in the namespaceMap, then compute and put it in the map ImportElement importElement = importDirective.getElement(); if (importElement != null) { NamespaceBuilder builder = new NamespaceBuilder(); namespace = builder.createImportNamespaceForDirective(importElement); namespaceMap.put(importDirective, namespace); } } return namespace; } /** * The {@link #libraryMap} is a mapping between a library elements and a list of import * directives, but when adding these mappings into the {@link #libraryMap}, this method can be * used to simply add the mapping between the library element an an import directive without * needing to check to see if a list needs to be created. */ private void putIntoLibraryMap(LibraryElement libraryElement, ImportDirective importDirective) { ArrayList<ImportDirective> importList = libraryMap.get(libraryElement); if (importList == null) { importList = new ArrayList<ImportDirective>(3); libraryMap.put(libraryElement, importList); } importList.add(importDirective); } private Void visitIdentifier(Element element, String name) { if (element == null) { return null; } // If the element is multiply defined then call this method recursively for each of the // conflicting elements. if (element instanceof MultiplyDefinedElement) { MultiplyDefinedElement multiplyDefinedElement = (MultiplyDefinedElement) element; for (Element elt : multiplyDefinedElement.getConflictingElements()) { visitIdentifier(elt, name); } return null; } else if (element instanceof PrefixElement) { ArrayList<ImportDirective> importDirectives = prefixElementMap.get(element); if (importDirectives != null) { for (ImportDirective importDirective : importDirectives) { unusedImports.remove(importDirective); } } return null; } else if (!(element.getEnclosingElement() instanceof CompilationUnitElement)) { // Identifiers that aren't a prefix element and whose enclosing element isn't a // CompilationUnit are ignored- this covers the case the identifier is a relative-reference, // a reference to an identifier not imported by this library. return null; } LibraryElement containingLibrary = element.getLibrary(); if (containingLibrary == null) { return null; } // If the element is declared in the current library, return. if (currentLibrary.equals(containingLibrary)) { return null; } ArrayList<ImportDirective> importsFromSameLibrary = libraryMap.get(containingLibrary); if (importsFromSameLibrary == null) { return null; } if (importsFromSameLibrary.size() == 1) { // If there is only one import directive for this library, then it must be the directive that // this element is imported with, remove it from the unusedImports list. ImportDirective usedImportDirective = importsFromSameLibrary.get(0); unusedImports.remove(usedImportDirective); } else { // Otherwise, for each of the imported directives, use the namespaceMap to for (ImportDirective importDirective : importsFromSameLibrary) { // Get the namespace for this import Namespace namespace = computeNamespace(importDirective); if (namespace != null && namespace.get(name) != null) { unusedImports.remove(importDirective); } } } return null; } /** * Given some {@link NodeList} of {@link Annotation}s, ensure that the identifiers are visited by * this visitor. Specifically, this covers the cases where AST nodes don't have their identifiers * visited by this visitor, but still need their annotations visited. * * @param annotations the list of annotations to visit */ private void visitMetadata(NodeList<Annotation> annotations) { int count = annotations.size(); for (int i = 0; i < count; i++) { annotations.get(i).accept(this); } } }