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

io.github.apexdevtools.apexls.CheckForIssues.scala Maven / Gradle / Ivy

/*
 Copyright (c) 2020 Kevin Jones, All rights reserved.
 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions
 are met:
 1. Redistributions of source code must retain the above copyright
    notice, this list of conditions and the following disclaimer.
 2. Redistributions in binary form must reproduce the above copyright
    notice, this list of conditions and the following disclaimer in the
    documentation and/or other materials provided with the distribution.
 3. The name of the author may not be used to endorse or promote products
    derived from this software without specific prior written permission.
 */

package io.github.apexdevtools.apexls

import com.nawforce.apexlink.api._
import com.nawforce.apexlink.rpc.OpenOptions
import com.nawforce.runtime.platform.Path
import io.github.apexdevtools.api.IssueLocation
import mainargs.{Flag, ParserForMethods, TokensReader, arg, main}

import java.time.Instant
import scala.annotation.unused
import scala.collection.immutable.ArraySeq
import scala.collection.mutable

/** Command line for running project analysis.
  *
  * Defaults to reporting issue but can also be used to report dependency information.
  */
object CheckForIssues {
  private final val STATUS_OK: Int        = 0
  private final val STATUS_ARGS: Int      = 1
  private final val STATUS_EXCEPTION: Int = 3
  private final val STATUS_ISSUES: Int    = 4

  case class Param(providerId: String, name: String, values: Option[List[String]])

  private object Param {
    def toMap(params: Seq[Param]): Map[String, List[(String, List[String])]] = {
      val collected = mutable.Map[String, List[(String, List[String])]]()
      params.foreach(param => {
        val providerParams = collected.getOrElse(param.providerId, Nil)
        collected.put(param.providerId, (param.name, param.values.getOrElse(Nil)) :: providerParams)
      })
      collected.toMap
    }
  }

  implicit object ParamRead extends TokensReader.Simple[Param] {
    def shortName = "param"

    def read(text: Seq[String]): Either[String, Param] = {
      val parts     = text.head.split("=", 2)
      val value     = Option.when(parts.length == 2) { parts(1) }
      val headParts = parts.head.split(":", 2)
      if (headParts.length != 2) {
        Left(
          s"Expecting params to have format :[=[,...], not '$text"
        )
      } else {
        Right(
          Param(headParts.head.trim, headParts(1).trim, value.map(_.split(",").map(_.trim).toList))
        )
      }
    }
  }

  @unused
  @main(name = "io.github.apexdevtools.apexls.CheckForIssues")
  def mainWithArgs(
    @arg(short = 'f', doc = "Output format text (default), json or pmd")
    format: String = "text",
    @arg(short = 'l', doc = "Text output logging level, none (default), info or debug")
    logging: String = "none",
    @arg(short = 'd', doc = "Detail level, errors (default), warnings, unused")
    detail: String = "errors",
    @arg(short = 'n', doc = "Disable cache use")
    nocache: Flag,
    @arg(
      short = 'p',
      doc = "Analysis provider param in format :[=[,...]]"
    )
    param: Seq[Param],
    @arg(short = 'w', doc = "Workspace directory path, defaults to current directory")
    workspace: String = "",
    @arg(short = 'c', doc = "Cache directory path, defaults to env or home dir")
    cacheDir: String = ""
  ): Unit = {
    System.exit(run(format, logging, detail, nocache.value, param, workspace, cacheDir))
  }

  def main(args: Array[String]): Unit = {
    ParserForMethods(this).runOrExit(ArraySeq.unsafeWrapArray(args))
  }

  def run(
    format: String,
    logging: String,
    detail: String,
    nocache: Boolean,
    params: Seq[Param],
    directory: String,
    cacheDirectory: String
  ): Int = {
    try {
      val workspace = Path(directory)
      val outputFormat = format match {
        case "text" | "json" | "pmd" => format
        case _ =>
          System.err.println(
            s"Unknown output format provided '$format', should be 'text', 'json' or 'pmd'"
          )
          return STATUS_ARGS
      }

      val loggingLevel =
        if (outputFormat != "text")
          "none"
        else
          logging match {
            case "none" | "info" | "debug" => logging
            case _ =>
              System.err.println(
                s"Unknown logging level provided '$logging', should be 'none', 'info' or 'debug'"
              )
              return STATUS_ARGS
          }

      val detailLevel = detail match {
        case "errors" | "warnings" | "unused" => detail
        case _ =>
          System.err.println(
            s"Unknown detail level provided '$detail', should be 'errors', 'warnings' or 'unused'"
          )
          return STATUS_ARGS
      }

      val options = OpenOptions
        .default()
        .withParser("OutlineSingle")
        .withAutoFlush(enabled = false)
        .withExternalAnalysisMode(LoadAndRefreshAnalysis.shortName, Param.toMap(params))
        .withLoggingLevel(loggingLevel)
        .withCache(!nocache)
        .withCacheDirectory(cacheDirectory)
        .withUnused(detailLevel == "unused")

      // Load org and flush to cache if we are using it
      val org = Org.newOrg(Path(workspace), options)
      if (!nocache) {
        org.flush()
      }

      // Output issues
      val includeWarnings = detailLevel == "warnings" || detailLevel == "unused"
      if (outputFormat == "pmd") {
        writeIssuesPMD(org, includeWarnings)
      } else {
        writeIssues(org, outputFormat == "json", includeWarnings)
      }

    } catch {
      case ex: Throwable =>
        ex.printStackTrace(System.err)
        STATUS_EXCEPTION
    }
  }

  private def writeIssues(org: Org, asJSON: Boolean, includeWarnings: Boolean): Int = {

    val issues = org.issues.issuesForFiles(null, includeWarnings, 0)
    val writer = if (asJSON) new JSONMessageWriter() else new TextMessageWriter()
    writer.startOutput()
    var hasErrors = false
    var lastPath  = ""

    issues.foreach(issue => {
      hasErrors |= issue.isError()
      if (includeWarnings || issue.isError) {

        if (issue.filePath() != lastPath) {
          if (lastPath.nonEmpty)
            writer.endDocument()
          lastPath = issue.filePath()
          writer.startDocument(lastPath)
        }

        writer.writeMessage(issue.rule().name(), issue.fileLocation(), issue.message)

      }
    })
    if (lastPath.nonEmpty)
      writer.endDocument()

    print(writer.output)
    if (hasErrors) STATUS_ISSUES else STATUS_OK
  }

  private trait MessageWriter {
    def startOutput(): Unit

    def startDocument(path: String): Unit

    def writeMessage(category: String, location: IssueLocation, message: String): Unit

    def endDocument(): Unit

    def output: String
  }

  private class TextMessageWriter(showPath: Boolean = true) extends MessageWriter {
    private val buffer = new mutable.StringBuilder()

    override def startOutput(): Unit = buffer.clear()

    override def startDocument(path: String): Unit = if (showPath) buffer ++= path + '\n'

    override def writeMessage(category: String, location: IssueLocation, message: String): Unit =
      buffer ++= s"$category: ${location.displayPosition}: $message\n"

    override def endDocument(): Unit = {}

    override def output: String = buffer.toString()
  }

  private class JSONMessageWriter extends MessageWriter {
    private val buffer                 = new mutable.StringBuilder()
    private var firstDocument: Boolean = _
    private var firstMessage: Boolean  = _

    override def startOutput(): Unit = {
      buffer.clear()
      buffer ++= s"""{ "files": [\n"""
      firstDocument = true
    }

    override def startDocument(path: String): Unit = {
      buffer ++= (if (firstDocument) "" else ",\n")
      buffer ++= s"""{ "path": "${JSON.encode(path)}", "messages": [\n"""
      firstDocument = false
      firstMessage = true
    }

    override def writeMessage(category: String, location: IssueLocation, message: String): Unit = {
      buffer ++= (if (firstMessage) "" else ",\n")
      buffer ++= s"""{${locationAsJSON(location)}, "category": "$category", "message": "${JSON
          .encode(message)}"}"""
      firstMessage = false
    }

    override def endDocument(): Unit = buffer ++= "\n]}"

    override def output: String = {
      buffer ++= "]}\n"
      buffer.toString()
    }

    private def locationAsJSON(location: IssueLocation): String =
      s""""start": {"line": ${location.startLineNumber()}, "offset": ${location
          .startCharOffset()} }, "end": {"line": ${location.endLineNumber()}, "offset": ${location
          .endCharOffset()} }"""
  }

  private def writeIssuesPMD(org: Org, includeWarnings: Boolean): Int = {

    val issues       = org.issues.issuesForFiles(null, includeWarnings, 0)
    val issuesByFile = issues.groupBy(_.filePath())
    val files = issuesByFile.map(kv => {
      val path   = kv._1
      val issues = kv._2

      val violations = issues.map(issue => {
        
          {issue.message()}
        
      })
      
        {violations}
      
    })

    val timestamp = Instant.now().toString
    val pmd = 
      {files}
    

    val printer = new scala.xml.PrettyPrinter(80, 2)
    println(printer.format(pmd))
    STATUS_OK
  }

  private object JSON {
    def encode(value: String): String = {
      val buf = new mutable.StringBuilder()
      value.foreach {
        case '"'                 => buf.append("\\\"")
        case '\\'                => buf.append("\\\\")
        case '\b'                => buf.append("\\b")
        case '\f'                => buf.append("\\f")
        case '\n'                => buf.append("\\n")
        case '\r'                => buf.append("\\r")
        case '\t'                => buf.append("\\t")
        case char if char < 0x20 => buf.append("\\u%04x".format(char: Int))
        case char                => buf.append(char)
      }
      buf.mkString
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy