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

com.nawforce.apexlink.analysis.OrgAnalysis.scala Maven / Gradle / Ivy

/*
 * Copyright (c) 2022 FinancialForce.com, inc. All rights reserved.
 */
package com.nawforce.apexlink.analysis

import com.nawforce.apexlink.api.{LoadAndRefreshAnalysis, NoAnalysis, ServerOps}
import com.nawforce.apexlink.org.OPM.OrgImpl
import com.nawforce.pkgforce.diagnostics._
import com.nawforce.pkgforce.documents.ApexNature
import com.nawforce.pkgforce.path.Location
import com.nawforce.runtime.platform.Path
import io.github.apexdevtools.api.{Issue => APIIssue}
import io.github.apexdevtools.spi.AnalysisProvider

import java.util.ServiceLoader
import scala.collection.immutable.ArraySeq
import scala.collection.mutable
import scala.jdk.CollectionConverters._
import scala.util.{Success, Try, Failure}

/** Service to invoke custom analysis providers that can augment normal diagnostics.
  * @param org run analysis against for this org
  */
class OrgAnalysis(org: OrgImpl) {
  private val analysisProviders = ServiceLoader
    .load(classOf[AnalysisProvider])
    .iterator()
    .asScala
    .flatMap(provider => configureProvider(provider))
    .toList

  /** Apply custom parameters to a provider.
    * @param provider apply to this provider
    * @return the provider or None if an error occurred
    */
  private def configureProvider(provider: AnalysisProvider): Option[AnalysisProvider] = {
    Option.when(
      ServerOps.getExternalAnalysis.params
        .getOrElse(provider.getProviderId, Nil)
        .forall(param => {
          Try(provider.setConfiguration(param._1, param._2.asJava)) match {
            case Success(_) => true
            case Failure(ex) =>
              LoggerOps.info(
                s"Analysis provider '${provider.getProviderId} threw when setting parameter ${param._1}",
                ex
              )
              false
          }
        })
    ) { provider }
  }

  /** Invoke the providers after the org has been loaded.
    * Passed all Apex classes for analysis.
    */
  def afterLoad(): Unit = {
    if (ServerOps.getExternalAnalysis.mode != LoadAndRefreshAnalysis)
      return

    val workspaceProviders = analysisProviders.filter(_.isConfigured(Path(org.path).native))
    if (workspaceProviders.isEmpty)
      return

    // Collect Apex class files over all modules
    val files  = mutable.Set[Path]()
    var module = org.packages.headOption.flatMap(_.firstModule)
    while (module.nonEmpty) {
      module.get.index
        .getControllingDocuments(ApexNature)
        .map(_.path)
        .collect { case p: Path => p }
        .foreach(files.add)
      module = module.get.nextModule
    }
    runAnalysis(workspaceProviders, files.toSet)
  }

  /** Invoke the providers after some files have been changed.
    * @param paths the files (assumed to be Apex classes) that changed
    */
  def afterRefresh(paths: Set[Path]): Unit = {
    if (ServerOps.getExternalAnalysis.mode == NoAnalysis)
      return

    val workspaceProviders = analysisProviders.filter(_.isConfigured(Path(org.path).native))
    if (workspaceProviders.isEmpty)
      return

    runAnalysis(workspaceProviders, paths)
  }

  private def runAnalysis(providers: List[AnalysisProvider], files: Set[Path]): Unit = {
    val issueManager = org.issues
    val syntaxGroups = files.groupBy(file => issueManager.hasSyntaxIssues(file))
    // Clear provider issues for files that already have syntax errors to reduce noise
    syntaxGroups
      .getOrElse(true, Set())
      .foreach(path => org.issues.clearProviderIssues(path))

    providers
      .foreach(provider => {
        val providerId = provider.getProviderId

        // Collect and replace for other files
        val issuesByFile =
          ArraySeq
            .unsafeWrapArray(
              provider
                .collectIssues(
                  Path(org.path).native,
                  syntaxGroups.getOrElse(false, Set()).map(_.native).toArray
                )
            )
            .groupBy(issue => Path(issue.filePath))

        issuesByFile.foreach(kv =>
          org.issues.replaceProviderIssues(providerId, kv._1, kv._2.map(toIssue(providerId, _)))
        )
      })
  }

  private def toIssue(providerId: String, issue: APIIssue): Issue = {
    new Issue(
      Path(issue.filePath()),
      new Diagnostic(
        if (issue.isError) ERROR_CATEGORY else WARNING_CATEGORY,
        new Location(
          issue.fileLocation().startLineNumber(),
          issue.fileLocation().startCharOffset(),
          issue.fileLocation().endLineNumber(),
          issue.fileLocation().endCharOffset()
        ),
        issue.message()
      ),
      providerId
    )
  }
}

object OrgAnalysis {
  def afterLoad(org: OrgImpl): Unit = {
    val analysis = new OrgAnalysis(org)
    analysis.afterLoad()
  }

  def afterRefresh(org: OrgImpl, files: Set[Path]): Unit = {
    val analysis = new OrgAnalysis(org)
    analysis.afterRefresh(files)
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy