diff --git a/compiler/src/dotty/tools/dotc/reporting/ConsoleReporter.scala b/compiler/src/dotty/tools/dotc/reporting/ConsoleReporter.scala index eb40cab959a2..b763f3b6933e 100644 --- a/compiler/src/dotty/tools/dotc/reporting/ConsoleReporter.scala +++ b/compiler/src/dotty/tools/dotc/reporting/ConsoleReporter.scala @@ -34,8 +34,10 @@ class ConsoleReporter( true } - if (didPrint && ctx.shouldExplain(m)) + if (didPrint && ctx.shouldExplain(m)) { printMessage(explanation(m.contained())) + if (m.contained().links.nonEmpty) printMessage(documentationLinks(m.contained())) + } else if (didPrint && m.contained().explanation.nonEmpty) printMessage("\nlonger explanation available when compiling with `-explain`") } diff --git a/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala b/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala index 65443a2377e3..43466fb76bf9 100644 --- a/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala +++ b/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala @@ -4,14 +4,14 @@ package reporting import core.Contexts.Context import core.Decorators._ -import printing.Highlighting.{Blue, Red} +import printing.Highlighting.{Blue, Red, Yellow} import printing.SyntaxHighlighting import diagnostic.{ErrorMessageID, Message, MessageContainer, NoExplanation} import diagnostic.messages._ import util.SourcePosition -import util.Chars.{ LF, CR, FF, SU } -import scala.annotation.switch +import util.Chars.{CR, FF, LF, SU} +import scala.annotation.switch import scala.collection.mutable trait MessageRendering { @@ -130,13 +130,29 @@ trait MessageRendering { val sb = new StringBuilder( hl"""| |${Blue("Explanation")} - |${Blue("===========")}""" + |${Blue("===========")} + |""" ) - sb.append('\n').append(m.explanation) - if (m.explanation.lastOption != Some('\n')) sb.append('\n') + sb.append(m.explanation) + if (!m.explanation.endsWith("\n")) sb.append('\n') sb.toString } + /** Documentation links rendered under "Further reading" header */ + def documentationLinks(m: Message)(implicit ctx: Context): String = { + val sb = new StringBuilder( + hl"""| + |${Blue("Further reading")} + |${Blue("===============")} + |""" + ) + for (link <- m.links) { + sb.append(link.text).append(hl" ${Yellow(link.url)}\n") + } + sb.append('\n').toString + } + + /** The whole message rendered from `msg` */ def messageAndPos(msg: Message, pos: SourcePosition, diagnosticLevel: String)(implicit ctx: Context): String = { val sb = mutable.StringBuilder.newBuilder diff --git a/compiler/src/dotty/tools/dotc/reporting/diagnostic/Message.scala b/compiler/src/dotty/tools/dotc/reporting/diagnostic/Message.scala index 09d7ae9751be..8d6554fd3d78 100644 --- a/compiler/src/dotty/tools/dotc/reporting/diagnostic/Message.scala +++ b/compiler/src/dotty/tools/dotc/reporting/diagnostic/Message.scala @@ -17,6 +17,36 @@ object Message { new NoExplanation(str) } +sealed trait DocumentationLink { + def url: String + def text: String +} + +/** A documentation link can be rendered by tooling to direct the programmer to + * good resources for more details related to the compiler error or warning. + * + * To keep base links at a single place use the cases classes to share a common + * prefix. + * + * Links are checked for existence in tests in `dotty.tools.dotc.reporting.ErrorMessagesTests` + * if the error class is tested there. + */ +object DocumentationLink { + case class LanguageSpec(suffix: String, text: String = "Scala Language Specification") extends DocumentationLink { + val url = s"https://www.scala-lang.org/files/archive/spec/2.13/$suffix" + } + case class TourUrl(suffix: String, text: String) extends DocumentationLink { + val url = s"http://docs.scala-lang.org/overviews/$suffix" + } + case class Sip(suffix: String, text: String) extends DocumentationLink { + val url = s"http://docs.scala-lang.org/sips/$suffix" + } + case class DottyDocs(suffix: String, text: String = "Dotty documentation") extends DocumentationLink { + val url = s"http://dotty.epfl.ch/docs/$suffix" + } + case class FullUrl(url: String, text: String) extends DocumentationLink +} + /** A `Message` contains all semantic information necessary to easily * comprehend what caused the message to be logged. Each message can be turned * into a `MessageContainer` which contains the log level and can later be @@ -56,6 +86,11 @@ abstract class Message(val errorId: ErrorMessageID) { self => */ def explanation: String + /** Links may list URLs to Internet resources related to the error + * e.g. the Scala Language Specification. + */ + val links: List[DocumentationLink] + /** The implicit `Context` in messages is a large thing that we don't want * persisted. This method gets around that by duplicating the message * without the implicit context being passed along. @@ -64,6 +99,7 @@ abstract class Message(val errorId: ErrorMessageID) { self => val msg = self.msg val kind = self.kind val explanation = self.explanation + override val links = self.links } } @@ -83,6 +119,7 @@ class ExtendMessage(_msg: () => Message)(f: String => String) { self => val msg = self.msg val kind = self.kind val explanation = self.explanation + val links = Nil } /** Enclose this message in an `Error` container */ @@ -118,6 +155,7 @@ class ExtendMessage(_msg: () => Message)(f: String => String) { self => class NoExplanation(val msg: String) extends Message(ErrorMessageID.NoExplanationID) { val explanation = "" val kind = "" + val links = Nil override def toString(): String = s"NoExplanation($msg)" } diff --git a/compiler/src/dotty/tools/dotc/reporting/diagnostic/MessageContainer.scala b/compiler/src/dotty/tools/dotc/reporting/diagnostic/MessageContainer.scala index c2d94030ca0b..a4fda666d822 100644 --- a/compiler/src/dotty/tools/dotc/reporting/diagnostic/MessageContainer.scala +++ b/compiler/src/dotty/tools/dotc/reporting/diagnostic/MessageContainer.scala @@ -15,10 +15,7 @@ object MessageContainer { implicit class MessageContext(val c: Context) extends AnyVal { def shouldExplain(cont: MessageContainer): Boolean = { implicit val ctx = c - cont.contained().explanation match { - case "" => false - case _ => ctx.settings.explain.value - } + cont.contained().explanation.nonEmpty && c.settings.explain.value } } } diff --git a/compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala b/compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala index c98e219a027a..a81b4c85097b 100644 --- a/compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala @@ -23,6 +23,8 @@ import dotty.tools.dotc.ast.Trees import dotty.tools.dotc.config.ScalaVersion import dotty.tools.dotc.core.Flags._ import dotty.tools.dotc.core.SymDenotations.SymDenotation +import diagnostic.DocumentationLink.{DottyDocs, FullUrl, LanguageSpec, TourUrl} + import scala.util.control.NonFatal object messages { @@ -97,9 +99,13 @@ object messages { import ast.tpd /** Helper methods for messages */ - def implicitClassRestrictionsText(implicit ctx: Context) = - hl"""|${NoColor("For a full list of restrictions on implicit classes visit")} - |${Blue("http://docs.scala-lang.org/overviews/core/implicit-classes.html")}""" + private def tryExpressionLinks = LanguageSpec("06-expressions.html#try-expressions") :: Nil + + private def implicitClassLinks = TourUrl("core/implicit-classes.html", "More information on implicit classes") :: Nil + + private def patternMatchingLinks = + LanguageSpec("08-pattern-matching.html") :: + DottyDocs("reference/changed/pattern-matching.html") :: Nil // Syntax Errors ---------------------------------------------------------- // @@ -137,6 +143,7 @@ object messages { |It is recommended to use the ${"NonFatal"} extractor to catch all exceptions as it |correctly handles transfer functions like ${"return"}.""" } + } case class EmptyCatchBlock(tryBody: untpd.Tree)(implicit ctx: Context) @@ -145,6 +152,8 @@ object messages { val msg = hl"""|The ${"catch"} block does not contain a valid expression, try |adding a case like - `${"case e: Exception =>"}` to the block""" + + val links = tryExpressionLinks } case class EmptyCatchAndFinallyBlock(tryBody: untpd.Tree)(implicit ctx: Context) @@ -153,6 +162,8 @@ object messages { val msg = hl"""|A ${"try"} without ${"catch"} or ${"finally"} is equivalent to putting |its body in a block; no exceptions are handled.""" + + val links = tryExpressionLinks } case class DeprecatedWithOperator()(implicit ctx: Context) @@ -164,6 +175,8 @@ object messages { hl"""|Dotty introduces intersection types - `&' types. These replace the |use of the ${"with"} keyword. There are a few differences in |semantics between intersection types and using `${"with"}'.""" + + val links = DottyDocs("reference/intersection-types.html") :: Nil } case class CaseClassMissingParamList(cdef: untpd.TypeDef)(implicit ctx: Context) @@ -176,6 +189,8 @@ object messages { hl"""|${cdef.name} must have at least one parameter list, if you would rather |have a singleton representation of ${cdef.name}, use a "${"case object"}". |Or, add an explicit `()' as a parameter list to ${cdef.name}.""" + + val links = LanguageSpec("05-classes-and-objects.html#case-classes") :: Nil } case class AnonymousFunctionMissingParamType(param: untpd.ValDef, @@ -208,6 +223,8 @@ object messages { |Make sure you give it a type of what you expect to match and help the type inference system: | |${"val f: Any => Int = { case x: Int => x + 1 }"} """ + + val links = LanguageSpec("06-expressions.html#anonymous-functions") :: Nil } case class WildcardOnTypeArgumentNotAllowedOnNew()(implicit ctx: Context) @@ -241,6 +258,8 @@ object messages { |You must complete all the type parameters, for instance: | |$code2 """ + + val links = Nil } @@ -270,6 +289,8 @@ object messages { | |`${bind.name}` is not unique. Rename one of the bound variables!""" } + + val links = Nil } case class MissingIdent(tree: untpd.Ident, treeKind: String, name: String)(implicit ctx: Context) @@ -283,6 +304,8 @@ object messages { |That can happen for instance if $name or its declaration has either been |misspelt, or if you're forgetting an import""" } + + val links = Nil } case class TypeMismatch(found: Type, expected: Type, whyNoMatch: String = "", implicitFailure: String = "")(implicit ctx: Context) @@ -298,6 +321,8 @@ object messages { } val explanation = "" + + val links = Nil } case class NotAMember(site: Type, name: Name, selected: String)(implicit ctx: Context) @@ -364,6 +389,8 @@ object messages { } val explanation = "" + + val links = Nil } case class EarlyDefinitionsNotSupported()(implicit ctx: Context) @@ -409,7 +436,10 @@ object messages { | |$code2 |""" + } + + val links = DottyDocs("reference/trait-parameters.html") :: Nil } case class TopLevelImplicitClass(cdef: untpd.TypeDef)(implicit ctx: Context) @@ -429,8 +459,6 @@ object messages { |the same name created by the compiler which would cause a naming conflict if it |were allowed. | - |""" + implicitClassRestrictionsText + hl"""| - | |To resolve the conflict declare ${cdef.name} inside of an ${"object"} then import the class |from the object at the use site if needed, for example: | @@ -439,8 +467,10 @@ object messages { |} | |// At the use site: - |import Implicits.${cdef.name}""" + |${"import Implicits"}.${cdef.name}""" } + + val links = implicitClassLinks } case class ImplicitCaseClass(cdef: untpd.TypeDef)(implicit ctx: Context) @@ -451,9 +481,10 @@ object messages { val explanation = hl"""|implicit classes may not be case classes. Instead use a plain class: | - |implicit class ${cdef.name}... - | - |""" + implicitClassRestrictionsText + |${"implicit class"} ${cdef.name}... + |""" + + val links = implicitClassLinks } case class ImplicitClassPrimaryConstructorArity()(implicit ctx: Context) @@ -468,8 +499,10 @@ object messages { | |While it’s possible to create an implicit class with more than one non-implicit argument, |such classes aren’t used during implicit lookup. - |""" + implicitClassRestrictionsText + |""" } + + val links = Nil } case class ObjectMayNotHaveSelfType(mdef: untpd.ModuleDef)(implicit ctx: Context) @@ -488,6 +521,8 @@ object messages { | | object $name extends ${selfTpt.show}""" } + + val links = Nil } case class TupleTooLong(ts: List[untpd.Tree])(implicit ctx: Context) @@ -504,6 +539,8 @@ object messages { | |((${nestedRepresentation}))""" } + + val links = Nil } case class RepeatedModifier(modifier: String)(implicit ctx:Context) @@ -526,6 +563,8 @@ object messages { | |""" } + + val links = Nil } case class InterpolatedStringError()(implicit ctx:Context) @@ -533,17 +572,17 @@ object messages { val kind = "Syntax" val msg = "error in interpolated string: identifier or block expected" val explanation = { - val code1 = "s\"$new Point(0, 0)\"" - val code2 = "s\"${new Point(0, 0)}\"" hl"""|This usually happens when you forget to place your expressions inside curly braces. | - |$code1 + |${"s\"$new Point(0, 0)\""} | |should be written as | - |$code2 + |${"s\"${new Point(0, 0)}\""} |""" } + + val links = TourUrl("core/string-interpolation.html", "More on String Interpolation") :: Nil } case class UnboundPlaceholderParameter()(implicit ctx:Context) @@ -580,6 +619,8 @@ object messages { |Only fields can be left uninitialized in this manner; local variables |must be initialized. |""" + + val links = Nil } case class IllegalStartSimpleExpr(illegalToken: String)(implicit ctx: Context) @@ -599,6 +640,7 @@ object messages { | |which cannot start with ${Red(illegalToken)}.""" } + val links = Nil } case class MissingReturnType()(implicit ctx:Context) @@ -611,6 +653,7 @@ object messages { |trait Shape { | def area: Double // abstract declaration returning a ${"Double"} |}""" + val links = Nil } case class MissingReturnTypeWithReturnStatement(method: Symbol)(implicit ctx: Context) @@ -622,6 +665,7 @@ object messages { |explicit return type. For example: | |${"def good: Int /* explicit return type */ = return 1"}""" + val links = Nil } case class YieldOrDoExpectedInForComprehension()(implicit ctx: Context) @@ -655,6 +699,8 @@ object messages { |${"for i <- 1 to 3 do println(i) // notice the 'do' keyword"} | |""" + + val links = Nil } case class ProperDefinitionNotFound()(implicit ctx: Context) @@ -694,6 +740,8 @@ object messages { |usecase and the compiler makes sure that it is valid. Because of this, you're |only allowed to use ${"def"}s when defining usecases.""" } + + val links = Nil } case class ByNameParameterNotSupported()(implicit ctx: Context) @@ -718,6 +766,8 @@ object messages { |And the usage could be as such: |${"func(bool => // do something...)"} |""" + + val links = Nil } case class WrongNumberOfTypeArgs(fntpe: Type, expectedArgs: List[ParamInfo], actual: List[untpd.Tree])(implicit ctx: Context) @@ -768,6 +818,8 @@ object messages { hl"""|You have not supplied enough type parameters |If you specify one type parameter then you need to specify every type parameter.""" } + + val links = Nil } case class IllegalVariableInPatternAlternative()(implicit ctx: Context) @@ -797,6 +849,7 @@ object messages { | |$fixedVarInAlternative""" } + val links = patternMatchingLinks } case class IdentifierExpected(identifier: String)(implicit ctx: Context) @@ -818,6 +871,8 @@ object messages { | |""" } + + val links = Nil } case class AuxConstructorNeedsNonImplicitParameter()(implicit ctx:Context) @@ -834,6 +889,8 @@ object messages { | - forgotten parenthesis on ${"this"} (${"def this() = { ... }"}) | - auxiliary constructors specify the implicit value |""" + + val links = Nil } case class IncorrectRepeatedParameterSyntax()(implicit ctx: Context) @@ -861,6 +918,8 @@ object messages { |${"val ints = List(2, 3, 4) // ints: List[Int] = List(2, 3, 4)"} |${"square(ints: _*) // res1: List[Int] = List(4, 9, 16)"} |""".stripMargin + + val links = Nil } case class IllegalLiteral()(implicit ctx: Context) @@ -876,6 +935,8 @@ object messages { | - String Literals: "Hello, World!" | - null |""" + + val links = LanguageSpec(suffix = "01-lexical-syntax.html#literals") :: Nil } case class PatternMatchExhaustivity(uncovered: String)(implicit ctx: Context) @@ -893,6 +954,8 @@ object messages { | - If an extractor always return 'Some(...)', write 'Some[X]' for its return type | - Add a 'case _ => ...' at the end to match all remaining cases |""" + + val links = patternMatchingLinks } case class UncheckedTypePattern(msg: String)(implicit ctx: Context) @@ -905,6 +968,8 @@ object messages { | |You can either replace the type arguments by `_` or use `@unchecked`. |""" + + val links = Nil } case class MatchCaseUnreachable()(implicit ctx: Context) @@ -912,6 +977,7 @@ object messages { val kind = s"""Match ${hl"case"} Unreachable""" val msg = "unreachable code" val explanation = "" + val links = patternMatchingLinks } case class SeqWildcardPatternPos()(implicit ctx: Context) @@ -936,6 +1002,7 @@ object messages { | |would give 3 as a result""" } + val links = patternMatchingLinks } case class IllegalStartOfSimplePattern()(implicit ctx: Context) @@ -1016,6 +1083,8 @@ object messages { | ${"def unapplySeq[A](x: List[A]): Some[List[A]]"} |""" } + + val links = Nil } case class PkgDuplicateSymbol(existing: Symbol)(implicit ctx: Context) @@ -1023,6 +1092,7 @@ object messages { val kind = "Duplicate Symbol" val msg = hl"trying to define package with same name as `$existing`" val explanation = "" + val links = Nil } case class ExistentialTypesNoLongerSupported()(implicit ctx: Context) @@ -1046,6 +1116,7 @@ object messages { | |${"List[_]"} |""" + val links = DottyDocs("reference/dropped/existential-types.html") :: Nil } case class UnboundWildcardType()(implicit ctx: Context) @@ -1091,6 +1162,8 @@ object messages { | Use: | ${"val foo: Int = 3"} |""" + + val links = Nil } case class DanglingThisInPath()(implicit ctx: Context) extends Message(DanglingThisInPathID) { @@ -1127,6 +1200,8 @@ object messages { |- this is a valid type using a path |${typeCode} |""" + + val links = Nil } case class OverridesNothing(member: Symbol)(implicit ctx: Context) @@ -1139,6 +1214,8 @@ object messages { |class of `${member.owner}` to override it. Did you misspell it? |Are you extending the right classes? |""" + + val links = Nil } case class OverridesNothingButNameExists(member: Symbol, existing: List[Denotations.SingleDenotation])(implicit ctx: Context) @@ -1158,6 +1235,8 @@ object messages { |named `${member.name}`: | ${existingDecl} |""" + + val links = Nil } case class ForwardReferenceExtendsOverDefinition(value: Symbol, definition: Symbol)(implicit ctx: Context) @@ -1177,6 +1256,8 @@ object messages { |the declaration of `${definition.name}` and its use, |or define `${value.name}` as lazy. |""".stripMargin + + val links = Nil } case class ExpectedTokenButFound(expected: Token, found: Token)(implicit ctx: Context) @@ -1198,6 +1279,8 @@ object messages { else "" val explanation = s"$ifKeyword" + + val links = Nil } case class MixedLeftAndRightAssociativeOps(op1: Name, op2: Name, op2LeftAssoc: Boolean)(implicit ctx: Context) @@ -1232,6 +1315,8 @@ object messages { | (all other special characters) |Operators starting with a letter have lowest precedence, followed by operators starting with `|`, etc. |""".stripMargin + + val links = LanguageSpec("06-expressions.html#infix-operations") :: Nil } case class CantInstantiateAbstractClassOrTrait(cls: Symbol, isTrait: Boolean)(implicit ctx: Context) @@ -1251,6 +1336,8 @@ object messages { | |You need to implement any abstract members in both cases. |""".stripMargin + + val links = Nil } case class OverloadedOrRecursiveMethodNeedsResultType(tree: Names.TermName)(implicit ctx: Context) @@ -1265,6 +1352,8 @@ object messages { |Case 2: ${tree} is recursive |If `${tree.name}` calls itself on any path, you need to specify its return type. |""".stripMargin + + val links = Nil } case class RecursiveValueNeedsResultType(tree: Names.TermName)(implicit ctx: Context) @@ -1274,6 +1363,7 @@ object messages { val explanation = hl"""The definition of `${tree.name}` is recursive and you need to specify its type. |""".stripMargin + val links = Nil } case class CyclicReferenceInvolving(denot: SymDenotation)(implicit ctx: Context) @@ -1284,6 +1374,7 @@ object messages { hl"""|$denot is declared as part of a cycle which makes it impossible for the |compiler to decide upon ${denot.name}'s type. |""".stripMargin + val links = Nil } case class CyclicReferenceInvolvingImplicit(cycleSym: Symbol)(implicit ctx: Context) @@ -1294,6 +1385,7 @@ object messages { hl"""|This happens when the right hand-side of $cycleSym's definition involves an implicit search. |To avoid this error, give `${cycleSym.name}` an explicit type. |""".stripMargin + val links = Nil } case class SuperQualMustBeParent(qual: untpd.Ident, cls: Symbols.ClassSymbol)(implicit ctx: Context) @@ -1311,6 +1403,8 @@ object messages { |In this case, the parents of $cls are: |${parents.mkString(" - ", "\n - ", "")} |""".stripMargin + + val links = Nil } case class VarArgsParamMustComeLast()(implicit ctx: Context) @@ -1321,6 +1415,7 @@ object messages { hl"""|The varargs field must be the last field in the method signature. |Attempting to define a field in a method signature after a varargs field is an error. |""" + val links = Nil } case class AmbiguousImport(name: Names.Name, newPrec: Int, prevPrec: Int, prevCtx: Context)(implicit ctx: Context) @@ -1362,6 +1457,8 @@ object messages { |- You may replace a name when imported using | ${"import"} scala.{ $name => ${name.show + "Tick"} } |""" + + val links = Nil } case class MethodDoesNotTakeParameters(tree: tpd.Tree, methPartType: Types.Type)(err: typer.ErrorReporting.Errors)(implicit ctx: Context) @@ -1386,6 +1483,7 @@ object messages { s"""|You have specified more parameter lists as defined in the method definition(s). |$noParameters""".stripMargin + val links = Nil } case class AmbiguousOverload(tree: tpd.Tree, alts: List[SingleDenotation], pt: Type)( @@ -1405,6 +1503,8 @@ object messages { |- assigning it to a value with a specified type, or |- adding a type ascription as in `${"instance.myMethod: String => Int"}` |""" + + val links = Nil } case class ReassignmentToVal(name: Names.Name)(implicit ctx: Context) @@ -1419,6 +1519,7 @@ object messages { |variable | ${"var"} $name ${"="} ... |""".stripMargin + val links = Nil } case class TypeDoesNotTakeParameters(tpe: Types.Type, params: List[Trees.Tree[Trees.Untyped]])(implicit ctx: Context) @@ -1434,6 +1535,8 @@ object messages { i"""You specified ${NoColor(ps)} for ${hl"$tpe"}, which is not |declared to take any. |""" + + val links = Nil } case class ParameterizedTypeLacksArguments(psym: Symbol)(implicit ctx: Context) @@ -1444,6 +1547,7 @@ object messages { hl"""The $psym is declared with non-implicit parameters, you may not leave |out the parameter list when extending it. |""" + val links = Nil } case class VarValParametersMayNotBeCallByName(name: Names.TermName, mutable: Boolean)(implicit ctx: Context) @@ -1458,6 +1562,7 @@ object messages { | ${s" def $name() = ${name}Tick"} | ${"}"} |""" + val links = Nil } case class MissingTypeParameterFor(tpe: Type)(implicit ctx: Context) @@ -1465,6 +1570,7 @@ object messages { val msg = hl"missing type parameter for ${tpe}" val kind = "Syntax" val explanation = "" + val links = Nil } case class DoesNotConformToBound(tpe: Type, which: String, bound: Type)( @@ -1473,6 +1579,7 @@ object messages { val msg = hl"Type argument ${tpe} does not conform to $which bound $bound ${err.whyNoMatchStr(tpe, bound)}" val kind = "Type Mismatch" val explanation = "" + val links = Nil } case class DoesNotConformToSelfType(category: String, selfType: Type, cls: Symbol, @@ -1489,6 +1596,7 @@ object messages { |Note: Self types are indicated with the notation | ${s"class "}$other ${"{ this: "}$otherSelf${" => "} """ + val links = Nil } case class DoesNotConformToSelfTypeCantBeInstantiated(tp: Type, selfType: Type)( @@ -1502,6 +1610,7 @@ object messages { |Note: Self types are indicated with the notation | ${s"class "}$tp ${"{ this: "}$selfType${" => "} |""" + val links = Nil } case class AbstractMemberMayNotHaveModifier(sym: Symbol, flag: FlagSet)( @@ -1510,6 +1619,7 @@ object messages { val msg = hl"""${"abstract"} $sym may not have `$flag' modifier""" val kind = "Syntax" val explanation = "" + val links = Nil } case class TopLevelCantBeImplicit(sym: Symbol)( @@ -1518,6 +1628,7 @@ object messages { val msg = hl"""${"implicit"} modifier cannot be used for top-level definitions""" val kind = "Syntax" val explanation = "" + val links = Nil } case class TypesAndTraitsCantBeImplicit(sym: Symbol)( @@ -1526,6 +1637,7 @@ object messages { val msg = hl"""${"implicit"} modifier cannot be used for types or traits""" val kind = "Syntax" val explanation = "" + val links = Nil } case class OnlyClassesCanBeAbstract(sym: Symbol)( @@ -1534,6 +1646,7 @@ object messages { val msg = hl"""${"abstract"} modifier can be used only for classes; it should be omitted for abstract members""" val kind = "Syntax" val explanation = "" + val links = Nil } case class AbstractOverrideOnlyInTraits(sym: Symbol)( @@ -1542,6 +1655,7 @@ object messages { val msg = hl"""${"abstract override"} modifier only allowed for members of traits""" val kind = "Syntax" val explanation = "" + val links = Nil } case class TraitsMayNotBeFinal(sym: Symbol)( @@ -1551,6 +1665,7 @@ object messages { val kind = "Syntax" val explanation = "A trait can never be final since it is abstract and must be extended to be useful." + val links = Nil } case class NativeMembersMayNotHaveImplementation(sym: Symbol)( @@ -1559,6 +1674,7 @@ object messages { val msg = hl"""${"@native"} members may not have an implementation""" val kind = "Syntax" val explanation = "" + val links = Nil } case class OnlyClassesCanHaveDeclaredButUndefinedMembers(sym: Symbol)( @@ -1571,6 +1687,7 @@ object messages { val msg = hl"""only classes can have declared but undefined members""" val kind = "Syntax" val explanation = s"$varNote" + val links = Nil } case class CannotExtendAnyVal(sym: Symbol)(implicit ctx: Context) @@ -1582,6 +1699,7 @@ object messages { |${"Any"} to become ${Green("\"universal traits\"")} which may only have ${"def"} members. |Universal traits can be mixed into classes that extend ${"AnyVal"}. |""" + val links = Nil } case class CannotHaveSameNameAs(sym: Symbol, cls: Symbol, reason: CannotHaveSameNameAs.Reason)(implicit ctx: Context) @@ -1598,6 +1716,7 @@ object messages { val msg = hl"""$sym cannot have the same name as ${cls.showLocated} -- """ + reasonMessage val kind = "Syntax" val explanation = "" + val links = Nil } object CannotHaveSameNameAs { sealed trait Reason @@ -1610,6 +1729,7 @@ object messages { val msg = hl"""value classes may not define an inner class""" val kind = "Syntax" val explanation = "" + val links = Nil } case class ValueClassesMayNotDefineNonParameterField(valueClass: Symbol, field: Symbol)(implicit ctx: Context) @@ -1617,6 +1737,7 @@ object messages { val msg = hl"""value classes may not define non-parameter field""" val kind = "Syntax" val explanation = "" + val links = Nil } case class ValueClassesMayNotDefineASecondaryConstructor(valueClass: Symbol, constructor: Symbol)(implicit ctx: Context) @@ -1624,6 +1745,7 @@ object messages { val msg = hl"""value classes may not define a secondary constructor""" val kind = "Syntax" val explanation = "" + val links = Nil } case class ValueClassesMayNotContainInitalization(valueClass: Symbol)(implicit ctx: Context) @@ -1631,6 +1753,7 @@ object messages { val msg = hl"""value classes may not contain initialization statements""" val kind = "Syntax" val explanation = "" + val links = Nil } case class ValueClassesMayNotBeAbstract(valueClass: Symbol)(implicit ctx: Context) @@ -1638,6 +1761,7 @@ object messages { val msg = hl"""value classes may not be ${"abstract"}""" val kind = "Syntax" val explanation = "" + val links = Nil } case class ValueClassesMayNotBeContainted(valueClass: Symbol)(implicit ctx: Context) @@ -1646,6 +1770,7 @@ object messages { val msg = s"""value classes may not be a $localOrMember""" val kind = "Syntax" val explanation = "" + val links = Nil } case class ValueClassesMayNotWrapItself(valueClass: Symbol)(implicit ctx: Context) @@ -1653,6 +1778,7 @@ object messages { val msg = """a value class may not wrap itself""" val kind = "Syntax" val explanation = "" + val links = Nil } case class ValueClassParameterMayNotBeAVar(valueClass: Symbol, param: Symbol)(implicit ctx: Context) @@ -1662,6 +1788,7 @@ object messages { val explanation = hl"""A value class must have exactly one ${"val"} parameter. |""" + val links = Nil } case class ValueClassNeedsOneValParam(valueClass: Symbol)(implicit ctx: Context) @@ -1669,6 +1796,7 @@ object messages { val msg = hl"""value class needs one ${"val"} parameter""" val kind = "Syntax" val explanation = "" + val links = Nil } case class OnlyCaseClassOrCaseObjectAllowed()(implicit ctx: Context) @@ -1676,6 +1804,7 @@ object messages { val msg = "only `case class` or `case object` allowed" val kind = "Syntax" val explanation = "" + val links = Nil } case class ExpectedClassOrObjectDef()(implicit ctx: Context) @@ -1683,6 +1812,7 @@ object messages { val kind = "Syntax" val msg = "expected class or object definition" val explanation = "" + val links = Nil } case class SuperCallsNotAllowedInline(symbol: Symbol)(implicit ctx: Context) @@ -1690,6 +1820,7 @@ object messages { val kind = "Syntax" val msg = s"super call not allowed in inline $symbol" val explanation = "Method inlining prohibits calling superclass methods, as it may lead to confusion about which super is being called." + val links = Nil } case class ModifiersNotAllowed(flags: FlagSet, printableType: Option[String])(implicit ctx: Context) @@ -1697,20 +1828,16 @@ object messages { val kind = "Syntax" val msg = s"modifier(s) `$flags' not allowed for ${printableType.getOrElse("combination")}" val explanation = { - val first = "sealed def y: Int = 1" - val second = "sealed lazy class z" hl"""You tried to use a modifier that is inapplicable for the type of item under modification - | - | Please see the official Scala Language Specification section on modifiers: - | https://www.scala-lang.org/files/archive/spec/2.11/05-classes-and-objects.html#modifiers | |Consider the following example: - |$first + |${"sealed def y: Int = 1"} |In this instance, the modifier 'sealed' is not applicable to the item type 'def' (method) - |$second - |In this instance, the modifier combination is not supported - """ + |${"sealed lazy class z"} + |In this instance, the modifier combination is not supported""" } + + val links = LanguageSpec("05-classes-and-objects.html#modifiers") :: Nil } case class FunctionTypeNeedsNonEmptyParameterList(isImplicit: Boolean = true, isErased: Boolean = true)(implicit ctx: Context) @@ -1730,6 +1857,7 @@ object messages { | |$code2""".stripMargin } + val links = DottyDocs("reference/implicit-function-types.html") :: Nil } case class WrongNumberOfParameters(expected: Int)(implicit ctx: Context) @@ -1737,6 +1865,7 @@ object messages { val kind = "Syntax" val msg = s"wrong number of parameters, expected: $expected" val explanation = "" + val links = Nil } case class DuplicatePrivateProtectedQualifier()(implicit ctx: Context) @@ -1745,6 +1874,7 @@ object messages { val msg = "duplicate private/protected qualifier" val explanation = hl"It is not allowed to combine `private` and `protected` modifiers even if they are qualified to different scopes" + val links = Nil } case class ExpectedStartOfTopLevelDefinition()(implicit ctx: Context) @@ -1753,6 +1883,7 @@ object messages { val msg = "expected start of definition" val explanation = hl"you have to provide either ${"class"}, ${"trait"}, ${"object"}, or ${"enum"} definitions after qualifiers" + val links = LanguageSpec(suffix = "09-top-level-definitions.html") :: Nil } case class NoReturnFromInline(owner: Symbol)(implicit ctx: Context) @@ -1764,6 +1895,7 @@ object messages { |Instead, you should rely on the last expression's value being |returned from a method. |""" + val links = Nil } case class ReturnOutsideMethodDefinition(owner: Symbol)(implicit ctx: Context) @@ -1774,6 +1906,7 @@ object messages { hl"""You used ${"return"} in ${owner}. |${"return"} is a keyword and may only be used within method declarations. |""" + val links = Nil } case class ExtendFinalClass(clazz:Symbol, finalClazz: Symbol)(implicit ctx: Context) @@ -1782,6 +1915,7 @@ object messages { val msg = hl"$clazz cannot extend ${"final"} $finalClazz" val explanation = hl"""A class marked with the ${"final"} keyword cannot be extended""" + val links = Nil } case class ExpectedTypeBoundOrEquals(found: Token)(implicit ctx: Context) @@ -1800,6 +1934,8 @@ object messages { |An upper type bound ${"T <: A"} declares that type variable ${"T"} |refers to a subtype of type ${"A"}. |""" + + val links = Nil } case class ClassAndCompanionNameClash(cls: Symbol, other: Symbol)(implicit ctx: Context) @@ -1813,6 +1949,7 @@ object messages { | - ${cls.owner} defines ${cls} | - ${other.owner} defines ${other}""" } + val links = Nil } case class TailrecNotApplicable(method: Symbol)(implicit ctx: Context) @@ -1821,6 +1958,7 @@ object messages { val msg = hl"TailRec optimisation not applicable, $method is neither ${"private"} nor ${"final"}." val explanation = hl"A method annotated ${"@tailrec"} must be declared ${"private"} or ${"final"} so it can't be overridden." + val links = Nil } case class FailureToEliminateExistential(tp: Type, tp1: Type, tp2: Type, boundSyms: List[Symbol])(implicit ctx: Context) @@ -1833,6 +1971,7 @@ object messages { |reduces to : $tp1 |type used instead: $tp2""" } + val links = Nil } case class OnlyFunctionsCanBeFollowedByUnderscore(pt: Type)(implicit ctx: Context) @@ -1842,6 +1981,7 @@ object messages { val explanation = hl"""The syntax ${"x _"} is no longer supported if ${"x"} is not a function. |To convert to a function value, you need to explicitly write ${"() => x"}""" + val links = Nil } case class MissingEmptyArgumentList(method: Symbol)(implicit ctx: Context) @@ -1860,6 +2000,7 @@ object messages { |In Dotty, this idiom is an error. The application syntax has to follow exactly the parameter syntax. |Excluded from this rule are methods that are defined in Java or that override methods defined in Java.""" } + val links = Nil } case class DuplicateNamedTypeParameter(name: Name)(implicit ctx: Context) @@ -1867,6 +2008,7 @@ object messages { val kind = "Syntax" val msg = hl"Type parameter $name was defined multiple times." val explanation = "" + val links = Nil } case class UndefinedNamedTypeParameter(undefinedName: Name, definedNames: List[Name])(implicit ctx: Context) @@ -1874,6 +2016,7 @@ object messages { val kind = "Syntax" val msg = hl"Type parameter $undefinedName is undefined. Expected one of ${definedNames.map(_.show).mkString(", ")}." val explanation = "" + val links = Nil } case class IllegalStartOfStatement(isModifier: Boolean)(implicit ctx: Context) extends Message(IllegalStartOfStatementID) { @@ -1883,6 +2026,7 @@ object messages { "Illegal start of statement" + addendum } val explanation = "A statement is either an import, a definition or an expression." + val links = Nil } case class TraitIsExpected(symbol: Symbol)(implicit ctx: Context) extends Message(TraitIsExpectedID) { @@ -1911,12 +2055,14 @@ object messages { |$codeExample |""" } + val links = Nil } case class TraitRedefinedFinalMethodFromAnyRef(method: Symbol)(implicit ctx: Context) extends Message(TraitRedefinedFinalMethodFromAnyRefID) { val kind = "Syntax" val msg = hl"Traits cannot redefine final $method from ${"class AnyRef"}." val explanation = "" + val links = Nil } case class PackageNameAlreadyDefined(pkg: Symbol)(implicit ctx: Context) extends Message(PackageNameAlreadyDefinedID) { @@ -1924,6 +2070,7 @@ object messages { val kind = "Naming" val explanation = hl"An ${"object"} cannot have the same name as an existing ${"package"}. Rename either one of them." + val links = Nil } case class UnapplyInvalidNumberOfArguments(qual: untpd.Tree, argTypes: List[Type])(implicit ctx: Context) @@ -1937,6 +2084,7 @@ object messages { | |where subsequent arguments would have following types: ($argTypes%, %). |""".stripMargin + val links = Nil } case class StaticFieldsOnlyAllowedInObjects(member: Symbol)(implicit ctx: Context) extends Message(StaticFieldsOnlyAllowedInObjectsID) { @@ -1944,6 +2092,7 @@ object messages { val kind = "Syntax" val explanation = hl"${"@static"} members are only allowed inside objects." + val links = Nil } case class CyclicInheritance(symbol: Symbol, addendum: String)(implicit ctx: Context) extends Message(CyclicInheritanceID) { @@ -1961,6 +2110,7 @@ object messages { |creates a "cycle" where a not yet defined class A extends itself which makes |impossible to instantiate an object of this class""" } + val links = Nil } case class BadSymbolicReference(denot: SymDenotation)(implicit ctx: Context) extends Message(BadSymbolicReferenceID) { @@ -1981,12 +2131,15 @@ object messages { } val explanation = "" + + val links = Nil } case class UnableToExtendSealedClass(pclazz: Symbol)(implicit ctx: Context) extends Message(UnableToExtendSealedClassID) { val kind = "Syntax" val msg = hl"Cannot extend ${"sealed"} $pclazz in a different source file" val explanation = "A sealed class or trait can only be extended in the same file as its declaration" + val links = Nil } case class SymbolHasUnparsableVersionNumber(symbol: Symbol, migrationMessage: String)(implicit ctx: Context) @@ -1999,6 +2152,7 @@ object messages { |The ${symbol.showLocated} is marked with ${"@migration"} indicating it has changed semantics |between versions and the ${"-Xmigration"} settings is used to warn about constructs |whose behavior may have changed since version change.""" + val links = Nil } case class SymbolChangedSemanticsInVersion( @@ -2012,6 +2166,7 @@ object messages { |between versions and the ${"-Xmigration"} settings is used to warn about constructs |whose behavior may have changed since version change.""" } + val links = Nil } case class UnableToEmitSwitch(tooFewCases: Boolean)(implicit ctx: Context) @@ -2042,6 +2197,7 @@ object messages { |- the matched value is not a constant literal |- there are less than three cases""" } + val links = Nil } case class MissingCompanionForStatic(member: Symbol)(implicit ctx: Context) extends Message(MissingCompanionForStaticID) { @@ -2049,6 +2205,7 @@ object messages { val kind = "Syntax" val explanation = hl"An object that contains ${"@static"} members must have a companion class." + val links = Nil } case class PolymorphicMethodMissingTypeInParent(rsym: Symbol, parentSym: Symbol)(implicit ctx: Context) @@ -2059,6 +2216,7 @@ object messages { hl"""Polymorphic $rsym is not allowed in the structural refinement of $parentSym because |$rsym does not override any method in $parentSym. Structural refinement does not allow for |polymorphic methods.""" + val links = Nil } case class ParamsNoInline(owner: Symbol)(implicit ctx: Context) @@ -2066,6 +2224,7 @@ object messages { val kind = "Syntax" val msg = hl"""${"inline"} modifier cannot be used for a ${owner.showKind} parameter""" val explanation = "" + val links = Nil } case class JavaSymbolIsNotAValue(symbol: Symbol)(implicit ctx: Context) extends Message(JavaSymbolIsNotAValueID) { @@ -2078,5 +2237,6 @@ object messages { s"$kind is not a value" } val explanation = "" + val links = Nil } } diff --git a/compiler/test/dotty/tools/dotc/reporting/ErrorMessagesTest.scala b/compiler/test/dotty/tools/dotc/reporting/ErrorMessagesTest.scala index 05280e9f7546..5eb7087aee06 100644 --- a/compiler/test/dotty/tools/dotc/reporting/ErrorMessagesTest.scala +++ b/compiler/test/dotty/tools/dotc/reporting/ErrorMessagesTest.scala @@ -44,9 +44,4 @@ trait ErrorMessagesTest extends DottyTest { } } - def assertMessageCount(expected: Int, messages: List[Message]): Unit = - assertEquals(messages.mkString, - expected, - messages.length - ) } diff --git a/compiler/test/dotty/tools/dotc/reporting/ErrorMessagesTests.scala b/compiler/test/dotty/tools/dotc/reporting/ErrorMessagesTests.scala index 25f1e8868c93..9e31b6ffaaca 100644 --- a/compiler/test/dotty/tools/dotc/reporting/ErrorMessagesTests.scala +++ b/compiler/test/dotty/tools/dotc/reporting/ErrorMessagesTests.scala @@ -3,15 +3,39 @@ package dotc package reporting import core.Contexts.Context +import diagnostic.Message import diagnostic.messages._ import dotty.tools.dotc.core.Flags import dotty.tools.dotc.core.Flags.FlagSet import dotty.tools.dotc.core.Types.WildcardType import dotty.tools.dotc.parsing.Tokens import org.junit.Assert._ -import org.junit.Test +import org.junit.{AfterClass, BeforeClass, Test} + +object ErrorMessagesTests extends UrlExistenceChecker { + @BeforeClass + def initConnectivityCheck(): Unit = checkForConnectivity() + + @AfterClass + def evaluateConnectivityCheck(): Unit = checkUrlResponses() +} class ErrorMessagesTests extends ErrorMessagesTest { + import ErrorMessagesTests.checkLinks + + private def checkDocumentationLinks(messages: List[Message]): Unit = + messages.foreach { message => + checkLinks(message.getClass.getSimpleName, message.links.map(_.url)) + } + + def assertMessageCount(expected: Int, messages: List[Message]): Unit = { + assertEquals(messages.mkString, + expected, + messages.length + ) + checkDocumentationLinks(messages) + } + // In the case where there are no errors, we can do "expectNoErrors" in the // `Report` @Test def noErrors = @@ -1220,6 +1244,7 @@ class ErrorMessagesTests extends ErrorMessagesTest { """.stripMargin }.expect { (ictx, messages) => implicit val ctx: Context = ictx + assertMessageCount(1, messages) val StaticFieldsOnlyAllowedInObjects(field) = messages.head assertEquals(field.show, "method bar") } @@ -1245,6 +1270,7 @@ class ErrorMessagesTests extends ErrorMessagesTest { """.stripMargin }.expect { (itcx, messages) => implicit val ctx: Context = itcx + assertMessageCount(1, messages) val MissingCompanionForStatic(member) = messages.head assertEquals(member.show, "method bar") } diff --git a/compiler/test/dotty/tools/dotc/reporting/TestMessageLaziness.scala b/compiler/test/dotty/tools/dotc/reporting/TestMessageLaziness.scala index 858660075bb2..fd490a309969 100644 --- a/compiler/test/dotty/tools/dotc/reporting/TestMessageLaziness.scala +++ b/compiler/test/dotty/tools/dotc/reporting/TestMessageLaziness.scala @@ -23,6 +23,7 @@ class TestMessageLaziness extends DottyTest { val kind = "Test" val msg = "Please don't blow up" val explanation = "" + val links = Nil } @Test def assureLazy = diff --git a/compiler/test/dotty/tools/dotc/reporting/UrlExistenceChecker.scala b/compiler/test/dotty/tools/dotc/reporting/UrlExistenceChecker.scala new file mode 100644 index 000000000000..88ddc8582b66 --- /dev/null +++ b/compiler/test/dotty/tools/dotc/reporting/UrlExistenceChecker.scala @@ -0,0 +1,103 @@ +package dotty.tools.dotc.reporting + +import java.net.{HttpURLConnection, SocketTimeoutException, URL, UnknownHostException} +import java.util.concurrent.atomic.AtomicBoolean + +import org.junit.Assert.fail + +import scala.concurrent.{Await, Future} +import scala.concurrent.duration.DurationInt +import scala.concurrent.ExecutionContext.Implicits.global +import scala.util.control.NonFatal + +/** Checks URL existence by HTTP requests expecting OK responses (200). + * + * To not fail if no network is available all URL tests are cancelled if in case + * "https://www.github.com/" is not available. + * + * URL testing can be switched off via system property `linkChecking=false`. + */ +trait UrlExistenceChecker { + + private[this] val runChecks = new AtomicBoolean(true) + private[this] val httpOk = 200 + + private[this] var checkingUrls: Set[String] = Set.empty + private[this] var checks: List[Future[String]] = Nil + private[this] val urlTimeout = 10.seconds + private[this] val urlTimeoutInt: Int = urlTimeout.toMillis.toInt + private[this] val systemProperty = "linkChecking" + + /** Tries to connect to "https://www.github.com/" and cancels URL testing if + * unavailable. + * Checks system property `linkChecking` be undefined or to contain `true`. + */ + def checkForConnectivity(): Unit = { + def noChecking(text: String = + "Warning: URL checking has been switched off as github.com can't be reached."): Unit = { + runChecks.set(false) + println(text) + } + + if (sys.props.getOrElse(systemProperty, "true") == "true") + Future { + try { + if (connectTo("https://www.github.com/", timeout = 2000) != httpOk) + noChecking() + } catch { + case NonFatal(_) => + noChecking() + } + } + else + noChecking( + s"Warning: URL checking has been switched off via system property `$systemProperty`.") + } + + def checkLinks(message: String, links: List[String]): Unit = + if (runChecks.get()) + links.foreach { url => + if (!checkingUrls.contains(url)) { + checkingUrls += url + checks = checkUrl(message, url) :: checks + } + } + + /** Awaits completion of all URL checks and fails the test if any has a non empty + * String as a result. + */ + def checkUrlResponses(): Unit = + if (runChecks.get()) { + val allFutures = Future.sequence(checks) + val results = Await.result(allFutures, urlTimeout + 2.seconds).filter(_.nonEmpty) + if (results.nonEmpty) + fail(s"""Some URLs did not get response OK (200): + | ${results.mkString("\n ")} + |(Switch URL checking off in sbt via `set javaOptions in Test += "-DlinkChecking=false"`)""".stripMargin) + } + + private def checkUrl(message: String, url: String) = Future { + try { + val response = connectTo(url) + if (response != httpOk) + s"$message: Got HTTP $response response from $url" + else "" + } catch { + case _: SocketTimeoutException => + s"$message: Connect timeout for $url" + case _: UnknownHostException => + s"$message: Unknown host for $url" + case NonFatal(e) => + s"$message: $e for $url" + } + } + + private def connectTo(url: String, timeout: Int = urlTimeoutInt): Int = { + val conn = new URL(url).openConnection().asInstanceOf[HttpURLConnection] + conn.setRequestMethod("HEAD") + conn.setConnectTimeout(timeout) + conn.setReadTimeout(timeout) + conn.getResponseCode + } + +} diff --git a/sbt-bridge/src/xsbt/DelegatingReporter.scala b/sbt-bridge/src/xsbt/DelegatingReporter.scala index ffc4792ecef0..bac19b6bec99 100644 --- a/sbt-bridge/src/xsbt/DelegatingReporter.scala +++ b/sbt-bridge/src/xsbt/DelegatingReporter.scala @@ -47,8 +47,9 @@ final class DelegatingReporter(delegate: xsbti.Reporter) extends Reporter val sb = new StringBuilder() sb.append(messageAndPos(cont.contained(), cont.pos, diagnosticLevel(cont))) - if (ctx.shouldExplain(cont) && cont.contained().explanation.nonEmpty) { + if (ctx.shouldExplain(cont)) { sb.append(explanation(cont.contained())) + if (cont.contained().links.nonEmpty) sb.append(documentationLinks(cont.contained())) } delegate.log(position, sb.toString(), severity)