[Expect/Actual] Support matching of expect/actual in hierarchical MPP
This commit is contained in:
@@ -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 =
|
||||
|
||||
+6
-2
@@ -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)
|
||||
|
||||
+154
-34
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+40
-21
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
@@ -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));
|
||||
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user