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

org.scalastyle.scalariform.ScalaDocChecker.scala Maven / Gradle / Ivy

// Copyright (C) 2011-2012 the original author or authors.
// See the LICENCE.txt file distributed with this work for additional
// information regarding copyright ownership.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package org.scalastyle.scalariform

import org.scalastyle.CombinedAst
import org.scalastyle.CombinedChecker
import org.scalastyle.LineError
import org.scalastyle.Lines
import org.scalastyle.ScalastyleError
import org.scalastyle.scalariform.VisitorHelper.visit

import scalariform.lexer.Tokens.CLASS

import scalariform.lexer.{NoHiddenTokens, TokenType, HiddenTokens, Token}
import scalariform.parser._

/**
 * Checks that the ScalaDoc exists for all accessible members:
 * - classes, traits, case classes and objects
 * - methods
 * - vals, vars and types
 *
 * The ScalaDoc's structure must satisfy the parameter of the constructor in case of
 * case classes and classes, or the parameter of the methods. The ScalaDoc must include
 * the type parameters. Finally, the ScalaDoc must include return description for non-Unit
 * returning methods.
 */
class ScalaDocChecker extends CombinedChecker {
  protected val errorKey: String = "scaladoc"

  val DefaultIgnoreRegex = "^$"

  val skipPrivate = true
  val skipQualifiedPrivate = false
  val skipProtected = false
  val skipQualifiedProtected = false

  override def verify(ast: CombinedAst): List[ScalastyleError] = {
    val tokens = ast.compilationUnit.tokens
    val ignoreRegex = getString("ignoreRegex", DefaultIgnoreRegex)

    def trimToTokenOfType(list: List[Token], tokenType: TokenType): List[Token] = {
      if (list.isEmpty) {
        list
      } else {
        list.head match {
          case Token(`tokenType`, _, _, _) => list
          case _ => trimToTokenOfType(list.tail, tokenType)
        }
      }
    }

    val ts = trimToTokenOfType(tokens, CLASS)
    val ignore = !ts.isEmpty && ts(1).text.matches(ignoreRegex)
    ignore match {
      case true => Nil
      case false => localVisit(skip = false, HiddenTokens(Nil), ast.lines)(ast.compilationUnit.immediateChildren(0))
    }
  }

  import ScalaDocChecker._ // scalastyle:ignore underscore.import import.grouping

  /*
   * Finds the ScalaDoc hidden in the ``token``, falling back on ``fallback`` if ``token``
   * contains no ScalaDoc.
   *
   * This is useful when including access levels, annotations and such like,
   * which are not reported as part of the following token. So,
   *
   * ```
   * /**
   *  * Contains magic
   *  */
   * @magic protected val foo = 5
   * ```
   * is interpreted as
   *
   * ``FullDefOrDcl`` -> ``PatDefOrDcl``, with the ScalaDoc attached to the ``FulDefOrDcl``, which
   * finds its way to us here in ``fallback``.
   */
  private def findScalaDoc(token: Token, fallback: HiddenTokens): Option[ScalaDoc] = {
    def toScalaDoc(ht: HiddenTokens): Option[ScalaDoc] = ht.rawTokens.find(_.isScalaDocComment).map(ScalaDoc.apply)

    toScalaDoc(token.associatedWhitespaceAndComments).orElse(toScalaDoc(fallback))
  }

  // parse the parameters and report errors for the parameters (constructor or method)
  private def paramErrors(line: Int, paramClausesOpt: Option[ParamClauses])(scalaDoc: ScalaDoc): List[ScalastyleError] = {
    def params(xs: List[Token]): List[String] = xs match {
      // @annotation a: B; @annotation(...) a: B
      case Token(_, "@", _, _)::Token(_, annotation, _, _)::
           Token(_, paramName, _, _)::Token(_, ":", _, _)::Token(_, _, _, _)::t => paramName :: params(t)
      // a: B
      case Token(_, paramName, _, _)::Token(_, ":", _, _)::Token(_, _, _, _)::t => paramName :: params(t)
      // any other token
      case _::t => params(t)
      case Nil  => Nil
    }

    val paramNames = paramClausesOpt.map(pc => params(pc.tokens)).getOrElse(Nil)

    val missingScalaDocParams = paramNames.filterNot(name => scalaDoc.params.exists(_.name == name))
    val extraScalaDocParams = scalaDoc.params.filterNot(param => paramNames.contains(param.name))
    val validScalaDocParams = scalaDoc.params.filter(param => paramNames.contains(param.name))

    missingScalaDocParams.map(missing => LineError(line, List(missingParam(missing)))) ++
    extraScalaDocParams.map(extra => LineError(line, List(extraParam(extra.name)))) ++
    validScalaDocParams.filter(_.text.isEmpty).map(empty => LineError(line, List(emptyParam(empty.name))))

//      if (!scalaDoc.params.forall(p => paramNames.exists(name => p.name == name && !p.text.isEmpty))) List(LineError(line, List(MalformedParams)))
//      else Nil
  }

  // parse the type parameters and report errors for the parameters (constructor or method)
  private def tparamErrors(line: Int, tparamClausesOpt: Option[TypeParamClause])(scalaDoc: ScalaDoc): List[ScalastyleError] = {
    def tparams(xs: List[Token]): List[String] = xs match {
      // [@foo A, @bar(b) B]
      case Token(_, "@", _, _)::Token(_, annotation, _, _)::
        Token(tokenType, paramName, _, _)::t  if tokenType.name == "VARID"   => paramName :: tparams(t)
      // [A, B]
      case Token(tokenType, paramName, _, _)::t if tokenType.name == "VARID" => paramName :: tparams(t)
      // any other token
      case _::t => tparams(t)
      case Nil  => Nil
    }

    val tparamNames = tparamClausesOpt.map(tc => tparams(tc.tokens)).getOrElse(Nil)

    if (tparamNames.size != scalaDoc.typeParams.size) {
      // bad param sizes
      List(LineError(line, List(MalformedTypeParams)))
    } else {
      if (!scalaDoc.typeParams.forall(tp => tparamNames.contains(tp.name))) List(LineError(line, List(MalformedTypeParams))) else Nil
    }
  }

  // parse the parameters and report errors for the return types
  private def returnErrors(line: Int, returnTypeOpt: Option[(Token, Type)])(scalaDoc: ScalaDoc): List[ScalastyleError] = {
    val needsReturn = returnTypeOpt.exists { case (_, tpe) => tpe.firstToken.text != "Unit" }

    if (needsReturn && !scalaDoc.returns.isDefined) {
      List(LineError(line, List(MalformedReturn)))
    } else {
      Nil
    }
  }

  /*
   * process the AST, picking up only the parts that are interesting to us, that is
   * - access modifiers
   * - classes, traits, case classes and objects
   * - methods
   * - vals, vars and types
   *
   * we do not bother descending down any further
   */
  private def localVisit(skip: Boolean, fallback: HiddenTokens, lines: Lines)(ast: Any): List[ScalastyleError] = ast match {
    case t: FullDefOrDcl      =>
      // private, private[xxx];
      // protected, protected[xxx];

      // check if we are going to include or skip depending on access modifier
      val accessModifier = t.modifiers.find {
        case AccessModifier(_, _) => true
        case _                    => false
      }
      val skip = accessModifier.exists {
        case AccessModifier(pop, Some(_)) =>
          if (pop.text == "private") skipQualifiedPrivate else skipQualifiedProtected
        case AccessModifier(pop, None) =>
          if (pop.text == "private") skipPrivate else skipProtected
        case _ =>
          false
      }

      // pick the ScalaDoc "attached" to the modifier, which actually means
      // ScalaDoc of the following member
      val scalaDocs = for {
        token    <- t.tokens
        comment  <- token.associatedWhitespaceAndComments
        if comment.token.isScalaDocComment
      } yield comment

      // descend
      visit(t, localVisit(skip, HiddenTokens(fallback.tokens ++ scalaDocs), lines))
    case t: TmplDef      =>
      // trait Foo, trait Foo[A];
      // class Foo, class Foo[A](a: A);
      // case class Foo(), case class Foo[A](a: A);
      // object Foo;
      val (_, line) = lines.findLineAndIndex(t.firstToken.offset).get

      // we are checking parameters and type parameters
      val errors = if (skip) Nil else findScalaDoc(t.firstToken, fallback).
        map { scalaDoc =>
          paramErrors(line, t.paramClausesOpt)(scalaDoc) ++
          tparamErrors(line, t.typeParamClauseOpt)(scalaDoc)
        }.getOrElse(List(LineError(line, List(Missing))))

      // and we descend, because we're interested in seeing members of the types
      errors ++ visit(t, localVisit(skip, NoHiddenTokens, lines))
    case t: FunDefOrDcl  =>
      // def foo[A, B](a: Int): B = ...
      val (_, line) = lines.findLineAndIndex(t.firstToken.offset).get

      // we are checking parameters, type parameters and returns
      val errors = if (skip) Nil else findScalaDoc(t.firstToken, fallback).
        map { scalaDoc =>
          paramErrors(line, Some(t.paramClauses))(scalaDoc) ++
          tparamErrors(line, t.typeParamClauseOpt)(scalaDoc) ++
          returnErrors(line, t.returnTypeOpt)(scalaDoc)
        }.
        getOrElse(List(LineError(line, List(Missing))))

      // we don't descend any further
      errors
    case t: TypeDefOrDcl =>
      // type Foo = ...
      val (_, line) = lines.findLineAndIndex(t.firstToken.offset).get

      // error is non-existence
      val errors = if (skip) Nil else findScalaDoc(t.firstToken, fallback).
        map(_ => Nil).
        getOrElse(List(LineError(line, List(Missing))))

      // we don't descend any further
      errors

    case t: PatDefOrDcl  =>
      // val a = ...
      // var a = ...
      val (_, line) = lines.findLineAndIndex(t.valOrVarToken.offset).get
      val errors = if (skip) Nil else findScalaDoc(t.firstToken, fallback).
        map(_ => Nil).
        getOrElse(List(LineError(line, List(Missing))))
      // we don't descend any further
      errors

    case t: StatSeq =>
      localVisit(skip, fallback, lines)(t.firstStatOpt) ++ (
        for(statOpt <- t.otherStats)
          yield localVisit(skip, statOpt._1.associatedWhitespaceAndComments, lines)(statOpt._2)
        ).flatten

    case t: Any          =>
      // anything else, we descend (unless we stopped above)
      visit(t, localVisit(skip, fallback, lines))
  }

}

/**
 * Contains the ScalaDoc model with trivial parsers
 */
object ScalaDocChecker {
  val Missing = "Missing"
  def missingParam(name: String): String = "Missing @param " + name
  def extraParam(name: String): String = "Extra @param " + name
  def emptyParam(name: String): String = "Missing text for @param " + name
  val MalformedTypeParams = "Malformed @tparams"
  val MalformedReturn = "Malformed @return"

  /**
   * Companion for the ScalaDoc object that parses its text to pick up its elements
   */
  private object ScalaDoc {
    private val ParamRegex = "@param\\W+(\\w+)\\W+(.*)".r
    private val TypeParamRegex = "@tparam\\W+(\\w+)\\W+(.*)".r
    private val ReturnRegex = "@return\\W+(.*)".r

    private val TagRegex = """\W*[*]\W+\@(\w+)\W+(\w+)(.*)""".r

    sealed trait ScalaDocLine {
      def isTag: Boolean
    }
    case class TagSclaDocLine(tag: String, ref: String, rest: String) extends ScalaDocLine {
      def isTag: Boolean = true
    }
    case class RawScalaDocLine(text: String) extends ScalaDocLine {
      def isTag: Boolean = false
      override val toString = text.replaceFirst("\\*\\W+", "")
    }

    /**
     * Take the ``raw`` and parse an instance of ``ScalaDoc``
     * @param raw the token containing the ScalaDoc
     * @return the parsed instance
     */
    def apply(raw: Token): ScalaDoc = {
      val lines = raw.rawText.split("\\n").toList.flatMap(x => x.trim match {
        case TagRegex(tag, ref, rest) => Some(TagSclaDocLine(tag, ref, rest))
        case "/**"                    => None
        case "*/"                     => None
        case text: Any                => Some(RawScalaDocLine(text))
      })

      def combineScalaDocFor[A](lines: List[ScalaDocLine], tag: String, f: (String, String) => A): List[A] = lines match {
        case TagSclaDocLine(`tag`, ref, text)::ls =>
          val rawLines = ls.takeWhile(!_.isTag)
          f(ref, text + rawLines.mkString(" ")) :: combineScalaDocFor(ls.drop(rawLines.length), tag, f)
        case _::ls => combineScalaDocFor(ls, tag, f)
        case Nil => Nil
      }

      val params = combineScalaDocFor(lines, "param", ScalaDocParameter)
      val typeParams = combineScalaDocFor(lines, "tparam", ScalaDocParameter)
      val returns = combineScalaDocFor(lines, "return", _ + _).headOption

      ScalaDoc(raw.rawText, params, typeParams, returns, None)
    }
  }

  /**
   * Models a parameter: either plain or type
   * @param name the parameter name
   * @param text the parameter text
   */
  private case class ScalaDocParameter(name: String, text: String)

  /**
   * Models the parsed ScalaDoc
   * @param text arbitrary text
   * @param params the parameters
   * @param typeParams the type parameters
   * @param returns the returns clause, if present
   * @param throws the throws clause, if present
   */
  private case class ScalaDoc(text: String, params: List[ScalaDocParameter], typeParams: List[ScalaDocParameter],
                      returns: Option[String], throws: Option[String])
}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy