Skip to content

Commit 26646c6

Browse files
committed
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_: :`.
1 parent ac337b8 commit 26646c6

File tree

7 files changed

+88
-16
lines changed

7 files changed

+88
-16
lines changed

compiler/src/dotty/tools/dotc/ast/untpd.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
4444
extends MemberDef {
4545
type ThisTree[+T <: Untyped] <: Trees.NameTree[T] & Trees.MemberDef[T] & ModuleDef
4646
def withName(name: Name)(using Context): ModuleDef = cpy.ModuleDef(this)(name.toTermName, impl)
47+
def isBackquoted: Boolean = hasAttachment(Backquoted)
4748
}
4849

4950
/** An untyped template with a derives clause. Derived parents are added to the end

compiler/src/dotty/tools/dotc/core/NameOps.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ object NameOps {
8989
// Ends with operator characters
9090
while i >= 0 && isOperatorPart(name(i)) do i -= 1
9191
if i == -1 then return true
92-
// Optionnally prefixed with alpha-numeric characters followed by `_`
92+
// Optionally prefixed with alpha-numeric characters followed by `_`
9393
if name(i) != '_' then return false
9494
while i >= 0 && isIdentifierPart(name(i)) do i -= 1
9595
i == -1

compiler/src/dotty/tools/dotc/parsing/Parsers.scala

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3784,6 +3784,17 @@ object Parsers {
37843784
/* -------- DEFS ------------------------------------------- */
37853785

37863786
def finalizeDef(md: MemberDef, mods: Modifiers, start: Int): md.ThisTree[Untyped] =
3787+
def checkName(): Unit =
3788+
def checkName(name: Name): Unit =
3789+
if !Chars.isOperatorPart(name.firstCodePoint) // warn a_: not ::
3790+
&& name.endsWith(":")
3791+
then
3792+
report.warning(AmbiguousTemplateName(md), md.namePos)
3793+
md match
3794+
case md @ TypeDef(name, impl: Template) if impl.body.isEmpty && !md.isBackquoted => checkName(name)
3795+
case md @ ModuleDef(name, impl) if impl.body.isEmpty && !md.isBackquoted => checkName(name)
3796+
case _ =>
3797+
checkName()
37873798
md.withMods(mods).setComment(in.getDocComment(start))
37883799

37893800
type ImportConstr = (Tree, List[ImportSelector]) => Tree
@@ -4233,14 +4244,15 @@ object Parsers {
42334244

42344245
/** ClassDef ::= id ClassConstr TemplateOpt
42354246
*/
4236-
def classDef(start: Offset, mods: Modifiers): TypeDef = atSpan(start, nameStart) {
4237-
classDefRest(start, mods, ident().toTypeName)
4238-
}
4247+
def classDef(start: Offset, mods: Modifiers): TypeDef =
4248+
val td = atSpan(start, nameStart):
4249+
classDefRest(mods, ident().toTypeName)
4250+
finalizeDef(td, mods, start)
42394251

4240-
def classDefRest(start: Offset, mods: Modifiers, name: TypeName): TypeDef =
4252+
def classDefRest(mods: Modifiers, name: TypeName): TypeDef =
42414253
val constr = classConstr(if mods.is(Case) then ParamOwner.CaseClass else ParamOwner.Class)
42424254
val templ = templateOpt(constr)
4243-
finalizeDef(TypeDef(name, templ), mods, start)
4255+
TypeDef(name, templ)
42444256

42454257
/** ClassConstr ::= [ClsTypeParamClause] [ConstrMods] ClsTermParamClauses
42464258
*/
@@ -4258,11 +4270,15 @@ object Parsers {
42584270

42594271
/** ObjectDef ::= id TemplateOpt
42604272
*/
4261-
def objectDef(start: Offset, mods: Modifiers): ModuleDef = atSpan(start, nameStart) {
4262-
val name = ident()
4263-
val templ = templateOpt(emptyConstructor)
4264-
finalizeDef(ModuleDef(name, templ), mods, start)
4265-
}
4273+
def objectDef(start: Offset, mods: Modifiers): ModuleDef =
4274+
val md = atSpan(start, nameStart):
4275+
val nameIdent = termIdent()
4276+
val templ = templateOpt(emptyConstructor)
4277+
ModuleDef(nameIdent.name.asTermName, templ)
4278+
.tap: md =>
4279+
if nameIdent.isBackquoted then
4280+
md.pushAttachment(Backquoted, ())
4281+
finalizeDef(md, mods, start)
42664282

42674283
private def checkAccessOnly(mods: Modifiers, caseStr: String): Modifiers =
42684284
// We allow `infix` and `into` on `enum` definitions.

compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe
235235
case CannotInstantiateQuotedTypeVarID // errorNumber: 219
236236
case DefaultShadowsGivenID // errorNumber: 220
237237
case RecurseWithDefaultID // errorNumber: 221
238+
case AmbiguousTemplateNameID // errorNumber: 222
238239

239240
def errorNumber = ordinal - 1
240241

compiler/src/dotty/tools/dotc/reporting/messages.scala

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ import parsing.Tokens
1313
import printing.Highlighting.*
1414
import printing.Formatting
1515
import ErrorMessageID.*
16-
import ast.Trees
16+
import ast.Trees.*
1717
import ast.desugar
18+
import ast.tpd
19+
import ast.untpd
1820
import config.{Feature, MigrationVersion, ScalaVersion}
1921
import transform.patmat.Space
2022
import transform.patmat.SpaceEngine
@@ -25,9 +27,6 @@ import typer.Inferencing
2527
import scala.util.control.NonFatal
2628
import StdNames.nme
2729
import Formatting.{hl, delay}
28-
import ast.Trees.*
29-
import ast.untpd
30-
import ast.tpd
3130
import scala.util.matching.Regex
3231
import java.util.regex.Matcher.quoteReplacement
3332
import cc.CaptureSet.IdentityCaptRefMap
@@ -3109,7 +3108,7 @@ class MissingImplicitArgument(
31093108
def msg(using Context): String =
31103109

31113110
def formatMsg(shortForm: String)(headline: String = shortForm) = arg match
3112-
case arg: Trees.SearchFailureIdent[?] =>
3111+
case arg: SearchFailureIdent[?] =>
31133112
arg.tpe match
31143113
case _: NoMatchingImplicits => headline
31153114
case tpe: SearchFailureType =>
@@ -3741,3 +3740,8 @@ final class RecurseWithDefault(name: Name)(using Context) extends TypeMsg(Recurs
37413740
i"Recursive call used a default argument for parameter $name."
37423741
override protected def explain(using Context): String =
37433742
"It's more explicit to pass current or modified arguments in a recursion."
3743+
3744+
class AmbiguousTemplateName(tree: NamedDefTree[?])(using Context) extends SyntaxMsg(AmbiguousTemplateNameID):
3745+
override protected def msg(using Context) = i"name `${tree.name}` should be enclosed in backticks"
3746+
override protected def explain(using Context): String =
3747+
"Names with trailing operator characters may fuse with a subsequent colon if not set off by backquotes or spaces."

tests/warn/i16072.check

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
-- Warning: tests/warn/i16072.scala:4:2 --------------------------------------------------------------------------------
2+
4 | def x = 1 // warn too far right
3+
| ^
4+
| Line is indented too far to the right, or a `{` or `:` is missing
5+
-- [E222] Syntax Warning: tests/warn/i16072.scala:3:7 ------------------------------------------------------------------
6+
3 |object Hello_: // warn colon in name without backticks because the body is empty
7+
| ^^^^^^^
8+
| name `Hello_:` should be enclosed in backticks
9+
|
10+
| longer explanation available when compiling with `-explain`
11+
-- Deprecation Warning: tests/warn/i16072.scala:12:10 ------------------------------------------------------------------
12+
12 |object :: : // warn deprecated colon without backticks for operator name
13+
| ^
14+
| `:` after symbolic operator is deprecated; use backticks around operator instead
15+
-- Warning: tests/warn/i16072.scala:21:2 -------------------------------------------------------------------------------
16+
21 | def y = 1 // warn
17+
| ^
18+
| Line is indented too far to the right, or a `{` or `:` is missing
19+
-- [E222] Syntax Warning: tests/warn/i16072.scala:20:6 -----------------------------------------------------------------
20+
20 |class Uhoh_: // warn
21+
| ^^^^^^
22+
| name `Uhoh_:` should be enclosed in backticks
23+
|
24+
| longer explanation available when compiling with `-explain`

tests/warn/i16072.scala

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//> using options -deprecation
2+
3+
object Hello_: // warn colon in name without backticks because the body is empty
4+
def x = 1 // warn too far right
5+
6+
object Goodbye_: : // nowarn if non-empty body without nit-picking about backticks
7+
def x = 2
8+
9+
object `Byte_`:
10+
def x = 3
11+
12+
object :: : // warn deprecated colon without backticks for operator name
13+
def x = 42
14+
15+
object ::: // nowarn
16+
17+
object Braces_: { // nowarn because body is non-empty with an EmptyTree
18+
}
19+
20+
class Uhoh_: // warn
21+
def y = 1 // warn
22+
23+
@main def hello =
24+
println(Byte_)
25+
println(Hello_:) // apparently user did forget a colon, see https://youforgotapercentagesignoracolon.com/
26+
println(x)

0 commit comments

Comments
 (0)