flex/src/com/intellij/lang/javascript/validation/ActionScriptUnusedImportsHelper.java (322 lines of code) (raw):
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.lang.javascript.validation;
import com.intellij.lang.javascript.JSTokenTypes;
import com.intellij.lang.javascript.flex.ImportUtils;
import com.intellij.lang.javascript.flex.PredefinedImportSet;
import com.intellij.lang.javascript.flex.ScopedImportSet;
import com.intellij.lang.javascript.psi.*;
import com.intellij.lang.javascript.psi.ecmal4.*;
import com.intellij.lang.javascript.psi.impl.JSReferenceExpressionImpl;
import com.intellij.lang.javascript.psi.resolve.JSImportedElementResolveResult;
import com.intellij.lang.javascript.psi.resolve.JSResolveResult;
import com.intellij.lang.javascript.psi.resolve.JSResolveUtil;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.search.PsiElementProcessor;
import com.intellij.psi.util.*;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MultiMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
// TODO [ksafonov] think about working on a single Result instance instead of merging Result-s
public final class ActionScriptUnusedImportsHelper {
public static final class Results {
public final Collection<JSImportStatement> unusedImports;
public final MultiMap<Computable<JSElement>, String> importsByHolder;
public final Collection<JSReferenceExpression> fqnsToReplaceWithShortName;
public final MultiMap<JSImportStatement, JSReferenceExpression> usedImports;
private Results(Collection<JSReferenceExpression> fqnsToReplaceWithShortName,
Collection<JSImportStatement> unusedImports,
MultiMap<Computable<JSElement>, String> importsByHolder,
MultiMap<JSImportStatement, JSReferenceExpression> usedImports) {
this.fqnsToReplaceWithShortName = fqnsToReplaceWithShortName;
this.unusedImports = unusedImports;
this.importsByHolder = importsByHolder;
this.usedImports = usedImports;
}
private Results() {
this(new ArrayList<>(), new HashSet<>(),
MultiMap.createSet(),
MultiMap.createSet());
}
private void merge(Results results) {
fqnsToReplaceWithShortName.addAll(results.fqnsToReplaceWithShortName);
results.unusedImports.removeAll(usedImports.keySet());
unusedImports.addAll(results.unusedImports);
for (Computable<JSElement> holder : results.importsByHolder.keySet()) {
for (String s : results.importsByHolder.get(holder)) {
importsByHolder.putValue(holder, s);
}
}
for (JSImportStatement anImport : results.usedImports.keySet()) {
usedImports.put(anImport, results.usedImports.get(anImport));
unusedImports.remove(anImport);
}
}
public Collection<JSImportStatement> getAllImports() {
List<JSImportStatement> result = new ArrayList<>(unusedImports);
result.addAll(usedImports.keySet());
return result;
}
}
private static final Key<CachedValue<Results>> ourUnusedImportsKey = Key.create("js.unused.imports");
private final Set<JSImportStatement> myUnused = new HashSet<>();
private final Collection<JSReferenceExpression> fqnsToReplaceWithImport = new ArrayList<>();
private final PsiFile myContainingFile;
private final Collection<PsiElement> myElements;
private final MultiMap<JSImportStatement, JSReferenceExpression> myUsed = MultiMap.createSet();
private ActionScriptUnusedImportsHelper(PsiFile containingFile, Collection<PsiElement> elements) {
myContainingFile = containingFile;
myElements = elements;
}
private void registerUnused(final JSImportStatement importStatement) {
if (!myUsed.containsKey(importStatement) && importStatement.getImportText() != null) {
myUnused.add(importStatement);
}
}
private void process(JSReferenceExpression node) {
if (node.getQualifier() == null) {
String thisPackage = JSResolveUtil.findPackageStatementQualifier(node);
registerUsedImportsFromResolveResults(node, thisPackage);
}
else {
if (PsiTreeUtil.getParentOfType(node, JSImportStatement.class) != null) {
return;
}
if (node.getParent() instanceof JSClass &&
node.getPrevSibling() instanceof PsiWhiteSpace &&
node.getPrevSibling().getPrevSibling() != null &&
node.getPrevSibling().getPrevSibling().getNode().getElementType() == JSTokenTypes.CLASS_KEYWORD) {
return;
}
JSReferenceExpression topReference = JSResolveUtil.getTopReferenceExpression(node);
if (topReference.getParent() instanceof JSPackageStatement &&
topReference.getPrevSibling() instanceof PsiWhiteSpace &&
topReference.getPrevSibling().getPrevSibling() != null &&
topReference.getPrevSibling().getPrevSibling().getNode().getElementType() == JSTokenTypes.PACKAGE_KEYWORD) {
return;
}
registerUsedImportsFromResolveResults(node, null);
Couple<Boolean> replaceStatus = UnusedImportsUtil.getReplaceStatus(node);
if (replaceStatus.second) {
if (sameContainingFile(node.getContainingFile(), myContainingFile)) {
fqnsToReplaceWithImport.add(node);
}
}
}
}
private static @Nullable JSImportStatement getUsedImportStatement(JSReferenceExpression node, String thisPackage) {
for (ResolveResult r : node.multiResolve(false)) {
// TODO can we get different import statements here?
if (r instanceof JSResolveResult) {
JSImportStatement importStatement = ((JSResolveResult)r).getActionScriptImport();
if (importStatement != null && UnusedImportsUtil.isInstance(r.getElement(), UnusedImportsUtil.REFERENCED_ELEMENTS_CLASSES)) {
String importString = importStatement.getImportText();
String importedPackage = StringUtil.getPackageName(importString);
if (thisPackage == null || !thisPackage.equals(importedPackage)) {
return importStatement;
}
}
}
}
return null;
}
private void registerUsedImportsFromResolveResults(JSReferenceExpression node, String thisPackage) {
final JSImportStatement s = getUsedImportStatement(node, thisPackage);
if (s != null) registerUsed(s, node);
}
private static boolean sameContainingFile(PsiFile file1, PsiFile file2) {
PsiFile containing1 = getContainingFile(file1);
PsiFile containing2 = getContainingFile(file2);
return containing1 == containing2;
}
private void registerUsed(JSImportStatement importStatement, JSReferenceExpression node) {
assert importStatement.getImportText() != null;
myUnused.remove(importStatement);
myUsed.putValue(importStatement, node);
}
private Collection<JSImportStatement> filter(Collection<JSImportStatement> original) {
Collection<JSImportStatement> result = new ArrayList<>();
for (JSImportStatement importStatement : original) {
if (isAcceptable(importStatement)) {
result.add(importStatement);
}
}
return result;
}
private MultiMap<JSImportStatement, JSReferenceExpression> filter(MultiMap<JSImportStatement, JSReferenceExpression> original) {
MultiMap<JSImportStatement, JSReferenceExpression> result = MultiMap.createSet();
for (JSImportStatement importStatement : original.keySet()) {
if (isAcceptable(importStatement)) {
result.put(importStatement, original.get(importStatement));
}
}
return result;
}
private boolean isAcceptable(JSImportStatement importStatement) {
return importStatement.isValid() && sameContainingFile(importStatement.getContainingFile(), myContainingFile);
}
public static Results getUnusedImports(PsiFile file) {
final PsiFile containingFile = getContainingFile(file);
CachedValue<Results> data = containingFile.getUserData(ourUnusedImportsKey);
if (data == null) {
data = CachedValuesManager.getManager(file.getProject()).createCachedValue(() -> {
final Map<XmlTag, Collection<PsiElement>> allElements = new HashMap<>();
Collection<JSFile> processedFiles = new HashSet<>();
collectElements(containingFile, allElements, processedFiles, null);
Results allResults = new Results();
for (Collection<PsiElement> elements : allElements.values()) {
allResults.merge(new ActionScriptUnusedImportsHelper(containingFile, elements).getUnusedImports());
}
// TODO explicit depencencies
return new CachedValueProvider.Result<>(allResults, PsiModificationTracker.MODIFICATION_COUNT);
}, false);
containingFile.putUserData(ourUnusedImportsKey, data);
}
return data.getValue();
}
private static PsiFile getContainingFile(PsiFile file) {
return file.getContext() != null ? file.getContext().getContainingFile() : file;
}
private Results getUnusedImports() {
for (PsiElement e : myElements) {
if (e instanceof JSImportStatement importStatement) {
registerUnused(importStatement);
}
else if (e instanceof JSReferenceExpression) {
process((JSReferenceExpression)e);
}
}
MultiMap<Computable<JSElement>, String> importsByHolder = MultiMap.createSet();
for (JSImportStatement anImport : myUsed.keySet()) {
if (!isAcceptable(anImport)) continue;
Computable<JSElement> importHolder =
ImportUtils.getImportHolder(anImport, true, JSFunction.class, JSPackageStatement.class, JSFile.class);
assert importHolder != null : "Import holder not found for " + anImport.getText();
addImport(importsByHolder, importHolder, anImport.getImportText());
}
final List<JSReferenceExpression> replaceWithShortName = new ArrayList<>();
for (JSReferenceExpression qualifiedReference : fqnsToReplaceWithImport) {
final Collection<String> imports;
final Computable<JSElement> importHolder;
Computable<JSElement> enclosingFunction = ImportUtils.getImportHolder(qualifiedReference, false, JSFunction.class);
Computable<JSElement> enclosingPackage = ImportUtils.getImportHolder(qualifiedReference, false, JSPackageStatement.class);
if (enclosingFunction != null && !importsByHolder.get(enclosingFunction).isEmpty()) {
importHolder = enclosingFunction;
imports = new HashSet<>(importsByHolder.get(enclosingFunction));
imports.addAll(importsByHolder.get(enclosingPackage));
}
else if (enclosingPackage != null) {
importHolder = enclosingPackage;
imports = importsByHolder.get(enclosingPackage);
}
else {
importHolder = ImportUtils.getImportHolder(qualifiedReference, false, JSFile.class);
imports = importsByHolder.get(importHolder);
}
// first, get all results considering current import statements, that are used
final Collection<String> qnames = new HashSet<>();
String referencedName = qualifiedReference.getReferencedName();
if (referencedName != null) {
for (ResolveResult result : JSReferenceExpressionImpl.resolveUnqualified(referencedName, qualifiedReference, null)) {
if (result instanceof JSResolveResult) {
if (!myUnused.contains(((JSResolveResult)result).getActionScriptImport())) {
PsiElement element = (result).getElement();
if (qualifiedReference.getParent() instanceof JSNewExpression &&
element instanceof JSFunction &&
((JSFunction)element).isConstructor()) {
PsiElement parent = element.getParent();
if (parent instanceof JSQualifiedNamedElement) {
element = parent;
}
}
qnames.add(((JSQualifiedNamedElement)element).getQualifiedName());
}
}
}
}
// then all those that result from the import statements we will insert
PredefinedImportSet predefinedImportSet = new PredefinedImportSet(imports);
predefinedImportSet.process(qualifiedReference.getReferencedName(), null, myContainingFile, new ScopedImportSet.ImportProcessor<>() {
@Override
public Object process(@NotNull String referenceName, @NotNull ImportInfo info, @NotNull PsiNamedElement scope) {
final JSImportedElementResolveResult elementResolveResult = ScopedImportSet.resolveImportedClass(referenceName, scope, info);
if (elementResolveResult != null) qnames.add(elementResolveResult.qualifiedName);
return null;
}
});
final String fqn = qualifiedReference.getText();
if (qnames.isEmpty()) {
replaceWithShortName.add(qualifiedReference);
importsByHolder.putValue(importHolder, fqn);
}
else if (qnames.size() == 1 && fqn.equals(ContainerUtil.getFirstItem(qnames, null))) {
replaceWithShortName.add(qualifiedReference);
}
}
return new Results(replaceWithShortName, filter(myUnused), importsByHolder, filter(myUsed));
}
private static void addImport(MultiMap<Computable<JSElement>, String> importsByHolder,
Computable<JSElement> importHolder,
String newImport) {
String newPackageName = StringUtil.getPackageName(newImport);
String newShortName = StringUtil.getShortName(newImport);
for (Iterator<String> i = importsByHolder.get(importHolder).iterator(); i.hasNext();) {
String existing = i.next();
String existingPackageName = StringUtil.getPackageName(existing);
String existingShortName = StringUtil.getShortName(existing);
if (existingPackageName.equals(newPackageName)) {
if ("*".equals(existingShortName) && !"*".equals(newShortName)) {
return;
}
else if ("*".equals(newShortName)) {
i.remove();
}
}
}
importsByHolder.putValue(importHolder, newImport);
}
private static void collectElements(final PsiFile file,
final Map<XmlTag, Collection<PsiElement>> result,
final Collection<JSFile> processedFiles,
final @Nullable XmlTag rootTag) {
if (processedFiles.contains(file)) {
return;
}
if (file instanceof JSFile) {
processedFiles.add((JSFile)file);
PsiTreeUtil.processElements(file, new PsiElementProcessor<>() {
@Override
public boolean execute(@NotNull PsiElement element) {
if (element instanceof JSIncludeDirective) {
PsiFile includedFile = ((JSIncludeDirective)element).resolveFile();
// we check processed files before since we may include this file to self and setting context will make cycle
if (includedFile instanceof JSFile && !processedFiles.contains((JSFile)includedFile)) {
includedFile.putUserData(JSResolveUtil.contextKey, element);
collectElements(includedFile, result, processedFiles, rootTag);
}
}
else if (element instanceof JSElement && !(element instanceof JSFile)) {
Collection<PsiElement> elements = result.get(rootTag);
if (elements == null) {
elements = new ArrayList<>();
result.put(rootTag, elements);
}
elements.add(element);
}
return true;
}
});
}
else {
XmlBackedJSClass jsClass = XmlBackedJSClassFactory.getXmlBackedClass((XmlFile)file);
if (jsClass != null) {
jsClass.visitInjectedFiles(new XmlBackedJSClass.InjectedFileVisitor() {
@Override
public void visit(XmlTag rootTag, JSFile jsFile) {
collectElements(jsFile, result, processedFiles, rootTag);
}
});
}
}
}
}