diff --git a/compiler/src/dotty/tools/dotc/core/Comments.scala b/compiler/src/dotty/tools/dotc/core/Comments.scala index 1b20b75ad8ac..f8e531e89888 100644 --- a/compiler/src/dotty/tools/dotc/core/Comments.scala +++ b/compiler/src/dotty/tools/dotc/core/Comments.scala @@ -10,7 +10,7 @@ import util.{SourceFile, ReadOnlyMap} import util.Spans._ import util.CommentParsing._ import util.Property.Key -import parsing.Parsers.Parser +import parsing.Parsers import reporting.ProperDefinitionNotFound object Comments { @@ -125,7 +125,7 @@ object Comments { object UseCase { def apply(code: String, codePos: Span)(using Context): UseCase = { val tree = { - val tree = new Parser(SourceFile.virtual("", code)).localDef(codePos.start) + val tree = Parsers.parser(SourceFile.virtual("", code)).localDef(codePos.start) tree match { case tree: untpd.DefDef => val newName = ctx.compilationUnit.freshNames.newName(tree.name, NameKinds.DocArtifactName) diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 36dc8a642afc..e36a6baff168 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -12,7 +12,7 @@ import SymDenotations.SymDenotation import config.Printers.inlining import ErrorReporting.errorTree import dotty.tools.dotc.util.{SourceFile, SourcePosition, SrcPos} -import parsing.Parsers.Parser +import parsing.Parsers import transform.{PostTyper, Inlining, CrossVersionChecks} import staging.StagingLevel @@ -332,7 +332,7 @@ object Inlines: case ConstantType(Constant(code: String)) => val source2 = SourceFile.virtual("tasty-reflect", code) inContext(ctx.fresh.setNewTyperState().setTyper(new Typer(ctx.nestingLevel + 1)).setSource(source2)) { - val tree2 = new Parser(source2).block() + val tree2 = Parsers.parser(source2).block() if ctx.reporter.allErrors.nonEmpty then ctx.reporter.allErrors.map((ErrorKind.Parser, _)) else diff --git a/compiler/src/dotty/tools/dotc/parsing/ParserPhase.scala b/compiler/src/dotty/tools/dotc/parsing/ParserPhase.scala index a67bca34cae2..c2f130996025 100644 --- a/compiler/src/dotty/tools/dotc/parsing/ParserPhase.scala +++ b/compiler/src/dotty/tools/dotc/parsing/ParserPhase.scala @@ -27,7 +27,7 @@ class Parser extends Phase { unit.untpdTree = if (unit.isJava) new JavaParsers.JavaParser(unit.source).parse() else { - val p = new Parsers.Parser(unit.source) + val p = Parsers.parser(unit.source) // p.in.debugTokenStream = true val tree = p.parse() if (p.firstXmlPos.exists && !firstXmlPos.exists) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 3079b26df6cd..0ba8bcad9da5 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -23,11 +23,10 @@ import StdNames._ import util.Spans._ import Constants._ import Symbols.NoSymbol -import ScriptParsers._ import Decorators._ import util.Chars import scala.annotation.tailrec -import rewrites.Rewrites.{patch, overlapsPatch} +import rewrites.Rewrites.{patch, patchOver, overlapsPatch} import reporting._ import config.Feature import config.Feature.{sourceVersion, migrateTo3, globalOnlyImports} @@ -74,7 +73,7 @@ object Parsers { * if not, the AST will be supplemented. */ def parser(source: SourceFile)(using Context): Parser = - if source.isSelfContained then new ScriptParser(source) + if ctx.settings.rewrite.value.isDefined && ctx.settings.indent.value then new ToIndentParser(source) else new Parser(source) private val InCase: Region => Region = Scanners.InCase(_) @@ -169,13 +168,15 @@ object Parsers { } } - class Parser(source: SourceFile)(using Context) extends ParserCommon(source) { + class Parser private[Parsers] (source: SourceFile, allowRewrite: Boolean = true)(using Context) extends ParserCommon(source) { - val in: Scanner = new Scanner(source, profile = Profile.current) + val in: Scanner = createScanner() // in.debugTokenStream = true // uncomment to see the token stream of the standard scanner, but not syntax highlighting + def createScanner() = + new Scanner(source, profile = Profile.current, allowRewrite = allowRewrite) + /** This is the general parse entry point. - * Overridden by ScriptParser */ def parse(): Tree = { val t = compilationUnit() @@ -473,7 +474,6 @@ object Parsers { } case _ => - /** Convert (qual)ident to type identifier */ def convertToTypeId(tree: Tree): Tree = tree match { @@ -559,15 +559,14 @@ object Parsers { def inBraces[T](body: => T): T = enclosed(LBRACE, body) def inBrackets[T](body: => T): T = enclosed(LBRACKET, body) + def isAfterArrow = testChars(in.lastOffset - 3, " =>") + def inBracesOrIndented[T](body: => T, rewriteWithColon: Boolean = false): T = if in.token == INDENT then - val rewriteToBraces = in.rewriteNoIndent - && !testChars(in.lastOffset - 3, " =>") // braces are always optional after `=>` so none should be inserted - if rewriteToBraces then indentedToBraces(body) + // braces are always optional after `=>` so none should be inserted + if in.rewriteNoIndent && !isAfterArrow then indentedToBraces(body) else enclosed(INDENT, body) - else - if in.rewriteToIndent then bracesToIndented(body, rewriteWithColon) - else inBraces(body) + else inBraces(body) def inDefScopeBraces[T](body: => T, rewriteWithColon: Boolean = false): T = inBracesOrIndented(body, rewriteWithColon) @@ -638,20 +637,13 @@ object Parsers { /* -------- REWRITES ----------------------------------------------------------- */ - /** The last offset where a colon at the end of line would be required if a subsequent { ... } - * block would be converted to an indentation region. - */ - var possibleColonOffset: Int = -1 - - def testChar(idx: Int, p: Char => Boolean): Boolean = { + def testChar(idx: Int, p: Char => Boolean): Boolean = val txt = source.content - idx < txt.length && p(txt(idx)) - } + idx > -1 && idx < txt.length && p(txt(idx)) - def testChar(idx: Int, c: Char): Boolean = { + def testChar(idx: Int, c: Char): Boolean = val txt = source.content - idx < txt.length && txt(idx) == c - } + idx > -1 && idx < txt.length && txt(idx) == c def testChars(from: Int, str: String): Boolean = str.isEmpty || @@ -723,74 +715,21 @@ object Parsers { t end indentedToBraces - /** The region to eliminate when replacing an opening `(` or `{` that ends a line. - * The `(` or `{` is at in.offset. - */ - def startingElimRegion(colonRequired: Boolean): (Offset, Offset) = { - val skipped = skipBlanks(in.offset + 1) - if (in.isAfterLineEnd) - if (testChar(skipped, Chars.LF) && !colonRequired) - (in.lineOffset, skipped + 1) // skip the whole line - else - (in.offset, skipped) - else if (testChar(in.offset - 1, ' ')) (in.offset - 1, in.offset + 1) - else (in.offset, in.offset + 1) - } + /** The region to eliminate when replacing a brace or parenthesis that ends a line */ + def elimRegion(offset: Offset): (Offset, Offset) = + val (start, end) = blankLinesAround(offset, offset + 1) + if testChar(end, Chars.LF) then + if testChar(start - 1, Chars.LF) then (start, end + 1) // skip the whole line + else (start, end) // skip from previous char to end of line + else (offset, end) // skip from token to next char - /** The region to eliminate when replacing a closing `)` or `}` that starts a new line - * The `)` or `}` precedes in.lastOffset. - */ - def closingElimRegion(): (Offset, Offset) = { - val skipped = skipBlanks(in.lastOffset) - if (testChar(skipped, Chars.LF)) // if `)` or `}` is on a line by itself - (source.startOfLine(in.lastOffset), skipped + 1) // skip the whole line - else // else - (in.lastOffset - 1, skipped) // move the following text up to where the `)` or `}` was - } + /** Expand the current span to its surrounding blank space */ + def blankLinesAround(start: Offset, end: Offset): (Offset, Offset) = + (skipBlanks(start - 1, -1) + 1, skipBlanks(end, 1)) - /** Parse brace-enclosed `body` and rewrite it to be an indentation region instead, if possible. - * If possible means: - * 1. not inside (...), [...], case ... => - * 2. opening brace `{` is at end of line - * 3. closing brace `}` is at start of line - * 4. there is at least one token between the braces - * 5. the closing brace is also at the end of the line, or it is followed by one of - * `then`, `else`, `do`, `catch`, `finally`, `yield`, or `match`. - * 6. the opening brace does not follow a `=>`. The reason for this condition is that - * rewriting back to braces does not work after `=>` (since in most cases braces are omitted - * after a `=>` it would be annoying if braces were inserted). - */ - def bracesToIndented[T](body: => T, rewriteWithColon: Boolean): T = { - val underColonSyntax = possibleColonOffset == in.lastOffset - val colonRequired = rewriteWithColon || underColonSyntax - val (startOpening, endOpening) = startingElimRegion(colonRequired) - val isOutermost = in.currentRegion.isOutermost - def allBraces(r: Region): Boolean = r match { - case r: Indented => r.isOutermost || allBraces(r.enclosing) - case r: InBraces => allBraces(r.enclosing) - case _ => false - } - var canRewrite = allBraces(in.currentRegion) && // test (1) - !testChars(in.lastOffset - 3, " =>") // test(6) - val t = enclosed(LBRACE, { - canRewrite &= in.isAfterLineEnd // test (2) - val curOffset = in.offset - try body - finally { - canRewrite &= in.isAfterLineEnd && in.offset != curOffset // test (3)(4) - } - }) - canRewrite &= (in.isAfterLineEnd || statCtdTokens.contains(in.token)) // test (5) - if canRewrite && (!underColonSyntax || Feature.fewerBracesEnabled) then - val openingPatchStr = - if !colonRequired then "" - else if testChar(startOpening - 1, Chars.isOperatorPart(_)) then " :" - else ":" - val (startClosing, endClosing) = closingElimRegion() - patch(source, Span(startOpening, endOpening), openingPatchStr) - patch(source, Span(startClosing, endClosing), "") - t - } + /** When rewriting to indent, make sure there is an indent after a `=>\n` */ + def indentedRegionAfterArrow[T](body: => T, inCaseDef: Boolean = false): T = + body /** Drop (...) or { ... }, replacing the closing element with `endStr` */ def dropParensOrBraces(start: Offset, endStr: String): Unit = { @@ -803,7 +742,7 @@ object Parsers { val preFill = if (closingStartsLine || endStr.isEmpty) "" else " " val postFill = if (in.lastOffset == in.offset) " " else "" val (startClosing, endClosing) = - if (closingStartsLine && endStr.isEmpty) closingElimRegion() + if (closingStartsLine && endStr.isEmpty) elimRegion(in.lastOffset - 1) else (in.lastOffset - 1, in.lastOffset) patch(source, Span(startClosing, endClosing), s"$preFill$endStr$postFill") } @@ -1057,7 +996,7 @@ object Parsers { /** Accept identifier and return its name as a term name. */ def ident(): TermName = - if (isIdent) { + if isIdent then val name = in.name if name == nme.CONSTRUCTOR || name == nme.STATIC_CONSTRUCTOR then report.error( @@ -1065,11 +1004,9 @@ object Parsers { in.sourcePos()) in.nextToken() name - } - else { + else syntaxErrorOrIncomplete(ExpectedTokenButFound(IDENTIFIER, in.token)) nme.ERROR - } /** Accept identifier and return Ident with its name as a term name. */ def termIdent(): Ident = @@ -1312,14 +1249,14 @@ object Parsers { /* ------------- NEW LINES ------------------------------------------------- */ def newLineOpt(): Unit = - if (in.token == NEWLINE) in.nextToken() + if in.token == NEWLINE then in.nextToken() def newLinesOpt(): Unit = if in.isNewLine then in.nextToken() def newLineOptWhenFollowedBy(token: Int): Unit = // note: next is defined here because current == NEWLINE - if (in.token == NEWLINE && in.next.token == token) in.nextToken() + if in.token == NEWLINE && in.next.token == token then in.nextToken() def newLinesOptWhenFollowedBy(token: Int): Unit = if in.isNewLine && in.next.token == token then in.nextToken() @@ -1337,7 +1274,6 @@ object Parsers { syntaxErrorOrIncomplete(em"indented definitions expected, ${in} found") def colonAtEOLOpt(): Unit = - possibleColonOffset = in.lastOffset in.observeColonEOL(inTemplate = false) if in.token == COLONeol then in.nextToken() @@ -2391,7 +2327,7 @@ object Parsers { in.nextToken() else accept(ARROW) - val body = + val body = indentedRegionAfterArrow: if location == Location.InBlock then block() else if location == Location.InColonArg && in.token == INDENT then blockExpr() else expr() @@ -2412,7 +2348,7 @@ object Parsers { t def postfixExprRest(t: Tree, location: Location): Tree = - infixOps(t, in.canStartExprTokens, prefixExpr, location, ParseKind.Expr, + infixOps(t, in.canStartExprTokens, prefixExpr(_), location, ParseKind.Expr, isOperator = !(location.inArgs && followingIsVararg())) /** PrefixExpr ::= [PrefixOperator'] SimpleExpr @@ -2641,10 +2577,10 @@ object Parsers { */ def blockExpr(): Tree = atSpan(in.offset) { val simplify = in.token == INDENT - inDefScopeBraces { + inDefScopeBraces({ if (in.token == CASE) Match(EmptyTree, caseClauses(() => caseClause())) else block(simplify) - } + }) } /** Block ::= BlockStatSeq @@ -2809,14 +2745,16 @@ object Parsers { (pattern(), guard()) } CaseDef(pat, grd, atSpan(accept(ARROW)) { - if exprOnly then - if in.indentSyntax && in.isAfterLineEnd && in.token != INDENT then - warning(em"""Misleading indentation: this expression forms part of the preceding catch case. - |If this is intended, it should be indented for clarity. - |Otherwise, if the handler is intended to be empty, use a multi-line catch with - |an indented case.""") - expr() - else block() + indentedRegionAfterArrow({ + if exprOnly then + if in.indentSyntax && in.isAfterLineEnd && in.token != INDENT then + warning(em"""Misleading indentation: this expression forms part of the preceding catch case. + |If this is intended, it should be indented for clarity. + |Otherwise, if the handler is intended to be empty, use a multi-line catch with + |an indented case.""") + expr() + else block() + }, inCaseDef = true) }) } @@ -2834,7 +2772,7 @@ object Parsers { } } CaseDef(pat, EmptyTree, atSpan(accept(ARROW)) { - val t = rejectWildcardType(typ()) + val t = indentedRegionAfterArrow(rejectWildcardType(typ()), inCaseDef = true) if in.token == SEMI then in.nextToken() newLinesOptWhenFollowedBy(CASE) t @@ -4211,23 +4149,28 @@ object Parsers { */ def templateStatSeq(): (ValDef, List[Tree]) = checkNoEscapingPlaceholders { val stats = new ListBuffer[Tree] + val startsAfterLineEnd = in.isAfterLineEnd val self = selfType() - while - var empty = false - if (in.token == IMPORT) - stats ++= importClause() - else if (in.token == EXPORT) - stats ++= exportClause() - else if isIdent(nme.extension) && followingIsExtension() then - stats += extension() - else if (isDefIntro(modifierTokensOrCase)) - stats +++= defOrDcl(in.offset, defAnnotsMods(modifierTokens)) - else if (isExprIntro) - stats += expr1() - else - empty = true - statSepOrEnd(stats, noPrevStat = empty) - do () + def loop = + while + var empty = false + if (in.token == IMPORT) + stats ++= importClause() + else if (in.token == EXPORT) + stats ++= exportClause() + else if isIdent(nme.extension) && followingIsExtension() then + stats += extension() + else if (isDefIntro(modifierTokensOrCase)) + stats +++= defOrDcl(in.offset, defAnnotsMods(modifierTokens)) + else if (isExprIntro) + stats += expr1() + else + empty = true + statSepOrEnd(stats, noPrevStat = empty) + do () + if self != null && !startsAfterLineEnd then + indentedRegionAfterArrow(loop) + else loop (self, if stats.isEmpty then List(EmptyTree) else stats.toList) } @@ -4361,7 +4304,7 @@ object Parsers { /** OutlineParser parses top-level declarations in `source` to find declared classes, ignoring their bodies (which * must only have balanced braces). This is used to map class names to defining sources. */ - class OutlineParser(source: SourceFile)(using Context) extends Parser(source) with OutlineParserCommon { + class OutlineParser(source: SourceFile)(using Context) extends Parser(source, allowRewrite = false) with OutlineParserCommon { def skipBracesHook(): Option[Tree] = if (in.token == XMLSTART) Some(xmlLiteral()) else None @@ -4376,4 +4319,137 @@ object Parsers { (EmptyValDef, List(EmptyTree)) } } + + /** The Scala parser that can rewrite to indent */ + private class ToIndentParser(source: SourceFile)(using Context) extends Parser(source): + + override def createScanner(): Scanner = new Scanner(source): + override def nextToken(): Unit = + if token != EMPTY then patchIndent() + prev = saveCopy + super.nextToken() + + assert(in.rewriteToIndent) + + private var prev: TokenData = Scanners.newTokenData + + /** The last offset where a colon at the end of line would be required if a subsequent { ... } + * block would be converted to an indentation region. */ + private var possibleColonOffset: Int = -1 + + /** the minimum indent width to rewrite to */ + private var minimumIndent: IndentWidth = IndentWidth.Zero + + /** the maximum indent width to ensure an indent region is properly closed by outdentation */ + private var maximumIndent: Option[IndentWidth] = None + + override def colonAtEOLOpt(): Unit = + possibleColonOffset = in.lastOffset + super.colonAtEOLOpt() + + /** make sure there is an indent after a `=>\n` */ + override def indentedRegionAfterArrow[T](body: => T, inCaseDef: Boolean = false): T = + if in.rewriteToIndent && (inCaseDef || in.isAfterLineEnd) then toIndentedRegion(body) + else body + + override def inBracesOrIndented[T](body: => T, rewriteWithColon: Boolean = false): T = + if in.token == INDENT then + if isAfterArrow then enclosed(INDENT, body) + else enclosed(INDENT, toIndentedRegion(body)) + else + if isAfterArrow then + val t = enclosed(LBRACE, toIndentedRegion(body)) + maximumIndent = None // no need to force outdentation after `}` + t + else bracesToIndented(body, rewriteWithColon) + + private val bracesToIndentPredecessors = + colonEOLPredecessors | canStartIndentTokens | BitSet(IDENTIFIER) + + /** Parse brace-enclosed `body` and rewrite it to be an indentation region instead, if possible. + * If possible means: + * 1. not inside (...), [...], case ... => + * 2. opening brace `{` is at end of line + * 3. closing brace `}` is at start of line + * 4. there is at least one token between the braces + * 5. the closing brace is also at the end of the line, or it is followed by one of + * `then`, `else`, `do`, `catch`, `finally`, `yield`, or `match`. + * 6. the opening brace `}` follows a colonEOLPredecessors, a canStartIndentTokens or an + * identifier + * 7. last token is not a leading operator + */ + private def bracesToIndented[T](body: => T, rewriteWithColon: Boolean): T = + val prevSaved = prev.saveCopy + val lastOffsetSaved = in.lastOffset + val underColonSyntax = possibleColonOffset == in.lastOffset + val colonRequired = rewriteWithColon || underColonSyntax + val (startOpening, endOpening) = elimRegion(in.offset) + def isBracesOrIndented(r: Region): Boolean = r match + case r: (Indented | InBraces) => true + case _ => false + var canRewrite = + isBracesOrIndented(in.currentRegion) // test (1) + && bracesToIndentPredecessors.contains(prevSaved.token) // test (6) + && !(prevSaved.isOperator && prevSaved.isAfterLineEnd) // test (7) + val t = enclosed(LBRACE, { + if in.isAfterLineEnd && in.token != RBRACE then // test (2)(4) + toIndentedRegion: + try body + finally canRewrite &= in.isAfterLineEnd // test (3) + else + canRewrite = false + body + }) + canRewrite &= in.isAfterLineEnd || in.token == EOF || statCtdTokens.contains(in.token) // test (5) + if canRewrite && (!underColonSyntax || Feature.fewerBracesEnabled) then + val (startClosing, endClosing) = elimRegion(in.lastOffset - 1) + // patch over the added indentation to remove braces + patchOver(source, Span(startOpening, endOpening), "") + patchOver(source, Span(startClosing, endClosing), "") + if colonRequired then + if prevSaved.token == IDENTIFIER && prevSaved.isOperator then + patch(Span(prevSaved.offset, lastOffsetSaved), s"`${prevSaved.name}`:") + else if prevSaved.token == IDENTIFIER && prevSaved.name.last == '_' then + patch(Span(lastOffsetSaved), " :") + else patch(Span(lastOffsetSaved), ":") + else + // no need to force outdentation after `}` + maximumIndent = None + t + end bracesToIndented + + /** compute required indentation to indent region properly */ + private def toIndentedRegion[T](body: => T): T = + val enclosingIndent = minimumIndent + minimumIndent = + if enclosingIndent < in.currentRegion.indentWidth then + in.currentRegion.indentWidth + else if + in.token == CASE && ( + in.currentRegion.enclosing == null || + in.currentRegion.indentWidth == in.currentRegion.enclosing.indentWidth + ) + then enclosingIndent + else enclosingIndent.increment + try body + finally + maximumIndent = Some(minimumIndent) + minimumIndent = enclosingIndent + + /** check that indentaton is correct or patch */ + private def patchIndent(): Unit = + if in.isAfterLineEnd && !in.isNewLine && in.token != OUTDENT && in.token != INDENT then + val currentIndent = in.indentWidth(in.offset) + val indentEndOffset = in.lineOffset + currentIndent.size + def isDotOrClosing = (closingParens + DOT).contains(in.token) + val needsOutdent = maximumIndent.exists: max => + currentIndent >= max || (!isDotOrClosing && currentIndent > minimumIndent) + val offByOne = + currentIndent != minimumIndent && currentIndent.isClose(minimumIndent) + if needsOutdent || !(currentIndent >= minimumIndent) || offByOne then + patch(Span(in.lineOffset, indentEndOffset), minimumIndent.toPrefix) + // no need to outdent anymore + if in.token != RBRACE then + maximumIndent = None + end ToIndentParser } diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index fac73bfb4992..7eeb1fc8b0a7 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -60,7 +60,7 @@ object Scanners { /** the base of a number */ var base: Int = 0 - def copyFrom(td: TokenData): Unit = { + def copyFrom(td: TokenData): this.type = this.token = td.token this.offset = td.offset this.lastOffset = td.lastOffset @@ -68,7 +68,9 @@ object Scanners { this.name = td.name this.strVal = td.strVal this.base = td.base - } + this + + def saveCopy: TokenData = newTokenData.copyFrom(this) def isNewLine = token == NEWLINE || token == NEWLINES def isStatSep = isNewLine || token == SEMI @@ -86,12 +88,14 @@ object Scanners { def isOperator = token == BACKQUOTED_IDENT - || token == IDENTIFIER && isOperatorPart(name(name.length - 1)) + || token == IDENTIFIER && isOperatorPart(name.last) def isArrow = token == ARROW || token == CTXARROW } + def newTokenData: TokenData = new TokenData {} + abstract class ScannerCommon(source: SourceFile)(using Context) extends CharArrayReader with TokenData { val buf: Array[Char] = source.content def nextToken(): Unit @@ -170,7 +174,7 @@ object Scanners { errorButContinue(em"trailing separator is not allowed", offset + litBuf.length - 1) } - class Scanner(source: SourceFile, override val startFrom: Offset = 0, profile: Profile = NoProfile, allowIndent: Boolean = true)(using Context) extends ScannerCommon(source) { + class Scanner(source: SourceFile, override val startFrom: Offset = 0, profile: Profile = NoProfile, allowRewrite: Boolean = true, allowIndent: Boolean = true)(using Context) extends ScannerCommon(source) { val keepComments = !ctx.settings.YdropComments.value /** A switch whether operators at the start of lines can be infix operators */ @@ -179,7 +183,7 @@ object Scanners { var debugTokenStream = false val showLookAheadOnDebug = false - val rewrite = ctx.settings.rewrite.value.isDefined + val rewrite = allowRewrite && ctx.settings.rewrite.value.isDefined val oldSyntax = ctx.settings.oldSyntax.value val newSyntax = ctx.settings.newSyntax.value @@ -264,11 +268,10 @@ object Scanners { if (idx >= 0 && idx <= lastKeywordStart) handleMigration(kwArray(idx)) else IDENTIFIER - def newTokenData: TokenData = new TokenData {} - /** We need one token lookahead and one token history */ val next = newTokenData + val last = newTokenData private val prev = newTokenData /** The current region. This is initially an Indented region with zero indentation width. */ @@ -433,7 +436,7 @@ object Scanners { // in backticks and is a binary operator. Hence, `x` is not classified as a // leading infix operator. def assumeStartsExpr(lexeme: TokenData) = - (canStartExprTokens.contains(lexeme.token) || lexeme.token == COLONeol) + (canStartExprTokens.contains(lexeme.token) || lexeme.token == COLONfollow) && (!lexeme.isOperator || nme.raw.isUnary(lexeme.name)) val lookahead = LookaheadScanner() lookahead.allowLeadingInfixOperators = false @@ -483,7 +486,7 @@ object Scanners { if (nextChar == ch) recur(idx - 1, ch, n + 1, k) else { - val k1: IndentWidth => IndentWidth = if (n == 0) k else Conc(_, Run(ch, n)) + val k1: IndentWidth => IndentWidth = if (n == 0) k else iw => k(Conc(iw, Run(ch, n))) recur(idx - 1, nextChar, 1, k1) } else recur(idx - 1, ' ', 0, identity) @@ -523,7 +526,7 @@ object Scanners { * * The following tokens can start an indentation region: * - * : = => <- if then else while do try catch + * : = => <- if then else while do try catch * finally for yield match throw return with * * Inserting an INDENT starts a new indentation region with the indentation of the current @@ -638,7 +641,8 @@ object Scanners { currentRegion.knownWidth = nextWidth else if (lastWidth != nextWidth) val lw = lastWidth - errorButContinue(spaceTabMismatchMsg(lw, nextWidth)) + val msg = spaceTabMismatchMsg(lw, nextWidth) + if rewriteToIndent then report.warning(msg) else errorButContinue(msg) if token != OUTDENT then handleNewIndentWidth(currentRegion, _.otherIndentWidths += nextWidth) if next.token == EMPTY then @@ -762,6 +766,9 @@ object Scanners { if lookahead.token == EOF || source.offsetToLine(lookahead.offset) > endLine then return true + + if lookahead.token == LBRACE && rewriteToIndent then + patch(Span(offset, offset + 3), s"`end`") false /** Is there a blank line between the current token and the last one? @@ -1266,6 +1273,7 @@ object Scanners { putChar(ch) ; nextRawChar() loopRest() else + next.lineOffset = if next.lastOffset < lineStartOffset then lineStartOffset else -1 finishNamedToken(IDENTIFIER, target = next) end loopRest setStrVal() @@ -1312,10 +1320,10 @@ object Scanners { } end getStringPart - private def fetchStringPart(multiLine: Boolean) = { + private def fetchStringPart(multiLine: Boolean) = offset = charOffset - 1 + lineOffset = if lastOffset < lineStartOffset then lineStartOffset else -1 getStringPart(multiLine) - } private def isTripleQuote(): Boolean = if (ch == '"') { @@ -1545,7 +1553,7 @@ object Scanners { /* Initialization: read first char, then first token */ nextChar() - nextToken() + (this: @unchecked).nextToken() currentRegion = topLevelRegion(indentWidth(offset)) } end Scanner @@ -1657,21 +1665,31 @@ object Scanners { case Run(ch: Char, n: Int) case Conc(l: IndentWidth, r: Run) - def <= (that: IndentWidth): Boolean = this match { - case Run(ch1, n1) => - that match { - case Run(ch2, n2) => n1 <= n2 && (ch1 == ch2 || n1 == 0) - case Conc(l, r) => this <= l - } - case Conc(l1, r1) => - that match { - case Conc(l2, r2) => l1 == l2 && r1 <= r2 - case _ => false - } - } + def <= (that: IndentWidth): Boolean = (this, that) match + case (Run(ch1, n1), Run(ch2, n2)) => n1 <= n2 && (ch1 == ch2 || n1 == 0) + case (Conc(l1, r1), Conc(l2, r2)) => (l1 == l2 && r1 <= r2) || this <= l2 + case (_, Conc(l2, _)) => this <= l2 + case _ => false def < (that: IndentWidth): Boolean = this <= that && !(that <= this) + def >= (that: IndentWidth): Boolean = that <= this + + def >(that: IndentWidth): Boolean = that < this + + def size: Int = this match + case Run(_, n) => n + case Conc(l, r) => l.size + r.n + + /** Add one level of indentation (one tab or two spaces depending on the last char) */ + def increment: IndentWidth = + def incRun(ch: Char, n: Int): Run = ch match + case ' ' => IndentWidth.Run(' ', n + 2) + case ch => IndentWidth.Run(ch, n + 1) + this match + case Run(ch, n) => incRun(ch, n) + case Conc(l, Run(ch, n)) => Conc(l, incRun(ch, n)) + /** Does `this` differ from `that` by not more than a single space? */ def isClose(that: IndentWidth): Boolean = this match case Run(ch1, n1) => diff --git a/compiler/src/dotty/tools/dotc/parsing/ScriptParsers.scala b/compiler/src/dotty/tools/dotc/parsing/ScriptParsers.scala deleted file mode 100644 index d11db73b0455..000000000000 --- a/compiler/src/dotty/tools/dotc/parsing/ScriptParsers.scala +++ /dev/null @@ -1,147 +0,0 @@ -package dotty.tools -package dotc -package parsing - -import util.SourceFile -import core._ -import Contexts._ -import Parsers._ - - -/**

Performs the following context-free rewritings:

- *
    - *
  1. - * Places all pattern variables in Bind nodes. In a pattern, for - * identifiers x:
    - *                 x  => x @ _
    - *               x:T  => x @ (_ : T)
    - *
  2. - *
  3. Removes pattern definitions (PatDef's) as follows: - * If pattern is a simple (typed) identifier:
    - *        val x = e     ==>  val x = e
    - *        val x: T = e  ==>  val x: T = e
    - * - * if there are no variables in pattern
    - *        val p = e  ==>  e match (case p => ())
    - * - * if there is exactly one variable in pattern
    - *        val x_1 = e match (case p => (x_1))
    - * - * if there is more than one variable in pattern
    - *        val p = e  ==>  private synthetic val t$ = e match (case p => (x_1, ..., x_N))
    - *                        val x_1 = t$._1
    - *                        ...
    - *                        val x_N = t$._N
    - *
  4. - *
  5. - * Removes function types as follows:
    - *        (argtpes) => restpe   ==>   scala.Function_n[argtpes, restpe]
    - *
  6. - *
  7. - * Wraps naked case definitions in a match as follows:
    - *        { cases }   ==>   (x => x.match {cases}), except when already argument to match
    - *
  8. - *
- */ -object ScriptParsers { - - import ast.untpd._ - - class ScriptParser(source: SourceFile)(using Context) extends Parser(source) { - - /** This is the parse entry point for code which is not self-contained, e.g. - * a script which is a series of template statements. They will be - * swaddled in Trees until the AST is equivalent to the one returned - * by compilationUnit(). - */ - override def parse(): Tree = unsupported("parse") - } -} - - /* TODO: reinstantiate - - val stmts = templateStatSeq(false)._2 - accept(EOF) - - def mainModuleName = ctx.settings.script.value - - /** If there is only a single object template in the file and it has a - * suitable main method, we will use it rather than building another object - * around it. Since objects are loaded lazily the whole script would have - * been a no-op, so we're not taking much liberty. - */ - def searchForMain(): Option[Tree] = { - /** Have to be fairly liberal about what constitutes a main method since - * nothing has been typed yet - for instance we can't assume the parameter - * type will look exactly like "Array[String]" as it could have been renamed - * via import, etc. - */ - def isMainMethod(t: Tree) = t match { - case DefDef(_, nme.main, Nil, List(_), _, _) => true - case _ => false - } - /** For now we require there only be one top level object. */ - var seenModule = false - val newStmts = stmts collect { - case t @ Import(_, _) => t - case md @ ModuleDef(mods, name, template) - if !seenModule && (template.body exists isMainMethod) => - seenModule = true - /** This slightly hacky situation arises because we have no way to communicate - * back to the scriptrunner what the name of the program is. Even if we were - * willing to take the sketchy route of settings.script.value = progName, that - * does not work when using fsc. And to find out in advance would impose a - * whole additional parse. So instead, if the actual object's name differs from - * what the script is expecting, we transform it to match. - */ - md.derivedModuleDef(mods, mainModuleName.toTermName, template) - case _ => - /** If we see anything but the above, fail. */ - return None - } - Some(makePackaging(0, emptyPkg, newStmts)) - } - - if (mainModuleName == ScriptRunner.defaultScriptMain) - searchForMain() foreach { return _ } - - /** Here we are building an AST representing the following source fiction, - * where is from -Xscript (defaults to "Main") and are - * the result of parsing the script file. - * - * object { - * def main(argv: Array[String]): Unit = { - * val args = argv - * new AnyRef { - * - * } - * } - * } - */ - import definitions._ - - def emptyPkg = atPos(0, 0, 0) { Ident(nme.EMPTY_PACKAGE_NAME) } - def emptyInit = DefDef( - Modifiers(), - nme.CONSTRUCTOR, - Nil, - List(Nil), - TypeTree(), - Block(List(Apply(Select(Super(This(tpnme.EMPTY), tpnme.EMPTY), nme.CONSTRUCTOR), Nil)), Literal(Constant(()))) - ) - - // def main - def mainParamType = AppliedTypeTree(Ident(tpnme.Array), List(Ident(tpnme.String))) - def mainParameter = List(ValDef(Modifiers(Param), "argv", mainParamType, EmptyTree)) - def mainSetArgv = List(ValDef(Modifiers(), "args", TypeTree(), Ident("argv"))) - def mainNew = makeNew(Nil, emptyValDef, stmts, List(Nil), NoPosition, NoPosition) - def mainDef = DefDef(Modifiers(), nme.main, Nil, List(mainParameter), scalaDot(tpnme.Unit), Block(mainSetArgv, mainNew)) - - // object Main - def moduleName = ScriptRunner scriptMain settings - def moduleBody = Template(List(scalaScalaObjectConstr), emptyValDef, List(emptyInit, mainDef)) - def moduleDef = ModuleDef(Modifiers(), moduleName, moduleBody) - - // package { ... } - makePackaging(0, emptyPkg, List(moduleDef)) - }*/ diff --git a/compiler/src/dotty/tools/dotc/parsing/xml/MarkupParsers.scala b/compiler/src/dotty/tools/dotc/parsing/xml/MarkupParsers.scala index b3f41fab9eaa..34a179c1be01 100644 --- a/compiler/src/dotty/tools/dotc/parsing/xml/MarkupParsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/xml/MarkupParsers.scala @@ -324,8 +324,7 @@ object MarkupParsers { /** Some try/catch/finally logic used by xLiteral and xLiteralPattern. */ inline private def xLiteralCommon(f: () => Tree, ifTruncated: String => Unit): Tree = { assert(parser.in.token == Tokens.XMLSTART) - val saved = parser.in.newTokenData - saved.copyFrom(parser.in) + val saved = parser.in.saveCopy var output: Tree = null.asInstanceOf[Tree] try output = f() catch { @@ -404,7 +403,7 @@ object MarkupParsers { def escapeToScala[A](op: => A, kind: String): A = { xEmbeddedBlock = false val res = saving(parser.in.currentRegion, parser.in.currentRegion = _) { - val lbrace = parser.in.newTokenData + val lbrace = Scanners.newTokenData lbrace.token = LBRACE lbrace.offset = parser.in.charOffset - 1 lbrace.lastOffset = parser.in.lastOffset diff --git a/compiler/src/dotty/tools/dotc/printing/SyntaxHighlighting.scala b/compiler/src/dotty/tools/dotc/printing/SyntaxHighlighting.scala index 53e6b9472f5e..73b86ad42d35 100644 --- a/compiler/src/dotty/tools/dotc/printing/SyntaxHighlighting.scala +++ b/compiler/src/dotty/tools/dotc/printing/SyntaxHighlighting.scala @@ -6,7 +6,7 @@ import scala.language.unsafeNulls import dotty.tools.dotc.ast.untpd import dotty.tools.dotc.core.Contexts._ import dotty.tools.dotc.core.StdNames._ -import dotty.tools.dotc.parsing.Parsers.Parser +import dotty.tools.dotc.parsing.Parsers import dotty.tools.dotc.parsing.Scanners.Scanner import dotty.tools.dotc.parsing.Tokens._ import dotty.tools.dotc.reporting.Reporter @@ -124,7 +124,7 @@ object SyntaxHighlighting { } } - val parser = new Parser(source) + val parser = Parsers.parser(source) val trees = parser.blockStatSeq() TreeHighlighter.highlight(trees) diff --git a/compiler/src/dotty/tools/dotc/rewrites/Rewrites.scala b/compiler/src/dotty/tools/dotc/rewrites/Rewrites.scala index f2dfac88d464..bb9597dbc647 100644 --- a/compiler/src/dotty/tools/dotc/rewrites/Rewrites.scala +++ b/compiler/src/dotty/tools/dotc/rewrites/Rewrites.scala @@ -25,9 +25,14 @@ object Rewrites { def addPatch(span: Span, replacement: String): Unit = pbuf += Patch(span, replacement) + def patchOver(span: Span, replacement: String): Unit = + val prevPatchIdx = pbuf.lastIndexWhere(p => span.contains(p.span)) + if prevPatchIdx >= 0 then pbuf.remove(prevPatchIdx) + pbuf += Patch(span, replacement) + def apply(cs: Array[Char]): Array[Char] = { val delta = pbuf.map(_.delta).sum - val patches = pbuf.toList.sortBy(_.span.start) + val patches = pbuf.toList.sortBy(p => (p.span.start, p.span.end)) if (patches.nonEmpty) patches.reduceLeft {(p1, p2) => assert(p1.span.end <= p2.span.start, s"overlapping patches in $source: $p1 and $p2") @@ -71,6 +76,14 @@ object Rewrites { .addPatch(span, replacement) ) + /** Record a patch that replaces the first patch that it contains */ + def patchOver(source: SourceFile, span: Span, replacement: String)(using Context): Unit = + if ctx.reporter != Reporter.NoReporter // NoReporter is used for syntax highlighting + then ctx.settings.rewrite.value.foreach(_.patched + .getOrElseUpdate(source, new Patches(source)) + .patchOver(span, replacement) + ) + /** Patch position in `ctx.compilationUnit.source`. */ def patch(span: Span, replacement: String)(using Context): Unit = patch(ctx.compilationUnit.source, span, replacement) diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index df708057dd71..d18e04e09934 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -19,7 +19,7 @@ import config.{Config, Feature} import config.Printers.typr import inlines.{Inlines, PrepareInlineable} import parsing.JavaParsers.JavaParser -import parsing.Parsers.Parser +import parsing.Parsers import Annotations._ import Inferencing._ import transform.ValueClasses._ @@ -739,7 +739,7 @@ class Namer { typer: Typer => unit.untpdTree = if (unit.isJava) new JavaParser(unit.source).parse() - else new Parser(unit.source).parse() + else Parsers.parser(unit.source).parse() atPhase(Phases.typerPhase) { inContext(PrepareInlineable.initContext(ctx)) { diff --git a/compiler/src/dotty/tools/repl/ParseResult.scala b/compiler/src/dotty/tools/repl/ParseResult.scala index a67b247066f7..0130150496b1 100644 --- a/compiler/src/dotty/tools/repl/ParseResult.scala +++ b/compiler/src/dotty/tools/repl/ParseResult.scala @@ -5,7 +5,7 @@ import dotc.CompilationUnit import dotc.ast.untpd import dotc.core.Contexts._ import dotc.core.StdNames.str -import dotc.parsing.Parsers.Parser +import dotc.parsing.Parsers import dotc.parsing.Tokens import dotc.reporting.{Diagnostic, StoreReporter} import dotc.util.SourceFile @@ -121,7 +121,7 @@ object ParseResult { @sharable private val CommandExtract = """(:[\S]+)\s*(.*)""".r private def parseStats(using Context): List[untpd.Tree] = { - val parser = new Parser(ctx.source) + val parser = Parsers.parser(ctx.source) val stats = parser.blockStatSeq() parser.accept(Tokens.EOF) stats diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index 4e86a3b83383..7a36de330723 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -82,9 +82,14 @@ class CompilationTests { compileFile("tests/rewrites/rewrites3x.scala", defaultOptions.and("-rewrite", "-source", "future-migration")), compileFile("tests/rewrites/filtering-fors.scala", defaultOptions.and("-rewrite", "-source", "3.2-migration")), compileFile("tests/rewrites/refutable-pattern-bindings.scala", defaultOptions.and("-rewrite", "-source", "3.2-migration")), - compileFile("tests/rewrites/i8982.scala", defaultOptions.and("-indent", "-rewrite")), - compileFile("tests/rewrites/i9632.scala", defaultOptions.and("-indent", "-rewrite")), - compileFile("tests/rewrites/i11895.scala", defaultOptions.and("-indent", "-rewrite")), + compileFile("tests/rewrites/i8982.scala", indentRewrite), + compileFile("tests/rewrites/i9632.scala", indentRewrite), + compileFile("tests/rewrites/i11895.scala", indentRewrite), + compileFile("tests/rewrites/indent-rewrite.scala", indentRewrite), + compileFile("tests/rewrites/indent-comments.scala", indentRewrite), + compileFile("tests/rewrites/indent-mix-tab-space.scala", indentRewrite), + compileFile("tests/rewrites/indent-3-spaces.scala", indentRewrite), + compileFile("tests/rewrites/indent-mix-brace.scala", indentRewrite), compileFile("tests/rewrites/i12340.scala", unindentOptions.and("-rewrite")), compileFile("tests/rewrites/i17187.scala", unindentOptions.and("-rewrite")), compileFile("tests/rewrites/i17399.scala", unindentOptions.and("-rewrite")), diff --git a/compiler/test/dotty/tools/dotc/ast/UntypedTreeMapTest.scala b/compiler/test/dotty/tools/dotc/ast/UntypedTreeMapTest.scala index 472e47d859f3..f5ba121308d8 100644 --- a/compiler/test/dotty/tools/dotc/ast/UntypedTreeMapTest.scala +++ b/compiler/test/dotty/tools/dotc/ast/UntypedTreeMapTest.scala @@ -6,7 +6,7 @@ import org.junit.Test import org.junit.Assert._ import dotc.core.Contexts._ -import dotc.parsing.Parsers.Parser +import dotc.parsing.Parsers import dotc.util.SourceFile class UntpdTreeMapTest extends DottyTest { @@ -14,7 +14,7 @@ class UntpdTreeMapTest extends DottyTest { import untpd._ def parse(code: String): Tree = { - val (_, stats) = new Parser(SourceFile.virtual("", code)).templateStatSeq() + val (_, stats) = Parsers.parser(SourceFile.virtual("", code)).templateStatSeq() stats match { case List(stat) => stat; case stats => untpd.Thicket(stats) } } diff --git a/compiler/test/dotty/tools/dotc/parsing/ModifiersParsingTest.scala b/compiler/test/dotty/tools/dotc/parsing/ModifiersParsingTest.scala index 15d43653025a..65d291245df1 100644 --- a/compiler/test/dotty/tools/dotc/parsing/ModifiersParsingTest.scala +++ b/compiler/test/dotty/tools/dotc/parsing/ModifiersParsingTest.scala @@ -17,7 +17,7 @@ object ModifiersParsingTest { given Context = (new ContextBase).initialCtx def parse(code: String): Tree = { - val (_, stats) = new Parser(SourceFile.virtual("", code)).templateStatSeq() + val (_, stats) = Parsers.parser(SourceFile.virtual("", code)).templateStatSeq() stats match { case List(stat) => stat; case stats => Thicket(stats) } } diff --git a/compiler/test/dotty/tools/dotc/parsing/ParserTest.scala b/compiler/test/dotty/tools/dotc/parsing/ParserTest.scala index 4b166f838d5e..ebf119a6930c 100644 --- a/compiler/test/dotty/tools/dotc/parsing/ParserTest.scala +++ b/compiler/test/dotty/tools/dotc/parsing/ParserTest.scala @@ -25,7 +25,7 @@ class ParserTest extends DottyTest { private def parseSource(source: SourceFile): Tree = { //println("***** parsing " + source.file) - val parser = new Parser(source) + val parser = Parsers.parser(source) val tree = parser.parse() parsed += 1 parsedTrees += tree diff --git a/compiler/test/dotty/tools/vulpix/TestConfiguration.scala b/compiler/test/dotty/tools/vulpix/TestConfiguration.scala index 5d2992b50a09..170454ac3347 100644 --- a/compiler/test/dotty/tools/vulpix/TestConfiguration.scala +++ b/compiler/test/dotty/tools/vulpix/TestConfiguration.scala @@ -65,6 +65,7 @@ object TestConfiguration { val commonOptions = Array("-indent") ++ checkOptions ++ noCheckOptions ++ yCheckOptions val defaultOptions = TestFlags(basicClasspath, commonOptions) + val indentRewrite = defaultOptions.and("-rewrite") val unindentOptions = TestFlags(basicClasspath, Array("-no-indent") ++ checkOptions ++ noCheckOptions ++ yCheckOptions) val withCompilerOptions = defaultOptions.withClasspath(withCompilerClasspath).withRunClasspath(withCompilerClasspath) diff --git a/tests/pos/indent-colons.scala b/tests/pos/indent-colons.scala index eb3cbc3617ea..a48d0ffa010f 100644 --- a/tests/pos/indent-colons.scala +++ b/tests/pos/indent-colons.scala @@ -152,6 +152,11 @@ object Test23: val _ = 1 `+`: // ok x + // leading infix op + val _ = 1 + `+` : + x + val r = 1 to: 100 diff --git a/tests/rewrites/indent-3-spaces.check b/tests/rewrites/indent-3-spaces.check new file mode 100644 index 000000000000..a9dd85872ed4 --- /dev/null +++ b/tests/rewrites/indent-3-spaces.check @@ -0,0 +1,21 @@ +// Rewrite to indent, keeping 3 spaces as indentation + +def m1 = + def m2 = + "" + + "" + + "" + m2 + +def m4 = + def m5 = + def m6 = + val x = "" + x + .apply(0) + .toString + m6 + .toString + m5 + + m5 + .toString diff --git a/tests/rewrites/indent-3-spaces.scala b/tests/rewrites/indent-3-spaces.scala new file mode 100644 index 000000000000..9df6e253f074 --- /dev/null +++ b/tests/rewrites/indent-3-spaces.scala @@ -0,0 +1,26 @@ +// Rewrite to indent, keeping 3 spaces as indentation + +def m1 = { + def m2 = { + "" + + "" + + "" + } + m2 +} + +def m4 = { + def m5 = { + def m6 = { + val x = "" + x + .apply(0) + .toString + } + m6 + .toString + } + m5 + + m5 + .toString +} diff --git a/tests/rewrites/indent-comments.check b/tests/rewrites/indent-comments.check new file mode 100644 index 000000000000..8b5bec769366 --- /dev/null +++ b/tests/rewrites/indent-comments.check @@ -0,0 +1,25 @@ +// Rewriting to indent should preserve comments +class A: /* 1 */ /* 2 */ + def m1(b: Boolean) = /* 3 */ /* 4 */ + val x = if (b) + /* 5 */ { + "true" + } /* 6 */ + else + /* 7 */ + "false" + /* 8 */ + /* 9 */ x.toBoolean + /* 10 */ /* 11 */ + /* 12 */def m2 = // 12 + m1:// 14 + /* 15 */// 16 + true + /* 17 */// 18 +// because of the missing indent before { +// the scanner inserts a new line between || and { +// cannot rewrite to indentation without messing the comments up + true ||// 19 + /* 20 */{ + false + }// 21 diff --git a/tests/rewrites/indent-comments.scala b/tests/rewrites/indent-comments.scala new file mode 100644 index 000000000000..87bc8bda33d6 --- /dev/null +++ b/tests/rewrites/indent-comments.scala @@ -0,0 +1,27 @@ +// Rewriting to indent should preserve comments +class A /* 1 */ { /* 2 */ + def m1(b: Boolean) = /* 3 */ { /* 4 */ + val x = if (b) + /* 5 */ { + "true" + } /* 6 */ + else + { /* 7 */ + "false" + /* 8 */ } +/* 9 */ x.toBoolean + /* 10 */ } /* 11 */ +/* 12 */def m2 = {// 12 +m1// 14 + /* 15 */{// 16 +true +/* 17 */}// 18 +// because of the missing indent before { +// the scanner inserts a new line between || and { +// cannot rewrite to indentation without messing the comments up +true ||// 19 +/* 20 */{ + false +}// 21 +} +} diff --git a/tests/rewrites/indent-mix-brace.check b/tests/rewrites/indent-mix-brace.check new file mode 100644 index 000000000000..eb4752e1cb2b --- /dev/null +++ b/tests/rewrites/indent-mix-brace.check @@ -0,0 +1,17 @@ +// A mix of nested in-brace regions and indented regions + +class A: + def m1 = + "" + + def m2 = + def m3 = + val x = "" + x + m3 + +class B: + def foo = + def bar = + "" + bar diff --git a/tests/rewrites/indent-mix-brace.scala b/tests/rewrites/indent-mix-brace.scala new file mode 100644 index 000000000000..944537fc341f --- /dev/null +++ b/tests/rewrites/indent-mix-brace.scala @@ -0,0 +1,21 @@ +// A mix of nested in-brace regions and indented regions + +class A: + def m1 = { + "" + } + + def m2 = { +def m3 = + val x = "" + x +m3 + } + +class B { + def foo = + def bar = { + "" + } + bar +} diff --git a/tests/rewrites/indent-mix-tab-space.check b/tests/rewrites/indent-mix-tab-space.check new file mode 100644 index 000000000000..4f25839ccfda --- /dev/null +++ b/tests/rewrites/indent-mix-tab-space.check @@ -0,0 +1,22 @@ +// Contains an ugly but valid mix of spaces and tabs +// Rewrite to significant indentation syntax + +def m1 = + def m2 = + "" + + "" + + "" + m2 + +def m4 = + def m5 = + def m6 = + val x = "" + x + .apply(0) + .toString + m6 + .toString + m5 + + m5 + .toString diff --git a/tests/rewrites/indent-mix-tab-space.scala b/tests/rewrites/indent-mix-tab-space.scala new file mode 100644 index 000000000000..4a77fd1cbde6 --- /dev/null +++ b/tests/rewrites/indent-mix-tab-space.scala @@ -0,0 +1,27 @@ +// Contains an ugly but valid mix of spaces and tabs +// Rewrite to significant indentation syntax + +def m1 = { + def m2 = { + "" + + "" + + "" + } + m2 +} + +def m4 = { + def m5 = { + def m6 = { + val x = "" + x + .apply(0) + .toString + } + m6 + .toString + } + m5 + + m5 + .toString +} diff --git a/tests/rewrites/indent-rewrite.check b/tests/rewrites/indent-rewrite.check new file mode 100644 index 000000000000..592d1c9f4dd9 --- /dev/null +++ b/tests/rewrites/indent-rewrite.check @@ -0,0 +1,242 @@ +// A collection of patterns found when rewriting the community build to indent + +trait C1: + + class CC1 +// do not remove braces if empty region + class CC2 { + + } +// do not remove braces if open brace is not followed by new line + def m1(x: Int) = + { x + .toString + } +// add indent to pass an argument (fewer braces) + def m2: String = + m1: + 5 +// indent inner method + def m3: Int = + def seq = + Seq( + "1", + "2" + ) + seq + (1) + .toInt +// indent refinement + def m4: Any: + def foo: String + = + new: + def foo: String = + """ +Hello, World! +""" +// indent end marker + end m4 + +// fix off-by-one indentation + val x = "" + + def m5(x: String): String = + def inner: Boolean = + true + x + + // unindent properly when needed + def m6(xs: Seq[String]): String = + xs + .map: + x => x + .filter: + x => x.size > 0 + println("foo") + + def foo: String = + "" + foo + +// do not remove braces if closing braces not followed by new line + def m7: String = { + val x = "Hi" + x + }; def m8(x: String): String = + s"""Bye $x ${ + x + } +do not indent in a multiline string""" + def m9 = + val foo = "" + val x = Seq( + s"${foo}", + "" + ) + +// do not remove braces after closing brace + def m10(x: Int)(y: String) = y * x + m10 { 5 } { + "foo" + } + + // preserve indent of chained calls + def m11(xs: Seq[String]) = + xs + .filter: + _ => true + xs + .map { x => + val y = + if (x == "") "empty" + else x.size.toString + val z = x + y + z + } + .map { x => xs }.flatMap { xs => xs.map { x => + val y = + if (x == "") "empty" + else x.size.toString + val z = x + y + z + }} + .map: + x => val y = + if (x == "") "empty" + else x.size.toString + val z = x + y + z + .map: + x => + val y = + if (x == "") "empty" + else x.size.toString + val z = x + y + z + + // do not remove braces inside (...) or [...] + // remove braces after => + def m12(xs: List[Int]) = + println( + xs.size match { + case 1 => + xs match + case 1 :: Nil => "1" + case _ => s"${xs.head} :: Nil" + case _ => { + "xs" + } + } + ) + println( + if (xs.size > 0) { + "foo" + } else { + "bar" + } + ) + xs.map( + x => { + x + } + ).map: + x => { + x + } + import reflect.Selectable.reflectiveSelectable + def m13(xs: List[ + Any { + def foo: String + } + ]) = + xs.map(x => x.foo) + + // preserve indentation style before 'case' + // but fix indentation inside 'case' + def m14(o: Option[String]) = + o match + case Some(x) => x + case None => "" + + o match + case Some(x) => x + case None => "" + end match + + o match + case None => + "" + case Some(x) => + x + def m15(xs: List[Int]): String = + xs match + case _ :: tail => { + if tail.size == 0 then + println("log") + } + "foo" + case Nil => + "bar" + + // add backticks around operator + object `*:`: + def foo = ??? + def m16 = + val x = 5 * { + 2 + } == 10 `||`: + false + x `&&`: + true + + // leading infix operator + def m17 = + true + && { + false + } + + // ident ending with '_' + def m_(x: String) = ??? + m_ : + "foo" + + // do not remove braces in sequence of blocks + def m18(using ctx: String) = println(ctx) + { + given String = "foo" + m18 + } + { + given String = "bar" + m18 + } + def m19(x: String) = + { + given String = "foo" + m18 + } + { + given String = "bar" + m18 + } + + // back-quote end indent before match + def m20 = + val end = "Foo" + `end` match + case "Foo" => + case _ => + end take 3 + +// indent template after self type +class C2 { self => + val x = "" +} +trait C3: + self => + val x = "" +case class C4(): + self => + val y = "" diff --git a/tests/rewrites/indent-rewrite.scala b/tests/rewrites/indent-rewrite.scala new file mode 100644 index 000000000000..e00eaf535357 --- /dev/null +++ b/tests/rewrites/indent-rewrite.scala @@ -0,0 +1,275 @@ +// A collection of patterns found when rewriting the community build to indent + +trait C1 { + + class CC1 +// do not remove braces if empty region +class CC2 { + +} +// do not remove braces if open brace is not followed by new line +def m1(x: Int) = +{ x +.toString + } +// add indent to pass an argument (fewer braces) +def m2: String = { +m1 { +5 +} +} +// indent inner method + def m3: Int = { +def seq = { +Seq( +"1", +"2" +) +} +seq +(1) +.toInt +} +// indent refinement +def m4: Any { +def foo: String +} += + new { + def foo: String = + """ +Hello, World! +""" +} +// indent end marker +end m4 + +// fix off-by-one indentation + val x = "" + + def m5(x: String): String = { + def inner: Boolean = { + true + } + x + } + + // unindent properly when needed + def m6(xs: Seq[String]): String = { + xs + .map { + x => x + } + .filter { + x => x.size > 0 + } + println("foo") + + def foo: String = + "" + foo + } + +// do not remove braces if closing braces not followed by new line +def m7: String = { +val x = "Hi" +x +}; def m8(x: String): String = { +s"""Bye $x ${ + x +} +do not indent in a multiline string""" +} + def m9 = { + val foo = "" + val x = Seq( + s"${foo}", + "" + ) + } + +// do not remove braces after closing brace +def m10(x: Int)(y: String) = y * x +m10 { 5 } { + "foo" +} + + // preserve indent of chained calls + def m11(xs: Seq[String]) = { + xs + .filter { + _ => true + } + xs + .map { x => + val y = + if (x == "") "empty" + else x.size.toString + val z = x + y + z + } + .map { x => xs }.flatMap { xs => xs.map { x => + val y = + if (x == "") "empty" + else x.size.toString + val z = x + y + z + }} + .map { + x => val y = + if (x == "") "empty" + else x.size.toString + val z = x + y + z + } + .map { + x => + val y = + if (x == "") "empty" + else x.size.toString + val z = x + y + z + } + } + + // do not remove braces inside (...) or [...] + // remove braces after => + def m12(xs: List[Int]) = { + println( + xs.size match { + case 1 => + xs match { + case 1 :: Nil => "1" + case _ => s"${xs.head} :: Nil" + } + case _ => { + "xs" + } + } + ) + println( + if (xs.size > 0) { + "foo" + } else { + "bar" + } + ) + xs.map( + x => { + x + } + ).map { + x => { + x + } + } + } + import reflect.Selectable.reflectiveSelectable + def m13(xs: List[ + Any { + def foo: String + } + ]) = + xs.map(x => x.foo) + + // preserve indentation style before 'case' + // but fix indentation inside 'case' + def m14(o: Option[String]) = { + o match + case Some(x) => x + case None => "" + + o match + case Some(x) => x + case None => "" + end match + + o match { + case None => + "" + case Some(x) => + x + } + } + def m15(xs: List[Int]): String = { + xs match { + case _ :: tail => { + if tail.size == 0 then + println("log") + } + "foo" + case Nil => + "bar" + } + } + + // add backticks around operator + object *:{ + def foo = ??? + } + def m16 = + val x = 5 * { + 2 + } == 10 || { + false + } + x `&&` { + true + } + + // leading infix operator + def m17 = + true + && { + false + } + + // ident ending with '_' + def m_(x: String) = ??? + m_ { + "foo" + } + + // do not remove braces in sequence of blocks + def m18(using ctx: String) = println(ctx) + { + given String = "foo" + m18 + } + { + given String = "bar" + m18 + } + def m19(x: String) = { + { + given String = "foo" + m18 + } + { + given String = "bar" + m18 + } + } + + // back-quote end indent before match + def m20 = + val end = "Foo" + end match { + case "Foo" => + case _ => + } + end take 3 +} + +// indent template after self type +class C2 { self => +val x = "" +} +trait C3 { + self => +val x = "" +} +case class C4() { +self => + val y = "" +}