From 0d00987b7fd2310e25e256c9844b161b9afa5eac Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 30 Oct 2023 04:14:08 +0100 Subject: [PATCH 01/22] Added new rule for indentation syntax --- .../scala/fix/IndentationSyntax_Test.scala | 12 +++++++ .../scala/fix/IndentationSyntax_Test.scala | 5 +++ .../META-INF/services/scalafix.v1.Rule | 1 + .../main/scala/fix/IndentationSyntax.scala | 35 +++++++++++++++++++ 4 files changed, 53 insertions(+) create mode 100644 input/src/main/scala/fix/IndentationSyntax_Test.scala create mode 100644 output/src/main/scala/fix/IndentationSyntax_Test.scala create mode 100644 rules/src/main/scala/fix/IndentationSyntax.scala diff --git a/input/src/main/scala/fix/IndentationSyntax_Test.scala b/input/src/main/scala/fix/IndentationSyntax_Test.scala new file mode 100644 index 0000000..3e013ea --- /dev/null +++ b/input/src/main/scala/fix/IndentationSyntax_Test.scala @@ -0,0 +1,12 @@ +/* +rules = [ + IndentationSyntax +] + +IndentationSyntax.addEndMarkers = true +*/ +package fix + +object IndentationSyntax_Test { + +} diff --git a/output/src/main/scala/fix/IndentationSyntax_Test.scala b/output/src/main/scala/fix/IndentationSyntax_Test.scala new file mode 100644 index 0000000..4152281 --- /dev/null +++ b/output/src/main/scala/fix/IndentationSyntax_Test.scala @@ -0,0 +1,5 @@ +package fix + +object IndentationSyntax_Test { + +} diff --git a/rules/src/main/resources/META-INF/services/scalafix.v1.Rule b/rules/src/main/resources/META-INF/services/scalafix.v1.Rule index fba45a6..d6ba90b 100644 --- a/rules/src/main/resources/META-INF/services/scalafix.v1.Rule +++ b/rules/src/main/resources/META-INF/services/scalafix.v1.Rule @@ -1,2 +1,3 @@ fix.Scala3ControlSyntax fix.Scala2ControlSyntax +fix.IndentationSyntax diff --git a/rules/src/main/scala/fix/IndentationSyntax.scala b/rules/src/main/scala/fix/IndentationSyntax.scala new file mode 100644 index 0000000..57547d3 --- /dev/null +++ b/rules/src/main/scala/fix/IndentationSyntax.scala @@ -0,0 +1,35 @@ +package fix + +import scalafix.v1._ +import scala.meta._ +import metaconfig.Configured + +case class IndentationSyntaxParameters( + addEndMarkers: Boolean +) + +object IndentationSyntaxParameters { + val default = IndentationSyntaxParameters(false) + implicit val surface: metaconfig.generic.Surface[IndentationSyntaxParameters] = metaconfig.generic.deriveSurface[IndentationSyntaxParameters] + implicit val decoder: metaconfig.ConfDecoder[IndentationSyntaxParameters] = metaconfig.generic.deriveDecoder(default) +} + +class IndentationSyntax(params: IndentationSyntaxParameters) + extends SyntacticRule("IndentationSyntax") { + + def this() = this(IndentationSyntaxParameters.default) + + override def withConfiguration(config: Configuration): Configured[Rule] = + config.conf.getOrElse("IndentationSyntax")(this.params).map(newParams => new IndentationSyntax(newParams)) + + override def fix(implicit doc: SyntacticDocument): Patch = { + val isLeftBrace = (t: Token) => t.isInstanceOf[scala.meta.tokens.Token.LeftBrace] + val isRightBrace = (t: Token) => t.isInstanceOf[scala.meta.tokens.Token.RightBrace] + + val isWhitespace = (t: Token) => t.isInstanceOf[scala.meta.tokens.Token.Whitespace] + + doc.tree.collect { + case block: Term.Block => Patch.empty + }.asPatch + } +} From c2377c895e60339622cda43f4db0665aa2e1848b Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 30 Oct 2023 10:29:58 +0100 Subject: [PATCH 02/22] Implemented indentation syntax for if-statements --- .../scala/fix/IndentationSyntax_Test.scala | 23 +++- .../scala/fix/IndentationSyntax_Test.scala | 20 +++- .../main/scala/fix/IndentationSyntax.scala | 106 +++++++++++++++++- 3 files changed, 141 insertions(+), 8 deletions(-) diff --git a/input/src/main/scala/fix/IndentationSyntax_Test.scala b/input/src/main/scala/fix/IndentationSyntax_Test.scala index 3e013ea..f069268 100644 --- a/input/src/main/scala/fix/IndentationSyntax_Test.scala +++ b/input/src/main/scala/fix/IndentationSyntax_Test.scala @@ -8,5 +8,26 @@ IndentationSyntax.addEndMarkers = true package fix object IndentationSyntax_Test { - + if (true) { + println("a") + println("b") + println("c") + } + + if (true) { + println("a") + println("b") + println("c") + } + + if (true) { + println("a") + println("b") + println("c") + } + + if (true) + println("a") + println("b") + println("c") } diff --git a/output/src/main/scala/fix/IndentationSyntax_Test.scala b/output/src/main/scala/fix/IndentationSyntax_Test.scala index 4152281..2977202 100644 --- a/output/src/main/scala/fix/IndentationSyntax_Test.scala +++ b/output/src/main/scala/fix/IndentationSyntax_Test.scala @@ -1,5 +1,23 @@ package fix object IndentationSyntax_Test { - + if (true) + println("a") + println("b") + println("c") + + if (true) + println("a") + println("b") + println("c") + + if (true) + println("a") + println("b") + println("c") + + if (true) + println("a") + println("b") + println("c") } diff --git a/rules/src/main/scala/fix/IndentationSyntax.scala b/rules/src/main/scala/fix/IndentationSyntax.scala index 57547d3..a6c4d1d 100644 --- a/rules/src/main/scala/fix/IndentationSyntax.scala +++ b/rules/src/main/scala/fix/IndentationSyntax.scala @@ -5,11 +5,12 @@ import scala.meta._ import metaconfig.Configured case class IndentationSyntaxParameters( - addEndMarkers: Boolean + addEndMarkers: Boolean, + blockSize: Option[Int] // if block size > N lines, add end marker ) object IndentationSyntaxParameters { - val default = IndentationSyntaxParameters(false) + val default = IndentationSyntaxParameters(false, None) implicit val surface: metaconfig.generic.Surface[IndentationSyntaxParameters] = metaconfig.generic.deriveSurface[IndentationSyntaxParameters] implicit val decoder: metaconfig.ConfDecoder[IndentationSyntaxParameters] = metaconfig.generic.deriveDecoder(default) } @@ -23,13 +24,106 @@ class IndentationSyntax(params: IndentationSyntaxParameters) config.conf.getOrElse("IndentationSyntax")(this.params).map(newParams => new IndentationSyntax(newParams)) override def fix(implicit doc: SyntacticDocument): Patch = { - val isLeftBrace = (t: Token) => t.isInstanceOf[scala.meta.tokens.Token.LeftBrace] - val isRightBrace = (t: Token) => t.isInstanceOf[scala.meta.tokens.Token.RightBrace] + def isLeftBrace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.LeftBrace] + def isRightBrace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.RightBrace] - val isWhitespace = (t: Token) => t.isInstanceOf[scala.meta.tokens.Token.Whitespace] + def isWhitespace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Whitespace] + def isNewLine(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.EOL] + def isSpace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.HSpace] + def isIndentation(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Indentation] + def isIndent(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Indentation.Indent] + def isOutdent(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Indentation.Outdent] + doc.tree.collect { - case block: Term.Block => Patch.empty + case block: Term.Block => + // block.tokens.tokens.map(t => println(t, isSpace(t))) + + // the rule only applies to control structures + val isInControlStructure = block.parent match { + case Some(tree) => tree match { + case Term.If(_, _, _) => true + case _ => false + } + case None => false + } + + // if there is no { on the same line as the start of the block, + // assume that block is properly indented + // (according to the rules of significant indentation) + // and we have nothing to do + + val isBracedBlock = block.tokens.takeWhile(t => !isNewLine(t)).exists(isLeftBrace) + if (!isBracedBlock || !isInControlStructure) { + Patch.empty + } else { + val leftBrace = block.tokens.find(t => isLeftBrace(t)).get + // val RightBrace = block.tokens.reverse.find(t => isRightBrace(t)).get + // is it better to reverse and find, or to findLast? + val rightBrace = block.tokens.findLast(t => isRightBrace(t)).get + + // calculate the indentation level of the first line + def isAfterLeftBrace(t: Token) = t.start >= leftBrace.end + val firstIndentedToken = block.tokens.find(t => isAfterLeftBrace(t) && !isWhitespace(t)).get + val newLineAfterLeftBrace = block.tokens.find(t => isAfterLeftBrace(t) && isNewLine(t)).get + val firstIndentationLevel = firstIndentedToken.start - newLineAfterLeftBrace.end + // println("Indentation level for this block:", firstIndentationLevel) + + // If there is no indentation (=0), set it to the indentation of the parent+2 + + // match the indentation on all subsequent levels + def isAfterFirstIndentedToken(t: Token) = t.start >= firstIndentedToken.end + val newLineAfterFirstLine = block.tokens.find(t => isNewLine(t) && isAfterFirstIndentedToken(t)).get + def isFromSecondLine(t: Token) = t.start >= newLineAfterFirstLine.end + + var tokensToIndent = block.tokens.filter(t => isFromSecondLine(t) && !isRightBrace(t)) + var patches: List[Patch] = Nil + + + // while (!tokensToIndent.isEmpty) { + while (tokensToIndent.exists(isNewLine)) { + var (line, lines) = tokensToIndent.span(t => !isNewLine(t)) + // move the new line to the remaining tokens + line = line :+ lines.head + lines = lines.tail + + val (whitespaceToRemove, tokensOnLine) = line.span(t => isSpace(t)) + patches = Patch.removeTokens(whitespaceToRemove) :: patches + patches = Patch.addLeft(tokensOnLine.head, " " * firstIndentationLevel) :: patches + + tokensToIndent = lines + } + + // block.tokens.takeWhile() + + // get lines: split the tokens on new lines (space+, tokens+, \n) + // for every line in the block: + // remove leading whitespace + // put as many spaces as needed + + /* interesting booleans in dialects: + dialects.Scala3.allowEndMarker + dialects.Scala3.allowFewerBraces + dialects.Scala3.allowMultilinePrograms + dialects.Scala3.allowProcedureSyntax + dialects.Scala3.allowSignificantIndentation + dialects.Scala3.allowTypeInBlock + */ + + if (params.addEndMarkers) { + // add END at the indentation level of the parent + Patch.empty + } + + val removeBraces = Patch.removeToken(leftBrace) + Patch.removeToken(rightBrace) + + // We assume that the last token in the block is the closing brace + val whitespaceBeforeRightBrace = block.tokens.reverse.tail.takeWhile(isWhitespace) + val removeWhitespaceBeforeRightBrace = Patch.removeTokens(whitespaceBeforeRightBrace) + + patches.foldLeft(Patch.empty)(_ + _) + removeBraces + removeWhitespaceBeforeRightBrace + } + }.asPatch } } From 35d012935070f58222d95aee8c0ebe481cbccefc Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 30 Oct 2023 11:32:22 +0100 Subject: [PATCH 03/22] Implemented indentation syntax for control structures --- .../scala/fix/IndentationSyntax_Test.scala | 25 +++++++++++++++++++ .../scala/fix/IndentationSyntax_Test.scala | 21 ++++++++++++++++ .../main/scala/fix/IndentationSyntax.scala | 6 +++-- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/input/src/main/scala/fix/IndentationSyntax_Test.scala b/input/src/main/scala/fix/IndentationSyntax_Test.scala index f069268..6f4db3f 100644 --- a/input/src/main/scala/fix/IndentationSyntax_Test.scala +++ b/input/src/main/scala/fix/IndentationSyntax_Test.scala @@ -30,4 +30,29 @@ object IndentationSyntax_Test { println("a") println("b") println("c") + + while (true) { + println("a") + println("b") + println("c") + } + + // spaces instead of tabs + while (true) { + println("a") + println("b") + println("c") + } + + val xs = List(1, 2, 3) + for (x <- xs) { + println("a") + println("b") + println("c") + } + + for (x <- xs) yield { + val myFactor = 2 + myFactor * x + } } diff --git a/output/src/main/scala/fix/IndentationSyntax_Test.scala b/output/src/main/scala/fix/IndentationSyntax_Test.scala index 2977202..945f9a8 100644 --- a/output/src/main/scala/fix/IndentationSyntax_Test.scala +++ b/output/src/main/scala/fix/IndentationSyntax_Test.scala @@ -20,4 +20,25 @@ object IndentationSyntax_Test { println("a") println("b") println("c") + + while (true) + println("a") + println("b") + println("c") + + // spaces instead of tabs + while (true) + println("a") + println("b") + println("c") + + val xs = List(1, 2, 3) + for (x <- xs) + println("a") + println("b") + println("c") + + for (x <- xs) yield + val myFactor = 2 + myFactor * x } diff --git a/rules/src/main/scala/fix/IndentationSyntax.scala b/rules/src/main/scala/fix/IndentationSyntax.scala index a6c4d1d..a891137 100644 --- a/rules/src/main/scala/fix/IndentationSyntax.scala +++ b/rules/src/main/scala/fix/IndentationSyntax.scala @@ -37,12 +37,14 @@ class IndentationSyntax(params: IndentationSyntaxParameters) doc.tree.collect { case block: Term.Block => - // block.tokens.tokens.map(t => println(t, isSpace(t))) - // the rule only applies to control structures val isInControlStructure = block.parent match { case Some(tree) => tree match { case Term.If(_, _, _) => true + case Term.While(_, _) => true + case Term.For(_, _) => true + case Term.ForYield(_, _) => true + case Term.Try(_, _, _) => true case _ => false } case None => false From 16483c8d6e5239bc08057d15ba48635a2c1e6229 Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 30 Oct 2023 11:58:22 +0100 Subject: [PATCH 04/22] Implemented indentation syntax for case blocks (try-catch and pattern matching) --- input/src/main/scala/fix/IndentationSyntax_Test.scala | 10 ++++++++++ output/src/main/scala/fix/IndentationSyntax_Test.scala | 8 ++++++++ rules/src/main/scala/fix/IndentationSyntax.scala | 10 ++++------ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/input/src/main/scala/fix/IndentationSyntax_Test.scala b/input/src/main/scala/fix/IndentationSyntax_Test.scala index 6f4db3f..ed8b2bd 100644 --- a/input/src/main/scala/fix/IndentationSyntax_Test.scala +++ b/input/src/main/scala/fix/IndentationSyntax_Test.scala @@ -55,4 +55,14 @@ object IndentationSyntax_Test { val myFactor = 2 myFactor * x } + + try println(xs) catch { + case ex: Exception => println(ex.toString()) + case ex: NullPointerException => println("Null pointer exception") + } + + val z = Option(xs) match { + case Some(value) => "found" + case None => "not found" + } } diff --git a/output/src/main/scala/fix/IndentationSyntax_Test.scala b/output/src/main/scala/fix/IndentationSyntax_Test.scala index 945f9a8..8e77ed0 100644 --- a/output/src/main/scala/fix/IndentationSyntax_Test.scala +++ b/output/src/main/scala/fix/IndentationSyntax_Test.scala @@ -41,4 +41,12 @@ object IndentationSyntax_Test { for (x <- xs) yield val myFactor = 2 myFactor * x + + try println(xs) catch + case ex: Exception => println(ex.toString()) + case ex: NullPointerException => println("Null pointer exception") + + val z = Option(xs) match + case Some(value) => "found" + case None => "not found" } diff --git a/rules/src/main/scala/fix/IndentationSyntax.scala b/rules/src/main/scala/fix/IndentationSyntax.scala index a891137..54451c7 100644 --- a/rules/src/main/scala/fix/IndentationSyntax.scala +++ b/rules/src/main/scala/fix/IndentationSyntax.scala @@ -36,15 +36,14 @@ class IndentationSyntax(params: IndentationSyntaxParameters) def isOutdent(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Indentation.Outdent] doc.tree.collect { - case block: Term.Block => - // the rule only applies to control structures - val isInControlStructure = block.parent match { + case block @ (_: Term.Block | _: Term.Try | _: Term.Match) => + // if we have a block (not cases), the rule only applies to control structures + val isInControlStructure = if (!block.isInstanceOf[Term.Block]) true else block.parent match { case Some(tree) => tree match { case Term.If(_, _, _) => true case Term.While(_, _) => true case Term.For(_, _) => true case Term.ForYield(_, _) => true - case Term.Try(_, _, _) => true case _ => false } case None => false @@ -125,7 +124,6 @@ class IndentationSyntax(params: IndentationSyntaxParameters) patches.foldLeft(Patch.empty)(_ + _) + removeBraces + removeWhitespaceBeforeRightBrace } - - }.asPatch + }.asPatch } } From 07a182c0575df301e772b07b74357a16a70916a5 Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 30 Oct 2023 15:52:24 +0100 Subject: [PATCH 05/22] Implemented new syntax for templates --- .../scala/fix/IndentationSyntax_Test.scala | 3 +-- .../main/scala/fix/IndentationSyntax.scala | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/output/src/main/scala/fix/IndentationSyntax_Test.scala b/output/src/main/scala/fix/IndentationSyntax_Test.scala index 8e77ed0..8f73c2b 100644 --- a/output/src/main/scala/fix/IndentationSyntax_Test.scala +++ b/output/src/main/scala/fix/IndentationSyntax_Test.scala @@ -1,6 +1,6 @@ package fix -object IndentationSyntax_Test { +object IndentationSyntax_Test : if (true) println("a") println("b") @@ -49,4 +49,3 @@ object IndentationSyntax_Test { val z = Option(xs) match case Some(value) => "found" case None => "not found" -} diff --git a/rules/src/main/scala/fix/IndentationSyntax.scala b/rules/src/main/scala/fix/IndentationSyntax.scala index 54451c7..0689cee 100644 --- a/rules/src/main/scala/fix/IndentationSyntax.scala +++ b/rules/src/main/scala/fix/IndentationSyntax.scala @@ -36,6 +36,25 @@ class IndentationSyntax(params: IndentationSyntaxParameters) def isOutdent(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Indentation.Outdent] doc.tree.collect { + case template: Template => + + val isBracedBlock = template.tokens.takeWhile(t => !isNewLine(t)).exists(isLeftBrace) + if (!isBracedBlock) { + Patch.empty + } else { + val leftBrace = template.tokens.find(t => isLeftBrace(t)).get + val rightBrace = template.tokens.findLast(t => isRightBrace(t)).get + + val replaceLeftBraceWithColon = Patch.replaceToken(leftBrace, ":") + val removeBraces = Patch.removeToken(rightBrace) + + // We assume that the last token in the block is the closing brace + val whitespaceBeforeRightBrace = template.tokens.reverse.tail.takeWhile(isWhitespace) + val removeWhitespaceBeforeRightBrace = Patch.removeTokens(whitespaceBeforeRightBrace) + + replaceLeftBraceWithColon + removeBraces + removeWhitespaceBeforeRightBrace + } + case block @ (_: Term.Block | _: Term.Try | _: Term.Match) => // if we have a block (not cases), the rule only applies to control structures val isInControlStructure = if (!block.isInstanceOf[Term.Block]) true else block.parent match { From 0e3e599589b83b9b7bca58fb0f33ae437c2298b7 Mon Sep 17 00:00:00 2001 From: Mark Date: Sat, 4 Nov 2023 21:45:31 +0100 Subject: [PATCH 06/22] Added new tests --- .../scala/fix/IndentationSyntax_Test.scala | 17 ++++++- .../scala/fix/IndentationSyntax_Test.scala | 4 ++ .../main/scala/fix/IndentationSyntax.scala | 50 +++++++++++++++---- 3 files changed, 61 insertions(+), 10 deletions(-) diff --git a/input/src/main/scala/fix/IndentationSyntax_Test.scala b/input/src/main/scala/fix/IndentationSyntax_Test.scala index ed8b2bd..ddf1579 100644 --- a/input/src/main/scala/fix/IndentationSyntax_Test.scala +++ b/input/src/main/scala/fix/IndentationSyntax_Test.scala @@ -8,7 +8,11 @@ IndentationSyntax.addEndMarkers = true package fix object IndentationSyntax_Test { - if (true) { + if (true) + + + + { println("a") println("b") println("c") @@ -65,4 +69,15 @@ object IndentationSyntax_Test { case Some(value) => "found" case None => "not found" } + + if (true) { + println("nested") + } + + if (true) { + println("nested") + if (true) { + println("nested") + } + } } diff --git a/output/src/main/scala/fix/IndentationSyntax_Test.scala b/output/src/main/scala/fix/IndentationSyntax_Test.scala index 8f73c2b..ea7e739 100644 --- a/output/src/main/scala/fix/IndentationSyntax_Test.scala +++ b/output/src/main/scala/fix/IndentationSyntax_Test.scala @@ -49,3 +49,7 @@ object IndentationSyntax_Test : val z = Option(xs) match case Some(value) => "found" case None => "not found" + + if (true) + if (true) + println("nested") diff --git a/rules/src/main/scala/fix/IndentationSyntax.scala b/rules/src/main/scala/fix/IndentationSyntax.scala index 0689cee..708ef0a 100644 --- a/rules/src/main/scala/fix/IndentationSyntax.scala +++ b/rules/src/main/scala/fix/IndentationSyntax.scala @@ -6,7 +6,9 @@ import metaconfig.Configured case class IndentationSyntaxParameters( addEndMarkers: Boolean, - blockSize: Option[Int] // if block size > N lines, add end marker + blockSize: Option[Int], // if block size > N lines, add end marker + // insertEndMarkerMinLines (look at Scalafmt) + // useOptimalIndentation (instead of first line indentation width, use smallest possible indentation (1 tab)) ) object IndentationSyntaxParameters { @@ -28,12 +30,23 @@ class IndentationSyntax(params: IndentationSyntaxParameters) def isRightBrace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.RightBrace] def isWhitespace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Whitespace] - def isNewLine(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.EOL] + def isNewLine(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.AtEOL] def isSpace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.HSpace] def isIndentation(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Indentation] def isIndent(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Indentation.Indent] def isOutdent(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Indentation.Outdent] + + // Map [Int, Indentation={tabs and spaces}] + + // var x = ??? + + // look into scalatest/scalatest for code examples + + // look into scalaz + // look into dotty/community-build/community-projects + // look into dotty itself: it's a mix of Scala2 and Scala3 syntax + doc.tree.collect { case template: Template => @@ -59,10 +72,10 @@ class IndentationSyntax(params: IndentationSyntaxParameters) // if we have a block (not cases), the rule only applies to control structures val isInControlStructure = if (!block.isInstanceOf[Term.Block]) true else block.parent match { case Some(tree) => tree match { - case Term.If(_, _, _) => true - case Term.While(_, _) => true + case Term.If(_,_,_) | Term.While(_,_) => true case Term.For(_, _) => true case Term.ForYield(_, _) => true + // add val, var, def case _ => false } case None => false @@ -76,11 +89,11 @@ class IndentationSyntax(params: IndentationSyntaxParameters) val isBracedBlock = block.tokens.takeWhile(t => !isNewLine(t)).exists(isLeftBrace) if (!isBracedBlock || !isInControlStructure) { Patch.empty + + // addEndMarker } else { val leftBrace = block.tokens.find(t => isLeftBrace(t)).get - // val RightBrace = block.tokens.reverse.find(t => isRightBrace(t)).get - // is it better to reverse and find, or to findLast? - val rightBrace = block.tokens.findLast(t => isRightBrace(t)).get + val rightBrace = block.tokens.findLast(t => isRightBrace(t)).get // calculate the indentation level of the first line def isAfterLeftBrace(t: Token) = t.start >= leftBrace.end @@ -89,7 +102,7 @@ class IndentationSyntax(params: IndentationSyntaxParameters) val firstIndentationLevel = firstIndentedToken.start - newLineAfterLeftBrace.end // println("Indentation level for this block:", firstIndentationLevel) - // If there is no indentation (=0), set it to the indentation of the parent+2 + // If there is no indentation (=0), set it to the indentation of the parent+1 indentation (2 spaces or tab) // match the indentation on all subsequent levels def isAfterFirstIndentedToken(t: Token) = t.start >= firstIndentedToken.end @@ -130,18 +143,37 @@ class IndentationSyntax(params: IndentationSyntaxParameters) dialects.Scala3.allowTypeInBlock */ + // Further work: use dialects for refining rule + if (params.addEndMarkers) { // add END at the indentation level of the parent Patch.empty } + val addEndMarker = Patch.empty + /* + val addEndMarker = params.addEndMarkers match { + case true => + val endName = block match { + case _: Term.If => "if" + case _: Term.While => "while" + case _: Term.For => "for" + case _: Term.ForYield => "for" + case _: Term.Try => "try" + case _: Term.Match => "match" + } + Patch.addRight(rightBrace, "end " + endName) + case false => Patch.empty + } + */ + val removeBraces = Patch.removeToken(leftBrace) + Patch.removeToken(rightBrace) // We assume that the last token in the block is the closing brace val whitespaceBeforeRightBrace = block.tokens.reverse.tail.takeWhile(isWhitespace) val removeWhitespaceBeforeRightBrace = Patch.removeTokens(whitespaceBeforeRightBrace) - patches.foldLeft(Patch.empty)(_ + _) + removeBraces + removeWhitespaceBeforeRightBrace + Patch.fromIterable(patches) + addEndMarker + removeBraces + removeWhitespaceBeforeRightBrace } }.asPatch } From b9b2be01389b4695330f9bc00a591ac410a9cc4a Mon Sep 17 00:00:00 2001 From: Mark Date: Sat, 11 Nov 2023 22:00:06 +0100 Subject: [PATCH 07/22] Added end markers and removing whitespace before colon --- .../scala/fix/IndentationSyntax_Test.scala | 35 ++---- .../scala/fix/IndentationSyntax_Test.scala | 22 ++-- .../main/scala/fix/IndentationSyntax.scala | 109 +++++++++++++++--- 3 files changed, 104 insertions(+), 62 deletions(-) diff --git a/input/src/main/scala/fix/IndentationSyntax_Test.scala b/input/src/main/scala/fix/IndentationSyntax_Test.scala index ddf1579..d6f8d47 100644 --- a/input/src/main/scala/fix/IndentationSyntax_Test.scala +++ b/input/src/main/scala/fix/IndentationSyntax_Test.scala @@ -8,20 +8,16 @@ IndentationSyntax.addEndMarkers = true package fix object IndentationSyntax_Test { - if (true) - - - - { + if (true) { println("a") println("b") println("c") } if (true) { - println("a") - println("b") - println("c") + println("a2") + println("b2") + println("c2") } if (true) { @@ -60,24 +56,7 @@ object IndentationSyntax_Test { myFactor * x } - try println(xs) catch { - case ex: Exception => println(ex.toString()) - case ex: NullPointerException => println("Null pointer exception") - } - - val z = Option(xs) match { - case Some(value) => "found" - case None => "not found" - } + object MyObject: + override def toString(): String = "myObject" - if (true) { - println("nested") - } - - if (true) { - println("nested") - if (true) { - println("nested") - } - } -} +} \ No newline at end of file diff --git a/output/src/main/scala/fix/IndentationSyntax_Test.scala b/output/src/main/scala/fix/IndentationSyntax_Test.scala index ea7e739..a729f0c 100644 --- a/output/src/main/scala/fix/IndentationSyntax_Test.scala +++ b/output/src/main/scala/fix/IndentationSyntax_Test.scala @@ -1,15 +1,15 @@ package fix -object IndentationSyntax_Test : +object IndentationSyntax_Test: if (true) println("a") println("b") println("c") if (true) - println("a") - println("b") - println("c") + println("a2") + println("b2") + println("c2") if (true) println("a") @@ -42,14 +42,6 @@ object IndentationSyntax_Test : val myFactor = 2 myFactor * x - try println(xs) catch - case ex: Exception => println(ex.toString()) - case ex: NullPointerException => println("Null pointer exception") - - val z = Option(xs) match - case Some(value) => "found" - case None => "not found" - - if (true) - if (true) - println("nested") + object MyObject: + override def toString(): String = "myObject" + end MyObject \ No newline at end of file diff --git a/rules/src/main/scala/fix/IndentationSyntax.scala b/rules/src/main/scala/fix/IndentationSyntax.scala index 708ef0a..b3187bd 100644 --- a/rules/src/main/scala/fix/IndentationSyntax.scala +++ b/rules/src/main/scala/fix/IndentationSyntax.scala @@ -4,14 +4,18 @@ import scalafix.v1._ import scala.meta._ import metaconfig.Configured +case class RLEIndent(char: Char, count: Int) // check that char is strictly in {' ', '\t'} + case class IndentationSyntaxParameters( addEndMarkers: Boolean, blockSize: Option[Int], // if block size > N lines, add end marker // insertEndMarkerMinLines (look at Scalafmt) // useOptimalIndentation (instead of first line indentation width, use smallest possible indentation (1 tab)) + // defaultIndentation: List[RLEIndent] ) object IndentationSyntaxParameters { + // val default = IndentationSyntaxParameters(false, None, List(RLEIndent(' ', 2))) val default = IndentationSyntaxParameters(false, None) implicit val surface: metaconfig.generic.Surface[IndentationSyntaxParameters] = metaconfig.generic.deriveSurface[IndentationSyntaxParameters] implicit val decoder: metaconfig.ConfDecoder[IndentationSyntaxParameters] = metaconfig.generic.deriveDecoder(default) @@ -29,14 +33,11 @@ class IndentationSyntax(params: IndentationSyntaxParameters) def isLeftBrace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.LeftBrace] def isRightBrace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.RightBrace] - def isWhitespace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Whitespace] - def isNewLine(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.AtEOL] - def isSpace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.HSpace] - - def isIndentation(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Indentation] - def isIndent(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Indentation.Indent] - def isOutdent(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Indentation.Outdent] - + def isHSpace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.HSpace] // Space, Tab + def isNewLine(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.AtEOL] // CR, LF, FF + def isSpace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Space] // Space + def isWhitespace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Whitespace] // HSpace, AtEOL + // Map [Int, Indentation={tabs and spaces}] // var x = ??? @@ -47,25 +48,85 @@ class IndentationSyntax(params: IndentationSyntaxParameters) // look into dotty/community-build/community-projects // look into dotty itself: it's a mix of Scala2 and Scala3 syntax + /* + scala: + + * convert to lines + * get whitespace from line (return RLE encoded list of tabs+spaces) + * whitespace comparison operator < (2 tabs, 1 space < 2 tabs, 2 spaces) + * write tests for nested (if and matching, while and try) + 1. control structures 2. try-catch and matching 3. templates + * add end markers for the following types of blocks: + "if", "try", "template" + + */ doc.tree.collect { case template: Template => - - val isBracedBlock = template.tokens.takeWhile(t => !isNewLine(t)).exists(isLeftBrace) + val isBracedBlock = isLeftBrace(template.tokens.dropWhile(t => isWhitespace(t)).head) if (!isBracedBlock) { - Patch.empty + val addEndMarker = if (params.addEndMarkers) { + val lastToken = template.tokens.last + val indentationLevel = template.parent match { + case Some(parentTree) => + // println("I have a parent") + // println(parentTree) + // println(template) + def isColon(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Colon] + val colon = parentTree.tokens.find(isColon).get + def isAfterColon(t: Token) = t.start > colon.end + val whitespaceAfterColon = template.tokens.filter(isAfterColon).takeWhile(isHSpace) + val indentationLevelInParent = whitespaceAfterColon.size + + indentationLevelInParent + case None => + val currentIndentationLevel = 0 + currentIndentationLevel + } + + val newIndentation = indentationLevel - 2 + + // scala.meta.Defn.Object] + val templateName = template.parent.get match { + case member: scala.meta.Member => member.name + } + val stringToAdd = "\n" + " " * newIndentation + "end " + templateName + val endMarker = Patch.addRight(lastToken, stringToAdd) + + endMarker + } else { + Patch.empty + } + addEndMarker } else { val leftBrace = template.tokens.find(t => isLeftBrace(t)).get - val rightBrace = template.tokens.findLast(t => isRightBrace(t)).get + val rightBrace = template.tokens.findLast(t => isRightBrace(t)).get + + // remove HSpace inside the template (between the "extends" clauses and the left brace) + def isBeforeLeftBrace(t: Token) = t.end <= leftBrace.start + val HSpaceBeforeLeftBrace = template.tokens.reverse.filter(t => isBeforeLeftBrace(t)).takeWhile(t => isHSpace(t) && isBeforeLeftBrace(t)) + val removeHSpaceBeforeLeftBrace = Patch.removeTokens(HSpaceBeforeLeftBrace) + + // remove HSpace between the ctor (constructor arguments) and the start of the template + val removeHSpaceinParent = template.parent match { + case Some(parentTree) => + def isBeforeTemplate(t: Token) = t.end <= template.pos.start + val HSpaceBeforeTemplate = parentTree.tokens.reverse.filter(t => isBeforeTemplate(t)).takeWhile(t => isHSpace(t) && isBeforeTemplate(t)) + + val removeHSpaceBeforeTemplate = Patch.removeTokens(HSpaceBeforeTemplate) + + removeHSpaceBeforeTemplate + case None => Patch.empty + } val replaceLeftBraceWithColon = Patch.replaceToken(leftBrace, ":") - val removeBraces = Patch.removeToken(rightBrace) + val removeRightBrace = Patch.removeToken(rightBrace) // We assume that the last token in the block is the closing brace - val whitespaceBeforeRightBrace = template.tokens.reverse.tail.takeWhile(isWhitespace) + val whitespaceBeforeRightBrace = template.tokens.reverse.tail.takeWhile(isHSpace) val removeWhitespaceBeforeRightBrace = Patch.removeTokens(whitespaceBeforeRightBrace) - replaceLeftBraceWithColon + removeBraces + removeWhitespaceBeforeRightBrace + removeHSpaceinParent + removeHSpaceBeforeLeftBrace + replaceLeftBraceWithColon + removeWhitespaceBeforeRightBrace + removeRightBrace } case block @ (_: Term.Block | _: Term.Try | _: Term.Match) => @@ -81,12 +142,21 @@ class IndentationSyntax(params: IndentationSyntaxParameters) case None => false } - // if there is no { on the same line as the start of the block, + // if there is no { at the start of the block, // assume that block is properly indented // (according to the rules of significant indentation) // and we have nothing to do - val isBracedBlock = block.tokens.takeWhile(t => !isNewLine(t)).exists(isLeftBrace) + // the first non-whitespace token after the start of the block is { + val isBracedBlock = isLeftBrace(block.tokens.dropWhile(t => isWhitespace(t)).head) + + /* + println("in block") + println(block.toString()) + println("in control structure? : ", isInControlStructure) + println("is braced block ? : ", isBracedBlock) + */ + if (!isBracedBlock || !isInControlStructure) { Patch.empty @@ -116,11 +186,12 @@ class IndentationSyntax(params: IndentationSyntaxParameters) // while (!tokensToIndent.isEmpty) { while (tokensToIndent.exists(isNewLine)) { var (line, lines) = tokensToIndent.span(t => !isNewLine(t)) - // move the new line to the remaining tokens + // move the newLine to the tokens of the current line line = line :+ lines.head lines = lines.tail val (whitespaceToRemove, tokensOnLine) = line.span(t => isSpace(t)) + patches = Patch.removeTokens(whitespaceToRemove) :: patches patches = Patch.addLeft(tokensOnLine.head, " " * firstIndentationLevel) :: patches @@ -173,7 +244,7 @@ class IndentationSyntax(params: IndentationSyntaxParameters) val whitespaceBeforeRightBrace = block.tokens.reverse.tail.takeWhile(isWhitespace) val removeWhitespaceBeforeRightBrace = Patch.removeTokens(whitespaceBeforeRightBrace) - Patch.fromIterable(patches) + addEndMarker + removeBraces + removeWhitespaceBeforeRightBrace + Patch.fromIterable(patches) + addEndMarker + removeWhitespaceBeforeRightBrace + removeBraces } }.asPatch } From cb016cde2a913f7ce83c04de6594c220ee0ab3ee Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 13 Nov 2023 10:51:55 +0100 Subject: [PATCH 08/22] Added RLEIndent and addEndMarker method --- .../main/scala/fix/IndentationSyntax.scala | 91 ++++--------------- 1 file changed, 19 insertions(+), 72 deletions(-) diff --git a/rules/src/main/scala/fix/IndentationSyntax.scala b/rules/src/main/scala/fix/IndentationSyntax.scala index b3187bd..b62e50b 100644 --- a/rules/src/main/scala/fix/IndentationSyntax.scala +++ b/rules/src/main/scala/fix/IndentationSyntax.scala @@ -4,19 +4,12 @@ import scalafix.v1._ import scala.meta._ import metaconfig.Configured -case class RLEIndent(char: Char, count: Int) // check that char is strictly in {' ', '\t'} - case class IndentationSyntaxParameters( addEndMarkers: Boolean, - blockSize: Option[Int], // if block size > N lines, add end marker - // insertEndMarkerMinLines (look at Scalafmt) - // useOptimalIndentation (instead of first line indentation width, use smallest possible indentation (1 tab)) - // defaultIndentation: List[RLEIndent] ) object IndentationSyntaxParameters { - // val default = IndentationSyntaxParameters(false, None, List(RLEIndent(' ', 2))) - val default = IndentationSyntaxParameters(false, None) + val default = IndentationSyntaxParameters(false) implicit val surface: metaconfig.generic.Surface[IndentationSyntaxParameters] = metaconfig.generic.deriveSurface[IndentationSyntaxParameters] implicit val decoder: metaconfig.ConfDecoder[IndentationSyntaxParameters] = metaconfig.generic.deriveDecoder(default) } @@ -38,29 +31,26 @@ class IndentationSyntax(params: IndentationSyntaxParameters) def isSpace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Space] // Space def isWhitespace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Whitespace] // HSpace, AtEOL - // Map [Int, Indentation={tabs and spaces}] - - // var x = ??? + sealed trait IndentationCharacter + + case object Empty extends IndentationCharacter + case object Space extends IndentationCharacter + case object Tab extends IndentationCharacter + + // type RLEIndent = List[(Int, IndentationCharacter)] + case class RLEIndent(indents: List[(Int, IndentationCharacter)]) + + // val trial = RLEIndent(List((2, Tab), (4, Space), (1, Empty))) + + def addEndMarkerMethod(blockTree: Tree) = { + if (!params.addEndMarkers) { + Patch.empty + } else { + Patch.empty + } + } - // look into scalatest/scalatest for code examples - // look into scalaz - // look into dotty/community-build/community-projects - // look into dotty itself: it's a mix of Scala2 and Scala3 syntax - - /* - scala: - - * convert to lines - * get whitespace from line (return RLE encoded list of tabs+spaces) - * whitespace comparison operator < (2 tabs, 1 space < 2 tabs, 2 spaces) - * write tests for nested (if and matching, while and try) - 1. control structures 2. try-catch and matching 3. templates - * add end markers for the following types of blocks: - "if", "try", "template" - - */ - doc.tree.collect { case template: Template => val isBracedBlock = isLeftBrace(template.tokens.dropWhile(t => isWhitespace(t)).head) @@ -69,9 +59,6 @@ class IndentationSyntax(params: IndentationSyntaxParameters) val lastToken = template.tokens.last val indentationLevel = template.parent match { case Some(parentTree) => - // println("I have a parent") - // println(parentTree) - // println(template) def isColon(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Colon] val colon = parentTree.tokens.find(isColon).get def isAfterColon(t: Token) = t.start > colon.end @@ -150,13 +137,6 @@ class IndentationSyntax(params: IndentationSyntaxParameters) // the first non-whitespace token after the start of the block is { val isBracedBlock = isLeftBrace(block.tokens.dropWhile(t => isWhitespace(t)).head) - /* - println("in block") - println(block.toString()) - println("in control structure? : ", isInControlStructure) - println("is braced block ? : ", isBracedBlock) - */ - if (!isBracedBlock || !isInControlStructure) { Patch.empty @@ -197,24 +177,6 @@ class IndentationSyntax(params: IndentationSyntaxParameters) tokensToIndent = lines } - - // block.tokens.takeWhile() - - // get lines: split the tokens on new lines (space+, tokens+, \n) - // for every line in the block: - // remove leading whitespace - // put as many spaces as needed - - /* interesting booleans in dialects: - dialects.Scala3.allowEndMarker - dialects.Scala3.allowFewerBraces - dialects.Scala3.allowMultilinePrograms - dialects.Scala3.allowProcedureSyntax - dialects.Scala3.allowSignificantIndentation - dialects.Scala3.allowTypeInBlock - */ - - // Further work: use dialects for refining rule if (params.addEndMarkers) { // add END at the indentation level of the parent @@ -222,21 +184,6 @@ class IndentationSyntax(params: IndentationSyntaxParameters) } val addEndMarker = Patch.empty - /* - val addEndMarker = params.addEndMarkers match { - case true => - val endName = block match { - case _: Term.If => "if" - case _: Term.While => "while" - case _: Term.For => "for" - case _: Term.ForYield => "for" - case _: Term.Try => "try" - case _: Term.Match => "match" - } - Patch.addRight(rightBrace, "end " + endName) - case false => Patch.empty - } - */ val removeBraces = Patch.removeToken(leftBrace) + Patch.removeToken(rightBrace) From e678fa30e1a54aa92f37c979f3e4ed64748f4fa3 Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 13 Nov 2023 13:40:25 +0100 Subject: [PATCH 09/22] Added matchTreeTypeWithEndMarkers --- .../main/scala/fix/IndentationSyntax.scala | 46 ++++++++++--------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/rules/src/main/scala/fix/IndentationSyntax.scala b/rules/src/main/scala/fix/IndentationSyntax.scala index b62e50b..c0c7396 100644 --- a/rules/src/main/scala/fix/IndentationSyntax.scala +++ b/rules/src/main/scala/fix/IndentationSyntax.scala @@ -6,10 +6,11 @@ import metaconfig.Configured case class IndentationSyntaxParameters( addEndMarkers: Boolean, + endMarkers: List[String] ) object IndentationSyntaxParameters { - val default = IndentationSyntaxParameters(false) + val default = IndentationSyntaxParameters(false, List("object")) implicit val surface: metaconfig.generic.Surface[IndentationSyntaxParameters] = metaconfig.generic.deriveSurface[IndentationSyntaxParameters] implicit val decoder: metaconfig.ConfDecoder[IndentationSyntaxParameters] = metaconfig.generic.deriveDecoder(default) } @@ -42,27 +43,25 @@ class IndentationSyntax(params: IndentationSyntaxParameters) // val trial = RLEIndent(List((2, Tab), (4, Space), (1, Empty))) - def addEndMarkerMethod(blockTree: Tree) = { - if (!params.addEndMarkers) { - Patch.empty - } else { - Patch.empty + def matchTreeTypeWithEndMarkers(blockTree: Tree): Boolean = { + blockTree.parent.get match { + case _: Defn.Object => params.endMarkers.contains("object") + case _: Defn.Class => params.endMarkers.contains("class") + case _ => false } } - - doc.tree.collect { - case template: Template => - val isBracedBlock = isLeftBrace(template.tokens.dropWhile(t => isWhitespace(t)).head) - if (!isBracedBlock) { - val addEndMarker = if (params.addEndMarkers) { - val lastToken = template.tokens.last - val indentationLevel = template.parent match { + def addEndMarkerMethod(blockTree: Tree): Patch = { + if (!params.addEndMarkers || !matchTreeTypeWithEndMarkers(blockTree)) { + Patch.empty + } else { + val lastToken = blockTree.tokens.last + val indentationLevel = blockTree.parent match { case Some(parentTree) => def isColon(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Colon] val colon = parentTree.tokens.find(isColon).get def isAfterColon(t: Token) = t.start > colon.end - val whitespaceAfterColon = template.tokens.filter(isAfterColon).takeWhile(isHSpace) + val whitespaceAfterColon = blockTree.tokens.filter(isAfterColon).takeWhile(isHSpace) val indentationLevelInParent = whitespaceAfterColon.size indentationLevelInParent @@ -74,17 +73,22 @@ class IndentationSyntax(params: IndentationSyntaxParameters) val newIndentation = indentationLevel - 2 // scala.meta.Defn.Object] - val templateName = template.parent.get match { + val endMarkerName = blockTree.parent.get match { case member: scala.meta.Member => member.name } - val stringToAdd = "\n" + " " * newIndentation + "end " + templateName + val stringToAdd = "\n" + " " * newIndentation + "end " + endMarkerName val endMarker = Patch.addRight(lastToken, stringToAdd) endMarker - } else { - Patch.empty - } - addEndMarker + } + } + + + doc.tree.collect { + case template: Template => + val isBracedBlock = isLeftBrace(template.tokens.dropWhile(t => isWhitespace(t)).head) + if (!isBracedBlock) { + addEndMarkerMethod(template) } else { val leftBrace = template.tokens.find(t => isLeftBrace(t)).get val rightBrace = template.tokens.findLast(t => isRightBrace(t)).get From b6857b355d0b5d3e83dcdfc017e66e0e19b92525 Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 13 Nov 2023 17:56:05 +0100 Subject: [PATCH 10/22] Added splitOn; added end markers for if-statements --- .../scala/fix/IndentationSyntax_Test.scala | 4 + .../main/scala/fix/IndentationSyntax.scala | 94 ++++++++++++------- 2 files changed, 64 insertions(+), 34 deletions(-) diff --git a/output/src/main/scala/fix/IndentationSyntax_Test.scala b/output/src/main/scala/fix/IndentationSyntax_Test.scala index a729f0c..234aa85 100644 --- a/output/src/main/scala/fix/IndentationSyntax_Test.scala +++ b/output/src/main/scala/fix/IndentationSyntax_Test.scala @@ -5,21 +5,25 @@ object IndentationSyntax_Test: println("a") println("b") println("c") + end if if (true) println("a2") println("b2") println("c2") + end if if (true) println("a") println("b") println("c") + end if if (true) println("a") println("b") println("c") + end if while (true) println("a") diff --git a/rules/src/main/scala/fix/IndentationSyntax.scala b/rules/src/main/scala/fix/IndentationSyntax.scala index c0c7396..bcac3b8 100644 --- a/rules/src/main/scala/fix/IndentationSyntax.scala +++ b/rules/src/main/scala/fix/IndentationSyntax.scala @@ -3,6 +3,7 @@ package fix import scalafix.v1._ import scala.meta._ import metaconfig.Configured +import scala.annotation.tailrec case class IndentationSyntaxParameters( addEndMarkers: Boolean, @@ -10,7 +11,7 @@ case class IndentationSyntaxParameters( ) object IndentationSyntaxParameters { - val default = IndentationSyntaxParameters(false, List("object")) + val default = IndentationSyntaxParameters(false, List("object", "if")) implicit val surface: metaconfig.generic.Surface[IndentationSyntaxParameters] = metaconfig.generic.deriveSurface[IndentationSyntaxParameters] implicit val decoder: metaconfig.ConfDecoder[IndentationSyntaxParameters] = metaconfig.generic.deriveDecoder(default) } @@ -43,10 +44,29 @@ class IndentationSyntax(params: IndentationSyntaxParameters) // val trial = RLEIndent(List((2, Tab), (4, Space), (1, Empty))) + @tailrec + def splitOnTail[T](l: Seq[T], acc: Seq[Seq[T]], predicate: T => Boolean): Seq[Seq[T]] = { + val (left, right) = l.span((n: T) => !predicate(n)) + if (right.isEmpty) (left +: acc).reverse + else splitOnTail(right.tail, (left :+ right.head) +: acc, predicate) + } + + def splitOn[T](l: Seq[T], delimiter: T) = { + val res = splitOnTail(l, Seq.empty, ((x: T) => x == delimiter)) + + if (res.last == Seq.empty) { + res.init + } else { + res + } + } + + def matchTreeTypeWithEndMarkers(blockTree: Tree): Boolean = { blockTree.parent.get match { case _: Defn.Object => params.endMarkers.contains("object") case _: Defn.Class => params.endMarkers.contains("class") + case _: Term.If => params.endMarkers.contains("if") case _ => false } } @@ -56,30 +76,43 @@ class IndentationSyntax(params: IndentationSyntaxParameters) Patch.empty } else { val lastToken = blockTree.tokens.last - val indentationLevel = blockTree.parent match { - case Some(parentTree) => - def isColon(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Colon] - val colon = parentTree.tokens.find(isColon).get - def isAfterColon(t: Token) = t.start > colon.end - val whitespaceAfterColon = blockTree.tokens.filter(isAfterColon).takeWhile(isHSpace) - val indentationLevelInParent = whitespaceAfterColon.size - - indentationLevelInParent - case None => - val currentIndentationLevel = 0 - currentIndentationLevel - } - - val newIndentation = indentationLevel - 2 - - // scala.meta.Defn.Object] - val endMarkerName = blockTree.parent.get match { - case member: scala.meta.Member => member.name - } - val stringToAdd = "\n" + " " * newIndentation + "end " + endMarkerName - val endMarker = Patch.addRight(lastToken, stringToAdd) - - endMarker + val indentationLevel = blockTree.parent match { + case Some(parentTree) if blockTree.isInstanceOf[Template] => + def isObject(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwObject] + val objectKw = parentTree.tokens.find(isObject).get + def isBeforeObject(t: Token) = t.end <= objectKw.start + + val whitespaceAfterObject = blockTree.parent.get.parent.get.tokens.tokens.filter(isBeforeObject).reverse.takeWhile(isHSpace) + val indentationLevelInParent = whitespaceAfterObject.size + + indentationLevelInParent + case Some(parentTree) if parentTree.isInstanceOf[Term.If] => + def isIf(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwIf] + val ifKw = parentTree.tokens.find(isIf).get + def isBeforeIf(t: Token) = t.end <= ifKw.start + + val whitespaceAfterIf = parentTree.parent.get.tokens.tokens.filter(isBeforeIf).reverse.takeWhile(isHSpace) + val indentationLevelInParent = whitespaceAfterIf.size + + indentationLevelInParent + case None => + val currentIndentationLevel = 0 + currentIndentationLevel + case _ => + 0 + } + + val newIndentation = math.max(0, indentationLevel) + + // scala.meta.Defn.Object] + val endMarkerName = blockTree.parent.get match { + case member: scala.meta.Member => member.name + case _: Term.If => "if" + } + val stringToAdd = "\n" + " " * newIndentation + "end " + endMarkerName + val endMarker = Patch.addRight(lastToken, stringToAdd) + + endMarker } } @@ -142,9 +175,7 @@ class IndentationSyntax(params: IndentationSyntaxParameters) val isBracedBlock = isLeftBrace(block.tokens.dropWhile(t => isWhitespace(t)).head) if (!isBracedBlock || !isInControlStructure) { - Patch.empty - - // addEndMarker + addEndMarkerMethod(block) } else { val leftBrace = block.tokens.find(t => isLeftBrace(t)).get val rightBrace = block.tokens.findLast(t => isRightBrace(t)).get @@ -182,12 +213,7 @@ class IndentationSyntax(params: IndentationSyntaxParameters) tokensToIndent = lines } - if (params.addEndMarkers) { - // add END at the indentation level of the parent - Patch.empty - } - - val addEndMarker = Patch.empty + val addEndMarker = addEndMarkerMethod(block) val removeBraces = Patch.removeToken(leftBrace) + Patch.removeToken(rightBrace) From dfbc57f3484e9a28d0af7a8d17782c65aba31eb1 Mon Sep 17 00:00:00 2001 From: Mark Date: Tue, 14 Nov 2023 13:35:51 +0100 Subject: [PATCH 11/22] Added tests for end markers --- input/src/main/scala/fix/EndMarker_Test.scala | 12 ++++ .../src/main/scala/fix/EndMarker_Test.scala | 6 ++ .../main/scala/fix/IndentationSyntax.scala | 67 ++++++++++--------- 3 files changed, 53 insertions(+), 32 deletions(-) create mode 100644 input/src/main/scala/fix/EndMarker_Test.scala create mode 100644 output/src/main/scala/fix/EndMarker_Test.scala diff --git a/input/src/main/scala/fix/EndMarker_Test.scala b/input/src/main/scala/fix/EndMarker_Test.scala new file mode 100644 index 0000000..019f05a --- /dev/null +++ b/input/src/main/scala/fix/EndMarker_Test.scala @@ -0,0 +1,12 @@ +package fix +/* +rules = [ + IndentationSyntax +] +IndentationSyntax.addEndMarkers = true +*/ +object EndMarker_Test { + if (true) { + println(true) + } +} diff --git a/output/src/main/scala/fix/EndMarker_Test.scala b/output/src/main/scala/fix/EndMarker_Test.scala new file mode 100644 index 0000000..67dce6b --- /dev/null +++ b/output/src/main/scala/fix/EndMarker_Test.scala @@ -0,0 +1,6 @@ +package fix + +object EndMarker_Test: + if (true) + println(true) + end if diff --git a/rules/src/main/scala/fix/IndentationSyntax.scala b/rules/src/main/scala/fix/IndentationSyntax.scala index bcac3b8..b229f77 100644 --- a/rules/src/main/scala/fix/IndentationSyntax.scala +++ b/rules/src/main/scala/fix/IndentationSyntax.scala @@ -8,6 +8,12 @@ import scala.annotation.tailrec case class IndentationSyntaxParameters( addEndMarkers: Boolean, endMarkers: List[String] + + /* End markers supported: + - object + - class + - if + */ ) object IndentationSyntaxParameters { @@ -52,76 +58,74 @@ class IndentationSyntax(params: IndentationSyntaxParameters) } def splitOn[T](l: Seq[T], delimiter: T) = { - val res = splitOnTail(l, Seq.empty, ((x: T) => x == delimiter)) + val split = splitOnTail(l, Seq.empty, ((x: T) => x == delimiter)) - if (res.last == Seq.empty) { - res.init - } else { - res - } + if (split.last == Seq.empty) split.init else split } - def matchTreeTypeWithEndMarkers(blockTree: Tree): Boolean = { - blockTree.parent.get match { + def matchTreeTypeWithEndMarkers(tree: Tree): Boolean = { + tree match { case _: Defn.Object => params.endMarkers.contains("object") case _: Defn.Class => params.endMarkers.contains("class") case _: Term.If => params.endMarkers.contains("if") + case _ => false } } - def addEndMarkerMethod(blockTree: Tree): Patch = { - if (!params.addEndMarkers || !matchTreeTypeWithEndMarkers(blockTree)) { + def addEndMarkerMethod(tree: Tree): Patch = { + if (!params.addEndMarkers || !matchTreeTypeWithEndMarkers(tree)) { Patch.empty } else { - val lastToken = blockTree.tokens.last - val indentationLevel = blockTree.parent match { - case Some(parentTree) if blockTree.isInstanceOf[Template] => + val lastToken = tree.tokens.last + + // if tree already has an end marker, don't add it! + + // val matchIndentationOf = ??? + + val indentationLevel = tree match { + case objectTree: Defn.Object => def isObject(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwObject] - val objectKw = parentTree.tokens.find(isObject).get + val objectKw = objectTree.tokens.find(isObject).get def isBeforeObject(t: Token) = t.end <= objectKw.start - val whitespaceAfterObject = blockTree.parent.get.parent.get.tokens.tokens.filter(isBeforeObject).reverse.takeWhile(isHSpace) + val whitespaceAfterObject = objectTree.tokens.tokens.filter(isBeforeObject).reverse.takeWhile(isHSpace) val indentationLevelInParent = whitespaceAfterObject.size indentationLevelInParent - case Some(parentTree) if parentTree.isInstanceOf[Term.If] => + case ifTree: Term.If => def isIf(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwIf] - val ifKw = parentTree.tokens.find(isIf).get + val ifKw = ifTree.tokens.find(isIf).get def isBeforeIf(t: Token) = t.end <= ifKw.start - val whitespaceAfterIf = parentTree.parent.get.tokens.tokens.filter(isBeforeIf).reverse.takeWhile(isHSpace) + val whitespaceAfterIf = ifTree.tokens.tokens.filter(isBeforeIf).reverse.takeWhile(isHSpace) val indentationLevelInParent = whitespaceAfterIf.size indentationLevelInParent - case None => - val currentIndentationLevel = 0 - currentIndentationLevel case _ => - 0 + throw new NotImplementedError("End markers for this structure are not implemented.") } - val newIndentation = math.max(0, indentationLevel) - - // scala.meta.Defn.Object] - val endMarkerName = blockTree.parent.get match { + val endMarkerName = tree match { case member: scala.meta.Member => member.name - case _: Term.If => "if" + case _: Term.If => "if" } - val stringToAdd = "\n" + " " * newIndentation + "end " + endMarkerName + + val stringToAdd = "\n" + " " * indentationLevel + "end " + endMarkerName val endMarker = Patch.addRight(lastToken, stringToAdd) endMarker } } + // CHECK IF-THEN-ELSE doc.tree.collect { case template: Template => val isBracedBlock = isLeftBrace(template.tokens.dropWhile(t => isWhitespace(t)).head) if (!isBracedBlock) { - addEndMarkerMethod(template) + addEndMarkerMethod(template.parent.get) } else { val leftBrace = template.tokens.find(t => isLeftBrace(t)).get val rightBrace = template.tokens.findLast(t => isRightBrace(t)).get @@ -175,7 +179,7 @@ class IndentationSyntax(params: IndentationSyntaxParameters) val isBracedBlock = isLeftBrace(block.tokens.dropWhile(t => isWhitespace(t)).head) if (!isBracedBlock || !isInControlStructure) { - addEndMarkerMethod(block) + addEndMarkerMethod(block.parent.get) } else { val leftBrace = block.tokens.find(t => isLeftBrace(t)).get val rightBrace = block.tokens.findLast(t => isRightBrace(t)).get @@ -196,7 +200,6 @@ class IndentationSyntax(params: IndentationSyntaxParameters) var tokensToIndent = block.tokens.filter(t => isFromSecondLine(t) && !isRightBrace(t)) var patches: List[Patch] = Nil - // while (!tokensToIndent.isEmpty) { while (tokensToIndent.exists(isNewLine)) { @@ -213,7 +216,7 @@ class IndentationSyntax(params: IndentationSyntaxParameters) tokensToIndent = lines } - val addEndMarker = addEndMarkerMethod(block) + val addEndMarker = addEndMarkerMethod(block.parent.get) val removeBraces = Patch.removeToken(leftBrace) + Patch.removeToken(rightBrace) From d779ea3a190f43d46c7d93af16b36ea57b0fe003 Mon Sep 17 00:00:00 2001 From: Mark Date: Tue, 14 Nov 2023 23:16:21 +0100 Subject: [PATCH 12/22] Added new end markers --- .../main/scala/fix/IndentationSyntax.scala | 99 ++++++++++++++----- 1 file changed, 72 insertions(+), 27 deletions(-) diff --git a/rules/src/main/scala/fix/IndentationSyntax.scala b/rules/src/main/scala/fix/IndentationSyntax.scala index b229f77..7979fcf 100644 --- a/rules/src/main/scala/fix/IndentationSyntax.scala +++ b/rules/src/main/scala/fix/IndentationSyntax.scala @@ -7,7 +7,8 @@ import scala.annotation.tailrec case class IndentationSyntaxParameters( addEndMarkers: Boolean, - endMarkers: List[String] + endMarkers: List[String], // replace with excluded end markers + // minimumNumberOfLines /* End markers supported: - object @@ -38,10 +39,14 @@ class IndentationSyntax(params: IndentationSyntaxParameters) def isNewLine(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.AtEOL] // CR, LF, FF def isSpace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Space] // Space def isWhitespace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Whitespace] // HSpace, AtEOL + + // isKeyword for end markers + def isObject(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwObject] + def isIf(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwIf] + def isWhile(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwWhile] sealed trait IndentationCharacter - case object Empty extends IndentationCharacter case object Space extends IndentationCharacter case object Tab extends IndentationCharacter @@ -50,13 +55,15 @@ class IndentationSyntax(params: IndentationSyntaxParameters) // val trial = RLEIndent(List((2, Tab), (4, Space), (1, Empty))) + @tailrec def splitOnTail[T](l: Seq[T], acc: Seq[Seq[T]], predicate: T => Boolean): Seq[Seq[T]] = { val (left, right) = l.span((n: T) => !predicate(n)) if (right.isEmpty) (left +: acc).reverse else splitOnTail(right.tail, (left :+ right.head) +: acc, predicate) } - + + // maybe use collection.mutable.Seq ??? def splitOn[T](l: Seq[T], delimiter: T) = { val split = splitOnTail(l, Seq.empty, ((x: T) => x == delimiter)) @@ -66,9 +73,20 @@ class IndentationSyntax(params: IndentationSyntaxParameters) def matchTreeTypeWithEndMarkers(tree: Tree): Boolean = { tree match { - case _: Defn.Object => params.endMarkers.contains("object") - case _: Defn.Class => params.endMarkers.contains("class") - case _: Term.If => params.endMarkers.contains("if") + case _: Defn.Object => params.endMarkers.contains("object") + case _: Defn.Class => params.endMarkers.contains("class") + case _: Defn.Trait => params.endMarkers.contains("trait") + + case _: Term.If => params.endMarkers.contains("if") + case _: Term.While => params.endMarkers.contains("while") + case _: Term.For => params.endMarkers.contains("for") + case _: Term.ForYield => params.endMarkers.contains("forYield") + case _: Term.Match => params.endMarkers.contains("match") + case _: Term.Try => params.endMarkers.contains("try") + case _: Term.New => params.endMarkers.contains("new") + case _: Term.This => params.endMarkers.contains("this") + case _: Decl.Val => params.endMarkers.contains("val") + case _: Decl.Given => params.endMarkers.contains("given") case _ => false } @@ -81,35 +99,47 @@ class IndentationSyntax(params: IndentationSyntaxParameters) val lastToken = tree.tokens.last // if tree already has an end marker, don't add it! + // ideally, we wouldn't have to check for this - // val matchIndentationOf = ??? - - val indentationLevel = tree match { + val keyword = tree match { case objectTree: Defn.Object => - def isObject(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwObject] val objectKw = objectTree.tokens.find(isObject).get - def isBeforeObject(t: Token) = t.end <= objectKw.start - - val whitespaceAfterObject = objectTree.tokens.tokens.filter(isBeforeObject).reverse.takeWhile(isHSpace) - val indentationLevelInParent = whitespaceAfterObject.size - - indentationLevelInParent + objectKw case ifTree: Term.If => - def isIf(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwIf] val ifKw = ifTree.tokens.find(isIf).get - def isBeforeIf(t: Token) = t.end <= ifKw.start - - val whitespaceAfterIf = ifTree.tokens.tokens.filter(isBeforeIf).reverse.takeWhile(isHSpace) - val indentationLevelInParent = whitespaceAfterIf.size - - indentationLevelInParent + ifKw + case whileTree: Term.While => + val whileKw = whileTree.tokens.find(isWhile).get + whileKw case _ => - throw new NotImplementedError("End markers for this structure are not implemented.") + throw new NotImplementedError("End markers for this syntactic structure are not implemented.") } + def isBeforeKeyword(t: Token) = t.end <= keyword.start + val whitespaceAfterKeyword = tree.tokens.tokens.filter(isBeforeKeyword).reverse.takeWhile(isHSpace) + val indentationLevelInParent = whitespaceAfterKeyword.size + + val indentationLevel = indentationLevelInParent + val endMarkerName = tree match { - case member: scala.meta.Member => member.name + case n: Decl.Given if n.isNameAnonymous => "given" + case _: Term.If => "if" + case _: Term.While => "while" + case _: Term.For => "for" + case _: Term.ForYield => "for" + case _: Term.Match => "match" + + case _: Term.Try => "try" + // new this val given + + case _: Term.New => "new" + // case _: Term.This => "this" + case _: Decl.Val => "val" + // case _: Ctor.Secondary => "this" + + + case member: scala.meta.Member => member.name // test if it returns THIS for secondary constructor } val stringToAdd = "\n" + " " * indentationLevel + "end " + endMarkerName @@ -119,9 +149,21 @@ class IndentationSyntax(params: IndentationSyntaxParameters) } } - // CHECK IF-THEN-ELSE + // CHECK IF-THEN-ELSE, both for indentation and + /* + val lines: DocLines: List[Array[Token]] = ??? // compute lines + + val innerToken: Tree = ??? + val line: Int = lines.get(innerToken) + val indent: RLEIndent = lines.getIndent(line) + + + var patches = Nil + */ doc.tree.collect { + // case ifTree: Term.If => + // ??? // call addEndMarker case template: Template => val isBracedBlock = isLeftBrace(template.tokens.dropWhile(t => isWhitespace(t)).head) if (!isBracedBlock) { @@ -181,6 +223,9 @@ class IndentationSyntax(params: IndentationSyntaxParameters) if (!isBracedBlock || !isInControlStructure) { addEndMarkerMethod(block.parent.get) } else { + val tokenList = splitOn(block.tokens.tokens, "\n") + tokenList.foreach(l => l.foreach(t => println(t, t.getClass()))) + val leftBrace = block.tokens.find(t => isLeftBrace(t)).get val rightBrace = block.tokens.findLast(t => isRightBrace(t)).get @@ -216,7 +261,7 @@ class IndentationSyntax(params: IndentationSyntaxParameters) tokensToIndent = lines } - val addEndMarker = addEndMarkerMethod(block.parent.get) + val addEndMarker = addEndMarkerMethod(block.parent.get) // move to different case val removeBraces = Patch.removeToken(leftBrace) + Patch.removeToken(rightBrace) From ced7ec7a797ce5ab71668278b687faf785709819 Mon Sep 17 00:00:00 2001 From: Mark Date: Fri, 17 Nov 2023 08:04:44 +0100 Subject: [PATCH 13/22] Added support for modifier keywords --- input/src/main/scala/fix/EndMarker_Test.scala | 7 + .../scala/fix/IndentationSyntax_Test.scala | 1 - .../src/main/scala/fix/EndMarker_Test.scala | 11 ++ .../scala/fix/IndentationSyntax_Test.scala | 6 +- .../main/scala/fix/IndentationSyntax.scala | 152 +++++++++--------- 5 files changed, 98 insertions(+), 79 deletions(-) diff --git a/input/src/main/scala/fix/EndMarker_Test.scala b/input/src/main/scala/fix/EndMarker_Test.scala index 019f05a..3d11471 100644 --- a/input/src/main/scala/fix/EndMarker_Test.scala +++ b/input/src/main/scala/fix/EndMarker_Test.scala @@ -9,4 +9,11 @@ object EndMarker_Test { if (true) { println(true) } + + class MyNumber(number: Int): + println(number) + + case class MyText(text: String): + println(text) + } diff --git a/input/src/main/scala/fix/IndentationSyntax_Test.scala b/input/src/main/scala/fix/IndentationSyntax_Test.scala index d6f8d47..10f9bb0 100644 --- a/input/src/main/scala/fix/IndentationSyntax_Test.scala +++ b/input/src/main/scala/fix/IndentationSyntax_Test.scala @@ -58,5 +58,4 @@ object IndentationSyntax_Test { object MyObject: override def toString(): String = "myObject" - } \ No newline at end of file diff --git a/output/src/main/scala/fix/EndMarker_Test.scala b/output/src/main/scala/fix/EndMarker_Test.scala index 67dce6b..f78b7f2 100644 --- a/output/src/main/scala/fix/EndMarker_Test.scala +++ b/output/src/main/scala/fix/EndMarker_Test.scala @@ -4,3 +4,14 @@ object EndMarker_Test: if (true) println(true) end if + + class MyNumber(number: Int): + println(number) + end MyNumber + + case class MyText(text: String): + println(text) + end MyText + + +end EndMarker_Test \ No newline at end of file diff --git a/output/src/main/scala/fix/IndentationSyntax_Test.scala b/output/src/main/scala/fix/IndentationSyntax_Test.scala index 234aa85..6e6d410 100644 --- a/output/src/main/scala/fix/IndentationSyntax_Test.scala +++ b/output/src/main/scala/fix/IndentationSyntax_Test.scala @@ -29,12 +29,14 @@ object IndentationSyntax_Test: println("a") println("b") println("c") + end while // spaces instead of tabs while (true) println("a") println("b") println("c") + end while val xs = List(1, 2, 3) for (x <- xs) @@ -48,4 +50,6 @@ object IndentationSyntax_Test: object MyObject: override def toString(): String = "myObject" - end MyObject \ No newline at end of file + end MyObject + +end IndentationSyntax_Test \ No newline at end of file diff --git a/rules/src/main/scala/fix/IndentationSyntax.scala b/rules/src/main/scala/fix/IndentationSyntax.scala index 7979fcf..250bed3 100644 --- a/rules/src/main/scala/fix/IndentationSyntax.scala +++ b/rules/src/main/scala/fix/IndentationSyntax.scala @@ -7,8 +7,8 @@ import scala.annotation.tailrec case class IndentationSyntaxParameters( addEndMarkers: Boolean, - endMarkers: List[String], // replace with excluded end markers - // minimumNumberOfLines + skipEndMarkers: List[String], + minLinesForEndMarker: Int, /* End markers supported: - object @@ -18,7 +18,7 @@ case class IndentationSyntaxParameters( ) object IndentationSyntaxParameters { - val default = IndentationSyntaxParameters(false, List("object", "if")) + val default = IndentationSyntaxParameters(false, Nil, 0) implicit val surface: metaconfig.generic.Surface[IndentationSyntaxParameters] = metaconfig.generic.deriveSurface[IndentationSyntaxParameters] implicit val decoder: metaconfig.ConfDecoder[IndentationSyntaxParameters] = metaconfig.generic.deriveDecoder(default) } @@ -42,58 +42,42 @@ class IndentationSyntax(params: IndentationSyntaxParameters) // isKeyword for end markers def isObject(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwObject] - def isIf(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwIf] - def isWhile(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwWhile] + def isClass(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwClass] + def isTrait(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwTrait] + def isPackage(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwPackage] + // How come there's no support for the "extension" keyword ??? + // def isExtension(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwExtension] - sealed trait IndentationCharacter - - case object Space extends IndentationCharacter - case object Tab extends IndentationCharacter - - // type RLEIndent = List[(Int, IndentationCharacter)] - case class RLEIndent(indents: List[(Int, IndentationCharacter)]) - - // val trial = RLEIndent(List((2, Tab), (4, Space), (1, Empty))) - - - @tailrec - def splitOnTail[T](l: Seq[T], acc: Seq[Seq[T]], predicate: T => Boolean): Seq[Seq[T]] = { - val (left, right) = l.span((n: T) => !predicate(n)) - if (right.isEmpty) (left +: acc).reverse - else splitOnTail(right.tail, (left :+ right.head) +: acc, predicate) - } - - // maybe use collection.mutable.Seq ??? - def splitOn[T](l: Seq[T], delimiter: T) = { - val split = splitOnTail(l, Seq.empty, ((x: T) => x == delimiter)) - - if (split.last == Seq.empty) split.init else split - } + // can I use a union type here??? + def isMod(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.ModifierKeyword] || t.isInstanceOf[scala.meta.tokens.Token.KwCase] + // WHAT'S A "PACKAGE OBJECT" ??? + def isIf(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwIf] + def isWhile(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwWhile] - def matchTreeTypeWithEndMarkers(tree: Tree): Boolean = { + def endMarkerSkipped(tree: Tree): Boolean = { tree match { - case _: Defn.Object => params.endMarkers.contains("object") - case _: Defn.Class => params.endMarkers.contains("class") - case _: Defn.Trait => params.endMarkers.contains("trait") - - case _: Term.If => params.endMarkers.contains("if") - case _: Term.While => params.endMarkers.contains("while") - case _: Term.For => params.endMarkers.contains("for") - case _: Term.ForYield => params.endMarkers.contains("forYield") - case _: Term.Match => params.endMarkers.contains("match") - case _: Term.Try => params.endMarkers.contains("try") - case _: Term.New => params.endMarkers.contains("new") - case _: Term.This => params.endMarkers.contains("this") - case _: Decl.Val => params.endMarkers.contains("val") - case _: Decl.Given => params.endMarkers.contains("given") + case _: Defn.Object => params.skipEndMarkers.contains("object") + case _: Defn.Class => params.skipEndMarkers.contains("class") + case _: Defn.Trait => params.skipEndMarkers.contains("trait") + + case _: Term.If => params.skipEndMarkers.contains("if") + case _: Term.While => params.skipEndMarkers.contains("while") + case _: Term.For => params.skipEndMarkers.contains("for") + case _: Term.ForYield => params.skipEndMarkers.contains("forYield") + case _: Term.Match => params.skipEndMarkers.contains("match") + case _: Term.Try => params.skipEndMarkers.contains("try") + case _: Term.New => params.skipEndMarkers.contains("new") + case _: Term.This => params.skipEndMarkers.contains("this") + case _: Decl.Val => params.skipEndMarkers.contains("val") + case _: Decl.Given => params.skipEndMarkers.contains("given") case _ => false } } def addEndMarkerMethod(tree: Tree): Patch = { - if (!params.addEndMarkers || !matchTreeTypeWithEndMarkers(tree)) { + if (!params.addEndMarkers || endMarkerSkipped(tree)) { Patch.empty } else { val lastToken = tree.tokens.last @@ -101,29 +85,22 @@ class IndentationSyntax(params: IndentationSyntaxParameters) // if tree already has an end marker, don't add it! // ideally, we wouldn't have to check for this - val keyword = tree match { - case objectTree: Defn.Object => - val objectKw = objectTree.tokens.find(isObject).get - objectKw - case ifTree: Term.If => - val ifKw = ifTree.tokens.find(isIf).get - ifKw - case whileTree: Term.While => - val whileKw = whileTree.tokens.find(isWhile).get - whileKw - case _ => - throw new NotImplementedError("End markers for this syntactic structure are not implemented.") + val lookFor = tree match { + case defn: Stat.WithMods if !defn.mods.isEmpty => isMod _ + case _: Defn.Object => isObject _ + case _: Defn.Class => isClass _ + case _: Defn.Trait => isTrait _ + case _: Term.If => isIf _ + case _: Term.While => isWhile _ + case _ => throw new NotImplementedError("End markers for this syntactic structure are not implemented.") } + val keyword = tree.tokens.find(lookFor).getOrElse(throw new Exception("The given tree doesn't contain its defining keyword.")) def isBeforeKeyword(t: Token) = t.end <= keyword.start - val whitespaceAfterKeyword = tree.tokens.tokens.filter(isBeforeKeyword).reverse.takeWhile(isHSpace) - val indentationLevelInParent = whitespaceAfterKeyword.size - - val indentationLevel = indentationLevelInParent + val whitespaceBeforeKeyword = tree.tokens.tokens.filter(isBeforeKeyword).reverse.takeWhile(isHSpace) + val indentationLevel = whitespaceBeforeKeyword.size val endMarkerName = tree match { - case n: Decl.Given if n.isNameAnonymous => "given" - case _: Term.If => "if" case _: Term.While => "while" case _: Term.For => "for" @@ -131,13 +108,11 @@ class IndentationSyntax(params: IndentationSyntaxParameters) case _: Term.Match => "match" case _: Term.Try => "try" - // new this val given - case _: Term.New => "new" // case _: Term.This => "this" - case _: Decl.Val => "val" // case _: Ctor.Secondary => "this" - + case _: Decl.Val => "val" + case n: Decl.Given if n.isNameAnonymous => "given" case member: scala.meta.Member => member.name // test if it returns THIS for secondary constructor } @@ -149,6 +124,30 @@ class IndentationSyntax(params: IndentationSyntaxParameters) } } + sealed trait IndentationCharacter + + case object Space extends IndentationCharacter + case object Tab extends IndentationCharacter + + // type RLEIndent = List[(Int, IndentationCharacter)] + case class RLEIndent(indents: List[(Int, IndentationCharacter)]) + + // val trial = RLEIndent(List((2, Tab), (4, Space), (1, Empty))) + + @tailrec + def splitOnTail[T](l: Seq[T], acc: Seq[Seq[T]], predicate: T => Boolean): Seq[Seq[T]] = { + val (left, right) = l.span((n: T) => !predicate(n)) + if (right.isEmpty) (left +: acc).reverse + else splitOnTail(right.tail, (left :+ right.head) +: acc, predicate) + } + + // maybe use collection.mutable.Seq ??? + def splitOn[T](l: Seq[T], delimiter: T) = { + val split = splitOnTail(l, Seq.empty, ((x: T) => x == delimiter)) + + if (split.last == Seq.empty) split.init else split + } + // CHECK IF-THEN-ELSE, both for indentation and /* val lines: DocLines: List[Array[Token]] = ??? // compute lines @@ -160,14 +159,18 @@ class IndentationSyntax(params: IndentationSyntaxParameters) var patches = Nil */ + val tokenList = splitOn(doc.tree.tokens.tokens, "\n") + // tokenList.foreach(line => line.foreach(t => println(s"\"$t\", ${t.getClass().getCanonicalName()}"))) doc.tree.collect { - // case ifTree: Term.If => - // ??? // call addEndMarker + case controlStructureTree @ (_: Term.If | _: Term.While) => + addEndMarkerMethod(controlStructureTree) + case containingTemplateTree @ (_: Defn.Object | _: Defn.Class | _: Defn.Trait) => + addEndMarkerMethod(containingTemplateTree) case template: Template => val isBracedBlock = isLeftBrace(template.tokens.dropWhile(t => isWhitespace(t)).head) if (!isBracedBlock) { - addEndMarkerMethod(template.parent.get) + Patch.empty } else { val leftBrace = template.tokens.find(t => isLeftBrace(t)).get val rightBrace = template.tokens.findLast(t => isRightBrace(t)).get @@ -221,11 +224,8 @@ class IndentationSyntax(params: IndentationSyntaxParameters) val isBracedBlock = isLeftBrace(block.tokens.dropWhile(t => isWhitespace(t)).head) if (!isBracedBlock || !isInControlStructure) { - addEndMarkerMethod(block.parent.get) + Patch.empty } else { - val tokenList = splitOn(block.tokens.tokens, "\n") - tokenList.foreach(l => l.foreach(t => println(t, t.getClass()))) - val leftBrace = block.tokens.find(t => isLeftBrace(t)).get val rightBrace = block.tokens.findLast(t => isRightBrace(t)).get @@ -261,15 +261,13 @@ class IndentationSyntax(params: IndentationSyntaxParameters) tokensToIndent = lines } - val addEndMarker = addEndMarkerMethod(block.parent.get) // move to different case - val removeBraces = Patch.removeToken(leftBrace) + Patch.removeToken(rightBrace) // We assume that the last token in the block is the closing brace val whitespaceBeforeRightBrace = block.tokens.reverse.tail.takeWhile(isWhitespace) val removeWhitespaceBeforeRightBrace = Patch.removeTokens(whitespaceBeforeRightBrace) - Patch.fromIterable(patches) + addEndMarker + removeWhitespaceBeforeRightBrace + removeBraces + Patch.fromIterable(patches) + removeWhitespaceBeforeRightBrace + removeBraces } }.asPatch } From f49954e2cc47e6f280e835aa96f8bc319ecfa0da Mon Sep 17 00:00:00 2001 From: Mark Date: Fri, 17 Nov 2023 12:13:40 +0100 Subject: [PATCH 14/22] Added support for extensions --- input/src/main/scala/fix/EndMarker_Test.scala | 5 ++++- output/src/main/scala/fix/EndMarker_Test.scala | 6 +++++- rules/src/main/scala/fix/IndentationSyntax.scala | 10 +++++----- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/input/src/main/scala/fix/EndMarker_Test.scala b/input/src/main/scala/fix/EndMarker_Test.scala index 3d11471..e29c2c2 100644 --- a/input/src/main/scala/fix/EndMarker_Test.scala +++ b/input/src/main/scala/fix/EndMarker_Test.scala @@ -15,5 +15,8 @@ object EndMarker_Test { case class MyText(text: String): println(text) - + + extension (m: MyText) + def myPrint = println(m.text) + } diff --git a/output/src/main/scala/fix/EndMarker_Test.scala b/output/src/main/scala/fix/EndMarker_Test.scala index f78b7f2..49aeff3 100644 --- a/output/src/main/scala/fix/EndMarker_Test.scala +++ b/output/src/main/scala/fix/EndMarker_Test.scala @@ -12,6 +12,10 @@ object EndMarker_Test: case class MyText(text: String): println(text) end MyText - + + extension (m: MyText) + def myPrint = println(m.text) + end extension + end EndMarker_Test \ No newline at end of file diff --git a/rules/src/main/scala/fix/IndentationSyntax.scala b/rules/src/main/scala/fix/IndentationSyntax.scala index 250bed3..935ba5d 100644 --- a/rules/src/main/scala/fix/IndentationSyntax.scala +++ b/rules/src/main/scala/fix/IndentationSyntax.scala @@ -45,10 +45,8 @@ class IndentationSyntax(params: IndentationSyntaxParameters) def isClass(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwClass] def isTrait(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwTrait] def isPackage(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwPackage] - // How come there's no support for the "extension" keyword ??? - // def isExtension(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwExtension] + def isExtension(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Ident] && t.text == "extension" - // can I use a union type here??? def isMod(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.ModifierKeyword] || t.isInstanceOf[scala.meta.tokens.Token.KwCase] // WHAT'S A "PACKAGE OBJECT" ??? @@ -90,6 +88,7 @@ class IndentationSyntax(params: IndentationSyntaxParameters) case _: Defn.Object => isObject _ case _: Defn.Class => isClass _ case _: Defn.Trait => isTrait _ + case _: Defn.ExtensionGroup => isExtension _ case _: Term.If => isIf _ case _: Term.While => isWhile _ case _ => throw new NotImplementedError("End markers for this syntactic structure are not implemented.") @@ -113,6 +112,7 @@ class IndentationSyntax(params: IndentationSyntaxParameters) // case _: Ctor.Secondary => "this" case _: Decl.Val => "val" case n: Decl.Given if n.isNameAnonymous => "given" + case _: Defn.ExtensionGroup => "extension" case member: scala.meta.Member => member.name // test if it returns THIS for secondary constructor } @@ -160,12 +160,12 @@ class IndentationSyntax(params: IndentationSyntaxParameters) var patches = Nil */ val tokenList = splitOn(doc.tree.tokens.tokens, "\n") - // tokenList.foreach(line => line.foreach(t => println(s"\"$t\", ${t.getClass().getCanonicalName()}"))) + tokenList.foreach(line => line.foreach(t => println(s"\"$t\", ${t.getClass().getCanonicalName()}"))) doc.tree.collect { case controlStructureTree @ (_: Term.If | _: Term.While) => addEndMarkerMethod(controlStructureTree) - case containingTemplateTree @ (_: Defn.Object | _: Defn.Class | _: Defn.Trait) => + case containingTemplateTree @ (_: Defn.Object | _: Defn.Class | _: Defn.Trait | _: Defn.ExtensionGroup) => addEndMarkerMethod(containingTemplateTree) case template: Template => val isBracedBlock = isLeftBrace(template.tokens.dropWhile(t => isWhitespace(t)).head) From 5c1be188a68afaee4e520125a56be3ad20dd9147 Mon Sep 17 00:00:00 2001 From: Mark Date: Fri, 17 Nov 2023 13:00:24 +0100 Subject: [PATCH 15/22] Added comparison operators for RLEIndent --- .../main/scala/fix/IndentationSyntax.scala | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/rules/src/main/scala/fix/IndentationSyntax.scala b/rules/src/main/scala/fix/IndentationSyntax.scala index 935ba5d..a2aeacd 100644 --- a/rules/src/main/scala/fix/IndentationSyntax.scala +++ b/rules/src/main/scala/fix/IndentationSyntax.scala @@ -130,7 +130,55 @@ class IndentationSyntax(params: IndentationSyntaxParameters) case object Tab extends IndentationCharacter // type RLEIndent = List[(Int, IndentationCharacter)] - case class RLEIndent(indents: List[(Int, IndentationCharacter)]) + case class RLEIndent(indents: List[(Int, IndentationCharacter)]) { + def < (that: RLEIndent): Boolean = { + (this, that) match { + case (RLEIndent(Nil), RLEIndent(Nil)) => false + case (RLEIndent(Nil), RLEIndent(h2 :: t2)) => true + case (RLEIndent(h1 :: t1), RLEIndent(Nil)) => false + case (RLEIndent(h1 :: t1), RLEIndent(h2 :: t2)) if h1._2 == h2._2 && h1._1 < h2._1 => RLEIndent(t1) <= RLEIndent(t2) + case (RLEIndent(h1 :: t1), RLEIndent(h2 :: t2)) if h1._2 == h2._2 && h1._1 == h2._1 => RLEIndent(t1) < RLEIndent(t2) + case _ => false + } + } + + @scala.annotation.tailrec + def == (that: RLEIndent): Boolean = { + (this, that) match { + case (RLEIndent(Nil), RLEIndent(Nil)) => true + case (RLEIndent(h1 :: t1), RLEIndent(h2 :: t2)) if h1 == h2 => RLEIndent(t1) == RLEIndent(t2) + case _ => false + } + } + + def <= (that: RLEIndent): Boolean = this < that || this == that + def != (that: RLEIndent): Boolean = !(this == that) + def > (that: RLEIndent): Boolean = !(this <= that) + def >= (that: RLEIndent): Boolean = !(this < that) + } + + def rleFromTokens(tokens: Seq[Token]): RLEIndent = { + def pack[T](xs: Seq[T]): List[List[T]] = { + xs match { + case Nil => Nil + case head :: next => + val (same, rest) = next.span(_ == head) + (head :: same) :: pack(rest) + } + } + + def convertToIndentationCharacter(token: scala.meta.tokens.Token): IndentationCharacter = { + token match { + case _: scala.meta.tokens.Token.Space => Space + case _: scala.meta.tokens.Token.Tab => Tab + case _ => throw new Exception("The given token is not HSpace (not a space or a tab).") + } + } + + val indents = pack(tokens).map(l => (l.size, convertToIndentationCharacter(l.head))) + + RLEIndent(indents) + } // val trial = RLEIndent(List((2, Tab), (4, Space), (1, Empty))) From 3098c90a3a65a23aedeb6d227b480f93bf3c78d5 Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 20 Nov 2023 14:48:07 +0100 Subject: [PATCH 16/22] Reference example of end markers works --- .../fix/Scala3ReferenceEndMarker_Test.scala | 37 +++++ .../src/main/scala/fix/EndMarker_Test.scala | 1 + .../scala/fix/IndentationSyntax_Test.scala | 3 + .../fix/Scala3ReferenceEndMarker_Test.scala | 46 ++++++ .../main/scala/fix/IndentationSyntax.scala | 138 +++++++++++++----- 5 files changed, 187 insertions(+), 38 deletions(-) create mode 100644 input/src/main/scala/fix/Scala3ReferenceEndMarker_Test.scala create mode 100644 output/src/main/scala/fix/Scala3ReferenceEndMarker_Test.scala diff --git a/input/src/main/scala/fix/Scala3ReferenceEndMarker_Test.scala b/input/src/main/scala/fix/Scala3ReferenceEndMarker_Test.scala new file mode 100644 index 0000000..2a0781a --- /dev/null +++ b/input/src/main/scala/fix/Scala3ReferenceEndMarker_Test.scala @@ -0,0 +1,37 @@ +package p1.p2: +/* +rules = [ + IndentationSyntax +] +IndentationSyntax.addEndMarkers = true +*/ + abstract class C(): + + def this(x: Int) = + this() + if x > 0 then + val a :: b = + x :: Nil + var y = + x + while y > 0 do + println(y) + y -= 1 + try + x match + case 0 => println("0") + case _ => + finally + println("done") + + def f: String + + object C: + given C = + new C: + def f = "!" + + extension (x: C) + def ff: String = x.f ++ x.f + +end p2 diff --git a/output/src/main/scala/fix/EndMarker_Test.scala b/output/src/main/scala/fix/EndMarker_Test.scala index 49aeff3..1337ff5 100644 --- a/output/src/main/scala/fix/EndMarker_Test.scala +++ b/output/src/main/scala/fix/EndMarker_Test.scala @@ -15,6 +15,7 @@ object EndMarker_Test: extension (m: MyText) def myPrint = println(m.text) + end myPrint end extension diff --git a/output/src/main/scala/fix/IndentationSyntax_Test.scala b/output/src/main/scala/fix/IndentationSyntax_Test.scala index 6e6d410..1444d61 100644 --- a/output/src/main/scala/fix/IndentationSyntax_Test.scala +++ b/output/src/main/scala/fix/IndentationSyntax_Test.scala @@ -39,6 +39,7 @@ object IndentationSyntax_Test: end while val xs = List(1, 2, 3) + end xs for (x <- xs) println("a") println("b") @@ -46,10 +47,12 @@ object IndentationSyntax_Test: for (x <- xs) yield val myFactor = 2 + end myFactor myFactor * x object MyObject: override def toString(): String = "myObject" + end toString end MyObject end IndentationSyntax_Test \ No newline at end of file diff --git a/output/src/main/scala/fix/Scala3ReferenceEndMarker_Test.scala b/output/src/main/scala/fix/Scala3ReferenceEndMarker_Test.scala new file mode 100644 index 0000000..f4fb64a --- /dev/null +++ b/output/src/main/scala/fix/Scala3ReferenceEndMarker_Test.scala @@ -0,0 +1,46 @@ +package p1.p2: + + abstract class C(): + + def this(x: Int) = + this() + if x > 0 then + val a :: b = + x :: Nil + end val + var y = + x + end y + while y > 0 do + println(y) + y -= 1 + end while + try + x match + case 0 => println("0") + case _ => + end match + finally + println("done") + end try + end if + end this + + def f: String + end C + + object C: + given C = + new C: + def f = "!" + end f + end new + end given + end C + + extension (x: C) + def ff: String = x.f ++ x.f + end ff + end extension + +end p2 diff --git a/rules/src/main/scala/fix/IndentationSyntax.scala b/rules/src/main/scala/fix/IndentationSyntax.scala index a2aeacd..e2ff023 100644 --- a/rules/src/main/scala/fix/IndentationSyntax.scala +++ b/rules/src/main/scala/fix/IndentationSyntax.scala @@ -10,6 +10,11 @@ case class IndentationSyntaxParameters( skipEndMarkers: List[String], minLinesForEndMarker: Int, + // When to Use End Markers + // check docs!!! + // for stuff like checking for empty lines, we have to implement linesOfTree to get the lines corresponding to the tree + // then we will know whether there are empty lines and how many lines there are in total + /* End markers supported: - object - class @@ -49,28 +54,44 @@ class IndentationSyntax(params: IndentationSyntaxParameters) def isMod(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.ModifierKeyword] || t.isInstanceOf[scala.meta.tokens.Token.KwCase] + def isVal(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwVal] + def isVar(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwVar] + def isDef(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwDef] + + def isGiven(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwGiven] + + def isThis(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwThis] + def isNew(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwNew] + + // WHAT'S A "PACKAGE OBJECT" ??? def isIf(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwIf] def isWhile(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwWhile] + def isTry(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwTry] + + def isIdentifier(t: Token, name: String) = t.isInstanceOf[scala.meta.tokens.Token.Ident] && t.text == name def endMarkerSkipped(tree: Tree): Boolean = { tree match { - case _: Defn.Object => params.skipEndMarkers.contains("object") - case _: Defn.Class => params.skipEndMarkers.contains("class") - case _: Defn.Trait => params.skipEndMarkers.contains("trait") - - case _: Term.If => params.skipEndMarkers.contains("if") - case _: Term.While => params.skipEndMarkers.contains("while") - case _: Term.For => params.skipEndMarkers.contains("for") - case _: Term.ForYield => params.skipEndMarkers.contains("forYield") - case _: Term.Match => params.skipEndMarkers.contains("match") - case _: Term.Try => params.skipEndMarkers.contains("try") - case _: Term.New => params.skipEndMarkers.contains("new") - case _: Term.This => params.skipEndMarkers.contains("this") - case _: Decl.Val => params.skipEndMarkers.contains("val") - case _: Decl.Given => params.skipEndMarkers.contains("given") - - case _ => false + case _: Defn.Object => params.skipEndMarkers.contains("object") + case _: Defn.Class => params.skipEndMarkers.contains("class") + case _: Defn.Trait => params.skipEndMarkers.contains("trait") + + case _: Term.If => params.skipEndMarkers.contains("if") + case _: Term.While => params.skipEndMarkers.contains("while") + case _: Term.For => params.skipEndMarkers.contains("for") + case _: Term.ForYield => params.skipEndMarkers.contains("forYield") + case _: Term.Match => params.skipEndMarkers.contains("match") + case _: Term.Try => params.skipEndMarkers.contains("try") + case _: Term.NewAnonymous => params.skipEndMarkers.contains("new") + case _: Ctor.Secondary => params.skipEndMarkers.contains("this") + case _: Defn.Val => params.skipEndMarkers.contains("val") + case _: Defn.Var => params.skipEndMarkers.contains("var") + case _: Defn.Def => params.skipEndMarkers.contains("def") + + case _: Defn.GivenAlias => params.skipEndMarkers.contains("given") + + case _ => false } } @@ -85,13 +106,24 @@ class IndentationSyntax(params: IndentationSyntaxParameters) val lookFor = tree match { case defn: Stat.WithMods if !defn.mods.isEmpty => isMod _ - case _: Defn.Object => isObject _ - case _: Defn.Class => isClass _ - case _: Defn.Trait => isTrait _ - case _: Defn.ExtensionGroup => isExtension _ - case _: Term.If => isIf _ - case _: Term.While => isWhile _ - case _ => throw new NotImplementedError("End markers for this syntactic structure are not implemented.") + case _: Defn.Object => isObject _ + case _: Defn.Class => isClass _ + case _: Defn.Trait => isTrait _ + case _: Defn.ExtensionGroup => isExtension _ + case _: Term.If => isIf _ + case _: Term.While => isWhile _ + case _: Term.Try => isTry _ + + case _: Defn.Val => isVal _ + case _: Defn.Var => isVar _ + case _: Defn.Def => isDef _ + + case _: Defn.GivenAlias => isGiven _ + case _: Term.NewAnonymous => isNew _ + case _: Ctor.Secondary => isDef _ // secondary ctor definition starts with the "def" keyword + case matchTerm: Term.Match => isIdentifier(_, matchTerm.expr.toString()) // match expression starts with an identifier + + case _ => throw new NotImplementedError("End markers for this syntactic structure are not implemented.") } val keyword = tree.tokens.find(lookFor).getOrElse(throw new Exception("The given tree doesn't contain its defining keyword.")) @@ -107,12 +139,36 @@ class IndentationSyntax(params: IndentationSyntaxParameters) case _: Term.Match => "match" case _: Term.Try => "try" - case _: Term.New => "new" + case _: Term.NewAnonymous => "new" // case _: Term.This => "this" - // case _: Ctor.Secondary => "this" - case _: Decl.Val => "val" - case n: Decl.Given if n.isNameAnonymous => "given" + + case valTree: Defn.Val => valTree.pats.head match { + case pat: Pat.Var => pat.name.value + case _ => "val" + } + + case varTree: Defn.Var => varTree.pats.head match { + case pat: Pat.Var => pat.name.value + case _ => "var" + } + + case givenTree: Defn.GivenAlias => givenTree.name match { + case _: Name.Anonymous => "given" + case n: Name => n.value + } + case _: Defn.ExtensionGroup => "extension" + + case defTree: Defn.Def => defTree.name match { + case _: Name.Anonymous => "def" + case n: Name => n.value + } + + // case _: Ctor.Secondary => "this" + case ctorTree: Ctor.Secondary => ctorTree.name match { + case _: Name.This => "this" + case n: Name => n.value + } case member: scala.meta.Member => member.name // test if it returns THIS for secondary constructor } @@ -175,13 +231,11 @@ class IndentationSyntax(params: IndentationSyntaxParameters) } } - val indents = pack(tokens).map(l => (l.size, convertToIndentationCharacter(l.head))) + val indents = pack(tokens.map(convertToIndentationCharacter)).map(l => (l.size, l.head)) RLEIndent(indents) } - // val trial = RLEIndent(List((2, Tab), (4, Space), (1, Empty))) - @tailrec def splitOnTail[T](l: Seq[T], acc: Seq[Seq[T]], predicate: T => Boolean): Seq[Seq[T]] = { val (left, right) = l.span((n: T) => !predicate(n)) @@ -207,14 +261,21 @@ class IndentationSyntax(params: IndentationSyntaxParameters) var patches = Nil */ - val tokenList = splitOn(doc.tree.tokens.tokens, "\n") - tokenList.foreach(line => line.foreach(t => println(s"\"$t\", ${t.getClass().getCanonicalName()}"))) + val docLines = splitOn(doc.tree.tokens.tokens, "\n") + // docLines.foreach(line => line.foreach(token => println(s"\"$token\", ${token.getClass().getCanonicalName()}"))) + + var endMarkerPatches: List[Patch] = Nil doc.tree.collect { - case controlStructureTree @ (_: Term.If | _: Term.While) => - addEndMarkerMethod(controlStructureTree) - case containingTemplateTree @ (_: Defn.Object | _: Defn.Class | _: Defn.Trait | _: Defn.ExtensionGroup) => - addEndMarkerMethod(containingTemplateTree) + case controlStructureTree @ (_: Term.If | _: Term.While | _: Term.Try | _: Term.Match) => + endMarkerPatches = addEndMarkerMethod(controlStructureTree) :: endMarkerPatches + Patch.empty + case defnTree @ (_: Defn.Val | _: Defn.Var | _: Defn.Def | _: Defn.GivenAlias | _: Term.NewAnonymous) => + endMarkerPatches = addEndMarkerMethod(defnTree) :: endMarkerPatches + Patch.empty + case containingTemplateTree @ (_: Defn.Object | _: Defn.Class | _: Defn.Trait | _: Defn.ExtensionGroup | _: Ctor.Secondary) => + endMarkerPatches = addEndMarkerMethod(containingTemplateTree) :: endMarkerPatches + Patch.empty case template: Template => val isBracedBlock = isLeftBrace(template.tokens.dropWhile(t => isWhitespace(t)).head) if (!isBracedBlock) { @@ -269,7 +330,7 @@ class IndentationSyntax(params: IndentationSyntaxParameters) // and we have nothing to do // the first non-whitespace token after the start of the block is { - val isBracedBlock = isLeftBrace(block.tokens.dropWhile(t => isWhitespace(t)).head) + val isBracedBlock = !block.tokens.isEmpty && isLeftBrace(block.tokens.dropWhile(t => isWhitespace(t)).head) if (!isBracedBlock || !isInControlStructure) { Patch.empty @@ -317,6 +378,7 @@ class IndentationSyntax(params: IndentationSyntaxParameters) Patch.fromIterable(patches) + removeWhitespaceBeforeRightBrace + removeBraces } - }.asPatch + }.asPatch + Patch.fromIterable(endMarkerPatches) } + } From f06a60f62e4a14a37f0560f4157d30b697082191 Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 29 Nov 2023 11:55:30 +0100 Subject: [PATCH 17/22] Commiting before refactoring indentation patches --- .../main/scala/fix/IndentationSyntax.scala | 61 ++++++++++++------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/rules/src/main/scala/fix/IndentationSyntax.scala b/rules/src/main/scala/fix/IndentationSyntax.scala index e2ff023..948dc0c 100644 --- a/rules/src/main/scala/fix/IndentationSyntax.scala +++ b/rules/src/main/scala/fix/IndentationSyntax.scala @@ -4,26 +4,22 @@ import scalafix.v1._ import scala.meta._ import metaconfig.Configured import scala.annotation.tailrec +import scala.collection.mutable case class IndentationSyntaxParameters( addEndMarkers: Boolean, skipEndMarkers: List[String], minLinesForEndMarker: Int, + defaultIndentation: String, // When to Use End Markers // check docs!!! // for stuff like checking for empty lines, we have to implement linesOfTree to get the lines corresponding to the tree // then we will know whether there are empty lines and how many lines there are in total - - /* End markers supported: - - object - - class - - if - */ ) object IndentationSyntaxParameters { - val default = IndentationSyntaxParameters(false, Nil, 0) + val default = IndentationSyntaxParameters(false, Nil, 0, " ") implicit val surface: metaconfig.generic.Surface[IndentationSyntaxParameters] = metaconfig.generic.deriveSurface[IndentationSyntaxParameters] implicit val decoder: metaconfig.ConfDecoder[IndentationSyntaxParameters] = metaconfig.generic.deriveDecoder(default) } @@ -52,6 +48,7 @@ class IndentationSyntax(params: IndentationSyntaxParameters) def isPackage(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwPackage] def isExtension(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Ident] && t.text == "extension" + // mod or case def isMod(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.ModifierKeyword] || t.isInstanceOf[scala.meta.tokens.Token.KwCase] def isVal(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwVal] @@ -231,11 +228,13 @@ class IndentationSyntax(params: IndentationSyntaxParameters) } } - val indents = pack(tokens.map(convertToIndentationCharacter)).map(l => (l.size, l.head)) + val indents = pack(tokens.toList.map(convertToIndentationCharacter)).map(l => (l.size, l.head)) RLEIndent(indents) } + def rleFromString = ??? + @tailrec def splitOnTail[T](l: Seq[T], acc: Seq[Seq[T]], predicate: T => Boolean): Seq[Seq[T]] = { val (left, right) = l.span((n: T) => !predicate(n)) @@ -244,12 +243,15 @@ class IndentationSyntax(params: IndentationSyntaxParameters) } // maybe use collection.mutable.Seq ??? - def splitOn[T](l: Seq[T], delimiter: T) = { - val split = splitOnTail(l, Seq.empty, ((x: T) => x == delimiter)) + def splitOn(l: Seq[Token], delimiter: String) = { + val split = splitOnTail(l, Seq.empty, ((x: Token) => x.text == delimiter)) if (split.last == Seq.empty) split.init else split } + // subset of lines containing given tree + // def linesFromTree(t: Tree) + // CHECK IF-THEN-ELSE, both for indentation and /* val lines: DocLines: List[Array[Token]] = ??? // compute lines @@ -261,21 +263,30 @@ class IndentationSyntax(params: IndentationSyntaxParameters) var patches = Nil */ - val docLines = splitOn(doc.tree.tokens.tokens, "\n") - // docLines.foreach(line => line.foreach(token => println(s"\"$token\", ${token.getClass().getCanonicalName()}"))) + val linesByToken: Map[Token, Int] = ??? + val oldIndentationByLine: Seq[RLEIndent] = ??? + val newIndentationByLine: mutable.Seq[RLEIndent] = mutable.Seq(oldIndentationByLine: _*) - var endMarkerPatches: List[Patch] = Nil + val docLines: Seq[Seq[Token]] = splitOn(doc.tree.tokens.tokens, "\n") + val indentedLine = docLines.map(line => { + val (whitespace, remainingTokens) = line.span(isHSpace) - doc.tree.collect { + (rleFromTokens(whitespace), remainingTokens) + }) + // docLines.foreach(line => line.foreach(token => println(s"\"$token\", ${token.getClass().getCanonicalName()}, ${isHSpace(token)}"))) + indentedLine.foreach(println) + + val endMarkerPatches: List[Patch] = doc.tree.collect { + // end markers case controlStructureTree @ (_: Term.If | _: Term.While | _: Term.Try | _: Term.Match) => - endMarkerPatches = addEndMarkerMethod(controlStructureTree) :: endMarkerPatches - Patch.empty + addEndMarkerMethod(controlStructureTree) case defnTree @ (_: Defn.Val | _: Defn.Var | _: Defn.Def | _: Defn.GivenAlias | _: Term.NewAnonymous) => - endMarkerPatches = addEndMarkerMethod(defnTree) :: endMarkerPatches - Patch.empty + addEndMarkerMethod(defnTree) case containingTemplateTree @ (_: Defn.Object | _: Defn.Class | _: Defn.Trait | _: Defn.ExtensionGroup | _: Ctor.Secondary) => - endMarkerPatches = addEndMarkerMethod(containingTemplateTree) :: endMarkerPatches - Patch.empty + addEndMarkerMethod(containingTemplateTree) + }.reverse + + val bracePatches = doc.tree.collect { case template: Template => val isBracedBlock = isLeftBrace(template.tokens.dropWhile(t => isWhitespace(t)).head) if (!isBracedBlock) { @@ -312,6 +323,7 @@ class IndentationSyntax(params: IndentationSyntaxParameters) } case block @ (_: Term.Block | _: Term.Try | _: Term.Match) => + // later can be removed // if we have a block (not cases), the rule only applies to control structures val isInControlStructure = if (!block.isInstanceOf[Term.Block]) true else block.parent match { case Some(tree) => tree match { @@ -340,6 +352,7 @@ class IndentationSyntax(params: IndentationSyntaxParameters) // calculate the indentation level of the first line def isAfterLeftBrace(t: Token) = t.start >= leftBrace.end + block.parent.get.tokens.head val firstIndentedToken = block.tokens.find(t => isAfterLeftBrace(t) && !isWhitespace(t)).get val newLineAfterLeftBrace = block.tokens.find(t => isAfterLeftBrace(t) && isNewLine(t)).get val firstIndentationLevel = firstIndentedToken.start - newLineAfterLeftBrace.end @@ -378,7 +391,13 @@ class IndentationSyntax(params: IndentationSyntaxParameters) Patch.fromIterable(patches) + removeWhitespaceBeforeRightBrace + removeBraces } - }.asPatch + Patch.fromIterable(endMarkerPatches) + } + + val computeIndentationPatches = doc.tree.collect { + case _ => Patch.empty + } + + bracePatches.asPatch + endMarkerPatches.asPatch ++ computeIndentationPatches } } From 19f1ac3a6134a116c229198bf7b65e07bb352ede Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 29 Nov 2023 13:30:38 +0100 Subject: [PATCH 18/22] Added brace patches --- input/src/main/scala/fix/EndMarker_Test.scala | 3 +- .../src/main/scala/fix/EndMarker_Test.scala | 6 +- .../main/scala/fix/IndentationSyntax.scala | 302 +++++++++--------- 3 files changed, 150 insertions(+), 161 deletions(-) diff --git a/input/src/main/scala/fix/EndMarker_Test.scala b/input/src/main/scala/fix/EndMarker_Test.scala index e29c2c2..c67cb5f 100644 --- a/input/src/main/scala/fix/EndMarker_Test.scala +++ b/input/src/main/scala/fix/EndMarker_Test.scala @@ -10,8 +10,9 @@ object EndMarker_Test { println(true) } - class MyNumber(number: Int): + class MyNumber(number: Int) extends AnyRef { println(number) + } case class MyText(text: String): println(text) diff --git a/output/src/main/scala/fix/EndMarker_Test.scala b/output/src/main/scala/fix/EndMarker_Test.scala index 1337ff5..258b6d5 100644 --- a/output/src/main/scala/fix/EndMarker_Test.scala +++ b/output/src/main/scala/fix/EndMarker_Test.scala @@ -1,11 +1,11 @@ package fix object EndMarker_Test: - if (true) + if (true) println(true) end if - class MyNumber(number: Int): + class MyNumber(number: Int) extends AnyRef: println(number) end MyNumber @@ -17,6 +17,4 @@ object EndMarker_Test: def myPrint = println(m.text) end myPrint end extension - - end EndMarker_Test \ No newline at end of file diff --git a/rules/src/main/scala/fix/IndentationSyntax.scala b/rules/src/main/scala/fix/IndentationSyntax.scala index 948dc0c..f8771b8 100644 --- a/rules/src/main/scala/fix/IndentationSyntax.scala +++ b/rules/src/main/scala/fix/IndentationSyntax.scala @@ -5,6 +5,7 @@ import scala.meta._ import metaconfig.Configured import scala.annotation.tailrec import scala.collection.mutable +import com.google.protobuf.Empty case class IndentationSyntaxParameters( addEndMarkers: Boolean, @@ -33,41 +34,9 @@ class IndentationSyntax(params: IndentationSyntaxParameters) config.conf.getOrElse("IndentationSyntax")(this.params).map(newParams => new IndentationSyntax(newParams)) override def fix(implicit doc: SyntacticDocument): Patch = { - def isLeftBrace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.LeftBrace] - def isRightBrace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.RightBrace] - - def isHSpace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.HSpace] // Space, Tab - def isNewLine(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.AtEOL] // CR, LF, FF - def isSpace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Space] // Space - def isWhitespace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Whitespace] // HSpace, AtEOL - - // isKeyword for end markers - def isObject(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwObject] - def isClass(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwClass] - def isTrait(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwTrait] - def isPackage(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwPackage] - def isExtension(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Ident] && t.text == "extension" - - // mod or case - def isMod(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.ModifierKeyword] || t.isInstanceOf[scala.meta.tokens.Token.KwCase] - - def isVal(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwVal] - def isVar(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwVar] - def isDef(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwDef] - - def isGiven(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwGiven] - - def isThis(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwThis] - def isNew(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwNew] - - - // WHAT'S A "PACKAGE OBJECT" ??? - def isIf(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwIf] - def isWhile(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwWhile] - def isTry(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwTry] - - def isIdentifier(t: Token, name: String) = t.isInstanceOf[scala.meta.tokens.Token.Ident] && t.text == name - + /* + * START OF END MARKERS + */ def endMarkerSkipped(tree: Tree): Boolean = { tree match { case _: Defn.Object => params.skipEndMarkers.contains("object") @@ -98,9 +67,6 @@ class IndentationSyntax(params: IndentationSyntaxParameters) } else { val lastToken = tree.tokens.last - // if tree already has an end marker, don't add it! - // ideally, we wouldn't have to check for this - val lookFor = tree match { case defn: Stat.WithMods if !defn.mods.isEmpty => isMod _ case _: Defn.Object => isObject _ @@ -176,13 +142,18 @@ class IndentationSyntax(params: IndentationSyntaxParameters) endMarker } } + /* + * END OF END MARKERS + */ + /* + * START OF RLE INDENT + */ sealed trait IndentationCharacter case object Space extends IndentationCharacter case object Tab extends IndentationCharacter - // type RLEIndent = List[(Int, IndentationCharacter)] case class RLEIndent(indents: List[(Int, IndentationCharacter)]) { def < (that: RLEIndent): Boolean = { (this, that) match { @@ -233,8 +204,37 @@ class IndentationSyntax(params: IndentationSyntaxParameters) RLEIndent(indents) } - def rleFromString = ??? + def rleFromString(s: String): RLEIndent = { + def pack[T](xs: Seq[T]): List[List[T]] = { + xs match { + case Nil => Nil + case head :: next => + val (same, rest) = next.span(_ == head) + (head :: same) :: pack(rest) + } + } + + def convertToIndentationCharacter(character: Char): IndentationCharacter = { + character match { + case _: ' ' => Space + case _: '\t' => Tab + case _ => throw new Exception("The given character is not HSpace (not a space or a tab).") + } + } + + val indents = pack(s.toList.map(convertToIndentationCharacter)).map(l => (l.size, l.head)) + + RLEIndent(indents) + } + + val defaultIndentation = rleFromString(params.defaultIndentation) + /* + * END OF RLE INDENT + */ + /* + * START OF SPLITTING INTO LINES (INDENTS AND TOKENS) + */ @tailrec def splitOnTail[T](l: Seq[T], acc: Seq[Seq[T]], predicate: T => Boolean): Seq[Seq[T]] = { val (left, right) = l.span((n: T) => !predicate(n)) @@ -242,17 +242,34 @@ class IndentationSyntax(params: IndentationSyntaxParameters) else splitOnTail(right.tail, (left :+ right.head) +: acc, predicate) } - // maybe use collection.mutable.Seq ??? def splitOn(l: Seq[Token], delimiter: String) = { val split = splitOnTail(l, Seq.empty, ((x: Token) => x.text == delimiter)) if (split.last == Seq.empty) split.init else split } + + val docLines: Seq[Seq[Token]] = splitOn(doc.tree.tokens.tokens, "\n") + val indentsAndTokens = docLines.map(line => { + val (whitespace, remainingTokens) = line.span(isHSpace) - // subset of lines containing given tree - // def linesFromTree(t: Tree) + (rleFromTokens(whitespace), remainingTokens) + }) + + // println("*** START OF NEW DOCUMENT ***") + // indentsAndTokens.foreach(println) + /* + * END OF SPLITTING INTO LINES (INDENTS AND TOKENS) + */ + + def linesFromTree(t: Tree) = { + // returns subset of lines containing given tree + val (from, to) = (t.tokens.head, t.tokens.last) + val firstLine = docLines.indexWhere(_ == from) + val lastLine = docLines.lastIndexWhere(_ == to) + + docLines.slice(firstLine, lastLine + 1) + } - // CHECK IF-THEN-ELSE, both for indentation and /* val lines: DocLines: List[Array[Token]] = ??? // compute lines @@ -260,24 +277,17 @@ class IndentationSyntax(params: IndentationSyntaxParameters) val line: Int = lines.get(innerToken) val indent: RLEIndent = lines.getIndent(line) - var patches = Nil */ - val linesByToken: Map[Token, Int] = ??? - val oldIndentationByLine: Seq[RLEIndent] = ??? - val newIndentationByLine: mutable.Seq[RLEIndent] = mutable.Seq(oldIndentationByLine: _*) - val docLines: Seq[Seq[Token]] = splitOn(doc.tree.tokens.tokens, "\n") - val indentedLine = docLines.map(line => { - val (whitespace, remainingTokens) = line.span(isHSpace) - - (rleFromTokens(whitespace), remainingTokens) - }) - // docLines.foreach(line => line.foreach(token => println(s"\"$token\", ${token.getClass().getCanonicalName()}, ${isHSpace(token)}"))) - indentedLine.foreach(println) + // val linesByToken: Map[Token, Int] = ??? + // val oldIndentationByLine: Seq[RLEIndent] = ??? + // val newIndentationByLine: mutable.Seq[RLEIndent] = mutable.Seq(oldIndentationByLine: _*) + /* + * START PATCHES + */ val endMarkerPatches: List[Patch] = doc.tree.collect { - // end markers case controlStructureTree @ (_: Term.If | _: Term.While | _: Term.Try | _: Term.Match) => addEndMarkerMethod(controlStructureTree) case defnTree @ (_: Defn.Val | _: Defn.Var | _: Defn.Def | _: Defn.GivenAlias | _: Term.NewAnonymous) => @@ -286,118 +296,98 @@ class IndentationSyntax(params: IndentationSyntaxParameters) addEndMarkerMethod(containingTemplateTree) }.reverse - val bracePatches = doc.tree.collect { - case template: Template => - val isBracedBlock = isLeftBrace(template.tokens.dropWhile(t => isWhitespace(t)).head) - if (!isBracedBlock) { - Patch.empty - } else { - val leftBrace = template.tokens.find(t => isLeftBrace(t)).get - val rightBrace = template.tokens.findLast(t => isRightBrace(t)).get - - // remove HSpace inside the template (between the "extends" clauses and the left brace) - def isBeforeLeftBrace(t: Token) = t.end <= leftBrace.start - val HSpaceBeforeLeftBrace = template.tokens.reverse.filter(t => isBeforeLeftBrace(t)).takeWhile(t => isHSpace(t) && isBeforeLeftBrace(t)) - val removeHSpaceBeforeLeftBrace = Patch.removeTokens(HSpaceBeforeLeftBrace) - - // remove HSpace between the ctor (constructor arguments) and the start of the template - val removeHSpaceinParent = template.parent match { - case Some(parentTree) => - def isBeforeTemplate(t: Token) = t.end <= template.pos.start - val HSpaceBeforeTemplate = parentTree.tokens.reverse.filter(t => isBeforeTemplate(t)).takeWhile(t => isHSpace(t) && isBeforeTemplate(t)) - - val removeHSpaceBeforeTemplate = Patch.removeTokens(HSpaceBeforeTemplate) - - removeHSpaceBeforeTemplate - case None => Patch.empty - } + // TODO: think of something more clever + def isBraced(t: Tree) = t.tokens.exists(isLeftBrace) && t.tokens.exists(isRightBrace) - val replaceLeftBraceWithColon = Patch.replaceToken(leftBrace, ":") - val removeRightBrace = Patch.removeToken(rightBrace) + val bracePatches = doc.tree.collect { + case template: Template if isBraced(template) => + val leftBrace = template.parent.get.tokens.find(isLeftBrace).get + val rightBrace = template.parent.get.tokens.findLast(isRightBrace).get - // We assume that the last token in the block is the closing brace - val whitespaceBeforeRightBrace = template.tokens.reverse.tail.takeWhile(isHSpace) - val removeWhitespaceBeforeRightBrace = Patch.removeTokens(whitespaceBeforeRightBrace) + def isBeforeLeftBrace(t: Token) = t.end <= leftBrace.start + def isBeforeRightBrace(t: Token) = t.end <= rightBrace.start + + val tokensBeforeLeftBrace = template.parent.get.tokens.takeWhile(isBeforeLeftBrace) + val whitespaceBeforeLeftBrace = tokensBeforeLeftBrace.takeRightWhile(isWhitespace) + val tokensBeforeRightBrace = template.parent.get.tokens.takeWhile(isBeforeRightBrace) + val whitespaceBeforeRightBrace = tokensBeforeRightBrace.takeRightWhile(isWhitespace) - removeHSpaceinParent + removeHSpaceBeforeLeftBrace + replaceLeftBraceWithColon + removeWhitespaceBeforeRightBrace + removeRightBrace - } + val removeWhitespaceBeforeLeftBrace = Patch.removeTokens(whitespaceBeforeLeftBrace) + val replaceLeftBraceWithColon = Patch.replaceToken(leftBrace, ":") + val removeWhitespaceBeforeRightBrace = Patch.removeTokens(whitespaceBeforeRightBrace) + val removeRightBrace = Patch.removeToken(rightBrace) - case block @ (_: Term.Block | _: Term.Try | _: Term.Match) => - // later can be removed - // if we have a block (not cases), the rule only applies to control structures - val isInControlStructure = if (!block.isInstanceOf[Term.Block]) true else block.parent match { - case Some(tree) => tree match { - case Term.If(_,_,_) | Term.While(_,_) => true - case Term.For(_, _) => true - case Term.ForYield(_, _) => true - // add val, var, def - case _ => false - } - case None => false - } + removeWhitespaceBeforeLeftBrace + replaceLeftBraceWithColon + removeWhitespaceBeforeRightBrace + removeRightBrace - // if there is no { at the start of the block, - // assume that block is properly indented - // (according to the rules of significant indentation) - // and we have nothing to do - - // the first non-whitespace token after the start of the block is { - val isBracedBlock = !block.tokens.isEmpty && isLeftBrace(block.tokens.dropWhile(t => isWhitespace(t)).head) - - if (!isBracedBlock || !isInControlStructure) { - Patch.empty - } else { - val leftBrace = block.tokens.find(t => isLeftBrace(t)).get - val rightBrace = block.tokens.findLast(t => isRightBrace(t)).get - - // calculate the indentation level of the first line - def isAfterLeftBrace(t: Token) = t.start >= leftBrace.end - block.parent.get.tokens.head - val firstIndentedToken = block.tokens.find(t => isAfterLeftBrace(t) && !isWhitespace(t)).get - val newLineAfterLeftBrace = block.tokens.find(t => isAfterLeftBrace(t) && isNewLine(t)).get - val firstIndentationLevel = firstIndentedToken.start - newLineAfterLeftBrace.end - // println("Indentation level for this block:", firstIndentationLevel) - - // If there is no indentation (=0), set it to the indentation of the parent+1 indentation (2 spaces or tab) - - // match the indentation on all subsequent levels - def isAfterFirstIndentedToken(t: Token) = t.start >= firstIndentedToken.end - val newLineAfterFirstLine = block.tokens.find(t => isNewLine(t) && isAfterFirstIndentedToken(t)).get - def isFromSecondLine(t: Token) = t.start >= newLineAfterFirstLine.end - - var tokensToIndent = block.tokens.filter(t => isFromSecondLine(t) && !isRightBrace(t)) - var patches: List[Patch] = Nil - - // while (!tokensToIndent.isEmpty) { - while (tokensToIndent.exists(isNewLine)) { - var (line, lines) = tokensToIndent.span(t => !isNewLine(t)) - // move the newLine to the tokens of the current line - line = line :+ lines.head - lines = lines.tail - - val (whitespaceToRemove, tokensOnLine) = line.span(t => isSpace(t)) - - patches = Patch.removeTokens(whitespaceToRemove) :: patches - patches = Patch.addLeft(tokensOnLine.head, " " * firstIndentationLevel) :: patches - - tokensToIndent = lines - } + case block: Term.Block if isBraced(block) => + val leftBrace = block.parent.get.tokens.find(isLeftBrace).get + val rightBrace = block.parent.get.tokens.findLast(isRightBrace).get - val removeBraces = Patch.removeToken(leftBrace) + Patch.removeToken(rightBrace) + def isBeforeLeftBrace(t: Token) = t.end <= leftBrace.start + def isBeforeRightBrace(t: Token) = t.end <= rightBrace.start + + val tokensBeforeLeftBrace = block.parent.get.tokens.takeWhile(isBeforeLeftBrace) + val whitespaceBeforeLeftBrace = tokensBeforeLeftBrace.takeRightWhile(isWhitespace) + val tokensBeforeRightBrace = block.parent.get.tokens.takeWhile(isBeforeRightBrace) + val whitespaceBeforeRightBrace = tokensBeforeRightBrace.takeRightWhile(isWhitespace) - // We assume that the last token in the block is the closing brace - val whitespaceBeforeRightBrace = block.tokens.reverse.tail.takeWhile(isWhitespace) - val removeWhitespaceBeforeRightBrace = Patch.removeTokens(whitespaceBeforeRightBrace) + val removeWhitespaceBeforeLeftBrace = Patch.removeTokens(whitespaceBeforeLeftBrace) + val removeLeftBrace = Patch.removeToken(leftBrace) + val removeWhitespaceBeforeRightBrace = Patch.removeTokens(whitespaceBeforeRightBrace) + val removeRightBrace = Patch.removeToken(rightBrace) - Patch.fromIterable(patches) + removeWhitespaceBeforeRightBrace + removeBraces - } - } + removeWhitespaceBeforeLeftBrace + removeLeftBrace + removeWhitespaceBeforeRightBrace + removeRightBrace + + case _ => Patch.empty + } val computeIndentationPatches = doc.tree.collect { case _ => Patch.empty } - + bracePatches.asPatch + endMarkerPatches.asPatch ++ computeIndentationPatches + /* + * END PATCHES + */ } + /* + * START OF HELPER FUNCTIONS (TOKEN PREDICATES) + */ + def isLeftBrace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.LeftBrace] + def isRightBrace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.RightBrace] + + def isHSpace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.HSpace] // Space, Tab + def isNewLine(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.AtEOL] // CR, LF, FF + def isSpace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Space] // Space + def isWhitespace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Whitespace] // HSpace, AtEOL + + def isObject(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwObject] + def isClass(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwClass] + def isTrait(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwTrait] + def isPackage(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwPackage] + def isExtension(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Ident] && t.text == "extension" + def isMod(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.ModifierKeyword] || t.isInstanceOf[scala.meta.tokens.Token.KwCase] + def isVal(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwVal] + def isVar(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwVar] + def isDef(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwDef] + def isGiven(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwGiven] + def isThis(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwThis] + def isNew(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwNew] + def isIf(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwIf] + def isWhile(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwWhile] + def isTry(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwTry] + def isIdentifier(t: Token, name: String) = t.isInstanceOf[scala.meta.tokens.Token.Ident] && t.text == name + /* + * END OF HELPER FUNCTIONS (TOKEN PREDICATES) + */ + + /* a note regarding templates and package objects + + A template defines the type signature, behavior and initial state of a trait or class of objects or of a single object. + https://www.scala-lang.org/files/archive/spec/3.4/05-classes-and-objects.html#templates + + Also, package objects in Scalameta: + https://scalameta.org/docs/trees/quasiquotes.html + */ } From 6f82429d7b468b87e77ac931884597ebc3ccfea8 Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 29 Nov 2023 16:06:10 +0100 Subject: [PATCH 19/22] replace right brace with end marker --- .../scala/fix/IndentationSyntax_Test.scala | 2 +- .../scala/fix/IndentationSyntax_Test.scala | 17 ++- .../main/scala/fix/IndentationSyntax.scala | 137 +++++++----------- 3 files changed, 61 insertions(+), 95 deletions(-) diff --git a/input/src/main/scala/fix/IndentationSyntax_Test.scala b/input/src/main/scala/fix/IndentationSyntax_Test.scala index 10f9bb0..66cb78d 100644 --- a/input/src/main/scala/fix/IndentationSyntax_Test.scala +++ b/input/src/main/scala/fix/IndentationSyntax_Test.scala @@ -26,7 +26,7 @@ object IndentationSyntax_Test { println("c") } - if (true) + if (true) println("a") println("b") println("c") diff --git a/output/src/main/scala/fix/IndentationSyntax_Test.scala b/output/src/main/scala/fix/IndentationSyntax_Test.scala index 1444d61..b8cbff0 100644 --- a/output/src/main/scala/fix/IndentationSyntax_Test.scala +++ b/output/src/main/scala/fix/IndentationSyntax_Test.scala @@ -1,38 +1,38 @@ package fix object IndentationSyntax_Test: - if (true) + if (true) println("a") println("b") println("c") end if - if (true) + if (true) println("a2") println("b2") println("c2") end if - if (true) + if (true) println("a") println("b") println("c") end if - if (true) + if (true) println("a") println("b") println("c") end if - while (true) + while (true) println("a") println("b") println("c") end while // spaces instead of tabs - while (true) + while (true) println("a") println("b") println("c") @@ -40,15 +40,16 @@ object IndentationSyntax_Test: val xs = List(1, 2, 3) end xs - for (x <- xs) + for (x <- xs) println("a") println("b") println("c") - for (x <- xs) yield + for (x <- xs) yield val myFactor = 2 end myFactor myFactor * x + end for object MyObject: override def toString(): String = "myObject" diff --git a/rules/src/main/scala/fix/IndentationSyntax.scala b/rules/src/main/scala/fix/IndentationSyntax.scala index f8771b8..2cafb9c 100644 --- a/rules/src/main/scala/fix/IndentationSyntax.scala +++ b/rules/src/main/scala/fix/IndentationSyntax.scala @@ -61,39 +61,10 @@ class IndentationSyntax(params: IndentationSyntaxParameters) } } - def addEndMarkerMethod(tree: Tree): Patch = { + def getEndMarker(tree: Tree): Option[String] = { if (!params.addEndMarkers || endMarkerSkipped(tree)) { - Patch.empty + None } else { - val lastToken = tree.tokens.last - - val lookFor = tree match { - case defn: Stat.WithMods if !defn.mods.isEmpty => isMod _ - case _: Defn.Object => isObject _ - case _: Defn.Class => isClass _ - case _: Defn.Trait => isTrait _ - case _: Defn.ExtensionGroup => isExtension _ - case _: Term.If => isIf _ - case _: Term.While => isWhile _ - case _: Term.Try => isTry _ - - case _: Defn.Val => isVal _ - case _: Defn.Var => isVar _ - case _: Defn.Def => isDef _ - - case _: Defn.GivenAlias => isGiven _ - case _: Term.NewAnonymous => isNew _ - case _: Ctor.Secondary => isDef _ // secondary ctor definition starts with the "def" keyword - case matchTerm: Term.Match => isIdentifier(_, matchTerm.expr.toString()) // match expression starts with an identifier - - case _ => throw new NotImplementedError("End markers for this syntactic structure are not implemented.") - } - - val keyword = tree.tokens.find(lookFor).getOrElse(throw new Exception("The given tree doesn't contain its defining keyword.")) - def isBeforeKeyword(t: Token) = t.end <= keyword.start - val whitespaceBeforeKeyword = tree.tokens.tokens.filter(isBeforeKeyword).reverse.takeWhile(isHSpace) - val indentationLevel = whitespaceBeforeKeyword.size - val endMarkerName = tree match { case _: Term.If => "if" case _: Term.While => "while" @@ -136,10 +107,9 @@ class IndentationSyntax(params: IndentationSyntaxParameters) case member: scala.meta.Member => member.name // test if it returns THIS for secondary constructor } - val stringToAdd = "\n" + " " * indentationLevel + "end " + endMarkerName - val endMarker = Patch.addRight(lastToken, stringToAdd) + val stringToAdd = "end " + endMarkerName - endMarker + Some(stringToAdd) } } /* @@ -255,17 +225,19 @@ class IndentationSyntax(params: IndentationSyntaxParameters) (rleFromTokens(whitespace), remainingTokens) }) - // println("*** START OF NEW DOCUMENT ***") - // indentsAndTokens.foreach(println) + println("*** START OF NEW DOCUMENT ***") + indentsAndTokens.foreach(println) /* * END OF SPLITTING INTO LINES (INDENTS AND TOKENS) */ + val lineByToken: Map[Token, Int] = docLines.zipWithIndex.flatMap { case (tokens, index) => tokens.map(t => (t, index)) }.toMap + def linesFromTree(t: Tree) = { // returns subset of lines containing given tree val (from, to) = (t.tokens.head, t.tokens.last) - val firstLine = docLines.indexWhere(_ == from) - val lastLine = docLines.lastIndexWhere(_ == to) + val firstLine = lineByToken(from) + val lastLine = lineByToken(to) docLines.slice(firstLine, lastLine + 1) } @@ -280,72 +252,65 @@ class IndentationSyntax(params: IndentationSyntaxParameters) var patches = Nil */ - // val linesByToken: Map[Token, Int] = ??? - // val oldIndentationByLine: Seq[RLEIndent] = ??? - // val newIndentationByLine: mutable.Seq[RLEIndent] = mutable.Seq(oldIndentationByLine: _*) - - /* - * START PATCHES - */ - val endMarkerPatches: List[Patch] = doc.tree.collect { - case controlStructureTree @ (_: Term.If | _: Term.While | _: Term.Try | _: Term.Match) => - addEndMarkerMethod(controlStructureTree) - case defnTree @ (_: Defn.Val | _: Defn.Var | _: Defn.Def | _: Defn.GivenAlias | _: Term.NewAnonymous) => - addEndMarkerMethod(defnTree) - case containingTemplateTree @ (_: Defn.Object | _: Defn.Class | _: Defn.Trait | _: Defn.ExtensionGroup | _: Ctor.Secondary) => - addEndMarkerMethod(containingTemplateTree) - }.reverse + // old is for comparison + val oldIndentationByLine: Seq[RLEIndent] = indentsAndTokens.map(_._1) + // new is correct + val newIndentationByLine: mutable.Seq[RLEIndent] = mutable.Seq(oldIndentationByLine: _*) // TODO: think of something more clever def isBraced(t: Tree) = t.tokens.exists(isLeftBrace) && t.tokens.exists(isRightBrace) + // pattern match + // if it's a template, find the { before the "self" + // it it's a catch tree, find the first { after "catch" and before the first case (if "catch" exists) + // if it's an "enums" list inside a for-expression, find the first { after the "for" and before the first enumerator val bracePatches = doc.tree.collect { - case template: Template if isBraced(template) => - val leftBrace = template.parent.get.tokens.find(isLeftBrace).get - val rightBrace = template.parent.get.tokens.findLast(isRightBrace).get + case templateOrBlock @ (_: Template | _: Term.Block) if isBraced(templateOrBlock) => { + + // don't remove braces on all blocks! + // special cases: + // check if it's not in Term.ArgClause + // check if the parent is not a block + val leftBrace = templateOrBlock.parent.get.tokens.find(isLeftBrace).get + val rightBrace = templateOrBlock.parent.get.tokens.findLast(isRightBrace).get def isBeforeLeftBrace(t: Token) = t.end <= leftBrace.start def isBeforeRightBrace(t: Token) = t.end <= rightBrace.start - val tokensBeforeLeftBrace = template.parent.get.tokens.takeWhile(isBeforeLeftBrace) - val whitespaceBeforeLeftBrace = tokensBeforeLeftBrace.takeRightWhile(isWhitespace) - val tokensBeforeRightBrace = template.parent.get.tokens.takeWhile(isBeforeRightBrace) - val whitespaceBeforeRightBrace = tokensBeforeRightBrace.takeRightWhile(isWhitespace) + val tokensBeforeLeftBrace = templateOrBlock.parent.get.tokens.takeWhile(isBeforeLeftBrace) + val whitespaceBeforeLeftBrace = tokensBeforeLeftBrace.takeRightWhile(isWhitespace) val removeWhitespaceBeforeLeftBrace = Patch.removeTokens(whitespaceBeforeLeftBrace) - val replaceLeftBraceWithColon = Patch.replaceToken(leftBrace, ":") - val removeWhitespaceBeforeRightBrace = Patch.removeTokens(whitespaceBeforeRightBrace) - val removeRightBrace = Patch.removeToken(rightBrace) - - removeWhitespaceBeforeLeftBrace + replaceLeftBraceWithColon + removeWhitespaceBeforeRightBrace + removeRightBrace - - case block: Term.Block if isBraced(block) => - val leftBrace = block.parent.get.tokens.find(isLeftBrace).get - val rightBrace = block.parent.get.tokens.findLast(isRightBrace).get + val removeLeftBrace = templateOrBlock match { + case _: Template => Patch.replaceToken(leftBrace, ":") + case _: Term.Block => Patch.removeToken(leftBrace) + case _ => throw new Exception("The given tree is not a template body or a block.") + } + + val removeRightBrace = getEndMarker(templateOrBlock) match { + case Some(endMarker) => Patch.replaceToken(rightBrace, endMarker) + case None => + + val tokensBeforeRightBrace = templateOrBlock.parent.get.tokens.takeWhile(isBeforeRightBrace) + val whitespaceBeforeRightBrace = tokensBeforeRightBrace.takeRightWhile(isHSpace) + Patch.removeTokens(whitespaceBeforeRightBrace :+ rightBrace) + } - def isBeforeLeftBrace(t: Token) = t.end <= leftBrace.start - def isBeforeRightBrace(t: Token) = t.end <= rightBrace.start - - val tokensBeforeLeftBrace = block.parent.get.tokens.takeWhile(isBeforeLeftBrace) - val whitespaceBeforeLeftBrace = tokensBeforeLeftBrace.takeRightWhile(isWhitespace) - val tokensBeforeRightBrace = block.parent.get.tokens.takeWhile(isBeforeRightBrace) - val whitespaceBeforeRightBrace = tokensBeforeRightBrace.takeRightWhile(isWhitespace) - val removeWhitespaceBeforeLeftBrace = Patch.removeTokens(whitespaceBeforeLeftBrace) - val removeLeftBrace = Patch.removeToken(leftBrace) - val removeWhitespaceBeforeRightBrace = Patch.removeTokens(whitespaceBeforeRightBrace) - val removeRightBrace = Patch.removeToken(rightBrace) + // mutate newIndentationByLine - removeWhitespaceBeforeLeftBrace + removeLeftBrace + removeWhitespaceBeforeRightBrace + removeRightBrace - + removeWhitespaceBeforeLeftBrace + removeLeftBrace + removeRightBrace + } + case _: Term.Match => Patch.empty // x match { } find the braces + case _: Term.Try => Patch.empty // look for {} after "catch" and before "finally" if it exists. Everything else is a block. case _ => Patch.empty } - val computeIndentationPatches = doc.tree.collect { - case _ => Patch.empty - } + val computeIndentationPatches = + // generate patches from old VS new + List(Patch.empty) - bracePatches.asPatch + endMarkerPatches.asPatch ++ computeIndentationPatches + computeIndentationPatches.asPatch + bracePatches.asPatch /* * END PATCHES */ From ae64df90b7ce8f463893fccf176380c29f724bfb Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 29 Nov 2023 17:13:19 +0100 Subject: [PATCH 20/22] indentation works --- .../scala/fix/IndentationSyntax_Test.scala | 6 ++ .../src/main/scala/fix/EndMarker_Test.scala | 4 +- .../scala/fix/IndentationSyntax_Test.scala | 29 ++++---- .../main/scala/fix/IndentationSyntax.scala | 69 +++++++++++++++++-- 4 files changed, 87 insertions(+), 21 deletions(-) diff --git a/input/src/main/scala/fix/IndentationSyntax_Test.scala b/input/src/main/scala/fix/IndentationSyntax_Test.scala index 66cb78d..8e3974d 100644 --- a/input/src/main/scala/fix/IndentationSyntax_Test.scala +++ b/input/src/main/scala/fix/IndentationSyntax_Test.scala @@ -58,4 +58,10 @@ object IndentationSyntax_Test { object MyObject: override def toString(): String = "myObject" + + if (true) { + if (true) { +println("true") + } + } } \ No newline at end of file diff --git a/output/src/main/scala/fix/EndMarker_Test.scala b/output/src/main/scala/fix/EndMarker_Test.scala index 258b6d5..58b2281 100644 --- a/output/src/main/scala/fix/EndMarker_Test.scala +++ b/output/src/main/scala/fix/EndMarker_Test.scala @@ -11,10 +11,8 @@ object EndMarker_Test: case class MyText(text: String): println(text) - end MyText extension (m: MyText) def myPrint = println(m.text) - end myPrint - end extension + end EndMarker_Test \ No newline at end of file diff --git a/output/src/main/scala/fix/IndentationSyntax_Test.scala b/output/src/main/scala/fix/IndentationSyntax_Test.scala index b8cbff0..227e83c 100644 --- a/output/src/main/scala/fix/IndentationSyntax_Test.scala +++ b/output/src/main/scala/fix/IndentationSyntax_Test.scala @@ -9,8 +9,8 @@ object IndentationSyntax_Test: if (true) println("a2") - println("b2") - println("c2") + println("b2") + println("c2") end if if (true) @@ -23,37 +23,38 @@ object IndentationSyntax_Test: println("a") println("b") println("c") - end if while (true) println("a") - println("b") - println("c") + println("b") + println("c") end while // spaces instead of tabs while (true) println("a") - println("b") - println("c") + println("b") + println("c") end while val xs = List(1, 2, 3) - end xs for (x <- xs) println("a") - println("b") - println("c") + println("b") + println("c") + end for for (x <- xs) yield val myFactor = 2 - end myFactor - myFactor * x + myFactor * x end for object MyObject: override def toString(): String = "myObject" - end toString - end MyObject + if (true) + if (true) + println("true") + end if + end if end IndentationSyntax_Test \ No newline at end of file diff --git a/rules/src/main/scala/fix/IndentationSyntax.scala b/rules/src/main/scala/fix/IndentationSyntax.scala index 2cafb9c..06759f4 100644 --- a/rules/src/main/scala/fix/IndentationSyntax.scala +++ b/rules/src/main/scala/fix/IndentationSyntax.scala @@ -149,6 +149,31 @@ class IndentationSyntax(params: IndentationSyntaxParameters) def != (that: RLEIndent): Boolean = !(this == that) def > (that: RLEIndent): Boolean = !(this <= that) def >= (that: RLEIndent): Boolean = !(this < that) + + private def merge(a: (Int, IndentationCharacter), b: (Int, IndentationCharacter)): List[(Int, IndentationCharacter)] = { + if (a._2 == b._2) { + List((a._1 + b._1, a._2)) + } else { + List(a, b) + } + } + + def + (that: RLEIndent): RLEIndent = + (this.indents, that.indents) match { + case (Nil, _) => that + case (_, Nil) => this + case (a, b) => RLEIndent(a.init ++ merge(a.last, b.head) ++ b.tail) + } + + def asString = { + def loop(l: List[(Int, IndentationCharacter)]): String = l match { + case Nil => "" + case (n, Space) :: next => " " * n + loop(next) + case (n, Tab) :: next => "\t" * n + loop(next) + } + + loop(indents) + } } def rleFromTokens(tokens: Seq[Token]): RLEIndent = { @@ -225,14 +250,15 @@ class IndentationSyntax(params: IndentationSyntaxParameters) (rleFromTokens(whitespace), remainingTokens) }) - println("*** START OF NEW DOCUMENT ***") - indentsAndTokens.foreach(println) + // println("*** START OF NEW DOCUMENT ***") + // indentsAndTokens.foreach(println) /* * END OF SPLITTING INTO LINES (INDENTS AND TOKENS) */ val lineByToken: Map[Token, Int] = docLines.zipWithIndex.flatMap { case (tokens, index) => tokens.map(t => (t, index)) }.toMap + def linesFromTree(t: Tree) = { // returns subset of lines containing given tree val (from, to) = (t.tokens.head, t.tokens.last) @@ -257,6 +283,8 @@ class IndentationSyntax(params: IndentationSyntaxParameters) // new is correct val newIndentationByLine: mutable.Seq[RLEIndent] = mutable.Seq(oldIndentationByLine: _*) + def indentationByToken(t: Token) = newIndentationByLine(lineByToken(t)) + // TODO: think of something more clever def isBraced(t: Tree) = t.tokens.exists(isLeftBrace) && t.tokens.exists(isRightBrace) // pattern match @@ -275,6 +303,8 @@ class IndentationSyntax(params: IndentationSyntaxParameters) val rightBrace = templateOrBlock.parent.get.tokens.findLast(isRightBrace).get def isBeforeLeftBrace(t: Token) = t.end <= leftBrace.start + def isAfterLeftBrace(t: Token) = t.start >= leftBrace.end + def isBeforeRightBrace(t: Token) = t.end <= rightBrace.start val tokensBeforeLeftBrace = templateOrBlock.parent.get.tokens.takeWhile(isBeforeLeftBrace) @@ -287,7 +317,7 @@ class IndentationSyntax(params: IndentationSyntaxParameters) case _ => throw new Exception("The given tree is not a template body or a block.") } - val removeRightBrace = getEndMarker(templateOrBlock) match { + val removeRightBrace = getEndMarker(templateOrBlock.parent.get) match { case Some(endMarker) => Patch.replaceToken(rightBrace, endMarker) case None => @@ -298,7 +328,29 @@ class IndentationSyntax(params: IndentationSyntaxParameters) // mutate newIndentationByLine + val firstToken = templateOrBlock.tokens.find(t => isAfterLeftBrace(t) && !isWhitespace(t)).get + val lastToken = templateOrBlock.tokens.findLast(t => isBeforeRightBrace(t) && !isWhitespace(t)).get + val parentIndentation = indentationByToken(templateOrBlock.parent.get.tokens.head) + val insideIndentation = indentationByToken(firstToken) + + val correctIndentation = if (insideIndentation > parentIndentation) { + insideIndentation + } else { + parentIndentation + defaultIndentation + } + + for (line <- lineByToken(firstToken).to(lineByToken(lastToken))) { + if (newIndentationByLine(line) < correctIndentation) { + newIndentationByLine(line) = correctIndentation + } + } + + val rightBraceLine = lineByToken(rightBrace) + if (newIndentationByLine(rightBraceLine) != parentIndentation) { + newIndentationByLine(rightBraceLine) = parentIndentation + } + removeWhitespaceBeforeLeftBrace + removeLeftBrace + removeRightBrace } case _: Term.Match => Patch.empty // x match { } find the braces @@ -308,7 +360,16 @@ class IndentationSyntax(params: IndentationSyntaxParameters) val computeIndentationPatches = // generate patches from old VS new - List(Patch.empty) + for ((tokens, line) <- docLines.zipWithIndex) + yield { + val oldIndent = oldIndentationByLine(line) + val newIndent = newIndentationByLine(line) + if (oldIndent != newIndent && tokens.exists(t => !isWhitespace(t))) { + Patch.removeTokens(tokens.takeWhile(isHSpace)) + Patch.addLeft(tokens.head, newIndent.asString) + } else { + Patch.empty + } + } computeIndentationPatches.asPatch + bracePatches.asPatch /* From 8abfa80c04b6ba89c493f75ba948e539812d37bb Mon Sep 17 00:00:00 2001 From: Mark Date: Tue, 12 Dec 2023 14:20:17 +0100 Subject: [PATCH 21/22] Added new rule for end markers; added support for enums and package objects --- input/src/main/scala/fix/EndMarker_Test.scala | 17 +- .../fix/Scala3ReferenceEndMarker_Test.scala | 4 +- .../src/main/scala/fix/EndMarker_Test.scala | 13 +- .../META-INF/services/scalafix.v1.Rule | 1 + rules/src/main/scala/fix/AddEndMarkers.scala | 217 ++++++++++++++++++ 5 files changed, 240 insertions(+), 12 deletions(-) create mode 100644 rules/src/main/scala/fix/AddEndMarkers.scala diff --git a/input/src/main/scala/fix/EndMarker_Test.scala b/input/src/main/scala/fix/EndMarker_Test.scala index c67cb5f..1c52a59 100644 --- a/input/src/main/scala/fix/EndMarker_Test.scala +++ b/input/src/main/scala/fix/EndMarker_Test.scala @@ -1,23 +1,24 @@ package fix /* rules = [ - IndentationSyntax + AddEndMarkers ] -IndentationSyntax.addEndMarkers = true +AddEndMarkers.addEndMarkers = true */ -object EndMarker_Test { - if (true) { +object EndMarker_Test: + if true then println(true) - } - class MyNumber(number: Int) extends AnyRef { + class MyNumber(number: Int) extends AnyRef: println(number) - } case class MyText(text: String): println(text) extension (m: MyText) def myPrint = println(m.text) + + enum Color: + case Red, Green, Blue -} +package object A diff --git a/input/src/main/scala/fix/Scala3ReferenceEndMarker_Test.scala b/input/src/main/scala/fix/Scala3ReferenceEndMarker_Test.scala index 2a0781a..061b311 100644 --- a/input/src/main/scala/fix/Scala3ReferenceEndMarker_Test.scala +++ b/input/src/main/scala/fix/Scala3ReferenceEndMarker_Test.scala @@ -1,9 +1,9 @@ package p1.p2: /* rules = [ - IndentationSyntax + AddEndMarkers ] -IndentationSyntax.addEndMarkers = true +AddEndMarkers.addEndMarkers = true */ abstract class C(): diff --git a/output/src/main/scala/fix/EndMarker_Test.scala b/output/src/main/scala/fix/EndMarker_Test.scala index 58b2281..c69e457 100644 --- a/output/src/main/scala/fix/EndMarker_Test.scala +++ b/output/src/main/scala/fix/EndMarker_Test.scala @@ -1,7 +1,7 @@ package fix object EndMarker_Test: - if (true) + if true then println(true) end if @@ -11,8 +11,17 @@ object EndMarker_Test: case class MyText(text: String): println(text) + end MyText extension (m: MyText) def myPrint = println(m.text) + end myPrint + end extension + + enum Color: + case Red, Green, Blue + end Color +end EndMarker_Test -end EndMarker_Test \ No newline at end of file +package object A +end A \ No newline at end of file diff --git a/rules/src/main/resources/META-INF/services/scalafix.v1.Rule b/rules/src/main/resources/META-INF/services/scalafix.v1.Rule index d6ba90b..bc108af 100644 --- a/rules/src/main/resources/META-INF/services/scalafix.v1.Rule +++ b/rules/src/main/resources/META-INF/services/scalafix.v1.Rule @@ -1,3 +1,4 @@ fix.Scala3ControlSyntax fix.Scala2ControlSyntax fix.IndentationSyntax +fix.AddEndMarkers diff --git a/rules/src/main/scala/fix/AddEndMarkers.scala b/rules/src/main/scala/fix/AddEndMarkers.scala new file mode 100644 index 0000000..b5c834a --- /dev/null +++ b/rules/src/main/scala/fix/AddEndMarkers.scala @@ -0,0 +1,217 @@ +package fix + +import scalafix.v1._ +import scala.meta._ +import metaconfig.Configured +import scala.annotation.tailrec +import scala.collection.mutable +import com.google.protobuf.Empty + +case class AddEndMarkersParameters( + addEndMarkers: Boolean, + skipEndMarkers: List[String], + minLinesForEndMarker: Int, + defaultIndentation: String, +) + +object AddEndMarkersParameters { + val default = AddEndMarkersParameters(false, Nil, 0, " ") + implicit val surface: metaconfig.generic.Surface[AddEndMarkersParameters] = metaconfig.generic.deriveSurface[AddEndMarkersParameters] + implicit val decoder: metaconfig.ConfDecoder[AddEndMarkersParameters] = metaconfig.generic.deriveDecoder(default) +} + +class AddEndMarkers(params: AddEndMarkersParameters) + extends SyntacticRule("AddEndMarkers") { + + def this() = this(AddEndMarkersParameters.default) + + override def withConfiguration(config: Configuration): Configured[Rule] = + config.conf.getOrElse("AddEndMarkers")(this.params).map(newParams => new AddEndMarkers(newParams)) + + override def fix(implicit doc: SyntacticDocument): Patch = { + /* + * START OF END MARKERS + */ + def endMarkerSkipped(tree: Tree): Boolean = { + tree match { + case _: Defn.Object => params.skipEndMarkers.contains("object") + case _: Defn.Class => params.skipEndMarkers.contains("class") + case _: Defn.Trait => params.skipEndMarkers.contains("trait") + + case _: Term.If => params.skipEndMarkers.contains("if") + case _: Term.While => params.skipEndMarkers.contains("while") + case _: Term.For => params.skipEndMarkers.contains("for") + case _: Term.ForYield => params.skipEndMarkers.contains("forYield") + case _: Term.Match => params.skipEndMarkers.contains("match") + case _: Term.Try => params.skipEndMarkers.contains("try") + case _: Term.NewAnonymous => params.skipEndMarkers.contains("new") + case _: Ctor.Secondary => params.skipEndMarkers.contains("this") + case _: Defn.Val => params.skipEndMarkers.contains("val") + case _: Defn.Var => params.skipEndMarkers.contains("var") + case _: Defn.Def => params.skipEndMarkers.contains("def") + case _: Defn.Enum => params.skipEndMarkers.contains("enum") + + case _: Defn.GivenAlias => params.skipEndMarkers.contains("given") + + case _: Pkg.Object => params.skipEndMarkers.contains("packageObject") + + case _ => false + } + } + + def addEndMarkerMethod(tree: Tree): Patch = { + if (!params.addEndMarkers || endMarkerSkipped(tree)) { + Patch.empty + } else { + val lastToken = tree.tokens.last + + val lookFor = tree match { + case defn: Stat.WithMods if !defn.mods.isEmpty => isMod _ + case _: Defn.Object => isObject _ + case _: Defn.Class => isClass _ + case _: Defn.Trait => isTrait _ + case _: Defn.ExtensionGroup => isExtension _ + case _: Term.If => isIf _ + case _: Term.While => isWhile _ + case _: Term.Try => isTry _ + + case _: Defn.Val => isVal _ + case _: Defn.Var => isVar _ + case _: Defn.Def => isDef _ + case _: Defn.Enum => isEnum _ + + case _: Defn.GivenAlias => isGiven _ + case _: Term.NewAnonymous => isNew _ + case _: Ctor.Secondary => isDef _ // secondary ctor definition starts with the "def" keyword + case matchTerm: Term.Match => isIdentifier(_, matchTerm.expr.toString()) // match expression starts with an identifier + + case _: Pkg.Object => isPackage _ + + case _ => throw new NotImplementedError("End markers for this syntactic structure are not implemented.") + } + + val keyword = tree.tokens.find(lookFor).getOrElse(throw new Exception("The given tree doesn't contain its defining keyword.")) + def isBeforeKeyword(t: Token) = t.end <= keyword.start + val whitespaceBeforeKeyword = tree.tokens.tokens.filter(isBeforeKeyword).reverse.takeWhile(isHSpace) + val indentationLevel = whitespaceBeforeKeyword.size + + val endMarkerName = tree match { + case _: Term.If => "if" + case _: Term.While => "while" + case _: Term.For => "for" + case _: Term.ForYield => "for" + case _: Term.Match => "match" + + case _: Term.Try => "try" + case _: Term.NewAnonymous => "new" + // case _: Term.This => "this" + + case valTree: Defn.Val => valTree.pats.head match { + case pat: Pat.Var => pat.name.value + case _ => "val" + } + + case varTree: Defn.Var => varTree.pats.head match { + case pat: Pat.Var => pat.name.value + case _ => "var" + } + + case givenTree: Defn.GivenAlias => givenTree.name match { + case _: Name.Anonymous => "given" + case n: Name => n.value + } + + case _: Defn.ExtensionGroup => "extension" + + case defTree: Defn.Def => defTree.name match { + case _: Name.Anonymous => "def" + case n: Name => n.value + } + + case enumTree: Defn.Enum => enumTree.name match { + case _: Name.Anonymous => "enum" + case n: Name => n.value + } + + // case _: Ctor.Secondary => "this" + case ctorTree: Ctor.Secondary => ctorTree.name match { + case _: Name.This => "this" + case n: Name => n.value + } + + case pkgObjectTree: Pkg.Object => pkgObjectTree.name match { + case n: Name => n.value + } + + case member: scala.meta.Member => member.name // test if it returns THIS for secondary constructor + } + + val stringToAdd = "\n" + " " * indentationLevel + "end " + endMarkerName + val endMarker = Patch.addRight(lastToken, stringToAdd) + + endMarker + } + } + /* + * END OF END MARKERS + */ + + /* + * START PATCHES + */ + val endMarkerPatches: List[Patch] = doc.tree.collect { + case controlStructureTree @ (_: Term.If | _: Term.While | _: Term.Try | _: Term.Match) => + addEndMarkerMethod(controlStructureTree) + case defnTree @ (_: Defn.Val | _: Defn.Var | _: Defn.Def | _: Defn.Enum | _: Defn.GivenAlias | _: Term.NewAnonymous) => + addEndMarkerMethod(defnTree) + case containingTemplateTree @ (_: Defn.Object | _: Defn.Class | _: Defn.Trait | _: Defn.ExtensionGroup | _: Ctor.Secondary | _: Pkg.Object) => + addEndMarkerMethod(containingTemplateTree) + }.reverse + + endMarkerPatches.asPatch + /* + * END PATCHES + */ + } + + /* + * START OF HELPER FUNCTIONS (TOKEN PREDICATES) + */ + def isLeftBrace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.LeftBrace] + def isRightBrace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.RightBrace] + + def isHSpace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.HSpace] // Space, Tab + def isNewLine(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.AtEOL] // CR, LF, FF + def isSpace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Space] // Space + def isWhitespace(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Whitespace] // HSpace, AtEOL + + def isObject(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwObject] + def isClass(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwClass] + def isTrait(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwTrait] + def isPackage(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwPackage] + def isExtension(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Ident] && t.text == "extension" + def isMod(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.ModifierKeyword] || t.isInstanceOf[scala.meta.tokens.Token.KwCase] + def isVal(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwVal] + def isVar(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwVar] + def isDef(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwDef] + def isEnum(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwEnum] + def isGiven(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwGiven] + def isThis(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwThis] + def isNew(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwNew] + def isIf(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwIf] + def isWhile(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwWhile] + def isTry(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.KwTry] + def isIdentifier(t: Token, name: String) = t.isInstanceOf[scala.meta.tokens.Token.Ident] && t.text == name + /* + * END OF HELPER FUNCTIONS (TOKEN PREDICATES) + */ + + /* a note regarding templates and package objects + + A template defines the type signature, behavior and initial state of a trait or class of objects or of a single object. + https://www.scala-lang.org/files/archive/spec/3.4/05-classes-and-objects.html#templates + + Also, package objects in Scalameta: + https://scalameta.org/docs/trees/quasiquotes.html + */ +} \ No newline at end of file From bd699ceb30394c13437452955b803de5fe622b5f Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 13 Dec 2023 23:48:43 +0100 Subject: [PATCH 22/22] Added new reference test --- .../scala/fix/IndentationSyntax_Test.scala | 7 + input/src/main/scala/fix/ReferenceTest.scala | 282 ++++++++++++++++++ .../scala/fix/IndentationSyntax_Test.scala | 8 + output/src/main/scala/fix/ReferenceTest.scala | 273 +++++++++++++++++ .../main/scala/fix/IndentationSyntax.scala | 165 ++++++++-- tests/src/test/scala/fix/Rules_Tests.scala | 58 +++- 6 files changed, 772 insertions(+), 21 deletions(-) create mode 100644 input/src/main/scala/fix/ReferenceTest.scala create mode 100644 output/src/main/scala/fix/ReferenceTest.scala diff --git a/input/src/main/scala/fix/IndentationSyntax_Test.scala b/input/src/main/scala/fix/IndentationSyntax_Test.scala index 8e3974d..2970906 100644 --- a/input/src/main/scala/fix/IndentationSyntax_Test.scala +++ b/input/src/main/scala/fix/IndentationSyntax_Test.scala @@ -64,4 +64,11 @@ object IndentationSyntax_Test { println("true") } } + + val num = 2 + val numRes = num match { + case 2 => "two" + case _ => "not two" + } + } \ No newline at end of file diff --git a/input/src/main/scala/fix/ReferenceTest.scala b/input/src/main/scala/fix/ReferenceTest.scala new file mode 100644 index 0000000..75a966c --- /dev/null +++ b/input/src/main/scala/fix/ReferenceTest.scala @@ -0,0 +1,282 @@ +/* +rules = [ + IndentationSyntax +] + +IndentationSyntax.addEndMarkers = false +*/ +// 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/output/src/main/scala/fix/IndentationSyntax_Test.scala b/output/src/main/scala/fix/IndentationSyntax_Test.scala index 227e83c..95b7758 100644 --- a/output/src/main/scala/fix/IndentationSyntax_Test.scala +++ b/output/src/main/scala/fix/IndentationSyntax_Test.scala @@ -57,4 +57,12 @@ object IndentationSyntax_Test: println("true") end if end if + + val num = 2 + val numRes = + num match + case 2 => "two" + case _ => "not two" + end match + end IndentationSyntax_Test \ No newline at end of file diff --git a/output/src/main/scala/fix/ReferenceTest.scala b/output/src/main/scala/fix/ReferenceTest.scala new file mode 100644 index 0000000..76515d1 --- /dev/null +++ b/output/src/main/scala/fix/ReferenceTest.scala @@ -0,0 +1,273 @@ +// 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/rules/src/main/scala/fix/IndentationSyntax.scala b/rules/src/main/scala/fix/IndentationSyntax.scala index 06759f4..1aa4c69 100644 --- a/rules/src/main/scala/fix/IndentationSyntax.scala +++ b/rules/src/main/scala/fix/IndentationSyntax.scala @@ -6,6 +6,7 @@ import metaconfig.Configured import scala.annotation.tailrec import scala.collection.mutable import com.google.protobuf.Empty +import java.lang.Character.{MATH_SYMBOL, OTHER_SYMBOL} case class IndentationSyntaxParameters( addEndMarkers: Boolean, @@ -20,15 +21,15 @@ case class IndentationSyntaxParameters( ) object IndentationSyntaxParameters { - val default = IndentationSyntaxParameters(false, Nil, 0, " ") - implicit val surface: metaconfig.generic.Surface[IndentationSyntaxParameters] = metaconfig.generic.deriveSurface[IndentationSyntaxParameters] - implicit val decoder: metaconfig.ConfDecoder[IndentationSyntaxParameters] = metaconfig.generic.deriveDecoder(default) + val default = AddEndMarkersParameters(false, Nil, 0, " ") + implicit val surface: metaconfig.generic.Surface[AddEndMarkersParameters] = metaconfig.generic.deriveSurface[AddEndMarkersParameters] + implicit val decoder: metaconfig.ConfDecoder[AddEndMarkersParameters] = metaconfig.generic.deriveDecoder(default) } -class IndentationSyntax(params: IndentationSyntaxParameters) +class IndentationSyntax(params: AddEndMarkersParameters) extends SyntacticRule("IndentationSyntax") { - def this() = this(IndentationSyntaxParameters.default) + def this() = this(AddEndMarkersParameters.default) override def withConfiguration(config: Configuration): Configured[Rule] = config.conf.getOrElse("IndentationSyntax")(this.params).map(newParams => new IndentationSyntax(newParams)) @@ -62,7 +63,7 @@ class IndentationSyntax(params: IndentationSyntaxParameters) } def getEndMarker(tree: Tree): Option[String] = { - if (!params.addEndMarkers || endMarkerSkipped(tree)) { + if (!params.addEndMarkers || endMarkerSkipped(tree) || tree.isInstanceOf[Term.ArgClause]) { None } else { val endMarkerName = tree match { @@ -286,14 +287,20 @@ class IndentationSyntax(params: IndentationSyntaxParameters) def indentationByToken(t: Token) = newIndentationByLine(lineByToken(t)) // TODO: think of something more clever - def isBraced(t: Tree) = t.tokens.exists(isLeftBrace) && t.tokens.exists(isRightBrace) + def isBraced(t: Tree) = t.tokens.nonEmpty && isLeftBrace(t.tokens.head) && isRightBrace(t.tokens.last) + // t.tokens.exists(isLeftBrace) && t.tokens.exists(isRightBrace) // pattern match // if it's a template, find the { before the "self" // it it's a catch tree, find the first { after "catch" and before the first case (if "catch" exists) // if it's an "enums" list inside a for-expression, find the first { after the "for" and before the first enumerator val bracePatches = doc.tree.collect { - case templateOrBlock @ (_: Template | _: Term.Block) if isBraced(templateOrBlock) => { + case templateOrBlock @ (_: Template | _: Term.Block) + if isBraced(templateOrBlock) + && !templateOrBlock.parent.get.isInstanceOf[Term.ArgClause] + && !templateOrBlock.tokens.forall(t => isWhitespace(t) || isLeftBrace(t) || isRightBrace(t)) + => { + // don't remove braces on all blocks! // special cases: @@ -310,9 +317,32 @@ class IndentationSyntax(params: IndentationSyntaxParameters) val tokensBeforeLeftBrace = templateOrBlock.parent.get.tokens.takeWhile(isBeforeLeftBrace) val whitespaceBeforeLeftBrace = tokensBeforeLeftBrace.takeRightWhile(isWhitespace) + def addBackticks = { + val operatorIdent = templateOrBlock.parent.get.tokens.findLast(t => isBeforeLeftBrace(t) && !isWhitespace(t)).get + def isIdent(t: Token) = t.isInstanceOf[scala.meta.tokens.Token.Ident] + def hasBackticks(t: Token) = t.text.last == '`' + + def isOperator(t: Token): Boolean = t.text.last match { + case '~' | '!' | '@' | '#' | '%' | + '^' | '*' | '+' | '-' | '<' | + '>' | '?' | ':' | '=' | '&' | + '|' | '/' | '\\' => true + case c => isSpecial(c) + } + def isSpecial(c: Char): Boolean = { + val chtp = Character.getType(c) + chtp == MATH_SYMBOL.toInt || chtp == OTHER_SYMBOL.toInt + } + if (isIdent(operatorIdent) && !hasBackticks(operatorIdent) && isOperator(operatorIdent)) { + Patch.addAround(operatorIdent, "`", "`") + } else { + Patch.empty + } + } + val removeWhitespaceBeforeLeftBrace = Patch.removeTokens(whitespaceBeforeLeftBrace) val removeLeftBrace = templateOrBlock match { - case _: Template => Patch.replaceToken(leftBrace, ":") + case _: Template => Patch.replaceToken(leftBrace, ":") + addBackticks case _: Term.Block => Patch.removeToken(leftBrace) case _ => throw new Exception("The given tree is not a template body or a block.") } @@ -353,8 +383,112 @@ class IndentationSyntax(params: IndentationSyntaxParameters) removeWhitespaceBeforeLeftBrace + removeLeftBrace + removeRightBrace } - case _: Term.Match => Patch.empty // x match { } find the braces - case _: Term.Try => Patch.empty // look for {} after "catch" and before "finally" if it exists. Everything else is a block. + // x match { } find the braces + case matchTree: Term.Match => { + val leftBrace = matchTree.parent.get.tokens.find(isLeftBrace).get + val rightBrace = matchTree.parent.get.tokens.findLast(isRightBrace).get + + def isBeforeLeftBrace(t: Token) = t.end <= leftBrace.start + def isAfterLeftBrace(t: Token) = t.start >= leftBrace.end + + def isBeforeRightBrace(t: Token) = t.end <= rightBrace.start + + val tokensBeforeLeftBrace = matchTree.parent.get.tokens.takeWhile(isBeforeLeftBrace) + val whitespaceBeforeLeftBrace = tokensBeforeLeftBrace.takeRightWhile(isWhitespace) + + val removeWhitespaceBeforeLeftBrace = Patch.removeTokens(whitespaceBeforeLeftBrace) + + val removeLeftBrace = Patch.removeToken(leftBrace) + + val removeRightBrace = getEndMarker(matchTree) match { + case Some(endMarker) => Patch.replaceToken(rightBrace, endMarker) + case None => + + val tokensBeforeRightBrace = matchTree.parent.get.tokens.takeWhile(isBeforeRightBrace) + val whitespaceBeforeRightBrace = tokensBeforeRightBrace.takeRightWhile(isHSpace) + Patch.removeTokens(whitespaceBeforeRightBrace :+ rightBrace) + } + + + // mutate newIndentationByLine + val firstToken = matchTree.tokens.find(t => isAfterLeftBrace(t) && !isWhitespace(t)).get + val lastToken = matchTree.tokens.findLast(t => isBeforeRightBrace(t) && !isWhitespace(t)).get + + val parentIndentation = indentationByToken(matchTree.parent.get.tokens.head) + val insideIndentation = indentationByToken(firstToken) + + val correctIndentation = if (insideIndentation > parentIndentation) { + insideIndentation + } else { + parentIndentation + defaultIndentation + } + + for (line <- lineByToken(firstToken).to(lineByToken(lastToken))) { + if (newIndentationByLine(line) < correctIndentation) { + newIndentationByLine(line) = correctIndentation + } + } + + val rightBraceLine = lineByToken(rightBrace) + if (newIndentationByLine(rightBraceLine) != parentIndentation) { + newIndentationByLine(rightBraceLine) = parentIndentation + } + + removeWhitespaceBeforeLeftBrace + removeLeftBrace + removeRightBrace + } + // look for {} after "catch" and before "finally" if it exists. Everything else is a block. + case tryTree: Term.Try => { + val leftBrace = tryTree.parent.get.tokens.find(isLeftBrace).get + val rightBrace = tryTree.parent.get.tokens.findLast(isRightBrace).get + + def isBeforeLeftBrace(t: Token) = t.end <= leftBrace.start + def isAfterLeftBrace(t: Token) = t.start >= leftBrace.end + + def isBeforeRightBrace(t: Token) = t.end <= rightBrace.start + + val tokensBeforeLeftBrace = tryTree.parent.get.tokens.takeWhile(isBeforeLeftBrace) + val whitespaceBeforeLeftBrace = tokensBeforeLeftBrace.takeRightWhile(isWhitespace) + + val removeWhitespaceBeforeLeftBrace = Patch.removeTokens(whitespaceBeforeLeftBrace) + + val removeLeftBrace = Patch.removeToken(leftBrace) + + val removeRightBrace = getEndMarker(tryTree) match { + case Some(endMarker) => Patch.replaceToken(rightBrace, endMarker) + case None => + + val tokensBeforeRightBrace = tryTree.parent.get.tokens.takeWhile(isBeforeRightBrace) + val whitespaceBeforeRightBrace = tokensBeforeRightBrace.takeRightWhile(isHSpace) + Patch.removeTokens(whitespaceBeforeRightBrace :+ rightBrace) + } + + + // mutate newIndentationByLine + val firstToken = tryTree.tokens.find(t => isAfterLeftBrace(t) && !isWhitespace(t)).get + val lastToken = tryTree.tokens.findLast(t => isBeforeRightBrace(t) && !isWhitespace(t)).get + + val parentIndentation = indentationByToken(tryTree.parent.get.tokens.head) + val insideIndentation = indentationByToken(firstToken) + + val correctIndentation = if (insideIndentation > parentIndentation) { + insideIndentation + } else { + parentIndentation + defaultIndentation + } + + for (line <- lineByToken(firstToken).to(lineByToken(lastToken))) { + if (newIndentationByLine(line) < correctIndentation) { + newIndentationByLine(line) = correctIndentation + } + } + + val rightBraceLine = lineByToken(rightBrace) + if (newIndentationByLine(rightBraceLine) != parentIndentation) { + newIndentationByLine(rightBraceLine) = parentIndentation + } + + removeWhitespaceBeforeLeftBrace + removeLeftBrace + removeRightBrace + } case _ => Patch.empty } @@ -407,13 +541,4 @@ class IndentationSyntax(params: IndentationSyntaxParameters) /* * END OF HELPER FUNCTIONS (TOKEN PREDICATES) */ - - /* a note regarding templates and package objects - - A template defines the type signature, behavior and initial state of a trait or class of objects or of a single object. - https://www.scala-lang.org/files/archive/spec/3.4/05-classes-and-objects.html#templates - - Also, package objects in Scalameta: - https://scalameta.org/docs/trees/quasiquotes.html - */ } diff --git a/tests/src/test/scala/fix/Rules_Tests.scala b/tests/src/test/scala/fix/Rules_Tests.scala index 38b5b8b..4b8ed68 100644 --- a/tests/src/test/scala/fix/Rules_Tests.scala +++ b/tests/src/test/scala/fix/Rules_Tests.scala @@ -3,6 +3,14 @@ package fix import scalafix.testkit.AbstractSemanticRuleSuite import org.scalatest.funsuite.AnyFunSuiteLike import scalafix.testkit.AbstractSyntacticRuleSuite +import scalafix.testkit.RuleTest +import scalafix.internal.patch.PatchInternals +import scalafix.testkit.SemanticRuleSuite +import scala.meta.io.AbsolutePath +import java.io.File +import java.io.BufferedWriter +import java.io.FileWriter +import scala.meta.XtensionTokenizeInputLike class RuleSuite extends AbstractSemanticRuleSuite with AnyFunSuiteLike { // Doesn't really work; bugs with Scalameta @@ -28,5 +36,53 @@ class RuleSuite extends AbstractSemanticRuleSuite with AnyFunSuiteLike { } } */ - runAllTests() + + // val (passing, failing) = testsToRun.partition(!_.path.testName.contains("_fails")) + // passing.foreach(runOn) + // runSpecificTests("Cats5") + writeTestResult("ReferenceTest") + +// writeTestResult("implicits/") +// writeTestResult("types/") + // for running only one test if using Intellij + def runSpecificTests(name: String): Unit = + filterRuleTest(name).map(runOn) + + // for overwriting a result test in output directory + def writeTestResult(name: String): Unit = + filterRuleTest(name).map(runAndWriteResult) + + private def filterRuleTest(name: String): List[RuleTest] = { + testsToRun.foreach(t => println(t.path.testName)) + testsToRun.filter(_.path.testName.toLowerCase.contains(name.toLowerCase())) + } + + // Write the result directly to output folder, to avoid fixing by hand the diff + private def runAndWriteResult(ruleTest: RuleTest): Unit = { + val (rule, sdoc) = ruleTest.run.apply() + rule.beforeStart() + val res = + try rule.semanticPatch(sdoc, suppress = false) + finally rule.afterComplete() + // verify to verify that tokenPatchApply and fixed are the same + val fixed = + PatchInternals.tokenPatchApply(res.ruleCtx, res.semanticdbIndex, res.patches) + val tokens = fixed.tokenize.get + val _ :: obtained = SemanticRuleSuite.stripTestkitComments(tokens).linesIterator.toList + ruleTest.path.resolveOutput(props) match { + case Right(file) => writeFile(file, obtained.mkString("\n")) + case Left(err) => throw new Exception(s"File not found $err") + } + } + + private def writeFile(path: AbsolutePath, s: String): Unit = { + val filename = path.toNIO.getFileName.toString + val parent = path.toNIO.getParent.toString + val file = new File(parent, filename) + val bw = new BufferedWriter(new FileWriter(file)) + bw.write(s) + bw.close() + } + + // runAllTests() } \ No newline at end of file