From 2d7e18d8cbe212421ed05170942378d902b3946b Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Thu, 11 Sep 2025 14:02:49 -0700 Subject: [PATCH 1/5] Warn if empty template name has trailing colon This is to guard the edge case `object X_:` where the user may or may not have intended colon syntax. The next line does not tell us, since it may be indented yet not nested. Therefore, any empty template with a suspicious name will warn. Non-empty templates are given a pass even if written `object X_: :`. --- compiler/src/dotty/tools/dotc/ast/untpd.scala | 1 + .../src/dotty/tools/dotc/core/NameOps.scala | 2 +- .../dotty/tools/dotc/parsing/Parsers.scala | 39 +++++++++++++------ .../tools/dotc/reporting/ErrorMessageID.scala | 1 + .../dotty/tools/dotc/reporting/messages.scala | 14 ++++--- tests/neg/i16072.scala | 3 ++ tests/warn/i16072.check | 24 ++++++++++++ tests/warn/i16072.scala | 26 +++++++++++++ 8 files changed, 93 insertions(+), 17 deletions(-) create mode 100644 tests/neg/i16072.scala create mode 100644 tests/warn/i16072.check create mode 100644 tests/warn/i16072.scala diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index bad70cb3a01c..7b957f711555 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -44,6 +44,7 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { extends MemberDef { type ThisTree[+T <: Untyped] <: Trees.NameTree[T] & Trees.MemberDef[T] & ModuleDef def withName(name: Name)(using Context): ModuleDef = cpy.ModuleDef(this)(name.toTermName, impl) + def isBackquoted: Boolean = hasAttachment(Backquoted) } /** An untyped template with a derives clause. Derived parents are added to the end diff --git a/compiler/src/dotty/tools/dotc/core/NameOps.scala b/compiler/src/dotty/tools/dotc/core/NameOps.scala index f157da843f41..a433ed4375c6 100644 --- a/compiler/src/dotty/tools/dotc/core/NameOps.scala +++ b/compiler/src/dotty/tools/dotc/core/NameOps.scala @@ -89,7 +89,7 @@ object NameOps { // Ends with operator characters while i >= 0 && isOperatorPart(name(i)) do i -= 1 if i == -1 then return true - // Optionnally prefixed with alpha-numeric characters followed by `_` + // Optionally prefixed with alpha-numeric characters followed by `_` if name(i) != '_' then return false while i >= 0 && isIdentifierPart(name(i)) do i -= 1 i == -1 diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index f44703a562f1..21cf667f45e4 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -3862,6 +3862,18 @@ object Parsers { /* -------- DEFS ------------------------------------------- */ def finalizeDef(md: MemberDef, mods: Modifiers, start: Int): md.ThisTree[Untyped] = + def checkName(): Unit = + def checkName(name: Name): Unit = + if !name.isEmpty + && !Chars.isOperatorPart(name.firstCodePoint) // warn a_: not :: + && name.endsWith(":") + then + report.warning(AmbiguousTemplateName(md), md.namePos) + md match + case md @ TypeDef(name, impl: Template) if impl.body.isEmpty && !md.isBackquoted => checkName(name) + case md @ ModuleDef(name, impl) if impl.body.isEmpty && !md.isBackquoted => checkName(name) + case _ => + checkName() md.withMods(mods).setComment(in.getDocComment(start)) type ImportConstr = (Tree, List[ImportSelector]) => Tree @@ -4311,14 +4323,15 @@ object Parsers { /** ClassDef ::= id ClassConstr TemplateOpt */ - def classDef(start: Offset, mods: Modifiers): TypeDef = atSpan(start, nameStart) { - classDefRest(start, mods, ident().toTypeName) - } + def classDef(start: Offset, mods: Modifiers): TypeDef = + val td = atSpan(start, nameStart): + classDefRest(mods, ident().toTypeName) + finalizeDef(td, mods, start) - def classDefRest(start: Offset, mods: Modifiers, name: TypeName): TypeDef = + def classDefRest(mods: Modifiers, name: TypeName): TypeDef = val constr = classConstr(if mods.is(Case) then ParamOwner.CaseClass else ParamOwner.Class) val templ = templateOpt(constr) - finalizeDef(TypeDef(name, templ), mods, start) + TypeDef(name, templ) /** ClassConstr ::= [ClsTypeParamClause] [ConstrMods] ClsTermParamClauses */ @@ -4336,11 +4349,15 @@ object Parsers { /** ObjectDef ::= id TemplateOpt */ - def objectDef(start: Offset, mods: Modifiers): ModuleDef = atSpan(start, nameStart) { - val name = ident() - val templ = templateOpt(emptyConstructor) - finalizeDef(ModuleDef(name, templ), mods, start) - } + def objectDef(start: Offset, mods: Modifiers): ModuleDef = + val md = atSpan(start, nameStart): + val nameIdent = termIdent() + val templ = templateOpt(emptyConstructor) + ModuleDef(nameIdent.name.asTermName, templ) + .tap: md => + if nameIdent.isBackquoted then + md.pushAttachment(Backquoted, ()) + finalizeDef(md, mods, start) private def checkAccessOnly(mods: Modifiers, caseStr: String): Modifiers = // We allow `infix` and `into` on `enum` definitions. @@ -4572,7 +4589,7 @@ object Parsers { Template(constr, parents, Nil, EmptyValDef, Nil) else if !newSyntaxAllowed || in.token == WITH && tparams.isEmpty && vparamss.isEmpty - // if new syntax is still allowed and there are parameters, they mist be new style conditions, + // if new syntax is still allowed and there are parameters, they must be new style conditions, // so old with-style syntax would not be allowed. then withTemplate(constr, parents) diff --git a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala index 5f5a0c01db17..169e33011f9a 100644 --- a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala +++ b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala @@ -236,6 +236,7 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe case DefaultShadowsGivenID // errorNumber: 220 case RecurseWithDefaultID // errorNumber: 221 case EncodedPackageNameID // errorNumber: 222 + case AmbiguousTemplateNameID // errorNumber: 223 def errorNumber = ordinal - 1 diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index a2afaab0ecce..cd6469a5913f 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -13,8 +13,10 @@ import parsing.Tokens import printing.Highlighting.* import printing.Formatting import ErrorMessageID.* -import ast.Trees +import ast.Trees.* import ast.desugar +import ast.tpd +import ast.untpd import config.{Feature, MigrationVersion, ScalaVersion} import transform.patmat.Space import transform.patmat.SpaceEngine @@ -25,9 +27,6 @@ import typer.Inferencing import scala.util.control.NonFatal import StdNames.nme import Formatting.{hl, delay} -import ast.Trees.* -import ast.untpd -import ast.tpd import scala.util.matching.Regex import java.util.regex.Matcher.quoteReplacement import cc.CaptureSet.IdentityCaptRefMap @@ -3097,7 +3096,7 @@ class MissingImplicitArgument( def msg(using Context): String = def formatMsg(shortForm: String)(headline: String = shortForm) = arg match - case arg: Trees.SearchFailureIdent[?] => + case arg: SearchFailureIdent[?] => arg.tpe match case _: NoMatchingImplicits => headline case tpe: SearchFailureType => @@ -3741,3 +3740,8 @@ final class EncodedPackageName(name: Name)(using Context) extends SyntaxMsg(Enco |or `myfile-test.scala` can produce encoded names for the generated package objects. | |In this case, the name `$name` is encoded as `${name.encode}`.""" + +class AmbiguousTemplateName(tree: NamedDefTree[?])(using Context) extends SyntaxMsg(AmbiguousTemplateNameID): + override protected def msg(using Context) = i"name `${tree.name}` should be enclosed in backticks" + override protected def explain(using Context): String = + "Names with trailing operator characters may fuse with a subsequent colon if not set off by backquotes or spaces." diff --git a/tests/neg/i16072.scala b/tests/neg/i16072.scala new file mode 100644 index 000000000000..870a9710c9b9 --- /dev/null +++ b/tests/neg/i16072.scala @@ -0,0 +1,3 @@ + +enum Oops_: + case Z // error // error expected { and } diff --git a/tests/warn/i16072.check b/tests/warn/i16072.check new file mode 100644 index 000000000000..decc43f99146 --- /dev/null +++ b/tests/warn/i16072.check @@ -0,0 +1,24 @@ +-- Warning: tests/warn/i16072.scala:4:2 -------------------------------------------------------------------------------- +4 | def x = 1 // warn too far right + | ^ + | Line is indented too far to the right, or a `{` or `:` is missing +-- [E222] Syntax Warning: tests/warn/i16072.scala:3:7 ------------------------------------------------------------------ +3 |object Hello_: // warn colon in name without backticks because the body is empty + | ^^^^^^^ + | name `Hello_:` should be enclosed in backticks + | + | longer explanation available when compiling with `-explain` +-- Deprecation Warning: tests/warn/i16072.scala:12:10 ------------------------------------------------------------------ +12 |object :: : // warn deprecated colon without backticks for operator name + | ^ + | `:` after symbolic operator is deprecated; use backticks around operator instead +-- Warning: tests/warn/i16072.scala:21:2 ------------------------------------------------------------------------------- +21 | def y = 1 // warn + | ^ + | Line is indented too far to the right, or a `{` or `:` is missing +-- [E222] Syntax Warning: tests/warn/i16072.scala:20:6 ----------------------------------------------------------------- +20 |class Uhoh_: // warn + | ^^^^^^ + | name `Uhoh_:` should be enclosed in backticks + | + | longer explanation available when compiling with `-explain` diff --git a/tests/warn/i16072.scala b/tests/warn/i16072.scala new file mode 100644 index 000000000000..7bfdbbe3813d --- /dev/null +++ b/tests/warn/i16072.scala @@ -0,0 +1,26 @@ +//> using options -deprecation + +object Hello_: // warn colon in name without backticks because the body is empty + def x = 1 // warn too far right + +object Goodbye_: : // nowarn if non-empty body without nit-picking about backticks + def x = 2 + +object `Byte_`: + def x = 3 + +object :: : // warn deprecated colon without backticks for operator name + def x = 42 + +object ::: // nowarn + +object Braces_: { // nowarn because body is non-empty with an EmptyTree +} + +class Uhoh_: // warn + def y = 1 // warn + +@main def hello = + println(Byte_) + println(Hello_:) // apparently user did forget a colon, see https://youforgotapercentagesignoracolon.com/ + println(x) From 5ecf66944f43c2edaaff1ed701452a1401c27db3 Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Thu, 11 Sep 2025 17:36:40 -0700 Subject: [PATCH 2/5] Improve message for colon fusion When the user accidentally writes `val x_: Int` where the colon belongs to the identifier as an operator suffix, tell them so. --- .../dotty/tools/dotc/parsing/Parsers.scala | 9 ++++++- tests/neg/i18020b.check | 26 +++++++++++++++++++ tests/neg/i18020b.scala | 8 ++++++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 tests/neg/i18020b.check create mode 100644 tests/neg/i18020b.scala diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 21cf667f45e4..1ff0033dde75 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -4086,7 +4086,14 @@ object Parsers { val tpt = typedOpt() val rhs = if tpt.isEmpty || in.token == EQUALS then - accept(EQUALS) + if tpt.isEmpty && in.token != EQUALS then + lhs match + case Ident(name) :: Nil if name.endsWith(":") => + val help = i"; identifier ends in colon, did you mean `${name.toSimpleName.dropRight(1)}`: in backticks?" + syntaxErrorOrIncomplete(ExpectedTokenButFound(EQUALS, in.token, suffix = help)) + case _ => accept(EQUALS) + else + accept(EQUALS) val rhsOffset = in.offset subExpr() match case rhs0 @ Ident(name) if placeholderParams.nonEmpty && name == placeholderParams.head.name diff --git a/tests/neg/i18020b.check b/tests/neg/i18020b.check new file mode 100644 index 000000000000..64bfe981d7c3 --- /dev/null +++ b/tests/neg/i18020b.check @@ -0,0 +1,26 @@ +-- [E040] Syntax Error: tests/neg/i18020b.scala:2:17 ------------------------------------------------------------------- +2 |class i18020(a_: Int): // error + | ^^^ + | ':' expected, but identifier found +-- [E040] Syntax Error: tests/neg/i18020b.scala:3:12 ------------------------------------------------------------------- +3 | def f(b_: Int) = 42 // error + | ^^^ + | ':' expected, but identifier found +-- [E040] Syntax Error: tests/neg/i18020b.scala:4:10 ------------------------------------------------------------------- +4 | def g_: Int = 27 // error + | ^^^ + | '=' expected, but identifier found +-- [E040] Syntax Error: tests/neg/i18020b.scala:6:12 ------------------------------------------------------------------- +6 | val x_: Int = 1 // error + | ^^^ + | '=' expected, but identifier found; identifier ends in colon, did you mean `x_`: in backticks? +-- [E040] Syntax Error: tests/neg/i18020b.scala:7:12 ------------------------------------------------------------------- +7 | val y_: Int = 2 // error + | ^^^ + | '=' expected, but identifier found; identifier ends in colon, did you mean `y_`: in backticks? +-- [E006] Not Found Error: tests/neg/i18020b.scala:8:4 ----------------------------------------------------------------- +8 | x_ + y_ // error + | ^^ + | Not found: x_ - did you mean x_:? + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/i18020b.scala b/tests/neg/i18020b.scala new file mode 100644 index 000000000000..76aff27380f9 --- /dev/null +++ b/tests/neg/i18020b.scala @@ -0,0 +1,8 @@ +// problems with colon fusion, a harder challenge than cold fusion +class i18020(a_: Int): // error + def f(b_: Int) = 42 // error + def g_: Int = 27 // error + def k = + val x_: Int = 1 // error + val y_: Int = 2 // error + x_ + y_ // error From 53a472e303850ba5b212e197024df499c74d2742 Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Thu, 11 Sep 2025 17:53:23 -0700 Subject: [PATCH 3/5] Help with defdef --- compiler/src/dotty/tools/dotc/parsing/Parsers.scala | 4 ++++ tests/neg/i18020b.check | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 1ff0033dde75..fb3a0f91cdc0 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -4191,6 +4191,10 @@ object Parsers { tpt = scalaUnit if (in.token == LBRACE) expr() else EmptyTree + else if in.token == IDENTIFIER && paramss.isEmpty && name.endsWith(":") then + val help = i"; identifier ends in colon, did you mean `${name.toSimpleName.dropRight(1)}`: in backticks?" + syntaxErrorOrIncomplete(ExpectedTokenButFound(EQUALS, in.token, suffix = help)) + EmptyTree else if (!isExprIntro) syntaxError(MissingReturnType(), in.lastOffset) accept(EQUALS) diff --git a/tests/neg/i18020b.check b/tests/neg/i18020b.check index 64bfe981d7c3..1e2d22cacf99 100644 --- a/tests/neg/i18020b.check +++ b/tests/neg/i18020b.check @@ -9,7 +9,7 @@ -- [E040] Syntax Error: tests/neg/i18020b.scala:4:10 ------------------------------------------------------------------- 4 | def g_: Int = 27 // error | ^^^ - | '=' expected, but identifier found + | '=' expected, but identifier found; identifier ends in colon, did you mean `g_`: in backticks? -- [E040] Syntax Error: tests/neg/i18020b.scala:6:12 ------------------------------------------------------------------- 6 | val x_: Int = 1 // error | ^^^ From 957c8be738ca6d7523d9c0f6d20b2d64ec5c2bf3 Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Fri, 19 Sep 2025 14:34:16 -0700 Subject: [PATCH 4/5] Promote to overloaded accept --- compiler/src/dotty/tools/dotc/parsing/Parsers.scala | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index fb3a0f91cdc0..d23f26769fba 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -328,6 +328,13 @@ object Parsers { if in.token == token then in.nextToken() offset + def accept(token: Int, help: String): Int = + val offset = in.offset + if in.token != token then + syntaxErrorOrIncomplete(ExpectedTokenButFound(token, in.token, suffix = help)) + if in.token == token then in.nextToken() + offset + def accept(name: Name): Int = { val offset = in.offset if !isIdent(name) then @@ -4090,7 +4097,7 @@ object Parsers { lhs match case Ident(name) :: Nil if name.endsWith(":") => val help = i"; identifier ends in colon, did you mean `${name.toSimpleName.dropRight(1)}`: in backticks?" - syntaxErrorOrIncomplete(ExpectedTokenButFound(EQUALS, in.token, suffix = help)) + accept(EQUALS, help) case _ => accept(EQUALS) else accept(EQUALS) @@ -4193,7 +4200,7 @@ object Parsers { else EmptyTree else if in.token == IDENTIFIER && paramss.isEmpty && name.endsWith(":") then val help = i"; identifier ends in colon, did you mean `${name.toSimpleName.dropRight(1)}`: in backticks?" - syntaxErrorOrIncomplete(ExpectedTokenButFound(EQUALS, in.token, suffix = help)) + accept(EQUALS, help) EmptyTree else if (!isExprIntro) syntaxError(MissingReturnType(), in.lastOffset) From 7874d127661345a2b5a27aa2c7ed95b9552f7f49 Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Fri, 19 Sep 2025 16:17:40 -0700 Subject: [PATCH 5/5] Indentation warning is a syntax warning --- .../dotty/tools/dotc/parsing/Parsers.scala | 6 ++---- .../dotty/tools/dotc/parsing/Scanners.scala | 4 ++-- .../tools/dotc/reporting/ErrorMessageID.scala | 1 + .../dotty/tools/dotc/reporting/messages.scala | 19 +++++++++++++++---- tests/warn/i16072.check | 16 ++++++++++------ 5 files changed, 30 insertions(+), 16 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index d23f26769fba..25452a0094bb 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -712,9 +712,7 @@ object Parsers { if in.isNewLine && !(nextIndentWidth < startIndentWidth) then warning( if startIndentWidth <= nextIndentWidth then - em"""Line is indented too far to the right, or a `{` is missing before: - | - |${t.tryToShow}""" + IndentationWarning(missing = LBRACE, before = t.tryToShow) else in.spaceTabMismatchMsg(startIndentWidth, nextIndentWidth), in.next.offset @@ -729,7 +727,7 @@ object Parsers { if in.isNewLine then val nextIndentWidth = in.indentWidth(in.next.offset) if in.currentRegion.indentWidth < nextIndentWidth && in.currentRegion.closedBy == OUTDENT then - warning(em"Line is indented too far to the right, or a `{` or `:` is missing", in.next.offset) + warning(IndentationWarning(missing = Seq(LBRACE, COLONop)*), in.next.offset) /* -------- REWRITES ----------------------------------------------------------- */ diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index ec246f7a3742..8887ab1b6944 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -20,7 +20,7 @@ import config.Feature import config.Feature.{migrateTo3, sourceVersion} import config.SourceVersion.{`3.0`, `3.0-migration`} import config.MigrationVersion -import reporting.{NoProfile, Profile, Message} +import reporting.* import java.util.Objects import dotty.tools.dotc.reporting.Message.rewriteNotice @@ -652,7 +652,7 @@ object Scanners { if r.enclosing.isClosedByUndentAt(nextWidth) then insert(OUTDENT, offset) else if r.isInstanceOf[InBraces] && !closingRegionTokens.contains(token) then - report.warning("Line is indented too far to the left, or a `}` is missing", sourcePos()) + report.warning(IndentationWarning(isLeft = true, missing = RBRACE), sourcePos()) else if lastWidth < nextWidth || lastWidth == nextWidth && (lastToken == MATCH || lastToken == CATCH) && token == CASE then if canStartIndentTokens.contains(lastToken) then diff --git a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala index 169e33011f9a..3056f8109b47 100644 --- a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala +++ b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala @@ -237,6 +237,7 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe case RecurseWithDefaultID // errorNumber: 221 case EncodedPackageNameID // errorNumber: 222 case AmbiguousTemplateNameID // errorNumber: 223 + case IndentationWarningID // errorNumber: 224 def errorNumber = ordinal - 1 diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index cd6469a5913f..cfc81f441ca5 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -9,7 +9,7 @@ import Denotations.SingleDenotation import SymDenotations.SymDenotation import NameKinds.{WildcardParamName, ContextFunctionParamName} import parsing.Scanners.Token -import parsing.Tokens +import parsing.Tokens, Tokens.showToken import printing.Highlighting.* import printing.Formatting import ErrorMessageID.* @@ -1232,12 +1232,12 @@ extends ReferenceMsg(ForwardReferenceExtendsOverDefinitionID) { class ExpectedTokenButFound(expected: Token, found: Token, prefix: String = "", suffix: String = "")(using Context) extends SyntaxMsg(ExpectedTokenButFoundID) { - private def foundText = Tokens.showToken(found) + private def foundText = showToken(found) def msg(using Context) = val expectedText = if (Tokens.isIdentifier(expected)) "an identifier" - else Tokens.showToken(expected) + else showToken(expected) i"""$prefix$expectedText expected, but $foundText found$suffix""" def explain(using Context) = @@ -1928,7 +1928,7 @@ class ExtendFinalClass(clazz:Symbol, finalClazz: Symbol)(using Context) class ExpectedTypeBoundOrEquals(found: Token)(using Context) extends SyntaxMsg(ExpectedTypeBoundOrEqualsID) { - def msg(using Context) = i"${hl("=")}, ${hl(">:")}, or ${hl("<:")} expected, but ${Tokens.showToken(found)} found" + def msg(using Context) = i"${hl("=")}, ${hl(">:")}, or ${hl("<:")} expected, but ${showToken(found)} found" def explain(using Context) = i"""Type parameters and abstract types may be constrained by a type bound. @@ -3745,3 +3745,14 @@ class AmbiguousTemplateName(tree: NamedDefTree[?])(using Context) extends Syntax override protected def msg(using Context) = i"name `${tree.name}` should be enclosed in backticks" override protected def explain(using Context): String = "Names with trailing operator characters may fuse with a subsequent colon if not set off by backquotes or spaces." + +class IndentationWarning(isLeft: Boolean = false, before: String = "", missing: Token*)(using Context) +extends SyntaxMsg(IndentationWarningID): + override protected def msg(using Context) = + s"Line is indented too far to the ${if isLeft then "left" else "right"}, or a ${ + missing.map(showToken).mkString(" or ") + } is missing${ + if !before.isEmpty then i" before:\n\n$before" else "" + }" + override protected def explain(using Context): String = + "Indentation that does not reflect syntactic nesting may be due to a typo such as missing punctuation." diff --git a/tests/warn/i16072.check b/tests/warn/i16072.check index decc43f99146..770f87997726 100644 --- a/tests/warn/i16072.check +++ b/tests/warn/i16072.check @@ -1,8 +1,10 @@ --- Warning: tests/warn/i16072.scala:4:2 -------------------------------------------------------------------------------- +-- [E224] Syntax Warning: tests/warn/i16072.scala:4:2 ------------------------------------------------------------------ 4 | def x = 1 // warn too far right | ^ - | Line is indented too far to the right, or a `{` or `:` is missing --- [E222] Syntax Warning: tests/warn/i16072.scala:3:7 ------------------------------------------------------------------ + | Line is indented too far to the right, or a '{' or ':' is missing + | + | longer explanation available when compiling with `-explain` +-- [E223] Syntax Warning: tests/warn/i16072.scala:3:7 ------------------------------------------------------------------ 3 |object Hello_: // warn colon in name without backticks because the body is empty | ^^^^^^^ | name `Hello_:` should be enclosed in backticks @@ -12,11 +14,13 @@ 12 |object :: : // warn deprecated colon without backticks for operator name | ^ | `:` after symbolic operator is deprecated; use backticks around operator instead --- Warning: tests/warn/i16072.scala:21:2 ------------------------------------------------------------------------------- +-- [E224] Syntax Warning: tests/warn/i16072.scala:21:2 ----------------------------------------------------------------- 21 | def y = 1 // warn | ^ - | Line is indented too far to the right, or a `{` or `:` is missing --- [E222] Syntax Warning: tests/warn/i16072.scala:20:6 ----------------------------------------------------------------- + | Line is indented too far to the right, or a '{' or ':' is missing + | + | longer explanation available when compiling with `-explain` +-- [E223] Syntax Warning: tests/warn/i16072.scala:20:6 ----------------------------------------------------------------- 20 |class Uhoh_: // warn | ^^^^^^ | name `Uhoh_:` should be enclosed in backticks