Files
openide/platform/analysis-impl/src/com/intellij/codeInspection/InspectionEngine.java
Yuriy Artamonov f8f9f82c96 [inspections] IDEA-321711 Try to eliminate overhead of PsiElementVisitor calling checkCancelled in inspections
GitOrigin-RevId: a507f6e0a41aae12abe4257e5047eb4a210cbf44
2023-09-16 15:18:22 +00:00

500 lines
27 KiB
Java

// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInspection;
import com.intellij.analysis.AnalysisScope;
import com.intellij.codeInsight.daemon.impl.Divider;
import com.intellij.codeInsight.daemon.impl.analysis.HighlightingLevelManager;
import com.intellij.codeInspection.ex.*;
import com.intellij.codeInspection.reference.RefElement;
import com.intellij.codeInspection.reference.RefEntity;
import com.intellij.codeInspection.reference.RefManagerImpl;
import com.intellij.codeInspection.reference.RefVisitor;
import com.intellij.concurrency.ConcurrentCollectionFactory;
import com.intellij.concurrency.JobLauncher;
import com.intellij.diagnostic.PluginException;
import com.intellij.lang.Language;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.EmptyProgressIndicator;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Predicates;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.util.CommonProcessors;
import com.intellij.util.PairProcessor;
import com.intellij.util.Processor;
import com.intellij.util.TimeoutUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.SmartHashSet;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static java.util.Collections.singletonList;
public final class InspectionEngine {
private static final Logger LOG = Logger.getInstance(InspectionEngine.class);
private static boolean createVisitorAndAcceptElements(@NotNull LocalInspectionTool tool,
@NotNull ProblemsHolder holder,
boolean isOnTheFly,
@NotNull LocalInspectionToolSession session,
@NotNull List<? extends PsiElement> elements,
Map<Class<?>, Collection<Class<?>>> targetPsiClasses) {
PsiElementVisitor visitor = createVisitor(tool, holder, isOnTheFly, session);
// if inspection returned empty visitor then it should be skipped
if (visitor == PsiElementVisitor.EMPTY_VISITOR) return false;
List<Class<?>> acceptingPsiTypes = InspectionVisitorsOptimizer.getAcceptingPsiTypes(visitor);
tool.inspectionStarted(session, isOnTheFly);
acceptElements(elements, visitor, targetPsiClasses, acceptingPsiTypes);
tool.inspectionFinished(session, holder);
return true;
}
@NotNull
public static PsiElementVisitor createVisitor(@NotNull LocalInspectionTool tool,
@NotNull ProblemsHolder holder,
boolean isOnTheFly,
@NotNull LocalInspectionToolSession session) {
PsiElementVisitor visitor = tool.buildVisitor(holder, isOnTheFly, session);
//noinspection ConstantConditions
if (visitor == null) {
LOG.error("Tool " + tool + " (" + tool.getClass() + ") must not return null from the buildVisitor() method");
}
else if (visitor instanceof PsiRecursiveVisitor) {
LOG.error("The visitor returned from LocalInspectionTool.buildVisitor() must not be recursive: " + tool);
}
return visitor;
}
private static void acceptElements(@NotNull List<? extends PsiElement> elements,
@NotNull PsiElementVisitor elementVisitor,
Map<Class<?>, Collection<Class<?>>> targetPsiClasses,
List<Class<?>> acceptingPsiTypes) {
if (acceptingPsiTypes == InspectionVisitorsOptimizer.ALL_ELEMENTS_VISIT_LIST) {
for (int i = 0, elementsSize = elements.size(); i < elementsSize; i++) {
PsiElement element = elements.get(i);
element.accept(elementVisitor);
ProgressManager.checkCanceled();
}
}
else {
Set<Class<?>> accepts = InspectionVisitorsOptimizer.getVisitorAcceptClasses(targetPsiClasses, acceptingPsiTypes);
if (accepts == null || accepts.isEmpty()) {
return; // nothing to visit in this run
}
for (int i = 0, elementsSize = elements.size(); i < elementsSize; i++) {
PsiElement element = elements.get(i);
if (accepts.contains(element.getClass())) {
element.accept(elementVisitor);
ProgressManager.checkCanceled();
}
}
}
}
/**
* @deprecated use {@link #inspectEx(List, PsiFile, TextRange, TextRange, boolean, boolean, boolean, ProgressIndicator, PairProcessor)}
*/
@Deprecated
// returns map (toolName -> problem descriptors)
public static @NotNull Map<String, List<ProblemDescriptor>> inspectEx(@NotNull List<? extends LocalInspectionToolWrapper> toolWrappers,
@NotNull PsiFile psiFile,
@NotNull InspectionManager iManager,
boolean isOnTheFly,
@NotNull ProgressIndicator indicator) {
Map<LocalInspectionToolWrapper, List<ProblemDescriptor>> map =
inspectEx(toolWrappers, psiFile, psiFile.getTextRange(), psiFile.getTextRange(), isOnTheFly, false, true, indicator, PairProcessor.alwaysTrue());
return map.entrySet().stream().map(e->Pair.create(e.getKey().getShortName(), e.getValue())).collect(Collectors.toMap(p->p.getFirst(), p->p.getSecond()));
}
// returns map (tool -> problem descriptors)
@NotNull
public static Map<LocalInspectionToolWrapper, List<ProblemDescriptor>> inspectEx(@NotNull List<? extends LocalInspectionToolWrapper> toolWrappers,
@NotNull PsiFile psiFile,
@NotNull TextRange restrictRange,
@NotNull TextRange priorityRange,
boolean isOnTheFly,
boolean inspectInjectedPsi,
boolean ignoreSuppressedElements,
@NotNull ProgressIndicator indicator,
// when returned true -> add to the holder, false -> do not add to the holder
@NotNull PairProcessor<? super LocalInspectionToolWrapper, ? super ProblemDescriptor> foundDescriptorCallback) {
if (toolWrappers.isEmpty()) return Collections.emptyMap();
List<Divider.DividedElements> allDivided = new ArrayList<>();
Divider.divideInsideAndOutsideAllRoots(psiFile, restrictRange, priorityRange, Predicates.alwaysTrue(), new CommonProcessors.CollectProcessor<>(allDivided));
List<PsiElement> elements = ContainerUtil.concat(
ContainerUtil.map(allDivided, d -> ContainerUtil.concat(d.inside(), d.outside(), d.parents())));
Map<LocalInspectionToolWrapper, List<ProblemDescriptor>> map = inspectElements(toolWrappers, psiFile, restrictRange, ignoreSuppressedElements, isOnTheFly, indicator, elements, foundDescriptorCallback);
if (inspectInjectedPsi) {
InjectedLanguageManager injectedLanguageManager = InjectedLanguageManager.getInstance(psiFile.getProject());
Set<Pair<PsiFile, PsiElement>> injectedFiles = new HashSet<>();
for (PsiElement element : elements) {
if (element instanceof PsiLanguageInjectionHost) {
List<Pair<PsiElement, TextRange>> files = injectedLanguageManager.getInjectedPsiFiles(element);
if (files != null) {
for (Pair<PsiElement, TextRange> pair : files) {
PsiFile injectedFile = (PsiFile)pair.getFirst();
injectedFiles.add(Pair.create(injectedFile, element));
}
}
}
}
if (!JobLauncher.getInstance().invokeConcurrentlyUnderProgress(new ArrayList<>(injectedFiles), indicator, pair -> {
PsiFile injectedFile = pair.getFirst();
PsiElement host = pair.getSecond();
List<PsiElement> injectedElements = new ArrayList<>();
Set<String> injectedDialects = new HashSet<>();
getAllElementsAndDialectsFrom(injectedFile, injectedElements, injectedDialects);
Map<LocalInspectionToolWrapper, List<ProblemDescriptor>> result =
inspectElements(toolWrappers, injectedFile, injectedFile.getTextRange(), isOnTheFly, indicator, ignoreSuppressedElements,
injectedElements,
injectedDialects, foundDescriptorCallback);
for (Map.Entry<LocalInspectionToolWrapper, List<ProblemDescriptor>> entry : result.entrySet()) {
LocalInspectionToolWrapper toolWrapper = entry.getKey();
List<ProblemDescriptor> descriptors = entry.getValue();
List<ProblemDescriptor> filtered = ignoreSuppressedElements ? ContainerUtil.filter(descriptors, descriptor -> !toolWrapper.getTool().isSuppressedFor(host)) : descriptors;
// in case two injected fragments contain result of the same inspection, concatenate them
// assume map is ConcurrentHashMap here, otherwise synchronization would be needed
map.merge(toolWrapper, filtered, (oldList, newList)->ContainerUtil.concat(oldList, newList));
}
return true;
})) {
throw new ProcessCanceledException();
}
}
return map;
}
private static void getAllElementsAndDialectsFrom(@NotNull PsiFile psiFile,
@NotNull List<? super PsiElement> outElements,
@NotNull Set<? super String> outDialects) {
FileViewProvider viewProvider = psiFile.getViewProvider();
Set<Language> processedLanguages = new SmartHashSet<>();
// we hope that injected file here is small enough for PsiRecursiveElementVisitor
PsiElementVisitor visitor = new PsiRecursiveElementVisitor() {
@Override
public void visitElement(@NotNull PsiElement element) {
ProgressManager.checkCanceled();
PsiElement child = element.getFirstChild();
while (child != null) {
outElements.add(child);
child.accept(this);
appendDialects(child, processedLanguages, outDialects);
child = child.getNextSibling();
}
}
};
for (Language language : viewProvider.getLanguages()) {
PsiFile psiRoot = viewProvider.getPsi(language);
if (psiRoot == null || !HighlightingLevelManager.getInstance(psiFile.getProject()).shouldInspect(psiRoot)) {
continue;
}
outElements.add(psiRoot);
psiRoot.accept(visitor);
appendDialects(psiRoot, processedLanguages, outDialects);
}
}
private static void appendDialects(@NotNull PsiElement element,
@NotNull Set<? super Language> outProcessedLanguages,
@NotNull Set<? super String> outDialectIds) {
Language language = element.getLanguage();
outDialectIds.add(language.getID());
if (outProcessedLanguages.add(language)) {
for (Language dialect : language.getDialects()) {
outDialectIds.add(dialect.getID());
}
}
}
// returns map tool -> list of descriptors found
public static @NotNull Map<LocalInspectionToolWrapper, List<ProblemDescriptor>> inspectElements(@NotNull List<? extends LocalInspectionToolWrapper> toolWrappers,
@NotNull PsiFile psiFile,
@NotNull TextRange restrictRange,
boolean ignoreSuppressedElements,
boolean isOnTheFly,
@NotNull ProgressIndicator indicator,
@NotNull List<? extends PsiElement> elements,
// when returned true -> add to the holder, false -> do not add to the holder
@NotNull PairProcessor<? super LocalInspectionToolWrapper, ? super ProblemDescriptor> foundDescriptorCallback) {
return inspectElements(toolWrappers, psiFile, restrictRange, isOnTheFly, indicator, ignoreSuppressedElements, elements, calcElementDialectIds(elements),
foundDescriptorCallback);
}
@ApiStatus.Internal
public static void withSession(@NotNull PsiFile psiFile,
@NotNull TextRange restrictRange,
@NotNull TextRange priorityRange,
@Nullable HighlightSeverity minimumSeverity,
boolean isOnTheFly,
@NotNull Consumer<? super LocalInspectionToolSession> runnable) {
LocalInspectionToolSession session = new LocalInspectionToolSession(psiFile, priorityRange, restrictRange, minimumSeverity);
runnable.accept(session);
}
private static final Set<String> ourToolsWithInformationProblems = ConcurrentCollectionFactory.createConcurrentSet();
private static boolean warnAboutInformationLevelInBatchMode(@NotNull ProblemHighlightType highlightType,
@NotNull LocalInspectionToolWrapper toolWrapper,
@NotNull PsiFile psiFile) {
if (highlightType == ProblemHighlightType.INFORMATION) {
String shortName = toolWrapper.getShortName();
if (ourToolsWithInformationProblems.add(shortName)) {
String message = "Tool #" + shortName + " (" + toolWrapper.getTool().getClass()+")"+
" registers 'INFORMATION'-level problem in batch mode on " + psiFile + ". " +
"Warnings of the 'INFORMATION' level are invisible in the editor and should not become visible in batch mode. " +
"Moreover, since 'INFORMATION'-level fixes act more like intention actions, they could e.g. change semantics and " +
"thus should not be suggested for batch transformations";
LocalInspectionEP extension = toolWrapper.getExtension();
if (extension == null) {
LOG.error(message);
}
else {
LOG.error(new PluginException(message, extension.getPluginDescriptor().getPluginId()));
}
}
return true;
}
return false;
}
private static @NotNull Map<LocalInspectionToolWrapper, List<ProblemDescriptor>> inspectElements(@NotNull List<? extends LocalInspectionToolWrapper> toolWrappers,
@NotNull PsiFile psiFile,
@NotNull TextRange restrictRange,
boolean isOnTheFly,
@NotNull ProgressIndicator indicator,
boolean ignoreSuppressedElements,
@NotNull List<? extends PsiElement> elements,
@NotNull Set<String> elementDialectIds,
// when returned true -> add to the holder, false -> do not add to the holder
@NotNull PairProcessor<? super LocalInspectionToolWrapper, ? super ProblemDescriptor> foundDescriptorCallback) {
Map<LocalInspectionToolWrapper, List<ProblemDescriptor>> resultDescriptors = new ConcurrentHashMap<>();
withSession(psiFile, restrictRange, restrictRange, HighlightSeverity.INFORMATION, isOnTheFly, session -> {
List<LocalInspectionToolWrapper> applicableTools = filterToolsApplicableByLanguage(toolWrappers, elementDialectIds);
Map<Class<?>, Collection<Class<?>>> targetPsiClasses = InspectionVisitorsOptimizer.getTargetPsiClasses(elements);
Processor<LocalInspectionToolWrapper> processor = toolWrapper -> {
ProblemsHolder holder = new ProblemsHolder(InspectionManager.getInstance(psiFile.getProject()), psiFile, isOnTheFly){
@Override
public void registerProblem(@NotNull ProblemDescriptor descriptor) {
if (!isOnTheFly) {
ProblemHighlightType highlightType = descriptor.getHighlightType();
if (warnAboutInformationLevelInBatchMode(highlightType, toolWrapper, psiFile)) {
return;
}
if (highlightType == ProblemHighlightType.POSSIBLE_PROBLEM) {
return;
}
}
if (foundDescriptorCallback.process(toolWrapper, descriptor)) {
super.registerProblem(descriptor);
}
}
};
LocalInspectionTool tool = toolWrapper.getTool();
long inspectionStartTime = System.nanoTime();
boolean inspectionWasRun = createVisitorAndAcceptElements(tool, holder, isOnTheFly, session, elements, targetPsiClasses);
long inspectionDuration = TimeoutUtil.getDurationMillis(inspectionStartTime);
reportToQodana(psiFile, isOnTheFly, toolWrapper, inspectionWasRun, inspectionDuration, holder.getResultCount());
if (holder.hasResults()) {
List<ProblemDescriptor> descriptors = ContainerUtil.filter(holder.getResults(), descriptor -> {
PsiElement element = descriptor.getPsiElement();
return element == null || !ignoreSuppressedElements || !SuppressionUtil.inspectionResultSuppressed(element, tool);
});
resultDescriptors.put(toolWrapper, descriptors);
}
return true;
};
JobLauncher.getInstance().invokeConcurrentlyUnderProgress(applicableTools, indicator, processor);
});
return resultDescriptors;
}
private static void reportToQodana(@NotNull PsiFile psiFile,
boolean isOnTheFly,
@NotNull LocalInspectionToolWrapper toolWrapper,
boolean inspectionWasRun,
long inspectionDuration, int resultCount) {
boolean needToReportStatsToQodana = inspectionWasRun && !isOnTheFly;
if (needToReportStatsToQodana) {
InspectListener publisher = psiFile.getProject().getMessageBus().syncPublisher(GlobalInspectionContextEx.INSPECT_TOPIC);
publisher.inspectionFinished(inspectionDuration, Thread.currentThread().getId(), resultCount, toolWrapper,
InspectListener.InspectionKind.LOCAL, psiFile, psiFile.getProject());
}
}
public static @NotNull @Unmodifiable List<ProblemDescriptor> runInspectionOnFile(@NotNull PsiFile psiFile,
@NotNull InspectionToolWrapper<?, ?> toolWrapper,
@NotNull GlobalInspectionContext inspectionContext) {
InspectionManager inspectionManager = InspectionManager.getInstance(psiFile.getProject());
toolWrapper.initialize(inspectionContext);
RefManagerImpl refManager = (RefManagerImpl)inspectionContext.getRefManager();
List<ProblemDescriptor> result = new ArrayList<>();
refManager.runInsideInspectionReadAction(() -> {
try {
if (toolWrapper instanceof LocalInspectionToolWrapper) {
Map<LocalInspectionToolWrapper, List<ProblemDescriptor>> problemDescriptors =
inspectEx(Collections.singletonList((LocalInspectionToolWrapper)toolWrapper), psiFile, psiFile.getTextRange(), psiFile.getTextRange(),
false,
false, true, new EmptyProgressIndicator(), PairProcessor.alwaysTrue());
for (List<ProblemDescriptor> group : problemDescriptors.values()) {
result.addAll(group);
}
}
else if (toolWrapper instanceof GlobalInspectionToolWrapper) {
GlobalInspectionTool globalTool = ((GlobalInspectionToolWrapper)toolWrapper).getTool();
if (globalTool instanceof GlobalSimpleInspectionTool simpleTool) {
ProblemsHolder problemsHolder = new ProblemsHolder(inspectionManager, psiFile, false);
ProblemDescriptionsProcessor collectProcessor = new ProblemDescriptionsProcessor() {
@Override
public CommonProblemDescriptor[] getDescriptions(@NotNull RefEntity refEntity) {
return result.toArray(CommonProblemDescriptor.EMPTY_ARRAY);
}
@Override
public void ignoreElement(@NotNull RefEntity refEntity) {
throw new UnsupportedOperationException();
}
@Override
public void resolveProblem(@NotNull CommonProblemDescriptor descriptor) {
throw new UnsupportedOperationException();
}
@Override
public void addProblemElement(@Nullable RefEntity refEntity, CommonProblemDescriptor @NotNull ... commonProblemDescriptors) {
if (!(refEntity instanceof RefElement)) return;
PsiElement element = ((RefElement)refEntity).getPsiElement();
convertToProblemDescriptors(element, commonProblemDescriptors, result);
}
@Override
public RefEntity getElement(@NotNull CommonProblemDescriptor descriptor) {
throw new RuntimeException();
}
};
simpleTool.checkFile(psiFile, inspectionManager, problemsHolder, inspectionContext, collectProcessor);
}
else {
RefElement fileRef = refManager.getReference(psiFile);
AnalysisScope scope = new AnalysisScope(psiFile);
assert fileRef != null;
fileRef.accept(new RefVisitor() {
@Override
public void visitElement(@NotNull RefEntity elem) {
CommonProblemDescriptor[] elemDescriptors = globalTool.checkElement(elem, scope, inspectionManager, inspectionContext);
if (elemDescriptors != null) {
convertToProblemDescriptors(psiFile, elemDescriptors, result);
}
for (RefEntity child : elem.getChildren()) {
child.accept(this);
}
}
});
}
}
}
finally {
toolWrapper.cleanup(psiFile.getProject());
inspectionContext.cleanup();
}
});
return result;
}
private static void convertToProblemDescriptors(@NotNull PsiElement element,
CommonProblemDescriptor @NotNull [] commonProblemDescriptors,
@NotNull List<? super ProblemDescriptor> outDescriptors) {
for (CommonProblemDescriptor common : commonProblemDescriptors) {
if (common instanceof ProblemDescriptor) {
outDescriptors.add((ProblemDescriptor)common);
}
else {
ProblemDescriptorBase base =
new ProblemDescriptorBase(element, element, common.getDescriptionTemplate(), (LocalQuickFix[])common.getFixes(),
ProblemHighlightType.GENERIC_ERROR_OR_WARNING, false, null, false, false);
outDescriptors.add(base);
}
}
}
public static @NotNull List<LocalInspectionToolWrapper> filterToolsApplicableByLanguage(@NotNull Collection<? extends LocalInspectionToolWrapper> tools,
@NotNull Set<String> elementDialectIds) {
Map<String, Boolean> resultsWithDialects = new HashMap<>();
Map<String, Boolean> resultsNoDialects = new HashMap<>();
return ContainerUtil.filter(tools, tool -> {
String toolLanguageId = tool.getLanguage();
if (toolLanguageId == null || toolLanguageId.isBlank() || "any".equals(toolLanguageId)) return true;
boolean applyToDialects = tool.applyToDialects();
Map<String, Boolean> map = applyToDialects ? resultsWithDialects : resultsNoDialects;
return map.computeIfAbsent(toolLanguageId, __ ->
ToolLanguageUtil.isToolLanguageOneOf(elementDialectIds, toolLanguageId, applyToDialects));
});
}
public static @NotNull Set<String> calcElementDialectIds(@NotNull List<? extends PsiElement> inside, @NotNull List<? extends PsiElement> outside) {
Set<String> dialectIds = new HashSet<>();
Set<Language> processedLanguages = new HashSet<>();
addDialects(inside, processedLanguages, dialectIds);
addDialects(outside, processedLanguages, dialectIds);
return dialectIds;
}
private static @NotNull Set<String> calcElementDialectIds(@NotNull List<? extends PsiElement> elements) {
Set<String> dialectIds = new HashSet<>();
Set<Language> processedLanguages = new HashSet<>();
addDialects(elements, processedLanguages, dialectIds);
return dialectIds;
}
private static void addDialects(@NotNull List<? extends PsiElement> elements,
@NotNull Set<? super Language> outProcessedLanguages,
@NotNull Set<? super String> outDialectIds) {
for (PsiElement element : elements) {
Language language = element.getLanguage();
outDialectIds.add(language.getID());
addDialects(language, outProcessedLanguages, outDialectIds);
}
}
private static void addDialects(@NotNull Language language,
@NotNull Set<? super Language> outProcessedLanguages,
@NotNull Set<? super String> outDialectIds) {
if (outProcessedLanguages.add(language)) {
Collection<Language> dialects = language.getTransitiveDialects();
outProcessedLanguages.addAll(dialects);
for (Language dialect : dialects) {
outDialectIds.add(dialect.getID());
}
}
}
}