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

fix.UseNamedParameters.scala Maven / Gradle / Ivy

The newest version!
package fix

import metaconfig.Configured
import scalafix.v1._

import scala.annotation.tailrec
import scala.meta._

final class UseNamedParameters(config: UseNamedParametersConfig)
    extends SemanticRule(classOf[UseNamedParameters].getSimpleName) {
  def this() = this(UseNamedParametersConfig.default)

  private val singleAlphabetPattern = "^[A-Za-z]\\d*$".r

  override def withConfiguration(config: Configuration): Configured[Rule] = {
    val requiredScalacOption = "-P:semanticdb:synthetics:on"
    if (config.scalacOptions.contains(requiredScalacOption)) {
      config.conf
        .getOrElse(this.getClass.getSimpleName)(this.config)
        .map(newConfig => new UseNamedParameters(newConfig))
    } else {
      Configured.error(
        s"""This rule requires SemanticDB synthetics to work properly (e.g. to detect case class apply).
          |Please add "$requiredScalacOption" to scala compiler options (e.g. scalacOptions in SBT).""".stripMargin
      )
    }
  }

  override def fix(implicit doc: SemanticDocument): Patch = {
    doc.tree
      .collect {
        case Init(_, name, argss) if !hasPlaceholder(argss.flatten) =>
          resolveScalaMethodSignatureFromSymbol(name.symbol) match {
            case Some(methodSig) =>
              val patchGens: List[(Term, Int) => Patch] =
                methodSig.parameterLists.zipWithIndex.map { case (_, idx) =>
                  mkPatchGenForArgList(config, methodSig, idx)
                }
              argss
                .zip(patchGens)
                .flatMap { case (argsInBlock, patchGen) =>
                  argsInBlock.zipWithIndex.map { case (t, idx) => patchGen(t, idx) }
                }.asPatch.atomic
            case None => Patch.empty
          }
        case Term.Apply(fun, args) if !hasPlaceholder(args) =>
          resolveFunctionTerm(fun) match {
            case Some(fname) =>
              val methodSignatureOpt =
                resolveScalaMethodSignatureFromSymbol(fname.symbol).orElse(resolveFromSynthetics(fname))
              (methodSignatureOpt match {
                case Some(methodSig)
                    if methodSig.parameterLists.nonEmpty => // parameterLists.nonEmpty filters out FunctionX types
                  val patchGen: (Term, Int) => Patch =
                    mkPatchGenForArgList(config, methodSig, determineParamBlockIndex(fname))
                  args.zipWithIndex.map { case (t, idx) => patchGen(t, idx) }
                case _ => List.empty
              }).asPatch.atomic
            case _ => Patch.empty
          }
      }
      .asPatch
  }

  private def hasPlaceholder(argTerms: List[Term]): Boolean =
    argTerms.collect { case Term.Placeholder() => true }.exists(x => x)

  private def resolveFunctionTerm(term: Term): Option[Term] =
    term match {
      case fname: Term.Name => Some(fname)
      case fname: Term.Apply =>
        // For curried functions, return the Term as is as we need
        // it to figure out which param block we're currently handling
        Some(fname)
      case Term.ApplyType(fname, _) => Some(fname)
      case s: Term.Select => Some(s.name)
      case _ => None
    }

  private def mkPatchGenForArgList(
    config: UseNamedParametersConfig,
    methodSig: MethodSignature,
    paramBlockIdx: Int
  )(implicit doc: SemanticDocument): (Term, Int) => Patch = {
    // If the result of applying the method is a function, it will be IndexOutOfBounds
    if (methodSig.parameterLists.size > paramBlockIdx) {
      val thisParamBlock = methodSig.parameterLists(paramBlockIdx)
      // Whether to apply named param patching is dependent on parameters
      // in the method definition, not use site.
      if (thisParamBlock.length < config.minParams) { (_, _) =>
        Patch.empty
      } else { (term: Term, idx: Int) =>
        {
          term match {
            case _: Term.Assign => Patch.empty // Already using named param, no patch needed
            case _: Term.Block => Patch.empty // map { _ => _ }
            case _: Term.PartialFunction => Patch.empty // map { case _ => _ }
            case t =>
              // Term.Name will escape any weird identifiers
              thisParamBlock.lift(idx) match {
                case Some(symInfo)
                    // In the case of repeated parameters, if the parameter name is given only at the beginning, it is broken.
                    if !symInfo.signature.toString().startsWith("* Tuple") =>
                  val paramName = Term.Name(symInfo.displayName).toString
                  if (config.skipSingleAlphabet && singleAlphabetPattern.findFirstIn(paramName).nonEmpty) {
                    Patch.empty
                  } else {
                    Patch.addLeft(t, s"$paramName = ")
                  }
                case _ => // Var args
                  Patch.empty
              }
          }
        }
      }
    } else { (_, _) =>
      Patch.empty
    }
  }

  private def resolveScalaMethodSignatureFromSymbol(
    funcSymbol: Symbol
  )(implicit doc: SemanticDocument): Option[MethodSignature] =
    funcSymbol.info.flatMap { symInfo =>
      if (symInfo.isScala)
        symInfo.signature match {
          case m: MethodSignature => Some(m)
          case _ => None
        }
      else None
    }

  // To resolve companion object .apply methods
  private def resolveFromSynthetics(funcTerm: Term)(implicit doc: SemanticDocument): Option[MethodSignature] = {
    funcTerm.synthetics
      .flatMap(_.symbol)
      .flatMap(_.info)
      .map(_.signature)
      .collectFirst { case m: MethodSignature =>
        m
      }
  }

  @tailrec
  private def determineParamBlockIndex(curFuncTerm: Term, curIndex: Int = 0): Int =
    curFuncTerm match {
      case Term.Apply(innerFuncTerm, _) => determineParamBlockIndex(innerFuncTerm, curIndex + 1)
      case _ => curIndex
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy