diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 61b7b981f529..6b0da5faed52 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -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", @@ -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: diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 64eb442c239a..36e5c25650a9 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -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 @@ -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) @@ -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) @@ -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) { @@ -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) } @@ -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] @@ -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 => @@ -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 => diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 52e03de60dea..02850a83a24b 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -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 = @@ -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() @@ -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() @@ -1525,11 +1523,9 @@ object Scanners { token = LONGLIT case _ => } - checkNoTrailingSeparator() - setStrVal() - } + end getNumber private def finishCharLit(): Unit = { nextChar() diff --git a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala index 103687abdbff..f04320b8494b 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 DetachedUnaryMinusID // errorNumber: 222 + case UnaryMinusInSelectID // 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 9c4ee9926bf2..8c36e3345711 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -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." diff --git a/tests/neg/i24162.scala b/tests/neg/i24162.scala new file mode 100644 index 000000000000..22d97ce99521 --- /dev/null +++ b/tests/neg/i24162.scala @@ -0,0 +1,5 @@ + +def test(x: Int) = + x match + case `-`42 => true // error => expected + case _ => false // error unindent expected, case found diff --git a/tests/pos/i7910.scala b/tests/pos/i7910.scala new file mode 100644 index 000000000000..5ffaa30cc2d3 --- /dev/null +++ b/tests/pos/i7910.scala @@ -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 + } +} diff --git a/tests/warn/unary-minus.scala b/tests/warn/unary-minus.scala new file mode 100644 index 000000000000..54eee6d64d51 --- /dev/null +++ b/tests/warn/unary-minus.scala @@ -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 +}