Skip to content

Commit 2d7e18d

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 a374c1d commit 2d7e18d

File tree

8 files changed

+93
-17
lines changed

8 files changed

+93
-17
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: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3862,6 +3862,18 @@ object Parsers {
38623862
/* -------- DEFS ------------------------------------------- */
38633863

38643864
def finalizeDef(md: MemberDef, mods: Modifiers, start: Int): md.ThisTree[Untyped] =
3865+
def checkName(): Unit =
3866+
def checkName(name: Name): Unit =
3867+
if !name.isEmpty
3868+
&& !Chars.isOperatorPart(name.firstCodePoint) // warn a_: not ::
3869+
&& name.endsWith(":")
3870+
then
3871+
report.warning(AmbiguousTemplateName(md), md.namePos)
3872+
md match
3873+
case md @ TypeDef(name, impl: Template) if impl.body.isEmpty && !md.isBackquoted => checkName(name)
3874+
case md @ ModuleDef(name, impl) if impl.body.isEmpty && !md.isBackquoted => checkName(name)
3875+
case _ =>
3876+
checkName()
38653877
md.withMods(mods).setComment(in.getDocComment(start))
38663878

38673879
type ImportConstr = (Tree, List[ImportSelector]) => Tree
@@ -4311,14 +4323,15 @@ object Parsers {
43114323

43124324
/** ClassDef ::= id ClassConstr TemplateOpt
43134325
*/
4314-
def classDef(start: Offset, mods: Modifiers): TypeDef = atSpan(start, nameStart) {
4315-
classDefRest(start, mods, ident().toTypeName)
4316-
}
4326+
def classDef(start: Offset, mods: Modifiers): TypeDef =
4327+
val td = atSpan(start, nameStart):
4328+
classDefRest(mods, ident().toTypeName)
4329+
finalizeDef(td, mods, start)
43174330

4318-
def classDefRest(start: Offset, mods: Modifiers, name: TypeName): TypeDef =
4331+
def classDefRest(mods: Modifiers, name: TypeName): TypeDef =
43194332
val constr = classConstr(if mods.is(Case) then ParamOwner.CaseClass else ParamOwner.Class)
43204333
val templ = templateOpt(constr)
4321-
finalizeDef(TypeDef(name, templ), mods, start)
4334+
TypeDef(name, templ)
43224335

43234336
/** ClassConstr ::= [ClsTypeParamClause] [ConstrMods] ClsTermParamClauses
43244337
*/
@@ -4336,11 +4349,15 @@ object Parsers {
43364349

43374350
/** ObjectDef ::= id TemplateOpt
43384351
*/
4339-
def objectDef(start: Offset, mods: Modifiers): ModuleDef = atSpan(start, nameStart) {
4340-
val name = ident()
4341-
val templ = templateOpt(emptyConstructor)
4342-
finalizeDef(ModuleDef(name, templ), mods, start)
4343-
}
4352+
def objectDef(start: Offset, mods: Modifiers): ModuleDef =
4353+
val md = atSpan(start, nameStart):
4354+
val nameIdent = termIdent()
4355+
val templ = templateOpt(emptyConstructor)
4356+
ModuleDef(nameIdent.name.asTermName, templ)
4357+
.tap: md =>
4358+
if nameIdent.isBackquoted then
4359+
md.pushAttachment(Backquoted, ())
4360+
finalizeDef(md, mods, start)
43444361

43454362
private def checkAccessOnly(mods: Modifiers, caseStr: String): Modifiers =
43464363
// We allow `infix` and `into` on `enum` definitions.
@@ -4572,7 +4589,7 @@ object Parsers {
45724589
Template(constr, parents, Nil, EmptyValDef, Nil)
45734590
else if !newSyntaxAllowed
45744591
|| in.token == WITH && tparams.isEmpty && vparamss.isEmpty
4575-
// if new syntax is still allowed and there are parameters, they mist be new style conditions,
4592+
// if new syntax is still allowed and there are parameters, they must be new style conditions,
45764593
// so old with-style syntax would not be allowed.
45774594
then
45784595
withTemplate(constr, parents)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe
236236
case DefaultShadowsGivenID // errorNumber: 220
237237
case RecurseWithDefaultID // errorNumber: 221
238238
case EncodedPackageNameID // errorNumber: 222
239+
case AmbiguousTemplateNameID // errorNumber: 223
239240

240241
def errorNumber = ordinal - 1
241242

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
@@ -3097,7 +3096,7 @@ class MissingImplicitArgument(
30973096
def msg(using Context): String =
30983097

30993098
def formatMsg(shortForm: String)(headline: String = shortForm) = arg match
3100-
case arg: Trees.SearchFailureIdent[?] =>
3099+
case arg: SearchFailureIdent[?] =>
31013100
arg.tpe match
31023101
case _: NoMatchingImplicits => headline
31033102
case tpe: SearchFailureType =>
@@ -3741,3 +3740,8 @@ final class EncodedPackageName(name: Name)(using Context) extends SyntaxMsg(Enco
37413740
|or `myfile-test.scala` can produce encoded names for the generated package objects.
37423741
|
37433742
|In this case, the name `$name` is encoded as `${name.encode}`."""
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/neg/i16072.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
2+
enum Oops_:
3+
case Z // error // error expected { and }

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)