Skip to content

Commit

Permalink
Fix type mismatch errors inside the copy function are not shown
Browse files Browse the repository at this point in the history
  • Loading branch information
JavierSegoviaCordoba committed Aug 28, 2024
1 parent bb4a24c commit c6c7a2e
Show file tree
Hide file tree
Showing 10 changed files with 437 additions and 225 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

### Fixed

- type mismatch errors inside the `copy` function are not shown

### Removed

### Updated
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ internal class FirKopyCheckerExtension(

override val declarationCheckers: DeclarationCheckers = FirKopyDeclarationCheckers

override val expressionCheckers: ExpressionCheckers = FirKopyExpressionCheckers
override val expressionCheckers: ExpressionCheckers = FirKopyExpressionCheckers(session)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package com.javiersc.kotlin.kopy.compiler.fir.checker

import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.diagnostics.KtDiagnosticFactory1
import org.jetbrains.kotlin.diagnostics.KtDiagnosticFactory2
import org.jetbrains.kotlin.diagnostics.KtDiagnosticFactoryToRendererMap
import org.jetbrains.kotlin.diagnostics.error1
import org.jetbrains.kotlin.diagnostics.error2
import org.jetbrains.kotlin.diagnostics.rendering.BaseDiagnosticRendererFactory
import org.jetbrains.kotlin.diagnostics.rendering.Renderer
import org.jetbrains.kotlin.diagnostics.rendering.RootDiagnosticRendererFactory
Expand All @@ -24,31 +26,39 @@ internal object FirKopyError : BaseDiagnosticRendererFactory() {

val NO_COPY_SCOPE: KtDiagnosticFactory1<String> by error1<PsiElement, String>()

val ARGUMENT_TYPE_MISMATCH: KtDiagnosticFactory2<String, String> by error2<PsiElement, String, String>()

override val MAP: KtDiagnosticFactoryToRendererMap = rendererMap { map ->
map.put(
factory = NON_DATA_CLASS_KOPY_ANNOTATED,
message = "The class `{0}` must be a data class",
message = "The class ''{0}'' must be a data class",
rendererA = Renderer { t: String -> t },
)
map.put(
factory = INVALID_CALL_CHAIN,
message = "Call chain broken at `{0}`",
message = "Call chain broken at ''{0}''",
rendererA = Renderer { t: String -> t },
)
map.put(
factory = MISSING_DATA_CLASS,
message = "The property `{0}` does not belong to a data class",
message = "The property ''{0}'' does not belong to a data class",
rendererA = Renderer { t: String -> t },
)
map.put(
factory = MISSING_KOPY_ANNOTATION,
message = "The property `{0}` does not belong to a data class annotated with `@Kopy`",
message = "The property ''{0}'' does not belong to a data class annotated with ''@Kopy''",
rendererA = Renderer { t: String -> t },
)
map.put(
factory = NO_COPY_SCOPE,
message = "Invalid scope `{0}`, `set/update` are only allowed inside a `copy` scope",
message = "Invalid scope ''{0}'', ''set/update'' are only allowed inside a ''copy'' scope",
rendererA = Renderer { t: String -> t },
)
map.put(
factory = ARGUMENT_TYPE_MISMATCH,
message = "Argument type mismatch: actual type is ''{1}'', but ''{0}'' was expected.",
rendererA = Renderer { t: String -> t },
rendererB = Renderer { t: String -> t },
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,19 @@

package com.javiersc.kotlin.kopy.compiler.fir.checker.checkers

import com.javiersc.kotlin.compiler.extensions.common.classId
import com.javiersc.kotlin.compiler.extensions.fir.asFirOrNull
import com.javiersc.kotlin.kopy.KopyFunctionCopy
import com.javiersc.kotlin.kopy.KopyFunctionInvoke
import com.javiersc.kotlin.kopy.KopyFunctionSet
import com.javiersc.kotlin.kopy.KopyFunctionUpdate
import com.javiersc.kotlin.kopy.KopyFunctionUpdateEach
import com.javiersc.kotlin.kopy.compiler.fir.checker.FirKopyError
import com.javiersc.kotlin.kopy.compiler.fir.checker.checkers.BreakingCallsChecker.CheckerResult.Failure
import com.javiersc.kotlin.kopy.compiler.fir.checker.checkers.BreakingCallsChecker.CheckerResult.Ignore
import com.javiersc.kotlin.kopy.compiler.fir.checker.checkers.BreakingCallsChecker.CheckerResult.Success
import org.jetbrains.kotlin.KtSourceElement
import org.jetbrains.kotlin.diagnostics.DiagnosticReporter
import org.jetbrains.kotlin.diagnostics.SourceElementPositioningStrategies
import org.jetbrains.kotlin.diagnostics.reportOn
import org.jetbrains.kotlin.fir.FirElement
import com.javiersc.kotlin.kopy.compiler.fir.checker.checkers.expression.BreakingCallsChecker
import com.javiersc.kotlin.kopy.compiler.fir.checker.checkers.expression.ArgumentTypeMismatchTypeChecker
import org.jetbrains.kotlin.fir.FirSession
import org.jetbrains.kotlin.fir.analysis.checkers.MppCheckerKind
import org.jetbrains.kotlin.fir.analysis.checkers.context.CheckerContext
import org.jetbrains.kotlin.fir.analysis.checkers.expression.ExpressionCheckers
import org.jetbrains.kotlin.fir.analysis.checkers.expression.FirCallChecker
import org.jetbrains.kotlin.fir.declarations.FirDeclaration
import org.jetbrains.kotlin.fir.declarations.hasAnnotation
import org.jetbrains.kotlin.fir.declarations.utils.isData
import org.jetbrains.kotlin.fir.expressions.FirAnonymousFunctionExpression
import org.jetbrains.kotlin.fir.expressions.FirCall
import org.jetbrains.kotlin.fir.expressions.FirExpression
import org.jetbrains.kotlin.fir.expressions.FirFunctionCall
import org.jetbrains.kotlin.fir.expressions.FirPropertyAccessExpression
import org.jetbrains.kotlin.fir.expressions.FirResolvable
import org.jetbrains.kotlin.fir.expressions.FirThisReceiverExpression
import org.jetbrains.kotlin.fir.references.symbol
import org.jetbrains.kotlin.fir.references.toResolvedFunctionSymbol
import org.jetbrains.kotlin.fir.render
import org.jetbrains.kotlin.fir.symbols.FirBasedSymbol
import org.jetbrains.kotlin.fir.symbols.impl.FirFunctionSymbol
import org.jetbrains.kotlin.fir.types.resolvedType
import org.jetbrains.kotlin.fir.types.toRegularClassSymbol
import org.jetbrains.kotlin.name.ClassId

internal object FirKopyExpressionCheckers : ExpressionCheckers() {
internal class FirKopyExpressionCheckers(
private val session: FirSession,
) : ExpressionCheckers() {
override val callCheckers: Set<FirCallChecker> =
setOf(
BreakingCallsChecker,
ArgumentTypeMismatchTypeChecker(session = session),
)
// override val variableAssignmentCheckers: Set<FirVariableAssignmentChecker> =
// setOf(
Expand All @@ -63,185 +32,3 @@ internal object FirKopyExpressionCheckers : ExpressionCheckers() {
// TODO("Report assignments issues (same way we report `set` function issues")
// }
// }

private object BreakingCallsChecker : FirCallChecker(MppCheckerKind.Common) {

override fun check(expression: FirCall, context: CheckerContext, reporter: DiagnosticReporter) {
when (val checkerResult: CheckerResult = expression.isBreakingCallsChain(context)) {
is Ignore -> return
is Success -> return
is Failure.BrokenChain -> {
reporter.reportOn(
source = checkerResult.source,
factory = FirKopyError.INVALID_CALL_CHAIN,
a = checkerResult.element.render(),
context = context,
positioningStrategy = SourceElementPositioningStrategies.DEFAULT,
)
}

is Failure.MissingDataClass -> {
reporter.reportOn(
source = checkerResult.source,
factory = FirKopyError.MISSING_DATA_CLASS,
a = checkerResult.element.render(),
context = context,
positioningStrategy = SourceElementPositioningStrategies.DEFAULT,
)
}

// is Failure.MissingKopyAnnotation -> {
// reporter.reportOn(
// source = checkerResult.source,
// factory = FirKopyError.MISSING_KOPY_ANNOTATION,
// a = checkerResult.element.render(),
// context = context,
// positioningStrategy = SourceElementPositioningStrategies.DEFAULT,
// )
// }

is Failure.NoCopyScope -> {
reporter.reportOn(
source = checkerResult.source,
factory = FirKopyError.NO_COPY_SCOPE,
a = checkerResult.element.render(),
context = context,
positioningStrategy = SourceElementPositioningStrategies.DEFAULT,
)
}
}
}

private val FirCall.isKopyFunctionSetOrUpdateOrUpdateEachCall: Boolean
get() {
val annotations =
asFirOrNull<FirFunctionCall>()
?.calleeReference
?.symbol
?.resolvedAnnotationClassIds
return annotations?.any { it.isKopyFunctionSetOrUpdateOrUpdateEach == true } == true
}

private val ClassId?.isKopyFunctionSetOrUpdateOrUpdateEach: Boolean
get() =
this == classId<KopyFunctionSet>() ||
this == classId<KopyFunctionUpdate>() ||
this == classId<KopyFunctionUpdateEach>()

private fun FirCall.isBreakingCallsChain(context: CheckerContext): CheckerResult {
if (!isKopyFunctionSetOrUpdateOrUpdateEachCall) return Ignore

val session: FirSession = context.session
val setOrUpdateCall: FirFunctionCall = asFirOrNull() ?: return Failure.BrokenChain(this)

val noCopyScopeFailure: Failure? = checkFailureNoCopyScope(context, setOrUpdateCall)
if (noCopyScopeFailure != null) return noCopyScopeFailure

val extensionReceiver: FirPropertyAccessExpression =
setOrUpdateCall.extensionReceiver?.asFirOrNull<FirPropertyAccessExpression>()
?: return Failure.BrokenChain(setOrUpdateCall)
val updateOrSetThisBoundSymbol: FirBasedSymbol<*> =
setOrUpdateCall.dispatchReceiver
?.asFirOrNull<FirThisReceiverExpression>()
?.calleeReference
?.boundSymbol ?: return Failure.BrokenChain(setOrUpdateCall)

val extensionDispatchReceiver: FirExpression =
extensionReceiver.dispatchReceiver
?: return Failure.BrokenChain(extensionReceiver.calleeReference)

val checkerResult: CheckerResult =
extensionDispatchReceiver.isBreakingChainCall(session, updateOrSetThisBoundSymbol)
return checkerResult
}

private fun checkFailureNoCopyScope(
context: CheckerContext,
setOrUpdateCall: FirFunctionCall
): Failure.NoCopyScope? {
val session: FirSession = context.session
val setOrUpdateCallLambdaAnonymousFunction: FirDeclaration =
setOrUpdateCall.dispatchReceiver
?.asFirOrNull<FirThisReceiverExpression>()
?.calleeReference
?.boundSymbol
?.fir ?: return Failure.NoCopyScope(setOrUpdateCall)
val isCopyScope: Boolean =
context.callsOrAssignments
.asSequence()
.filterIsInstance<FirFunctionCall>()
.map { it to it.argumentList.arguments }
.mapNotNull { (functionCall, arguments) ->
val symbol: FirFunctionSymbol<*> =
functionCall.calleeReference.toResolvedFunctionSymbol()
?: return@mapNotNull null
val isKopyInvoke = symbol.hasAnnotation(classId<KopyFunctionInvoke>(), session)
val isKopyCopy = symbol.hasAnnotation(classId<KopyFunctionCopy>(), session)
if (isKopyInvoke || isKopyCopy) arguments else null
}
.flatMap { arguments ->
arguments
.asSequence()
.filterIsInstance<FirAnonymousFunctionExpression>()
.map(FirAnonymousFunctionExpression::anonymousFunction)
}
.any { it == setOrUpdateCallLambdaAnonymousFunction }

return if (!isCopyScope) Failure.NoCopyScope(setOrUpdateCall.calleeReference) else null
}

private fun FirExpression.isBreakingChainCall(
session: FirSession,
updateOrSetThisBoundSymbol: FirBasedSymbol<*>
): CheckerResult {
val thisBoundSymbol: FirBasedSymbol<*>? =
this.asFirOrNull<FirThisReceiverExpression>()?.calleeReference?.boundSymbol
if (updateOrSetThisBoundSymbol == thisBoundSymbol) return Success

if (this !is FirPropertyAccessExpression) return Failure.BrokenChain(this)

val isDataClass: Boolean = this.resolvedType.toRegularClassSymbol(session)?.isData ?: false

val receiver: FirExpression? = this.dispatchReceiver

val dispatcherBoundSymbol: FirBasedSymbol<*>? =
receiver?.asFirOrNull<FirThisReceiverExpression>()?.calleeReference?.boundSymbol

val hasSameBoundSymbol: Boolean = dispatcherBoundSymbol == updateOrSetThisBoundSymbol

return when {
!isDataClass -> Failure.MissingDataClass(this.calleeReference)
hasSameBoundSymbol -> Success
receiver == null -> Failure.BrokenChain(this)
receiver !is FirPropertyAccessExpression -> {
val element = receiver.asFirOrNull<FirResolvable>()?.calleeReference ?: receiver
Failure.BrokenChain(element)
}

else -> receiver.isBreakingChainCall(session, updateOrSetThisBoundSymbol)
}
}

sealed interface CheckerResult {

data object Ignore : CheckerResult

data object Success : CheckerResult

sealed interface Failure : CheckerResult {

val element: FirElement

val source: KtSourceElement
get() = element.source ?: error("No source for $this")

data class BrokenChain(override val element: FirElement) : Failure

data class MissingDataClass(override val element: FirElement) : Failure

// data class MissingKopyAnnotation(override val element: FirElement) : Failure

data class NoCopyScope(override val element: FirElement) : Failure
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.javiersc.kotlin.kopy.compiler.fir.checker.checkers.expression

import com.javiersc.kotlin.kopy.compiler.fir.checker.FirKopyError
import com.javiersc.kotlin.kopy.compiler.fir.utils.isKopyFunctionSetCall
import org.jetbrains.kotlin.diagnostics.DiagnosticReporter
import org.jetbrains.kotlin.diagnostics.SourceElementPositioningStrategies
import org.jetbrains.kotlin.diagnostics.reportOn
import org.jetbrains.kotlin.fir.FirSession
import org.jetbrains.kotlin.fir.analysis.checkers.MppCheckerKind
import org.jetbrains.kotlin.fir.analysis.checkers.context.CheckerContext
import org.jetbrains.kotlin.fir.analysis.checkers.expression.FirCallChecker
import org.jetbrains.kotlin.fir.expressions.FirCall
import org.jetbrains.kotlin.fir.expressions.FirFunctionCall
import org.jetbrains.kotlin.fir.expressions.arguments
import org.jetbrains.kotlin.fir.types.ConeKotlinType
import org.jetbrains.kotlin.fir.types.renderReadable
import org.jetbrains.kotlin.fir.types.resolvedType
import org.jetbrains.kotlin.fir.types.typeContext
import org.jetbrains.kotlin.types.AbstractTypeChecker

internal class ArgumentTypeMismatchTypeChecker(
private val session: FirSession,
) : FirCallChecker(MppCheckerKind.Common) {

override fun check(expression: FirCall, context: CheckerContext, reporter: DiagnosticReporter) {
if (!expression.isKopyFunctionSetCall) return
if (expression !is FirFunctionCall) return

val extensionType: ConeKotlinType? = expression.extensionReceiver?.resolvedType
val argumentType: ConeKotlinType? = expression.arguments.firstOrNull()?.resolvedType

if (extensionType == null || argumentType == null) return

val isSubtype: Boolean =
AbstractTypeChecker.isSubtypeOf(
context = session.typeContext,
subType = extensionType,
superType = argumentType,
)
if (!isSubtype) {
reporter.reportOn(
source = expression.arguments.first().source,
factory = FirKopyError.ARGUMENT_TYPE_MISMATCH,
a = extensionType.renderReadable(),
b = argumentType.renderReadable(),
context = context,
positioningStrategy = SourceElementPositioningStrategies.DEFAULT,
)
}
}
}
Loading

0 comments on commit c6c7a2e

Please sign in to comment.