scala / scala3

The Scala 3 compiler, also known as Dotty.
https://dotty.epfl.ch
Apache License 2.0
5.79k stars 1.04k forks source link

Two escaped variables by ` can't be compiled when it is used with scalatest's #14230

Open catap opened 2 years ago

catap commented 2 years ago

Compiler version

This test reproduces with scalatest-3.2.10.

Compilers and results:

Minimized code

import org.scalatest._
import org.scalatest.matchers._

class BugTest extends wordspec.AnyWordSpec with should.Matchers {
  "can't compile" in {
    val `n1` = 42
    val `n2` = 44

    `n1` shouldBe `n1`
    `n2` shouldBe `n2`
  }

  "can compile 1" in {
    val `n1` = 42

    `n1` shouldBe `n1`
  }

  "can compile 2" in {
    val n1 = 42
    val n2 = 44

    n1 shouldBe n1
    n2 shouldBe n2
  }
}

Output

[error] -- Error: .../src/test/scala/BugTest.scala:10:4 
[error] 9 |    `n1` shouldBe `n1`
[error] 10 |    `n2` shouldBe `n2`
[error]    |    ^
[error]    |postfix operator `n2` needs to be enabled
[error]    |by making the implicit value scala.language.postfixOps visible.
[error]    |----
[error]    |This can be achieved by adding the import clause 'import scala.language.postfixOps'
[error]    |or by setting the compiler option -language:postfixOps.
[error]    |See the Scaladoc for value scala.language.postfixOps for a discussion
[error]    |why the feature needs to be explicitly enabled.

Suggested by compiler approach with adding postfixOps leads to this:

error] -- [E008] Not Found Error: .../src/test/scala/BugTest.scala:12:4 
[error] 11 |    `n1` shouldBe `n1`
[error] 12 |    `n2` shouldBe `n2`
[error]    |    ^
[error]    |    value n2 is not a member of org.scalatest.Assertion.
[error]    |    Note that `n2` is treated as an infix operator in Scala 3.
[error]    |    If you do not want that, insert a `;` or empty line in front
[error]    |    or drop any spaces behind the operator.
[error] -- [E051] Reference Error: .../src/test/scala/BugTest.scala:12:9 
[error] 12 |    `n2` shouldBe `n2`
[error]    |         ^^^^^^^^
[error]    |Ambiguous overload. The overloaded alternatives of method shouldBe in trait Matchers with types
[error]    | [T, U >: T]
[error]    |  (leftSideValue: T)
[error]    |    (using pos: org.scalactic.source.Position, prettifier: 
[error]    |      org.scalactic.Prettifier
[error]    |    )
[error]    |      (resultOfAnWordApplication: 
[error]    |        org.scalatest.matchers.dsl.ResultOfAnWordToBePropertyMatcherApplication[
[error]    |          U
[error]    |        ]
[error]    |      )(implicit ev: T <:< AnyRef): org.scalatest.Assertion
[error]    | [T, U >: T]
[error]    |  (leftSideValue: T)
[error]    |    (using pos: org.scalactic.source.Position, prettifier: 
[error]    |      org.scalactic.Prettifier
[error]    |    )
[error]    |      (resultOfAWordApplication: 
[error]    |        org.scalatest.matchers.dsl.ResultOfAWordToBePropertyMatcherApplication[U
[error]    |          ]
[error]    |      )(implicit ev: T <:< AnyRef): org.scalatest.Assertion
[error]    | [T]
[error]    |  (leftSideValue: T)
[error]    |    (using pos: org.scalactic.source.Position, prettifier: 
[error]    |      org.scalactic.Prettifier
[error]    |    )
[error]    |      (bePropertyMatcher: org.scalatest.matchers.BePropertyMatcher[T])
[error]    |        (implicit ev: T <:< AnyRef): org.scalatest.Assertion
[error]    | [T]
[error]    |  (leftSideValue: T)
[error]    |    (using pos: org.scalactic.source.Position, prettifier: 
[error]    |      org.scalactic.Prettifier
[error]    |    )
[error]    |      (resultOfAnWordApplication: 
[error]    |        org.scalatest.matchers.dsl.ResultOfAnWordToSymbolApplication
[error]    |      )(implicit toAnyRef: T <:< AnyRef): org.scalatest.Assertion
[error]    | [T]
[error]    |  (leftSideValue: T)
[error]    |    (using pos: org.scalactic.source.Position, prettifier: 
[error]    |      org.scalactic.Prettifier
[error]    |    )
[error]    |      (resultOfAWordApplication: 
[error]    |        org.scalatest.matchers.dsl.ResultOfAWordToSymbolApplication
[error]    |      )(implicit toAnyRef: T <:< AnyRef): org.scalatest.Assertion
[error]    | [T]
[error]    |  (leftSideValue: T)
[error]    |    (using pos: org.scalactic.source.Position, prettifier: 
[error]    |      org.scalactic.Prettifier
[error]    |    )(symbol: Symbol)(implicit toAnyRef: T <:< AnyRef): org.scalatest.Assertion
[error]    | [T]
[error]    |  (leftSideValue: T)
[error]    |    (using pos: org.scalactic.source.Position, prettifier: 
[error]    |      org.scalactic.Prettifier
[error]    |    )
[error]    |      (resultOfSameInstanceAsApplication: 
[error]    |        org.scalatest.matchers.dsl.ResultOfTheSameInstanceAsApplication
[error]    |      )(implicit toAnyRef: T <:< AnyRef): org.scalatest.Assertion
[error]    | [T]
[error]    |  (leftSideValue: T)
[error]    |    (using pos: org.scalactic.source.Position, prettifier: 
[error]    |      org.scalactic.Prettifier
[error]    |    )(right: Null)(implicit ev: T <:< AnyRef): org.scalatest.Assertion
[error]    | [T]
[error]    |  (leftSideValue: T)
[error]    |    (using pos: org.scalactic.source.Position, prettifier: 
[error]    |      org.scalactic.Prettifier
[error]    |    )
[error]    |      (right: org.scalatest.matchers.dsl.DefinedWord)
[error]    |        (implicit definition: org.scalatest.enablers.Definition[T]): 
[error]    |          org.scalatest.Assertion
[error]    | [T]
[error]    |  (leftSideValue: T)
[error]    |    (using pos: org.scalactic.source.Position, prettifier: 
[error]    |      org.scalactic.Prettifier
[error]    |    )
[error]    |      (right: org.scalatest.matchers.dsl.EmptyWord)
[error]    |        (implicit emptiness: org.scalatest.enablers.Emptiness[T]): 
[error]    |          org.scalatest.Assertion
[error]    | [T]
[error]    |  (leftSideValue: T)
[error]    |    (using pos: org.scalactic.source.Position, prettifier: 
[error]    |      org.scalactic.Prettifier
[error]    |    )
[error]    |      (right: org.scalatest.matchers.dsl.WritableWord)
[error]    |        (implicit writability: org.scalatest.enablers.Writability[T]): 
[error]    |          org.scalatest.Assertion
[error]    | [T]
[error]    |  (leftSideValue: T)
[error]    |    (using pos: org.scalactic.source.Position, prettifier: 
[error]    |      org.scalactic.Prettifier
[error]    |    )
[error]    |      (right: org.scalatest.matchers.dsl.ReadableWord)
[error]    |        (implicit readability: org.scalatest.enablers.Readability[T]): 
[error]    |          org.scalatest.Assertion
[error]    | [T]
[error]    |  (leftSideValue: T)
[error]    |    (using pos: org.scalactic.source.Position, prettifier: 
[error]    |      org.scalactic.Prettifier
[error]    |    )
[error]    |      (anType: org.scalatest.matchers.dsl.ResultOfAnTypeInvocation[?]): 
[error]    |        org.scalatest.Assertion
[error]    | [T]
[error]    |  (leftSideValue: T)
[error]    |    (using pos: org.scalactic.source.Position, prettifier: 
[error]    |      org.scalactic.Prettifier
[error]    |    )
[error]    |      (aType: org.scalatest.matchers.dsl.ResultOfATypeInvocation[?]): 
[error]    |        org.scalatest.Assertion
[error]    | [T]
[error]    |  (leftSideValue: T)
[error]    |    (using pos: org.scalactic.source.Position, prettifier: 
[error]    |      org.scalactic.Prettifier
[error]    |    )
[error]    |      (right: org.scalatest.matchers.dsl.SortedWord)
[error]    |        (implicit sortable: org.scalatest.enablers.Sortable[T]): 
[error]    |          org.scalatest.Assertion
[error]    | [T]
[error]    |  (leftSideValue: T)
[error]    |    (using pos: org.scalactic.source.Position, prettifier: 
[error]    |      org.scalactic.Prettifier
[error]    |    )
[error]    |      (spread: org.scalactic.TripleEqualsSupport.Spread[T]): 
[error]    |        org.scalatest.Assertion
[error]    | [T]
[error]    |  (leftSideValue: T)
[error]    |    (using pos: org.scalactic.source.Position, prettifier: 
[error]    |      org.scalactic.Prettifier
[error]    |    )(beMatcher: org.scalatest.matchers.BeMatcher[T]): org.scalatest.Assertion
[error]    | [T]
[error]    |  (leftSideValue: T)
[error]    |    (using pos: org.scalactic.source.Position, prettifier: 
[error]    |      org.scalactic.Prettifier
[error]    |    )
[error]    |      (comparison: 
[error]    |        org.scalatest.matchers.dsl.ResultOfGreaterThanOrEqualToComparison[T]
[error]    |      ): org.scalatest.Assertion
[error]    | [T]
[error]    |  (leftSideValue: T)
[error]    |    (using pos: org.scalactic.source.Position, prettifier: 
[error]    |      org.scalactic.Prettifier
[error]    |    )
[error]    |      (comparison: 
[error]    |        org.scalatest.matchers.dsl.ResultOfLessThanOrEqualToComparison[T]
[error]    |      ): org.scalatest.Assertion
[error]    | [T]
[error]    |  (leftSideValue: T)
[error]    |    (using pos: org.scalactic.source.Position, prettifier: 
[error]    |      org.scalactic.Prettifier
[error]    |    )
[error]    |      (comparison: org.scalatest.matchers.dsl.ResultOfGreaterThanComparison[T]
[error]    |        ): 
[error]    |      org.scalatest.Assertion
[error]    | [T]
[error]    |  (leftSideValue: T)
[error]    |    (using pos: org.scalactic.source.Position, prettifier: 
[error]    |      org.scalactic.Prettifier
[error]    |    )
[error]    |      (comparison: org.scalatest.matchers.dsl.ResultOfLessThanComparison[T]): 
[error]    |        org.scalatest.Assertion
[error]    | [T, R]
[error]    |  (leftSideValue: T)
[error]    |    (using pos: org.scalactic.source.Position, prettifier: 
[error]    |      org.scalactic.Prettifier
[error]    |    )(right: R)(implicit caneq: CanEqual[T, R]): org.scalatest.Assertion
[error]    |all match expected type <?>

Expectation

Should work on all tested compilers.

som-snytt commented 2 years ago

This looks like "leading infix" syntax, where the leading operator is in backticks.

Putting myop in backticks is special in Scala 3 because members are deemed "eligible for infix" syntax.

The syntax is supported in Scala 2.13 with -Xsource:3 but the behavior may diverge a bit.

The advice in the message is to make it not look like infix. (Add ; or remove a space, etc.)

I wonder if it could emit a better message when the operator name also designates a definition in scope, as in this example. Alternatively, change the feature so that it is actually ambiguous if there is both a member myop and a definition in scope; or, less likely, actually pick the intended meaning and use the definition in a new statement, but then leading infix is not just syntax.

catap commented 2 years ago

@som-snytt anyway, I do not understand why is using one such variable works fine.

som-snytt commented 2 years ago

I was going to make a clever example, but I was done in by unary ops:

scala> {
     | val + = 42
     | 1 + 2
     | + + 1
     | }
val +: Int = 42
val res0: Int = 4

where I intended

scala> {
     | val + = 42
     | 1 + 2
     | +.+(1)
     | }
val +: Int = 42
val res6: Int = 3
val res7: Int = 43
som-snytt commented 2 years ago

I tried it out with the scalatest dependency -- I still have to create an sbt project instead of using ammonite or other quickie tools -- and I wanted to register that I was also confused by the first error, about postfix.

I tried it out with scala 2 -Xsource;3 with similar results.

Parser says

[info]     "can\'t compile" in
[info]       {
[info]         val n1 = 42
[info]         val n2 = 44
[info]         `n1` shouldBe `n1` `n2` shouldBe `n2`
[info]       }

so infix line continuation says only "join the lines".

Sorry I didn't have a quick answer.

can't compile inspired the notion can't complain where the compiler is really a complainer, so thanks for that inspiration.

catap commented 2 years ago

@som-snytt anyway, workaround is simple: just rename variable to something without ` :)

som-snytt commented 2 years ago

or for improved funkiness

`n2`shouldBe `n2`
som-snytt commented 2 years ago

-Xsource:3 says value n2 is not a member of org.scalatest.Assertion, not found: value shouldBe, postfix operator n2 needs to be enabled, meaning the second n2 is postfix.

pikinier20 commented 2 years ago

@som-snytt In order to do some quick test, you may want to use scala-cli Having it in version 0.0.9, you just create file:

// using scala "3.1.2-RC1-bin-20220106-0b4c6e7-NIGHTLY"
// using lib "org.scalatest::scalatest:3.2.10"

import org.scalatest._
import org.scalatest.matchers._

class BugTest extends wordspec.AnyWordSpec with should.Matchers {
  "can't compile" in {
    val `n1` = 42
    val `n2` = 44

    `n1` shouldBe `n1`
    `n2` shouldBe `n2`
  }

  "can compile 1" in {
    val `n1` = 42

    `n1` shouldBe `n1`
  }

  "can compile 2" in {
    val n1 = 42
    val n2 = 44

    n1 shouldBe n1
    n2 shouldBe n2
  }
}

and run scala-cli test <filename>

som-snytt commented 2 years ago

@pikinier20 thanks! Sorry to bump the thread just for "thanks!", but that is an especially good feature. I've meant to try scala-cli since I learned about it.

som-snytt commented 3 months ago

The message is slightly less provoking in 3.4.2 or at least more direct:

[error] 11 |    `n2` shouldBe `n2`
[error]    |                  ^^
[error]    |                  end of statement expected but identifier found

My previous comment did not go far enough. I propose the progressive elimination of all space in favor of backticks, a style I call spaceticks:

    `n1` shouldBe `n1`
    `n2` shouldBe `n2`

    `n1`shouldBe`n1`
    `n2`shouldBe`n2`

    n1`shouldBe`n1
    n2`shouldBe`n2