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
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/config/ScalaSettings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ private sealed trait WarningSettings:
private val WtoStringInterpolated = BooleanSetting(WarningSetting, "Wtostring-interpolated", "Warn a standard interpolator used toString on a reference type.")
private val WrecurseWithDefault = BooleanSetting(WarningSetting, "Wrecurse-with-default", "Warn when a method calls itself with a default argument.")
private val WwrongArrow = BooleanSetting(WarningSetting, "Wwrong-arrow", "Warn if function arrow was used instead of context literal ?=>.")
private val Wsyntax = BooleanSetting(WarningSetting, "Wsyntax", "Warn if code relies on misleading syntax.")
private val Wunused: Setting[List[ChoiceWithHelp[String]]] = MultiChoiceHelpSetting(
WarningSetting,
name = "Wunused",
Expand Down Expand Up @@ -308,6 +309,7 @@ private sealed trait WarningSettings:
def recurseWithDefault(using Context): Boolean = allOr(WrecurseWithDefault)
def wrongArrow(using Context): Boolean = allOr(WwrongArrow)
def safeInit(using Context): Boolean = allOr(WsafeInit)
def syntax(using Context): Boolean = allOr(Wsyntax)

/** -X "Extended" or "Advanced" settings */
private sealed trait XSettings:
Expand Down
49 changes: 25 additions & 24 deletions compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -219,9 +219,8 @@ object Parsers {
isIdent(nme.erased) && in.erasedEnabled && in.isSoftModifierInParamModifierPosition
def isConsume =
isIdent(nme.consume) && ccEnabled //\&& in.isSoftModifierInParamModifierPosition
def isSimpleLiteral =
simpleLiteralTokens.contains(in.token)
|| isIdent(nme.raw.MINUS) && numericLitTokens.contains(in.lookahead.token)
def isNegatedNumber = isIdent(nme.raw.MINUS) && numericLitTokens.contains(in.lookahead.token)
def isSimpleLiteral = simpleLiteralTokens.contains(in.token) || isNegatedNumber
def isLiteral = literalTokens contains in.token
def isNumericLit = numericLitTokens contains in.token
def isTemplateIntro = templateIntroTokens contains in.token
Expand Down Expand Up @@ -1343,9 +1342,7 @@ object Parsers {
*/
def simpleLiteral(): Tree =
if isIdent(nme.raw.MINUS) then
val start = in.offset
in.nextToken()
literal(negOffset = start, inTypeOrSingleton = true)
literal(start = in.skipToken(), inTypeOrSingleton = true)
else
literal(inTypeOrSingleton = true)

Expand All @@ -1354,15 +1351,17 @@ object Parsers {
* | symbolLiteral
* | ‘null’
*
* @param negOffset The offset of a preceding `-' sign, if any.
* If the literal is not negated, negOffset == in.offset.
* @param start The offset of a preceding `-' sign, if any.
* If the literal is not negated, start == in.offset.
*/
def literal(negOffset: Int = in.offset, inPattern: Boolean = false, inTypeOrSingleton: Boolean = false, inStringInterpolation: Boolean = false): Tree = {
def literal(start: Int = in.offset, inPattern: Boolean = false, inTypeOrSingleton: Boolean = false, inStringInterpolation: Boolean = false): Tree = {
def literalOf(token: Token): Tree = {
val isNegated = negOffset < in.offset
val isNegated = start < in.offset
def digits0 = in.removeNumberSeparators(in.strVal)
def digits = if (isNegated) "-" + digits0 else digits0
def digits = if isNegated then "-" + digits0 else digits0
if !inTypeOrSingleton then
if isNegated && start < in.offset - 1 && ctx.settings.Whas.syntax then
warning(DetachedUnaryMinus(), start)
token match {
case INTLIT => return Number(digits, NumberKind.Whole(in.base))
case DECILIT => return Number(digits, NumberKind.Decimal)
Expand Down Expand Up @@ -1395,15 +1394,15 @@ object Parsers {
val t = in.token match {
case STRINGLIT | STRINGPART =>
val value = in.strVal
atSpan(negOffset, negOffset, negOffset + value.length) { Literal(Constant(value)) }
atSpan(start, start, start + value.length) { Literal(Constant(value)) }
case _ =>
syntaxErrorOrIncomplete(IllegalLiteral())
atSpan(negOffset) { Literal(Constant(null)) }
atSpan(start) { Literal(Constant(null)) }
}
in.nextToken()
t
}
else atSpan(negOffset) {
else atSpan(start) {
if (in.token == QUOTEID)
if ((staged & StageKind.Spliced) != 0 && Chars.isIdentifierStart(in.name(0))) {
val t = atSpan(in.offset + 1) {
Expand Down Expand Up @@ -1476,7 +1475,7 @@ object Parsers {
nextSegment(in.offset + offsetCorrection)
offsetCorrection = 0
if (in.token == STRINGLIT)
segmentBuf += literal(inPattern = inPattern, negOffset = in.offset + offsetCorrection, inStringInterpolation = true)
segmentBuf += literal(in.offset + offsetCorrection, inPattern = inPattern, inStringInterpolation = true)

InterpolatedString(interpolator, segmentBuf.toList)
}
Expand Down Expand Up @@ -2743,14 +2742,15 @@ object Parsers {
*/
val prefixExpr: Location => Tree = location =>
if in.token == IDENTIFIER && nme.raw.isUnary(in.name)
&& in.canStartExprTokens.contains(in.lookahead.token)
&& {
val lookahead = in.lookahead
in.canStartExprTokens.contains(lookahead.token) && lookahead.lineOffset < 0
}
then
val start = in.offset
val op = termIdent()
if (op.name == nme.raw.MINUS && isNumericLit)
simpleExprRest(literal(start), location, canApply = true)
if isNegatedNumber then
simpleExprRest(literal(start = in.skipToken()), location, canApply = true)
else
atSpan(start) { PrefixOp(op, simpleExpr(location)) }
atSpan(in.offset) { PrefixOp(termIdent(), simpleExpr(location)) }
else simpleExpr(location)

/** SimpleExpr ::= ‘new’ ConstrApp {`with` ConstrApp} [TemplateBody]
Expand Down Expand Up @@ -2826,6 +2826,8 @@ object Parsers {
if (canApply) argumentStart()
in.token match
case DOT =>
if ctx.settings.Whas.syntax then
t match { case Number(n, _) if n(0) == '-' => warning(UnaryMinusInSelect(), t.span.start) case _ => }
in.nextToken()
simpleExprRest(selectorOrMatch(t), location, canApply = true)
case LBRACKET =>
Expand Down Expand Up @@ -3300,9 +3302,8 @@ object Parsers {
*/
def simplePattern(): Tree = in.token match {
case IDENTIFIER | BACKQUOTED_IDENT | THIS | SUPER =>
simpleRef() match
case id @ Ident(nme.raw.MINUS) if isNumericLit => literal(startOffset(id))
case t => simplePatternRest(t)
if isNegatedNumber then literal(start = in.skipToken())
else simplePatternRest(simpleRef())
case USCORE =>
wildcardIdent()
case LPAREN =>
Expand Down
42 changes: 19 additions & 23 deletions compiler/src/dotty/tools/dotc/parsing/Scanners.scala
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,9 @@ object Scanners {
strVal = litBuf.toString
litBuf.clear()

@inline def isNumberSeparator(c: Char): Boolean = c == '_'
inline def isNumberSeparator(c: Char): Boolean = c == '_'

@inline def removeNumberSeparators(s: String): String = if (s.indexOf('_') == -1) s else s.replace("_", "")
def removeNumberSeparators(s: String): String = if (s.indexOf('_') == -1) s else s.replace("_", "")

// disallow trailing numeric separator char, but continue lexing
def checkNoTrailingSeparator(): Unit =
Expand Down Expand Up @@ -916,21 +916,7 @@ object Scanners {
putChar('/')
getOperatorRest()
}
case '0' =>
def fetchLeadingZero(): Unit = {
nextChar()
ch match {
case 'x' | 'X' => base = 16 ; nextChar()
case 'b' | 'B' => base = 2 ; nextChar()
case _ => base = 10 ; putChar('0')
}
if (base != 10 && !isNumberSeparator(ch) && digit2int(ch, base) < 0)
error(em"invalid literal number")
}
fetchLeadingZero()
getNumber()
case '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' =>
base = 10
case '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' =>
getNumber()
case '`' =>
getBackquotedIdent()
Expand Down Expand Up @@ -1500,9 +1486,21 @@ object Scanners {
if (isIdentifierPart(ch) && ch >= ' ')
error(em"Invalid literal number")

/** Read a number into strVal and set base
*/
protected def getNumber(): Unit = {
/** Read a number into strVal and set base and token.
*/
def getNumber(): Unit =
def checkNumberChar() =
if !isNumberSeparator(ch) && digit2int(ch, base) < 0 then
error(em"invalid literal number")
if ch == '0' then
nextChar()
ch match
case 'x' | 'X' => base = 16; nextChar(); checkNumberChar()
case 'b' | 'B' => base = 2; nextChar(); checkNumberChar()
case _ => base = 10; putChar('0')
else
base = 10

while (isNumberSeparator(ch) || digit2int(ch, base) >= 0) {
putChar(ch)
nextChar()
Expand All @@ -1525,11 +1523,9 @@ object Scanners {
token = LONGLIT
case _ =>
}

checkNoTrailingSeparator()

setStrVal()
}
end getNumber

private def finishCharLit(): Unit = {
nextChar()
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 DetachedUnaryMinusID // errorNumber: 222
case UnaryMinusInSelectID // errorNumber: 223

def errorNumber = ordinal - 1

Expand Down
12 changes: 12 additions & 0 deletions compiler/src/dotty/tools/dotc/reporting/messages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3741,3 +3741,15 @@ 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."

final class DetachedUnaryMinus()(using Context) extends SyntaxMsg(DetachedUnaryMinusID):
override protected def msg(using Context): String =
"Unary minus is too far to the left."
override protected def explain(using Context): String =
"The expression is parsed as a literal, and intervening space is ignored."

final class UnaryMinusInSelect()(using Context) extends SyntaxMsg(UnaryMinusInSelectID):
override protected def msg(using Context): String =
"Negative literal should be parenthesized before dot selection."
override protected def explain(using Context): String =
"The receiver is parsed as a negative literal, not as the negation of the whole expression."
5 changes: 5 additions & 0 deletions tests/neg/i24162.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

def test(x: Int) =
x match
case `-`42 => true // error => expected
case _ => false // error unindent expected, case found
13 changes: 13 additions & 0 deletions tests/pos/i7910.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@

class C {
def k = {
object + extends Function1[Int, Int] { def apply(i: Int): Int = i + 1 }
val g: Int => Int = +
g(1)
}
def ok = {
val i = 42
val n = +i
n
}
}
13 changes: 13 additions & 0 deletions tests/warn/unary-minus.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//> using options -Wsyntax -Wnonunit-statement

class C {
def f1 = -2.abs // warn funky precedence
def f2 = - 2.abs // warn meaningless space
def f3 = - 2 // warn meaningless space
def f4 = 42
-2.abs // warn precedence // hides warn unused expression
def f5 = 42
- 2.abs // nowarn infix
def f6 = (-2).abs // nowarn explicit precedence
def f7 = -3.14 // nowarn decimal point
}
Loading