scalameta / munit

Scala testing library with actionable errors and extensible APIs
https://scalameta.org/munit
Apache License 2.0
429 stars 90 forks source link

Broken compileErrors behavior on Scala 3 #711

Closed MateuszKubuszok closed 6 months ago

MateuszKubuszok commented 1 year ago

MUnit implements compileErrors on Scala 3 with inline def delegating to typeCheckErrors. In the source of typeCheckErrors we can read:

/** Whether the code type checks in the current context? If not,
 *  returns a list of errors encountered on compilation.
 *  IMPORTANT: No stability guarantees are provided on the format of these
 *  errors. This means the format and the API may change from
 *  version to version. This API is to be used for testing purposes
 *  only.
 *
 *  An inline definition with a call to `typeCheckErrors` should be transparent.
 *
 *  @param code The code to be type checked
 *
 *  @return a list of errors encountered during parsing and typechecking.
 *
 *  The code should be a sequence of expressions or statements that may appear in a block.
 */
transparent inline def typeCheckErrors(inline code: String): List[Error] = ...

This lack of transparent is a source of unwanted behavior that I noticed in my project and, from what I heard, also other people in their projects. However, it is quite difficult to create a reproduction that is completely independent of external libraries, which is why nobody reported it yet.

I also failed to came up with a simple reproduction, but I can demonstrate the issue using existing libraries:

//> using scala 3.3.1
//> using dep io.scalaland::chimney::0.8.0
//> using dep org.scalameta::munit::1.0.0-M10
import io.scalaland.chimney.dsl.*
import munit.internal.MacroCompat

object IOnlyNeedErrors extends MacroCompat.CompileErrorMacro {

  /* Copy/Paste from munit, with transparent keyword added. */
  transparent inline def compileErrorsFixed(inline code: String): String = {
    val errors = scala.compiletime.testing.typeCheckErrors(code)
    errors
      .map { error =>
        val indent = " " * (error.column - 1)
        val trimMessage = error.message.linesIterator
          .map { line =>
            if line.matches(" +") then ""
            else line
          }
          .mkString("\n")
        val separator = if error.message.contains('\n') then "\n" else " "
        s"error:$separator$trimMessage\n${error.lineContent}\n$indent^"
      }
      .mkString("\n")
  }
}

case class Source(a: Int)
case class Target(b: String)

// If uncommented:
//Source(1).transformInto[Target]
// results in:
//Chimney can't derive transformation from Playground.Source to Playground.Target
//
//Playground.Target
//  b: java.lang.String - no accessor named b in source type Playground.Source
//
//
//Consult https://chimney.readthedocs.io for usage examples.

println("munit, not fixed (inline def):")
println(IOnlyNeedErrors.compileErrors("Source(1).transformInto[Target]"))

println("munit, fixed (transparent inline def):")
println(IOnlyNeedErrors.compileErrorsFixed("Source(1).transformInto[Target]"))

(see Scastie)

As we can see on this example, expected error, the one we would see if compiling the code outside compileErrors would be:

Chimney can't derive transformation from Playground.Source to Playground.Target

Playground.Target
  b: java.lang.String - no accessor named b in source type Playground.Source

Consult https://chimney.readthedocs.io for usage examples.

meanwhile, MUnit produces:


No given instance of type io.scalaland.chimney.Transformer.AutoDerived[Playground.Source,
  Playground.Target] was found for parameter transformer of method transformInto in package io.scalaland.chimney.dsl.
I found:

    io.scalaland.chimney.Transformer.AutoDerived.deriveAutomatic[Playground.Source,
      Playground.Target]

But method deriveAutomatic in trait TransformerAutoDerivedCompanionPlatform does not match type io.scalaland.chimney.Transformer.AutoDerived[Playground.Source,
  Playground.Target].

The following import might make progress towards fixing the problem:

  import io.scalaland.chimney.auto.deriveAutomaticTransformer

The behavior works as expected on Scala 2. Fixing it is as easy as adding transparent before inline.

Unfortunately, as I said, I cannot provide better reproduction which would have no external dependencies.

tgodzik commented 6 months ago

Testing if it breaks anything in https://github.com/scalameta/munit/pull/759