Skip to content

Report the correct errors in the IDE and add test infrastructure for diagnostics #5484

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Nov 21, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions compiler/src/dotty/tools/dotc/reporting/Reporter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,10 @@ abstract class Reporter extends interfaces.ReporterResult {
def isHidden(m: MessageContainer)(implicit ctx: Context): Boolean =
ctx.mode.is(Mode.Printing)

/** Does this reporter contain not yet reported errors or warnings? */
def hasPendingErrors: Boolean = false
/** Does this reporter contain errors that have yet to be reported by its outer reporter ?
* Note: this is always false when there is no outer reporter.
*/
def hasUnreportedErrors: Boolean = false

/** If this reporter buffers messages, remove and return all buffered messages. */
def removeBufferedMessages(implicit ctx: Context): List[MessageContainer] = Nil
Expand Down
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/reporting/StoreReporter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ class StoreReporter(outer: Reporter) extends Reporter {
infos += m
}

override def hasPendingErrors: Boolean =
infos != null && infos.exists(_.isInstanceOf[Error])
override def hasUnreportedErrors: Boolean =
outer != null && infos != null && infos.exists(_.isInstanceOf[Error])

override def removeBufferedMessages(implicit ctx: Context): List[MessageContainer] =
if (infos != null) try infos.toList finally infos = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,11 +291,10 @@ object messages {
val kind: String = "Type Mismatch"
val msg: String = {
val (where, printCtx) = Formatting.disambiguateTypes(found, expected)
val whereSuffix = if (where.isEmpty) where else s"\n\n$where"
val (fnd, exp) = Formatting.typeDiff(found, expected)(printCtx)
s"""|found: $fnd
|required: $exp
|
|$where""".stripMargin + whyNoMatch + implicitFailure
|required: $exp""".stripMargin + whereSuffix + whyNoMatch + implicitFailure
}

val explanation: String = ""
Expand Down
5 changes: 1 addition & 4 deletions compiler/src/dotty/tools/dotc/typer/Inferencing.scala
Original file line number Diff line number Diff line change
Expand Up @@ -432,10 +432,7 @@ trait Inferencing { this: Typer =>
// found : Int(1)
// required: String
// val y: List[List[String]] = List(List(1))
val hasUnreportedErrors = state.reporter match {
case r: StoreReporter if r.hasErrors => true
case _ => false
}
val hasUnreportedErrors = state.reporter.hasUnreportedErrors
def constraint = state.constraint
for (tvar <- qualifying)
if (!tvar.isInstantiated && state.constraint.contains(tvar)) {
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ object ProtoTypes {
targ = arg.withType(WildcardType)
else {
targ = typerFn(arg)
if (!ctx.reporter.hasPendingErrors) {
if (!ctx.reporter.hasUnreportedErrors) {
// FIXME: This can swallow warnings by updating the typerstate from a nested
// context that gets discarded later. But we do have to update the
// typerstate if there are no errors. If we also omitted the next two lines
Expand Down
1 change: 0 additions & 1 deletion compiler/src/dotty/tools/dotc/util/ParsedComment.scala
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ object ParsedComment {
"@throws" -> TagFormatter("Throws", toDescriptionList),
"@see" -> TagFormatter("See Also", toMarkdownList),
"@example" -> TagFormatter("Examples", toCodeFences("scala")),
"@usecase" -> TagFormatter("Usecases", toCodeFences("scala")),
"@note" -> TagFormatter("Note", toMarkdownList),
"@author" -> TagFormatter("Authors", toMarkdownList),
"@since" -> TagFormatter("Since", toMarkdownList),
Expand Down
7 changes: 0 additions & 7 deletions compiler/test-resources/repl/errmsgs
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,30 @@ scala> val x: List[String] = List(1)
| ^
| found: Int(1)
| required: String
|
scala> val y: List[List[String]] = List(List(1))
1 | val y: List[List[String]] = List(List(1))
| ^
| found: Int(1)
| required: String
|
scala> val z: (List[String], List[Int]) = (List(1), List("a"))
1 | val z: (List[String], List[Int]) = (List(1), List("a"))
| ^
| found: Int(1)
| required: String
|
1 | val z: (List[String], List[Int]) = (List(1), List("a"))
| ^^^
| found: String("a")
| required: Int
|
scala> val a: Inv[String] = new Inv(new Inv(1))
1 | val a: Inv[String] = new Inv(new Inv(1))
| ^^^^^^^^^^
| found: Inv[Int]
| required: String
|
scala> val b: Inv[String] = new Inv(1)
1 | val b: Inv[String] = new Inv(1)
| ^
| found: Int(1)
| required: String
|
scala> abstract class C { type T; val x: T; val s: Unit = { type T = String; var y: T = x; locally { def f() = { type T = Int; val z: T = y }; f() } }; }
1 | abstract class C { type T; val x: T; val s: Unit = { type T = String; var y: T = x; locally { def f() = { type T = Int; val z: T = y }; f() } }; }
| ^
Expand All @@ -59,7 +53,6 @@ scala> val x: List[Int] = "foo" :: List(1)
| ^^^^^
| found: String("foo")
| required: Int
|
scala> { def f: Int = g; val x: Int = 1; def g: Int = 5; }
1 | { def f: Int = g; val x: Int = 1; def g: Int = 5; }
| ^
Expand Down
1 change: 0 additions & 1 deletion compiler/test-resources/repl/importFromObj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ scala> buf += xs
| ^^
| found: List[Int](o.xs)
| required: Int
|
scala> buf ++= xs
val res0: scala.collection.mutable.ListBuffer[Int] = ListBuffer(1, 2, 3)
scala> import util.foo
Expand Down
1 change: 0 additions & 1 deletion compiler/test-resources/type-printer/type-mismatch
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,3 @@ scala> val x: Foo[String] = res0
| ^^^^
| found: Foo[Int](res0)
| required: Foo[String]
|
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package dotty.tools.languageserver

import org.junit.Test

import dotty.tools.dotc.reporting.diagnostic.ErrorMessageID._
import dotty.tools.languageserver.util.Code._
import org.eclipse.lsp4j.DiagnosticSeverity._

class DiagnosticsTest {
@Test def diagnosticWrongType: Unit =
code"""object Test {
| val x: Int = $m1"foo"$m2
|}""".withSource
.diagnostics(m1,
(m1 to m2, """found: String("foo")
|required: Int""".stripMargin, Error, Some(TypeMismatchID))
)

@Test def diagnosticMissingLambdaBody: Unit =
code"""object Test {
| Nil.map(x => x).filter(x$m1 =>$m2)
|$m3}""".withSource
.diagnostics(m1,
(m2 to m3, "illegal start of simple expression", Error, Some(IllegalStartSimpleExprID)),
(m1 to m1, """found: Null
|required: Boolean""".stripMargin, Error, Some(TypeMismatchID))
)

@Test def diagnosticPureExpression: Unit =
code"""object Test {
| ${m1}1$m2
|}""".withSource
.diagnostics(m1,
(m1 to m2,
"a pure expression does nothing in statement position; you may be omitting necessary parentheses",
Warning, Some(PureExpressionInStatementPositionID)))

@Test def diagnosticWorksheetPureExpression: Unit =
ws"""${m1}1""".withSource
.diagnostics(m1 /* no "pure expression" warning because this is a worksheet */)
}
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,6 @@ class HoverTest {
| myFoo.bar[Int, String](0, "hello, world")
| ```
|
|**Usecases**
| - ```scala
| def bar(fizz: Int, buzz: String): Any
| ```
|
|**Note**
| - A note
|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ class SymbolTest {
code"class $Bar",
code"""package foo
|class $fooFoo {
| class Bar
| class ${m7}Bar$m8
|}
"""
) .symbol("Foo", Foo.range.symInfo("Foo", SymbolKind.Class), fooFoo.range.symInfo("Foo", SymbolKind.Class, "foo"))
.symbol("Bar", Bar.range.symInfo("Bar", SymbolKind.Class))
.symbol("Bar", Bar.range.symInfo("Bar", SymbolKind.Class), (m7 to m8).symInfo("Bar", SymbolKind.Class, "Foo"))
}

@Test def symbolShowModule: Unit = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ object Code {
}

if (pi.hasNext)
stringBuilder.append(pi.next())
stringBuilder.append(pi.next().stripMargin)

(stringBuilder.result(), positions.result())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ case class CodeRange(start: CodeMarker, end: CodeMarker) {
if (!checked) {
assert(start.file == end.file, s"$start and $end where not in the same file")
assert(start.line <= end.line, s"Expected $end to be after $start")
assert(start.line != end.line || start.character < end.character, s"Expected $end to be after $start")
assert(start.line != end.line || start.character <= end.character, s"Expected $end to be at or after $start")
checked = true
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import dotty.tools.languageserver.util.actions._
import dotty.tools.languageserver.util.embedded.CodeMarker
import dotty.tools.languageserver.util.server.{TestFile, TestServer}

import dotty.tools.dotc.reporting.diagnostic.ErrorMessageID
import dotty.tools.dotc.util.Signatures.Signature

import org.eclipse.lsp4j.{CompletionItemKind, DocumentHighlightKind}
import org.eclipse.lsp4j.{ CompletionItemKind, DocumentHighlightKind, Diagnostic, DiagnosticSeverity }

import org.junit.Assert.assertEquals

/**
* Simulates an LSP client for test in a project defined by `sources`.
Expand All @@ -30,6 +33,31 @@ class CodeTester(projects: List[Project]) {

private val positions: PositionContext = getPositions(files)

/** Check that the last diagnostics that have been published so far by the server
* for a given file match `expected`.
*
* @param marker The marker defining the source file from which to query.
* @param expected The expected diagnostics to be found
*/
def diagnostics(marker: CodeMarker,
expected: (CodeRange, String, DiagnosticSeverity, Option[ErrorMessageID])*): this.type = {
implicit val posCtx = positions

def toDiagnostic(range: CodeRange, message: String, severity: DiagnosticSeverity,
errorCode: Option[ErrorMessageID]): Diagnostic = {
new Diagnostic(range.toRange, message, severity, /*source =*/"",
errorCode.map(_.errorNumber.toString).orNull)
}

val expectedParams = marker.toPublishDiagnosticsParams(expected.toList.map(toDiagnostic))
// Find the latest published diagnostics for the current source file
val actualParams = testServer.client.diagnostics.get.reverse.find(_.getUri == marker.uri)
.getOrElse(throw new Exception(s"No published diagnostics for ${marker.uri}"))
assertEquals(expectedParams, actualParams)

this
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other "actions" in this file usually do

def foo(marker: CodeMarker, expected: ...) = doAction(new Foo(marker, expected))

Any reason for not following the same pattern?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly because there's no action (request sent to the server) involved here, the request is "didOpen" but it's done implicitly. But if you think this could be refactored, please do!


/**
* Perform a hover over `range`, verifies that result matches `expected`.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package dotty.tools.languageserver.util.embedded
import dotty.tools.languageserver.util.server.TestFile
import dotty.tools.languageserver.util.{CodeRange, PositionContext}

import scala.collection.JavaConverters._

import org.eclipse.lsp4j._

import PositionContext.PosCtx
Expand All @@ -16,6 +18,9 @@ class CodeMarker(val name: String) extends Embedded {
/** The file containing this marker. */
def file: PosCtx[TestFile] = posCtx.positionOf(this)._1

/** The uri of the file containing this marker. */
def uri: PosCtx[String] = file.uri

/** The line containing this marker. */
def line: PosCtx[Int] = posCtx.positionOf(this)._2

Expand All @@ -34,6 +39,9 @@ class CodeMarker(val name: String) extends Embedded {
def toCompletionParams: PosCtx[CompletionParams] =
new CompletionParams(toTextDocumentIdentifier, toPosition)

def toPublishDiagnosticsParams(diagnostics: List[Diagnostic]): PosCtx[PublishDiagnosticsParams] =
new PublishDiagnosticsParams(file.uri, diagnostics.asJava)

def toRenameParams(newName: String): PosCtx[RenameParams] =
new RenameParams(toTextDocumentIdentifier, toPosition, newName)

Expand Down
2 changes: 1 addition & 1 deletion project/Build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -894,7 +894,7 @@ object Build {
).
settings(
ideTestsCompilerVersion := (version in `dotty-compiler`).value,
ideTestsCompilerArguments := (scalacOptions in `dotty-compiler`).value,
ideTestsCompilerArguments := Seq(),
ideTestsDependencyClasspath := {
val dottyLib = (classDirectory in `dotty-library-bootstrapped` in Compile).value
val scalaLib =
Expand Down