Skip to content

Commit 2e05d9e

Browse files
authored
Merge pull request #11804 from lampepfl/scaladoc/generic-path-based
Scaladoc: Add generic path-based arguments implementation
2 parents 6de2883 + 0cc6215 commit 2e05d9e

File tree

4 files changed

+71
-72
lines changed

4 files changed

+71
-72
lines changed

scaladoc/src/dotty/tools/scaladoc/DocContext.scala

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,7 @@ extension (r: report.type)
6969
case class NavigationNode(name: String, dri: DRI, nested: Seq[NavigationNode])
7070

7171
case class DocContext(args: Scaladoc.Args, compilerContext: CompilerContext):
72-
lazy val sourceLinks: SourceLinks = SourceLinks.load(using this)
73-
72+
lazy val sourceLinks = SourceLinks.load(args.sourceLinks, args.revision)(using compilerContext)
7473
lazy val staticSiteContext = args.docsRoot.map(path => StaticSiteContext(
7574
File(path).getAbsoluteFile(),
7675
args,
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package dotty.tools.scaladoc
2+
3+
import java.nio.file.{Path, Paths}
4+
5+
case class PathBased[T](entries: List[PathBased.Entry[T]], projectRoot: Path):
6+
def get(path: Path): Option[PathBased.Result[T]] =
7+
if path.isAbsolute then
8+
if path.startsWith(projectRoot) then get(projectRoot.relativize(path))
9+
else None
10+
else entries.find(_.path.forall(p => path.startsWith(p))).map(entry =>
11+
PathBased.Result(entry.path.fold(path)(_.relativize(path)), entry.elem)
12+
)
13+
14+
trait ArgParser[T]:
15+
def parse(s: String): Either[String, T]
16+
17+
object PathBased:
18+
case class Entry[T](path: Option[Path], elem: T)
19+
case class ParsingResult[T](errors: List[String], result: PathBased[T])
20+
case class Result[T](path: Path, elem: T)
21+
22+
private val PathExtractor = "([^=]+)=(.+)".r
23+
24+
25+
def parse[T](args: Seq[String], projectRoot: Path = Paths.get("").toAbsolutePath())(using parser: ArgParser[T]): ParsingResult[T] = {
26+
val parsed = args.map {
27+
case PathExtractor(path, arg) => parser.parse(arg).map(elem => Entry(Some(Paths.get(path)), elem))
28+
case arg => parser.parse(arg).map(elem => Entry(None, elem))
29+
}
30+
val errors = parsed.collect {
31+
case Left(error) => error
32+
}.toList
33+
34+
val entries = parsed.collect {
35+
case Right(entry) => entry
36+
}.toList
37+
38+
ParsingResult(errors, PathBased(entries, projectRoot))
39+
}

scaladoc/src/dotty/tools/scaladoc/SourceLinks.scala

Lines changed: 23 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,6 @@ trait SourceLink:
1414
val path: Option[Path] = None
1515
def render(memberName: String, path: Path, operation: String, line: Option[Int]): String
1616

17-
case class PrefixedSourceLink(val myPath: Path, nested: SourceLink) extends SourceLink:
18-
val myPrefix = pathToString(myPath)
19-
override val path = Some(myPath)
20-
override def render(memberName: String, path: Path, operation: String, line: Option[Int]): String =
21-
nested.render(memberName, myPath.relativize(path), operation, line)
22-
23-
2417
case class TemplateSourceLink(val urlTemplate: String) extends SourceLink:
2518
override val path: Option[Path] = None
2619
override def render(memberName: String, path: Path, operation: String, line: Option[Int]): String =
@@ -48,8 +41,7 @@ case class WebBasedSourceLink(prefix: String, revision: String, subPath: String)
4841
val linePart = line.fold("")(l => s"#L$l")
4942
s"$prefix/$action/$revision$subPath/${pathToString(path)}$linePart"
5043

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

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

71-
def parse(string: String, revision: Option[String]): Either[String, SourceLink] =
72-
73-
string match
63+
def parse(string: String): Either[String, SourceLink] =
64+
val res = string match
7465
case KnownProvider(name, organization, repo, rawRevision, rawSubPath) =>
7566
val subPath = Option(rawSubPath).fold("")("/" + _.drop(1))
7667
val pathRev = Option(rawRevision).map(_.drop(1)).orElse(revision)
@@ -87,14 +78,6 @@ object SourceLink:
8778
WebBasedSourceLink(gitlabPrefix(organization, repo), rev, subPath))
8879
case other =>
8980
Left(s"'$other' is not a known provider, please provide full source path template.")
90-
91-
case SubPath(prefix, config) =>
92-
parse(config, revision) match
93-
case l: Left[String, _] => l
94-
case Right(_:PrefixedSourceLink) =>
95-
Left(s"Source path $string has duplicated subpath setting (scm template can not contains '=')")
96-
case Right(nested) =>
97-
Right(PrefixedSourceLink(Paths.get(prefix), nested))
9881
case BrokenKnownProvider("gitlab" | "github") =>
9982
Left(s"Does not match known provider syntax: `<name>://organization/repository`")
10083
case scaladocSetting if ScalaDocPatten.findFirstIn(scaladocSetting).nonEmpty =>
@@ -104,28 +87,23 @@ object SourceLink:
10487
else Right(TemplateSourceLink(supported.foldLeft(string)((template, pattern) =>
10588
template.replace(pattern, SupportedScalaDocPatternReplacements(pattern)))))
10689
case other =>
107-
Right(TemplateSourceLink(""))
90+
Left("Does not match any implemented source link syntax")
91+
res match {
92+
case Left(error) => Left(s"'$string': $error")
93+
case other => other
94+
}
10895

10996

11097
type Operation = "view" | "edit"
11198

112-
case class SourceLinks(links: Seq[SourceLink], projectRoot: Path):
99+
class SourceLinks(val sourceLinks: PathBased[SourceLink]):
113100
def pathTo(rawPath: Path, memberName: String = "", line: Option[Int] = None, operation: Operation = "view"): Option[String] =
114-
def resolveRelativePath(path: Path) =
115-
links
116-
.find(_.path.forall(p => path.startsWith(p)))
117-
.map(_.render(memberName, path, operation, line))
118-
119-
if rawPath.isAbsolute then
120-
if rawPath.startsWith(projectRoot) then resolveRelativePath(projectRoot.relativize(rawPath))
121-
else None
122-
else resolveRelativePath(rawPath)
101+
sourceLinks.get(rawPath).map(res => res.elem.render(memberName, res.path, operation, line))
123102

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

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

153-
def load(
154-
configs: Seq[String],
155-
revision: Option[String],
156-
projectRoot: Path)(
157-
using Context): SourceLinks =
158-
val mappings = configs.map(str => str -> SourceLink.parse(str, revision))
159-
160-
val errors = mappings.collect {
161-
case (template, Left(message)) =>
162-
s"'$template': $message"
163-
}.mkString("\n")
164-
165-
if errors.nonEmpty then report.warning(
166-
s"""Following templates has invalid format:
167-
|$errors
168-
|
169-
|$usage
170-
|""".stripMargin
171-
)
172-
173-
SourceLinks(mappings.collect {case (_, Right(link)) => link}, projectRoot)
174-
175-
def load(using ctx: DocContext): SourceLinks =
176-
load(
177-
ctx.args.sourceLinks,
178-
ctx.args.revision,
179-
// TODO (https://github.com/lampepfl/scaladoc/issues/240): configure source root
180-
Paths.get("").toAbsolutePath
181-
)
131+
def load(config: Seq[String], revision: Option[String], projectRoot: Path = Paths.get("").toAbsolutePath)(using CompilerContext): SourceLinks =
132+
PathBased.parse(config, projectRoot)(using SourceLinkParser(revision)) match {
133+
case PathBased.ParsingResult(errors, sourceLinks) =>
134+
if errors.nonEmpty then report.warning(
135+
s"""Following templates has invalid format:
136+
|$errors
137+
|
138+
|${SourceLinks.usage}
139+
|""".stripMargin
140+
)
141+
SourceLinks(sourceLinks)
142+
}
143+

scaladoc/test/dotty/tools/scaladoc/SourceLinksTests.scala

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,22 @@ class SourceLinkTest:
99
@Test
1010
def testBasicFailures() =
1111
def testFailure(template: String, messagePart: String) =
12-
val res = SourceLink.parse(template, None)
12+
val res = SourceLinkParser(None).parse(template)
1313
assertTrue(s"Expected failure containing $messagePart: $res", res.left.exists(_.contains(messagePart)))
1414

15-
val resWithVersion = SourceLink.parse(template, Some("develop"))
15+
val resWithVersion = SourceLinkParser(Some("develop")).parse(template)
1616
assertEquals(res, resWithVersion)
1717

1818
testFailure("ala://ma/kota", "known provider")
19-
testFailure("ala=ala=ala://ma/kota", "known provider")
20-
testFailure("ala=ala=ala", "subpath")
19+
testFailure("ala=ala=ala://ma/kota", "source link syntax")
20+
testFailure("ala=ala=ala", "source link syntax")
2121
testFailure("""€{TPL_OWNER}""", "scaladoc")
2222

2323

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

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

4140

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

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

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

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

0 commit comments

Comments
 (0)