Skip to content

Scaladoc: Add generic path-based arguments implementation #11804

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions scaladoc/src/dotty/tools/scaladoc/DocContext.scala
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,7 @@ extension (r: report.type)
case class NavigationNode(name: String, dri: DRI, nested: Seq[NavigationNode])

case class DocContext(args: Scaladoc.Args, compilerContext: CompilerContext):
lazy val sourceLinks: SourceLinks = SourceLinks.load(using this)

lazy val sourceLinks = SourceLinks.load(args.sourceLinks, args.revision)(using compilerContext)
lazy val staticSiteContext = args.docsRoot.map(path => StaticSiteContext(
File(path).getAbsoluteFile(),
args,
Expand Down
39 changes: 39 additions & 0 deletions scaladoc/src/dotty/tools/scaladoc/PathBased.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package dotty.tools.scaladoc

import java.nio.file.{Path, Paths}

case class PathBased[T](entries: List[PathBased.Entry[T]], projectRoot: Path):
def get(path: Path): Option[PathBased.Result[T]] =
if path.isAbsolute then
if path.startsWith(projectRoot) then get(projectRoot.relativize(path))
else None
else entries.find(_.path.forall(p => path.startsWith(p))).map(entry =>
PathBased.Result(entry.path.fold(path)(_.relativize(path)), entry.elem)
)

trait ArgParser[T]:
def parse(s: String): Either[String, T]

object PathBased:
case class Entry[T](path: Option[Path], elem: T)
case class ParsingResult[T](errors: List[String], result: PathBased[T])
case class Result[T](path: Path, elem: T)

private val PathExtractor = "([^=]+)=(.+)".r


def parse[T](args: Seq[String], projectRoot: Path = Paths.get("").toAbsolutePath())(using parser: ArgParser[T]): ParsingResult[T] = {
val parsed = args.map {
case PathExtractor(path, arg) => parser.parse(arg).map(elem => Entry(Some(Paths.get(path)), elem))
case arg => parser.parse(arg).map(elem => Entry(None, elem))
}
val errors = parsed.collect {
case Left(error) => error
}.toList

val entries = parsed.collect {
case Right(entry) => entry
}.toList

ParsingResult(errors, PathBased(entries, projectRoot))
}
84 changes: 23 additions & 61 deletions scaladoc/src/dotty/tools/scaladoc/SourceLinks.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,6 @@ trait SourceLink:
val path: Option[Path] = None
def render(memberName: String, path: Path, operation: String, line: Option[Int]): String

case class PrefixedSourceLink(val myPath: Path, nested: SourceLink) extends SourceLink:
val myPrefix = pathToString(myPath)
override val path = Some(myPath)
override def render(memberName: String, path: Path, operation: String, line: Option[Int]): String =
nested.render(memberName, myPath.relativize(path), operation, line)


case class TemplateSourceLink(val urlTemplate: String) extends SourceLink:
override val path: Option[Path] = None
override def render(memberName: String, path: Path, operation: String, line: Option[Int]): String =
Expand Down Expand Up @@ -48,8 +41,7 @@ case class WebBasedSourceLink(prefix: String, revision: String, subPath: String)
val linePart = line.fold("")(l => s"#L$l")
s"$prefix/$action/$revision$subPath/${pathToString(path)}$linePart"

object SourceLink:
val SubPath = "([^=]+)=(.+)".r
class SourceLinkParser(revision: Option[String]) extends ArgParser[SourceLink]:
val KnownProvider = raw"(\w+):\/\/([^\/#]+)\/([^\/#]+)(\/[^\/#]+)?(#.+)?".r
val BrokenKnownProvider = raw"(\w+):\/\/.+".r
val ScalaDocPatten = raw"€\{(TPL_NAME|TPL_OWNER|FILE_PATH|FILE_EXT|FILE_LINE|FILE_PATH_EXT)\}".r
Expand All @@ -68,9 +60,8 @@ object SourceLink:

private def parseLinkDefinition(s: String): Option[SourceLink] = ???

def parse(string: String, revision: Option[String]): Either[String, SourceLink] =

string match
def parse(string: String): Either[String, SourceLink] =
val res = string match
case KnownProvider(name, organization, repo, rawRevision, rawSubPath) =>
val subPath = Option(rawSubPath).fold("")("/" + _.drop(1))
val pathRev = Option(rawRevision).map(_.drop(1)).orElse(revision)
Expand All @@ -87,14 +78,6 @@ object SourceLink:
WebBasedSourceLink(gitlabPrefix(organization, repo), rev, subPath))
case other =>
Left(s"'$other' is not a known provider, please provide full source path template.")

case SubPath(prefix, config) =>
parse(config, revision) match
case l: Left[String, _] => l
case Right(_:PrefixedSourceLink) =>
Left(s"Source path $string has duplicated subpath setting (scm template can not contains '=')")
case Right(nested) =>
Right(PrefixedSourceLink(Paths.get(prefix), nested))
case BrokenKnownProvider("gitlab" | "github") =>
Left(s"Does not match known provider syntax: `<name>://organization/repository`")
case scaladocSetting if ScalaDocPatten.findFirstIn(scaladocSetting).nonEmpty =>
Expand All @@ -104,28 +87,23 @@ object SourceLink:
else Right(TemplateSourceLink(supported.foldLeft(string)((template, pattern) =>
template.replace(pattern, SupportedScalaDocPatternReplacements(pattern)))))
case other =>
Right(TemplateSourceLink(""))
Left("Does not match any implemented source link syntax")
res match {
case Left(error) => Left(s"'$string': $error")
case other => other
}


type Operation = "view" | "edit"

case class SourceLinks(links: Seq[SourceLink], projectRoot: Path):
class SourceLinks(val sourceLinks: PathBased[SourceLink]):
def pathTo(rawPath: Path, memberName: String = "", line: Option[Int] = None, operation: Operation = "view"): Option[String] =
def resolveRelativePath(path: Path) =
links
.find(_.path.forall(p => path.startsWith(p)))
.map(_.render(memberName, path, operation, line))

if rawPath.isAbsolute then
if rawPath.startsWith(projectRoot) then resolveRelativePath(projectRoot.relativize(rawPath))
else None
else resolveRelativePath(rawPath)
sourceLinks.get(rawPath).map(res => res.elem.render(memberName, res.path, operation, line))

def pathTo(member: Member): Option[String] =
member.sources.flatMap(s => pathTo(s.path, member.name, Option(s.lineNumber).map(_ + 1)))

object SourceLinks:

val usage =
"""Source links provide a mapping between file in documentation and code repository.
|
Expand All @@ -150,32 +128,16 @@ object SourceLinks:
|Template can defined only by subset of sources defined by path prefix represented by `<sub-path>`.
|In such case paths used in templates will be relativized against `<sub-path>`""".stripMargin

def load(
configs: Seq[String],
revision: Option[String],
projectRoot: Path)(
using Context): SourceLinks =
val mappings = configs.map(str => str -> SourceLink.parse(str, revision))

val errors = mappings.collect {
case (template, Left(message)) =>
s"'$template': $message"
}.mkString("\n")

if errors.nonEmpty then report.warning(
s"""Following templates has invalid format:
|$errors
|
|$usage
|""".stripMargin
)

SourceLinks(mappings.collect {case (_, Right(link)) => link}, projectRoot)

def load(using ctx: DocContext): SourceLinks =
load(
ctx.args.sourceLinks,
ctx.args.revision,
// TODO (https://github.com/lampepfl/scaladoc/issues/240): configure source root
Paths.get("").toAbsolutePath
)
def load(config: Seq[String], revision: Option[String], projectRoot: Path = Paths.get("").toAbsolutePath)(using CompilerContext): SourceLinks =
PathBased.parse(config, projectRoot)(using SourceLinkParser(revision)) match {
case PathBased.ParsingResult(errors, sourceLinks) =>
if errors.nonEmpty then report.warning(
s"""Following templates has invalid format:
|$errors
|
|${SourceLinks.usage}
|""".stripMargin
)
SourceLinks(sourceLinks)
}

17 changes: 8 additions & 9 deletions scaladoc/test/dotty/tools/scaladoc/SourceLinksTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,22 @@ class SourceLinkTest:
@Test
def testBasicFailures() =
def testFailure(template: String, messagePart: String) =
val res = SourceLink.parse(template, None)
val res = SourceLinkParser(None).parse(template)
assertTrue(s"Expected failure containing $messagePart: $res", res.left.exists(_.contains(messagePart)))

val resWithVersion = SourceLink.parse(template, Some("develop"))
val resWithVersion = SourceLinkParser(Some("develop")).parse(template)
assertEquals(res, resWithVersion)

testFailure("ala://ma/kota", "known provider")
testFailure("ala=ala=ala://ma/kota", "known provider")
testFailure("ala=ala=ala", "subpath")
testFailure("ala=ala=ala://ma/kota", "source link syntax")
testFailure("ala=ala=ala", "source link syntax")
testFailure("""€{TPL_OWNER}""", "scaladoc")


@Test
def testProperTemplates() =
def test(template: String) =
val res = try SourceLink.parse(template, Some("develop")) catch
val res = try SourceLinkParser(Some("develop")).parse(template) catch
case e: Exception => throw RuntimeException(s"When testing $template", e)
assertTrue(s"Bad template: $template", res.isRight)

Expand All @@ -35,19 +35,18 @@ class SourceLinkTest:
"https://github.com/scala/scala/blob/2.13.x€{FILE_PATH_EXT}#€{FILE_LINE}"
).foreach{ template =>
test(template)
test(s"docs/dotty=$template")
}


@Test
def testSourceProviderWithoutRevision() =
Seq("github", "gitlab").foreach { provider =>
val template = s"$provider://ala/ma"
val res = SourceLink.parse(template, None)
val res = SourceLinkParser(None).parse(template)
assertTrue(s"Expected failure containing missing revision: $res", res.left.exists(_.contains("revision")))

Seq(s"$provider://ala/ma/", s"$provider://ala", s"$provider://ala/ma/develop/on/master").foreach { template =>
val res = SourceLink.parse(template, Some("develop"))
val res = SourceLinkParser(Some("develop")).parse(template)
assertTrue(s"Expected failure syntax info: $res", res.left.exists(_.contains("syntax")))
}

Expand All @@ -62,7 +61,7 @@ class SourceLinksTest:
type Args = String | (String, Operation) | (String, Int) | (String, Int, Operation)

private def testLink(config: Seq[String], revision: Option[String])(cases: (Args, String | None.type)*): Unit =
val links = SourceLinks.load(config, revision, projectRoot)(using testContext)
val links = SourceLinks.load(config, revision)(using testContext)
cases.foreach { case (args, expected) =>
val res = args match
case path: String => links.pathTo(projectRoot.resolve(path))
Expand Down