Skip to content

Commit aea6555

Browse files
committed
scaladoc: support diagnostic expectations in snippets
1 parent e3f9fc1 commit aea6555

File tree

18 files changed

+616
-114
lines changed

18 files changed

+616
-114
lines changed

docs/_docs/reference/experimental/capture-checking/advanced.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,4 @@ By leveraging capability polymorphism, capability members, and path-dependent ca
9494
* `Label`s store the free capabilities `C` of the `block` passed to `boundary` in their capability member `Fv`.
9595
* When suspending on a given label, the suspension handler can capture at most the capabilities that occur freely at the `boundary` that introduced the label. That prevents mentioning nested bound labels.
9696

97-
[Back to Capability Polymorphism](polymorphism.md)
97+
[Back to Capability Polymorphism](polymorphism.md)

docs/_docs/reference/experimental/capture-checking/basics.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ trait LzyList[+A]:
8585
object LzyList:
8686
def apply[T](xs: T*): LzyList[T] = ???
8787
//}
88-
val xs = usingLogFile { f =>
88+
val xs = usingLogFile { f => // error // error
8989
LzyList(1, 2, 3).map { x => f.write(x); x * x }
9090
}
9191
```
@@ -372,7 +372,7 @@ like this:
372372
```scala sc:fail sc-compile-with:logfile-checked
373373
var loophole: () => Unit = () => ()
374374
usingLogFile { f =>
375-
loophole = () => f.write(0)
375+
loophole = () => f.write(0) // error
376376
}
377377
loophole()
378378
```

docs/_docs/reference/experimental/capture-checking/cc.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ title: "Capture Checking"
44
nightlyOf: https://docs.scala-lang.org/scala3/reference/experimental/capture-checking/index.html
55
---
66

7-
Capture checking is a research project that modifies the Scala type system to track references to capabilities in values.
7+
Capture checking is a research project that modifies the Scala type system to track references to capabilities in values.

docs/_docs/reference/experimental/capture-checking/checked-exceptions.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,9 @@ As with other capability based schemes, one needs to guard against capabilities
7373
that are captured in results. For instance, here is a problematic use case:
7474
```scala sc:fail sc-compile-with:checked-exceptions-base
7575
def escaped(xs: Double*): (() => Double) throws LimitExceeded =
76-
try () => xs.map(f).sum // error: CanThrow escapes into returned closure
76+
try () => xs.map(f).sum
7777
catch case ex: LimitExceeded => () => -1
78-
val crasher = escaped(1, 2, 10e+11)
78+
val crasher = escaped(1, 2, 10e+11) // error: CanThrow escapes into returned closure
7979
crasher()
8080
```
8181
This code needs to be rejected since otherwise the call to `crasher()` would cause

docs/_docs/reference/experimental/capture-checking/internals.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,5 +80,3 @@ This section lists all variables that appeared in previous diagnostics and their
8080
- variable `31` has a constant fixed superset `{xs, f}`
8181
- variable `32` has no dependencies.
8282

83-
84-

docs/_docs/reference/experimental/capture-checking/scoped-capabilities.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ If any scope refuses to absorb the capability, capture checking fails:
142142

143143
```scala sc:fail sc-compile-with:scoped-fs-context
144144
def process(fs: FileSystem^): Unit =
145-
val f: () -> Unit = () => fs.read() // Error: fs cannot flow into {}
145+
val f: () -> Unit = () => fs.read() // error: fs cannot flow into {}
146146
```
147147

148148
The closure is declared pure (`() -> Unit`), meaning its local `any` is the empty set. The
@@ -312,14 +312,14 @@ determines the binding structure automatically from where `fresh` appears in the
312312

313313
The rules above establish a key practical distinction when writing function types. Consider:
314314

315-
```scala sc:fail
315+
```scala sc:fail sc-compile-with:scoped-cc-context
316316
import caps.fresh
317317
class A
318318
class B
319319

320320
def test(): Unit =
321-
val f: (x: A^) -> B^{fresh} = ??? // B^{fresh}: existentially bound
322-
val g: A^ -> B^ = ??? // B^{any}: enclosing scope's local any
321+
val f: (x: A^) -> B^{fresh} = ??? // B^{fresh}: existentially bound
322+
val g: A^ -> B^ = ??? // B^{any}: enclosing scope's local any
323323

324324
val _: A^ -> B^ = f // error: fresh is not in {any}
325325
val _: A^ -> B^{fresh} = f // ok
@@ -390,7 +390,7 @@ directly returning a closure that captures it:
390390
```scala sc:fail sc-compile-with:scoped-withfile-context
391391
withFile[() => File^]("test.txt"): f =>
392392
// ^^^^^^^^^^^ T = () => File^, i.e., () ->{any} File^{any} for some outer any
393-
() => f // error: We want to return this as () => File^
393+
() => f // error // error // error: We want to return this as () => File^
394394
```
395395

396396
The lambda `(f: File^) => () => f` has inferred type:
@@ -414,7 +414,7 @@ into this outer `any`, so the assignment fails.
414414
Otherwise, allowing widening `∃fresh. () ->{fresh} File^{fresh}` to `() => File^` would let the scoped file escape:
415415

416416
```scala sc:fail sc-compile-with:scoped-withfile-context
417-
val escaped: () => File^ = withFile[() => File^]("test.txt")(f => () => f)
417+
val escaped: () => File^ = withFile[() => File^]("test.txt")(f => () => f) // error // error
418418
// ^^^^^^^^^^^ any here is in the outer scope
419419
escaped().read() // Use-after-close!
420420
```

project/Build.scala

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2137,9 +2137,7 @@ object Build {
21372137
.add(NoLinkWarnings(true))
21382138
.add(NoLinkAssetWarnings(true))
21392139
.add(GenerateAPI(false))
2140-
.add(SnippetCompiler(List(
2141-
s"${tempDocsRoot.getAbsolutePath}=compile"
2142-
)))
2140+
.add(SnippetCompiler(referenceSnippetCompilerTargets(tempRoot.getAbsolutePath)))
21432141
}
21442142

21452143
generateDocumentation(config)
@@ -3071,8 +3069,14 @@ object ScaladocConfigs {
30713069
"enums",
30723070
"experimental/capture-checking",
30733071
)
3072+
def captureCheckingSnippetTestTargets(docsRoot: String) = List(
3073+
s"$docsRoot/_docs/reference/experimental/capture-checking/basics.md=compile+test",
3074+
s"$docsRoot/_docs/reference/experimental/capture-checking/checked-exceptions.md=compile+test",
3075+
s"$docsRoot/_docs/reference/experimental/capture-checking/scoped-capabilities.md=compile+test"
3076+
)
30743077
def referenceSnippetCompilerTargets(docsRoot: String) =
3075-
referenceSnippetRelativeRoots.map(path => s"$docsRoot/_docs/reference/$path=compile")
3078+
referenceSnippetRelativeRoots.map(path => s"$docsRoot/_docs/reference/$path=compile") ++
3079+
captureCheckingSnippetTestTargets(docsRoot)
30763080

30773081
lazy val Scala3 = Def.task {
30783082
val stdlib = { // relative path to the stdlib directory ('library/')

scaladoc/src/dotty/tools/scaladoc/site/templates.scala

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -76,24 +76,24 @@ case class TemplateFile(
7676
def isIndexPage() = file.isFile && (file.getName == "index.md" || file.getName == "index.html")
7777

7878
private[site] def resolveInner(ctx: RenderingContext)(using ssctx: StaticSiteContext): ResolvedPage =
79-
8079
lazy val snippetCheckingFunc: SnippetChecker.SnippetCheckingFunc =
8180
val path = Some(Paths.get(file.getAbsolutePath))
8281
val pathBasedArg = ssctx.snippetCompilerArgs.get(path)
8382
val sourceFile = dotty.tools.dotc.util.SourceFile(dotty.tools.io.AbstractFile.getFile(path.get), scala.io.Codec.UTF8)
84-
(snippet: SnippetSource, argOverride: Option[SnippetCompilerArg]) => {
85-
val arg = argOverride.fold(pathBasedArg)(pathBasedArg.merge(_))
86-
val compilerData = SnippetCompilerData(
87-
"staticsitesnippet",
88-
SnippetCompilerData.Position(configOffset - 1, 0)
89-
)
90-
ssctx.snippetChecker.checkSnippet(snippet, Some(compilerData), arg, sourceFile, 0).collect {
91-
case r: SnippetCompilationResult if !r.isSuccessful =>
92-
ssctx.bufferSnippetMessages(r.messages)
93-
r
94-
case r => r
95-
}
96-
}
83+
(snippet: SnippetSource, argOverride: Option[SnippetCompilerArg]) =>
84+
val arg = argOverride.fold(pathBasedArg)(pathBasedArg.merge(_))
85+
val compilerData = SnippetCompilerData("staticsitesnippet", SnippetCompilerData.Position(configOffset - 1, 0))
86+
val result = ssctx.snippetChecker.checkSnippet(
87+
snippet,
88+
Some(compilerData),
89+
arg,
90+
sourceFile,
91+
0
92+
)
93+
result.foreach: r =>
94+
if !r.isSuccessful then
95+
ssctx.bufferSnippetMessages(r.messages)
96+
result
9797

9898
if (ctx.resolving.contains(file.getAbsolutePath))
9999
throw new RuntimeException(s"Cycle in templates involving $file: ${ctx.resolving}")

scaladoc/src/dotty/tools/scaladoc/snippets/FlexmarkSnippetProcessor.scala

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package snippets
33

44
import com.vladsch.flexmark.util.{ast => mdu, sequence}
55
import com.vladsch.flexmark.{ast => mda}
6-
import com.vladsch.flexmark.formatter.Formatter
76
import scala.jdk.CollectionConverters._
87

98
import dotty.tools.scaladoc.tasty.comments.markdown.ExtendedFencedCodeBlock
@@ -20,7 +19,7 @@ object FlexmarkSnippetProcessor:
2019
nodes.foldLeft[Map[String, SnippetSource]](Map()) { (snippetMap, node) =>
2120
val lineOffset = node.getStartLineNumber + preparsed.fold(0)(_.strippedLinesBeforeNo)
2221
val codeStartLine = lineOffset + SnippetChecker.codeFenceContentLineOffset
23-
val info = node.getInfo.toString.split(" ")
22+
val info = node.getInfo.toString.split(" ").filter(_.nonEmpty)
2423
if info.contains("scala") then {
2524
val flagOverride = info
2625
.find(_.startsWith("sc:"))

scaladoc/src/dotty/tools/scaladoc/snippets/SnippetChecker.scala

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
11
package dotty.tools.scaladoc
22
package snippets
33

4-
import dotty.tools.scaladoc.DocContext
5-
import java.nio.file.Paths
6-
import java.io.File
7-
4+
import dotty.tools.dotc.config.Settings._
5+
import dotty.tools.dotc.fromtasty.TastyFileUtil
86
import dotty.tools.dotc.util.SourceFile
97
import dotty.tools.io.AbstractFile
10-
import dotty.tools.dotc.fromtasty.TastyFileUtil
11-
import dotty.tools.dotc.config.Settings._
12-
import dotty.tools.dotc.config.ScalaSettings
138

149
class SnippetChecker(val args: Scaladoc.Args)(using cctx: CompilerContext):
1510
private val sep = System.getProperty("path.separator")
@@ -18,15 +13,19 @@ class SnippetChecker(val args: Scaladoc.Args)(using cctx: CompilerContext):
1813
args.tastyFiles
1914
.map(_.getAbsolutePath())
2015
.map(AbstractFile.getFile(_))
21-
.flatMap(t => try { TastyFileUtil.getClassPath(t) } catch { case e: AssertionError => Seq() })
22-
.distinct.mkString(sep),
16+
.flatMap(t => try TastyFileUtil.getClassPath(t) catch case _: AssertionError => Seq.empty)
17+
.distinct
18+
.mkString(sep),
2319
args.classpath
2420
).mkString(sep)
2521

26-
private val snippetCompilerSettings: Seq[SnippetCompilerSetting[?]] = cctx.settings.userSetSettings(cctx.settingsState).filter(_ != cctx.settings.classpath)
27-
.map[SnippetCompilerSetting[?]]( s =>
28-
SnippetCompilerSetting(s, s.valueIn(cctx.settingsState))
29-
) :+ SnippetCompilerSetting(cctx.settings.classpath, fullClasspath)
22+
private val snippetCompilerSettings: Seq[SnippetCompilerSetting[?]] =
23+
val userSetSettings =
24+
cctx.settings.userSetSettings(cctx.settingsState)
25+
.filter(_ != cctx.settings.classpath)
26+
.map[SnippetCompilerSetting[?]]: setting =>
27+
SnippetCompilerSetting(setting, setting.valueIn(cctx.settingsState))
28+
userSetSettings :+ SnippetCompilerSetting(cctx.settings.classpath, fullClasspath)
3029

3130
private val compiler: SnippetCompiler = SnippetCompiler(snippetCompilerSettings = snippetCompilerSettings)
3231

@@ -36,23 +35,31 @@ class SnippetChecker(val args: Scaladoc.Args)(using cctx: CompilerContext):
3635
arg: SnippetCompilerArg,
3736
sourceFile: SourceFile,
3837
sourceColumnOffset: Int
39-
): Option[SnippetCompilationResult] = {
38+
): Option[SnippetCompilationResult] =
4039
if arg.flag != SCFlags.NoCompile then
4140
val baseLineOffset = data.fold(0)(_.position.line)
4241
val baseColumnOffset = data.fold(0)(_.position.column) + sourceColumnOffset
42+
val sourceLines = snippet.sourceLines.map(_.map(_ + baseLineOffset))
43+
val adjustedSnippet = snippet.copy(
44+
sourceLines = sourceLines,
45+
outerLineOffset = snippet.outerLineOffset + baseLineOffset
46+
)
4347
val wrapped = WrappedSnippet(
4448
snippet.snippet,
4549
data.map(_.packageName),
4650
snippet.outerLineOffset + baseLineOffset,
4751
baseColumnOffset,
48-
snippet.sourceLines.map(_.map(_ + baseLineOffset))
52+
sourceLines
4953
)
50-
Some(compiler.compile(wrapped, arg, sourceFile))
54+
Some(compiler.compile(
55+
adjustedSnippet,
56+
wrapped,
57+
arg,
58+
sourceFile
59+
))
5160
else
5261
None
5362

54-
}
55-
5663
object SnippetChecker:
5764
// The first line of snippet content is two lines below the opening code fence.
5865
val codeFenceContentLineOffset = 2

0 commit comments

Comments
 (0)