[Expect/Actual] Support matching of expect/actual in hierarchical MPP

This commit is contained in:
Dmitry Savvinov
2019-04-12 13:23:58 +03:00
parent 4e12701145
commit 29beed9b21
12 changed files with 247 additions and 68 deletions
@@ -39,3 +39,6 @@ interface ModuleInfo {
val Capability = ModuleDescriptor.Capability<ModuleInfo>("ModuleInfo")
}
}
val ModuleDescriptor.moduleInfo: ModuleInfo?
get() = getCapability(ModuleInfo.Capability)
@@ -280,6 +280,7 @@ interface PlatformAnalysisParameters {
interface CombinedModuleInfo : ModuleInfo {
val containedModules: List<ModuleInfo>
val platformModule: ModuleInfo
}
fun ModuleInfo.flatten(): List<ModuleInfo> = when (this) {
@@ -287,6 +288,8 @@ fun ModuleInfo.flatten(): List<ModuleInfo> = when (this) {
else -> listOf(this)
}
fun ModuleInfo.unwrapPlatform(): ModuleInfo = if (this is CombinedModuleInfo) platformModule else this
interface TrackableModuleInfo : ModuleInfo {
fun createModificationTracker(): ModificationTracker
}
@@ -11,6 +11,7 @@ import com.intellij.psi.PsiElement;
import com.intellij.psi.impl.source.tree.LeafPsiElement;
import kotlin.Pair;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.kotlin.analyzer.ModuleInfo;
import org.jetbrains.kotlin.cfg.WhenMissingCase;
import org.jetbrains.kotlin.config.LanguageFeature;
import org.jetbrains.kotlin.config.LanguageVersion;
@@ -645,6 +646,10 @@ public interface Errors {
DiagnosticFactory2<KtNamedDeclaration, MemberDescriptor,
Map<Incompatible, Collection<MemberDescriptor>>> ACTUAL_WITHOUT_EXPECT =
DiagnosticFactory2.create(ERROR, INCOMPATIBLE_DECLARATION);
DiagnosticFactory2<KtNamedDeclaration, DeclarationDescriptor, Collection<String>> AMBIGUOUS_ACTUALS =
DiagnosticFactory2.create(ERROR, INCOMPATIBLE_DECLARATION);
DiagnosticFactory2<KtNamedDeclaration, DeclarationDescriptor, Collection<String>> AMBIGUOUS_EXPECTS =
DiagnosticFactory2.create(ERROR, INCOMPATIBLE_DECLARATION);
DiagnosticFactory2<KtNamedDeclaration, ClassDescriptor,
List<Pair<MemberDescriptor, Map<Incompatible, Collection<MemberDescriptor>>>>> NO_ACTUAL_CLASS_MEMBER_FOR_EXPECTED_CLASS =
@@ -280,10 +280,14 @@ public class DefaultErrorMessages {
"Expected function source is not found, therefore it's impossible to generate default argument values declared there. " +
"Please add the corresponding file to compilation sources");
MAP.put(NO_ACTUAL_FOR_EXPECT, "Expected {0} has no actual declaration in module{1}{2}", DECLARATION_NAME_WITH_KIND,
PLATFORM, PlatformIncompatibilityDiagnosticRenderer.TEXT);
MAP.put(NO_ACTUAL_FOR_EXPECT, "Expected {0} has no actual declaration in module {1}{2}", DECLARATION_NAME_WITH_KIND,
MODULE_WITH_PLATFORM, PlatformIncompatibilityDiagnosticRenderer.TEXT);
MAP.put(ACTUAL_WITHOUT_EXPECT, "{0} has no corresponding expected declaration{1}", CAPITALIZED_DECLARATION_NAME_WITH_KIND_AND_PLATFORM,
PlatformIncompatibilityDiagnosticRenderer.TEXT);
MAP.put(AMBIGUOUS_ACTUALS, "{0} has several compatible actual declarations in modules {1}", CAPITALIZED_DECLARATION_NAME_WITH_KIND_AND_PLATFORM, commaSeparated(
STRING));
MAP.put(AMBIGUOUS_EXPECTS, "{0} has several compatible expect declarations in modules {1}", CAPITALIZED_DECLARATION_NAME_WITH_KIND_AND_PLATFORM, commaSeparated(
STRING));
MAP.put(NO_ACTUAL_CLASS_MEMBER_FOR_EXPECTED_CLASS, "Actual class ''{0}'' has no corresponding members for expected class members:{1}",
NAME, IncompatibleExpectedActualClassScopesRenderer.TEXT);
@@ -21,6 +21,8 @@ import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.analyzer.ModuleInfo
import org.jetbrains.kotlin.analyzer.moduleInfo
import org.jetbrains.kotlin.analyzer.unwrapPlatform
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.cfg.WhenMissingCase
import org.jetbrains.kotlin.cfg.hasUnknown
@@ -85,14 +87,14 @@ object Renderers {
val NAME = Renderer<Named> { it.name.asString() }
@JvmField
val PLATFORM = Renderer<ModuleDescriptor> {
val platform = it.platform
" ${it.getCapability(ModuleInfo.Capability)?.displayedName ?: ""}" + when {
platform == null || platform.isCommon() -> ""
else -> " for " + platform.single().platformName
}
}
val MODULE_WITH_PLATFORM = Renderer<ModuleDescriptor> { module ->
val platform = module.platform
val moduleName = module.moduleInfo?.unwrapPlatform()?.displayedName ?: ""
val platformNameIfAny = if (platform == null || platform.isCommon()) "" else " for " + platform.single().platformName
moduleName + platformNameIfAny
}
@JvmField
val VISIBILITY = Renderer<Visibility> {
if (it == Visibilities.INVISIBLE_FAKE)
@@ -18,6 +18,8 @@ package org.jetbrains.kotlin.resolve.checkers
import com.intellij.openapi.vfs.VfsUtilCore
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.analyzer.CombinedModuleInfo
import org.jetbrains.kotlin.analyzer.ModuleInfo
import org.jetbrains.kotlin.config.AnalysisFlags
import org.jetbrains.kotlin.config.LanguageFeature
import org.jetbrains.kotlin.descriptors.*
@@ -35,13 +37,17 @@ import org.jetbrains.kotlin.resolve.multiplatform.ExpectedActualResolver
import org.jetbrains.kotlin.resolve.multiplatform.ExpectedActualResolver.Compatibility
import org.jetbrains.kotlin.resolve.multiplatform.ExpectedActualResolver.Compatibility.Compatible
import org.jetbrains.kotlin.resolve.multiplatform.ExpectedActualResolver.Compatibility.Incompatible
import org.jetbrains.kotlin.resolve.multiplatform.ModuleFilter
import org.jetbrains.kotlin.resolve.source.PsiSourceFile
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
import org.jetbrains.kotlin.utils.ifEmpty
import java.io.File
class ExpectedActualDeclarationChecker(val argumentExtractors: Iterable<ActualAnnotationArgumentExtractor>) : DeclarationChecker {
class ExpectedActualDeclarationChecker(
val moduleStructureOracle: ModuleStructureOracle,
val argumentExtractors: Iterable<ActualAnnotationArgumentExtractor>
) : DeclarationChecker {
interface ActualAnnotationArgumentExtractor {
fun extractDefaultValue(parameter: ValueParameterDescriptor, expectedType: KotlinType): ConstantValue<*>?
}
@@ -56,44 +62,126 @@ class ExpectedActualDeclarationChecker(val argumentExtractors: Iterable<ActualAn
if (descriptor !is MemberDescriptor || DescriptorUtils.isEnumEntry(descriptor)) return
if (descriptor.isExpect) {
checkExpectedDeclarationHasActual(declaration, descriptor, context.trace, descriptor.module, context.expectActualTracker)
checkExpectedDeclarationHasProperActuals(declaration, descriptor, context.trace, context.expectActualTracker)
} else {
val checkActual = !context.languageVersionSettings.getFlag(AnalysisFlags.multiPlatformDoNotCheckActual)
checkActualDeclarationHasExpected(declaration, descriptor, context.trace, checkActual)
val allImplementedModules = moduleStructureOracle.findAllDependsOnPaths(descriptor.module).flatMap { it.nodes }.toHashSet()
checkActualDeclarationHasExpected(
declaration,
descriptor,
context.trace,
checkActual,
moduleVisibilityFilter = { it in allImplementedModules }
)
}
}
fun checkExpectedDeclarationHasActual(
private fun checkExpectedDeclarationHasProperActuals(
reportOn: KtNamedDeclaration,
descriptor: MemberDescriptor,
trace: BindingTrace,
platformModule: ModuleDescriptor,
expectActualTracker: ExpectActualTracker
) {
val allActualizationPaths = moduleStructureOracle.findAllReversedDependsOnPaths(descriptor.module)
val leafModuleToVisibleModules = allActualizationPaths.groupBy { it.nodes.last() }
.mapValues { it.value.flatMap { it.nodes }.toSet() }
for ((leafModule, modulesVisibleFromLeaf) in leafModuleToVisibleModules) {
checkExpectedDeclarationHasAtLeastOneActual(
reportOn,
descriptor,
trace,
leafModule,
expectActualTracker,
moduleVisibilityFilter = { it in modulesVisibleFromLeaf }
)
}
/*
Note that we have to check for 'duplicate actuals' separately, considering paths
instead of only leaf-module, because currently we have to distinguish the following
two cases:
expect class A expect class A
/ \ / \
/ \ / \
actual class A actual class A actual class A (no actual)
\ / \ /
\ / \ /
(warning or nothing, TBD) actual class A
(ERROR: duplicate actuals)
If we merge behaviour (e.g. decide to report ERROR for first case too)
for those two cases, we can drop separate logic for DUPLICATE_ACTUALS
*/
for (path in allActualizationPaths) {
val modulesOnThisPath = path.nodes.toSet()
checkExpectedDeclarationHasAtMostOneActual(
reportOn, descriptor, trace, path, moduleVisibilityFilter = { it in modulesOnThisPath }
)
}
}
private fun checkExpectedDeclarationHasAtMostOneActual(
reportOn: KtNamedDeclaration,
descriptor: MemberDescriptor,
trace: BindingTrace,
path: ModulePath,
moduleVisibilityFilter: ModuleFilter
) {
val compatibility = path.nodes
.mapNotNull { ExpectedActualResolver.findActualForExpected(descriptor, it, moduleVisibilityFilter) }
.ifEmpty { return }
.fold(LinkedHashMap<Compatibility, List<MemberDescriptor>>()) { resultMap, partialMap ->
resultMap.apply { putAll(partialMap) }
}
// Several compatible actuals on one path: report AMBIGUIOUS_ACTUALS here
val atLeastWeaklyCompatibleActuals = compatibility.filterKeys { it.isCompatibleOrWeakCompatible() }.values.flatten()
if (atLeastWeaklyCompatibleActuals.size > 1) {
trace.report(Errors.AMBIGUOUS_ACTUALS.on(
reportOn,
descriptor,
atLeastWeaklyCompatibleActuals.map { DescriptorUtils.getContainingSourceFile(it).let { it.name ?: it.toString() } }
))
}
}
private fun checkExpectedDeclarationHasAtLeastOneActual(
reportOn: KtNamedDeclaration,
descriptor: MemberDescriptor,
trace: BindingTrace,
module: ModuleDescriptor,
expectActualTracker: ExpectActualTracker,
moduleVisibilityFilter: ModuleFilter
) {
// Only look for top level actual members; class members will be handled as a part of that expected class
if (descriptor.containingDeclaration !is PackageFragmentDescriptor) return
val compatibility = ExpectedActualResolver.findActualForExpected(descriptor, platformModule) ?: return
val compatibility = ExpectedActualResolver.findActualForExpected(descriptor, module, moduleVisibilityFilter) ?: return
// Only strong incompatibilities, but this is an OptionalExpectation -- don't report it
if (compatibility.allStrongIncompatibilities() && isOptionalAnnotationClass(descriptor)) return
val shouldReportError =
compatibility.allStrongIncompatibilities() ||
Compatible !in compatibility && descriptor.hasNoActualWithDiagnostic(compatibility)
if (shouldReportError) {
// Only strong incompatibilities, or error won't be reported on actual: report NO_ACTUAL_FOR_EXPECT here
if (compatibility.allStrongIncompatibilities() ||
Compatible !in compatibility && descriptor.hasNoActualWithDiagnostic(compatibility)
) {
assert(compatibility.keys.all { it is Incompatible })
@Suppress("UNCHECKED_CAST")
val incompatibility = compatibility as Map<Incompatible, Collection<MemberDescriptor>>
trace.report(Errors.NO_ACTUAL_FOR_EXPECT.on(reportOn, descriptor, platformModule, incompatibility))
} else {
val actualMembers = compatibility.asSequence()
.filter { (compatibility, _) ->
compatibility is Compatible || (compatibility is Incompatible && compatibility.kind != Compatibility.IncompatibilityKind.STRONG)
}.flatMap { it.value.asSequence() }
expectActualTracker.reportExpectActual(expected = descriptor, actualMembers = actualMembers)
trace.report(Errors.NO_ACTUAL_FOR_EXPECT.on(reportOn, descriptor, module, incompatibility))
return
}
// Here we have exactly one compatible actual and/or some weakly incompatible. In either case, we don't report anything on expect
val actualMembers = compatibility.asSequence()
.filter { (compatibility, _) ->
compatibility is Compatible || (compatibility is Incompatible && compatibility.kind != Compatibility.IncompatibilityKind.STRONG)
}.flatMap { it.value.asSequence() }
expectActualTracker.reportExpectActual(expected = descriptor, actualMembers = actualMembers)
}
private fun MemberDescriptor.hasNoActualWithDiagnostic(
@@ -122,19 +210,16 @@ class ExpectedActualDeclarationChecker(val argumentExtractors: Iterable<ActualAn
?.run { VfsUtilCore.virtualToIoFile(psiFile.virtualFile) }
private fun checkActualDeclarationHasExpected(
reportOn: KtNamedDeclaration, descriptor: MemberDescriptor, trace: BindingTrace, checkActual: Boolean
reportOn: KtNamedDeclaration,
descriptor: MemberDescriptor,
trace: BindingTrace,
checkActual: Boolean,
moduleVisibilityFilter: ModuleFilter
) {
// TODO: ideally, we should always use common module here
// However, in compiler context platform & common modules are joined into one module,
// so there is yet no "common module" in this situation.
// So yet we are using own module in compiler context and common module in IDE context.
val commonOrOwnModules = descriptor.module.expectedByModules.ifEmpty { listOf(descriptor.module) }
val compatibility = commonOrOwnModules
.mapNotNull { ExpectedActualResolver.findExpectedForActual(descriptor, it) }
.ifEmpty { return }
.fold(LinkedHashMap<Compatibility, List<MemberDescriptor>>()) { resultMap, partialMap ->
resultMap.apply { putAll(partialMap) }
}
val compatibility = ExpectedActualResolver.findExpectedForActual(descriptor, descriptor.module, moduleVisibilityFilter)
?: return
checkAmbiguousExpects(compatibility, trace, reportOn, descriptor)
val hasActualModifier = descriptor.isActual && reportOn.hasActualModifier()
if (!hasActualModifier) {
@@ -180,7 +265,7 @@ class ExpectedActualDeclarationChecker(val argumentExtractors: Iterable<ActualAn
if (nonTrivialUnfulfilled.isNotEmpty()) {
val classDescriptor =
(descriptor as? TypeAliasDescriptor)?.expandedType?.constructor?.declarationDescriptor as? ClassDescriptor
?: (descriptor as ClassDescriptor)
?: (descriptor as ClassDescriptor)
trace.report(
Errors.NO_ACTUAL_CLASS_MEMBER_FOR_EXPECTED_CLASS.on(
reportOn, classDescriptor, nonTrivialUnfulfilled
@@ -196,8 +281,8 @@ class ExpectedActualDeclarationChecker(val argumentExtractors: Iterable<ActualAn
val expected = compatibility[Compatible]!!.first()
if (expected is ClassDescriptor && expected.kind == ClassKind.ANNOTATION_CLASS) {
val actualConstructor =
(descriptor as? ClassDescriptor)?.constructors?.singleOrNull() ?:
(descriptor as? TypeAliasDescriptor)?.constructors?.singleOrNull()?.underlyingConstructorDescriptor
(descriptor as? ClassDescriptor)?.constructors?.singleOrNull()
?: (descriptor as? TypeAliasDescriptor)?.constructors?.singleOrNull()?.underlyingConstructorDescriptor
val expectedConstructor = expected.constructors.singleOrNull()
if (expectedConstructor != null && actualConstructor != null) {
checkAnnotationConstructors(expectedConstructor, actualConstructor, trace, reportOn)
@@ -206,6 +291,30 @@ class ExpectedActualDeclarationChecker(val argumentExtractors: Iterable<ActualAn
}
}
private fun checkAmbiguousExpects(
compatibility: Map<Compatibility, List<MemberDescriptor>>,
trace: BindingTrace,
reportOn: KtNamedDeclaration,
descriptor: MemberDescriptor
) {
val filesWithAtLeastWeaklyCompatibleExpects = compatibility.asSequence()
.filter { (compatibility, _) ->
compatibility.isCompatibleOrWeakCompatible()
}
.map { (_, members) -> members }
.flatten()
.map { DescriptorUtils.getContainingSourceFile(it).let { it.name ?: it.toString() } }
.toList()
if (filesWithAtLeastWeaklyCompatibleExpects.size > 1) {
trace.report(Errors.AMBIGUOUS_EXPECTS.on(reportOn, descriptor, filesWithAtLeastWeaklyCompatibleExpects))
}
}
private fun Compatibility.isCompatibleOrWeakCompatible() =
this is Compatible ||
this is Incompatible && kind == ExpectedActualResolver.Compatibility.IncompatibilityKind.WEAK
// we don't require `actual` modifier on
// - annotation constructors, because annotation classes can only have one constructor
// - inline class primary constructors, because inline class must have primary constructor
@@ -295,5 +404,16 @@ class ExpectedActualDeclarationChecker(val argumentExtractors: Iterable<ActualAn
fun Map<out Compatibility, Collection<MemberDescriptor>>.allStrongIncompatibilities(): Boolean =
this.keys.all { it is Incompatible && it.kind == Compatibility.IncompatibilityKind.STRONG }
private fun <K, V> LinkedHashMap<K, List<V>>.merge(other: Map<K, List<V>>): LinkedHashMap<K, List<V>> {
for ((key, newValue) in other) {
val oldValue = this[key] ?: emptyList()
this[key] = oldValue + newValue
}
return this
}
private fun ModuleInfo.unwrapModuleInfo(): List<ModuleInfo> = if (this is CombinedModuleInfo) this.containedModules else listOf(this)
}
}
@@ -29,6 +29,7 @@ import org.jetbrains.kotlin.utils.SmartList
import org.jetbrains.kotlin.utils.keysToMap
object ExpectedActualResolver {
// FIXME(dsavvinov): review clients, as they won't work properly in HMPP projects
fun MemberDescriptor.findCompatibleActualForExpected(platformModule: ModuleDescriptor): List<MemberDescriptor> =
findActualForExpected(this, platformModule)?.get(Compatible).orEmpty()
@@ -42,10 +43,14 @@ object ExpectedActualResolver {
fun MemberDescriptor.findCompatibleExpectedForActual(commonModule: ModuleDescriptor): List<MemberDescriptor> =
findExpectedForActual(this, commonModule)?.get(Compatible).orEmpty()
fun findActualForExpected(expected: MemberDescriptor, platformModule: ModuleDescriptor): Map<Compatibility, List<MemberDescriptor>>? {
fun findActualForExpected(
expected: MemberDescriptor,
platformModule: ModuleDescriptor,
moduleVisibilityFilter: ModuleFilter = onlyFromThisModule(platformModule)
): Map<Compatibility, List<MemberDescriptor>>? {
return when (expected) {
is CallableMemberDescriptor -> {
expected.findNamesakesFromModule(platformModule).filter { actual ->
expected.findNamesakesFromModule(platformModule, moduleVisibilityFilter).filter { actual ->
expected != actual && !actual.isExpect &&
// TODO: support non-source definitions (e.g. from Java)
actual.source.containingFile != SourceFile.NO_SOURCE_FILE
@@ -54,7 +59,7 @@ object ExpectedActualResolver {
}
}
is ClassDescriptor -> {
expected.findClassifiersFromModule(platformModule).filter { actual ->
expected.findClassifiersFromModule(platformModule, moduleVisibilityFilter).filter { actual ->
expected != actual && !actual.isExpect &&
actual.source.containingFile != SourceFile.NO_SOURCE_FILE
}.groupBy { actual ->
@@ -65,7 +70,11 @@ object ExpectedActualResolver {
}
}
fun findExpectedForActual(actual: MemberDescriptor, commonModule: ModuleDescriptor): Map<Compatibility, List<MemberDescriptor>>? {
fun findExpectedForActual(
actual: MemberDescriptor,
commonModule: ModuleDescriptor,
moduleFilter: (ModuleDescriptor) -> Boolean = onlyFromThisModule(commonModule)
): Map<Compatibility, List<MemberDescriptor>>? {
return when (actual) {
is CallableMemberDescriptor -> {
val container = actual.containingDeclaration
@@ -73,10 +82,10 @@ object ExpectedActualResolver {
is ClassifierDescriptorWithTypeParameters -> {
// TODO: replace with 'singleOrNull' as soon as multi-module diagnostic tests are refactored
val expectedClass =
findExpectedForActual(container, commonModule)?.values?.firstOrNull()?.firstOrNull() as? ClassDescriptor
findExpectedForActual(container, commonModule, moduleFilter)?.values?.firstOrNull()?.firstOrNull() as? ClassDescriptor
expectedClass?.getMembers(actual.name)?.filterIsInstance<CallableMemberDescriptor>().orEmpty()
}
is PackageFragmentDescriptor -> actual.findNamesakesFromModule(commonModule)
is PackageFragmentDescriptor -> actual.findNamesakesFromModule(commonModule, moduleFilter)
else -> return null // do not report anything for incorrect code, e.g. 'actual' local function
}
@@ -95,7 +104,7 @@ object ExpectedActualResolver {
}
}
is ClassifierDescriptorWithTypeParameters -> {
actual.findClassifiersFromModule(commonModule).filter { declaration ->
actual.findClassifiersFromModule(commonModule, moduleFilter).filter { declaration ->
actual != declaration &&
declaration is ClassDescriptor && declaration.isExpect
}.groupBy { expected ->
@@ -106,14 +115,16 @@ object ExpectedActualResolver {
}
}
private fun CallableMemberDescriptor.findNamesakesFromModule(module: ModuleDescriptor): Collection<CallableMemberDescriptor> {
val containingDeclaration = containingDeclaration
val scopes = when (containingDeclaration) {
private fun CallableMemberDescriptor.findNamesakesFromModule(
module: ModuleDescriptor,
moduleFilter: (ModuleDescriptor) -> Boolean
): Collection<CallableMemberDescriptor> {
val scopes = when (val containingDeclaration = containingDeclaration) {
is PackageFragmentDescriptor -> {
listOf(module.getPackage(containingDeclaration.fqName).memberScope)
}
is ClassDescriptor -> {
val classes = containingDeclaration.findClassifiersFromModule(module)
val classes = containingDeclaration.findClassifiersFromModule(module, moduleFilter)
.mapNotNull { if (it is TypeAliasDescriptor) it.classDescriptor else it }
.filterIsInstance<ClassDescriptor>()
if (this is ConstructorDescriptor) return classes.flatMap { it.constructors }
@@ -124,15 +135,23 @@ object ExpectedActualResolver {
}
return when (this) {
is FunctionDescriptor -> scopes.flatMap { it.getContributedFunctions(name, NoLookupLocation.FOR_ALREADY_TRACKED) }
is PropertyDescriptor -> scopes.flatMap { it.getContributedVariables(name, NoLookupLocation.FOR_ALREADY_TRACKED) }
is FunctionDescriptor -> scopes.flatMap {
it.getContributedDescriptors(DescriptorKindFilter.FUNCTIONS, { it == name })
.filter { it.name == name } as Collection<CallableMemberDescriptor>
}
is PropertyDescriptor -> scopes.flatMap {
it.getContributedDescriptors(DescriptorKindFilter.VARIABLES, { it == name })
.filter { it.name == name } as Collection<CallableMemberDescriptor>
}
else -> throw AssertionError("Unsupported declaration: $this")
}.onlyFromThis(module)
}.applyFilter(moduleFilter)
}
private fun ClassifierDescriptorWithTypeParameters.findClassifiersFromModule(
module: ModuleDescriptor,
includeDependencies: Boolean = false
moduleFilter: (ModuleDescriptor) -> Boolean
): Collection<ClassifierDescriptorWithTypeParameters> {
val classId = classId ?: return emptyList()
@@ -142,7 +161,7 @@ object ExpectedActualResolver {
val segments = classId.relativeClassName.pathSegments()
var classifiers = module.getPackage(classId.packageFqName).memberScope.getAllClassifiers(segments.first())
if (!includeDependencies) classifiers = classifiers.onlyFromThis(module)
classifiers = classifiers.applyFilter(moduleFilter)
for (name in segments.subList(1, segments.size)) {
classifiers = classifiers.mapNotNull { classifier ->
@@ -155,9 +174,6 @@ object ExpectedActualResolver {
return classifiers
}
private fun <T : DeclarationDescriptor> Iterable<T>.onlyFromThis(module: ModuleDescriptor): List<T> =
filter { it.module == module }
sealed class Compatibility {
// For IncompatibilityKind.STRONG `actual` declaration is considered as overload and error reports on expected declaration
enum class IncompatibilityKind {
@@ -231,7 +247,9 @@ object ExpectedActualResolver {
platformModule: ModuleDescriptor = b.module,
parentSubstitutor: Substitutor? = null
): Compatibility {
assert(a.name == b.name) { "This function should be invoked only for declarations with the same name: $a, $b" }
assert(a.name == b.name) {
"This function should be invoked only for declarations with the same name: $a, $b"
}
assert(a.containingDeclaration is ClassifierDescriptorWithTypeParameters == b.containingDeclaration is ClassifierDescriptorWithTypeParameters) {
"This function should be invoked only for declarations in the same kind of container (both members or both top level): $a, $b"
}
@@ -329,7 +347,7 @@ object ExpectedActualResolver {
return expected is ClassifierDescriptorWithTypeParameters &&
expected.isExpect &&
actual is ClassifierDescriptorWithTypeParameters &&
expected.findClassifiersFromModule(platformModule, includeDependencies = true).any { classifier ->
expected.findClassifiersFromModule(platformModule, moduleFilter = ALL_MODULES).any { classifier ->
// Note that it's fine to only check that this "actual typealias" expands to the expected class, without checking
// whether the type arguments in the expansion are in the correct order or have the correct variance, because we only
// allow simple cases like "actual typealias Foo<A, B> = FooImpl<A, B>", see DeclarationsChecker#checkActualTypeAlias
@@ -537,3 +555,4 @@ object ExpectedActualResolver {
(parent?.invoke(type) ?: type)?.asTypeProjection()?.let(typeSubstitutor::substitute)?.type
}
}
@@ -0,0 +1,18 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.resolve.multiplatform
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.resolve.descriptorUtil.module
typealias ModuleFilter = (ModuleDescriptor) -> Boolean
fun onlyFromThisModule(module: ModuleDescriptor): ModuleFilter = { it == module }
val ALL_MODULES: ModuleFilter = { true }
fun <T : DeclarationDescriptor> Iterable<T>.applyFilter(filter: ModuleFilter): List<T> = filter { filter(it.module) }
@@ -464,7 +464,7 @@ interface SourceForBinaryModuleInfo : IdeaModuleInfo {
}
data class PlatformModuleInfo(
val platformModule: ModuleSourceInfo,
override val platformModule: ModuleSourceInfo,
private val commonModules: List<ModuleSourceInfo> // NOTE: usually contains a single element for current implementation
) : IdeaModuleInfo, CombinedModuleInfo, TrackableModuleInfo {
override val capabilities: Map<ModuleDescriptor.Capability<*>, Any?>
@@ -485,6 +485,9 @@ data class PlatformModuleInfo(
override fun dependencies() = platformModule.dependencies()
override val expectedBy: List<ModuleInfo>
get() = platformModule.expectedBy
override fun modulesWhoseInternalsAreVisible() = containedModules.flatMap { it.modulesWhoseInternalsAreVisible() }
override val name: Name
@@ -181,8 +181,8 @@ public class IdeErrorMessages {
MAP.put(EXPERIMENTAL_FEATURE_WARNING, "<html>{0}</html>", new LanguageFeatureMessageRenderer(LanguageFeatureMessageRenderer.Type.WARNING, true));
MAP.put(EXPERIMENTAL_FEATURE_ERROR, "<html>{0}</html>", new LanguageFeatureMessageRenderer(LanguageFeatureMessageRenderer.Type.ERROR, true));
MAP.put(NO_ACTUAL_FOR_EXPECT, "<html>Expected {0} has no actual declaration in module{1}{2}</html>", DECLARATION_NAME_WITH_KIND,
PLATFORM, new PlatformIncompatibilityDiagnosticRenderer(IdeMultiplatformDiagnosticRenderingMode.INSTANCE));
MAP.put(NO_ACTUAL_FOR_EXPECT, "<html>Expected {0} has no actual declaration in module {1}{2}</html>", DECLARATION_NAME_WITH_KIND,
MODULE_WITH_PLATFORM, new PlatformIncompatibilityDiagnosticRenderer(IdeMultiplatformDiagnosticRenderingMode.INSTANCE));
MAP.put(ACTUAL_WITHOUT_EXPECT, "<html>{0} has no corresponding expected declaration{1}</html>", CAPITALIZED_DECLARATION_NAME_WITH_KIND_AND_PLATFORM,
new PlatformIncompatibilityDiagnosticRenderer(IdeMultiplatformDiagnosticRenderingMode.INSTANCE));
@@ -6,6 +6,7 @@
package org.jetbrains.kotlin.idea.project
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.idea.caches.project.implementedDescriptors
import org.jetbrains.kotlin.idea.caches.project.implementingDescriptors
import org.jetbrains.kotlin.resolve.ModulePath
import org.jetbrains.kotlin.resolve.ModuleStructureOracle
@@ -28,7 +29,7 @@ class IdeaModuleStructureOracle : ModuleStructureOracle {
val currentPath: Stack<ModuleDescriptor> = Stack()
return sequence<ModulePath> {
yieldPathsFromSubgraph(module, currentPath, getChilds = { it.expectedByModules })
yieldPathsFromSubgraph(module, currentPath, getChilds = { it.implementedDescriptors })
}.toList()
}
@@ -35,6 +35,7 @@ class OptionalExpectationInspection : AbstractKotlinInspection() {
val descriptor = classOrObject.resolveToDescriptorIfAny() ?: return
if (!descriptor.annotations.hasAnnotation(ExpectedActualDeclarationChecker.OPTIONAL_EXPECTATION_FQ_NAME)) return
// FIXME(dsavvinov): this is wrong in HMPP model, use logic similar to ExpectedActualDeclarationChecker
val implementingModules = classOrObject.findModuleDescriptor().implementingDescriptors
if (implementingModules.isEmpty()) return