diff --git a/docs/developers/patch.md b/docs/developers/patch.md index 4b376cfd9..d207fd45a 100644 --- a/docs/developers/patch.md +++ b/docs/developers/patch.md @@ -122,7 +122,9 @@ doc.tree.collect { It's best to target as precise tree nodes as possible when using `replaceTree()` to avoid conflicts with other patches. -A common mistake is to use `replaceTree()` in combination with ` +A common mistake is to use `replaceTree()` in combination with quasiquotes. The +tree node reference that is passed to `replaceTree()` must appear in the source +file in order to produce a diff. ### `removeImportee()` diff --git a/scalafix-core/src/main/scala/scala/meta/internal/scalafix/ScalafixScalametaHacks.scala b/scalafix-core/src/main/scala/scala/meta/internal/scalafix/ScalafixScalametaHacks.scala index acc1cfcd5..7d737e894 100644 --- a/scalafix-core/src/main/scala/scala/meta/internal/scalafix/ScalafixScalametaHacks.scala +++ b/scalafix-core/src/main/scala/scala/meta/internal/scalafix/ScalafixScalametaHacks.scala @@ -2,8 +2,8 @@ package scala.meta.internal.scalafix import scala.meta.Dialect import scala.meta.Tree -import scala.meta.internal.trees.Origin import scala.meta.dialects +import scala.meta.internal.trees.Origin object ScalafixScalametaHacks { def dialect(language: String): Dialect = diff --git a/scalafix-core/src/main/scala/scalafix/config/CustomMessage.scala b/scalafix-core/src/main/scala/scalafix/config/CustomMessage.scala index 4d92d000c..ab7b477db 100644 --- a/scalafix-core/src/main/scala/scalafix/config/CustomMessage.scala +++ b/scalafix-core/src/main/scala/scalafix/config/CustomMessage.scala @@ -1,8 +1,9 @@ package scalafix.config -import metaconfig.{Conf, ConfDecoder} -import scalafix.v0.Symbol +import metaconfig.Conf +import metaconfig.ConfDecoder import scalafix.internal.config.ScalafixMetaconfigReaders._ +import scalafix.v0.Symbol class CustomMessage[T]( val value: T, diff --git a/scalafix-core/src/main/scala/scalafix/internal/config/ConfigRulePatches.scala b/scalafix-core/src/main/scala/scalafix/internal/config/ConfigRulePatches.scala index ff87c7fd5..e15f03fbf 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/config/ConfigRulePatches.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/config/ConfigRulePatches.scala @@ -1,12 +1,10 @@ package scalafix.internal.config -import scalafix.patch.Patch -import scalafix.patch.TreePatch.AddGlobalImport -import scalafix.patch.TreePatch.ReplaceSymbol -import scalafix.patch.TreePatch.RemoveGlobalImport import metaconfig.ConfDecoder import metaconfig.generic import metaconfig.generic.Surface +import scalafix.patch.Patch +import scalafix.patch.Patch.internal._ case class ConfigRulePatches( replaceSymbols: List[ReplaceSymbol] = Nil, diff --git a/scalafix-core/src/main/scala/scalafix/internal/config/DebugConfig.scala b/scalafix-core/src/main/scala/scalafix/internal/config/DebugConfig.scala index ddd32f7f4..9a55ffc33 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/config/DebugConfig.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/config/DebugConfig.scala @@ -1,7 +1,7 @@ package scalafix.internal.config -import metaconfig.generic import metaconfig.ConfDecoder +import metaconfig.generic import metaconfig.generic.Surface case class DebugConfig( diff --git a/scalafix-core/src/main/scala/scalafix/internal/config/FilterMatcher.scala b/scalafix-core/src/main/scala/scalafix/internal/config/FilterMatcher.scala index 6c100aef2..3c732485d 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/config/FilterMatcher.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/config/FilterMatcher.scala @@ -1,8 +1,8 @@ package scalafix.internal.config +import metaconfig._ import scala.meta.io.AbsolutePath import scala.util.matching.Regex -import metaconfig._ case class FilterMatcher( includeFilters: Regex, diff --git a/scalafix-core/src/main/scala/scalafix/internal/config/LintConfig.scala b/scalafix-core/src/main/scala/scalafix/internal/config/LintConfig.scala index d706c046a..fda268be7 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/config/LintConfig.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/config/LintConfig.scala @@ -1,9 +1,9 @@ package scalafix.internal.config -import scalafix.lint.LintSeverity import metaconfig.ConfDecoder import metaconfig.generic import metaconfig.generic.Surface +import scalafix.lint.LintSeverity case class LintConfig( explain: Boolean = false, diff --git a/scalafix-core/src/main/scala/scalafix/internal/config/MetaconfigOps.scala b/scalafix-core/src/main/scala/scalafix/internal/config/MetaconfigOps.scala index cba812f90..02baaad63 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/config/MetaconfigOps.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/config/MetaconfigOps.scala @@ -1,13 +1,13 @@ package scalafix.internal.config -import scala.{meta => m} -import metaconfig.Input -import metaconfig.Position import metaconfig.Conf import metaconfig.ConfDecoder import metaconfig.ConfError import metaconfig.Configured +import metaconfig.Input +import metaconfig.Position import metaconfig.internal.ConfGet +import scala.{meta => m} object MetaconfigOps { def traverse[T](lst: Seq[Configured[T]]): Configured[List[T]] = { diff --git a/scalafix-core/src/main/scala/scalafix/internal/config/PrintStreamReporter.scala b/scalafix-core/src/main/scala/scalafix/internal/config/PrintStreamReporter.scala index e5c10df27..9b8883a7d 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/config/PrintStreamReporter.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/config/PrintStreamReporter.scala @@ -3,8 +3,8 @@ package scalafix.internal.config import java.io.PrintStream import scala.meta.Position import scalafix.internal.util.PositionSyntax._ -import scalafix.lint.RuleDiagnostic import scalafix.lint.LintSeverity +import scalafix.lint.RuleDiagnostic /** A ScalafixReporter that emits messages to a PrintStream. */ case class PrintStreamReporter( diff --git a/scalafix-core/src/main/scala/scalafix/internal/config/ReaderUtil.scala b/scalafix-core/src/main/scala/scalafix/internal/config/ReaderUtil.scala index cc9c7dcdc..80455b88d 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/config/ReaderUtil.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/config/ReaderUtil.scala @@ -1,11 +1,10 @@ package scalafix.internal.config -import scala.reflect.ClassTag - -import metaconfig.ConfDecoder import metaconfig.Conf +import metaconfig.ConfDecoder import metaconfig.ConfError import metaconfig.Configured +import scala.reflect.ClassTag object ReaderUtil { def oneOf[T: ClassTag](options: sourcecode.Text[T]*): ConfDecoder[T] = { diff --git a/scalafix-core/src/main/scala/scalafix/internal/config/ScalafixConfig.scala b/scalafix-core/src/main/scala/scalafix/internal/config/ScalafixConfig.scala index c5f742ae8..9fa9db4eb 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/config/ScalafixConfig.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/config/ScalafixConfig.scala @@ -1,9 +1,9 @@ package scalafix.internal.config -import scala.meta._ -import scala.meta.dialects.Scala212 import metaconfig._ import metaconfig.generic.Surface +import scala.meta._ +import scala.meta.dialects.Scala212 case class ScalafixConfig( parser: ParserConfig = ParserConfig(), diff --git a/scalafix-core/src/main/scala/scalafix/internal/config/ScalafixMetaconfigReaders.scala b/scalafix-core/src/main/scala/scalafix/internal/config/ScalafixMetaconfigReaders.scala index 52fe97b52..620213a45 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/config/ScalafixMetaconfigReaders.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/config/ScalafixMetaconfigReaders.scala @@ -1,25 +1,25 @@ package scalafix.internal.config -import scala.meta.Ref -import scala.meta._ -import scala.meta.parsers.Parse -import scalafix.v0.Symbol -import scala.reflect.ClassTag -import scala.util.Try -import scala.util.matching.Regex -import scalafix.patch.TreePatch._ import java.io.OutputStream import java.io.PrintStream import java.net.URI import java.util.regex.Pattern import java.util.regex.PatternSyntaxException -import scala.util.control.NonFatal import metaconfig.Conf import metaconfig.ConfDecoder import metaconfig.ConfError import metaconfig.Configured import metaconfig.Configured.Ok +import scala.meta.Ref +import scala.meta._ +import scala.meta.parsers.Parse +import scala.reflect.ClassTag +import scala.util.Try +import scala.util.control.NonFatal +import scala.util.matching.Regex import scalafix.internal.util.SymbolOps +import scalafix.patch.Patch.internal._ +import scalafix.v0.Symbol import scalafix.v0._ object ScalafixMetaconfigReaders extends ScalafixMetaconfigReaders @@ -31,8 +31,8 @@ trait ScalafixMetaconfigReaders { ReaderUtil.oneOf[MetaParser](parseSource, parseStat, parseCase) } implicit lazy val dialectReader: ConfDecoder[Dialect] = { - import scala.meta.dialects._ import ScalafixConfig.{DefaultDialect => Default} + import scala.meta.dialects._ ReaderUtil.oneOf[Dialect]( Default, Scala211, diff --git a/scalafix-core/src/main/scala/scalafix/internal/config/ScalafixReporter.scala b/scalafix-core/src/main/scala/scalafix/internal/config/ScalafixReporter.scala index aafe063ae..67b5c8ae1 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/config/ScalafixReporter.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/config/ScalafixReporter.scala @@ -3,8 +3,8 @@ package scalafix.internal.config import metaconfig.ConfDecoder import metaconfig.ConfEncoder import scala.meta.Position -import scalafix.lint.RuleDiagnostic import scalafix.lint.LintSeverity +import scalafix.lint.RuleDiagnostic trait ScalafixReporter { private[scalafix] def report( diff --git a/scalafix-core/src/main/scala/scalafix/internal/diff/DiffDisable.scala b/scalafix-core/src/main/scala/scalafix/internal/diff/DiffDisable.scala index 0285e3d5b..d0d0a62fd 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/diff/DiffDisable.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/diff/DiffDisable.scala @@ -1,10 +1,8 @@ package scalafix.internal.diff -import scala.meta.inputs.Input -import scala.meta.Position - import scala.collection.mutable.StringBuilder - +import scala.meta.Position +import scala.meta.inputs.Input import scalafix.internal.util.IntervalSet object DiffDisable { diff --git a/scalafix-core/src/main/scala/scalafix/internal/patch/CrashingSemanticdbIndex.scala b/scalafix-core/src/main/scala/scalafix/internal/patch/CrashingSemanticdbIndex.scala index 5b592a820..4dad2cc56 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/patch/CrashingSemanticdbIndex.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/patch/CrashingSemanticdbIndex.scala @@ -1,8 +1,8 @@ package scalafix.internal.patch import scala.meta.Classpath -import scalafix.v0.SemanticdbIndex import scalafix.v0.Document +import scalafix.v0.SemanticdbIndex trait CrashingSemanticdbIndex extends SemanticdbIndex { final override def classpath: Classpath = diff --git a/scalafix-core/src/main/scala/scalafix/internal/patch/EscapeHatch.scala b/scalafix-core/src/main/scala/scalafix/internal/patch/EscapeHatch.scala index ebbe9d6ed..28464ad35 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/patch/EscapeHatch.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/patch/EscapeHatch.scala @@ -8,17 +8,14 @@ import scala.meta._ import scala.meta.contrib._ import scala.meta.tokens.Token import scala.meta.tokens.Token.Comment -import scalafix.v0._ import scalafix.internal.config.FilterMatcher import scalafix.internal.patch.EscapeHatch._ +import scalafix.internal.util.LintSyntax._ import scalafix.lint.RuleDiagnostic -import scalafix.patch.AtomicPatch -import scalafix.patch.Concat -import scalafix.patch.EmptyPatch -import scalafix.patch.LintPatch +import scalafix.patch.Patch.internal._ import scalafix.rule.RuleName import scalafix.util.TreeExtractors.Mods -import scalafix.internal.util.LintSyntax._ +import scalafix.v0._ /** EscapeHatch is an algorithm to selectively disable rules. There * are two mechanisms to do so: anchored comments and the @@ -37,7 +34,7 @@ class EscapeHatch private ( val diagnostics = List.newBuilder[RuleDiagnostic] patchesByName.foreach { case (rule, rulePatch) => - Patch.foreach(rulePatch) { + PatchInternals.foreach(rulePatch) { case LintPatch(message) => diagnostics += message.toDiagnostic(rule, ctx.config) case rewritePatch => @@ -75,7 +72,7 @@ class EscapeHatch private ( def loop(name: RuleName, patch: Patch): Patch = patch match { case AtomicPatch(underlying) => val hasDisabledPatch = { - val patches = Patch.treePatchApply(underlying)(ctx, index) + val patches = PatchInternals.treePatchApply(underlying)(ctx, index) patches.exists { tp => val byGit = diff.isDisabled(tp.tok.pos) val byEscape = isDisabledByEscape(name, tp.tok.pos.start) diff --git a/scalafix-core/src/main/scala/scalafix/internal/patch/ImportPatchOps.scala b/scalafix-core/src/main/scala/scalafix/internal/patch/ImportPatchOps.scala index 88ab03a50..09cae57b3 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/patch/ImportPatchOps.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/patch/ImportPatchOps.scala @@ -1,19 +1,17 @@ package scalafix.internal.patch import scala.annotation.tailrec -import scala.collection.immutable.Seq import scala.collection.mutable import scala.meta._ -import scalafix.v0.Symbol -import scalafix.v0.Signature import scalafix.internal.util.SymbolOps import scalafix.patch.Patch -import scalafix.patch.TreePatch -import scalafix.patch.TreePatch.ImportPatch +import scalafix.patch.Patch.internal._ import scalafix.rule.RuleCtx import scalafix.syntax._ import scalafix.util.Newline import scalafix.util.SemanticdbIndex +import scalafix.v0.Signature +import scalafix.v0.Symbol object ImportPatchOps { object symbols { @@ -89,11 +87,11 @@ object ImportPatchOps { } val isRemovedImportee = mutable.LinkedHashSet.empty[Importee] importPatches.foreach { - case TreePatch.RemoveGlobalImport(sym) => + case RemoveGlobalImport(sym) => allImporteeSymbols .withFilter(_._1 == sym.normalized) .foreach { case (_, x) => isRemovedImportee += x } - case x: TreePatch.RemoveImportee => isRemovedImportee += x.importee + case x: RemoveImportee => isRemovedImportee += x.importee case _ => } val importersToAdd = { @@ -108,13 +106,13 @@ object ImportPatchOps { isAlreadyImported += underlying } importPatches.flatMap { - case TreePatch.AddGlobalSymbol(symbol) + case AddGlobalSymbol(symbol) if !allNamedImports.contains(symbol) && !isAlreadyImported(symbol) && !isPredef(symbol) => isAlreadyImported += symbol SymbolOps.toImporter(symbol).toList - case TreePatch.AddGlobalImport(importer) + case AddGlobalImport(importer) // best effort deduplication for syntactic addGlobalImport(Importer) if !allImportersSyntax.contains(importer.syntax) => importer :: Nil diff --git a/scalafix-core/src/main/scala/scalafix/internal/patch/DeprecatedPatchOps.scala b/scalafix-core/src/main/scala/scalafix/internal/patch/LegacyPatchOps.scala similarity index 88% rename from scalafix-core/src/main/scala/scalafix/internal/patch/DeprecatedPatchOps.scala rename to scalafix-core/src/main/scala/scalafix/internal/patch/LegacyPatchOps.scala index 625d7640d..0ccb5f3ee 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/patch/DeprecatedPatchOps.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/patch/LegacyPatchOps.scala @@ -2,15 +2,17 @@ package scalafix.internal.patch import scala.meta.Importee import scala.meta.Importer -import scalafix.v0.Symbol import scala.meta.Token import scala.meta.Tree import scala.meta.tokens.Tokens -import scalafix.v0._ +import scalafix.internal.patch.LegacyPatchOps.DeprecationMessage +import scalafix.internal.util.SymbolOps.Root +import scalafix.patch.Patch.internal._ import scalafix.patch.PatchOps -import DeprecatedPatchOps.DeprecationMessage +import scalafix.v0.Symbol +import scalafix.v0._ -trait DeprecatedPatchOps extends PatchOps { +trait LegacyPatchOps extends PatchOps { @deprecated(DeprecationMessage, "0.6.0") final def removeImportee(importee: Importee): Patch = Patch.removeImportee(importee) @@ -49,15 +51,15 @@ trait DeprecatedPatchOps extends PatchOps { @deprecated(DeprecationMessage, "0.6.0") final def removeGlobalImport(symbol: Symbol)( implicit index: SemanticdbIndex): Patch = - Patch.removeGlobalImport(symbol) + RemoveGlobalImport(symbol) @deprecated(DeprecationMessage, "0.6.0") final def addGlobalImport(symbol: Symbol)( implicit index: SemanticdbIndex): Patch = - Patch.addGlobalImport(symbol) + AddGlobalSymbol(symbol) @deprecated(DeprecationMessage, "0.6.0") final def replaceSymbol(fromSymbol: Symbol.Global, toSymbol: Symbol.Global)( implicit index: SemanticdbIndex): Patch = - Patch.replaceSymbol(fromSymbol, toSymbol) + ReplaceSymbol(fromSymbol, toSymbol) @deprecated(DeprecationMessage, "0.6.0") final def replaceSymbols(toReplace: (String, String)*)( implicit index: SemanticdbIndex): Patch = @@ -70,12 +72,12 @@ trait DeprecatedPatchOps extends PatchOps { @deprecated(DeprecationMessage, "0.6.0") final def renameSymbol(fromSymbol: Symbol.Global, toName: String)( implicit index: SemanticdbIndex): Patch = - Patch.renameSymbol(fromSymbol, toName) + ReplaceSymbol(fromSymbol, Root(Signature.Term(toName))) @deprecated(DeprecationMessage, "0.6.0") final def lint(msg: Diagnostic): Patch = Patch.lint(msg) } -object DeprecatedPatchOps { +object LegacyPatchOps { private[scalafix] final val DeprecationMessage = "Use scalafix.Patch instead" } diff --git a/scalafix-core/src/main/scala/scalafix/internal/patch/PatchInternals.scala b/scalafix-core/src/main/scala/scalafix/internal/patch/PatchInternals.scala new file mode 100644 index 000000000..0430c9afc --- /dev/null +++ b/scalafix-core/src/main/scala/scalafix/internal/patch/PatchInternals.scala @@ -0,0 +1,167 @@ +package scalafix.internal.patch + +import scala.meta._ +import scalafix.internal.diff.DiffUtils +import scalafix.internal.util.Failure +import scalafix.internal.util.SuppressOps +import scalafix.internal.util.TokenOps +import scalafix.internal.v0.LegacyRuleCtx +import scalafix.internal.v0.LegacySemanticdbIndex +import scalafix.lint.RuleDiagnostic +import scalafix.patch.Patch.internal._ +import scalafix.patch._ +import scalafix.syntax._ +import scalafix.v0 +import scalafix.v1 + +object PatchInternals { + def merge(a: TokenPatch, b: TokenPatch): TokenPatch = (a, b) match { + case (add1: Add, add2: Add) => + Add( + add1.tok, + add1.addLeft + add2.addLeft, + add1.addRight + add2.addRight, + add1.keepTok && add2.keepTok) + case (_: Remove, add: Add) => add.copy(keepTok = false) + case (add: Add, _: Remove) => add.copy(keepTok = false) + case (rem: Remove, rem2: Remove) => rem + case _ => throw Failure.TokenPatchMergeError(a, b) + } + + // Patch.apply and Patch.lintMessages package private. Feel free to use them + // for your application, but please ask on the Gitter channel to see if we + // can expose a better api for your use case. + def apply( + patchesByName: Map[scalafix.rule.RuleName, scalafix.Patch], + ctx: v0.RuleCtx, + index: Option[v0.SemanticdbIndex], + suppress: Boolean = false + ): (String, List[RuleDiagnostic]) = { + if (FastPatch.shortCircuit(patchesByName, ctx)) { + (ctx.input.text, Nil) + } else { + val idx = index.getOrElse(v0.SemanticdbIndex.empty) + val (patch, lints) = ctx.escapeHatch.filter(patchesByName, ctx, idx) + val finalPatch = + if (suppress) { + patch + SuppressOps.addComments(ctx.tokens, lints.map(_.position)) + } else { + patch + } + val patches = treePatchApply(finalPatch)(ctx, idx) + (tokenPatchApply(ctx, patches), lints) + } + } + + def syntactic( + patchesByName: Map[scalafix.rule.RuleName, scalafix.Patch], + doc: v1.SyntacticDocument, + suppress: Boolean + ): (String, List[RuleDiagnostic]) = { + apply(patchesByName, new LegacyRuleCtx(doc), None, suppress) + } + + def semantic( + patchesByName: Map[scalafix.rule.RuleName, scalafix.Patch], + doc: v1.SemanticDocument, + suppress: Boolean + ): (String, List[RuleDiagnostic]) = { + apply( + patchesByName, + new LegacyRuleCtx(doc.internal.doc), + Some(new LegacySemanticdbIndex(doc)), + suppress) + } + + def treePatchApply(patch: Patch)( + implicit + ctx: v0.RuleCtx, + index: v0.SemanticdbIndex): Iterable[TokenPatch] = { + val base = underlying(patch) + val moveSymbol = underlying( + ReplaceSymbolOps.naiveMoveSymbolPatch(base.collect { + case m: ReplaceSymbol => m + })(ctx, index)) + val patches = base.filterNot(_.isInstanceOf[ReplaceSymbol]) ++ moveSymbol + val tokenPatches = patches.collect { case e: TokenPatch => e } + val importPatches = patches.collect { case e: ImportPatch => e } + val importTokenPatches = { + val result = + ImportPatchOps.superNaiveImportPatchToTokenPatchConverter( + ctx, + importPatches + )(index) + + underlying(result.asPatch) + .collect { + case x: TokenPatch => x + case els => + throw Failure.InvariantFailedException( + s"Expected TokenPatch, got $els") + } + } + importTokenPatches ++ tokenPatches + } + + private def tokenPatchApply( + ctx: v0.RuleCtx, + patches: Iterable[TokenPatch]): String = { + val patchMap = patches + .groupBy(x => TokenOps.hash(x.tok)) + .mapValues(_.reduce(merge).newTok) + ctx.tokens.toIterator + .map(tok => patchMap.getOrElse(TokenOps.hash(tok), tok.syntax)) + .mkString + } + + private def underlying(patch: Patch): Seq[Patch] = { + val builder = Seq.newBuilder[Patch] + foreach(patch) { + case _: LintPatch => + () + case els => + builder += els + } + builder.result() + } + + def isOnlyLintMessages(patch: Patch): Boolean = { + // TODO(olafur): foreach should really return Stream[Patch] for early termination. + var onlyLint = true + var hasLintMessage = false + foreach(patch) { + case _: LintPatch => hasLintMessage = true + case _ => onlyLint = false + } + patch.isEmpty || hasLintMessage && onlyLint + } + + def foreach(patch: Patch)(f: Patch => Unit): Unit = { + def loop(patch: Patch): Unit = patch match { + case Concat(a, b) => + loop(a) + loop(b) + case EmptyPatch => + () + case AtomicPatch(underlying) => + loop(underlying) + case els => + f(els) + } + loop(patch) + } + + def unifiedDiff(original: Input, revised: Input): String = { + unifiedDiff(original, revised, 3) + } + + def unifiedDiff(original: Input, revised: Input, context: Int): String = { + DiffUtils.unifiedDiff( + original.label, + revised.label, + new String(original.chars).lines.toList, + new String(revised.chars).lines.toList, + context) + } + +} diff --git a/scalafix-core/src/main/scala/scalafix/internal/patch/ReplaceSymbolOps.scala b/scalafix-core/src/main/scala/scalafix/internal/patch/ReplaceSymbolOps.scala index 1cf311bb3..670b7ea39 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/patch/ReplaceSymbolOps.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/patch/ReplaceSymbolOps.scala @@ -2,12 +2,12 @@ package scalafix.internal.patch import scala.meta._ import scala.meta.internal.trees._ -import scalafix.v0._ import scalafix.internal.util.SymbolOps.Root import scalafix.internal.util.SymbolOps.SignatureName import scalafix.patch.Patch -import scalafix.patch.TreePatch.ReplaceSymbol +import scalafix.patch.Patch.internal.ReplaceSymbol import scalafix.syntax._ +import scalafix.v0._ object ReplaceSymbolOps { private object Select { diff --git a/scalafix-core/src/main/scala/scalafix/internal/rule/RuleCtxImpl.scala b/scalafix-core/src/main/scala/scalafix/internal/rule/RuleCtxImpl.scala index 6fae2381c..1433f4676 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/rule/RuleCtxImpl.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/rule/RuleCtxImpl.scala @@ -5,22 +5,22 @@ import org.scalameta.logger import scala.meta._ import scala.meta.contrib.AssociatedComments import scala.meta.tokens.Tokens -import scalafix.v0._ -import scalafix.syntax._ import scalafix.internal.config.ScalafixConfig import scalafix.internal.diff.DiffDisable -import scalafix.internal.patch.DeprecatedPatchOps import scalafix.internal.patch.EscapeHatch +import scalafix.internal.patch.LegacyPatchOps +import scalafix.syntax._ import scalafix.util.MatchingParens import scalafix.util.SemanticdbIndex import scalafix.util.TokenList +import scalafix.v0._ case class RuleCtxImpl( tree: Tree, config: ScalafixConfig, diffDisable: DiffDisable) extends RuleCtx - with DeprecatedPatchOps { ctx => + with LegacyPatchOps { ctx => def syntax: String = s"""${tree.input.syntax} |${logger.revealWhitespace(tree.syntax.take(100))}""".stripMargin diff --git a/scalafix-core/src/main/scala/scalafix/internal/util/ClassloadRule.scala b/scalafix-core/src/main/scala/scalafix/internal/util/ClassloadRule.scala index 50bb8df3d..4fe8dc74a 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/util/ClassloadRule.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/util/ClassloadRule.scala @@ -1,13 +1,13 @@ package scalafix.internal.util import java.lang.reflect.InvocationTargetException -import scalafix.v0.SemanticdbIndex +import metaconfig.ConfError +import metaconfig.Configured import scala.reflect.ClassTag import scala.util.Success import scala.util.Try import scalafix.v0.Rule -import metaconfig.ConfError -import metaconfig.Configured +import scalafix.v0.SemanticdbIndex class ClassloadRule[T](classLoader: ClassLoader)(implicit ev: ClassTag[T]) { private val t = ev.runtimeClass diff --git a/scalafix-core/src/main/scala/scalafix/internal/util/DenotationOps.scala b/scalafix-core/src/main/scala/scalafix/internal/util/DenotationOps.scala index bd826fa2a..c61cc5717 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/util/DenotationOps.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/util/DenotationOps.scala @@ -1,7 +1,7 @@ package scalafix.internal.util -import scala.meta._ import scala.meta.Dialect +import scala.meta._ import scalafix.v0._ object DenotationOps { diff --git a/scalafix-core/src/main/scala/scalafix/internal/util/EagerInMemorySemanticdbIndex.scala b/scalafix-core/src/main/scala/scalafix/internal/util/EagerInMemorySemanticdbIndex.scala index 5d7dbccba..3ea5e4589 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/util/EagerInMemorySemanticdbIndex.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/util/EagerInMemorySemanticdbIndex.scala @@ -2,12 +2,12 @@ package scalafix.internal.util import scala.collection.mutable import scala.meta._ -import scala.meta.internal.{semanticdb => s} import scala.meta.internal.symtab._ +import scala.meta.internal.{semanticdb => s} import scalafix.internal.v0._ import scalafix.util.SemanticdbIndex -import scalafix.v0._ import scalafix.v0 +import scalafix.v0._ case class EagerInMemorySemanticdbIndex( database: Database, diff --git a/scalafix-core/src/main/scala/scalafix/internal/util/LintSyntax.scala b/scalafix-core/src/main/scala/scalafix/internal/util/LintSyntax.scala index 4a22356ee..1a861e75c 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/util/LintSyntax.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/util/LintSyntax.scala @@ -1,9 +1,9 @@ package scalafix.internal.util import scalafix.internal.config.ScalafixConfig -import scalafix.lint.RuleDiagnostic -import scalafix.lint.LintID import scalafix.lint.Diagnostic +import scalafix.lint.LintID +import scalafix.lint.RuleDiagnostic import scalafix.rule.RuleName object LintSyntax { diff --git a/scalafix-core/src/main/scala/scalafix/internal/util/PositionSyntax.scala b/scalafix-core/src/main/scala/scalafix/internal/util/PositionSyntax.scala index e6a4ca758..66823065c 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/util/PositionSyntax.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/util/PositionSyntax.scala @@ -1,8 +1,8 @@ package scalafix.internal.util -import scala.meta.internal.{semanticdb => s} import scala.meta._ import scala.meta.internal.ScalametaInternals +import scala.meta.internal.{semanticdb => s} object PositionSyntax { diff --git a/scalafix-core/src/main/scala/scalafix/internal/util/PrettyType.scala b/scalafix-core/src/main/scala/scalafix/internal/util/PrettyType.scala index 1923c2326..5ee9086be 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/util/PrettyType.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/util/PrettyType.scala @@ -2,10 +2,10 @@ package scalafix.internal.util import scala.collection.mutable import scala.meta._ -import scala.meta.internal.symtab.SymbolTable import scala.meta.internal.semanticdb.Scala._ import scala.meta.internal.semanticdb.SymbolInformation.{Kind => k} import scala.meta.internal.semanticdb.SymbolInformation.{Property => p} +import scala.meta.internal.symtab.SymbolTable import scala.meta.internal.{semanticdb => s} import scala.util.control.NoStackTrace import scala.util.control.NonFatal diff --git a/scalafix-core/src/main/scala/scalafix/internal/util/ProductStructure.scala b/scalafix-core/src/main/scala/scalafix/internal/util/ProductStructure.scala index 660a84198..3686c8477 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/util/ProductStructure.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/util/ProductStructure.scala @@ -3,9 +3,9 @@ package scalafix.internal.util import org.typelevel.paiges._ import scala.meta.Tree import scala.meta._ +import scalafix.internal.util.DocConstants._ import scalafix.v1.Symbol import scalafix.v1.SymbolInformation -import DocConstants._ object ProductLabeledStructure extends ProductStructure( diff --git a/scalafix-core/src/main/scala/scalafix/internal/util/SuppressOps.scala b/scalafix-core/src/main/scala/scalafix/internal/util/SuppressOps.scala index fdba7e928..c88f6df66 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/util/SuppressOps.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/util/SuppressOps.scala @@ -1,8 +1,9 @@ package scalafix.internal.util -import scala.meta.inputs.Position import scala.annotation.tailrec -import scala.meta.tokens.{Token, Tokens} +import scala.meta.inputs.Position +import scala.meta.tokens.Token +import scala.meta.tokens.Tokens import scalafix.patch.Patch object SuppressOps { diff --git a/scalafix-core/src/main/scala/scalafix/internal/util/SymbolOps.scala b/scalafix-core/src/main/scala/scalafix/internal/util/SymbolOps.scala index 24bb54899..36a5c761d 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/util/SymbolOps.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/util/SymbolOps.scala @@ -2,9 +2,9 @@ package scalafix.internal.util import scala.meta._ import scala.meta.internal.ScalametaInternals +import scala.meta.internal.semanticdb.Scala._ import scalafix.v0._ import scalafix.v1 -import scala.meta.internal.semanticdb.Scala._ object SymbolOps { diff --git a/scalafix-core/src/main/scala/scalafix/internal/v0/LegacyRuleCtx.scala b/scalafix-core/src/main/scala/scalafix/internal/v0/LegacyRuleCtx.scala index 7e1d77b4e..43838a416 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/v0/LegacyRuleCtx.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/v0/LegacyRuleCtx.scala @@ -5,7 +5,7 @@ import scala.meta.Input import scala.meta.Tree import scala.meta.contrib.AssociatedComments import scala.meta.tokens.Tokens -import scalafix.internal.patch.DeprecatedPatchOps +import scalafix.internal.patch.LegacyPatchOps import scalafix.util.MatchingParens import scalafix.util.SemanticdbIndex import scalafix.util.TokenList @@ -14,7 +14,7 @@ import scalafix.v1.SyntacticDocument class LegacyRuleCtx(doc: SyntacticDocument) extends RuleCtx - with DeprecatedPatchOps { + with LegacyPatchOps { override def tree: Tree = doc.tree override def input: Input = doc.input override def tokens: Tokens = doc.tokens diff --git a/scalafix-core/src/main/scala/scalafix/internal/v0/LegacySyntacticRule.scala b/scalafix-core/src/main/scala/scalafix/internal/v0/LegacySyntacticRule.scala index 073a381fc..1b9e217f7 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/v0/LegacySyntacticRule.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/v0/LegacySyntacticRule.scala @@ -4,8 +4,8 @@ import metaconfig.Configured import scalafix.patch.Patch import scalafix.v0 import scalafix.v1.Configuration -import scalafix.v1.SyntacticDocument import scalafix.v1.Rule +import scalafix.v1.SyntacticDocument import scalafix.v1.SyntacticRule class LegacySyntacticRule(rule: v0.Rule) extends SyntacticRule(rule.name) { diff --git a/scalafix-core/src/main/scala/scalafix/internal/v0/package.scala b/scalafix-core/src/main/scala/scalafix/internal/v0/package.scala index 278c7bfc6..568b30f13 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/v0/package.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/v0/package.scala @@ -1,10 +1,10 @@ package scalafix.internal -import scalafix.v0.{Flags => d} -import scala.meta.internal.{semanticdb => s} -import scala.meta.internal.semanticdb.SymbolInformation.{Property => p} import scala.meta.internal.semanticdb.SymbolInformation.{Kind => k} +import scala.meta.internal.semanticdb.SymbolInformation.{Property => p} +import scala.meta.internal.{semanticdb => s} import scalafix.v0.Denotation +import scalafix.v0.{Flags => d} package object v0 { diff --git a/scalafix-core/src/main/scala/scalafix/internal/v1/InternalSemanticDoc.scala b/scalafix-core/src/main/scala/scalafix/internal/v1/InternalSemanticDoc.scala index 260287b95..6e5148ea0 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/v1/InternalSemanticDoc.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/v1/InternalSemanticDoc.scala @@ -9,11 +9,11 @@ import scala.meta.internal.symtab.SymbolTable import scala.meta.internal.{semanticdb => s} import scalafix.internal.config.ScalafixConfig import scalafix.lint.Diagnostic -import scalafix.v1.SyntacticDocument import scalafix.v1.SemanticTree import scalafix.v1.Symbol import scalafix.v1.SymbolInformation import scalafix.v1.Symtab +import scalafix.v1.SyntacticDocument final class InternalSemanticDoc( val doc: SyntacticDocument, diff --git a/scalafix-core/src/main/scala/scalafix/internal/v1/PositionSearch.scala b/scalafix-core/src/main/scala/scalafix/internal/v1/PositionSearch.scala index 1ab9d577e..3cd5c7408 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/v1/PositionSearch.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/v1/PositionSearch.scala @@ -1,8 +1,8 @@ package scalafix.internal.v1 -import scala.meta.Tree -import scala.meta.Term import scala.meta.Position +import scala.meta.Term +import scala.meta.Tree import scalafix.internal.util.PositionSyntax._ object PositionSearch { diff --git a/scalafix-core/src/main/scala/scalafix/internal/v1/Rules.scala b/scalafix-core/src/main/scala/scalafix/internal/v1/Rules.scala index 620ce792b..d3f1aeb4c 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/v1/Rules.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/v1/Rules.scala @@ -4,16 +4,17 @@ import java.util.ServiceLoader import metaconfig.Configured import scala.meta.tokens.Tokens import scalafix.internal.config.MetaconfigOps +import scalafix.internal.patch.PatchInternals import scalafix.internal.util.SuppressOps -import scalafix.lint.RuleDiagnostic import scalafix.lint.Diagnostic +import scalafix.lint.RuleDiagnostic import scalafix.patch.Patch import scalafix.rule.RuleName import scalafix.v1.Configuration -import scalafix.v1.SyntacticDocument import scalafix.v1.Rule import scalafix.v1.SemanticDocument import scalafix.v1.SemanticRule +import scalafix.v1.SyntacticDocument import scalafix.v1.SyntacticRule case class Rules(rules: List[Rule] = Nil) { @@ -53,7 +54,7 @@ case class Rules(rules: List[Rule] = Nil) { case rule: SyntacticRule => rule.name -> rule.fix(sdoc.internal.doc) }.toMap - scalafix.Patch.semantic(fixes, sdoc, suppress) + PatchInternals.semantic(fixes, sdoc, suppress) } def syntacticPatch( @@ -63,7 +64,7 @@ case class Rules(rules: List[Rule] = Nil) { val fixes = syntacticRules.iterator.map { rule => rule.name -> rule.fix(doc) }.toMap - scalafix.Patch.syntactic(fixes, doc, suppress) + PatchInternals.syntactic(fixes, doc, suppress) } } diff --git a/scalafix-core/src/main/scala/scalafix/internal/v1/SemanticdbDiagnostic.scala b/scalafix-core/src/main/scala/scalafix/internal/v1/SemanticdbDiagnostic.scala index db79b2893..8a5f46d6e 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/v1/SemanticdbDiagnostic.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/v1/SemanticdbDiagnostic.scala @@ -1,12 +1,12 @@ package scalafix.internal.v1 -import scala.meta.Position import scala.meta.Input -import scalafix.lint.Diagnostic -import scalafix.lint.LintSeverity -import scala.meta.internal.{semanticdb => s} +import scala.meta.Position import scala.meta.internal.semanticdb.Diagnostic.{Severity => d} +import scala.meta.internal.{semanticdb => s} import scalafix.internal.util.PositionSyntax._ +import scalafix.lint.Diagnostic +import scalafix.lint.LintSeverity case class SemanticdbDiagnostic(input: Input, diagnostic: s.Diagnostic) extends Diagnostic { diff --git a/scalafix-core/src/main/scala/scalafix/internal/v1/package.scala b/scalafix-core/src/main/scala/scalafix/internal/v1/package.scala index c02f06860..c1aac29af 100644 --- a/scalafix-core/src/main/scala/scalafix/internal/v1/package.scala +++ b/scalafix-core/src/main/scala/scalafix/internal/v1/package.scala @@ -4,12 +4,12 @@ import java.net.URI import java.nio.charset.StandardCharsets import java.nio.file.Files import java.nio.file.Paths -import scala.meta.io.AbsolutePath -import scala.meta.io.RelativePath import scala.collection.JavaConverters._ import scala.meta.inputs.Input import scala.meta.internal.io.FileIO import scala.meta.internal.{semanticdb => s} +import scala.meta.io.AbsolutePath +import scala.meta.io.RelativePath package object v1 { diff --git a/scalafix-core/src/main/scala/scalafix/patch/Patch.scala b/scalafix-core/src/main/scala/scalafix/patch/Patch.scala index a1a26928f..923be021f 100644 --- a/scalafix-core/src/main/scala/scalafix/patch/Patch.scala +++ b/scalafix-core/src/main/scala/scalafix/patch/Patch.scala @@ -1,70 +1,39 @@ package scalafix.patch -import scala.collection.immutable.Seq -import scala.meta._ -import scalafix.syntax._ -import scalafix.patch.TokenPatch.Add -import scalafix.patch.TokenPatch.Remove -import scalafix.patch.TreePatch.ImportPatch -import scalafix.internal.diff.DiffUtils -import scalafix.internal.patch.ImportPatchOps -import scalafix.internal.patch.ReplaceSymbolOps -import scalafix.internal.util.Failure -import scalafix.internal.util.TokenOps -import scalafix.lint.Diagnostic -import scalafix.patch.TreePatch.ReplaceSymbol import org.scalameta.logger import scala.meta.Token -import scala.meta.tokens.Tokens -import scalafix.internal.patch.FastPatch +import scala.meta._ import scalafix.internal.config.ScalafixMetaconfigReaders -import scalafix.internal.util.SuppressOps import scalafix.internal.util.SymbolOps.Root -import scalafix.internal.v0.LegacyRuleCtx -import scalafix.internal.v0.LegacySemanticdbIndex -import scalafix.lint.RuleDiagnostic -import scalafix.patch.TreePatch.AddGlobalImport -import scalafix.patch.TreePatch.RemoveGlobalImport -import scalafix.rule.RuleCtx -import scalafix.util.SemanticdbIndex -import scalafix.v1.SyntacticDocument -import scalafix.v1.SemanticContext -import scalafix.v1.SemanticDocument -import scalafix.v0.Signature -import scalafix.v0.Symbol +import scalafix.lint.Diagnostic +import scalafix.patch.Patch.internal._ +import scalafix.v0 import scalafix.v1 +import scalafix.v1.SemanticContext /** - * A data structure that can produce a .patch file. - * - * The best way to build a Patch is with a RuleCtx inside a Rule. - * For example, `Rule.syntactic(ctx => ctx.addLeft(ctx.tree.tokens.head): Patch)` - * - * Patches can be composed with Patch.+ and Patch.++. A Seq[Patch] can be combined - * into a single patch with `Seq[Patch](...).asPatch` with `import scalafix.v0._`. - * - * Patches are split into low-level token patches and high-level tree patches. - * A token patch works on scala.meta.Token and provides surgical precision over - * how details like formatting are managed by the rule. + * An immutable sealed data structure that describes how to rewrite and lint a source file. * - * NOTE: Patch current only works for a single file, but it may be possible - * to add support in the future for combining patches for different files - * with Patch + Patch. + * Documentation: https://scalacenter.github.io/scalafix/docs/developers/patch.html */ -sealed abstract class Patch { - // NOTE: potential bottle-neck, this might be very slow for large - // patches. We might want to group related patches and enforce some ordering. +sealed abstract class Patch extends Product { + + /** Merge this patch together with the other patch. */ def +(other: Patch): Patch = if (this eq other) this else if (isEmpty) other else if (other.isEmpty) this else Concat(this, other) + + /** Merge this patch together with the other patch if non-empty. */ def +(other: Option[Patch]): Patch = this.+(other.getOrElse(Patch.empty)) + + /** Merge this patch together with the other patches. */ def ++(other: Iterable[Patch]): Patch = other.foldLeft(this)(_ + _) /** - * Returns true if this patch is Patch.empty, false otherwise. + * Returns true if this patch is equal to Patch.empty, false otherwise. * * Note, may return false for patches that produce empty diffs. For example, a [[Patch]].replaceSymbol * returns false even if the symbol from is not referenced in the code resulting in an empty diff. @@ -81,268 +50,135 @@ sealed abstract class Patch { } -////////////////////////////// -// Low-level patches -////////////////////////////// -trait LowLevelPatch - -abstract class TokenPatch(val tok: Token, val newTok: String) - extends Patch - with LowLevelPatch { - override def toString: String = - if (newTok.isEmpty) - s"TokenPatch.Remove(${logger.revealWhitespace(tok.structure)})" - else - s"TokenPatch.${this.getClass.getSimpleName}(${logger.revealWhitespace( - tok.syntax)}, ${tok.structure}, $newTok)" -} -private[scalafix] object TokenPatch { - case class Remove(override val tok: Token) extends TokenPatch(tok, "") - case class Add( - override val tok: Token, - addLeft: String, - addRight: String, - keepTok: Boolean = true) - extends TokenPatch( - tok, - s"""$addLeft${if (keepTok) tok else ""}$addRight""") - -} - -////////////////////////////// -// High-level patches -////////////////////////////// -abstract class TreePatch extends Patch -private[scalafix] object TreePatch { - abstract class ImportPatch extends TreePatch - case class RemoveGlobalImport(symbol: Symbol) extends ImportPatch - case class RemoveImportee(importee: Importee) extends ImportPatch - case class AddGlobalImport(importer: Importer) extends ImportPatch - case class AddGlobalSymbol(symbol: Symbol) extends ImportPatch - case class ReplaceSymbol(from: Symbol.Global, to: Symbol.Global) - extends TreePatch -} +object Patch { -// implementation detail -private[scalafix] case class AtomicPatch(underlying: Patch) extends Patch -private[scalafix] case class LintPatch(message: Diagnostic) extends Patch -private[scalafix] case class Concat(a: Patch, b: Patch) extends Patch -private[scalafix] case object EmptyPatch extends Patch with LowLevelPatch + /** Combine a sequence of patches into a single patch. */ + def fromIterable(seq: Iterable[Patch]): Patch = + seq.foldLeft(empty)(_ + _) -object Patch { + /** Do nothing: no diff, no diagnostics. */ + val empty: Patch = EmptyPatch + /** Reports error/warning/info message at a position. */ def lint(msg: Diagnostic): Patch = LintPatch(msg) - // Syntactic patch ops. + /** Remove this importee reference. */ def removeImportee(importee: Importee): Patch = - TreePatch.RemoveImportee(importee) + RemoveImportee(importee) + /** Add this importer to the global import list. */ def addGlobalImport(importer: Importer): Patch = AddGlobalImport(importer) + /** Remove the token and insert this string at the same position. */ def replaceToken(token: Token, toReplace: String): Patch = Add(token, "", toReplace, keepTok = false) - def removeTokens(tokens: Tokens): Patch = - doRemoveTokens(tokens) + /** Remove all of the these tokens from the source file. */ def removeTokens(tokens: Iterable[Token]): Patch = - doRemoveTokens(tokens) - private def doRemoveTokens(tokens: Iterable[Token]): Patch = - tokens.foldLeft(Patch.empty)(_ + TokenPatch.Remove(_)) - def removeToken(token: Token): Patch = Add(token, "", "", keepTok = false) + tokens.foldLeft(Patch.empty)(_ + Remove(_)) + /** Remove this single token from the source file. */ + def removeToken(token: Token): Patch = + Add(token, "", "", keepTok = false) + + /** + * Remove all tokens from this tree and add a string add the same position. + * + * Beware that this patch does not compose with other patches touching the same + * tree node or its children. Avoid using this method for large tree nodes like + * classes of methods. It's recommended to target as precise tree nodes as possible. + * + * It is better to use addRight/addLeft if you only insert new code, example: + * - bad: Patch.replaceTree(tree, "(" + tree.syntax + ")") + * - good: Patch.addLeft(tree, "(") + Patch.addRight(tree, + ")") + */ def replaceTree(from: Tree, to: String): Patch = { - val tokens = from.tokens - removeTokens(tokens) + tokens.headOption.map(x => addRight(x, to)) + if (from.pos == Position.None) Patch.empty + else { + removeTokens(from.tokens) + + from.tokens.headOption.map(x => addRight(x, to)) + } } - def addRight(tok: Token, toAdd: String): Patch = Add(tok, "", toAdd) + /** Add this string to the right of this token. */ + def addRight(tok: Token, toAdd: String): Patch = + Add(tok, "", toAdd) + /** Add this string to the right of this tree. */ def addRight(tree: Tree, toAdd: String): Patch = tree.tokens.lastOption.fold(Patch.empty)(addRight(_, toAdd)) - def addLeft(tok: Token, toAdd: String): Patch = Add(tok, toAdd, "") + /** Add this string to the left of this token. */ + def addLeft(tok: Token, toAdd: String): Patch = + Add(tok, toAdd, "") + /** Add this string to the left of this tree. */ def addLeft(tree: Tree, toAdd: String): Patch = tree.tokens.headOption.fold(Patch.empty)(addLeft(_, toAdd)) - // Semantic patch ops. + /** + * Remove named imports for this symbol. + * + * Does not remove wildcard imports for the enclosing package or class. + */ def removeGlobalImport(symbol: v1.Symbol)( implicit c: SemanticContext): Patch = - RemoveGlobalImport(Symbol(symbol.value)) + RemoveGlobalImport(v0.Symbol(symbol.value)) + /** Place named import for this symbol at the bottom of the global import list */ def addGlobalImport(symbol: v1.Symbol)(implicit c: SemanticContext): Patch = - TreePatch.AddGlobalSymbol(Symbol(symbol.value)) - def removeGlobalImport(symbol: Symbol)(implicit c: SemanticContext): Patch = - RemoveGlobalImport(symbol) - def addGlobalImport(symbol: Symbol)(implicit c: SemanticContext): Patch = - TreePatch.AddGlobalSymbol(symbol) - def replaceSymbol(fromSymbol: Symbol.Global, toSymbol: Symbol.Global)( - implicit c: SemanticContext): Patch = - TreePatch.ReplaceSymbol(fromSymbol, toSymbol) + AddGlobalSymbol(v0.Symbol(symbol.value)) + + /** + * Replace occurrences of fromSymbol to reference toSymbol instead. + * + * `toSymbol` must be a global symbol such as an object/class or a static method. + * + * May produce broken code in some cases, works best when toSymbol has the same depth + * as fromSymbol, example: + * - good: replace:com.foo.Bar/org.qux.Buz + * - bad: replace:com.Bar/org.qux.Buz + */ def replaceSymbols(toReplace: (String, String)*)( implicit c: SemanticContext): Patch = toReplace.foldLeft(Patch.empty) { case (a, (from, to)) => val (fromSymbol, toSymbol) = ScalafixMetaconfigReaders.parseReplaceSymbol(from, to).get - a + Patch.replaceSymbol(fromSymbol, toSymbol) - } - def replaceSymbols(toReplace: Seq[(String, String)])( - implicit noop: DummyImplicit, - c: SemanticContext): Patch = { - replaceSymbols(toReplace: _*) - } - def renameSymbol(fromSymbol: Symbol.Global, toName: String)( - implicit c: SemanticContext): Patch = - TreePatch.ReplaceSymbol(fromSymbol, Root(Signature.Term(toName))) - - /** Combine a sequence of patches into a single patch */ - def fromIterable(seq: Iterable[Patch]): Patch = - seq.foldLeft(empty)(_ + _) - - /** A patch that does no diff/rule */ - val empty: Patch = EmptyPatch - - def merge(a: TokenPatch, b: TokenPatch): TokenPatch = (a, b) match { - case (add1: Add, add2: Add) => - Add( - add1.tok, - add1.addLeft + add2.addLeft, - add1.addRight + add2.addRight, - add1.keepTok && add2.keepTok) - case (_: Remove, add: Add) => add.copy(keepTok = false) - case (add: Add, _: Remove) => add.copy(keepTok = false) - case (rem: Remove, rem2: Remove) => rem - case _ => throw Failure.TokenPatchMergeError(a, b) - } - - // Patch.apply and Patch.lintMessages package private. Feel free to use them - // for your application, but please ask on the Gitter channel to see if we - // can expose a better api for your use case. - private[scalafix] def apply( - patchesByName: Map[scalafix.rule.RuleName, scalafix.Patch], - ctx: RuleCtx, - index: Option[SemanticdbIndex], - suppress: Boolean = false - ): (String, List[RuleDiagnostic]) = { - if (FastPatch.shortCircuit(patchesByName, ctx)) { - (ctx.input.text, Nil) - } else { - val idx = index.getOrElse(SemanticdbIndex.empty) - val (patch, lints) = ctx.escapeHatch.filter(patchesByName, ctx, idx) - val finalPatch = - if (suppress) { - patch + SuppressOps.addComments(ctx.tokens, lints.map(_.position)) - } else { - patch - } - val patches = treePatchApply(finalPatch)(ctx, idx) - (tokenPatchApply(ctx, patches), lints) + a + ReplaceSymbol(fromSymbol, toSymbol) } - } - - private[scalafix] def syntactic( - patchesByName: Map[scalafix.rule.RuleName, scalafix.Patch], - doc: SyntacticDocument, - suppress: Boolean - ): (String, List[RuleDiagnostic]) = { - apply(patchesByName, new LegacyRuleCtx(doc), None, suppress) - } - - private[scalafix] def semantic( - patchesByName: Map[scalafix.rule.RuleName, scalafix.Patch], - doc: SemanticDocument, - suppress: Boolean - ): (String, List[RuleDiagnostic]) = { - apply( - patchesByName, - new LegacyRuleCtx(doc.internal.doc), - Some(new LegacySemanticdbIndex(doc)), - suppress) - } - def treePatchApply(patch: Patch)( - implicit - ctx: RuleCtx, - index: SemanticdbIndex): Iterable[TokenPatch] = { - val base = underlying(patch) - val moveSymbol = underlying( - ReplaceSymbolOps.naiveMoveSymbolPatch(base.collect { - case m: ReplaceSymbol => m - })(ctx, index)) - val patches = base.filterNot(_.isInstanceOf[ReplaceSymbol]) ++ moveSymbol - val tokenPatches = patches.collect { case e: TokenPatch => e } - val importPatches = patches.collect { case e: ImportPatch => e } - val importTokenPatches = { - val result = - ImportPatchOps.superNaiveImportPatchToTokenPatchConverter( - ctx, - importPatches - )(index) - - Patch - .underlying(result.asPatch) - .collect { - case x: TokenPatch => x - case els => - throw Failure.InvariantFailedException( - s"Expected TokenPatch, got $els") - } - } - importTokenPatches ++ tokenPatches - } - - private def tokenPatchApply( - ctx: RuleCtx, - patches: Iterable[TokenPatch]): String = { - val patchMap = patches - .groupBy(x => TokenOps.hash(x.tok)) - .mapValues(_.reduce(merge).newTok) - ctx.tokens.toIterator - .map(tok => patchMap.getOrElse(TokenOps.hash(tok), tok.syntax)) - .mkString - } - - private def underlying(patch: Patch): Seq[Patch] = { - val builder = Seq.newBuilder[Patch] - foreach(patch) { - case _: LintPatch => - () - case els => - builder += els - } - builder.result() - } - - private[scalafix] def isOnlyLintMessages(patch: Patch): Boolean = { - // TODO(olafur): foreach should really return Stream[Patch] for early termination. - var onlyLint = true - var hasLintMessage = false - foreach(patch) { - case _: LintPatch => hasLintMessage = true - case _ => onlyLint = false - } - patch.isEmpty || hasLintMessage && onlyLint - } - - private[scalafix] def foreach(patch: Patch)(f: Patch => Unit): Unit = { - def loop(patch: Patch): Unit = patch match { - case Concat(a, b) => - loop(a) - loop(b) - case EmptyPatch => - () - case AtomicPatch(underlying) => - loop(underlying) - case els => - f(els) + /** Replace occurrences of fromSymbol to use toName instead */ + def renameSymbol(fromSymbol: v1.Symbol, toName: String)( + implicit c: SemanticContext): Patch = + ReplaceSymbol( + v0.Symbol(fromSymbol.value).asInstanceOf[v0.Symbol.Global], + Root(v0.Signature.Term(toName))) + + private[scalafix] object internal { + trait LowLevelPatch + abstract class TokenPatch(val tok: Token, val newTok: String) + extends Patch + with LowLevelPatch { + override def toString: String = + if (newTok.isEmpty) + s"Remove(${logger.revealWhitespace(tok.structure)})" + else + s"$productPrefix(${logger.revealWhitespace(tok.syntax)}, ${tok.structure}, $newTok)" } - loop(patch) - } - - def unifiedDiff(original: Input, revised: Input): String = { - unifiedDiff(original, revised, 3) - } - - def unifiedDiff(original: Input, revised: Input, context: Int): String = { - DiffUtils.unifiedDiff( - original.label, - revised.label, - new String(original.chars).lines.toList, - new String(revised.chars).lines.toList, - context) + case class Remove(override val tok: Token) extends TokenPatch(tok, "") + case class Add( + override val tok: Token, + addLeft: String, + addRight: String, + keepTok: Boolean = true) + extends TokenPatch( + tok, + s"""$addLeft${if (keepTok) tok else ""}$addRight""") + abstract class TreePatch extends Patch + abstract class ImportPatch extends TreePatch + case class RemoveGlobalImport(symbol: v0.Symbol) extends ImportPatch + case class RemoveImportee(importee: Importee) extends ImportPatch + case class AddGlobalImport(importer: Importer) extends ImportPatch + case class AddGlobalSymbol(symbol: v0.Symbol) extends ImportPatch + case class ReplaceSymbol(from: v0.Symbol.Global, to: v0.Symbol.Global) + extends TreePatch + case class AtomicPatch(underlying: Patch) extends Patch + case class LintPatch(message: Diagnostic) extends Patch + case class Concat(a: Patch, b: Patch) extends Patch + case object EmptyPatch extends Patch with LowLevelPatch } } diff --git a/scalafix-core/src/main/scala/scalafix/patch/PatchOps.scala b/scalafix-core/src/main/scala/scalafix/patch/PatchOps.scala index c587d8597..a4d344398 100644 --- a/scalafix-core/src/main/scala/scalafix/patch/PatchOps.scala +++ b/scalafix-core/src/main/scala/scalafix/patch/PatchOps.scala @@ -1,9 +1,9 @@ package scalafix.patch import scala.meta._ -import scalafix.v0.Symbol import scalafix.lint.Diagnostic import scalafix.util.SemanticdbIndex +import scalafix.v0.Symbol trait PatchOps { diff --git a/scalafix-core/src/main/scala/scalafix/syntax/package.scala b/scalafix-core/src/main/scala/scalafix/syntax/package.scala index b9a0f11d1..2a05973b8 100644 --- a/scalafix-core/src/main/scala/scalafix/syntax/package.scala +++ b/scalafix-core/src/main/scala/scalafix/syntax/package.scala @@ -1,13 +1,13 @@ package scalafix -import scala.meta._ -import scalafix.v0.Symbol import scala.compat.Platform.EOL +import scala.meta._ import scala.meta.internal.scalafix.ScalafixScalametaHacks -import scalafix.internal.util.SymbolOps import scalafix.internal.util.DenotationOps +import scalafix.internal.util.SymbolOps import scalafix.util.SymbolMatcher import scalafix.util.TreeOps +import scalafix.v0.Symbol package object syntax { implicit class XtensionRefSymbolOpt(tree: Tree)( diff --git a/scalafix-core/src/main/scala/scalafix/util/MatchingParens.scala b/scalafix-core/src/main/scala/scalafix/util/MatchingParens.scala index 4c3e0c0d0..2d9537fc1 100644 --- a/scalafix-core/src/main/scala/scalafix/util/MatchingParens.scala +++ b/scalafix-core/src/main/scala/scalafix/util/MatchingParens.scala @@ -1,8 +1,8 @@ package scalafix.util import scala.meta._ +import scala.meta.tokens.Token._ import scalafix.internal.util.TokenOps._ -import tokens.Token._ sealed abstract class MatchingParens(map: Map[TokenHash, Token]) { private def lookup(token: Token) = map.get(hash(token)) diff --git a/scalafix-core/src/main/scala/scalafix/v0/LintCategory.scala b/scalafix-core/src/main/scala/scalafix/v0/LintCategory.scala index 6a321d695..d43a85e5b 100644 --- a/scalafix-core/src/main/scala/scalafix/v0/LintCategory.scala +++ b/scalafix-core/src/main/scala/scalafix/v0/LintCategory.scala @@ -1,7 +1,7 @@ package scalafix.v0 -import scalafix.internal.config.LintConfig import scala.meta.inputs.Position +import scalafix.internal.config.LintConfig import scalafix.lint.LintSeverity /** A unique identifier for one kind of a linter message. diff --git a/scalafix-core/src/main/scala/scalafix/v0/Rule.scala b/scalafix-core/src/main/scala/scalafix/v0/Rule.scala index 8231e6108..c01474114 100644 --- a/scalafix-core/src/main/scala/scalafix/v0/Rule.scala +++ b/scalafix-core/src/main/scala/scalafix/v0/Rule.scala @@ -1,12 +1,13 @@ package scalafix.v0 +import metaconfig.Conf +import metaconfig.Configured import scala.meta._ import scalafix.internal.config.MetaconfigOps import scalafix.internal.config.ScalafixConfig -import scalafix.syntax._ -import metaconfig.Conf -import metaconfig.Configured +import scalafix.internal.patch.PatchInternals import scalafix.lint.RuleDiagnostic +import scalafix.syntax._ /** A Scalafix Rule. * @@ -79,19 +80,19 @@ abstract class Rule(ruleName: RuleName) { self => final def apply(ctx: RuleCtx, patches: Map[RuleName, Patch]): String = { // This overload of apply if purely for convenience // Use `applyAndLint` to iterate over Diagnostic without printing to the console - val (fixed, diagnostics) = Patch(patches, ctx, semanticOption) + val (fixed, diagnostics) = PatchInternals(patches, ctx, semanticOption) diagnostics.foreach(diag => ctx.config.reporter.lint(diag)) fixed } final def applyAndLint(ctx: RuleCtx): (String, List[RuleDiagnostic]) = - Patch(fixWithName(ctx), ctx, semanticOption) + PatchInternals(fixWithName(ctx), ctx, semanticOption) /** Returns unified diff from applying this patch */ final def diff(ctx: RuleCtx): String = diff(ctx, fix(ctx)) final protected def diff(ctx: RuleCtx, patch: Patch): String = { val original = ctx.tree.input - Patch.unifiedDiff( + PatchInternals.unifiedDiff( original, Input.VirtualFile(original.label, apply(ctx, patch))) } diff --git a/scalafix-core/src/main/scala/scalafix/v0/RuleCtx.scala b/scalafix-core/src/main/scala/scalafix/v0/RuleCtx.scala index e6a06091b..2ea2f5139 100644 --- a/scalafix-core/src/main/scala/scalafix/v0/RuleCtx.scala +++ b/scalafix-core/src/main/scala/scalafix/v0/RuleCtx.scala @@ -1,16 +1,16 @@ package scalafix.v0 +import org.scalameta.FileLine import scala.meta._ import scala.meta.contrib.AssociatedComments import scala.meta.tokens.Tokens import scalafix.internal.config.ScalafixConfig import scalafix.internal.diff.DiffDisable +import scalafix.internal.patch.EscapeHatch import scalafix.internal.rule.RuleCtxImpl import scalafix.patch.PatchOps import scalafix.util.MatchingParens import scalafix.util.TokenList -import org.scalameta.FileLine -import scalafix.internal.patch.EscapeHatch trait RuleCtx extends PatchOps { diff --git a/scalafix-core/src/main/scala/scalafix/v0/SemanticdbIndex.scala b/scalafix-core/src/main/scala/scalafix/v0/SemanticdbIndex.scala index 1df414d03..5308c7898 100644 --- a/scalafix-core/src/main/scala/scalafix/v0/SemanticdbIndex.scala +++ b/scalafix-core/src/main/scala/scalafix/v0/SemanticdbIndex.scala @@ -1,10 +1,10 @@ package scalafix.v0 import scala.meta._ +import scala.meta.internal.semanticdb.SymbolInformation import scala.meta.internal.symtab.SymbolTable +import scalafix.v0.SemanticdbIndex.DeprecationMessage import scalafix.v1.SemanticContext -import SemanticdbIndex.DeprecationMessage -import scala.meta.internal.semanticdb.SymbolInformation /** An index for looking up data in a scala.meta.Database. */ trait SemanticdbIndex extends SemanticContext { diff --git a/scalafix-core/src/main/scala/scalafix/v1/SemanticDocument.scala b/scalafix-core/src/main/scala/scalafix/v1/SemanticDocument.scala index 794cd6db3..0af9b4654 100644 --- a/scalafix-core/src/main/scala/scalafix/v1/SemanticDocument.scala +++ b/scalafix-core/src/main/scala/scalafix/v1/SemanticDocument.scala @@ -1,13 +1,13 @@ package scalafix.v1 -import scala.meta.io.RelativePath import scala.meta._ import scala.meta.contrib.AssociatedComments import scala.meta.internal.symtab.SymbolTable -import scalafix.util.MatchingParens -import scalafix.util.TokenList import scala.meta.internal.{semanticdb => s} +import scala.meta.io.RelativePath import scalafix.internal.v1._ +import scalafix.util.MatchingParens +import scalafix.util.TokenList final class SemanticDocument private[scalafix] ( private[scalafix] val internal: InternalSemanticDoc diff --git a/scalafix-core/src/main/scala/scalafix/v1/SymbolInformation.scala b/scalafix-core/src/main/scala/scalafix/v1/SymbolInformation.scala index c7309a2d9..e86e10814 100644 --- a/scalafix-core/src/main/scala/scalafix/v1/SymbolInformation.scala +++ b/scalafix-core/src/main/scala/scalafix/v1/SymbolInformation.scala @@ -1,12 +1,11 @@ package scalafix.v1 import scala.meta.internal.metap.PrinterSymtab -import scala.meta.internal.{semanticdb => s} import scala.meta.internal.semanticdb._ -import scalafix.internal.v1.SymtabFromProtobuf - +import scala.meta.internal.{semanticdb => s} import scala.meta.metap.Format import scalafix.internal.v1.SymbolInformationAnnotations._ +import scalafix.internal.v1.SymtabFromProtobuf /** * Describes metadata about a symbol such as a method, class or trait. diff --git a/scalafix-docs/src/main/scala/scalafix/docs/PatchDocs.scala b/scalafix-docs/src/main/scala/scalafix/docs/PatchDocs.scala index 3d1c7e3f1..f2d330e6b 100644 --- a/scalafix-docs/src/main/scala/scalafix/docs/PatchDocs.scala +++ b/scalafix-docs/src/main/scala/scalafix/docs/PatchDocs.scala @@ -8,6 +8,7 @@ import scala.meta.metap.Format import scalafix.internal.reflect.ClasspathOps import scalafix.internal.v1.InternalSemanticDoc import scalafix.patch.Patch +import scalafix.internal.patch.PatchInternals import scalafix.v1.RuleName import scalafix.v1.SemanticDocument import scalafix.v1.Symbol @@ -20,7 +21,10 @@ object PatchDocs { implicit class XtensionPatch(p: Patch) { def output(implicit doc: SemanticDocument): String = { val (obtained, _) = - Patch.semantic(Map(RuleName("patch") -> p), doc, suppress = false) + PatchInternals.semantic( + Map(RuleName("patch") -> p), + doc, + suppress = false) obtained } def showDiff(context: Int = 0)(implicit doc: SemanticDocument): Unit = { @@ -29,7 +33,7 @@ object PatchDocs { } implicit class XtensionPatchs(p: Iterable[Patch]) { def showLints()(implicit doc: SemanticDocument): Unit = { - val (_, diagnostics) = Patch.semantic( + val (_, diagnostics) = PatchInternals.semantic( Map(RuleName("RuleName") -> p.asPatch), doc, suppress = false) @@ -56,7 +60,7 @@ object PatchDocs { implicit doc: SemanticDocument): String = { val in = Input.VirtualFile("before patch", doc.input.text) val out = Input.VirtualFile("after patch", obtained) - Patch.unifiedDiff(in, out, context) + PatchInternals.unifiedDiff(in, out, context) } lazy val compiler = InteractiveSemanticdb.newCompiler(List("-Ywarn-unused")) lazy val symtab = GlobalSymbolTable(ClasspathOps.thisClasspath) diff --git a/scalafix-reflect/src/main/scala/scalafix/v1/RuleDecoder.scala b/scalafix-reflect/src/main/scala/scalafix/v1/RuleDecoder.scala index 79a853949..21cc575b1 100644 --- a/scalafix-reflect/src/main/scala/scalafix/v1/RuleDecoder.scala +++ b/scalafix-reflect/src/main/scala/scalafix/v1/RuleDecoder.scala @@ -14,7 +14,6 @@ import scalafix.internal.reflect.RuleDecoderOps.tryClassload import scalafix.internal.reflect.ScalafixToolbox import scalafix.internal.reflect.ScalafixToolbox.CompiledRules import scalafix.internal.v1.Rules -import scalafix.patch.TreePatch import scalafix.v1 import scala.meta.io.Classpath import scalafix.internal.reflect.ClasspathOps @@ -55,7 +54,7 @@ object RuleDecoder { // Patch.replaceSymbols(from, to) case UriRuleString("replace", replace @ SlashSeparated(from, to)) => val constant = parseReplaceSymbol(from, to) - .map(TreePatch.ReplaceSymbol.tupled) + .map(Patch.internal.ReplaceSymbol.tupled) .map(p => scalafix.v1.SemanticRule.constant(replace, p.atomic)) constant :: Nil // Classload rule from classloader diff --git a/scalafix-tests/unit/src/main/scala/scalafix/test/ExplicitSynthetic.scala b/scalafix-tests/unit/src/main/scala/scalafix/test/ExplicitSynthetic.scala index 06a236f42..0c4d7146f 100644 --- a/scalafix-tests/unit/src/main/scala/scalafix/test/ExplicitSynthetic.scala +++ b/scalafix-tests/unit/src/main/scala/scalafix/test/ExplicitSynthetic.scala @@ -49,7 +49,7 @@ class ExplicitSynthetic(insertInfixTypeParam: Boolean) Patch.addRight(t, ".apply") } } - patches.flatten.asPatch + patches.flatten.asPatch + Patch.replaceTree(q"a", "b") } }