/* * Copyright 2013 Guidewire Software, Inc. */ package gw.plugin.ij.intentions; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.collect.ComparisonChain; import com.google.common.collect.HashMultiset; import com.google.common.collect.Multiset; import com.intellij.openapi.module.impl.scopes.ModuleWithDependentsScope; import com.intellij.psi.*; import com.intellij.psi.codeStyle.CodeStyleSettings; import com.intellij.psi.codeStyle.CodeStyleSettingsManager; import com.intellij.psi.codeStyle.PackageEntry; import com.intellij.psi.search.SearchScope; import gw.internal.gosu.parser.expressions.TypeLiteral; import gw.lang.init.ModuleFileUtil; import gw.lang.parser.IParsedElement; import gw.lang.parser.ITypeUsesMap; import gw.lang.parser.expressions.ITemplateStringLiteral; import gw.lang.reflect.IType; import gw.lang.reflect.TypeSystem; import gw.lang.reflect.module.IModule; import gw.plugin.ij.lang.psi.IGosuPsiElement; import gw.plugin.ij.lang.psi.api.IGosuPackageDefinition; import gw.plugin.ij.lang.psi.api.IGosuResolveResult; import gw.plugin.ij.lang.psi.api.statements.IGosuUsesStatement; import gw.plugin.ij.lang.psi.api.statements.IGosuUsesStatementList; import gw.plugin.ij.lang.psi.api.types.IGosuCodeReferenceElement; import gw.plugin.ij.lang.psi.api.types.IGosuTypeVariable; import gw.plugin.ij.lang.psi.impl.expressions.GosuTypeLiteralImpl; import gw.plugin.ij.util.ClassLord; import gw.plugin.ij.util.GosuModuleUtil; import gw.util.cache.FqnCache; import gw.util.cache.FqnCacheNode; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; import static com.google.common.base.Objects.firstNonNull; import static com.google.common.base.Strings.isNullOrEmpty; import static com.google.common.base.Strings.nullToEmpty; import static com.google.common.collect.Iterables.filter; import static com.google.common.collect.Iterables.transform; import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Sets.newHashSet; import static com.intellij.psi.util.ClassUtil.extractClassName; import static com.intellij.psi.util.ClassUtil.extractPackageName; import static com.intellij.psi.util.PsiTreeUtil.getChildOfType; import static com.intellij.psi.util.PsiTreeUtil.getParentOfType; import static gw.internal.gosu.parser.TypeLord.getPureGenericType; import static gw.plugin.ij.lang.psi.util.GosuPsiParseUtil.parseUsesList; import static gw.plugin.ij.util.ClassLord.hasImplicitImport; public class GosuImportReferenceAnalyzer { private final CodeStyleSettings settings; private final FqnCache<Integer> packageWeights = new FqnCache<>(); private final Collector collector; private final PsiFile file; private final ITypeUsesMap usesMap; private Integer defaultWeight; private String inPackage = "__unknown__"; private IGosuUsesStatementList newImports; public GosuImportReferenceAnalyzer(PsiFile file) { this.file = file; settings = CodeStyleSettingsManager.getSettings(this.file.getProject()); collector = new Collector(); int weight = 0; for (PackageEntry entry : settings.IMPORT_LAYOUT_TABLE.getEntries()) { if (entry.isStatic()) { continue; } if (entry == PackageEntry.BLANK_LINE_ENTRY || entry == PackageEntry.ALL_OTHER_STATIC_IMPORTS_ENTRY) { collector.entries.add(new ImportEntry(weight)); } else if (entry == PackageEntry.ALL_OTHER_IMPORTS_ENTRY) { defaultWeight = weight; } else { packageWeights.add(entry.getPackageName(), weight); } ++ weight; } if (defaultWeight == null) { defaultWeight = weight; } usesMap = findUsesMap(file); } private ITypeUsesMap findUsesMap(PsiFile file) { // if (file instanceof AbstractGosuClassFileImpl) { // ITypeUsesMap usesMap = GosuParserConfigurer.getTypeUsesMap((AbstractGosuClassFileImpl) file); // if (usesMap != null) { // return usesMap; // } // } return TypeSystem.getDefaultTypeUsesMap(); } @Nullable public IGosuUsesStatementList getNewImportList() { return newImports; } public void analyze() { file.accept(new Analyzer()); } class Analyzer extends PsiRecursiveElementVisitor { public void visitElement(PsiElement element) { boolean handled = false; if (element instanceof IGosuCodeReferenceElement) { handled = visitRefElement((IGosuCodeReferenceElement) element); } else if (element instanceof IGosuPackageDefinition) { visitPackageDefinition((IGosuPackageDefinition) element); handled = true; } if (!handled && element instanceof IGosuPsiElement) { IParsedElement pe = ((IGosuPsiElement) element).getParsedElement(); if (pe instanceof ITemplateStringLiteral) { List<TypeLiteral> literalsInString = new ArrayList<>(); pe.getContainedParsedElementsByType(TypeLiteral.class, literalsInString); for (TypeLiteral literal : literalsInString) { processReferenceToClass(literal, element); } } else if (pe instanceof TypeLiteral) { processReferenceToClass((TypeLiteral) pe, element); } } super.visitElement(element); } } public void visitPackageDefinition(@NotNull IGosuPackageDefinition packageDefinition) { String inPackage = packageDefinition.getPackageName(); if (inPackage != null) { this.inPackage = inPackage; } } private boolean visitRefElement(@NotNull IGosuCodeReferenceElement referenceElement) { boolean handled = false; for (IGosuResolveResult resolveResult : (IGosuResolveResult[]) referenceElement.multiResolve(false)) { final PsiElement element = resolveResult.getElement(); if (element == null || element instanceof IGosuTypeVariable || element.getLanguage().getID().equals("Properties") || element.getContainingFile() == file) { continue; } if (!(referenceElement instanceof GosuTypeLiteralImpl)) { continue; } GosuTypeLiteralImpl literal = (GosuTypeLiteralImpl) referenceElement; if (underUses(referenceElement)) { continue; } if (element instanceof PsiClass) { processReferenceToClass((PsiClass) element, literal); handled = true; } } return handled; } private boolean underUses(PsiElement element) { return getParentOfType(element, IGosuUsesStatement.class) != null; } private final Set<String> implicitImport = new java.util.HashSet<>(); private boolean isImplicitImport(String fqn) { IModule module = GosuModuleUtil.findModuleForPsiElement(file); if (module == null) { module = TypeSystem.getGlobalModule(); } TypeSystem.pushModule(module); try { if (hasImplicitImport(fqn, usesMap)) { implicitImport.add(fqn); return true; } else { return false; } } finally { TypeSystem.popModule(module); } } private void processReferenceToClass(@NotNull PsiClass referredClass, @NotNull GosuTypeLiteralImpl referenceElement ) { final String qualifiedName = purgeFQN(referredClass.getQualifiedName()); if (isNullOrEmpty(qualifiedName)) { return; } boolean explicitAccess = qualifiedName.equals(purgeFQN(referenceElement.getText())); if (explicitAccess) { return; } if (isImplicitImport(qualifiedName)) { return; } //Access to inner classes case if (getChildOfType(referenceElement, GosuTypeLiteralImpl.class) != null) { return; } // Reference from type literal final String referredClassPackage = getPackageName(referredClass); PsiClass outerClass = getOutermostClass(referredClass); String actualPackage = outerClass != null ? getPackageName(outerClass) : referredClassPackage; if (!inPackage.equals(referredClassPackage)) { collector.add(getPackageWeight(actualPackage), referredClassPackage, qualifiedName, referredClass.getName()); } } private String purgeFQN(String name) { return ClassLord.purgeClassName(name); } private void processReferenceToClass(@NotNull TypeLiteral referredClass, @NotNull PsiElement referenceElement) { if (underUses(referenceElement)) { return; } if (referredClass.getType() == null) { return; } IType type = referredClass.getType().getType(); if (type == null) { return; } final String qualifiedName = purgeFQN(getPureGenericType(type).getName()); if (isNullOrEmpty(qualifiedName)) { return; } boolean explicitAccess = qualifiedName.equals(purgeFQN(referenceElement.getText())); if (explicitAccess) { return; } if (isImplicitImport(qualifiedName)) { return; } //Access to inner classes case if (getChildOfType(referenceElement, GosuTypeLiteralImpl.class) != null) { return; } // Reference from type literal final String referredClassPackage = type.getNamespace(); String actualPackage = referredClassPackage; if (!inPackage.equals(referredClassPackage)) { collector.add(getPackageWeight(actualPackage), referredClassPackage, qualifiedName, type.getRelativeName()); } } class ImportsBuilder { boolean newLineComing; boolean hasImports; final StringBuilder result; ImportsBuilder(int capacity) { result = new StringBuilder(capacity); } public void newLine() { newLineComing = true; } public void appendSingle(String fqn) { checkNewLine(); hasImports = true; result.append("uses ").append(fqn).append("\n"); } public void appendWildcard(String packageName) { checkNewLine(); hasImports = true; result.append("uses ").append(packageName).append(".*\n"); } private void checkNewLine() { if (newLineComing) { result.append("\n"); newLineComing = false; } } } public void processImports() { if (collector.imported.isEmpty()) { return; } Set<String> wildcardPackagesFromSettings = new HashSet<>(); for (PackageEntry entry : settings.PACKAGES_TO_USE_IMPORT_ON_DEMAND.getEntries()) { wildcardPackagesFromSettings.add(entry.getPackageName()); } ImportsBuilder importsBuilder = new ImportsBuilder(collector.entries.size() * 30); Set<String> wildcardImport = new HashSet<>(); for (Object o : usesMap.getTypeUses()) { String type = o.toString(); if (type.endsWith(".")) { wildcardImport.add(type.substring(0, type.length() - 1)); } } Set<String> useWilcard = new HashSet<>(); for (ImportEntry impEntry : collector.entries) { if (impEntry instanceof ClassImportEntry) { String pkg = ((ClassImportEntry) impEntry).packageName; int packageCount = collector.packagesUsage.count(pkg); boolean wildcard = (! settings.USE_SINGLE_CLASS_IMPORTS || wildcardPackagesFromSettings.contains(pkg) || packageCount >= settings.CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND) && !interferencesWithImplicit(pkg); if (wildcard) { useWilcard.add(pkg); } } } for (ImportEntry impEntry : collector.entries) { if (!(impEntry instanceof ClassImportEntry)) { importsBuilder.newLine(); continue; } ClassImportEntry entry = (ClassImportEntry) impEntry; String pkg = entry.packageName; boolean wildcard = useWilcard.contains(pkg) && !conflicting(pkg, entry.simpleName, useWilcard); if (wildcard) { if (!wildcardImport.add(pkg)) { continue; } importsBuilder.appendWildcard(pkg); } else { importsBuilder.appendSingle(entry.qualifiedName); } } if (importsBuilder.hasImports) { newImports = parseUsesList(importsBuilder.result.toString(), file); } else { newImports = null; } } private boolean interferencesWithImplicit(String wildPckg) { Set<String> implicitImport = new HashSet<>(this.implicitImport); Set<String> implicitSimpleNames = newHashSet(transform(implicitImport, FQN_TO_SIMPLE_NAME)); for (PsiClass pc : filter(findClasses(wildPckg), scopePredicate)) { if (implicitImport.contains(pc.getQualifiedName())) { continue; } if (implicitSimpleNames.contains(pc.getName())) { return true; } } return false; } private boolean conflicting(String basePackage, String simpleName, Set<String> imported) { for (String pkg : imported) { if (pkg.equals(basePackage)) { continue; } if (contains(pkg, simpleName)) { return true; } } return false; } public static final Function<PsiClass, String> PSI_TO_SIMPLE_NAME = new Function<PsiClass, String>() { public String apply(@Nullable PsiClass psiClass) { return psiClass.getName(); } }; public static final Function<String, String> FQN_TO_SIMPLE_NAME = new Function<String, String>() { public String apply(String fqn) { return extractClassName(fqn); } }; private Predicate<PsiClass> scopePredicate = new Predicate<PsiClass>() { public boolean apply(PsiClass psiClass) { SearchScope scope = psiClass.getUseScope(); if (scope instanceof ModuleWithDependentsScope) { return ((ModuleWithDependentsScope) scope).contains(file.getVirtualFile()); } else { return true; } } }; private Iterable<PsiClass> findClasses(String fromPackage) { PsiPackage pckg = JavaPsiFacade.getInstance(file.getProject()).findPackage(fromPackage); if (pckg == null) { return Collections.emptyList(); } else { return newArrayList(pckg.getClasses()); } } private boolean contains(String basePackage, String simpleName) { for (PsiClass pc : filter(findClasses(basePackage), scopePredicate)) { if (simpleName.equals(pc.getName())) { return true; } } return false; } private String getPackageName(@NotNull PsiClass cls) { return firstNonNull(extractPackageName(cls.getQualifiedName()), ""); } @Nullable private PsiClass getOutermostClass(@NotNull PsiClass cls) { PsiClass candidate = null; PsiClass enclosingClass = cls.getContainingClass(); while (enclosingClass != null) { candidate = enclosingClass; enclosingClass = enclosingClass.getContainingClass(); } return candidate; } private int getPackageWeight(@NotNull String packageName) { String[] fqn = FqnCache.getParts(packageName); FqnCacheNode<Integer> root = packageWeights.getRoot(); FqnCacheNode<Integer> lastFound = root; for (String part : fqn) { FqnCacheNode<Integer> segment = lastFound.getChild(part); if (segment != null) { lastFound = segment; } else { break; } } Integer weight = lastFound.getUserData(); if (weight == null) { weight = defaultWeight; } return weight; } public Set<String> getRequiredImports() { return collector.imported; } } class EntriesComparator implements Comparator<ImportEntry> { @Override public int compare(ImportEntry o1, ImportEntry o2) { ComparisonChain chain = ComparisonChain.start().compare(o1.weight, o2.weight); boolean i1 = o1 instanceof ClassImportEntry; boolean i2 = o2 instanceof ClassImportEntry; if (i1 && i2) { ClassImportEntry c1 = (ClassImportEntry) o1; ClassImportEntry c2 = (ClassImportEntry) o2; chain = chain .compare(c1.packageName, c2.packageName) .compare(c1.qualifiedName, c2.qualifiedName); } else { chain = chain.compareFalseFirst(i2, i1); } return chain.result(); } } class ImportEntry { final int weight; ImportEntry(int weight) { this.weight = weight; } } class ClassImportEntry extends ImportEntry { final String packageName; final String qualifiedName; final String simpleName; public ClassImportEntry(int weight, String packageName, String qualifiedName, String simpleName) { super(weight); this.packageName = nullToEmpty(packageName); this.qualifiedName = nullToEmpty(qualifiedName); this.simpleName = nullToEmpty(simpleName); } } class Collector { final Set<ImportEntry> entries = new TreeSet<>(new EntriesComparator()); final Multiset<String> packagesUsage = HashMultiset.create(); final Set<String> imported = new HashSet<>(); public void add(int weight, String packageName, String qualifiedName, String simpleName) { if (imported.add(qualifiedName)) { entries.add(new ClassImportEntry(weight, packageName, qualifiedName, simpleName)); packagesUsage.add(packageName); } } }