blob: 600eb701b346d63a12396a7432e61b0fd0a65057 [file] [log] [blame]
/*
* Copyright (c) 2010-2020 BSI Business Systems Integration AG.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BSI Business Systems Integration AG - initial API and implementation
*/
package org.eclipse.scout.sdk.s2i
import com.intellij.lang.java.JavaLanguage
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleUtil
import com.intellij.openapi.progress.EmptyProgressIndicator
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.JavaSdk
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.roots.ProjectFileIndex
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VfsUtilCore
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiElement
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.SearchScope
import com.intellij.psi.search.searches.ClassInheritorsSearch
import com.intellij.structuralsearch.*
import com.intellij.structuralsearch.plugin.util.CollectingMatchResultSink
import com.intellij.util.CollectionQuery
import com.intellij.util.Query
import com.intellij.util.containers.stream
import org.eclipse.scout.sdk.core.log.SdkLog
import org.eclipse.scout.sdk.core.log.SdkLog.onTrace
import org.eclipse.scout.sdk.core.model.api.IJavaEnvironment
import org.eclipse.scout.sdk.core.model.api.IType
import org.eclipse.scout.sdk.core.s.environment.IEnvironment
import org.eclipse.scout.sdk.core.s.environment.IProgress
import org.eclipse.scout.sdk.core.util.FinalValue
import org.eclipse.scout.sdk.core.util.SdkException
import org.eclipse.scout.sdk.core.util.visitor.IBreadthFirstVisitor
import org.eclipse.scout.sdk.core.util.visitor.TreeTraversals
import org.eclipse.scout.sdk.core.util.visitor.TreeVisitResult
import org.eclipse.scout.sdk.s2i.environment.IdeaEnvironment
import org.eclipse.scout.sdk.s2i.environment.IdeaEnvironment.Factory.computeInReadAction
import org.eclipse.scout.sdk.s2i.environment.IdeaProgress
import org.eclipse.scout.sdk.s2i.environment.model.JavaEnvironmentWithIdea
import java.lang.reflect.InvocationTargetException
import java.nio.file.Path
import java.util.function.Function
import java.util.stream.Stream
private val useLegacyMatcher: FinalValue<Boolean> = FinalValue()
fun IType.resolvePsi(): PsiClass? {
val module = this.javaEnvironment().toIdea().module
return computeInReadAction(module.project) {
module.project
.findTypesByName(name(), GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module, true))
.firstOrNull()
}
}
fun ProgressIndicator.toScoutProgress(): IdeaProgress = IdeaProgress(this)
fun PsiElement.resolveSourceRoot(): VirtualFile? {
return this.containingFile
?.virtualFile
?.let { ProjectFileIndex.getInstance(this.project).getSourceRootForFile(it) }
}
/**
* Converts this [PsiClass] into its corresponding Scout [IType].
*
* If the [PsiClass] is within a file in the [Project] (part of the project sources), the classpath of the parent module is used to resolve the Scout [IType].
*
* If not in the [Project] files and [returnReferencingModuleIfNotInFilesystem] is true, the [PsiClass] will be resolved based on the classpath of a [Module] that contains this [PsiClass]. It is undefined which [Module] exactly that is used.
*
* If not in the [Project] files and [returnReferencingModuleIfNotInFilesystem] is false, null is returned (no attempt is performed to resolve the [PsiClass]).
*
* @param returnReferencingModuleIfNotInFilesystem specifies how to handle [PsiClass]es which are not part of the [Project] files (see above). The default is true.
*
* @return The [IType] corresponding to this [PsiClass].
*/
fun PsiClass.toScoutType(env: IdeaEnvironment, returnReferencingModuleIfNotInFilesystem: Boolean = true): IType? =
containingModule(returnReferencingModuleIfNotInFilesystem)
?.let { env.toScoutJavaEnvironment(it) }
?.let { toScoutType(it) }
fun PsiClass.toScoutType(env: IJavaEnvironment): IType? {
val fqn = computeInReadAction(this.project) { this.qualifiedName }
return env.findType(fqn).orElse(null)
}
/**
* Gets the [Module] of the receiver.
* @param returnReferencingModuleIfNotInFilesystem specifies how to handle [PsiElement]s which are not part of the [Project] files (e.g. exist in a library).
* If true, querying the [Module] for such an element will return an instance that includes the [PsiElement] in its classpath.
* If false only files in the [Project] will return a [Module].
* @return The [Module] in which this [PsiElement] exists.
*
*/
fun PsiElement.containingModule(returnReferencingModuleIfNotInFilesystem: Boolean = true): Module? {
val isInProject = containingFile?.virtualFile?.isInLocalFileSystem ?: true /* a psi element which has not a file (e.g. PsiDirectory) */
if (!returnReferencingModuleIfNotInFilesystem && !isInProject) {
return null
}
val searchElement = if (isInProject) this else containingFile ?: this
return computeInReadAction(this.project) {
this
.takeIf { it.isValid }
?.let { ModuleUtil.findModuleForPsiElement(searchElement) }
}
}
fun IProgress.toIdea(): IdeaProgress = this as IdeaProgress
fun Module.isJavaModule(): Boolean = ModuleRootManager.getInstance(this).sdk?.sdkType == JavaSdk.getInstance()
fun IEnvironment.toIdea(): IdeaEnvironment = this as IdeaEnvironment
fun IJavaEnvironment.toIdea(): JavaEnvironmentWithIdea = this.unwrap() as JavaEnvironmentWithIdea
fun PsiClass.newSubTypeHierarchy(scope: SearchScope, checkDeep: Boolean, includeAnonymous: Boolean, includeRoot: Boolean): Query<PsiClass> {
val children = ClassInheritorsSearch.search(this, scope, checkDeep, true, includeAnonymous)
if (!includeRoot) {
return children
}
val resultWithRoot = children.findAll()
resultWithRoot.add(this)
return CollectionQuery(resultWithRoot)
}
fun Path.toVirtualFile() = VfsUtil.findFile(this, true)
?.takeIf { it.isValid }
fun Project.findAllTypesAnnotatedWith(annotation: String, scope: SearchScope) = findAllTypesAnnotatedWith(annotation, scope, null)
fun Project.findAllTypesAnnotatedWith(annotation: String, scope: SearchScope, indicator: ProgressIndicator?): Sequence<PsiClass> {
val options = MatchOptions()
options.dialect = JavaLanguage.INSTANCE
options.isCaseSensitiveMatch = true
options.isRecursiveSearch = true
options.scope = scope
options.searchPattern = "@$annotation( )\nclass \$Class\$ {}"
val constraint = MatchVariableConstraint()
constraint.name = "Class"
options.addVariableConstraint(constraint)
return structuralSearch(options, indicator)
.map { it.match }
.filter { it.isValid }
.filter { it.isPhysical }
.filter { it is PsiClass }
.map { it as PsiClass }
}
fun Project.structuralSearch(query: MatchOptions, indicator: ProgressIndicator?): Sequence<MatchResult> {
val progress = indicator ?: EmptyProgressIndicator()
val result = object : CollectingMatchResultSink() {
override fun getProgressIndicator(): ProgressIndicator {
return progress
}
}
findMatches(Matcher(this, query), result, query)
return result.matches.asSequence()
}
private fun findMatches(matcher: Matcher, result: MatchResultSink, options: MatchOptions) {
// sample taken from com.intellij.structuralsearch.plugin.ui.SearchCommand
// the API is different in IntelliJ 19x than in 20x
try {
if (useLegacyMatcher.computeIfAbsentAndGet { isUseLegacyMatcher() }) {
findMatchesLegacy(matcher, result, options)
} else {
findMatchesNew(matcher, result)
}
} catch (e: InvocationTargetException) {
throw expandInvocationTargetException(e)
}
}
private fun expandInvocationTargetException(e: InvocationTargetException): RuntimeException {
var original: Throwable? = e
while (original is InvocationTargetException) {
original = e.cause
}
if (original is RuntimeException) {
return original
}
return SdkException(original)
}
// Can be removed if the supported min. IJ version is 2020.1
private fun isUseLegacyMatcher() =
try {
findMatchesMethodNew()
false
} catch (e: NoSuchMethodException) {
SdkLog.debug("Using legacy structural search API", onTrace(e))
true
}
private fun findMatchesMethodNew() = Matcher::class.java.getMethod("findMatches", MatchResultSink::class.java)
private fun findMatchesMethodLegacy() = Matcher::class.java.getMethod("findMatches", MatchResultSink::class.java, MatchOptions::class.java)
private fun findMatchesNew(matcher: Matcher, result: MatchResultSink) = findMatchesMethodNew().invoke(matcher, result)
private fun findMatchesLegacy(matcher: Matcher, result: MatchResultSink, options: MatchOptions) = findMatchesMethodLegacy().invoke(matcher, result, options)
fun Project.findTypesByName(fqn: String) = findTypesByName(fqn, GlobalSearchScope.allScope(this))
fun Project.findTypesByName(fqn: String, scope: GlobalSearchScope) =
computeInReadAction(this) {
JavaPsiFacade.getInstance(this)
.findClasses(fqn, scope)
.toSet()
}
.filter { it.isValid }
fun VirtualFile.toNioPath(): Path = VfsUtilCore.virtualToIoFile(this).toPath()
fun VirtualFile.containingModule(project: Project) = ProjectFileIndex.getInstance(project).getModuleForFile(this)
fun PsiClass.visitSupers(visitor: IBreadthFirstVisitor<PsiClass>): TreeVisitResult {
val supplier: Function<PsiClass, Stream<out PsiClass>> = Function { a -> a.supers.stream() }
return TreeTraversals.create(visitor, supplier).traverse(this)
}
fun PsiClass.isInstanceOf(vararg parentFqn: String): Boolean =
computeInReadAction(project) {
visitSupers(IBreadthFirstVisitor { element, _, _ ->
if (parentFqn.contains(element.qualifiedName))
TreeVisitResult.TERMINATE
else
TreeVisitResult.CONTINUE
}) == TreeVisitResult.TERMINATE
}