/* * Copyright (c) Joachim Ansorg, [email protected] * * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 * * 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.ansorgit.plugins.bash.lang.psi.util; import com.ansorgit.plugins.bash.lang.psi.api.*; import com.ansorgit.plugins.bash.lang.psi.api.command.BashIncludeCommand; import com.ansorgit.plugins.bash.lang.psi.api.function.BashFunctionDef; import com.ansorgit.plugins.bash.lang.psi.api.loops.BashLoop; import com.ansorgit.plugins.bash.lang.psi.api.vars.BashVar; import com.ansorgit.plugins.bash.lang.psi.api.vars.BashVarDef; import com.ansorgit.plugins.bash.lang.psi.impl.Keys; import com.ansorgit.plugins.bash.lang.psi.impl.vars.BashVarProcessor; import com.ansorgit.plugins.bash.lang.psi.stubs.index.BashIncludeCommandIndex; import com.ansorgit.plugins.bash.lang.psi.stubs.index.BashVarDefIndex; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.intellij.ide.scratch.ScratchFileService; import com.intellij.injected.editor.VirtualFileWindow; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.FileIndexFacade; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.ResolveState; import com.intellij.psi.scope.PsiScopeProcessor; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.stubs.StubIndex; import com.intellij.psi.util.PsiTreeUtil; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Collection; import java.util.List; import java.util.Set; import java.util.function.Function; public final class BashResolveUtil { private BashResolveUtil() { } public static GlobalSearchScope varDefSearchScope(BashVar reference, boolean withIncludedFiles) { PsiFile referenceFile = BashPsiUtils.findFileContext(reference); if (!withIncludedFiles) { return GlobalSearchScope.fileScope(referenceFile.getProject(), referenceFile.getVirtualFile()); } Set<VirtualFile> result = Sets.newLinkedHashSet(); result.add(referenceFile.getVirtualFile()); int referenceFileOffset = BashPsiUtils.getFileTextOffset(reference); BashFunctionDef referenceFunctionContainer = BashPsiUtils.findNextVarDefFunctionDefScope(reference); for (BashIncludeCommand command : BashPsiUtils.findIncludeCommands(referenceFile, null)) { boolean includeIsInFunction = BashPsiUtils.findNextVarDefFunctionDefScope(command) != null; //either one of var or include command is in a function or the var is used after the include command if (referenceFunctionContainer != null || includeIsInFunction || (referenceFileOffset > BashPsiUtils.getFileTextEndOffset(command))) { BashFileReference fileReference = command.getFileReference(); PsiFile includedFile = fileReference != null ? fileReference.findReferencedFile() : null; if (includedFile != null) { result.add(includedFile.getVirtualFile()); //also, add all files included in the valid include command's file for (PsiFile file : BashPsiUtils.findIncludedFiles(includedFile, true)) { result.add(file.getVirtualFile()); } } } } return GlobalSearchScope.filesScope(referenceFile.getProject(), result); } /** * Iterate all variable definitions which apply to the given variable. * This includes redeclarations and the original definition it resolves to. * * @param reference The variable to use as starting point * @param resultProcessor The function called with each of the located definitions */ public static void walkVariableDefinitions(@NotNull BashVar reference, @NotNull Function<BashVarDef, Boolean> resultProcessor) { String varName = reference.getName(); if (StringUtils.isBlank(varName)) { return; } BashVarProcessor processor = new BashVarProcessor(reference, varName, true, false, true); resolve(reference, false, processor); Collection<PsiElement> results = processor.getResults(); if (results != null) { for (PsiElement result : results) { if (result instanceof BashVarDef) { resultProcessor.apply((BashVarDef) result); } } } } public static PsiElement resolve(BashVar bashVar, boolean dumbMode, boolean preferNeighborhood) { if (bashVar == null || !bashVar.isPhysical()) { return null; } final String varName = bashVar.getReferenceName(); if (varName == null) { return null; } return resolve(bashVar, dumbMode, new BashVarProcessor(bashVar, varName, true, preferNeighborhood, false)); } public static PsiElement resolve(BashVar bashVar, boolean dumbMode, ResolveProcessor processor) { if (bashVar == null || !bashVar.isPhysical()) { return null; } final String varName = bashVar.getReferenceName(); if (varName == null) { return null; } PsiFile psiFile = BashPsiUtils.findFileContext(bashVar); VirtualFile virtualFile = psiFile.getVirtualFile(); String filePath = virtualFile != null ? virtualFile.getPath() : null; Project project = bashVar.getProject(); ResolveState resolveState = ResolveState.initial(); GlobalSearchScope fileScope = GlobalSearchScope.fileScope(psiFile); Collection<BashVarDef> varDefs; if (dumbMode || isScratchFile(virtualFile) || isNotIndexedFile(project, virtualFile)) { varDefs = PsiTreeUtil.collectElementsOfType(psiFile, BashVarDef.class); } else { varDefs = StubIndex.getElements(BashVarDefIndex.KEY, varName, project, fileScope, BashVarDef.class); } for (BashVarDef varDef : varDefs) { ProgressManager.checkCanceled(); processor.execute(varDef, resolveState); } if (!dumbMode && filePath != null) { Collection<BashIncludeCommand> includeCommands = StubIndex.getElements(BashIncludeCommandIndex.KEY, filePath, project, fileScope, BashIncludeCommand.class); if (!includeCommands.isEmpty()) { boolean varIsInFunction = BashPsiUtils.findNextVarDefFunctionDefScope(bashVar) != null; for (BashIncludeCommand command : includeCommands) { ProgressManager.checkCanceled(); boolean includeIsInFunction = BashPsiUtils.findNextVarDefFunctionDefScope(command) != null; //either one of var or include command is in a function or the var is used after the include command if (varIsInFunction || includeIsInFunction || (BashPsiUtils.getFileTextOffset(bashVar) > BashPsiUtils.getFileTextEndOffset(command))) { try { resolveState = resolveState.put(Keys.resolvingIncludeCommand, command); command.processDeclarations(processor, resolveState, command, bashVar); } finally { resolveState = resolveState.put(Keys.resolvingIncludeCommand, null); } } } } } processor.prepareResults(); return processor.getBestResult(false, bashVar); } public static boolean isNotIndexedFile(@NonNls Project project, @Nullable VirtualFile virtualFile) { return virtualFile == null || virtualFile instanceof VirtualFileWindow || !FileIndexFacade.getInstance(project).isInContent(virtualFile); } public static boolean processContainerDeclarations(BashPsiElement thisElement, @NotNull final PsiScopeProcessor processor, @NotNull final ResolveState state, final PsiElement lastParent, @NotNull final PsiElement place) { if (thisElement == lastParent) { return true; } if (!processor.execute(thisElement, state)) { return false; } //process the current's elements children from first until lastParent is reached, a definition has to be before the use List<PsiElement> functions = Lists.newLinkedList(); //fixme use stubs for (PsiElement child = thisElement.getFirstChild(); child != null && child != lastParent; child = child.getNextSibling()) { if (child instanceof BashFunctionDef) { functions.add(child); } else if (!child.processDeclarations(processor, state, lastParent, place)) { return false; } } for (PsiElement function : functions) { if (!function.processDeclarations(processor, state, lastParent, place)) { return false; } } //fixme this is very slow atm if (lastParent != null && lastParent.getParent().isEquivalentTo(thisElement) && BashPsiUtils.findNextVarDefFunctionDefScope(place) != null) { for (PsiElement sibling = lastParent.getNextSibling(); sibling != null; sibling = sibling.getNextSibling()) { if (!sibling.processDeclarations(processor, state, null, place)) { return false; } } } return true; } public static boolean isScratchFile(@Nullable PsiFile file) { return file != null && isScratchFile(file.getVirtualFile()); } public static boolean isScratchFile(@Nullable VirtualFile file) { return file != null && ScratchFileService.getInstance().getRootType(file) != null; } /** * @return true if the definition of this variable is not child of a conditional command or loop. */ public static boolean hasStaticVarDefPath(BashVar bashVar) { BashReference reference = bashVar.getNeighborhoodReference(); if (reference == null) { return false; } PsiElement closestDef = reference.resolve(); if (closestDef == null) { return false; } // if the closest def is in a different def scope, then we can't handle that // (e.g. var is top-level, def is in a function or var is in a function and def in another function, etc.) BashFunctionDef varScope = BashPsiUtils.findNextVarDefFunctionDefScope(bashVar); BashFunctionDef defScope = BashPsiUtils.findNextVarDefFunctionDefScope(closestDef); if (varScope == null && defScope != null) { return false; } // we can't handle different functions as scope if (varScope != null && !varScope.isEquivalentTo(defScope)) { return false; } // atm we can't handle different files PsiFile psiFile = bashVar.getContainingFile(); if (varScope == null && !psiFile.isEquivalentTo(closestDef.getContainingFile())) { return false; } Collection<BashVarDef> allDefs = StubIndex.getElements(BashVarDefIndex.KEY, bashVar.getReferenceName(), bashVar.getProject(), GlobalSearchScope.fileScope(psiFile), BashVarDef.class); for (BashVarDef candidateDef : allDefs) { ProgressManager.checkCanceled(); // skip var defs which are not in our own def scope BashFunctionDef scope = BashPsiUtils.findNextVarDefFunctionDefScope(candidateDef); if (varScope != null && !varScope.isEquivalentTo(scope)) { continue; } // it's not a static path if the var def is in a conditional block or loop and if our var is not PsiElement parent = PsiTreeUtil.findFirstParent(candidateDef, psi -> psi instanceof BashConditionalBlock || psi instanceof BashLoop); if (parent != null && !PsiTreeUtil.isAncestor(parent, bashVar, true)) { return false; } } return true; } }