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..1c52a59 --- /dev/null +++ b/input/src/main/scala/fix/EndMarker_Test.scala @@ -0,0 +1,24 @@ +package fix +/* +rules = [ + AddEndMarkers +] +AddEndMarkers.addEndMarkers = true +*/ +object EndMarker_Test: + if true then + println(true) + + 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/IndentationSyntax_Test.scala b/input/src/main/scala/fix/IndentationSyntax_Test.scala new file mode 100644 index 0000000..2970906 --- /dev/null +++ b/input/src/main/scala/fix/IndentationSyntax_Test.scala @@ -0,0 +1,74 @@ +/* +rules = [ + IndentationSyntax +] + +IndentationSyntax.addEndMarkers = true +*/ +package fix + +object IndentationSyntax_Test { + if (true) { + println("a") + println("b") + println("c") + } + + if (true) { + println("a2") + println("b2") + println("c2") + } + + if (true) { + println("a") + println("b") + println("c") + } + + if (true) + 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 + } + + object MyObject: + override def toString(): String = "myObject" + + if (true) { + if (true) { +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/input/src/main/scala/fix/Scala3ReferenceEndMarker_Test.scala b/input/src/main/scala/fix/Scala3ReferenceEndMarker_Test.scala new file mode 100644 index 0000000..061b311 --- /dev/null +++ b/input/src/main/scala/fix/Scala3ReferenceEndMarker_Test.scala @@ -0,0 +1,37 @@ +package p1.p2: +/* +rules = [ + AddEndMarkers +] +AddEndMarkers.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 new file mode 100644 index 0000000..c69e457 --- /dev/null +++ b/output/src/main/scala/fix/EndMarker_Test.scala @@ -0,0 +1,27 @@ +package fix + +object EndMarker_Test: + if true then + println(true) + end if + + class MyNumber(number: Int) extends AnyRef: + println(number) + end MyNumber + + 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 + +package object A +end A \ 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 new file mode 100644 index 0000000..95b7758 --- /dev/null +++ b/output/src/main/scala/fix/IndentationSyntax_Test.scala @@ -0,0 +1,68 @@ +package fix + +object IndentationSyntax_Test: + if (true) + 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") + + while (true) + 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) + println("a") + println("b") + println("c") + end for + + for (x <- xs) yield + val myFactor = 2 + myFactor * x + end for + + object MyObject: + override def toString(): String = "myObject" + + if (true) + if (true) + 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/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/resources/META-INF/services/scalafix.v1.Rule b/rules/src/main/resources/META-INF/services/scalafix.v1.Rule index fba45a6..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,2 +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 diff --git a/rules/src/main/scala/fix/IndentationSyntax.scala b/rules/src/main/scala/fix/IndentationSyntax.scala new file mode 100644 index 0000000..1aa4c69 --- /dev/null +++ b/rules/src/main/scala/fix/IndentationSyntax.scala @@ -0,0 +1,544 @@ +package fix + +import scalafix.v1._ +import scala.meta._ +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, + 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 +) + +object IndentationSyntaxParameters { + 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: AddEndMarkersParameters) + extends SyntacticRule("IndentationSyntax") { + + def this() = this(AddEndMarkersParameters.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 = { + /* + * 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.GivenAlias => params.skipEndMarkers.contains("given") + + case _ => false + } + } + + def getEndMarker(tree: Tree): Option[String] = { + if (!params.addEndMarkers || endMarkerSkipped(tree) || tree.isInstanceOf[Term.ArgClause]) { + None + } else { + 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 _: 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 + } + + val stringToAdd = "end " + endMarkerName + + Some(stringToAdd) + } + } + /* + * END OF END MARKERS + */ + + /* + * START OF RLE INDENT + */ + sealed trait IndentationCharacter + + case object Space extends IndentationCharacter + case object Tab extends 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) + + 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 = { + 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.toList.map(convertToIndentationCharacter)).map(l => (l.size, l.head)) + + RLEIndent(indents) + } + + 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)) + if (right.isEmpty) (left +: acc).reverse + else splitOnTail(right.tail, (left :+ right.head) +: acc, predicate) + } + + 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) + + (rleFromTokens(whitespace), remainingTokens) + }) + + // 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 = lineByToken(from) + val lastLine = lineByToken(to) + + docLines.slice(firstLine, lastLine + 1) + } + + /* + 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 + */ + + // old is for comparison + val oldIndentationByLine: Seq[RLEIndent] = indentsAndTokens.map(_._1) + // 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.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) + && !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: + // 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 isAfterLeftBrace(t: Token) = t.start >= leftBrace.end + + def isBeforeRightBrace(t: Token) = t.end <= rightBrace.start + + 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, ":") + addBackticks + 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.parent.get) 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) + } + + + // 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 + } + // 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 + } + + val computeIndentationPatches = + // generate patches from old VS new + 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 + /* + * 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) + */ +} 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