All Downloads are FREE. Search and download functionalities are using the official Maven repository.

scalafix.internal.v1.MainOps.scala Maven / Gradle / Ivy

The newest version!
package scalafix.internal.v1

import java.io.ByteArrayOutputStream
import java.io.PrintStream
import java.nio.CharBuffer
import java.nio.charset.StandardCharsets
import java.nio.file.FileVisitResult
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.SimpleFileVisitor
import java.nio.file.attribute.BasicFileAttributes

import scala.annotation.tailrec
import scala.collection.mutable.ListBuffer
import scala.util.Failure
import scala.util.Success
import scala.util.Try
import scala.util.control.NoStackTrace
import scala.util.control.NonFatal

import scala.meta.inputs.Input
import scala.meta.internal.inputs.XtensionInput
import scala.meta.internal.semanticdb.TextDocument
import scala.meta.io.AbsolutePath
import scala.meta.parsers.ParseException

import metaconfig.Conf
import metaconfig.ConfEncoder
import metaconfig.Configured
import metaconfig.annotation.Hidden
import metaconfig.annotation.Inline
import metaconfig.generic.Setting
import metaconfig.generic.Settings
import metaconfig.internal.Case
import org.typelevel.paiges.{Doc => D}
import scalafix.Versions
import scalafix.cli.ExitStatus
import scalafix.interfaces.ScalafixEvaluation
import scalafix.internal.config.PrintStreamReporter
import scalafix.internal.diff.DiffUtils
import scalafix.internal.interfaces.ScalafixEvaluationImpl
import scalafix.internal.interfaces.ScalafixFileEvaluationImpl
import scalafix.internal.patch.PatchInternals
import scalafix.internal.patch.PatchInternals.tokenPatchApply
import scalafix.v0
import scalafix.v0.RuleCtx
import scalafix.v1.Rule
import scalafix.v1.SemanticDocument
import scalafix.v1.SyntacticDocument

object MainOps {

  def run(args: Array[String], base: Args): ExitStatus = {
    val expanded = ArgExpansion.expand(args, base.cwd)
    val out = base.out
    Conf
      .parseCliArgs[Args](expanded)
      .andThen(c => c.as[Args](Args.decoder(base))) match {
      case Configured.Ok(args) =>
        if (args.help) {
          MainOps.helpMessage(out, 80)
          ExitStatus.Ok
        } else if (args.version) {
          out.println(Versions.version)
          ExitStatus.Ok
        } else if (args.bash) {
          out.println(CompletionsOps.bashCompletions)
          ExitStatus.Ok
        } else if (args.zsh) {
          out.println(CompletionsOps.zshCompletions)
          ExitStatus.Ok
        } else {
          args.validate match {
            case Configured.Ok(validated) =>
              if (validated.rules.isEmpty) {
                out.println("No rules requested to run")
                ExitStatus.NoRulesError
              } else {
                MainOps.run(validated)
              }
            case Configured.NotOk(err) =>
              PrintStreamReporter(out = out).error(err.toString())
              ExitStatus.CommandLineError
          }
        }
      case Configured.NotOk(err) =>
        PrintStreamReporter(out = out).error(err.toString())
        ExitStatus.CommandLineError
    }
  }

  def runWithResult(args: ValidatedArgs): ScalafixEvaluation = {
    // first run beforeStart of each rule
    args.rules.rules.foreach(_.beforeStart())

    args.rules.rules match {
      case Nil => {
        ScalafixEvaluationImpl(
          ExitStatus.NoRulesError,
          Some("No rules requested to run")
        )
      }
      case _: Seq[Rule] =>
        val files = getFilesFrom(args)
        if (files.nonEmpty) {
          val fileEvaluations = {
            files.map { file =>
              val input = args.input(file)
              val result = Try(getPatchesAndDiags(args, input, file))
              result match {
                case Success(result) =>
                  ScalafixFileEvaluationImpl.from(
                    file,
                    Some(result.fixed),
                    ExitStatus.Ok,
                    result.patches,
                    result.diagnostics
                  )(args, result.ruleCtx, result.semanticdbIndex)

                case Failure(exception) =>
                  ScalafixFileEvaluationImpl
                    .from(
                      file,
                      ExitStatus.from(exception),
                      exception.getMessage
                    )(
                      args
                    )
              }
            }
          }
          // Then afterComplete for each rule
          args.rules.rules.foreach(_.afterComplete())

          ScalafixEvaluationImpl.from(fileEvaluations, ExitStatus.Ok)
        } else
          ScalafixEvaluationImpl(
            ExitStatus.NoFilesError,
            Some("No files to fix")
          )

    }
  }

  def getFilesFrom(args: ValidatedArgs): Seq[AbsolutePath] =
    args.args.ls match {
      case Ls.Find =>
        val buf = Vector.newBuilder[AbsolutePath]
        val visitor = new SimpleFileVisitor[Path] {
          override def visitFile(
              file: Path,
              attrs: BasicFileAttributes
          ): FileVisitResult = {
            val path = AbsolutePath(file)
            val relpath = path.toRelative(args.sourceroot)
            if (args.matches(relpath)) {
              buf += path
            }
            FileVisitResult.CONTINUE
          }
        }

        val roots =
          if (args.args.files.isEmpty) args.sourceroot :: Nil
          else args.args.files

        roots.foreach { root =>
          if (root.isFile) {
            if (args.matches(root.toRelative(args.args.cwd))) {
              buf += root
            }
          } else if (root.isDirectory) Files.walkFileTree(root.toNIO, visitor)
          else args.config.reporter.error(s"$root is not a file")
        }
        buf.result()
    }

  final class StaleSemanticDB(val path: AbsolutePath, val diff: String)
      extends Exception(s"Stale SemanticDB\n$diff")
      with NoStackTrace

  @tailrec private final def trimStackTrace(
      e: Throwable,
      untilMethod: String
  ): Unit = {
    val relevantStackTrace =
      e.getStackTrace.takeWhile(_.getMethodName != untilMethod)
    e.setStackTrace(relevantStackTrace)
    if (e.getCause != null) {
      trimStackTrace(e.getCause, untilMethod)
    }
  }
  def handleException(ex: Throwable, out: PrintStream): Unit = {}

  def adjustExitCode(
      args: ValidatedArgs,
      code: ExitStatus,
      files: collection.Seq[AbsolutePath]
  ): ExitStatus = {
    if (args.callback.hasLintErrors) {
      ExitStatus.merge(ExitStatus.LinterError, code)
    } else if (args.callback.hasErrors && code.isOk) {
      ExitStatus.merge(ExitStatus.UnexpectedError, code)
    } else if (files.isEmpty) {
      args.config.reporter.error("No files to fix")
      ExitStatus.merge(ExitStatus.NoFilesError, code)
    } else {
      code
    }
  }

  def assertFreshSemanticDB(
      input: Input,
      file: AbsolutePath,
      fix: String,
      doc: TextDocument
  ): Unit = {
    if (input.text == fix) {
      // Fix is a no-op, ignore. This frequently happens in cross-built modules
      // where the JVM project fixes sources and the Scala.js project becomes stale.
      ()
    } else if (doc.md5.isEmpty) {
      throw new IllegalArgumentException("-P:semanticdb:md5:on is required.")
    } else {
      val inputMD5 = FingerprintOps.md5(
        StandardCharsets.UTF_8.encode(CharBuffer.wrap(input.chars))
      )
      if (inputMD5 == doc.md5) {
        () // OK!
      } else {
        val diff = if (doc.text.isEmpty) {
          DiffUtils.unifiedDiff(
            input.syntax + "-ondisk-md5-fingerprint",
            input.syntax + "-semanticdb-md5-fingerprint",
            inputMD5 :: Nil,
            doc.md5 :: Nil,
            1
          )
        } else {
          DiffUtils.unifiedDiff(
            input.syntax + "-ondisk",
            input.syntax + "-semanticdb",
            input.text.linesIterator.toList,
            doc.text.linesIterator.toList,
            3
          )
        }
        throw new StaleSemanticDB(file, diff)
      }
    }
  }

  def unsafeHandleFile(args: ValidatedArgs, file: AbsolutePath): ExitStatus = {
    val input = args.input(file)
    val result =
      getPatchesAndDiags(args, input, file)
    val messages = result.diagnostics
    val fixed = result.fixed
    if (!args.args.autoSuppressLinterErrors) {
      messages.foreach { diag =>
        args.config.reporter.lint(diag)
      }
    }
    if (args.args.check) {
      if (fixed == input.text) {
        ExitStatus.Ok
      } else {
        val diff = DiffUtils.unifiedDiff(
          file.toString(),
          "",
          input.text.linesIterator.toList,
          fixed.linesIterator.toList,
          3
        )
        args.args.out.println(diff)
        ExitStatus.TestError
      }
    } else if (args.args.stdout) {
      args.args.out.println(fixed)
      ExitStatus.Ok
    } else {
      if (fixed != input.text) {
        val toFix = args.pathReplace(file).toNIO
        Files.createDirectories(toFix.getParent)
        Files.write(toFix, fixed.getBytes(args.args.charset))
      }
      ExitStatus.Ok
    }
  }

  private def getPatchesAndDiags(
      args: ValidatedArgs,
      input: Input,
      file: AbsolutePath
  ): PatchInternals.ResultWithContext = {
    val doc = SyntacticDocument(input, args.diffDisable, args.config)
    if (args.rules.isSemantic) {
      val relpath = file.toRelative(args.sourceroot)
      val sdoc = SemanticDocument.fromPath(
        doc,
        relpath,
        args.classLoader,
        args.symtab
      )
      val result =
        args.rules.semanticPatch(sdoc, args.args.autoSuppressLinterErrors)
      assertFreshSemanticDB(
        input,
        file,
        result.fixed,
        sdoc.internal.textDocument
      )
      result
    } else {
      args.rules.syntacticPatch(doc, args.args.autoSuppressLinterErrors)
    }
  }

  def previewPatches(
      patches: Seq[v0.Patch],
      ctx: RuleCtx,
      index: Option[v0.SemanticdbIndex]
  ): Option[String] =
    Try(tokenPatchApply(ctx, index, patches)).toOption

  def applyPatches(
      args: ValidatedArgs,
      patches: Seq[v0.Patch],
      ctx: RuleCtx,
      index: Option[v0.SemanticdbIndex],
      file: AbsolutePath
  ): Try[ExitStatus] = {
    val input = args.input(file)
    for {
      fixed <- Try(tokenPatchApply(ctx, index, patches))
      if (fixed != input.text)
      toFix = args.pathReplace(file).toNIO
      _ <- Try(Files.createDirectories(toFix.getParent))
      _ <- Try(Files.write(toFix, fixed.getBytes(args.args.charset)))
    } yield ExitStatus.Ok
  }

  def applyDiff(
      args: ValidatedArgs,
      file: AbsolutePath,
      fixed: String
  ): Try[ExitStatus] = {
    val input = args.input(file)
    if (fixed != input.text) {
      val toFix = args.pathReplace(file).toNIO

      for {
        _ <- Try(Files.createDirectories(toFix.getParent))
        _ <- Try(Files.write(toFix, fixed.getBytes(args.args.charset)))
      } yield ExitStatus.Ok
    } else Success(ExitStatus.Ok)
  }

  def handleFile(args: ValidatedArgs, file: AbsolutePath): ExitStatus = {
    try {
      unsafeHandleFile(args, file)
    } catch {
      case e: ParseException =>
        args.config.reporter.error(e.shortMessage, e.pos)
        ExitStatus.ParseError
      case e: SemanticDocument.Error.MissingSemanticdb =>
        args.config.reporter.error(e.getMessage)
        ExitStatus.MissingSemanticdbError
      case e: StaleSemanticDB =>
        if (args.args.noStaleSemanticdb) ExitStatus.Ok
        else {
          args.config.reporter.error(e.getMessage)
          ExitStatus.StaleSemanticdbError
        }
      case NonFatal(e) =>
        val ex = FileException(file, e)
        trimStackTrace(ex, untilMethod = "handleFile")
        e match {
          case _: java.lang.AssertionError
              if e.getMessage() != null &&
                e.getMessage().startsWith("assertion failed:") &&
                e.getMessage().contains("reconstructed args: ") =>
          case _ =>
            ex.printStackTrace(args.args.out)
        }
        ExitStatus.UnexpectedError
    }
  }

  def run(args: ValidatedArgs): ExitStatus = {
    val files = getFilesFrom(args)
    var i = 0
    val N = files.length
    val width = N.toString.length
    var exit = ExitStatus.Ok

    args.rules.rules.foreach(_.beforeStart())

    files.foreach { file =>
      if (args.args.verbose) {
        val message = s"Processing (%${width}s/%s) %s".format(i, N, file)
        args.config.reporter.info(message)
        i += 1
      }
      val next = handleFile(args, file)
      exit = ExitStatus.merge(exit, next)
    }

    args.rules.rules.foreach(_.afterComplete())

    adjustExitCode(args, exit, files)
  }

  def version: String =
    s"Scalafix ${Versions.version}"
  def usage: String =
    """|Usage: scalafix [options] [ ...]
      |""".stripMargin
  def description: D =
    D.paragraph(
      """|Scalafix is a refactoring and linting tool.
        |Scalafix supports both syntactic and semantic linter and rewrite rules.
        |Syntactic rules can run on source code without compilation.
        |Semantic rules can run on source code that has been compiled with the
        |SemanticDB compiler plugin.
        |""".stripMargin
    )

  /** Line wrap prose while keeping markdown code fences unchanged. */
  def markdownish(text: String): D = {
    val buf = ListBuffer.empty[String]
    val paragraphs = ListBuffer.empty[D]
    var insideCodeFence = false
    def flush(): Unit = {
      if (insideCodeFence) {
        paragraphs += D.intercalate(D.line, buf.map(D.text))
      } else {
        paragraphs += D.paragraph(buf.mkString("\n"))
      }
      buf.clear()
    }
    text.linesIterator.foreach { line =>
      if (line.startsWith("```")) {
        flush()
        insideCodeFence = !insideCodeFence
      }
      buf += line
    }
    flush()
    D.intercalate(D.line, paragraphs)
  }

  def options(width: Int): String = {
    val sb = new StringBuilder()
    val settings = Settings[Args]
    val default = ConfEncoder[Args].writeObj(Args.default)
    def printOption(setting: Setting, value: Conf): Unit = {
      if (setting.annotations.exists(_.isInstanceOf[Hidden])) return
      setting.annotations.foreach {
        case section: Section =>
          sb.append("\n")
            .append(section.name)
            .append(":\n")
        case _ =>
      }
      val name = Case.camelToKebab(setting.name)
      sb.append("\n")
        .append("  --")
        .append(name)
      setting.extraNames.foreach { name =>
        if (name.length == 1) {
          sb.append(" | -")
            .append(Case.camelToKebab(name))
        }
      }
      if (!setting.isBoolean) {
        sb.append(" ")
          .append(setting.tpe)
          .append(" (default: ")
          .append(value.toString())
          .append(")")
      }
      sb.append("\n")
      setting.description.foreach { description =>
        sb.append("    ")
          .append(markdownish(description).nested(4).render(width))
          .append('\n')
      }
    }

    settings.settings.zip(default.values).foreach {
      case (setting, (_, value)) =>
        if (setting.annotations.exists(_.isInstanceOf[Inline])) {
          for {
            underlying <- setting.underlying.toList
            (field, (_, fieldDefault)) <- underlying.settings
              .zip(value.asInstanceOf[Conf.Obj].values)
          } {
            printOption(field, fieldDefault)
          }
        } else {
          printOption(setting, value)
        }
    }
    sb.toString()
  }

  def helpMessage(out: PrintStream, width: Int): Unit = {
    out.println(version)
    out.println(usage)
    out.println(description.render(width))
    out.println(options(width))
  }
  def helpMessage(width: Int): String = {
    val baos = new ByteArrayOutputStream()
    helpMessage(new PrintStream(baos), width)
    baos.toString(StandardCharsets.UTF_8.name())
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy