diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index 89933fcab8a2..bf52f24b75b2 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 766cf4abf8c4..318eeab1a1cd 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 77369c828113..2442af1ce429 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 @@ -703,9 +710,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 @@ -720,7 +725,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 ----------------------------------------------------------- */ @@ -1523,7 +1528,7 @@ object Parsers { if MigrationVersion.Scala2to3.needsPatch then patch(source, Span(in.offset), " ") - def possibleTemplateStart(isNew: Boolean = false): Unit = + def possibleTemplateStart(): Unit = in.observeColonEOL(inTemplate = true) if in.token == COLONeol then if in.lookahead.token == END then in.token = NEWLINE @@ -2866,7 +2871,7 @@ object Parsers { val parents = if in.isNestedStart then Nil else constrApps(exclude = COMMA) - possibleTemplateStart(isNew = true) + possibleTemplateStart() parents match { case parent :: Nil if !in.isNestedStart => reposition(if (parent.isType) ensureApplied(wrapNew(parent)) else parent) @@ -3784,6 +3789,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 @@ -3996,7 +4013,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 @@ -4094,6 +4118,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) @@ -4233,14 +4261,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 */ @@ -4258,11 +4287,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. @@ -4494,7 +4527,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 52e03de60dea..71fdb7d829dc 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 @@ -650,7 +650,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 103687abdbff..f2118e7acae7 100644 --- a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala +++ b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala @@ -235,6 +235,8 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe case CannotInstantiateQuotedTypeVarID // errorNumber: 219 case DefaultShadowsGivenID // errorNumber: 220 case RecurseWithDefaultID // errorNumber: 221 + case AmbiguousTemplateNameID // errorNumber: 222 + case IndentationWarningID // 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 2bad86f8967b..55bb2801f899 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 @@ -1234,12 +1233,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) = @@ -1941,7 +1940,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. @@ -3109,7 +3108,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 RecurseWithDefault(name: Name)(using Context) extends TypeMsg(Recurs i"Recursive call used a default argument for parameter $name." override protected def explain(using Context): String = "It's more explicit to pass current or modified arguments in a recursion." + +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..542a7c046cc1 --- /dev/null +++ b/tests/warn/i16072.check @@ -0,0 +1,28 @@ +-- [E223] 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` +-- [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 +-- [E223] 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` +-- [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)