// 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 elements, Map, Collection>> 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> 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 elements, @NotNull PsiElementVisitor elementVisitor, Map, Collection>> targetPsiClasses, List> 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> 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> inspectEx(@NotNull List toolWrappers, @NotNull PsiFile psiFile, @NotNull InspectionManager iManager, boolean isOnTheFly, @NotNull ProgressIndicator indicator) { Map> 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> inspectEx(@NotNull List 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 foundDescriptorCallback) { if (toolWrappers.isEmpty()) return Collections.emptyMap(); List allDivided = new ArrayList<>(); Divider.divideInsideAndOutsideAllRoots(psiFile, restrictRange, priorityRange, Predicates.alwaysTrue(), new CommonProcessors.CollectProcessor<>(allDivided)); List elements = ContainerUtil.concat( ContainerUtil.map(allDivided, d -> ContainerUtil.concat(d.inside(), d.outside(), d.parents()))); Map> map = inspectElements(toolWrappers, psiFile, restrictRange, ignoreSuppressedElements, isOnTheFly, indicator, elements, foundDescriptorCallback); if (inspectInjectedPsi) { InjectedLanguageManager injectedLanguageManager = InjectedLanguageManager.getInstance(psiFile.getProject()); Set> injectedFiles = new HashSet<>(); for (PsiElement element : elements) { if (element instanceof PsiLanguageInjectionHost) { List> files = injectedLanguageManager.getInjectedPsiFiles(element); if (files != null) { for (Pair 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 injectedElements = new ArrayList<>(); Set injectedDialects = new HashSet<>(); getAllElementsAndDialectsFrom(injectedFile, injectedElements, injectedDialects); Map> result = inspectElements(toolWrappers, injectedFile, injectedFile.getTextRange(), isOnTheFly, indicator, ignoreSuppressedElements, injectedElements, injectedDialects, foundDescriptorCallback); for (Map.Entry> entry : result.entrySet()) { LocalInspectionToolWrapper toolWrapper = entry.getKey(); List descriptors = entry.getValue(); List 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 outElements, @NotNull Set outDialects) { FileViewProvider viewProvider = psiFile.getViewProvider(); Set 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 outProcessedLanguages, @NotNull Set 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> inspectElements(@NotNull List toolWrappers, @NotNull PsiFile psiFile, @NotNull TextRange restrictRange, boolean ignoreSuppressedElements, boolean isOnTheFly, @NotNull ProgressIndicator indicator, @NotNull List elements, // when returned true -> add to the holder, false -> do not add to the holder @NotNull PairProcessor 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 runnable) { LocalInspectionToolSession session = new LocalInspectionToolSession(psiFile, priorityRange, restrictRange, minimumSeverity); runnable.accept(session); } private static final Set 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> inspectElements(@NotNull List toolWrappers, @NotNull PsiFile psiFile, @NotNull TextRange restrictRange, boolean isOnTheFly, @NotNull ProgressIndicator indicator, boolean ignoreSuppressedElements, @NotNull List elements, @NotNull Set elementDialectIds, // when returned true -> add to the holder, false -> do not add to the holder @NotNull PairProcessor foundDescriptorCallback) { Map> resultDescriptors = new ConcurrentHashMap<>(); withSession(psiFile, restrictRange, restrictRange, HighlightSeverity.INFORMATION, isOnTheFly, session -> { List applicableTools = filterToolsApplicableByLanguage(toolWrappers, elementDialectIds); Map, Collection>> targetPsiClasses = InspectionVisitorsOptimizer.getTargetPsiClasses(elements); Processor 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 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 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 result = new ArrayList<>(); refManager.runInsideInspectionReadAction(() -> { try { if (toolWrapper instanceof LocalInspectionToolWrapper) { Map> problemDescriptors = inspectEx(Collections.singletonList((LocalInspectionToolWrapper)toolWrapper), psiFile, psiFile.getTextRange(), psiFile.getTextRange(), false, false, true, new EmptyProgressIndicator(), PairProcessor.alwaysTrue()); for (List 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 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 filterToolsApplicableByLanguage(@NotNull Collection tools, @NotNull Set elementDialectIds) { Map resultsWithDialects = new HashMap<>(); Map 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 map = applyToDialects ? resultsWithDialects : resultsNoDialects; return map.computeIfAbsent(toolLanguageId, __ -> ToolLanguageUtil.isToolLanguageOneOf(elementDialectIds, toolLanguageId, applyToDialects)); }); } public static @NotNull Set calcElementDialectIds(@NotNull List inside, @NotNull List outside) { Set dialectIds = new HashSet<>(); Set processedLanguages = new HashSet<>(); addDialects(inside, processedLanguages, dialectIds); addDialects(outside, processedLanguages, dialectIds); return dialectIds; } private static @NotNull Set calcElementDialectIds(@NotNull List elements) { Set dialectIds = new HashSet<>(); Set processedLanguages = new HashSet<>(); addDialects(elements, processedLanguages, dialectIds); return dialectIds; } private static void addDialects(@NotNull List elements, @NotNull Set outProcessedLanguages, @NotNull Set 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 outProcessedLanguages, @NotNull Set outDialectIds) { if (outProcessedLanguages.add(language)) { Collection dialects = language.getTransitiveDialects(); outProcessedLanguages.addAll(dialects); for (Language dialect : dialects) { outDialectIds.add(dialect.getID()); } } } }