Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/ast/untpd.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/core/NameOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
69 changes: 51 additions & 18 deletions compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 ----------------------------------------------------------- */

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
*/
Expand All @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/parsing/Scanners.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
33 changes: 24 additions & 9 deletions compiler/src/dotty/tools/dotc/reporting/messages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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) =
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 =>
Expand Down Expand Up @@ -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."
3 changes: 3 additions & 0 deletions tests/neg/i16072.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

enum Oops_:
case Z // error // error expected { and }
26 changes: 26 additions & 0 deletions tests/neg/i18020b.check
Original file line number Diff line number Diff line change
@@ -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`
8 changes: 8 additions & 0 deletions tests/neg/i18020b.scala
Original file line number Diff line number Diff line change
@@ -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
28 changes: 28 additions & 0 deletions tests/warn/i16072.check
Original file line number Diff line number Diff line change
@@ -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`
26 changes: 26 additions & 0 deletions tests/warn/i16072.scala
Original file line number Diff line number Diff line change
@@ -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)
Loading