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..25452a0094bb 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 @@ -705,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 @@ -722,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 ----------------------------------------------------------- */ @@ -3862,6 +3867,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 @@ -4074,7 +4091,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?" + accept(EQUALS, help) + case _ => accept(EQUALS) + else + accept(EQUALS) val rhsOffset = in.offset subExpr() match case rhs0 @ Ident(name) if placeholderParams.nonEmpty && name == placeholderParams.head.name @@ -4172,6 +4196,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?" + accept(EQUALS, help) + EmptyTree else if (!isExprIntro) syntaxError(MissingReturnType(), in.lastOffset) accept(EQUALS) @@ -4311,14 +4339,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 +4365,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 +4605,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/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 5f5a0c01db17..3056f8109b47 100644 --- a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala +++ b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala @@ -236,6 +236,8 @@ 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 + 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 a2afaab0ecce..cfc81f441ca5 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -9,12 +9,14 @@ 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.* -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 @@ -1233,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) = @@ -1929,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. @@ -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,19 @@ 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." + +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/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/neg/i18020b.check b/tests/neg/i18020b.check new file mode 100644 index 000000000000..1e2d22cacf99 --- /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; 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 + | ^^^ + | '=' 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 diff --git a/tests/warn/i16072.check b/tests/warn/i16072.check new file mode 100644 index 000000000000..770f87997726 --- /dev/null +++ b/tests/warn/i16072.check @@ -0,0 +1,28 @@ +-- [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 + | + | 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 + | + | 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 +-- [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 + | + | 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 + | + | 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)