mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-55589/PY-65967 Implement support for partial stubs
GitOrigin-RevId: eae99e99b99d7d52b4b7f6851bef2fe868e80f1a
This commit is contained in:
committed by
intellij-monorepo-bot
parent
a5c09533c6
commit
a30073abff
@@ -160,6 +160,7 @@
|
||||
|
||||
<pluginSuggestionProvider order="first" implementation="com.jetbrains.python.suggestions.PycharmProSuggestionProvider"/>
|
||||
<postStartupActivity implementation="com.jetbrains.python.poetry.PoetryPyProjectTomlPostStartupActivity"/>
|
||||
<postStartupActivity implementation="com.jetbrains.python.psi.resolve.PyPartialStubMarkersInvalidatorActivity"/>
|
||||
|
||||
<virtualFileCustomDataProvider implementation="com.jetbrains.python.psi.PyLangLevelVirtualFileCustomDataProvider"/>
|
||||
<statistics.counterUsagesCollector implementationClass="com.jetbrains.python.run.features.PyRunToolUsageCollector"/>
|
||||
|
||||
@@ -66,6 +66,8 @@ jvm_library(
|
||||
"//libraries/hdr-histogram",
|
||||
"//libraries/kotlinx/collections-immutable:libraries-kotlinx-collections-immutable",
|
||||
"//python/python-requirements:requirements",
|
||||
"//plugins/toml",
|
||||
"//plugins/toml/core",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -128,6 +130,8 @@ jvm_library(
|
||||
"//libraries/hdr-histogram:hdr-histogram_test_lib",
|
||||
"//libraries/kotlinx/collections-immutable:libraries-kotlinx-collections-immutable_test_lib",
|
||||
"//python/python-requirements:requirements_test_lib",
|
||||
"//plugins/toml:toml_test_lib",
|
||||
"//plugins/toml/core:core_test_lib",
|
||||
],
|
||||
)
|
||||
### auto-generated section `build intellij.python.psi.impl` end
|
||||
|
||||
@@ -52,5 +52,7 @@
|
||||
<orderEntry type="module" module-name="intellij.libraries.hdr.histogram" />
|
||||
<orderEntry type="module" module-name="intellij.libraries.kotlinx.collections.immutable" />
|
||||
<orderEntry type="module" module-name="intellij.python.requirements" />
|
||||
<orderEntry type="module" module-name="intellij.toml" />
|
||||
<orderEntry type="module" module-name="intellij.toml.core" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -1,10 +1,14 @@
|
||||
<idea-plugin visibility="public">
|
||||
<dependencies>
|
||||
<module name="intellij.python.psi"/>
|
||||
<module name="intellij.python.sdk"/>
|
||||
<!-- region Generated dependencies - run `Generate Product Layouts` to regenerate -->
|
||||
<plugin id="org.toml.lang"/>
|
||||
<module name="intellij.python.community"/>
|
||||
<module name="intellij.python.community.helpersLocator"/>
|
||||
<module name="intellij.python.psi"/>
|
||||
<module name="intellij.python.requirements"/>
|
||||
<module name="intellij.python.sdk"/>
|
||||
<module name="intellij.toml.core"/>
|
||||
<!-- endregion -->
|
||||
</dependencies>
|
||||
|
||||
<extensionPoints>
|
||||
|
||||
+49
-1
@@ -3,19 +3,25 @@
|
||||
|
||||
package com.jetbrains.python.codeInsight.typing
|
||||
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManager
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.roots.ProjectFileIndex
|
||||
import com.intellij.openapi.util.Key
|
||||
import com.intellij.openapi.vfs.VfsUtilCore
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.psi.PsiDirectory
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.psi.PsiFileSystemItem
|
||||
import com.intellij.psi.PsiManager
|
||||
import com.intellij.psi.util.CachedValueProvider
|
||||
import com.intellij.psi.util.CachedValuesManager
|
||||
import com.intellij.psi.util.QualifiedName
|
||||
import com.jetbrains.python.psi.PyFile
|
||||
import com.jetbrains.python.psi.PyUtil
|
||||
import com.jetbrains.python.psi.resolve.RatedResolveResult
|
||||
import com.jetbrains.python.psi.resolve.isNamespacePackage
|
||||
import com.jetbrains.python.pyi.PyiFile
|
||||
import com.jetbrains.python.pyi.PyiUtil
|
||||
|
||||
@@ -137,6 +143,26 @@ private fun getClassOrContentOrSourceRoot(project: Project, file: VirtualFile):
|
||||
*/
|
||||
fun isInStubPackage(element: PsiElement) = element.getUserData(STUB_PACKAGE_KEY) == true
|
||||
|
||||
internal fun isInPartialStubPackage(element: PsiElement): Boolean {
|
||||
// The enclosing stub package (the element itself, its package directory, or any ancestor up to the root) is explicitly
|
||||
// marked as partial via its `py.typed` marker. Works both for package directories and for plain stub modules
|
||||
// (e.g. `foo-stubs/bar.pyi`), so that submodules of a partial stub package still fall back to the runtime package.
|
||||
val pyTyped = getPyTyped(element) ?: return false
|
||||
return isPartialPyTypedMarker(element.project, pyTyped)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the stub package [element] directly contains a sub-package named [name] that is a namespace package.
|
||||
*
|
||||
* Namespace packages cannot ship complete type information (PEP 561), so submodules resolved underneath them must still
|
||||
* fall back to the runtime package even when the enclosing stub package itself is complete.
|
||||
*/
|
||||
internal fun hasNamespaceSubPackage(element: PsiElement, name: String): Boolean {
|
||||
val packageDir = getPackageDir(element) ?: return false
|
||||
val subPackage = packageDir.findSubdirectory(name) ?: return false
|
||||
return isNamespacePackage(subPackage)
|
||||
}
|
||||
|
||||
/**
|
||||
* See [https://www.python.org/dev/peps/pep-0561/#packaging-type-information].
|
||||
* Value is cached in element's user data.
|
||||
@@ -165,12 +191,34 @@ private fun getPyTyped(element: PsiElement?): VirtualFile? {
|
||||
val root = getClassOrContentOrSourceRoot(element.project, file) ?: return null
|
||||
var current = if (file.isDirectory) file else file.parent
|
||||
|
||||
while (current != null && current != root && current.isDirectory) {
|
||||
while (current != null && current.isDirectory) {
|
||||
val pyTyped = current.findChild("py.typed")
|
||||
if (pyTyped != null && !pyTyped.isDirectory) return pyTyped
|
||||
|
||||
if (current == root) break
|
||||
current = current.parent
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getPackageDir(element: PsiElement): PsiDirectory? {
|
||||
return when (val packageDir = PyUtil.turnInitIntoDir(element)) {
|
||||
is PsiDirectory -> packageDir
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun isPartialPyTypedMarker(project: Project, pyTyped: VirtualFile): Boolean {
|
||||
val psiFile = PsiManager.getInstance(project).findFile(pyTyped) ?: return false
|
||||
return CachedValuesManager.getCachedValue(psiFile) {
|
||||
CachedValueProvider.Result.create(readPyTypedMarker(pyTyped), psiFile)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readPyTypedMarker(pyTyped: VirtualFile): Boolean {
|
||||
return runCatching {
|
||||
val text = FileDocumentManager.getInstance().getCachedDocument(pyTyped)?.text ?: VfsUtilCore.loadText(pyTyped)
|
||||
"partial\n" in text
|
||||
}.getOrDefault(false)
|
||||
}
|
||||
|
||||
@@ -16,14 +16,31 @@
|
||||
package com.jetbrains.python.codeInsight.typing
|
||||
|
||||
import com.intellij.openapi.application.PathManager
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManager
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.util.Key
|
||||
import com.intellij.openapi.vfs.StandardFileSystems
|
||||
import com.intellij.openapi.vfs.VfsUtilCore
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.openapi.vfs.VirtualFileManager
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiFileFactory
|
||||
import com.intellij.psi.PsiFileSystemItem
|
||||
import com.intellij.psi.PsiManager
|
||||
import com.intellij.psi.util.CachedValue
|
||||
import com.intellij.psi.util.CachedValueProvider
|
||||
import com.intellij.psi.util.CachedValuesManager
|
||||
import com.intellij.psi.util.QualifiedName
|
||||
import com.intellij.python.community.helpersLocator.PythonHelpersLocator
|
||||
import com.jetbrains.python.PythonRuntimeService
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
import org.toml.lang.psi.TomlFile
|
||||
import org.toml.lang.psi.TomlFileType
|
||||
import org.toml.lang.psi.TomlKeyValue
|
||||
import org.toml.lang.psi.TomlLiteral
|
||||
import org.toml.lang.psi.ext.TomlLiteralKind
|
||||
import org.toml.lang.psi.ext.kind
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
@@ -33,6 +50,10 @@ import java.io.File
|
||||
*
|
||||
*/
|
||||
object PyTypeShed {
|
||||
private const val METADATA_TOML = "METADATA.toml"
|
||||
private const val PARTIAL_STUB_KEY = "partial_stub"
|
||||
private val PARTIAL_THIRD_PARTY_STUB_PACKAGE_KEY =
|
||||
Key.create<CachedValue<Boolean>>("PyTypeShed.partialThirdPartyStubPackage")
|
||||
|
||||
private val stdlibNamesAvailableOnlyInSubsetOfSupportedLanguageLevels = mapOf(
|
||||
// name to python versions when this name was introduced and removed
|
||||
@@ -230,12 +251,65 @@ object PyTypeShed {
|
||||
fun isInStandardLibrary(file: VirtualFile): Boolean =
|
||||
stdlibRoot?.let { VfsUtilCore.isAncestor(it, file, false) } == true
|
||||
|
||||
fun isInPartialThirdPartyStubPackage(element: PsiElement): Boolean {
|
||||
val file = (element as? PsiFileSystemItem)?.virtualFile ?: element.containingFile?.virtualFile ?: return false
|
||||
val distributionRoot = getThirdPartyDistributionRoot(file) ?: return false
|
||||
return isPartialThirdPartyStubPackage(element.project, distributionRoot)
|
||||
}
|
||||
|
||||
private fun getThirdPartyDistributionRoot(file: VirtualFile): VirtualFile? {
|
||||
val stubsRoot = thirdPartyStubRoot ?: return null
|
||||
if (!VfsUtilCore.isAncestor(stubsRoot, file, false)) return null
|
||||
|
||||
var current = if (file.isDirectory) file else file.parent
|
||||
while (current != null && current.parent != stubsRoot) {
|
||||
current = current.parent
|
||||
}
|
||||
|
||||
return current?.takeIf { it.parent == stubsRoot }
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the directory containing .pyi stubs for the package [packageName] under `typeshed/stubs`.
|
||||
*
|
||||
* [packageName] should match the name of the package on PyPI.
|
||||
*/
|
||||
fun getStubRootForPackage(packageName: String): VirtualFile? {
|
||||
return thirdPartyStubRoot?.findChild(packageName)
|
||||
val stubsRoot = thirdPartyStubRoot ?: return null
|
||||
return stubsRoot.findChild(packageName)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isPartialThirdPartyStubPackage(project: Project, distributionRoot: VirtualFile): Boolean {
|
||||
val psiDirectory = PsiManager.getInstance(project).findDirectory(distributionRoot) ?: return false
|
||||
val metadataFile = distributionRoot.findChild(METADATA_TOML)
|
||||
return CachedValuesManager.getManager(project).getCachedValue(psiDirectory, PARTIAL_THIRD_PARTY_STUB_PACKAGE_KEY, {
|
||||
val metadataPsiFile = metadataFile?.let { PsiManager.getInstance(project).findFile(it) as? TomlFile }
|
||||
val isPartial = when {
|
||||
metadataPsiFile != null -> hasPartialStubMarker(metadataPsiFile)
|
||||
metadataFile != null -> readPartialStubMarker(project, metadataFile)
|
||||
else -> false
|
||||
}
|
||||
CachedValueProvider.Result.create(
|
||||
isPartial,
|
||||
metadataPsiFile ?: metadataFile ?: psiDirectory,
|
||||
VirtualFileManager.VFS_STRUCTURE_MODIFICATIONS,
|
||||
)
|
||||
}, false)
|
||||
}
|
||||
|
||||
private fun readPartialStubMarker(project: Project, metadataFile: VirtualFile): Boolean {
|
||||
val metadataText = FileDocumentManager.getInstance().getCachedDocument(metadataFile)?.text ?: VfsUtilCore.loadText(metadataFile)
|
||||
val psiFile =
|
||||
PsiFileFactory.getInstance(project).createFileFromText(METADATA_TOML, TomlFileType, metadataText) as? TomlFile ?: return false
|
||||
return hasPartialStubMarker(psiFile)
|
||||
}
|
||||
|
||||
private fun hasPartialStubMarker(metadataFile: TomlFile): Boolean {
|
||||
val partialStubEntry = metadataFile.children
|
||||
.filterIsInstance<TomlKeyValue>()
|
||||
.firstOrNull { it.key.text == PARTIAL_STUB_KEY } ?: return false
|
||||
val literal = partialStubEntry.value as? TomlLiteral ?: return false
|
||||
|
||||
return literal.kind is TomlLiteralKind.Boolean && literal.text == "true"
|
||||
}
|
||||
}
|
||||
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.psi.resolve
|
||||
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.editor.EditorFactory
|
||||
import com.intellij.openapi.editor.event.DocumentEvent
|
||||
import com.intellij.openapi.editor.event.DocumentListener
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManager
|
||||
import com.intellij.openapi.module.ModuleManager
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.roots.ProjectFileIndex
|
||||
import com.intellij.openapi.roots.ProjectRootManager
|
||||
import com.intellij.openapi.startup.ProjectActivity
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.openapi.vfs.VirtualFileManager
|
||||
import com.intellij.openapi.vfs.newvfs.BulkFileListener
|
||||
import com.intellij.openapi.vfs.newvfs.events.VFileCopyEvent
|
||||
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
|
||||
import com.intellij.openapi.vfs.newvfs.events.VFilePropertyChangeEvent
|
||||
import com.intellij.util.asDisposable
|
||||
import com.jetbrains.python.sdk.legacy.PythonSdkUtil
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
@Service(Service.Level.PROJECT)
|
||||
class PyPartialStubMarkersInvalidator(
|
||||
private val project: Project,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
) {
|
||||
fun subscribe() {
|
||||
val invalidationRequests = MutableSharedFlow<Unit>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
val parentDisposable = coroutineScope.asDisposable()
|
||||
|
||||
coroutineScope.launch {
|
||||
invalidationRequests
|
||||
.debounce(INVALIDATION_DELAY)
|
||||
.collectLatest {
|
||||
clearPathCaches(project)
|
||||
}
|
||||
}
|
||||
|
||||
project.messageBus.connect(parentDisposable).subscribe(VirtualFileManager.VFS_CHANGES, object : BulkFileListener {
|
||||
override fun after(events: MutableList<out VFileEvent>) {
|
||||
if (events.any { affectsPartialStubMarkers(project, it) }) {
|
||||
invalidationRequests.tryEmit(Unit)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
EditorFactory.getInstance().eventMulticaster.addDocumentListener(object : DocumentListener {
|
||||
override fun documentChanged(event: DocumentEvent) {
|
||||
val file = FileDocumentManager.getInstance().getFile(event.document) ?: return
|
||||
if (isProjectPartialStubMarkerFile(project, file)) {
|
||||
invalidationRequests.tryEmit(Unit)
|
||||
}
|
||||
}
|
||||
}, parentDisposable)
|
||||
}
|
||||
|
||||
private fun clearPathCaches(project: Project) {
|
||||
if (project.isDisposed) return
|
||||
val sdks = linkedSetOf<Sdk>()
|
||||
|
||||
for (module in ModuleManager.getInstance(project).modules) {
|
||||
PythonModulePathCache.getInstance(module).clearCache()
|
||||
PythonSdkUtil.findPythonSdk(module)?.let(sdks::add)
|
||||
}
|
||||
|
||||
ProjectRootManager.getInstance(project).projectSdk
|
||||
?.takeIf(PythonSdkUtil::isPythonSdk)
|
||||
?.let(sdks::add)
|
||||
|
||||
for (sdk in sdks) {
|
||||
PythonSdkPathCache.getInstance(project, sdk).clearCache()
|
||||
}
|
||||
}
|
||||
|
||||
private fun isPartialStubMarkerFileName(fileName: String?): Boolean {
|
||||
return fileName == "py.typed"
|
||||
}
|
||||
|
||||
private fun affectsPartialStubMarkers(project: Project, event: VFileEvent): Boolean {
|
||||
if (event.file?.let { isProjectPartialStubMarkerFile(project, it) } == true) {
|
||||
return true
|
||||
}
|
||||
|
||||
return when (event) {
|
||||
is VFileCopyEvent ->
|
||||
isPartialStubMarkerFileName(event.newChildName) &&
|
||||
isProjectRelevantLocation(project, event.newParent)
|
||||
is VFilePropertyChangeEvent ->
|
||||
event.isRename &&
|
||||
(isPartialStubMarkerFileName(event.oldValue as? String) || isPartialStubMarkerFileName(event.newValue as? String)) &&
|
||||
event.file.parent?.let { isProjectRelevantLocation(project, it) } == true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun isProjectPartialStubMarkerFile(project: Project, file: VirtualFile): Boolean {
|
||||
return isPartialStubMarkerFileName(file.name) && isProjectRelevantLocation(project, file)
|
||||
}
|
||||
|
||||
private fun isProjectRelevantLocation(project: Project, file: VirtualFile): Boolean {
|
||||
if (!project.isInitialized || !file.isValid) {
|
||||
return false
|
||||
}
|
||||
|
||||
val index = ProjectFileIndex.getInstance(project)
|
||||
return index.isInProject(file)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val INVALIDATION_DELAY = 300.milliseconds
|
||||
}
|
||||
}
|
||||
|
||||
internal class PyPartialStubMarkersInvalidatorActivity : ProjectActivity {
|
||||
override suspend fun execute(project: Project) {
|
||||
project.service<PyPartialStubMarkersInvalidator>().subscribe()
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,9 @@ import com.intellij.psi.util.QualifiedName
|
||||
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
|
||||
import com.intellij.util.concurrency.annotations.RequiresReadLock
|
||||
import com.jetbrains.python.codeInsight.typing.PyTypeShed
|
||||
import com.jetbrains.python.codeInsight.typing.hasNamespaceSubPackage
|
||||
import com.jetbrains.python.codeInsight.typing.isInInlinePackage
|
||||
import com.jetbrains.python.codeInsight.typing.isInPartialStubPackage
|
||||
import com.jetbrains.python.codeInsight.typing.isInStubPackage
|
||||
import com.jetbrains.python.facet.PythonPathContributingFacet
|
||||
import com.jetbrains.python.module.PyModuleService
|
||||
@@ -113,7 +115,7 @@ private fun resolveModuleFromRoots(name: QualifiedName, context: PyQualifiedName
|
||||
val head = name.removeTail(name.componentCount - 1)
|
||||
val nameNoHead = name.removeHead(1)
|
||||
return nameNoHead.components.fold(resultsFromRoots(head, context).distinct()) { results, component ->
|
||||
filterTopPriorityResults(results, context.module)
|
||||
filterTopPriorityResults(results, context.module, component)
|
||||
.asSequence()
|
||||
.filterIsInstance<PsiFileSystemItem>()
|
||||
.flatMap { resolveModuleAt(QualifiedName.fromComponents(component), it, context).asSequence() }
|
||||
@@ -408,7 +410,7 @@ private fun filterTopPriorityResultsWithFallback(
|
||||
* Filters resolved elements according to their import priority in sys.path and
|
||||
* [PEP 561](https://www.python.org/dev/peps/pep-0561/#type-checker-module-resolution-order) rules.
|
||||
*/
|
||||
private fun filterTopPriorityResults(resolved: List<PsiElement>, module: Module?): List<PsiElement> {
|
||||
private fun filterTopPriorityResults(resolved: List<PsiElement>, module: Module?, nextComponent: String? = null): List<PsiElement> {
|
||||
if (resolved.isEmpty()) return emptyList()
|
||||
|
||||
val groupedResults = resolved.groupByTo(sortedMapOf<Priority, MutableList<PsiElement>>()) { resolvedElementPriority(it, module) }
|
||||
@@ -417,12 +419,12 @@ private fun filterTopPriorityResults(resolved: List<PsiElement>, module: Module?
|
||||
if (groupedResults.topResultIs(Priority.NAMESPACE_PACKAGE)) return groupedResults[Priority.NAMESPACE_PACKAGE]!! + skeletons
|
||||
groupedResults.remove(Priority.NAMESPACE_PACKAGE)
|
||||
|
||||
val topPriority = groupedResults.keys.firstOrNull()
|
||||
val topResult = topPriority?.let { groupedResults[it]?.firstOrNull() }
|
||||
val priorityResults = when {
|
||||
groupedResults.isEmpty() -> emptyList()
|
||||
// stub packages can be partial
|
||||
groupedResults.topResultIs(Priority.STUB_PACKAGE) -> firstResultWithFallback(groupedResults, Priority.STUB_PACKAGE)
|
||||
// third party sdk should not overwrite packages from the same vendor
|
||||
groupedResults.topResultIs(Priority.THIRD_PARTY_SDK) -> firstResultWithFallback(groupedResults, Priority.THIRD_PARTY_SDK)
|
||||
topPriority != null && topResult != null && shouldFallbackToNextPriority(topPriority, topResult, module, nextComponent) ->
|
||||
firstResultWithFallback(groupedResults, topPriority)
|
||||
else -> listOf(groupedResults.values.first().first())
|
||||
}
|
||||
return priorityResults + skeletons
|
||||
@@ -439,6 +441,25 @@ private fun firstResultWithFallback(results: SortedMap<Priority, MutableList<Psi
|
||||
return listOfNotNull(first, nextByPriority)
|
||||
}
|
||||
|
||||
private fun shouldFallbackToNextPriority(priority: Priority, element: PsiElement, module: Module?, nextComponent: String?): Boolean {
|
||||
return when (priority) {
|
||||
Priority.USER_STUB -> isUserFile(element, module) && isInPartialStub(element, nextComponent)
|
||||
Priority.STUB_PACKAGE -> isInPartialStub(element, nextComponent)
|
||||
Priority.TYPESHED -> PyTypeShed.isInPartialThirdPartyStubPackage(element)
|
||||
Priority.THIRD_PARTY_SDK -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A stub package result must keep the runtime package as a fallback when the stub does not authoritatively cover the
|
||||
* path being resolved: either the enclosing stub package is marked partial, or the next component to resolve is a
|
||||
* namespace package inside the stub (namespace packages are inherently incomplete, see PEP 561).
|
||||
*/
|
||||
private fun isInPartialStub(element: PsiElement, nextComponent: String?): Boolean {
|
||||
return isInPartialStubPackage(element) || (nextComponent != null && hasNamespaceSubPackage(element, nextComponent))
|
||||
}
|
||||
|
||||
/**
|
||||
* See [https://www.python.org/dev/peps/pep-0561/#type-checker-module-resolution-order].
|
||||
*/
|
||||
@@ -491,5 +512,3 @@ private enum class Priority {
|
||||
NAMESPACE_PACKAGE, // namespace package but may contain several entries in resolve result
|
||||
SKELETON // generated skeletons have lowest priority but are always included in the resolve result as a fallback
|
||||
}
|
||||
|
||||
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
from mypackage.module2 import d
|
||||
|
||||
print(d)
|
||||
+1
@@ -0,0 +1 @@
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
b: int
|
||||
+1
@@ -0,0 +1 @@
|
||||
partial
|
||||
+1
@@ -0,0 +1 @@
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
d = ["hello"]
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
b = 1
|
||||
c = ""
|
||||
+1
@@ -0,0 +1 @@
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
from mypackage.module2 import d
|
||||
|
||||
print(d)
|
||||
+1
@@ -0,0 +1 @@
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
b: int
|
||||
+1
@@ -0,0 +1 @@
|
||||
partial
|
||||
+1
@@ -0,0 +1 @@
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
d = ["hello"]
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
b = 1
|
||||
c = ""
|
||||
+1
@@ -0,0 +1 @@
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
from mypackage.module2 import d
|
||||
|
||||
print(d)
|
||||
+1
@@ -0,0 +1 @@
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
b: int
|
||||
+1
@@ -0,0 +1 @@
|
||||
partial
|
||||
+1
@@ -0,0 +1 @@
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
d = ["hello"]
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
b = 1
|
||||
c = ""
|
||||
@@ -0,0 +1 @@
|
||||
# Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
@@ -0,0 +1 @@
|
||||
# Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
@@ -0,0 +1,4 @@
|
||||
# Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
|
||||
def bar(a: str, b: str):
|
||||
return a + b
|
||||
@@ -0,0 +1,6 @@
|
||||
# Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
|
||||
from pkg import foo
|
||||
|
||||
foo.bar("a", "b")
|
||||
# <ref>
|
||||
+1
@@ -0,0 +1 @@
|
||||
# Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
# Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
|
||||
def foo():
|
||||
return 42
|
||||
@@ -0,0 +1,6 @@
|
||||
# Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
|
||||
from synthetic_complete_typeshed import shortcuts
|
||||
|
||||
shortcuts.foo()
|
||||
# <ref>
|
||||
@@ -0,0 +1 @@
|
||||
# Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
@@ -0,0 +1,4 @@
|
||||
# Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
|
||||
def bar(a, b):
|
||||
return a + b
|
||||
@@ -0,0 +1,6 @@
|
||||
# Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
|
||||
from pkg import foo
|
||||
|
||||
foo.bar("a", "b")
|
||||
# <ref>
|
||||
@@ -0,0 +1 @@
|
||||
# Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
def foo():
|
||||
return 42
|
||||
@@ -0,0 +1,4 @@
|
||||
from synthetic_false_partial_typeshed import shortcuts
|
||||
|
||||
shortcuts.foo()
|
||||
# <ref>
|
||||
@@ -0,0 +1 @@
|
||||
not-partial-no-no
|
||||
@@ -0,0 +1,2 @@
|
||||
def bar(a, b):
|
||||
return a + b
|
||||
@@ -0,0 +1,4 @@
|
||||
from pkg import foo
|
||||
|
||||
foo.bar("a", "b")
|
||||
# <ref>
|
||||
+1
@@ -0,0 +1 @@
|
||||
def declared(a: str, b: str) -> str: ...
|
||||
+1
@@ -0,0 +1 @@
|
||||
partial
|
||||
@@ -0,0 +1,6 @@
|
||||
def declared(a, b):
|
||||
return a + b
|
||||
|
||||
|
||||
def runtime_only(a, b):
|
||||
return a + b
|
||||
@@ -0,0 +1,4 @@
|
||||
from pkg import foo
|
||||
|
||||
foo.runtime_only("a", "b")
|
||||
# <ref>
|
||||
@@ -0,0 +1 @@
|
||||
pass
|
||||
@@ -0,0 +1 @@
|
||||
def stub_only() -> None: ...
|
||||
@@ -0,0 +1 @@
|
||||
pass
|
||||
@@ -0,0 +1,2 @@
|
||||
def bar(a, b):
|
||||
return a + b
|
||||
@@ -0,0 +1,4 @@
|
||||
from pkg.nspkg import foo
|
||||
|
||||
foo.bar("a", "b")
|
||||
# <ref>
|
||||
@@ -0,0 +1,4 @@
|
||||
from mypackage.package2 import mymodule
|
||||
|
||||
mymodule.bar("a", "b")
|
||||
# <ref>
|
||||
@@ -0,0 +1,2 @@
|
||||
def bar(a, b):
|
||||
return a + b
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
def bar(a, b):
|
||||
return a + b
|
||||
+1
@@ -0,0 +1 @@
|
||||
partial
|
||||
@@ -0,0 +1 @@
|
||||
no
|
||||
+1
@@ -0,0 +1 @@
|
||||
partial
|
||||
@@ -0,0 +1,2 @@
|
||||
def bar(a, b):
|
||||
return a + b
|
||||
@@ -0,0 +1,4 @@
|
||||
from pkg.foo import bar
|
||||
|
||||
bar("a", "b")
|
||||
# <ref>
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
def foo():
|
||||
return 42
|
||||
@@ -0,0 +1,4 @@
|
||||
from synthetic_partial_typeshed import shortcuts
|
||||
|
||||
shortcuts.foo()
|
||||
# <ref>
|
||||
@@ -0,0 +1,2 @@
|
||||
def bar(a, b):
|
||||
return a + b
|
||||
@@ -0,0 +1,4 @@
|
||||
from pkg import foo
|
||||
|
||||
foo.bar("a", "b")
|
||||
# <ref>
|
||||
@@ -0,0 +1 @@
|
||||
partial
|
||||
@@ -0,0 +1,4 @@
|
||||
from mypackage import mymodule
|
||||
|
||||
mymodule.bar("a", "b")
|
||||
# <ref>
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
def bar(a, b):
|
||||
return a + b
|
||||
+1
@@ -0,0 +1 @@
|
||||
partial
|
||||
@@ -0,0 +1,2 @@
|
||||
version = "1.0.*"
|
||||
upstream_repository = "https://example.invalid/pycharm-test-complete-typeshed"
|
||||
+1
@@ -0,0 +1 @@
|
||||
def existing() -> int: ...
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version = "1.0.*"
|
||||
upstream_repository = "https://example.invalid/pycharm-test-false-partial-typeshed"
|
||||
partial_stub = false
|
||||
@@ -0,0 +1,3 @@
|
||||
version = "1.0.*"
|
||||
upstream_repository = "https://example.invalid/pycharm-test-partial-typeshed"
|
||||
partial_stub = true
|
||||
+1
@@ -0,0 +1 @@
|
||||
def existing() -> int: ...
|
||||
@@ -1,14 +1,22 @@
|
||||
// Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
package com.jetbrains.python;
|
||||
|
||||
import com.intellij.openapi.application.WriteAction;
|
||||
import com.intellij.openapi.editor.Document;
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManager;
|
||||
import com.intellij.openapi.module.Module;
|
||||
import com.intellij.openapi.util.RecursionManager;
|
||||
import com.intellij.openapi.util.StackOverflowPreventedException;
|
||||
import com.intellij.openapi.vfs.VfsUtil;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.psi.PsiElement;
|
||||
import com.intellij.psi.PsiFile;
|
||||
import com.intellij.psi.PsiPolyVariantReference;
|
||||
import com.intellij.psi.PsiReference;
|
||||
import com.intellij.psi.ResolveResult;
|
||||
import com.intellij.psi.util.PsiTreeUtil;
|
||||
import com.intellij.testFramework.PlatformTestUtil;
|
||||
import com.intellij.testFramework.PsiTestUtil;
|
||||
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil;
|
||||
import com.jetbrains.python.fixtures.PyResolveTestCase;
|
||||
import com.jetbrains.python.fixtures.PyTestCase;
|
||||
@@ -24,6 +32,8 @@ import com.jetbrains.python.psi.impl.PyPsiUtils;
|
||||
import com.jetbrains.python.psi.types.TypeEvalContext;
|
||||
import com.jetbrains.python.pyi.PyiUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
||||
public class Py3ResolveTest extends PyResolveTestCase {
|
||||
|
||||
@@ -732,9 +742,23 @@ public class Py3ResolveTest extends PyResolveTestCase {
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: this should be fixed after introducing an ability to check visited paths while resolving some qualified name
|
||||
// PY-30942
|
||||
public void _testNoInlinePackageInsteadStubPackage() {
|
||||
// PY-55589
|
||||
public void testPyInsteadPartialStubPackageAnotherImport() {
|
||||
final String path = "resolve/" + getTestName(false);
|
||||
myFixture.configureByFile(path + "/main.py");
|
||||
|
||||
runWithAdditionalClassEntryInSdkRoots(
|
||||
path + "/lib",
|
||||
() -> {
|
||||
final PsiElement element = PyResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve();
|
||||
assertInstanceOf(element, PyFunction.class);
|
||||
assertEquals("foo.py", element.getContainingFile().getName());
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// PY-55589
|
||||
public void testNoRuntimeMembersInsteadPartialStubPackageModule() {
|
||||
final String path = "resolve/" + getTestName(false);
|
||||
myFixture.configureByFile(path + "/main.py");
|
||||
|
||||
@@ -744,9 +768,8 @@ public class Py3ResolveTest extends PyResolveTestCase {
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: this should be fixed after introducing an ability to check visited paths while resolving some qualified name
|
||||
// PY-30942
|
||||
public void _testNoInlinePackageInsteadStubPackageAnotherImport() {
|
||||
public void testNoInlinePackageInsteadStubPackage() {
|
||||
final String path = "resolve/" + getTestName(false);
|
||||
myFixture.configureByFile(path + "/main.py");
|
||||
|
||||
@@ -756,6 +779,212 @@ public class Py3ResolveTest extends PyResolveTestCase {
|
||||
);
|
||||
}
|
||||
|
||||
// PY-30942
|
||||
public void testNoInlinePackageInsteadStubPackageAnotherImport() {
|
||||
final String path = "resolve/" + getTestName(false);
|
||||
myFixture.configureByFile(path + "/main.py");
|
||||
|
||||
runWithAdditionalClassEntryInSdkRoots(
|
||||
path + "/lib",
|
||||
() -> assertNull(PyResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve())
|
||||
);
|
||||
}
|
||||
|
||||
// PY-55589
|
||||
public void testNoPyInsteadNonPartialStubPackageMarker() {
|
||||
final String path = "resolve/" + getTestName(false);
|
||||
myFixture.configureByFile(path + "/main.py");
|
||||
|
||||
runWithAdditionalClassEntryInSdkRoots(
|
||||
path + "/lib",
|
||||
() -> assertNull(PyResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve())
|
||||
);
|
||||
}
|
||||
|
||||
// PY-55589
|
||||
public void testPyInsteadNamespaceStubPackage() {
|
||||
final String path = "resolve/" + getTestName(false);
|
||||
myFixture.configureByFile(path + "/main.py");
|
||||
|
||||
runWithAdditionalClassEntryInSdkRoots(
|
||||
path + "/lib",
|
||||
() -> {
|
||||
final PsiElement element = PyResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve();
|
||||
assertInstanceOf(element, PyFunction.class);
|
||||
assertEquals("foo.py", element.getContainingFile().getName());
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// PY-55589
|
||||
public void testPyInsteadPartialUserStubPackage() {
|
||||
final String path = "resolve/" + getTestName(false);
|
||||
myFixture.copyDirectoryToProject(path + "/project", "");
|
||||
myFixture.configureByFile("main.py");
|
||||
|
||||
runWithAdditionalClassEntryInSdkRoots(
|
||||
path + "/lib",
|
||||
() -> {
|
||||
final PsiElement element = PyResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve();
|
||||
assertInstanceOf(element, PyFunction.class);
|
||||
assertEquals("foo.py", element.getContainingFile().getName());
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// PY-55589
|
||||
public void testPyInsteadPartialUserStubPackageSourceRoot() {
|
||||
final String path = "resolve/" + getTestName(false);
|
||||
myFixture.copyDirectoryToProject(path, "");
|
||||
myFixture.configureByFile("main.py");
|
||||
|
||||
final Module module = myFixture.getModule();
|
||||
final VirtualFile sourceRoot = myFixture.findFileInTempDir("stubs");
|
||||
assertNotNull(sourceRoot);
|
||||
|
||||
PsiTestUtil.addSourceRoot(module, sourceRoot);
|
||||
try {
|
||||
final PsiElement element = PyResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve();
|
||||
assertInstanceOf(element, PyFunction.class);
|
||||
assertEquals("mymodule.py", element.getContainingFile().getName());
|
||||
}
|
||||
finally {
|
||||
PsiTestUtil.removeSourceRoot(module, sourceRoot);
|
||||
}
|
||||
}
|
||||
|
||||
// PY-55589
|
||||
public void testPyInsteadNestedPartialUserStubPackage() {
|
||||
final String path = "resolve/" + getTestName(false);
|
||||
myFixture.copyDirectoryToProject(path, "");
|
||||
myFixture.configureByFile("main.py");
|
||||
|
||||
final Module module = myFixture.getModule();
|
||||
final VirtualFile sourceRoot = myFixture.findFileInTempDir("stubs");
|
||||
assertNotNull(sourceRoot);
|
||||
|
||||
PsiTestUtil.addSourceRoot(module, sourceRoot);
|
||||
try {
|
||||
final PsiElement element = PyResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve();
|
||||
assertInstanceOf(element, PyFunction.class);
|
||||
assertEquals("mymodule.py", element.getContainingFile().getName());
|
||||
}
|
||||
finally {
|
||||
PsiTestUtil.removeSourceRoot(module, sourceRoot);
|
||||
}
|
||||
}
|
||||
|
||||
// PY-55589
|
||||
public void testEditingPyTypedInvalidatesPartialUserStubPackageResolveCache() {
|
||||
final String path = "resolve/PyInsteadPartialUserStubPackage";
|
||||
myFixture.copyDirectoryToProject(path + "/project", "");
|
||||
myFixture.configureByFile("main.py");
|
||||
|
||||
runWithAdditionalClassEntryInSdkRoots(
|
||||
path + "/lib",
|
||||
() -> {
|
||||
final PsiElement initial = PyResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve();
|
||||
assertInstanceOf(initial, PyFunction.class);
|
||||
assertEquals("foo.py", initial.getContainingFile().getName());
|
||||
|
||||
final VirtualFile pyTyped = myFixture.findFileInTempDir("pkg/py.typed");
|
||||
assertNotNull(pyTyped);
|
||||
|
||||
WriteAction.run(() -> {
|
||||
try {
|
||||
VfsUtil.saveText(pyTyped, "");
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
PlatformTestUtil.waitForAlarm(300);
|
||||
|
||||
assertNull(PyResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve());
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// PY-55589
|
||||
public void testEditingUnsavedPyTypedInvalidatesPartialUserStubPackageResolveCache() {
|
||||
final String path = "resolve/PyInsteadPartialUserStubPackage";
|
||||
myFixture.copyDirectoryToProject(path + "/project", "");
|
||||
myFixture.configureByFile("main.py");
|
||||
|
||||
runWithAdditionalClassEntryInSdkRoots(
|
||||
path + "/lib",
|
||||
() -> {
|
||||
final PsiElement initial = PyResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve();
|
||||
assertInstanceOf(initial, PyFunction.class);
|
||||
assertEquals("foo.py", initial.getContainingFile().getName());
|
||||
|
||||
final VirtualFile pyTyped = myFixture.findFileInTempDir("pkg/py.typed");
|
||||
assertNotNull(pyTyped);
|
||||
|
||||
final Document pyTypedDocument = FileDocumentManager.getInstance().getDocument(pyTyped);
|
||||
assertNotNull(pyTypedDocument);
|
||||
|
||||
WriteAction.run(() -> pyTypedDocument.setText(""));
|
||||
assertTrue(FileDocumentManager.getInstance().isDocumentUnsaved(pyTypedDocument));
|
||||
PlatformTestUtil.waitForAlarm(300);
|
||||
|
||||
assertNull(PyResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve());
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// PY-55589
|
||||
public void testNoPyInsteadCompleteUserStubPackage() {
|
||||
final String path = "resolve/" + getTestName(false);
|
||||
myFixture.copyDirectoryToProject(path + "/project", "");
|
||||
myFixture.configureByFile("main.py");
|
||||
|
||||
runWithAdditionalClassEntryInSdkRoots(
|
||||
path + "/lib",
|
||||
() -> assertNull(PyResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve())
|
||||
);
|
||||
}
|
||||
|
||||
// PY-65967
|
||||
public void testPyInsteadPartialTypeShed() throws IOException {
|
||||
final String path = "resolve/" + getTestName(false);
|
||||
myFixture.configureByFile(path + "/main.py");
|
||||
enableTestDataTypeshedStubsForPackages("pycharm-test-partial-typeshed");
|
||||
|
||||
runWithAdditionalClassEntryInSdkRoots(
|
||||
path + "/lib",
|
||||
() -> {
|
||||
final PsiElement element = PyResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve();
|
||||
assertInstanceOf(element, PyFunction.class);
|
||||
assertEquals("shortcuts.py", element.getContainingFile().getName());
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// PY-65967
|
||||
public void testNoPyInsteadCompleteTypeShed() throws IOException {
|
||||
final String path = "resolve/" + getTestName(false);
|
||||
myFixture.configureByFile(path + "/main.py");
|
||||
enableTestDataTypeshedStubsForPackages("pycharm-test-complete-typeshed");
|
||||
|
||||
runWithAdditionalClassEntryInSdkRoots(
|
||||
path + "/lib",
|
||||
() -> assertNull(PyResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve())
|
||||
);
|
||||
}
|
||||
|
||||
// PY-65967
|
||||
public void testNoPyInsteadFalsePartialTypeShed() throws IOException {
|
||||
final String path = "resolve/" + getTestName(false);
|
||||
myFixture.configureByFile(path + "/main.py");
|
||||
enableTestDataTypeshedStubsForPackages("pycharm-test-false-partial-typeshed");
|
||||
|
||||
runWithAdditionalClassEntryInSdkRoots(
|
||||
path + "/lib",
|
||||
() -> assertNull(PyResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve())
|
||||
);
|
||||
}
|
||||
|
||||
// PY-31354
|
||||
public void testStubPackageInOtherRoot() {
|
||||
final String path = "resolve/" + getTestName(false);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user