From d31549363282a24a4570f1ccd14ea7a3ed00be06 Mon Sep 17 00:00:00 2001 From: Russ White <356303+russwyte@users.noreply.github.com> Date: Sun, 25 Oct 2026 16:53:47 -0500 Subject: [PATCH 1/2] Mark end regions by blank gaps, not by line count. Scalafmt is 3.11.5. A long for with no blank line no longer picks up an end marker. Fors named only to avoid that marker are expressions again. --- .scalafmt.conf | 4 +++- cli/src/main/scala/marklit/cli/Daemon.scala | 2 ++ .../scala/marklit/cli/DaemonProtocol.scala | 3 +++ .../main/scala/marklit/cli/MarklitCli.scala | 3 +++ .../marklit/cli/DependencyResolverSpec.scala | 1 + .../compiler/shim/DotcInvokerImpl.scala | 3 +++ .../compiler/shim/DotcInvokerImplSpec.scala | 1 + compiler/src/main/scala/marklit/Marklit.scala | 6 ++++++ .../src/main/scala/marklit/MarklitRun.scala | 2 ++ .../scala/marklit/compiler/Compiler.scala | 3 +++ .../marklit/compiler/CompilerFactory.scala | 4 ++++ .../marklit/compiler/ScalaCompiler.scala | 5 +++++ .../test/scala/marklit/MarklitRunSpec.scala | 1 + .../src/test/scala/marklit/MarklitSpec.scala | 1 + .../test/scala/marklit/RunResourceSpec.scala | 1 + .../marklit/compiler/LocalClassWarnSpec.scala | 1 + .../compiler/OutputIsolationSpec.scala | 1 + .../marklit/compiler/ScalaCompilerSpec.scala | 1 + .../compiler/TopLevelCompileSpec.scala | 1 + .../main/scala/marklit/cache/BlockCache.scala | 6 ++++++ .../main/scala/marklit/model/CodeBlock.scala | 1 + .../scala/marklit/model/CompileResult.scala | 1 + .../scala/marklit/model/MarklitError.scala | 1 + .../main/scala/marklit/model/Modifier.scala | 1 + .../scala/marklit/model/ScopeConfig.scala | 3 +++ .../marklit/parser/InfoStringParser.scala | 1 + .../scala/marklit/parser/MarkdownParser.scala | 1 + .../marklit/processor/DocumentProcessor.scala | 20 +++++++++++-------- .../marklit/renderer/MarkdownRenderer.scala | 11 ++++++++++ .../marklit/resolver/DependencyResolver.scala | 3 +++ .../scala/marklit/results/ResultsFile.scala | 5 +++++ .../scala/marklit/scope/ScopeManager.scala | 9 +++++++++ .../scala/marklit/cache/BlockCacheSpec.scala | 1 + .../scala/marklit/model/CodeBlockSpec.scala | 1 + .../scala/marklit/model/ModifierSpec.scala | 1 + .../marklit/parser/InfoStringParserSpec.scala | 1 + .../parser/MarkdownParserEdgeCasesSpec.scala | 1 + .../marklit/parser/MarkdownParserSpec.scala | 1 + .../processor/DocumentProcessorSpec.scala | 2 ++ .../renderer/MarkdownRendererSpec.scala | 1 + .../resolver/DependencyResolverSpec.scala | 1 + .../marklit/results/ResultsFileSpec.scala | 1 + .../marklit/scope/ScopeManagerSpec.scala | 1 + .../test/scala/marklit/docs/BuildSite.scala | 1 - 44 files changed, 110 insertions(+), 10 deletions(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index e6fc93e..66cb10a 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,2 +1,4 @@ version = "3.11.5" -runner.dialect = scala3 \ No newline at end of file +runner.dialect = scala3 +rewrite.scala3.endMarker.insert.blankGaps.min = 1 +rewrite.scala3.endMarker.remove.blankGaps.max = 0 diff --git a/cli/src/main/scala/marklit/cli/Daemon.scala b/cli/src/main/scala/marklit/cli/Daemon.scala index 004c794..e158774 100644 --- a/cli/src/main/scala/marklit/cli/Daemon.scala +++ b/cli/src/main/scala/marklit/cli/Daemon.scala @@ -74,6 +74,7 @@ object Daemon: // which is correct: by here, the daemon's main fiber has already // resolved cleanly and we just need the JVM to follow. .ensuring(ZIO.succeed(java.lang.Runtime.getRuntime.halt(0))) + end run /** Race [[serveOnce]] against an idle timer. Whichever fiber lands first * publishes its outcome to a Promise; the loser is fire-and-forget @@ -225,3 +226,4 @@ object Daemon: protocolOut.println(resp.toJson) protocolOut.flush() } +end Daemon diff --git a/cli/src/main/scala/marklit/cli/DaemonProtocol.scala b/cli/src/main/scala/marklit/cli/DaemonProtocol.scala index e6b90a3..bfdd351 100644 --- a/cli/src/main/scala/marklit/cli/DaemonProtocol.scala +++ b/cli/src/main/scala/marklit/cli/DaemonProtocol.scala @@ -33,6 +33,7 @@ object DaemonProtocol: case object Shutdown extends Request given JsonCodec[Request] = DeriveJsonCodec.gen[Request] + end Request /** Parameters for the `clear-cache` RPC. The daemon doesn't keep cache state * between requests; the caller tells it which directory to clear. @@ -74,6 +75,7 @@ object DaemonProtocol: final case class Err(method: String, message: String) extends Response given JsonCodec[Response] = DeriveJsonCodec.gen[Response] + end Response /** Per-file outcome from one compile-document request. */ final case class FileResult( @@ -89,3 +91,4 @@ object DaemonProtocol: def isSuccess: Boolean = files.forall(_.success) object CompileResponse: given JsonCodec[CompileResponse] = DeriveJsonCodec.gen[CompileResponse] +end DaemonProtocol diff --git a/cli/src/main/scala/marklit/cli/MarklitCli.scala b/cli/src/main/scala/marklit/cli/MarklitCli.scala index 290b5e9..b9eb2e3 100644 --- a/cli/src/main/scala/marklit/cli/MarklitCli.scala +++ b/cli/src/main/scala/marklit/cli/MarklitCli.scala @@ -307,3 +307,6 @@ object MarklitCli extends ZIOCliDefault: .fail(new RuntimeException(s"${result.failedCount} file(s) failed")) .unless(result.success) yield () + end for + end runMarklit +end MarklitCli diff --git a/cli/src/test/scala/marklit/cli/DependencyResolverSpec.scala b/cli/src/test/scala/marklit/cli/DependencyResolverSpec.scala index c64e377..a398e32 100644 --- a/cli/src/test/scala/marklit/cli/DependencyResolverSpec.scala +++ b/cli/src/test/scala/marklit/cli/DependencyResolverSpec.scala @@ -167,3 +167,4 @@ object DependencyResolverSpec extends ZIOSpecDefault: } ) ) +end DependencyResolverSpec diff --git a/compiler-shim/src/main/scala/marklit/compiler/shim/DotcInvokerImpl.scala b/compiler-shim/src/main/scala/marklit/compiler/shim/DotcInvokerImpl.scala index c3c1fb0..c38ba12 100644 --- a/compiler-shim/src/main/scala/marklit/compiler/shim/DotcInvokerImpl.scala +++ b/compiler-shim/src/main/scala/marklit/compiler/shim/DotcInvokerImpl.scala @@ -56,6 +56,7 @@ final class DotcInvokerImpl extends DotcInvoker: .asJava new CompileResponse(!reporter.hasErrors, diags) + end compile end DotcInvokerImpl @@ -102,3 +103,5 @@ private final class CollectingReporter extends Reporter: else None _diagnostics += ShimDiag(severity, dia.message, line, column, file) + end doReport +end CollectingReporter diff --git a/compiler-shim/src/test/scala/marklit/compiler/shim/DotcInvokerImplSpec.scala b/compiler-shim/src/test/scala/marklit/compiler/shim/DotcInvokerImplSpec.scala index b7f2605..98e4c63 100644 --- a/compiler-shim/src/test/scala/marklit/compiler/shim/DotcInvokerImplSpec.scala +++ b/compiler-shim/src/test/scala/marklit/compiler/shim/DotcInvokerImplSpec.scala @@ -140,3 +140,4 @@ object DotcInvokerImplSpec extends ZIOSpecDefault: finally cleanup(tmp) } ) +end DotcInvokerImplSpec diff --git a/compiler/src/main/scala/marklit/Marklit.scala b/compiler/src/main/scala/marklit/Marklit.scala index 741821b..332b357 100644 --- a/compiler/src/main/scala/marklit/Marklit.scala +++ b/compiler/src/main/scala/marklit/Marklit.scala @@ -26,6 +26,7 @@ final case class MarklitResult( val blockCount = document.codeBlocks.size val errorCount = errors.size s"$status: $sourceFile ($blockCount blocks, $errorCount errors, ${processingResult.processingTime.toMillis}ms)" +end MarklitResult /** Main entry point for marklit document processing */ trait Marklit: @@ -40,6 +41,7 @@ trait Marklit: /** Process multiple files */ def processFiles(paths: Vector[Path]): IO[MarklitError, Vector[MarklitResult]] +end Marklit object Marklit: def processFile(path: Path): ZIO[Marklit, MarklitError, MarklitResult] = @@ -124,6 +126,7 @@ object Marklit: CompilerFactory.layer >>> liveWithFactory( CompilerFactory.defaultScalaVersion ) +end Marklit /** Adapter from Compiler(s) to CompilerService. * @@ -328,6 +331,7 @@ private final class CompilerServiceAdapter( case Some(dir) => c.executeFromDir(dir, ctx).map(_.output) case None => c.execute(code, ctx).map(_.output) } +end CompilerServiceAdapter private object CompilerServiceAdapter: def fixed(compiler: Compiler): CompilerService = @@ -429,6 +433,7 @@ private object CompilerServiceAdapter: sb.append(java.lang.Integer.toHexString(v)) } sb.toString +end CompilerServiceAdapter /** Live implementation */ final class MarklitLive( @@ -480,3 +485,4 @@ final class MarklitLive( paths: Vector[Path] ): IO[MarklitError, Vector[MarklitResult]] = ZIO.foreach(paths)(processFile) +end MarklitLive diff --git a/compiler/src/main/scala/marklit/MarklitRun.scala b/compiler/src/main/scala/marklit/MarklitRun.scala index 8147b29..467744b 100644 --- a/compiler/src/main/scala/marklit/MarklitRun.scala +++ b/compiler/src/main/scala/marklit/MarklitRun.scala @@ -368,3 +368,5 @@ object MarklitRun: rendered = rendered, outputPath = outputPath ) + end buildReport +end MarklitRun diff --git a/compiler/src/main/scala/marklit/compiler/Compiler.scala b/compiler/src/main/scala/marklit/compiler/Compiler.scala index a4f8561..1969fdd 100644 --- a/compiler/src/main/scala/marklit/compiler/Compiler.scala +++ b/compiler/src/main/scala/marklit/compiler/Compiler.scala @@ -29,6 +29,7 @@ final case class ScopeContext( */ def hoistedCode: String = topLevelPriorCode.mkString("\n\n") +end ScopeContext object ScopeContext: val empty: ScopeContext = ScopeContext() @@ -77,6 +78,7 @@ trait Compiler: case None => execute(code, context) else ZIO.succeed(ExecutionResult("", Map.empty)) yield (compileResult, execResult) +end Compiler object Compiler: def compile( @@ -96,3 +98,4 @@ object Compiler: context: ScopeContext ): ZIO[Compiler, MarklitError, (CompileResult, ExecutionResult)] = ZIO.serviceWithZIO[Compiler](_.compileAndExecute(code, context)) +end Compiler diff --git a/compiler/src/main/scala/marklit/compiler/CompilerFactory.scala b/compiler/src/main/scala/marklit/compiler/CompilerFactory.scala index 8cea58d..8809ce3 100644 --- a/compiler/src/main/scala/marklit/compiler/CompilerFactory.scala +++ b/compiler/src/main/scala/marklit/compiler/CompilerFactory.scala @@ -62,6 +62,7 @@ trait CompilerFactory: scalaVersion: String, extraClasspath: Vector[String] ): URIO[Scope, ClassLoader] +end CompilerFactory object CompilerFactory: @@ -234,6 +235,7 @@ object CompilerFactory: platformLoader.loadClass(name) true catch case _: ClassNotFoundException => false + end ApiOnlyParent private final class Live( shim3Jar: Path, @@ -318,3 +320,5 @@ object CompilerFactory: .newInstance() .asInstanceOf[DotcInvoker] VersionBundle(invoker, compilerJars, loader) + end Live +end CompilerFactory diff --git a/compiler/src/main/scala/marklit/compiler/ScalaCompiler.scala b/compiler/src/main/scala/marklit/compiler/ScalaCompiler.scala index 8aa925e..f9f4520 100644 --- a/compiler/src/main/scala/marklit/compiler/ScalaCompiler.scala +++ b/compiler/src/main/scala/marklit/compiler/ScalaCompiler.scala @@ -140,6 +140,7 @@ final class ScalaCompiler( classFilesDir = if resp.success() then Some(perBlockOut) else None ) finally Files.deleteIfExists(sourceFile) + end try } .mapError(e => MarklitError.CompileError( @@ -268,6 +269,7 @@ final class ScalaCompiler( finally java.lang.System.setOut(oldOut) java.lang.System.setErr(oldErr) + end try } .mapError { e => val cause = e match @@ -400,6 +402,7 @@ final class ScalaCompiler( // If the user classloader can't see scala.Console (e.g., a non-Scala // runtime path), there's nothing to redirect. Return a no-op. case _: ClassNotFoundException => () => () +end ScalaCompiler object ScalaCompiler: /** Scala version returned by the bundled shim. Used as the fallback when no @@ -439,6 +442,7 @@ private class ChildFirstClassLoader( catch case _: ClassNotFoundException => super.loadClass(name, resolve) +end ChildFirstClassLoader private object ChildFirstClassLoader: /** The platform classloader can load every JDK platform-module class @@ -453,3 +457,4 @@ private object ChildFirstClassLoader: platformLoader.loadClass(name) true catch case _: ClassNotFoundException => false +end ChildFirstClassLoader diff --git a/compiler/src/test/scala/marklit/MarklitRunSpec.scala b/compiler/src/test/scala/marklit/MarklitRunSpec.scala index ec314bc..8c15590 100644 --- a/compiler/src/test/scala/marklit/MarklitRunSpec.scala +++ b/compiler/src/test/scala/marklit/MarklitRunSpec.scala @@ -132,3 +132,4 @@ object MarklitRunSpec extends ZIOSpecDefault: yield assertTrue(exit.isFailure) } ) @@ TestAspect.sequential @@ TestAspect.timeout(120.seconds) +end MarklitRunSpec diff --git a/compiler/src/test/scala/marklit/MarklitSpec.scala b/compiler/src/test/scala/marklit/MarklitSpec.scala index 7fd26a7..3275165 100644 --- a/compiler/src/test/scala/marklit/MarklitSpec.scala +++ b/compiler/src/test/scala/marklit/MarklitSpec.scala @@ -748,3 +748,4 @@ object MarklitSpec extends ZIOSpecDefault: ) @@ TestAspect.sequential @@ TestAspect.timeout( 60.seconds ) +end MarklitSpec diff --git a/compiler/src/test/scala/marklit/RunResourceSpec.scala b/compiler/src/test/scala/marklit/RunResourceSpec.scala index 1cbbe2d..016307a 100644 --- a/compiler/src/test/scala/marklit/RunResourceSpec.scala +++ b/compiler/src/test/scala/marklit/RunResourceSpec.scala @@ -269,3 +269,4 @@ object RunResourceSpec extends ZIOSpecDefault: } } ) @@ TestAspect.sequential @@ TestAspect.timeout(180.seconds) +end RunResourceSpec diff --git a/compiler/src/test/scala/marklit/compiler/LocalClassWarnSpec.scala b/compiler/src/test/scala/marklit/compiler/LocalClassWarnSpec.scala index e3c5cb7..6d06a82 100644 --- a/compiler/src/test/scala/marklit/compiler/LocalClassWarnSpec.scala +++ b/compiler/src/test/scala/marklit/compiler/LocalClassWarnSpec.scala @@ -66,3 +66,4 @@ object LocalClassWarnSpec extends ZIOSpecDefault: ).provideShared(TestCompilerLayer.layer) @@ TestAspect.timeout( 120.seconds ) @@ TestAspect.withLiveClock @@ TestAspect.sequential +end LocalClassWarnSpec diff --git a/compiler/src/test/scala/marklit/compiler/OutputIsolationSpec.scala b/compiler/src/test/scala/marklit/compiler/OutputIsolationSpec.scala index 78647b8..21c760f 100644 --- a/compiler/src/test/scala/marklit/compiler/OutputIsolationSpec.scala +++ b/compiler/src/test/scala/marklit/compiler/OutputIsolationSpec.scala @@ -262,3 +262,4 @@ object OutputIsolationSpec extends ZIOSpecDefault: ).provideShared( TestCompilerLayer.layer ) @@ TestAspect.sequential +end OutputIsolationSpec diff --git a/compiler/src/test/scala/marklit/compiler/ScalaCompilerSpec.scala b/compiler/src/test/scala/marklit/compiler/ScalaCompilerSpec.scala index 3677ed9..edd253c 100644 --- a/compiler/src/test/scala/marklit/compiler/ScalaCompilerSpec.scala +++ b/compiler/src/test/scala/marklit/compiler/ScalaCompilerSpec.scala @@ -145,3 +145,4 @@ object ScalaCompilerSpec extends ZIOSpecDefault: ).provideShared(TestCompilerLayer.layer) @@ TestAspect.timeout( 60.seconds ) @@ TestAspect.withLiveClock @@ TestAspect.sequential +end ScalaCompilerSpec diff --git a/compiler/src/test/scala/marklit/compiler/TopLevelCompileSpec.scala b/compiler/src/test/scala/marklit/compiler/TopLevelCompileSpec.scala index eed945c..b3ae854 100644 --- a/compiler/src/test/scala/marklit/compiler/TopLevelCompileSpec.scala +++ b/compiler/src/test/scala/marklit/compiler/TopLevelCompileSpec.scala @@ -120,3 +120,4 @@ object TopLevelCompileSpec extends ZIOSpecDefault: ).provideShared(TestCompilerLayer.layer) @@ TestAspect.timeout( 120.seconds ) @@ TestAspect.withLiveClock @@ TestAspect.sequential +end TopLevelCompileSpec diff --git a/core/src/main/scala/marklit/cache/BlockCache.scala b/core/src/main/scala/marklit/cache/BlockCache.scala index b989f4a..64876ed 100644 --- a/core/src/main/scala/marklit/cache/BlockCache.scala +++ b/core/src/main/scala/marklit/cache/BlockCache.scala @@ -36,6 +36,7 @@ trait BlockCache: */ def put(key: BlockCacheKey, result: CompileResult): UIO[Unit] def clear: UIO[Unit] +end BlockCache object BlockCache: @@ -53,6 +54,7 @@ object BlockCache: * layer above (here, swallowed) — the user still gets correct compilation. */ def disk(root: Path): BlockCache = new DiskBlockCache(root) +end BlockCache private final class DiskBlockCache(root: java.nio.file.Path) extends BlockCache: @@ -143,6 +145,7 @@ private final class DiskBlockCache(root: java.nio.file.Path) extends BlockCache: if !(keepRoot && f == p) then Files.deleteIfExists(f): Unit } finally it.close() +end DiskBlockCache /** A 64-character lowercase hex SHA-256 of the cache inputs. The wrapper type * keeps us from accidentally treating a raw user string as a key. @@ -224,6 +227,7 @@ object BlockCacheKey: sb.append(java.lang.Integer.toHexString(v)) } BlockCacheKey(sb.toString) +end BlockCacheKey /** On-disk JSON shape for the metadata side of a cached CompileResult. Class * files live in a sibling `.classes/` directory and are wired back in by @@ -249,6 +253,7 @@ private object CachedCompileResult: success = cr.success, diagnostics = cr.diagnostics.map(CachedDiagnostic.from) ) +end CachedCompileResult private final case class CachedDiagnostic( severity: String, @@ -284,3 +289,4 @@ private object CachedDiagnostic: column = d.column, file = d.file ) +end CachedDiagnostic diff --git a/core/src/main/scala/marklit/model/CodeBlock.scala b/core/src/main/scala/marklit/model/CodeBlock.scala index d2029e2..138e7d5 100644 --- a/core/src/main/scala/marklit/model/CodeBlock.scala +++ b/core/src/main/scala/marklit/model/CodeBlock.scala @@ -129,3 +129,4 @@ final case class CodeBlock( sharedMajor match case None => false case Some(mv) => version.takeWhile(_ != '.') == mv +end CodeBlock diff --git a/core/src/main/scala/marklit/model/CompileResult.scala b/core/src/main/scala/marklit/model/CompileResult.scala index 1607105..c8d9ecd 100644 --- a/core/src/main/scala/marklit/model/CompileResult.scala +++ b/core/src/main/scala/marklit/model/CompileResult.scala @@ -21,6 +21,7 @@ final case class CompileResult( def warnings: List[ScalaDiagnostic] = diagnostics.filter(_.severity == DiagnosticSeverity.Warning) +end CompileResult /** Result of executing compiled code */ final case class ExecutionResult( diff --git a/core/src/main/scala/marklit/model/MarklitError.scala b/core/src/main/scala/marklit/model/MarklitError.scala index b1bea67..fc4c257 100644 --- a/core/src/main/scala/marklit/model/MarklitError.scala +++ b/core/src/main/scala/marklit/model/MarklitError.scala @@ -30,3 +30,4 @@ enum MarklitError: s"Runtime error: ${ex.getMessage}\nOutput: $output" case ValidationError(loc, msg) => s"Validation error at ${loc.pretty}: $msg" case ResolutionError(msg) => s"Dependency resolution failed: $msg" +end MarklitError diff --git a/core/src/main/scala/marklit/model/Modifier.scala b/core/src/main/scala/marklit/model/Modifier.scala index 01debee..3bcfd9d 100644 --- a/core/src/main/scala/marklit/model/Modifier.scala +++ b/core/src/main/scala/marklit/model/Modifier.scala @@ -49,3 +49,4 @@ object Modifier: case Shared => "scala=shared" case SharedMajor(m) => s"scala=shared-$m" case TopLevel => "top-level" +end Modifier diff --git a/core/src/main/scala/marklit/model/ScopeConfig.scala b/core/src/main/scala/marklit/model/ScopeConfig.scala index e81afdf..efb74cc 100644 --- a/core/src/main/scala/marklit/model/ScopeConfig.scala +++ b/core/src/main/scala/marklit/model/ScopeConfig.scala @@ -33,6 +33,7 @@ final case class ScopeConfig( /** Whether this block creates an anonymous child scope */ def createsAnonymousChild: Boolean = extendsScope.isDefined && id.isEmpty && !append +end ScopeConfig object ScopeConfig: val empty: ScopeConfig = ScopeConfig() @@ -56,3 +57,5 @@ object ScopeConfig: } ScopeConfig(id, extendsScope, append, scalaVersion).validate + end parse +end ScopeConfig diff --git a/core/src/main/scala/marklit/parser/InfoStringParser.scala b/core/src/main/scala/marklit/parser/InfoStringParser.scala index a9312c1..2f3d08d 100644 --- a/core/src/main/scala/marklit/parser/InfoStringParser.scala +++ b/core/src/main/scala/marklit/parser/InfoStringParser.scala @@ -133,3 +133,4 @@ object InfoStringParser: case _: Parsed.Failure => // Parse failure - treat as non-scala passthrough ParsedInfoString(false, Set(Modifier.Passthrough), ScopeConfig.empty) +end InfoStringParser diff --git a/core/src/main/scala/marklit/parser/MarkdownParser.scala b/core/src/main/scala/marklit/parser/MarkdownParser.scala index bd40e94..f4d9344 100644 --- a/core/src/main/scala/marklit/parser/MarkdownParser.scala +++ b/core/src/main/scala/marklit/parser/MarkdownParser.scala @@ -322,3 +322,4 @@ object MarkdownParser: // Internal sentinel for the blockquoted-fence rejection path. private class BlockquoteFenceException(val line: Int, val col: Int) extends RuntimeException +end MarkdownParser diff --git a/core/src/main/scala/marklit/processor/DocumentProcessor.scala b/core/src/main/scala/marklit/processor/DocumentProcessor.scala index a2cce46..e9b55a7 100644 --- a/core/src/main/scala/marklit/processor/DocumentProcessor.scala +++ b/core/src/main/scala/marklit/processor/DocumentProcessor.scala @@ -59,6 +59,7 @@ final case class BlockResult( else if crossExecutions.nonEmpty then crossExecutions.forall(x => isExecutionSuccess(x.compileResult, x.error)) else isExecutionSuccess(compileResult, error) +end BlockResult /** Result of processing an entire document */ final case class DocumentResult( @@ -91,6 +92,7 @@ final case class DocumentResult( else br.compileResult.toVector crs.filterNot(_.success).map(cr => (br.block, cr.diagnostics)) } +end DocumentResult /** Processes markdown documents with Scala code blocks */ trait DocumentProcessor: @@ -149,6 +151,7 @@ trait CompilerService: topLevel: Boolean = false, topLevelPriorCode: Vector[String] = Vector.empty ): IO[MarklitError, String] +end CompilerService /** Live implementation that processes blocks sequentially, respecting scope * dependencies @@ -285,6 +288,7 @@ final class DocumentProcessorLive( java.time.Duration.between(startTime, endTime) ) } + end process private def processBlock( block: CodeBlock, @@ -355,7 +359,7 @@ final class DocumentProcessorLive( else if block.isAnyShared then processSharedBlock(block, effectiveVersion, versionsInUse) else - val effect = for + (for // Resolve scope and get inherited code, split into hoisted top-level // definitions (emitted at file scope) and run-body prior code. resolved <- scopeManager.resolveScope( @@ -397,9 +401,7 @@ final class DocumentProcessorLive( executionOutput = execResult.output, error = execResult.error, // Captured error for crash blocks effectiveScalaVersion = Some(effectiveVersion) - ) - - effect.catchAll { error => + )).catchAll { error => ZIO.succeed( BlockResult( block, @@ -410,6 +412,8 @@ final class DocumentProcessorLive( ) ) } + end if + end processBlockChecked /** Fan-out path for `scala=shared` / `scala=shared-{mv}`: compile+execute the * same source against every applicable version in use, gathering one @@ -428,7 +432,7 @@ final class DocumentProcessorLive( ZIO .foreach(targets) { v => - val effect = for + (for resolved <- scopeManager.resolveScope( ScopeConfig.empty, block.location, @@ -447,9 +451,7 @@ final class DocumentProcessorLive( compileResult = Some(compileResult), executionOutput = execResult.output, error = execResult.error - ) - - effect.catchAll(err => + )).catchAll(err => ZIO.succeed( BlockExecution( scalaVersion = v, @@ -471,6 +473,7 @@ final class DocumentProcessorLive( crossExecutions = execs ) } + end processSharedBlock private def compileBlock( block: CodeBlock, @@ -593,6 +596,7 @@ final class DocumentProcessorLive( } else doExecute.map(out => ExecResult(Some(out), None)) else ZIO.succeed(ExecResult(None, None)) +end DocumentProcessorLive object DocumentProcessorLive: def layer: URLayer[ScopeManager & CompilerService, DocumentProcessor] = diff --git a/core/src/main/scala/marklit/renderer/MarkdownRenderer.scala b/core/src/main/scala/marklit/renderer/MarkdownRenderer.scala index 1fff49e..4ef668a 100644 --- a/core/src/main/scala/marklit/renderer/MarkdownRenderer.scala +++ b/core/src/main/scala/marklit/renderer/MarkdownRenderer.scala @@ -42,6 +42,7 @@ object MarkdownRenderer: } sb.toString + end render private def renderCodeBlock( sb: StringBuilder, @@ -87,6 +88,7 @@ object MarkdownRenderer: val ver = br.effectiveScalaVersion renderSingleExecution(sb, block, br, ver, config) } + end renderCodeBlock private def renderSingleExecution( sb: StringBuilder, @@ -186,6 +188,7 @@ object MarkdownRenderer: sb.append(output) if !output.endsWith("\n") then sb.append("\n") sb.append("```\n") + end renderOutputWithHeader /** Render a `scala=shared` / `scala=shared-{mv}` block's per-version * executions. When every execution succeeded with identical stdout, render a @@ -250,6 +253,7 @@ object MarkdownRenderer: case _ => () } } + end renderCrossExecutions private def renderDiagnostics( sb: StringBuilder, @@ -279,6 +283,7 @@ object MarkdownRenderer: sb.append(s"${config.errorPrefix}$severity: $cleanMessage\n") } sb.append("```\n") + end renderDiagnostics /** Render a document using merged results from multiple Scala version runs */ def renderMerged( @@ -301,6 +306,7 @@ object MarkdownRenderer: } sb.toString + end renderMerged private def renderCodeBlockMerged( sb: StringBuilder, @@ -373,7 +379,10 @@ object MarkdownRenderer: sb.append(s"${config.errorPrefix}$err\n") sb.append("```\n") } + end if } + end if + end renderCodeBlockMerged private def renderDiagnosticsFromEntries( sb: StringBuilder, @@ -395,3 +404,5 @@ object MarkdownRenderer: sb.append(s"${config.errorPrefix}${diag.severity}: $cleanMessage\n") } sb.append("```\n") + end renderDiagnosticsFromEntries +end MarkdownRenderer diff --git a/core/src/main/scala/marklit/resolver/DependencyResolver.scala b/core/src/main/scala/marklit/resolver/DependencyResolver.scala index 0453ebc..3767b32 100644 --- a/core/src/main/scala/marklit/resolver/DependencyResolver.scala +++ b/core/src/main/scala/marklit/resolver/DependencyResolver.scala @@ -170,6 +170,7 @@ object DependencyResolver: def isEmpty: Boolean = dependencies.isEmpty && scalaVersion.isEmpty && repositories.isEmpty && scalacOptions.isEmpty + end UsingDirectives extension (ud: UsingDirectives) def addDirective(d: Directive): UsingDirectives = d match @@ -288,6 +289,7 @@ object DependencyResolver: val fetch = Fetch.create().addDependencies(deps*) fetch.fetch().asScala.toVector.map(_.getAbsolutePath) + end resolveScalaLibrarySync /** Resolve compiler jars (with all transitive deps, including the matching * stdlib). Used by CompilerFactory to build a per-version classloader for @@ -326,3 +328,4 @@ object DependencyResolver: .asScala .toVector .map(_.getAbsolutePath) +end DependencyResolver diff --git a/core/src/main/scala/marklit/results/ResultsFile.scala b/core/src/main/scala/marklit/results/ResultsFile.scala index 55cf83c..7080b1b 100644 --- a/core/src/main/scala/marklit/results/ResultsFile.scala +++ b/core/src/main/scala/marklit/results/ResultsFile.scala @@ -26,6 +26,7 @@ object DiagnosticEntry: line = d.line, column = d.column ) +end DiagnosticEntry /** A serializable result for a single code block */ final case class BlockResultEntry( @@ -56,6 +57,7 @@ object BlockResultEntry: .getOrElse(Nil), runtimeError = br.error.map(_.pretty) ) +end BlockResultEntry /** Results from a single marklit run */ final case class RunResults( @@ -84,6 +86,7 @@ object RunResults: BlockResultEntry.fromBlockResult(_, scalaVersion) ) ) +end RunResults /** Merged results from multiple runs (different Scala versions) */ final case class MergedResults( @@ -106,6 +109,7 @@ final case class MergedResults( .get(locationKey) .map(_.map(_.scalaVersion)) .getOrElse(Vector.empty) +end MergedResults object MergedResults: given JsonCodec[MergedResults] = DeriveJsonCodec.gen[MergedResults] @@ -130,3 +134,4 @@ object MergedResults: else val results = parsed.collect { case Right(r) => r } Right(merge(results)) +end MergedResults diff --git a/core/src/main/scala/marklit/scope/ScopeManager.scala b/core/src/main/scala/marklit/scope/ScopeManager.scala index 08e6f15..b25036e 100644 --- a/core/src/main/scala/marklit/scope/ScopeManager.scala +++ b/core/src/main/scala/marklit/scope/ScopeManager.scala @@ -23,6 +23,7 @@ final case class Scope( copy(priorCode = priorCode :+ code) def allCode: String = priorCode.mkString("\n\n") +end Scope object Scope: /** Legacy single-default id, kept as the bare-version default for blocks @@ -58,6 +59,7 @@ object Scope: topLevel: Boolean = false ): Scope = Scope(id, scalaVersion, Vector.empty, parent, topLevel) +end Scope /** Result of resolving a scope for a code block. * @@ -97,6 +99,7 @@ final case class ResolvedScope( */ def allCode: String = (inheritedCode ++ scope.priorCode).mkString("\n\n") +end ResolvedScope /** Manages scope trees for markdown document processing */ trait ScopeManager: @@ -131,6 +134,7 @@ trait ScopeManager: /** Get scopes that can be compiled in parallel (no dependencies between them) */ def independentScopes: UIO[Vector[Set[String]]] +end ScopeManager object ScopeManager: def resolveScope( @@ -163,6 +167,7 @@ object ScopeManager: /** Create a new ScopeManager instance */ def make: UIO[ScopeManager] = Ref.make(ScopeState.empty).map(ScopeManagerLive(_)) +end ScopeManager /** Internal state for scope management */ private final case class ScopeState( @@ -201,6 +206,7 @@ private final case class ScopeState( def descendants(id: String): Set[String] = val direct = children.getOrElse(id, Set.empty) direct ++ direct.flatMap(descendants) +end ScopeState private object ScopeState: val empty: ScopeState = ScopeState( @@ -542,6 +548,8 @@ private final class ScopeManagerLive(stateRef: Ref[ScopeState]) ), state ) + end match + end resolveInternal /** Collect all code from ancestors in order (oldest first), split by scope * kind: `(topLevelCode, bodyCode)`. Because a top-level scope can only be @@ -587,3 +595,4 @@ private final class ScopeManagerLive(stateRef: Ref[ScopeState]) val byRoot = state.scopes.keys.groupBy(findRoot) byRoot.values.map(_.toSet).toVector } +end ScopeManagerLive diff --git a/core/src/test/scala/marklit/cache/BlockCacheSpec.scala b/core/src/test/scala/marklit/cache/BlockCacheSpec.scala index f841183..02d4ed6 100644 --- a/core/src/test/scala/marklit/cache/BlockCacheSpec.scala +++ b/core/src/test/scala/marklit/cache/BlockCacheSpec.scala @@ -326,3 +326,4 @@ object BlockCacheSpec extends ZIOSpecDefault: } } ) +end BlockCacheSpec diff --git a/core/src/test/scala/marklit/model/CodeBlockSpec.scala b/core/src/test/scala/marklit/model/CodeBlockSpec.scala index c3e2871..0ca06db 100644 --- a/core/src/test/scala/marklit/model/CodeBlockSpec.scala +++ b/core/src/test/scala/marklit/model/CodeBlockSpec.scala @@ -118,3 +118,4 @@ object CodeBlockSpec extends ZIOSpecDefault: assertTrue(b.showWarningsOverride == None) } ) +end CodeBlockSpec diff --git a/core/src/test/scala/marklit/model/ModifierSpec.scala b/core/src/test/scala/marklit/model/ModifierSpec.scala index 61f7c88..6932144 100644 --- a/core/src/test/scala/marklit/model/ModifierSpec.scala +++ b/core/src/test/scala/marklit/model/ModifierSpec.scala @@ -38,3 +38,4 @@ object ModifierSpec extends ZIOSpecDefault: } ) ) +end ModifierSpec diff --git a/core/src/test/scala/marklit/parser/InfoStringParserSpec.scala b/core/src/test/scala/marklit/parser/InfoStringParserSpec.scala index 3945f8e..0331e51 100644 --- a/core/src/test/scala/marklit/parser/InfoStringParserSpec.scala +++ b/core/src/test/scala/marklit/parser/InfoStringParserSpec.scala @@ -183,3 +183,4 @@ object InfoStringParserSpec extends ZIOSpecDefault: } ) ) +end InfoStringParserSpec diff --git a/core/src/test/scala/marklit/parser/MarkdownParserEdgeCasesSpec.scala b/core/src/test/scala/marklit/parser/MarkdownParserEdgeCasesSpec.scala index b663698..9127ed3 100644 --- a/core/src/test/scala/marklit/parser/MarkdownParserEdgeCasesSpec.scala +++ b/core/src/test/scala/marklit/parser/MarkdownParserEdgeCasesSpec.scala @@ -637,3 +637,4 @@ object MarkdownParserEdgeCasesSpec extends ZIOSpecDefault: yield assertTrue(doc.codeBlocks.head.code == src) } ) +end MarkdownParserEdgeCasesSpec diff --git a/core/src/test/scala/marklit/parser/MarkdownParserSpec.scala b/core/src/test/scala/marklit/parser/MarkdownParserSpec.scala index fc6eb2e..e132851 100644 --- a/core/src/test/scala/marklit/parser/MarkdownParserSpec.scala +++ b/core/src/test/scala/marklit/parser/MarkdownParserSpec.scala @@ -360,3 +360,4 @@ object MarkdownParserSpec extends ZIOSpecDefault: } ) ) +end MarkdownParserSpec diff --git a/core/src/test/scala/marklit/processor/DocumentProcessorSpec.scala b/core/src/test/scala/marklit/processor/DocumentProcessorSpec.scala index e85203c..ef6084a 100644 --- a/core/src/test/scala/marklit/processor/DocumentProcessorSpec.scala +++ b/core/src/test/scala/marklit/processor/DocumentProcessorSpec.scala @@ -86,6 +86,7 @@ object DocumentProcessorSpec extends ZIOSpecDefault: .RuntimeError(new RuntimeException("crash"), "crash output") ) else ZIO.succeed(executeResults.getOrElse(code, "")) + end TestCompiler def makeBlock( code: String, @@ -1397,3 +1398,4 @@ object DocumentProcessorSpec extends ZIOSpecDefault: } ) ).provide(ScopeManager.layer) @@ TestAspect.sequential +end DocumentProcessorSpec diff --git a/core/src/test/scala/marklit/renderer/MarkdownRendererSpec.scala b/core/src/test/scala/marklit/renderer/MarkdownRendererSpec.scala index 4927e8b..31bbe1b 100644 --- a/core/src/test/scala/marklit/renderer/MarkdownRendererSpec.scala +++ b/core/src/test/scala/marklit/renderer/MarkdownRendererSpec.scala @@ -835,3 +835,4 @@ object MarkdownRendererSpec extends ZIOSpecDefault: } ) ) +end MarkdownRendererSpec diff --git a/core/src/test/scala/marklit/resolver/DependencyResolverSpec.scala b/core/src/test/scala/marklit/resolver/DependencyResolverSpec.scala index 63f462c..43307ad 100644 --- a/core/src/test/scala/marklit/resolver/DependencyResolverSpec.scala +++ b/core/src/test/scala/marklit/resolver/DependencyResolverSpec.scala @@ -77,3 +77,4 @@ object DependencyResolverSpec extends ZIOSpecDefault: } ) ) @@ TestAspect.sequential +end DependencyResolverSpec diff --git a/core/src/test/scala/marklit/results/ResultsFileSpec.scala b/core/src/test/scala/marklit/results/ResultsFileSpec.scala index 42dabbb..d6f0388 100644 --- a/core/src/test/scala/marklit/results/ResultsFileSpec.scala +++ b/core/src/test/scala/marklit/results/ResultsFileSpec.scala @@ -256,3 +256,4 @@ object ResultsFileSpec extends ZIOSpecDefault: } ) ) +end ResultsFileSpec diff --git a/core/src/test/scala/marklit/scope/ScopeManagerSpec.scala b/core/src/test/scala/marklit/scope/ScopeManagerSpec.scala index 92de38e..27744b9 100644 --- a/core/src/test/scala/marklit/scope/ScopeManagerSpec.scala +++ b/core/src/test/scala/marklit/scope/ScopeManagerSpec.scala @@ -520,3 +520,4 @@ object ScopeManagerSpec extends ZIOSpecDefault: } ) ).provide(ScopeManager.layer) @@ TestAspect.sequential +end ScopeManagerSpec diff --git a/docs/src/test/scala/marklit/docs/BuildSite.scala b/docs/src/test/scala/marklit/docs/BuildSite.scala index 87d9317..365fb9a 100644 --- a/docs/src/test/scala/marklit/docs/BuildSite.scala +++ b/docs/src/test/scala/marklit/docs/BuildSite.scala @@ -51,5 +51,4 @@ object BuildSite extends DocsSite: SiteError.WriteFailed(index, err) } promote *> EarlyEffectTheme.writeLogo(out) - end afterBuild end BuildSite From 52c2a8f659d4410a5d388fd3ddc294c0b141c5a0 Mon Sep 17 00:00:00 2001 From: Russ White <356303+russwyte@users.noreply.github.com> Date: Sun, 25 Oct 2026 17:01:13 -0500 Subject: [PATCH 2/2] Format marklit to 120 columns. The other repos already set maxColumn = 120. Marklit was on the scalafmt default of 80. --- .scalafmt.conf | 1 + cli/src/main/scala/marklit/cli/Daemon.scala | 51 +++---- .../scala/marklit/cli/DaemonProtocol.scala | 29 ++-- .../main/scala/marklit/cli/MarklitCli.scala | 3 +- .../compiler/shim/NscInvokerImpl.scala | 17 +-- .../compiler/shim/DotcInvokerImpl.scala | 26 ++-- .../compiler/shim/DotcInvokerImplSpec.scala | 22 ++- compiler/src/main/scala/marklit/Marklit.scala | 98 ++++++------- .../src/main/scala/marklit/MarklitRun.scala | 70 ++++------ .../marklit/compiler/ClasspathEntries.scala | 13 +- .../scala/marklit/compiler/Compiler.scala | 24 ++-- .../marklit/compiler/CompilerFactory.scala | 132 +++++++----------- .../marklit/compiler/ScalaCompiler.scala | 77 ++++------ .../src/test/scala/marklit/MarklitSpec.scala | 5 +- .../test/scala/marklit/RunResourceSpec.scala | 56 +++----- .../compiler/ClasspathEntriesSpec.scala | 6 +- .../marklit/compiler/LocalClassWarnSpec.scala | 15 +- .../marklit/compiler/TestCompilerLayer.scala | 5 +- .../compiler/TopLevelCompileSpec.scala | 7 +- .../main/scala/marklit/cache/BlockCache.scala | 65 ++++----- .../main/scala/marklit/model/CodeBlock.scala | 52 +++---- .../scala/marklit/model/CompileResult.scala | 8 +- .../main/scala/marklit/model/Modifier.scala | 10 +- .../scala/marklit/model/ScopeConfig.scala | 18 +-- .../main/scala/marklit/model/ScopeMode.scala | 7 +- .../marklit/parser/InfoStringParser.scala | 5 +- .../scala/marklit/parser/MarkdownParser.scala | 47 +++---- .../marklit/processor/DocumentProcessor.scala | 97 +++++-------- .../marklit/renderer/MarkdownRenderer.scala | 50 +++---- .../marklit/resolver/DependencyResolver.scala | 99 ++++++------- .../scala/marklit/results/ResultsFile.scala | 6 +- .../scala/marklit/scope/ScopeManager.scala | 82 +++++------ .../scala/marklit/model/CodeBlockSpec.scala | 4 +- .../parser/MarkdownParserEdgeCasesSpec.scala | 10 +- .../processor/DocumentProcessorSpec.scala | 22 +-- .../resolver/DependencyResolverSpec.scala | 5 +- .../marklit/results/ResultsFileSpec.scala | 3 +- docs/src/test/scala/marklit/docs/Front.scala | 21 +-- .../scala/marklit/sbt/MarklitInProcess.scala | 18 +-- .../scala/marklit/sbt/MarklitPlugin.scala | 87 +++++------- .../scala/marklit/sbt/MarklitSession.scala | 20 ++- 41 files changed, 555 insertions(+), 838 deletions(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index 66cb10a..2c26d4b 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,5 @@ version = "3.11.5" runner.dialect = scala3 +maxColumn = 120 rewrite.scala3.endMarker.insert.blankGaps.min = 1 rewrite.scala3.endMarker.remove.blankGaps.max = 0 diff --git a/cli/src/main/scala/marklit/cli/Daemon.scala b/cli/src/main/scala/marklit/cli/Daemon.scala index e158774..23f1464 100644 --- a/cli/src/main/scala/marklit/cli/Daemon.scala +++ b/cli/src/main/scala/marklit/cli/Daemon.scala @@ -8,28 +8,24 @@ import java.nio.file.Paths /** Long-lived stdin/stdout JSON-RPC server. * - * Started by `marklit --daemon`. Reads one request per line from stdin, - * dispatches to a handler, writes one response per line to stdout. The loop - * ends on any of: + * Started by `marklit --daemon`. Reads one request per line from stdin, dispatches to a handler, writes one response + * per line to stdout. The loop ends on any of: * - * - stdin closes (parent process death) — primary shutdown trigger; the - * `Console.readLine` effect surfaces it via `IOException`/EOF. - * - idle timeout expires — defense in depth against hung-but-not-dead - * parents. + * - stdin closes (parent process death) — primary shutdown trigger; the `Console.readLine` effect surfaces it via + * `IOException`/EOF. + * - idle timeout expires — defense in depth against hung-but-not-dead parents. * - explicit `shutdown` RPC. */ object Daemon: - /** 15 minutes — long enough for sbt to come back from a `~test` cycle, short - * enough that an orphaned daemon doesn't sit forever. + /** 15 minutes — long enough for sbt to come back from a `~test` cycle, short enough that an orphaned daemon doesn't + * sit forever. */ val defaultIdleTimeout: Duration = 15.minutes - /** The original stdout FileDescriptor, captured before we redirect - * `System.out` to stderr. The JSON-RPC reply path writes through this - * reference so user-pipeline `Console.printLine`/`println` calls — which now - * go through the redirected `System.out` — can't corrupt the wire format no - * matter what classpath or library does the printing. + /** The original stdout FileDescriptor, captured before we redirect `System.out` to stderr. The JSON-RPC reply path + * writes through this reference so user-pipeline `Console.printLine`/`println` calls — which now go through the + * redirected `System.out` — can't corrupt the wire format no matter what classpath or library does the printing. */ private lazy val protocolOut: java.io.PrintStream = new java.io.PrintStream( @@ -76,19 +72,15 @@ object Daemon: .ensuring(ZIO.succeed(java.lang.Runtime.getRuntime.halt(0))) end run - /** Race [[serveOnce]] against an idle timer. Whichever fiber lands first - * publishes its outcome to a Promise; the loser is fire-and-forget - * interrupted. + /** Race [[serveOnce]] against an idle timer. Whichever fiber lands first publishes its outcome to a Promise; the + * loser is fire-and-forget interrupted. * - * Why a Promise instead of `serveOnce.race(timer)`: the reader fiber's - * `Console.readLine` ultimately blocks in `System.in.read()` on a non-daemon - * JVM thread that the kernel can't be interrupted out of. `race` (and - * `raceFirst`, and `join.race(join)`) all want to either await the loser's - * interruption or observe both completions in lockstep — empirically that - * hangs when the loser is wedged in a syscall, even if the winner has - * already produced a result. Going through `await` (which yields an `Exit` - * and never throws) and feeding the first landing into a Promise - * short-circuits that. + * Why a Promise instead of `serveOnce.race(timer)`: the reader fiber's `Console.readLine` ultimately blocks in + * `System.in.read()` on a non-daemon JVM thread that the kernel can't be interrupted out of. `race` (and + * `raceFirst`, and `join.race(join)`) all want to either await the loser's interruption or observe both completions + * in lockstep — empirically that hangs when the loser is wedged in a syscall, even if the winner has already + * produced a result. Going through `await` (which yields an `Exit` and never throws) and feeding the first landing + * into a Promise short-circuits that. */ private def serveOneRequest( idleTimeout: Duration @@ -116,10 +108,9 @@ object Daemon: private enum LoopOutcome: case Continue, Stop - /** Read one request, handle it, write one response. Returns - * [[LoopOutcome.Continue]] for normal requests, [[LoopOutcome.Stop]] for - * `shutdown`. EOF on stdin (parent closed our pipe) surfaces as an - * `EOFException` that the outer `run` catches. + /** Read one request, handle it, write one response. Returns [[LoopOutcome.Continue]] for normal requests, + * [[LoopOutcome.Stop]] for `shutdown`. EOF on stdin (parent closed our pipe) surfaces as an `EOFException` that the + * outer `run` catches. */ private def serveOnce: ZIO[Any, Throwable, LoopOutcome] = Console.readLine.flatMap { line => diff --git a/cli/src/main/scala/marklit/cli/DaemonProtocol.scala b/cli/src/main/scala/marklit/cli/DaemonProtocol.scala index bfdd351..f052f22 100644 --- a/cli/src/main/scala/marklit/cli/DaemonProtocol.scala +++ b/cli/src/main/scala/marklit/cli/DaemonProtocol.scala @@ -4,23 +4,19 @@ import zio.json.* /** JSON-RPC protocol for marklit's daemon mode. * - * One request per line over stdin, one response per line over stdout. The - * structure is intentionally close to [[MarklitOptions]] so a daemon request - * is roughly equivalent to "the CLI options for one invocation." Fields that - * only make sense at the CLI level (e.g. `daemon`, `idle-timeout`, `watch`) - * are not part of the protocol. + * One request per line over stdin, one response per line over stdout. The structure is intentionally close to + * [[MarklitOptions]] so a daemon request is roughly equivalent to "the CLI options for one invocation." Fields that + * only make sense at the CLI level (e.g. `daemon`, `idle-timeout`, `watch`) are not part of the protocol. * * Methods: - * - `compile-document` — process one or more markdown files; returns a - * [[CompileResponse]] with per-file success. - * - `clear-cache` — placeholder for the disk cache landing in Phase B. - * Currently a no-op that returns `ok`. + * - `compile-document` — process one or more markdown files; returns a [[CompileResponse]] with per-file success. + * - `clear-cache` — placeholder for the disk cache landing in Phase B. Currently a no-op that returns `ok`. * - `shutdown` — daemon writes an ack and exits its loop. */ object DaemonProtocol: - /** Tag-discriminated request union. zio-json's `@jsonDiscriminator` lets us - * use a sum-of-records ADT without writing AST-level codecs. + /** Tag-discriminated request union. zio-json's `@jsonDiscriminator` lets us use a sum-of-records ADT without writing + * AST-level codecs. */ @jsonDiscriminator("method") sealed trait Request @@ -35,15 +31,15 @@ object DaemonProtocol: given JsonCodec[Request] = DeriveJsonCodec.gen[Request] end Request - /** Parameters for the `clear-cache` RPC. The daemon doesn't keep cache state - * between requests; the caller tells it which directory to clear. + /** Parameters for the `clear-cache` RPC. The daemon doesn't keep cache state between requests; the caller tells it + * which directory to clear. */ final case class ClearCacheParams(cacheDir: String) object ClearCacheParams: given JsonCodec[ClearCacheParams] = DeriveJsonCodec.gen[ClearCacheParams] - /** A single compile request. Mirrors the subset of [[MarklitOptions]] that - * makes sense over the wire. Paths are strings (the daemon resolves them). + /** A single compile request. Mirrors the subset of [[MarklitOptions]] that makes sense over the wire. Paths are + * strings (the daemon resolves them). */ final case class CompileParams( inputFiles: List[String], @@ -69,8 +65,7 @@ object DaemonProtocol: sealed trait Response object Response: @jsonHint("ok") - final case class Ok(method: String, result: Option[CompileResponse] = None) - extends Response + final case class Ok(method: String, result: Option[CompileResponse] = None) extends Response @jsonHint("error") final case class Err(method: String, message: String) extends Response diff --git a/cli/src/main/scala/marklit/cli/MarklitCli.scala b/cli/src/main/scala/marklit/cli/MarklitCli.scala index b9eb2e3..8b28b13 100644 --- a/cli/src/main/scala/marklit/cli/MarklitCli.scala +++ b/cli/src/main/scala/marklit/cli/MarklitCli.scala @@ -251,8 +251,7 @@ object MarklitCli extends ZIOCliDefault: effect.as(options) } - /** Split a `--classpath`-style flag value into entries (colon/semicolon - * separated), or empty when absent. + /** Split a `--classpath`-style flag value into entries (colon/semicolon separated), or empty when absent. */ private def splitClasspath(cp: Option[String]): Vector[String] = cp.map(_.split("[;:]").toVector).getOrElse(Vector.empty) diff --git a/compiler-shim-2/src/main/scala/marklit/compiler/shim/NscInvokerImpl.scala b/compiler-shim-2/src/main/scala/marklit/compiler/shim/NscInvokerImpl.scala index 7827d20..b73e927 100644 --- a/compiler-shim-2/src/main/scala/marklit/compiler/shim/NscInvokerImpl.scala +++ b/compiler-shim-2/src/main/scala/marklit/compiler/shim/NscInvokerImpl.scala @@ -1,25 +1,18 @@ package marklit.compiler.shim -import marklit.compiler.api.{ - CompileRequest, - CompileResponse, - Diag, - DotcInvoker, - Severity -} +import marklit.compiler.api.{CompileRequest, CompileResponse, Diag, DotcInvoker, Severity} import scala.collection.mutable.ArrayBuffer import scala.jdk.CollectionConverters._ import scala.tools.nsc.reporters.StoreReporter import scala.tools.nsc.{Global, Settings} -/** The 2.13 sibling of `DotcInvokerImpl`. The ONLY class in marklit that - * imports `scala.tools.nsc.*`. Compiled into a small jar bundled as a CLI - * resource and loaded at runtime from a per-version URLClassLoader holding the +/** The 2.13 sibling of `DotcInvokerImpl`. The ONLY class in marklit that imports `scala.tools.nsc.*`. Compiled into a + * small jar bundled as a CLI resource and loaded at runtime from a per-version URLClassLoader holding the * user-requested `scala-compiler:2.13.x`. * - * Reuses the version-neutral `DotcInvoker` interface — the name is dotc- - * historical but the surface fits both compilers. + * Reuses the version-neutral `DotcInvoker` interface — the name is dotc- historical but the surface fits both + * compilers. */ final class NscInvokerImpl extends DotcInvoker { diff --git a/compiler-shim/src/main/scala/marklit/compiler/shim/DotcInvokerImpl.scala b/compiler-shim/src/main/scala/marklit/compiler/shim/DotcInvokerImpl.scala index c38ba12..610ca44 100644 --- a/compiler-shim/src/main/scala/marklit/compiler/shim/DotcInvokerImpl.scala +++ b/compiler-shim/src/main/scala/marklit/compiler/shim/DotcInvokerImpl.scala @@ -3,24 +3,16 @@ package marklit.compiler.shim import dotty.tools.dotc.Driver import dotty.tools.dotc.core.Contexts.Context import dotty.tools.dotc.reporting.{Diagnostic, Reporter} -import marklit.compiler.api.{ - CompileRequest, - CompileResponse, - Diag, - DotcInvoker, - Severity -} +import marklit.compiler.api.{CompileRequest, CompileResponse, Diag, DotcInvoker, Severity} import scala.collection.mutable.ArrayBuffer import scala.jdk.CollectionConverters.* -/** The shim implementation. This is the ONLY class in marklit that imports - * `dotty.tools.dotc.*`. It is compiled into a small jar that travels with the - * CLI as a resource, and at runtime it is loaded from a custom URLClassLoader +/** The shim implementation. This is the ONLY class in marklit that imports `dotty.tools.dotc.*`. It is compiled into a + * small jar that travels with the CLI as a resource, and at runtime it is loaded from a custom URLClassLoader * containing the user-requested `scala3-compiler` version. * - * The class must be public and have a no-arg constructor so the orchestrator - * can instantiate it via + * The class must be public and have a no-arg constructor so the orchestrator can instantiate it via * `Class.forName(...).getDeclaredConstructor().newInstance()`. */ final class DotcInvokerImpl extends DotcInvoker: @@ -60,9 +52,8 @@ final class DotcInvokerImpl extends DotcInvoker: end DotcInvokerImpl -/** Diagnostic record local to the shim. Kept Scala-native for ergonomic - * collection here; we convert to the version-neutral `Diag` POJO at the - * boundary in `compile`. +/** Diagnostic record local to the shim. Kept Scala-native for ergonomic collection here; we convert to the + * version-neutral `Diag` POJO at the boundary in `compile`. */ private final case class ShimDiag( severity: Severity, @@ -74,9 +65,8 @@ private final case class ShimDiag( /** Reporter that collects every diagnostic dotc emits during compilation. * - * Lives in the shim because it extends a dotc class — the JVM resolves - * `Reporter` (and `Diagnostic`, `Context`) through this class's defining - * classloader, which contains the per-version `scala3-compiler` jar. + * Lives in the shim because it extends a dotc class — the JVM resolves `Reporter` (and `Diagnostic`, `Context`) + * through this class's defining classloader, which contains the per-version `scala3-compiler` jar. */ private final class CollectingReporter extends Reporter: private val _diagnostics = ArrayBuffer.empty[ShimDiag] diff --git a/compiler-shim/src/test/scala/marklit/compiler/shim/DotcInvokerImplSpec.scala b/compiler-shim/src/test/scala/marklit/compiler/shim/DotcInvokerImplSpec.scala index 98e4c63..719f28e 100644 --- a/compiler-shim/src/test/scala/marklit/compiler/shim/DotcInvokerImplSpec.scala +++ b/compiler-shim/src/test/scala/marklit/compiler/shim/DotcInvokerImplSpec.scala @@ -8,20 +8,17 @@ import zio.test.* import java.nio.file.{Files, Path} import scala.jdk.CollectionConverters.* -/** Integration tests for the shim. The shim is the only place dotty.* is - * touched, so these tests are the only place we verify a real compilation - * against a real dotc — every other module mocks at the `Compiler` trait. +/** Integration tests for the shim. The shim is the only place dotty.* is touched, so these tests are the only place we + * verify a real compilation against a real dotc — every other module mocks at the `Compiler` trait. * - * The shim is exercised here within sbt's test classloader, which has the - * project's compile-time `scala3-compiler` on it. Cross-version loading - * happens later, in CompilerFactory's tests. + * The shim is exercised here within sbt's test classloader, which has the project's compile-time `scala3-compiler` on + * it. Cross-version loading happens later, in CompilerFactory's tests. */ object DotcInvokerImplSpec extends ZIOSpecDefault: - /** Resolve scala3-library + scala-library jars via Coursier, against the - * version the shim was compiled with. This is the same approach the - * production code (CompilerFactory) takes — keeping the test honest about - * the same boundary the real classpath construction uses. + /** Resolve scala3-library + scala-library jars via Coursier, against the version the shim was compiled with. This is + * the same approach the production code (CompilerFactory) takes — keeping the test honest about the same boundary + * the real classpath construction uses. */ private lazy val scalaLibraryJars: Vector[String] = val version = new DotcInvokerImpl().compilerVersion() @@ -35,9 +32,8 @@ object DotcInvokerImplSpec extends ZIOSpecDefault: .toVector .map(_.getAbsolutePath) - /** Allocate a writable temp dir and a `.scala` source file with the given - * content, then build a CompileRequest pointing at them. Returns the request - * plus the temp dir so the caller can clean up. + /** Allocate a writable temp dir and a `.scala` source file with the given content, then build a CompileRequest + * pointing at them. Returns the request plus the temp dir so the caller can clean up. */ private def mkRequest(source: String): (CompileRequest, Path) = val tmp = Files.createTempDirectory("marklit-shim-test-") diff --git a/compiler/src/main/scala/marklit/Marklit.scala b/compiler/src/main/scala/marklit/Marklit.scala index 332b357..33fbb8c 100644 --- a/compiler/src/main/scala/marklit/Marklit.scala +++ b/compiler/src/main/scala/marklit/Marklit.scala @@ -58,30 +58,26 @@ object Marklit: ): ZIO[Marklit, MarklitError, Vector[MarklitResult]] = ZIO.serviceWithZIO[Marklit](_.processFiles(paths)) - /** Create a layer with a fixed default Compiler. Per-block version requests - * cannot resolve to a different compiler in this mode — the block will be - * compiled by the default compiler and may fail if the requested version - * differs significantly. + /** Create a layer with a fixed default Compiler. Per-block version requests cannot resolve to a different compiler in + * this mode — the block will be compiled by the default compiler and may fail if the requested version differs + * significantly. * - * Prefer [[layer]] for production, which wires in a [[CompilerFactory]] - * capable of materializing arbitrary Scala 3 versions on demand. + * Prefer [[layer]] for production, which wires in a [[CompilerFactory]] capable of materializing arbitrary Scala 3 + * versions on demand. */ def live: ZLayer[Compiler, Nothing, Marklit] = ZLayer.fromFunction { (compiler: Compiler) => MarklitLive(CompilerServiceAdapter.fixed(compiler), ScopeMode.Isolated) } - /** Create a layer using a pre-built [[CompilerFactory]] and a default - * version. Per-block specific-version requests are resolved against the - * factory. + /** Create a layer using a pre-built [[CompilerFactory]] and a default version. Per-block specific-version requests + * are resolved against the factory. * * @param majorClasspaths - * per-major classpath overrides: when a cross-version block requests Scala - * major `m`, the adapter looks up `majorClasspaths(m)` and forwards that - * to the factory's `forVersion`. When no entry exists for the requested - * major, the cross-version compiler runs with no extra classpath (the safe - * default — see `CompilerServiceAdapter.compilerFor` for why we don't - * reuse the default-major classpath). + * per-major classpath overrides: when a cross-version block requests Scala major `m`, the adapter looks up + * `majorClasspaths(m)` and forwards that to the factory's `forVersion`. When no entry exists for the requested + * major, the cross-version compiler runs with no extra classpath (the safe default — see + * `CompilerServiceAdapter.compilerFor` for why we don't reuse the default-major classpath). */ def liveWithFactory( defaultScalaVersion: String, @@ -117,10 +113,8 @@ object Marklit: yield MarklitLive(adapter, scopeMode) } - /** Fully configured layer that resolves a default-version compiler via - * [[CompilerFactory]]. The factory caches by version, so when a document - * opts into a specific version the same factory instance returns the right - * compiler. + /** Fully configured layer that resolves a default-version compiler via [[CompilerFactory]]. The factory caches by + * version, so when a document opts into a specific version the same factory instance returns the right compiler. */ val layer: ZLayer[Any, Throwable, Marklit] = CompilerFactory.layer >>> liveWithFactory( @@ -131,18 +125,15 @@ end Marklit /** Adapter from Compiler(s) to CompilerService. * * Two flavors: - * - [[fixed]] uses one Compiler for everything; per-block version requests - * are ignored. - * - [[fromFactory]] holds a default compiler and a factory; per-block - * specific versions are resolved through the factory. + * - [[fixed]] uses one Compiler for everything; per-block version requests are ignored. + * - [[fromFactory]] holds a default compiler and a factory; per-block specific versions are resolved through the + * factory. * - * Cache integration: if a non-noop [[BlockCache]] is supplied and the caller - * passes a [[Location]] with the request, we hash every input that affects raw - * compiler output (block code, prior code, effective version, classpath, - * scalac options, ZIO-app flag, source location) and consult the cache before - * invoking the underlying compiler. Missing entries are populated on - * write-back. Classpath jar content hashes are pre-computed once at adapter - * construction (per-bucket) so per-block hashing stays cheap. + * Cache integration: if a non-noop [[BlockCache]] is supplied and the caller passes a [[Location]] with the request, + * we hash every input that affects raw compiler output (block code, prior code, effective version, classpath, scalac + * options, ZIO-app flag, source location) and consult the cache before invoking the underlying compiler. Missing + * entries are populated on write-back. Classpath jar content hashes are pre-computed once at adapter construction + * (per-bucket) so per-block hashing stays cheap. */ private final class CompilerServiceAdapter( defaultCompiler: Compiler, @@ -157,11 +148,9 @@ private final class CompilerServiceAdapter( override def defaultScalaVersion: String = defaultCompiler.scalaVersion - /** Per-major defaults used to resolve bare-major requests (`scala=2`, - * `scala=3`). When the major matches the service's own default, that wins - * (handled by the trait's base implementation). Otherwise we fall back to - * the bundled shim version for the requested major — `defaultScalaVersion` - * for `3`, `defaultScala2Version` for `2`. + /** Per-major defaults used to resolve bare-major requests (`scala=2`, `scala=3`). When the major matches the + * service's own default, that wins (handled by the trait's base implementation). Otherwise we fall back to the + * bundled shim version for the requested major — `defaultScalaVersion` for `3`, `defaultScala2Version` for `2`. */ override def defaultVersionForMajor(major: String): Option[String] = super.defaultVersionForMajor(major).orElse { @@ -175,7 +164,7 @@ private final class CompilerServiceAdapter( requested: Option[String] ): IO[MarklitError, Compiler] = requested match - case None => ZIO.succeed(defaultCompiler) + case None => ZIO.succeed(defaultCompiler) case Some(v) if v == defaultCompiler.scalaVersion => ZIO.succeed(defaultCompiler) case Some(v) => @@ -195,12 +184,10 @@ private final class CompilerServiceAdapter( /** Build the per-block ScopeContext. * - * The output marker MUST be deterministic from the block's inputs: it is - * baked into the compiled class files at compile time (as a `print(marker)` - * call ahead of the user code) and then matched at execute time to strip - * prior-code replay output. Compile and execute go through separate adapter - * calls — if the marker were a fresh `UUID.randomUUID()` each time, the - * execute-time marker would never match the one in the .class files, and the + * The output marker MUST be deterministic from the block's inputs: it is baked into the compiled class files at + * compile time (as a `print(marker)` call ahead of the user code) and then matched at execute time to strip + * prior-code replay output. Compile and execute go through separate adapter calls — if the marker were a fresh + * `UUID.randomUUID()` each time, the execute-time marker would never match the one in the .class files, and the * marker would leak verbatim into the rendered output. */ private def buildContext( @@ -215,8 +202,7 @@ private final class CompilerServiceAdapter( // file scope. Otherwise mark only when there's prior run-body code to skip. val marker = if topLevel then None - else if priorCode.nonEmpty then - Some(blockMarker(code, priorCode, isZIOApp, topLevelPriorCode)) + else if priorCode.nonEmpty then Some(blockMarker(code, priorCode, isZIOApp, topLevelPriorCode)) else None ScopeContext( priorCode = priorCode, @@ -250,21 +236,20 @@ private final class CompilerServiceAdapter( /** Pick the classpath + jar-content-hashes bucket appropriate to a request. * - No version requested → default classpath/hashes. - * - Same major as default → default classpath/hashes (the per-major bucket - * is for a *different* major than this service's default). + * - Same major as default → default classpath/hashes (the per-major bucket is for a *different* major than this + * service's default). * - Otherwise → per-major bucket if configured, empty otherwise. */ private def classpathFor( requested: Option[String] ): (Vector[String], Vector[String]) = requested match - case None => (defaultClasspath, defaultClasspathHashes) + case None => (defaultClasspath, defaultClasspathHashes) case Some(v) if v == defaultCompiler.scalaVersion => (defaultClasspath, defaultClasspathHashes) case Some(v) => val major = v.takeWhile(_ != '.') - if major == defaultCompiler.scalaVersion.takeWhile(_ != '.') then - (defaultClasspath, defaultClasspathHashes) + if major == defaultCompiler.scalaVersion.takeWhile(_ != '.') then (defaultClasspath, defaultClasspathHashes) else ( majorClasspaths.getOrElse(major, Vector.empty), @@ -369,16 +354,13 @@ private object CompilerServiceAdapter: majorHashes ) - /** Hash each classpath entry's bytes (jar or class file) once at adapter - * construction so per-block cache keys don't have to re-read jars off disk - * on every block. Directories are hashed by listing their files and - * concatenating their (relative-path, size, mtime) triples — a cheap - * approximation that catches the cases we care about (recompilation changes - * class files' mtimes; adding a class file extends the listing). + /** Hash each classpath entry's bytes (jar or class file) once at adapter construction so per-block cache keys don't + * have to re-read jars off disk on every block. Directories are hashed by listing their files and concatenating + * their (relative-path, size, mtime) triples — a cheap approximation that catches the cases we care about + * (recompilation changes class files' mtimes; adding a class file extends the listing). * - * We deliberately use the path string as the fallback when a file can't be - * hashed (missing, permission, unusual): this still varies the key if the - * user's classpath layout changes, while never blowing up the build. + * We deliberately use the path string as the fallback when a file can't be hashed (missing, permission, unusual): + * this still varies the key if the user's classpath layout changes, while never blowing up the build. */ private def hashClasspath(entries: Vector[String]): Vector[String] = entries.map { entry => diff --git a/compiler/src/main/scala/marklit/MarklitRun.scala b/compiler/src/main/scala/marklit/MarklitRun.scala index 467744b..a8b3ad7 100644 --- a/compiler/src/main/scala/marklit/MarklitRun.scala +++ b/compiler/src/main/scala/marklit/MarklitRun.scala @@ -10,11 +10,9 @@ import java.nio.file.{Files, Path} /** Configuration for an end-to-end marklit run over a set of markdown files. * - * This is the CLI-independent superset of the options the CLI used to thread - * through command-line flags. Classpaths arrive already split into entries - * (the CLI does the `[;:]` splitting before constructing this); `watch` and - * `daemon` have no place here — they are concerns of the caller's process - * model, not of the per-run orchestration. + * This is the CLI-independent superset of the options the CLI used to thread through command-line flags. Classpaths + * arrive already split into entries (the CLI does the `[;:]` splitting before constructing this); `watch` and `daemon` + * have no place here — they are concerns of the caller's process model, not of the per-run orchestration. */ final case class MarklitRunConfig( inputFiles: Vector[Path], @@ -34,9 +32,8 @@ final case class MarklitRunConfig( runResourceClass: Option[String] = None ) -/** Per-file outcome with no Console side effects. `rendered` is populated when - * an output directory is configured and the file is not in check mode (so a - * caller can choose to write it itself); `outputPath` is where it would land. +/** Per-file outcome with no Console side effects. `rendered` is populated when an output directory is configured and + * the file is not in check mode (so a caller can choose to write it itself); `outputPath` is where it would land. */ final case class MarklitFileReport( sourceFile: Path, @@ -48,11 +45,9 @@ final case class MarklitFileReport( outputPath: Option[Path] ) -/** Structured result of a run. `notices` are ordered informational lines the - * caller may surface verbatim (the facade has already applied the verbose gate - * when deciding which lines to include). Compile failures are reported as data - * via [[MarklitFileReport.success]]; only file-read/parse/resolution errors - * fail the effect. +/** Structured result of a run. `notices` are ordered informational lines the caller may surface verbatim (the facade + * has already applied the verbose gate when deciding which lines to include). Compile failures are reported as data + * via [[MarklitFileReport.success]]; only file-read/parse/resolution errors fail the effect. */ final case class MarklitRunResult( files: Vector[MarklitFileReport], @@ -61,17 +56,15 @@ final case class MarklitRunResult( def success: Boolean = files.forall(_.success) def failedCount: Int = files.count(!_.success) -/** The shared, side-effect-free orchestration that drives marklit over a set of - * files: parse `//> using` directives, aggregate and resolve dependencies - * once, build per-major classpaths, process each file through [[Marklit]], - * render and optionally write outputs. Both the CLI and the build-tool plugins - * call this so the multi-file logic lives in exactly one place. +/** The shared, side-effect-free orchestration that drives marklit over a set of files: parse `//> using` directives, + * aggregate and resolve dependencies once, build per-major classpaths, process each file through [[Marklit]], render + * and optionally write outputs. Both the CLI and the build-tool plugins call this so the multi-file logic lives in + * exactly one place. */ object MarklitRun: - /** Run with a freshly-built [[CompilerFactory]] (the CLI path). The factory - * is built once and shared across all files, so per-version compilers are - * reused within the run. + /** Run with a freshly-built [[CompilerFactory]] (the CLI path). The factory is built once and shared across all + * files, so per-version compilers are reused within the run. */ def run( config: MarklitRunConfig, @@ -87,9 +80,8 @@ object MarklitRun: .flatMap(env => runWith(config, env.get[CompilerFactory], writeOutputs)) } - /** Run with a caller-supplied, long-lived [[CompilerFactory]] (the plugin - * path). The factory's per-version cache survives across invocations, - * keeping compilers warm — the in-process replacement for the old daemon. + /** Run with a caller-supplied, long-lived [[CompilerFactory]] (the plugin path). The factory's per-version cache + * survives across invocations, keeping compilers warm — the in-process replacement for the old daemon. */ def runWith( config: MarklitRunConfig, @@ -261,21 +253,17 @@ object MarklitRun: noticeLines <- notices.get yield MarklitRunResult(reports, noticeLines.toVector) - /** Acquire the build-provided run resource (when configured) for the duration - * of the enclosing [[Scope]]. A no-op when `config.runResourceClass` is - * unset. + /** Acquire the build-provided run resource (when configured) for the duration of the enclosing [[Scope]]. A no-op + * when `config.runResourceClass` is unset. * - * The resource is a user class on the docs' own classpath implementing the - * JDK type `java.util.function.Supplier[AutoCloseable]`: `get()` performs - * setup (start a DB container, create a schema, …) and returns an - * `AutoCloseable` whose `close()` is the teardown. Using only JDK types - * keeps the instance usable across marklit's classloader boundary — see - * [[CompilerFactory.userClassLoader]]. + * The resource is a user class on the docs' own classpath implementing the JDK type + * `java.util.function.Supplier[AutoCloseable]`: `get()` performs setup (start a DB container, create a schema, …) + * and returns an `AutoCloseable` whose `close()` is the teardown. Using only JDK types keeps the instance usable + * across marklit's classloader boundary — see [[CompilerFactory.userClassLoader]]. * - * Both `get()` and `close()` are best-effort with respect to the run: a - * setup failure is surfaced as a notice and lets the run proceed (the docs - * that need the resource will fail on their own and report it); a teardown - * failure is logged and swallowed so it never masks the run's real result. + * Both `get()` and `close()` are best-effort with respect to the run: a setup failure is surfaced as a notice and + * lets the run proceed (the docs that need the resource will fail on their own and report it); a teardown failure is + * logged and swallowed so it never masks the run's real result. */ private def acquireRunResource( config: MarklitRunConfig, @@ -312,15 +300,13 @@ object MarklitRun: ) ) .foldZIO( - e => - addNote(s"run resource '$fqn' setup failed: ${e.getMessage}"), + e => addNote(s"run resource '$fqn' setup failed: ${e.getMessage}"), _ => ZIO.unit ) yield Some(loader) - /** Build a side-effect-free per-file report from a [[MarklitResult]], - * rendering output markdown when an output dir is configured and the run is - * not in check mode. + /** Build a side-effect-free per-file report from a [[MarklitResult]], rendering output markdown when an output dir is + * configured and the run is not in check mode. */ private def buildReport( result: MarklitResult, diff --git a/compiler/src/main/scala/marklit/compiler/ClasspathEntries.scala b/compiler/src/main/scala/marklit/compiler/ClasspathEntries.scala index cc60445..035091d 100644 --- a/compiler/src/main/scala/marklit/compiler/ClasspathEntries.scala +++ b/compiler/src/main/scala/marklit/compiler/ClasspathEntries.scala @@ -5,12 +5,10 @@ import java.util.concurrent.ConcurrentHashMap /** Normalize classpath entries for scalac / dotc. * - * Those compilers only treat regular files ending in `.jar` / `.jmod` / `.zip` - * (or directories) as archives. sbt 2.0.4's content-addressed store - * materializes jars as extensionless CAS files (`…/sha256-…-`), which - * URL classloaders accept but scalac silently skips — producing "Not found" - * errors for classes that are on the JVM classpath. Remap those to a temp - * `*.jar` link (or copy). + * Those compilers only treat regular files ending in `.jar` / `.jmod` / `.zip` (or directories) as archives. sbt + * 2.0.4's content-addressed store materializes jars as extensionless CAS files (`…/sha256-…-`), which URL + * classloaders accept but scalac silently skips — producing "Not found" errors for classes that are on the JVM + * classpath. Remap those to a temp `*.jar` link (or copy). */ private[marklit] object ClasspathEntries: @@ -58,8 +56,7 @@ private[marklit] object ClasspathEntries: val dest = dir.resolve(source.getFileName.toString + ".jar") try Files.createSymbolicLink(dest, source) catch - case _: UnsupportedOperationException | - _: java.nio.file.FileSystemException => + case _: UnsupportedOperationException | _: java.nio.file.FileSystemException => Files.copy(source, dest, StandardCopyOption.REPLACE_EXISTING) dest.toFile.deleteOnExit() dest.toAbsolutePath.toString diff --git a/compiler/src/main/scala/marklit/compiler/Compiler.scala b/compiler/src/main/scala/marklit/compiler/Compiler.scala index 1969fdd..646ac76 100644 --- a/compiler/src/main/scala/marklit/compiler/Compiler.scala +++ b/compiler/src/main/scala/marklit/compiler/Compiler.scala @@ -9,13 +9,10 @@ final case class ScopeContext( priorCode: Vector[String] = Vector.empty, classpath: Vector[String] = Vector.empty, scalacOptions: Vector[String] = Vector.empty, - outputMarker: Option[String] = - None, // UUID marker to identify where new output starts + outputMarker: Option[String] = None, // UUID marker to identify where new output starts isZIOApp: Boolean = false, // Wrap in ZIOAppDefault instead of plain object - topLevel: Boolean = - false, // Compile this block verbatim as its own compilation unit - topLevelPriorCode: Vector[String] = - Vector.empty // Inherited definitions hoisted ABOVE the wrapper + topLevel: Boolean = false, // Compile this block verbatim as its own compilation unit + topLevelPriorCode: Vector[String] = Vector.empty // Inherited definitions hoisted ABOVE the wrapper ): def append(code: String): ScopeContext = copy(priorCode = priorCode :+ code) @@ -23,9 +20,8 @@ final case class ScopeContext( def allCode: String = priorCode.mkString("\n\n") - /** Inherited top-level definitions, joined, to emit at file scope (above the - * `MarklitWrapper` object for normal blocks, or as the whole unit for - * top-level blocks). + /** Inherited top-level definitions, joined, to emit at file scope (above the `MarklitWrapper` object for normal + * blocks, or as the whole unit for top-level blocks). */ def hoistedCode: String = topLevelPriorCode.mkString("\n\n") @@ -45,9 +41,8 @@ trait Compiler: context: ScopeContext ): IO[MarklitError, CompileResult] - /** Execute previously compiled code and capture output. Implementations may - * recompile internally if they don't carry the per-block class files around - * — prefer [[executeFromDir]] when you already have a + /** Execute previously compiled code and capture output. Implementations may recompile internally if they don't carry + * the per-block class files around — prefer [[executeFromDir]] when you already have a * [[CompileResult.classFilesDir]] in hand. */ def execute( @@ -55,9 +50,8 @@ trait Compiler: context: ScopeContext ): IO[MarklitError, ExecutionResult] - /** Execute the wrapper class loaded from [[classFilesDir]] directly, without - * recompiling. The directory must be the one returned by a prior successful - * [[compile]] (or restored from the cache). + /** Execute the wrapper class loaded from [[classFilesDir]] directly, without recompiling. The directory must be the + * one returned by a prior successful [[compile]] (or restored from the cache). */ def executeFromDir( classFilesDir: java.nio.file.Path, diff --git a/compiler/src/main/scala/marklit/compiler/CompilerFactory.scala b/compiler/src/main/scala/marklit/compiler/CompilerFactory.scala index 8809ce3..dad2e0e 100644 --- a/compiler/src/main/scala/marklit/compiler/CompilerFactory.scala +++ b/compiler/src/main/scala/marklit/compiler/CompilerFactory.scala @@ -7,38 +7,33 @@ import zio.* import java.net.{URL, URLClassLoader} import java.nio.file.{Files, Path, StandardCopyOption} -/** Builds Compilers for arbitrary Scala 3 versions by resolving the requested - * `scala3-compiler` jars at runtime and invoking dotc reflectively across a - * per-version classloader. +/** Builds Compilers for arbitrary Scala 3 versions by resolving the requested `scala3-compiler` jars at runtime and + * invoking dotc reflectively across a per-version classloader. * - * The marklit JVM never directly imports `dotty.tools.*`. Instead, the shim - * (compiled into `marklit-compiler-shim.jar` and bundled as a CLI resource) - * implements a small Java-friendly interface, [[DotcInvoker]]. This factory: + * The marklit JVM never directly imports `dotty.tools.*`. Instead, the shim (compiled into `marklit-compiler-shim.jar` + * and bundled as a CLI resource) implements a small Java-friendly interface, [[DotcInvoker]]. This factory: * - * 1. Coursier-resolves `scala3-compiler_3:` for the requested - * version (cached by version). - * 2. Builds a [[URLClassLoader]] whose URLs are `[shim.jar, ...compiler - * jars]` and whose parent only exposes `marklit.compiler.api.*` and - * `java.*` from the host loader. - * 3. Reflectively constructs `marklit.compiler.shim.DotcInvokerImpl` from - * that loader and casts it to [[DotcInvoker]]. - * 4. Wraps the invoker in a [[ScalaCompiler]] bound to the same classpath so - * user code executes against the matching scala3-library at runtime. + * 1. Coursier-resolves `scala3-compiler_3:` for the requested version (cached by version). + * 2. Builds a [[URLClassLoader]] whose URLs are `[shim.jar, ...compiler jars]` and whose parent only exposes + * `marklit.compiler.api.*` and `java.*` from the host loader. + * 3. Reflectively constructs `marklit.compiler.shim.DotcInvokerImpl` from that loader and casts it to + * [[DotcInvoker]]. + * 4. Wraps the invoker in a [[ScalaCompiler]] bound to the same classpath so user code executes against the matching + * scala3-library at runtime. */ trait CompilerFactory: - /** Get a [[Compiler]] for the requested Scala 3 version. Repeated calls for - * the same version share the underlying classloader and resolved jars. + /** Get a [[Compiler]] for the requested Scala 3 version. Repeated calls for the same version share the underlying + * classloader and resolved jars. * * @param scalaVersion - * Full Scala 3 version (e.g., "3.7.0"). Bare-major requests like "3" must - * be resolved by the caller before reaching the factory. + * Full Scala 3 version (e.g., "3.7.0"). Bare-major requests like "3" must be resolved by the caller before + * reaching the factory. * @param extraClasspath - * Additional jars to put on the user-code compile/runtime classpath (e.g., - * resolved dependencies from `//> using dep` directives or the CLI's - * `--classpath`). + * Additional jars to put on the user-code compile/runtime classpath (e.g., resolved dependencies from + * `//> using dep` directives or the CLI's `--classpath`). * @param scalacOptions - * Compiler options to apply to every block compiled by the returned - * compiler (e.g., `-deprecation` from `//> using option`). + * Compiler options to apply to every block compiled by the returned compiler (e.g., `-deprecation` from + * `//> using option`). */ def forVersion( scalaVersion: String, @@ -48,15 +43,12 @@ trait CompilerFactory: shareUserClasses: Boolean = false ): UIO[Compiler] - /** A classloader that can load *user* classes (`extraClasspath`) against the - * per-version compiler stdlib — the same parent loader block execution uses - * ([[ScalaCompiler.executeCompiled]]). + /** A classloader that can load *user* classes (`extraClasspath`) against the per-version compiler stdlib — the same + * parent loader block execution uses ([[ScalaCompiler.executeCompiled]]). * - * Used to instantiate a build-provided run resource (a - * `java.util.function.Supplier[AutoCloseable]`) once per run, outside the - * per-block execution path. The returned loader is scoped: it is closed when - * the surrounding [[Scope]] closes, so callers acquire it inside the run's - * `ZIO.scoped` boundary. + * Used to instantiate a build-provided run resource (a `java.util.function.Supplier[AutoCloseable]`) once per run, + * outside the per-block execution path. The returned loader is scoped: it is closed when the surrounding [[Scope]] + * closes, so callers acquire it inside the run's `ZIO.scoped` boundary. */ def userClassLoader( scalaVersion: String, @@ -66,22 +58,18 @@ end CompilerFactory object CompilerFactory: - /** Default Scala 3 version — the version the bundled 3.x shim was compiled - * against. Used when no version is specified by document, CLI flag, or build - * plugin. + /** Default Scala 3 version — the version the bundled 3.x shim was compiled against. Used when no version is specified + * by document, CLI flag, or build plugin. * - * Read from a plain text resource embedded in the shim jar at build time - * (see build.sbt's `compilerShim` resource generator). Reading a `.txt` - * resource avoids loading any shim classes — which would transitively - * require `scala3-compiler` on the probe classpath, defeating the whole - * per-version isolation contract. + * Read from a plain text resource embedded in the shim jar at build time (see build.sbt's `compilerShim` resource + * generator). Reading a `.txt` resource avoids loading any shim classes — which would transitively require + * `scala3-compiler` on the probe classpath, defeating the whole per-version isolation contract. */ def defaultScalaVersion: String = readShimVersion("/marklit-compiler-shim.jar", "marklit-shim-version.txt") - /** Default Scala 2 version — the version the bundled 2.13 shim was compiled - * against. Used when a block requests a bare-major `scala=2` and the - * file/CLI default is not itself a 2.13.x version. + /** Default Scala 2 version — the version the bundled 2.13 shim was compiled against. Used when a block requests a + * bare-major `scala=2` and the file/CLI default is not itself a 2.13.x version. */ def defaultScala2Version: String = readShimVersion( @@ -109,9 +97,8 @@ object CompilerFactory: finally zip.close() finally Files.deleteIfExists(tmp): Unit - /** Build a CompilerFactory backed by extracted copies of both bundled shim - * jars (3.x dotc shim and 2.13 nsc shim) plus Coursier resolution for each - * requested version. + /** Build a CompilerFactory backed by extracted copies of both bundled shim jars (3.x dotc shim and 2.13 nsc shim) + * plus Coursier resolution for each requested version. */ val layer: ZLayer[Any, Throwable, CompilerFactory] = ZLayer.scoped { @@ -125,9 +112,8 @@ object CompilerFactory: yield new Live(shim3, shim2, cache) } - /** Layer variant for tests: takes both shim jar paths explicitly so tests can - * point at the freshly-built `compilerShim` / `compilerShim2` packageBin - * outputs without going through the CLI fat jar. + /** Layer variant for tests: takes both shim jar paths explicitly so tests can point at the freshly-built + * `compilerShim` / `compilerShim2` packageBin outputs without going through the CLI fat jar. */ def testLayer( shim3Jar: Path, @@ -151,10 +137,8 @@ object CompilerFactory: } )(p => ZIO.attempt(Files.deleteIfExists(p): Unit).ignore) - /** Cached per-version state: the resolved compiler jars, the per-version - * classloader, and the [[DotcInvoker]] instance loaded from it. Sharing - * these across calls is what makes repeated [[forVersion]] invocations - * cheap. + /** Cached per-version state: the resolved compiler jars, the per-version classloader, and the [[DotcInvoker]] + * instance loaded from it. Sharing these across calls is what makes repeated [[forVersion]] invocations cheap. */ private final case class VersionBundle( invoker: DotcInvoker, @@ -164,9 +148,8 @@ object CompilerFactory: // ---------- Internals ---------- - /** Copy a classpath resource to the given path. Errors out loudly if the - * resource is missing — that means the build is broken (the cli module's - * resourceGenerators step in build.sbt didn't run). + /** Copy a classpath resource to the given path. Errors out loudly if the resource is missing — that means the build + * is broken (the cli module's resourceGenerators step in build.sbt didn't run). */ private def copyResource(name: String, target: Path): Unit = val in = Option(getClass.getResourceAsStream(name)).getOrElse { @@ -178,9 +161,8 @@ object CompilerFactory: Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING): Unit finally in.close() - /** Build a per-version classloader. Parent is a filtering loader that exposes - * only `marklit.compiler.api.*` (so the [[DotcInvoker]] interface is a - * single shared class across loaders) and `java.*`/`javax.*` from the host. + /** Build a per-version classloader. Parent is a filtering loader that exposes only `marklit.compiler.api.*` (so the + * [[DotcInvoker]] interface is a single shared class across loaders) and `java.*`/`javax.*` from the host. * Everything else — including `dotty.*` and `scala.*` — comes from the URLs. */ private def buildLoader( @@ -188,24 +170,18 @@ object CompilerFactory: compilerJars: Vector[String] ): URLClassLoader = val urls: Array[URL] = - (Vector(shimJar.toUri.toURL) ++ compilerJars.map(p => - java.nio.file.Paths.get(p).toUri.toURL - )).toArray + (Vector(shimJar.toUri.toURL) ++ compilerJars.map(p => java.nio.file.Paths.get(p).toUri.toURL)).toArray new URLClassLoader(urls, new ApiOnlyParent(getClass.getClassLoader)) - /** A parent classloader that delegates only `marklit.compiler.api.*` (and - * `java.*`/`javax.*`, which the JVM bootstrap covers anyway) to the given - * host loader. Refuses to load anything else, forcing the child loader to - * find `dotty.*` and `scala.*` in its own URL list. + /** A parent classloader that delegates only `marklit.compiler.api.*` (and `java.*`/`javax.*`, which the JVM bootstrap + * covers anyway) to the given host loader. Refuses to load anything else, forcing the child loader to find `dotty.*` + * and `scala.*` in its own URL list. * - * The host has scala3-library on it (transitive of marklit's own compile), - * so without this filter the child would inherit a `scala.*` from the host - * AND find one in its URLs — yielding the very "package scala contains - * object and package with same name" duplicate-definition errors that - * triggered this whole refactor. + * The host has scala3-library on it (transitive of marklit's own compile), so without this filter the child would + * inherit a `scala.*` from the host AND find one in its URLs — yielding the very "package scala contains object and + * package with same name" duplicate-definition errors that triggered this whole refactor. */ - private final class ApiOnlyParent(host: ClassLoader) - extends ClassLoader(null): + private final class ApiOnlyParent(host: ClassLoader) extends ClassLoader(null): override def loadClass(name: String, resolve: Boolean): Class[?] = // Delegate the marklit<->shim bridge API plus everything the JDK platform // classloader owns. The platform check is authoritative and covers java.*, @@ -227,8 +203,8 @@ object CompilerFactory: private val platformLoader: ClassLoader = ClassLoader.getPlatformClassLoader - /** True when the JDK platform classloader can load `name` — the - * authoritative, version-current test for "is this a JDK platform class?". + /** True when the JDK platform classloader can load `name` — the authoritative, version-current test for "is this a + * JDK platform class?". */ def isPlatformClass(name: String): Boolean = try @@ -303,10 +279,8 @@ object CompilerFactory: private def buildBundle(scalaVersion: String): VersionBundle = val (shimJar, invokerClass) = - if scalaVersion.startsWith("3") then - (shim3Jar, "marklit.compiler.shim.DotcInvokerImpl") - else if scalaVersion.startsWith("2.13") then - (shim2Jar, "marklit.compiler.shim.NscInvokerImpl") + if scalaVersion.startsWith("3") then (shim3Jar, "marklit.compiler.shim.DotcInvokerImpl") + else if scalaVersion.startsWith("2.13") then (shim2Jar, "marklit.compiler.shim.NscInvokerImpl") else throw new IllegalArgumentException( s"Unsupported Scala version '$scalaVersion'. Marklit supports 2.13.x and 3.x." diff --git a/compiler/src/main/scala/marklit/compiler/ScalaCompiler.scala b/compiler/src/main/scala/marklit/compiler/ScalaCompiler.scala index f9f4520..6e790ad 100644 --- a/compiler/src/main/scala/marklit/compiler/ScalaCompiler.scala +++ b/compiler/src/main/scala/marklit/compiler/ScalaCompiler.scala @@ -8,40 +8,32 @@ import java.io.{ByteArrayOutputStream, PrintStream} import java.nio.file.{Files, Path} import scala.jdk.CollectionConverters.* -/** Scala 3 compiler implementation that delegates the actual dotc invocation to - * a [[DotcInvoker]] living on a per-version classloader. The marklit JVM does - * not link against `dotty.tools.*` directly — see [[CompilerFactory]] for the - * classloader-isolation contract. +/** Scala 3 compiler implementation that delegates the actual dotc invocation to a [[DotcInvoker]] living on a + * per-version classloader. The marklit JVM does not link against `dotty.tools.*` directly — see [[CompilerFactory]] + * for the classloader-isolation contract. * - * The classpath / scalacOptions / outputDir handling stays Scala-side: the - * shim takes a [[CompileRequest]] of those values and is itself version- - * neutral. + * The classpath / scalacOptions / outputDir handling stays Scala-side: the shim takes a [[CompileRequest]] of those + * values and is itself version- neutral. * * @param invoker * the version-specific dotc invoker. * @param classpath - * jars on the *user's* compile classpath (their fullClasspath, plus the - * resolved scala3-library/compiler jars from the factory). + * jars on the *user's* compile classpath (their fullClasspath, plus the resolved scala3-library/compiler jars from + * the factory). * @param scalacOptions * global options (e.g., from the build). * @param outputDir - * writable directory where dotc emits .class files and the executor loads - * them from. + * writable directory where dotc emits .class files and the executor loads them from. * @param scalaVersion - * reported version for filtering decisions in - * [[marklit.processor.DocumentProcessor]]. + * reported version for filtering decisions in [[marklit.processor.DocumentProcessor]]. * @param runtimeLoader - * classloader used to resolve user-code classes at execution time. When - * provided, this is the per-version compiler loader from the factory; when - * None we fall back to `getClass.getClassLoader` (legacy in-process path). + * classloader used to resolve user-code classes at execution time. When provided, this is the per-version compiler + * loader from the factory; when None we fall back to `getClass.getClassLoader` (legacy in-process path). * @param shareUserClasses - * when true, every block (including ZIO blocks) loads user classes - * parent-first from [[runtimeLoader]] instead of child-first. This makes a - * build-provided run resource a single shared instance across all blocks in - * the run — at the cost of the per-block-fresh init that the child-first - * loader otherwise gives ZIO blocks. Enabled only when a run resource is - * configured (see [[CompilerFactory.userClassLoader]]); the default path is - * unchanged. + * when true, every block (including ZIO blocks) loads user classes parent-first from [[runtimeLoader]] instead of + * child-first. This makes a build-provided run resource a single shared instance across all blocks in the run — at + * the cost of the per-block-fresh init that the child-first loader otherwise gives ZIO blocks. Enabled only when a + * run resource is configured (see [[CompilerFactory.userClassLoader]]); the default path is unchanged. */ final class ScalaCompiler( invoker: DotcInvoker, @@ -161,8 +153,7 @@ final class ScalaCompiler( context: ScopeContext ): IO[MarklitError, ExecutionResult] = compile(code, context).flatMap { compileResult => - if !compileResult.success then - ZIO.fail(MarklitError.CompileError(compileResult.errors)) + if !compileResult.success then ZIO.fail(MarklitError.CompileError(compileResult.errors)) else compileResult.classFilesDir match case Some(dir) => executeFromDir(dir, context) @@ -225,8 +216,7 @@ final class ScalaCompiler( // classFilesDir, not in U) loads from the child URLs. ZIO blocks // use this path too — sharing is chosen over per-block-fresh init. new java.net.URLClassLoader(urls, parent) - else if context.isZIOApp then - new ChildFirstClassLoader(urls, parent) + else if context.isZIOApp then new ChildFirstClassLoader(urls, parent) else new java.net.URLClassLoader(urls, parent) // Redirect scala.Console on the *user code's* classloader. Each @@ -257,8 +247,7 @@ final class ScalaCompiler( val newOutput = context.outputMarker match case Some(marker) => val markerIdx = fullOutput.indexOf(marker) - if markerIdx >= 0 then - fullOutput.substring(markerIdx + marker.length) + if markerIdx >= 0 then fullOutput.substring(markerIdx + marker.length) else fullOutput case None => fullOutput @@ -279,9 +268,8 @@ final class ScalaCompiler( MarklitError.RuntimeError(cause, "") } - /** Wrap code in an object so it can be compiled as a complete compilation - * unit. For Scala 3, uses indentation-sensitive syntax to avoid breaking - * user code. For Scala 2, uses brace syntax. + /** Wrap code in an object so it can be compiled as a complete compilation unit. For Scala 3, uses + * indentation-sensitive syntax to avoid breaking user code. For Scala 2, uses brace syntax. */ private def wrapInObject(code: String): String = if isScala3 then @@ -349,13 +337,10 @@ final class ScalaCompiler( /** Reflectively redirect `scala.Console.out` on the user-code classloader. * - * Each per-version classloader has its own `scala.Console$` Module object - * whose `out` field was captured from `System.out` the first time it was - * touched (typically during dotc's own compilation, before we redirected - * stdout). Calling `Console.withOut` on the marklit-loader's Console only - * affects the marklit-side Console — it's a different Class object from the - * user-loader's, so `println` from user code still goes to the original - * stream. + * Each per-version classloader has its own `scala.Console$` Module object whose `out` field was captured from + * `System.out` the first time it was touched (typically during dotc's own compilation, before we redirected stdout). + * Calling `Console.withOut` on the marklit-loader's Console only affects the marklit-side Console — it's a different + * Class object from the user-loader's, so `println` from user code still goes to the original stream. * * Returns a closure that restores the previous out. */ @@ -405,14 +390,13 @@ final class ScalaCompiler( end ScalaCompiler object ScalaCompiler: - /** Scala version returned by the bundled shim. Used as the fallback when no - * version is otherwise specified. + /** Scala version returned by the bundled shim. Used as the fallback when no version is otherwise specified. */ def defaultScalaVersion: String = CompilerFactory.defaultScalaVersion -/** Child-first classloader that loads classes from URLs before delegating to - * parent. Ensures libraries like ZIO get fresh initialization in each block. +/** Child-first classloader that loads classes from URLs before delegating to parent. Ensures libraries like ZIO get + * fresh initialization in each block. */ private class ChildFirstClassLoader( urls: Array[java.net.URL], @@ -445,10 +429,9 @@ private class ChildFirstClassLoader( end ChildFirstClassLoader private object ChildFirstClassLoader: - /** The platform classloader can load every JDK platform-module class - * (java.base, java.xml, java.sql, java.naming, the CORBA/RMI packages, …) - * regardless of package. It is the authoritative, version-current source for - * "is this a JDK class?" — far safer than enumerating package prefixes. + /** The platform classloader can load every JDK platform-module class (java.base, java.xml, java.sql, java.naming, the + * CORBA/RMI packages, …) regardless of package. It is the authoritative, version-current source for "is this a JDK + * class?" — far safer than enumerating package prefixes. */ private val platformLoader: ClassLoader = ClassLoader.getPlatformClassLoader diff --git a/compiler/src/test/scala/marklit/MarklitSpec.scala b/compiler/src/test/scala/marklit/MarklitSpec.scala index 3275165..57be870 100644 --- a/compiler/src/test/scala/marklit/MarklitSpec.scala +++ b/compiler/src/test/scala/marklit/MarklitSpec.scala @@ -7,9 +7,8 @@ import zio.test.* object MarklitSpec extends ZIOSpecDefault: - /** Layer that configures Marklit with `ScopeMode.Page` — the CLI/plugin - * opt-in that makes anonymous blocks share state per file. The default - * [[Marklit.layer]] uses `ScopeMode.Isolated`. + /** Layer that configures Marklit with `ScopeMode.Page` — the CLI/plugin opt-in that makes anonymous blocks share + * state per file. The default [[Marklit.layer]] uses `ScopeMode.Isolated`. */ private val pageScopeLayer: ZLayer[Any, Throwable, Marklit] = CompilerFactory.layer >>> Marklit.liveWithFactory( diff --git a/compiler/src/test/scala/marklit/RunResourceSpec.scala b/compiler/src/test/scala/marklit/RunResourceSpec.scala index 016307a..d1f6635 100644 --- a/compiler/src/test/scala/marklit/RunResourceSpec.scala +++ b/compiler/src/test/scala/marklit/RunResourceSpec.scala @@ -5,33 +5,25 @@ import zio.test.* import java.nio.file.{Files, Path} -/** End-to-end tests for the build-provided run-scoped resource - * ([[MarklitRunConfig.runResourceClass]]). +/** End-to-end tests for the build-provided run-scoped resource ([[MarklitRunConfig.runResourceClass]]). * - * The resource class is loaded by marklit's per-run user loader `U`, a - * different classloader from this test, so state is observed across the - * boundary via (a) a file named by the `marklit.test.events` system property - * for lifecycle events, and (b) rendered block output for the shared in-JVM - * counter. See `marklit.testfixtures.RunResourceFixtures`. + * The resource class is loaded by marklit's per-run user loader `U`, a different classloader from this test, so state + * is observed across the boundary via (a) a file named by the `marklit.test.events` system property for lifecycle + * events, and (b) rendered block output for the shared in-JVM counter. See `marklit.testfixtures.RunResourceFixtures`. */ object RunResourceSpec extends ZIOSpecDefault: - /** User classpath for the run, gathered from the test's own classloader URL - * chain (sbt's test runner uses layered classloaders, so `java.class.path` - * is unreliable here). Minus the Scala stdlib, which the per-version - * compiler loader supplies — leaving the host's copy on would clash. This - * mirrors how a build forwards its `fullClasspath` and gives `U` the - * fixtures *and* the full ZIO dependency set a zio-app block needs to - * compile (marklit's `ApiOnlyParent` hides the host's copies, so they must - * be passed explicitly). + /** User classpath for the run, gathered from the test's own classloader URL chain (sbt's test runner uses layered + * classloaders, so `java.class.path` is unreliable here). Minus the Scala stdlib, which the per-version compiler + * loader supplies — leaving the host's copy on would clash. This mirrors how a build forwards its `fullClasspath` + * and gives `U` the fixtures *and* the full ZIO dependency set a zio-app block needs to compile (marklit's + * `ApiOnlyParent` hides the host's copies, so they must be passed explicitly). */ private val testClasspath: Vector[String] = def loaderUrls(cl: ClassLoader): Vector[String] = val here = cl match case u: java.net.URLClassLoader => - u.getURLs.toVector.map(url => - java.nio.file.Paths.get(url.toURI).toString - ) + u.getURLs.toVector.map(url => java.nio.file.Paths.get(url.toURI).toString) case _ => Vector.empty val parent = Option(cl.getParent).map(loaderUrls).getOrElse(Vector.empty) parent ++ here @@ -62,10 +54,9 @@ object RunResourceSpec extends ZIOSpecDefault: }.ignore ) - /** A fresh temp file whose path is published to the `marklit.test.events` - * system property for the lifetime of the enclosing scope, then cleared. The - * property is how the fixture (on loader `U`) and this test agree on the - * event file across the classloader boundary. + /** A fresh temp file whose path is published to the `marklit.test.events` system property for the lifetime of the + * enclosing scope, then cleared. The property is how the fixture (on loader `U`) and this test agree on the event + * file across the classloader boundary. */ private def eventsFile: ZIO[Scope, Throwable, Path] = ZIO.acquireRelease( @@ -84,17 +75,14 @@ object RunResourceSpec extends ZIOSpecDefault: private def readLines(p: Path): ZIO[Any, Throwable, Vector[String]] = ZIO.attempt { import scala.jdk.CollectionConverters.* - if Files.exists(p) then - Files.readAllLines(p).asScala.toVector.filter(_.nonEmpty) + if Files.exists(p) then Files.readAllLines(p).asScala.toVector.filter(_.nonEmpty) else Vector.empty } - /** Pin the run to the Scala version this test was built with, so the ZIO jar - * on [[testClasspath]] (built for that version) has TASTy the resolved - * compiler can read. The bundled default shim is older and would reject - * newer ZIO TASTy — a version-skew artifact of the test harness, not of the - * feature. The per-run user loader `U` is built at this same version, so - * sharing lines up. + /** Pin the run to the Scala version this test was built with, so the ZIO jar on [[testClasspath]] (built for that + * version) has TASTy the resolved compiler can read. The bundled default shim is older and would reject newer ZIO + * TASTy — a version-skew artifact of the test harness, not of the feature. The per-run user loader `U` is built at + * this same version, so sharing lines up. */ private val hostScalaVersion: String = scala.util.Properties.versionNumberString @@ -241,9 +229,7 @@ object RunResourceSpec extends ZIOSpecDefault: yield assertTrue( // The run is not failed by a resource setup error — docs still process. result.success, - result.notices.exists(n => - n.contains("setup failed") && n.contains("boom-setup") - ) + result.notices.exists(n => n.contains("setup failed") && n.contains("boom-setup")) ) } }, @@ -262,9 +248,7 @@ object RunResourceSpec extends ZIOSpecDefault: ) yield assertTrue( result.success, - result.notices.exists(n => - n.contains("teardown failed") && n.contains("boom-teardown") - ) + result.notices.exists(n => n.contains("teardown failed") && n.contains("boom-teardown")) ) } } diff --git a/compiler/src/test/scala/marklit/compiler/ClasspathEntriesSpec.scala b/compiler/src/test/scala/marklit/compiler/ClasspathEntriesSpec.scala index 54ee5df..79beae0 100644 --- a/compiler/src/test/scala/marklit/compiler/ClasspathEntriesSpec.scala +++ b/compiler/src/test/scala/marklit/compiler/ClasspathEntriesSpec.scala @@ -30,8 +30,7 @@ object ClasspathEntriesSpec extends ZIOSpecDefault: val jar = Files.createTempFile("marklit-cp-spec-", ".jar") Files.write( jar, - Array[Byte]('P', 'K', 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0) + Array[Byte]('P', 'K', 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) ) val out = ClasspathEntries.forScalac(Vector(jar.toString, dir.toString)) assertTrue(out == Vector(jar.toString, dir.toString)) @@ -40,8 +39,7 @@ object ClasspathEntriesSpec extends ZIOSpecDefault: val cas = Files.createTempFile("marklit-cp-cas-", "") Files.write( cas, - Array[Byte]('P', 'K', 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0) + Array[Byte]('P', 'K', 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) ) // Drop the random suffix file's accidental extensionless nature is enough val a = ClasspathEntries.forScalac(Vector(cas.toString)).head diff --git a/compiler/src/test/scala/marklit/compiler/LocalClassWarnSpec.scala b/compiler/src/test/scala/marklit/compiler/LocalClassWarnSpec.scala index 6d06a82..16dd3b0 100644 --- a/compiler/src/test/scala/marklit/compiler/LocalClassWarnSpec.scala +++ b/compiler/src/test/scala/marklit/compiler/LocalClassWarnSpec.scala @@ -4,16 +4,13 @@ import marklit.model.* import zio.* import zio.test.* -/** Regression coverage for the core value proposition of `top-level`: a - * parameterized enum case matched against a non-local scrutinee type emits - * `the type test for X cannot be checked at runtime because it's a local - * class` when the enum is defined *inside* `def run()` (a local class). When - * the enum is hoisted to file scope (a real top-level class), the type test is - * checkable and the warning disappears. +/** Regression coverage for the core value proposition of `top-level`: a parameterized enum case matched against a + * non-local scrutinee type emits `the type test for X cannot be checked at runtime because it's a local class` when + * the enum is defined *inside* `def run()` (a local class). When the enum is hoisted to file scope (a real top-level + * class), the type test is checkable and the warning disappears. * - * This is exactly the warning that forced `show-warnings=false` on the conduit - * docs; `top-level` lets authors drop that workaround. Verified against the - * bundled Scala shim. + * This is exactly the warning that forced `show-warnings=false` on the conduit docs; `top-level` lets authors drop + * that workaround. Verified against the bundled Scala shim. */ object LocalClassWarnSpec extends ZIOSpecDefault: diff --git a/compiler/src/test/scala/marklit/compiler/TestCompilerLayer.scala b/compiler/src/test/scala/marklit/compiler/TestCompilerLayer.scala index e2653a0..aeffe4d 100644 --- a/compiler/src/test/scala/marklit/compiler/TestCompilerLayer.scala +++ b/compiler/src/test/scala/marklit/compiler/TestCompilerLayer.scala @@ -2,9 +2,8 @@ package marklit.compiler import zio.* -/** Layer that produces a default-version [[Compiler]] via [[CompilerFactory]] - * for tests that just need *some* working compiler. Reads the shim jar from - * the test classpath resource (wired in build.sbt's +/** Layer that produces a default-version [[Compiler]] via [[CompilerFactory]] for tests that just need *some* working + * compiler. Reads the shim jar from the test classpath resource (wired in build.sbt's * `compiler/Test/resourceGenerators`). */ object TestCompilerLayer: diff --git a/compiler/src/test/scala/marklit/compiler/TopLevelCompileSpec.scala b/compiler/src/test/scala/marklit/compiler/TopLevelCompileSpec.scala index b3ae854..cb5f64e 100644 --- a/compiler/src/test/scala/marklit/compiler/TopLevelCompileSpec.scala +++ b/compiler/src/test/scala/marklit/compiler/TopLevelCompileSpec.scala @@ -4,10 +4,9 @@ import marklit.model.* import zio.* import zio.test.* -/** Compiler-level coverage for the `top-level` feature: blocks that compile - * verbatim as their own compilation unit (no `MarklitWrapper`), and the - * hoisting of inherited top-level definitions above the wrapper for normal - * blocks that depend on them. +/** Compiler-level coverage for the `top-level` feature: blocks that compile verbatim as their own compilation unit (no + * `MarklitWrapper`), and the hoisting of inherited top-level definitions above the wrapper for normal blocks that + * depend on them. */ object TopLevelCompileSpec extends ZIOSpecDefault: diff --git a/core/src/main/scala/marklit/cache/BlockCache.scala b/core/src/main/scala/marklit/cache/BlockCache.scala index 64876ed..2052ba2 100644 --- a/core/src/main/scala/marklit/cache/BlockCache.scala +++ b/core/src/main/scala/marklit/cache/BlockCache.scala @@ -9,30 +9,25 @@ import java.security.MessageDigest /** A persistent content-addressed cache for compile results. * - * Each entry is keyed by a SHA-256 hash of every input that influences a - * block's compile outcome — block source, prior code, effective Scala version, - * classpath, scalac options, modifiers, and source location. Entries live in a - * fan-out directory layout `//.{json,classes}` to avoid - * 100k-files-in-one-dir issues on older filesystems. + * Each entry is keyed by a SHA-256 hash of every input that influences a block's compile outcome — block source, prior + * code, effective Scala version, classpath, scalac options, modifiers, and source location. Entries live in a fan-out + * directory layout `//.{json,classes}` to avoid 100k-files-in-one-dir issues on older + * filesystems. * * - `.json` holds the diagnostics + success flag. - * - `.classes/` holds the dotc-emitted class files, copied verbatim - * from the per-block compile output directory. The executor reads directly - * from this directory on a hit, so no materialization step is needed; the + * - `.classes/` holds the dotc-emitted class files, copied verbatim from the per-block compile output + * directory. The executor reads directly from this directory on a hit, so no materialization step is needed; the * cached dir is treated as immutable. * - * Compile-only: we cache `CompileResult` (success flag, diagnostics, class - * files) but never execution output. Execute blocks always re-run because - * their outputs may legitimately depend on side-effecting state we can't - * capture. + * Compile-only: we cache `CompileResult` (success flag, diagnostics, class files) but never execution output. Execute + * blocks always re-run because their outputs may legitimately depend on side-effecting state we can't capture. */ trait BlockCache: def get(key: BlockCacheKey): UIO[Option[CompileResult]] - /** Persist the result. When [[result.classFilesDir]] is set and - * [[result.success]] is true, the directory's contents are copied into the - * cache entry; the cached `CompileResult` returned from a future [[get]] - * will point at the cached copy. + /** Persist the result. When [[result.classFilesDir]] is set and [[result.success]] is true, the directory's contents + * are copied into the cache entry; the cached `CompileResult` returned from a future [[get]] will point at the + * cached copy. */ def put(key: BlockCacheKey, result: CompileResult): UIO[Unit] def clear: UIO[Unit] @@ -40,18 +35,17 @@ end BlockCache object BlockCache: - /** A no-op cache. Useful when the user runs without `--cache-dir` (raw - * one-shot CLI invocations) or for tests that want to bypass caching. + /** A no-op cache. Useful when the user runs without `--cache-dir` (raw one-shot CLI invocations) or for tests that + * want to bypass caching. */ val noop: BlockCache = new BlockCache: def get(key: BlockCacheKey): UIO[Option[CompileResult]] = ZIO.none def put(key: BlockCacheKey, result: CompileResult): UIO[Unit] = ZIO.unit def clear: UIO[Unit] = ZIO.unit - /** Disk-backed cache rooted at [[root]]. Misses (missing file, malformed - * JSON) return `None`; we never fail the compile path because of a cache - * read error. Writes are best-effort: a cache write failure is logged at the - * layer above (here, swallowed) — the user still gets correct compilation. + /** Disk-backed cache rooted at [[root]]. Misses (missing file, malformed JSON) return `None`; we never fail the + * compile path because of a cache read error. Writes are best-effort: a cache write failure is logged at the layer + * above (here, swallowed) — the user still gets correct compilation. */ def disk(root: Path): BlockCache = new DiskBlockCache(root) end BlockCache @@ -147,8 +141,8 @@ private final class DiskBlockCache(root: java.nio.file.Path) extends BlockCache: finally it.close() end DiskBlockCache -/** A 64-character lowercase hex SHA-256 of the cache inputs. The wrapper type - * keeps us from accidentally treating a raw user string as a key. +/** A 64-character lowercase hex SHA-256 of the cache inputs. The wrapper type keeps us from accidentally treating a raw + * user string as a key. */ final case class BlockCacheKey(value: String) extends AnyVal @@ -156,18 +150,14 @@ object BlockCacheKey: /** Build a key from every input that affects raw compiler output. * - * Modifiers like `expectsFailure` / `expectsWarnings` are deliberately *not* - * keyed — they alter how the document processor interprets the result (a - * "fail" block flips a compile failure into a successful BlockResult), but - * they don't change the bytes the compiler produces. Caching the raw - * [[CompileResult]] and letting the wrapper layer reinterpret it on every - * run is correct and keeps the key narrow. + * Modifiers like `expectsFailure` / `expectsWarnings` are deliberately *not* keyed — they alter how the document + * processor interprets the result (a "fail" block flips a compile failure into a successful BlockResult), but they + * don't change the bytes the compiler produces. Caching the raw [[CompileResult]] and letting the wrapper layer + * reinterpret it on every run is correct and keeps the key narrow. * - * Adding a field here invalidates every existing cache entry — that's the - * correct behavior, since old entries were computed without considering the - * new input. If you're tempted to keep entries across a key-shape change: - * don't. Bump the version prefix below instead, which also invalidates - * cleanly. + * Adding a field here invalidates every existing cache entry — that's the correct behavior, since old entries were + * computed without considering the new input. If you're tempted to keep entries across a key-shape change: don't. + * Bump the version prefix below instead, which also invalidates cleanly. */ def make( code: String, @@ -229,9 +219,8 @@ object BlockCacheKey: BlockCacheKey(sb.toString) end BlockCacheKey -/** On-disk JSON shape for the metadata side of a cached CompileResult. Class - * files live in a sibling `.classes/` directory and are wired back in by - * [[DiskBlockCache.get]] — they aren't carried in the JSON. +/** On-disk JSON shape for the metadata side of a cached CompileResult. Class files live in a sibling `.classes/` + * directory and are wired back in by [[DiskBlockCache.get]] — they aren't carried in the JSON. */ private final case class CachedCompileResult( success: Boolean, diff --git a/core/src/main/scala/marklit/model/CodeBlock.scala b/core/src/main/scala/marklit/model/CodeBlock.scala index 138e7d5..1c3b2f3 100644 --- a/core/src/main/scala/marklit/model/CodeBlock.scala +++ b/core/src/main/scala/marklit/model/CodeBlock.scala @@ -8,15 +8,14 @@ final case class CodeBlock( location: Location, showWarningsOverride: Option[Boolean] = None ): - /** Whether compile warnings should be rendered for this block, given the - * caller's default. Per-block override (from a `show-warnings=true|false` - * info-string option) wins over the default. + /** Whether compile warnings should be rendered for this block, given the caller's default. Per-block override (from a + * `show-warnings=true|false` info-string option) wins over the default. */ def showWarnings(default: Boolean): Boolean = showWarningsOverride.getOrElse(default) - /** Whether this block compiles verbatim as its own compilation unit (no - * `MarklitWrapper` object). Top-level blocks are always compile-only. + /** Whether this block compiles verbatim as its own compilation unit (no `MarklitWrapper` object). Top-level blocks + * are always compile-only. */ def isTopLevel: Boolean = modifiers.contains(Modifier.TopLevel) @@ -27,13 +26,12 @@ final case class CodeBlock( !modifiers.contains(Modifier.Fail) && !modifiers.contains(Modifier.Passthrough) - /** For a `top-level` block, the set of other modifiers it was combined with — - * `top-level` is strict and may only accompany scope options - * (`id`/`extends`/`append`), version selectors, and `show-warnings`, none of - * which live in [[modifiers]]. Any *other* modifier here is a misuse. + /** For a `top-level` block, the set of other modifiers it was combined with — `top-level` is strict and may only + * accompany scope options (`id`/`extends`/`append`), version selectors, and `show-warnings`, none of which live in + * [[modifiers]]. Any *other* modifier here is a misuse. * - * Returns `Some(message)` describing the conflict, or `None` when the block - * is either not top-level or top-level with no conflicting modifiers. + * Returns `Some(message)` describing the conflict, or `None` when the block is either not top-level or top-level + * with no conflicting modifiers. */ def modifierConflicts: Option[String] = if !isTopLevel then None @@ -71,16 +69,13 @@ final case class CodeBlock( /** Whether this block should be wrapped in ZIOAppDefault */ def isZIOApp: Boolean = modifiers.contains(Modifier.ZIOApp) - /** Whether this block should be skipped (filter behavior) given the current - * default Scala version. + /** Whether this block should be skipped (filter behavior) given the current default Scala version. * - * Filter only applies to **bare major** specifiers (`scala=2`, `scala=3`): - * if the major differs from the current default, skip the block. This is the - * "build a 2.13 doc and a 3.x doc from one source" use case. + * Filter only applies to **bare major** specifiers (`scala=2`, `scala=3`): if the major differs from the current + * default, skip the block. This is the "build a 2.13 doc and a 3.x doc from one source" use case. * - * **Specific versions** (`scala=3.7.0`, `scala=2.13.16`) are NOT filters — - * they request a cross-version compile against that exact version, handled - * by [[marklit.processor.DocumentProcessor]] via [[CompilerService]]. + * **Specific versions** (`scala=3.7.0`, `scala=2.13.16`) are NOT filters — they request a cross-version compile + * against that exact version, handled by [[marklit.processor.DocumentProcessor]] via [[CompilerService]]. * * The detection rule: a `.` in the version string means specific. */ @@ -97,31 +92,28 @@ final case class CodeBlock( val currentMajor = currentVersion.takeWhile(_ != '.') currentMajor == blockVersion - /** The specific Scala version this block requests, if any. Bare majors and - * absent values return None — those don't trigger a per-block compiler. + /** The specific Scala version this block requests, if any. Bare majors and absent values return None — those don't + * trigger a per-block compiler. */ def requestedSpecificScalaVersion: Option[String] = scopeConfig.scalaVersion.filter(_.contains(".")) - /** Whether this block contributes its code to every per-version default - * scope. + /** Whether this block contributes its code to every per-version default scope. */ def isShared: Boolean = modifiers.contains(Modifier.Shared) - /** The major version this block is shared with, if it carries a `shared-{mv}` - * modifier. + /** The major version this block is shared with, if it carries a `shared-{mv}` modifier. */ def sharedMajor: Option[String] = modifiers.collectFirst { case Modifier.SharedMajor(mv) => mv } - /** Whether this block should be prepended to *some* per-version default scope - * (either unconditionally or restricted to a major). + /** Whether this block should be prepended to *some* per-version default scope (either unconditionally or restricted + * to a major). */ def isAnyShared: Boolean = isShared || sharedMajor.isDefined - /** Whether this block contributes to the default scope for the given Scala - * version. Unshared blocks return false; `shared` returns true for any - * version; `shared-{mv}` returns true only when the version's major matches. + /** Whether this block contributes to the default scope for the given Scala version. Unshared blocks return false; + * `shared` returns true for any version; `shared-{mv}` returns true only when the version's major matches. */ def appliesToDefaultScope(version: String): Boolean = if isShared then true diff --git a/core/src/main/scala/marklit/model/CompileResult.scala b/core/src/main/scala/marklit/model/CompileResult.scala index c8d9ecd..b5505ae 100644 --- a/core/src/main/scala/marklit/model/CompileResult.scala +++ b/core/src/main/scala/marklit/model/CompileResult.scala @@ -5,11 +5,9 @@ import java.nio.file.Path /** Result of compiling a code block. * * @param classFilesDir - * directory holding the per-block class files emitted by dotc, when the - * compile succeeded and the underlying compiler retained them. Used by the - * executor to skip a redundant recompile on the same inputs. `None` for - * synthesized results (cached entries that don't carry classes, expected- - * failure stand-ins, etc.). + * directory holding the per-block class files emitted by dotc, when the compile succeeded and the underlying + * compiler retained them. Used by the executor to skip a redundant recompile on the same inputs. `None` for + * synthesized results (cached entries that don't carry classes, expected- failure stand-ins, etc.). */ final case class CompileResult( success: Boolean, diff --git a/core/src/main/scala/marklit/model/Modifier.scala b/core/src/main/scala/marklit/model/Modifier.scala index 3bcfd9d..9c10a1b 100644 --- a/core/src/main/scala/marklit/model/Modifier.scala +++ b/core/src/main/scala/marklit/model/Modifier.scala @@ -17,10 +17,8 @@ enum Modifier: case TopLevel // Compile verbatim as its own compilation unit (no wrapper) object Modifier: - /** Parse a modifier from its string representation. Shared / SharedMajor are - * not parsed here — they're constructed by - * [[marklit.parser.InfoStringParser]] from the `scala=shared` / - * `scala=shared-{mv}` info-string forms. + /** Parse a modifier from its string representation. Shared / SharedMajor are not parsed here — they're constructed by + * [[marklit.parser.InfoStringParser]] from the `scala=shared` / `scala=shared-{mv}` info-string forms. */ def parse(s: String): Option[Modifier] = s.toLowerCase match case "silent" => Some(Silent) @@ -34,8 +32,8 @@ object Modifier: case "top-level" => Some(TopLevel) case _ => None - /** Human-readable name for a modifier, matching its info-string spelling. - * Used in diagnostics (e.g. [[marklit.model.CodeBlock.modifierConflicts]]). + /** Human-readable name for a modifier, matching its info-string spelling. Used in diagnostics (e.g. + * [[marklit.model.CodeBlock.modifierConflicts]]). */ def displayName(m: Modifier): String = m match case Silent => "silent" diff --git a/core/src/main/scala/marklit/model/ScopeConfig.scala b/core/src/main/scala/marklit/model/ScopeConfig.scala index efb74cc..c12bfcd 100644 --- a/core/src/main/scala/marklit/model/ScopeConfig.scala +++ b/core/src/main/scala/marklit/model/ScopeConfig.scala @@ -7,8 +7,7 @@ package marklit.model * - `extends` without `id` creates anonymous child scope * - `extends` with `append` mutates the parent scope * - `id` with `extends` creates a named child scope - * - `scalaVersion` sets compiler version; scopes cannot extend across - * versions + * - `scalaVersion` sets compiler version; scopes cannot extend across versions */ final case class ScopeConfig( id: Option[String] = None, @@ -18,10 +17,8 @@ final case class ScopeConfig( ): /** Validate that the configuration is legal */ def validate: Either[String, ScopeConfig] = - if id.isDefined && append then - Left("Cannot use both 'id' and 'append' - they are mutually exclusive") - else if append && extendsScope.isEmpty then - Left("'append' requires 'extends' to specify the parent scope") + if id.isDefined && append then Left("Cannot use both 'id' and 'append' - they are mutually exclusive") + else if append && extendsScope.isEmpty then Left("'append' requires 'extends' to specify the parent scope") else Right(this) /** Whether this block creates a new named scope */ @@ -38,8 +35,7 @@ end ScopeConfig object ScopeConfig: val empty: ScopeConfig = ScopeConfig() - /** Parse scope config from modifier strings like "id=foo", "extends=bar", - * "append" + /** Parse scope config from modifier strings like "id=foo", "extends=bar", "append" */ def parse(modifiers: Seq[String]): Either[String, ScopeConfig] = var id: Option[String] = None @@ -49,11 +45,9 @@ object ScopeConfig: modifiers.foreach { mod => if mod.startsWith("id=") then id = Some(mod.stripPrefix("id=")) - else if mod.startsWith("extends=") then - extendsScope = Some(mod.stripPrefix("extends=")) + else if mod.startsWith("extends=") then extendsScope = Some(mod.stripPrefix("extends=")) else if mod == "append" then append = true - else if mod.startsWith("scala=") then - scalaVersion = Some(mod.stripPrefix("scala=")) + else if mod.startsWith("scala=") then scalaVersion = Some(mod.stripPrefix("scala=")) } ScopeConfig(id, extendsScope, append, scalaVersion).validate diff --git a/core/src/main/scala/marklit/model/ScopeMode.scala b/core/src/main/scala/marklit/model/ScopeMode.scala index 81e485a..12245c7 100644 --- a/core/src/main/scala/marklit/model/ScopeMode.scala +++ b/core/src/main/scala/marklit/model/ScopeMode.scala @@ -1,9 +1,8 @@ package marklit.model -/** Project-wide scope policy. The default is `Isolated` — every anonymous block - * compiles in a fresh scope. `Page` shares a single scope across all anonymous - * blocks of one file (mdoc-style). Future modes can be added without changing - * call-site signatures. +/** Project-wide scope policy. The default is `Isolated` — every anonymous block compiles in a fresh scope. `Page` + * shares a single scope across all anonymous blocks of one file (mdoc-style). Future modes can be added without + * changing call-site signatures. */ enum ScopeMode: case Isolated diff --git a/core/src/main/scala/marklit/parser/InfoStringParser.scala b/core/src/main/scala/marklit/parser/InfoStringParser.scala index 2f3d08d..6fec45e 100644 --- a/core/src/main/scala/marklit/parser/InfoStringParser.scala +++ b/core/src/main/scala/marklit/parser/InfoStringParser.scala @@ -59,8 +59,7 @@ object InfoStringParser: // Full info string: "scala" followed by optional "marklit:" or "mdoc:" and modifiers // We prefer "marklit:" but support "mdoc:" for compatibility - private def infoString[$: P] - : P[(Boolean, Seq[Either[String, (String, String)]])] = + private def infoString[$: P]: P[(Boolean, Seq[Either[String, (String, String)]])] = P( ws ~ (scalaLang ~ ws ~ @@ -115,7 +114,7 @@ object InfoStringParser: // them to the corresponding Modifier and leave scalaVersion // unset so downstream code never sees "shared" as a version. value.toLowerCase match - case "shared" => modifiers += Modifier.Shared + case "shared" => modifiers += Modifier.Shared case other if other.startsWith("shared-") => val mv = other.stripPrefix("shared-") if mv.nonEmpty then modifiers += Modifier.SharedMajor(mv) diff --git a/core/src/main/scala/marklit/parser/MarkdownParser.scala b/core/src/main/scala/marklit/parser/MarkdownParser.scala index f4d9344..c36597e 100644 --- a/core/src/main/scala/marklit/parser/MarkdownParser.scala +++ b/core/src/main/scala/marklit/parser/MarkdownParser.scala @@ -20,31 +20,27 @@ final case class ParsedDocument( /** Parser for markdown documents with Scala code blocks. * - * The parser is intentionally minimal: it recognizes CommonMark fenced code - * blocks and treats everything else as opaque text. The CommonMark fence rules - * implemented here: + * The parser is intentionally minimal: it recognizes CommonMark fenced code blocks and treats everything else as + * opaque text. The CommonMark fence rules implemented here: * * - Opener may be indented 0–3 spaces (4+ would be an indented code block). * - Fence character is `` ` `` or `~`, repeated at least 3 times. * - Closer must use the same character as the opener, at length ≥ opener. - * - Closer line may have only whitespace after the fence; non-whitespace - * trailing content disqualifies it as a closer. + * - Closer line may have only whitespace after the fence; non-whitespace trailing content disqualifies it as a + * closer. * - Backtick-fence info strings may not contain `` ` ``. * - An unterminated fence is implicitly closed at EOF. * - Content lines have up to opener-indent leading spaces stripped. * - * Input line endings (`\r\n`, lone `\r`) are normalized to `\n` before parsing - * so that line counts remain accurate and the grammar can speak in terms of - * `\n` only. + * Input line endings (`\r\n`, lone `\r`) are normalized to `\n` before parsing so that line counts remain accurate and + * the grammar can speak in terms of `\n` only. */ object MarkdownParser: - /** Internal block-level AST. Public callers see [[MarkdownSegment]]; this - * intermediate AST exists so the parser can preserve fence metadata (indent, - * char, length) for richer error reporting and future renderer fidelity. The - * renderer currently normalizes fences to ``` so most of this metadata is - * unused downstream — but it's a free byproduct of the spec-correct parser - * and useful for diagnostics. + /** Internal block-level AST. Public callers see [[MarkdownSegment]]; this intermediate AST exists so the parser can + * preserve fence metadata (indent, char, length) for richer error reporting and future renderer fidelity. The + * renderer currently normalizes fences to ``` so most of this metadata is unused downstream — but it's a free + * byproduct of the spec-correct parser and useful for diagnostics. */ private enum Block: case Fenced( @@ -64,8 +60,7 @@ object MarkdownParser: ): IO[MarklitError, ParsedDocument] = parseDocument(content, sourceFile) - /** Normalize line endings: CRLF and lone CR become LF. Preserves line count - * so `Location` reporting stays correct. + /** Normalize line endings: CRLF and lone CR become LF. Preserves line count so `Location` reporting stays correct. */ private def normalizeLineEndings(s: String): String = s.replace("\r\n", "\n").replace('\r', '\n') @@ -153,8 +148,8 @@ object MarkdownParser: P( !fenceCloseLine(ch, openLen) ~ (("\n".!) | - (CharsWhile(_ != '\n', 1).! ~ ("\n".! | Pass(""))).map { - case (line, nl) => line + nl + (CharsWhile(_ != '\n', 1).! ~ ("\n".! | Pass(""))).map { case (line, nl) => + line + nl }) ) @@ -213,8 +208,8 @@ object MarkdownParser: // --- driver ----------------------------------------------------------- - /** Strip up to `n` leading spaces from each line of `s`. Used to match - * CommonMark's indent-stripping for indented fence openers. + /** Strip up to `n` leading spaces from each line of `s`. Used to match CommonMark's indent-stripping for indented + * fence openers. */ private def stripLeadingIndent(s: String, n: Int): String = if n <= 0 then s @@ -254,11 +249,10 @@ object MarkdownParser: ) ) - /** Detects a line that starts with `>` (a blockquote prefix) and then opens a - * Scala fence. We reject these explicitly: marklit does not extract code - * from inside blockquotes, since output injection would break the quote - * structure. Returns Some((lineNumber, columnOfGreaterThan)) for the first - * offending line, or None if no offense is found. + /** Detects a line that starts with `>` (a blockquote prefix) and then opens a Scala fence. We reject these + * explicitly: marklit does not extract code from inside blockquotes, since output injection would break the quote + * structure. Returns Some((lineNumber, columnOfGreaterThan)) for the first offending line, or None if no offense is + * found. */ private def detectBlockquotedScalaFence( normalized: String @@ -320,6 +314,5 @@ object MarkdownParser: } // Internal sentinel for the blockquoted-fence rejection path. - private class BlockquoteFenceException(val line: Int, val col: Int) - extends RuntimeException + private class BlockquoteFenceException(val line: Int, val col: Int) extends RuntimeException end MarkdownParser diff --git a/core/src/main/scala/marklit/processor/DocumentProcessor.scala b/core/src/main/scala/marklit/processor/DocumentProcessor.scala index e9b55a7..f012c79 100644 --- a/core/src/main/scala/marklit/processor/DocumentProcessor.scala +++ b/core/src/main/scala/marklit/processor/DocumentProcessor.scala @@ -7,9 +7,8 @@ import zio.* /** A single per-version compile+execute attempt for a cross-version block. * - * Populated only for `scala=shared` / `scala=shared-{mv}` blocks, which are - * compiled and executed once per applicable version in the document. For - * non-shared blocks [[BlockResult.crossExecutions]] is empty. + * Populated only for `scala=shared` / `scala=shared-{mv}` blocks, which are compiled and executed once per applicable + * version in the document. For non-shared blocks [[BlockResult.crossExecutions]] is empty. */ final case class BlockExecution( scalaVersion: String, @@ -20,11 +19,10 @@ final case class BlockExecution( /** Result of processing a single code block. * - * For non-shared blocks, [[compileResult]] / [[executionOutput]] / [[error]] - * hold the single result and [[crossExecutions]] is empty. For shared blocks - * (`scala=shared`, `scala=shared-{mv}`) all per-version attempts are in - * [[crossExecutions]]; the legacy single fields mirror the *first* - * cross-execution so existing single-result consumers keep working. + * For non-shared blocks, [[compileResult]] / [[executionOutput]] / [[error]] hold the single result and + * [[crossExecutions]] is empty. For shared blocks (`scala=shared`, `scala=shared-{mv}`) all per-version attempts are + * in [[crossExecutions]]; the legacy single fields mirror the *first* cross-execution so existing single-result + * consumers keep working. */ final case class BlockResult( block: CodeBlock, @@ -48,16 +46,13 @@ final case class BlockResult( /** A block is successful if: * - It was skipped (version mismatch), OR - * - For non-shared blocks: compilation succeeded AND no unexpected errors - * occurred. For crash blocks, a RuntimeError is expected and counts as - * success. - * - For shared blocks: every cross-version execution succeeded under the - * same rules. + * - For non-shared blocks: compilation succeeded AND no unexpected errors occurred. For crash blocks, a + * RuntimeError is expected and counts as success. + * - For shared blocks: every cross-version execution succeeded under the same rules. */ def isSuccess: Boolean = if skipped then true - else if crossExecutions.nonEmpty then - crossExecutions.forall(x => isExecutionSuccess(x.compileResult, x.error)) + else if crossExecutions.nonEmpty then crossExecutions.forall(x => isExecutionSuccess(x.compileResult, x.error)) else isExecutionSuccess(compileResult, error) end BlockResult @@ -68,9 +63,8 @@ final case class DocumentResult( ): def isSuccess: Boolean = blockResults.forall(_.isSuccess) - /** Returns unexpected errors (excludes expected RuntimeErrors from crash - * blocks). Walks per-version cross-executions for shared blocks so a failure - * on any single cross version is surfaced. + /** Returns unexpected errors (excludes expected RuntimeErrors from crash blocks). Walks per-version cross-executions + * for shared blocks so a failure on any single cross version is surfaced. */ def errors: Vector[(CodeBlock, MarklitError)] = blockResults.flatMap { br => @@ -87,8 +81,7 @@ final case class DocumentResult( def compileErrors: Vector[(CodeBlock, List[ScalaDiagnostic])] = blockResults.flatMap { br => val crs = - if br.crossExecutions.nonEmpty then - br.crossExecutions.flatMap(_.compileResult) + if br.crossExecutions.nonEmpty then br.crossExecutions.flatMap(_.compileResult) else br.compileResult.toVector crs.filterNot(_.success).map(cr => (br.block, cr.diagnostics)) } @@ -105,29 +98,25 @@ object DocumentProcessor: ): ZIO[DocumentProcessor, MarklitError, DocumentResult] = ZIO.serviceWithZIO[DocumentProcessor](_.process(blocks)) -/** Compiler abstraction for DocumentProcessor - decoupled from actual compiler - * impl. +/** Compiler abstraction for DocumentProcessor - decoupled from actual compiler impl. * - * `scalaVersion` is the version of the *default* compiler this service is - * bound to (used for filtering blocks that target a different major). When a - * block requests a specific version (e.g. `scala=3.7.0`), the service must - * obtain a compiler for that version via [[forVersion]]. + * `scalaVersion` is the version of the *default* compiler this service is bound to (used for filtering blocks that + * target a different major). When a block requests a specific version (e.g. `scala=3.7.0`), the service must obtain a + * compiler for that version via [[forVersion]]. */ trait CompilerService: /** The default Scala version used when a block does not specify one. */ def defaultScalaVersion: String - /** A default version for the requested major. Used to resolve bare-major - * scope-config requests (`scala=2`, `scala=3`) to a concrete version when - * the service's [[defaultScalaVersion]] is not in the requested major. The - * default for the matching major is [[defaultScalaVersion]] itself. + /** A default version for the requested major. Used to resolve bare-major scope-config requests (`scala=2`, `scala=3`) + * to a concrete version when the service's [[defaultScalaVersion]] is not in the requested major. The default for + * the matching major is [[defaultScalaVersion]] itself. * - * Returns `None` if marklit can't pick a default for that major (e.g. an - * unsupported major like `1`); the block is then skipped. + * Returns `None` if marklit can't pick a default for that major (e.g. an unsupported major like `1`); the block is + * then skipped. */ def defaultVersionForMajor(major: String): Option[String] = - if defaultScalaVersion.takeWhile(_ != '.') == major then - Some(defaultScalaVersion) + if defaultScalaVersion.takeWhile(_ != '.') == major then Some(defaultScalaVersion) else None def compile( @@ -153,8 +142,7 @@ trait CompilerService: ): IO[MarklitError, String] end CompilerService -/** Live implementation that processes blocks sequentially, respecting scope - * dependencies +/** Live implementation that processes blocks sequentially, respecting scope dependencies */ final class DocumentProcessorLive( scopeManager: ScopeManager, @@ -164,9 +152,8 @@ final class DocumentProcessorLive( private val scalaVersion: String = compiler.defaultScalaVersion - /** Compute the version a block will be compiled against. Mirrors the - * precedence used by [[processBlock]]: per-block specific version, then - * bare-major, then `shared-{mv}` major, then service default. + /** Compute the version a block will be compiled against. Mirrors the precedence used by [[processBlock]]: per-block + * specific version, then bare-major, then `shared-{mv}` major, then service default. * * Returns `Left(())` when a bare-major has no default (block is skipped). */ @@ -185,13 +172,11 @@ final class DocumentProcessorLive( compiler.defaultVersionForMajor(m).toRight(()) case None => Right(scalaVersion) - /** Under page-scope, rewrite each otherwise-anonymous block's config to the - * exact shape a user could write by hand: the first such block (per Scala - * version) becomes `id=__page__`, every subsequent one becomes - * `extends=__page__, append`. Blocks that already carry an explicit - * `id`/`extends`/`append`, plus `passthrough`, `shared`, `fail`, `crash`, - * and `warn` blocks, are passed through untouched — they opt out of the page - * scope the same way they would in a hand-written file. + /** Under page-scope, rewrite each otherwise-anonymous block's config to the exact shape a user could write by hand: + * the first such block (per Scala version) becomes `id=__page__`, every subsequent one becomes + * `extends=__page__, append`. Blocks that already carry an explicit `id`/`extends`/`append`, plus `passthrough`, + * `shared`, `fail`, `crash`, and `warn` blocks, are passed through untouched — they opt out of the page scope the + * same way they would in a hand-written file. */ private def applyPageScope( blocks: Vector[CodeBlock] @@ -214,8 +199,7 @@ final class DocumentProcessorLive( case Right(v) => val pageId = MarklitScope.pageIdFor(v) val newCfg = - if initialized.contains(pageId) then - cfg.copy(extendsScope = Some(pageId), append = true) + if initialized.contains(pageId) then cfg.copy(extendsScope = Some(pageId), append = true) else cfg.copy(id = Some(pageId)) (acc :+ block.copy(scopeConfig = newCfg), initialized + pageId) } @@ -348,16 +332,13 @@ final class DocumentProcessorLive( case _ => (scalaVersion, None) // Handle passthrough blocks - no processing - if block.isPassthrough then - ZIO.succeed(BlockResult(block, None, None, None)) + if block.isPassthrough then ZIO.succeed(BlockResult(block, None, None, None)) // Skip when bare-major requests a major we have no default for. - else if resolvedVersion.contains(Left(())) then - ZIO.succeed(BlockResult(block, None, None, None, skipped = true)) + else if resolvedVersion.contains(Left(())) then ZIO.succeed(BlockResult(block, None, None, None, skipped = true)) // `scala=shared` / `scala=shared-{mv}` blocks fan out: compile+execute // once per applicable version in use, and never participate in a normal // (non-default) scope. - else if block.isAnyShared then - processSharedBlock(block, effectiveVersion, versionsInUse) + else if block.isAnyShared then processSharedBlock(block, effectiveVersion, versionsInUse) else (for // Resolve scope and get inherited code, split into hoisted top-level @@ -415,11 +396,9 @@ final class DocumentProcessorLive( end if end processBlockChecked - /** Fan-out path for `scala=shared` / `scala=shared-{mv}`: compile+execute the - * same source against every applicable version in use, gathering one - * [[BlockExecution]] per version. The block is already pre-seeded into each - * version's default-scope priorCode, so we strip the block's own code from - * the inherited prior to avoid compiling it twice. + /** Fan-out path for `scala=shared` / `scala=shared-{mv}`: compile+execute the same source against every applicable + * version in use, gathering one [[BlockExecution]] per version. The block is already pre-seeded into each version's + * default-scope priorCode, so we strip the block's own code from the inherited prior to avoid compiling it twice. */ private def processSharedBlock( block: CodeBlock, diff --git a/core/src/main/scala/marklit/renderer/MarkdownRenderer.scala b/core/src/main/scala/marklit/renderer/MarkdownRenderer.scala index 4ef668a..e36686a 100644 --- a/core/src/main/scala/marklit/renderer/MarkdownRenderer.scala +++ b/core/src/main/scala/marklit/renderer/MarkdownRenderer.scala @@ -82,8 +82,7 @@ object MarkdownRenderer: // Cross-version (`scala=shared` / `scala=shared-{mv}`) blocks have one // BlockExecution per applicable version. Render dedup'd or per-version // labeled — the helper picks the format. - if br.crossExecutions.nonEmpty then - renderCrossExecutions(sb, block, br.crossExecutions, config) + if br.crossExecutions.nonEmpty then renderCrossExecutions(sb, block, br.crossExecutions, config) else val ver = br.effectiveScalaVersion renderSingleExecution(sb, block, br, ver, config) @@ -113,8 +112,7 @@ object MarkdownRenderer: br.compileResult.foreach { cr => val warnings = cr.diagnostics.filter(_.severity == DiagnosticSeverity.Warning) - if warnings.nonEmpty && config.showCompileErrors then - renderDiagnostics(sb, warnings, config, ver) + if warnings.nonEmpty && config.showCompileErrors then renderDiagnostics(sb, warnings, config, ver) } // Also show normal output if any if block.showOutput then @@ -124,12 +122,10 @@ object MarkdownRenderer: // For crash blocks, show the exception else if block.expectsCrash then br.error.foreach { - case MarklitError.RuntimeError(ex, output) - if config.showRuntimeErrors => + case MarklitError.RuntimeError(ex, output) if config.showRuntimeErrors => if output.nonEmpty then renderOutput(sb, output, config, ver) sb.append("```\n") - if config.showScalaVersion then - ver.foreach(v => sb.append(s"// Scala $v\n")) + if config.showScalaVersion then ver.foreach(v => sb.append(s"// Scala $v\n")) sb.append( s"${config.errorPrefix}Exception: ${ex.getClass.getSimpleName}: ${ex.getMessage}\n" ) @@ -149,8 +145,7 @@ object MarkdownRenderer: br.compileResult.foreach { cr => val warnings = cr.diagnostics.filter(_.severity == DiagnosticSeverity.Warning) - if warnings.nonEmpty then - renderDiagnostics(sb, warnings, config, ver) + if warnings.nonEmpty then renderDiagnostics(sb, warnings, config, ver) } br.executionOutput.filter(_.nonEmpty).foreach { output => renderOutput(sb, output, config, ver) @@ -169,9 +164,8 @@ object MarkdownRenderer: scalaVersion.filter(_ => config.showScalaVersion).map(v => s"// Scala $v") ) - /** Emit a fenced output block with an optional pre-output header line (used - * by both per-version `// Scala X` labels and the `// All cross versions:` - * dedup'd header). + /** Emit a fenced output block with an optional pre-output header line (used by both per-version `// Scala X` labels + * and the `// All cross versions:` dedup'd header). */ private def renderOutputWithHeader( sb: StringBuilder, @@ -179,8 +173,7 @@ object MarkdownRenderer: config: RenderConfig, header: Option[String] ): Unit = - if config.outputFenceLanguage.nonEmpty then - sb.append(s"```${config.outputFenceLanguage}\n") + if config.outputFenceLanguage.nonEmpty then sb.append(s"```${config.outputFenceLanguage}\n") else sb.append("```\n") header.foreach(h => sb.append(s"$h\n")) @@ -190,11 +183,10 @@ object MarkdownRenderer: sb.append("```\n") end renderOutputWithHeader - /** Render a `scala=shared` / `scala=shared-{mv}` block's per-version - * executions. When every execution succeeded with identical stdout, render a - * single output block headed by `// All cross versions: Scala v1, v2, …`. - * Otherwise emit one labeled output (or compile-error / runtime-error) block - * per execution, in the order versions appear in [[crossExecutions]]. + /** Render a `scala=shared` / `scala=shared-{mv}` block's per-version executions. When every execution succeeded with + * identical stdout, render a single output block headed by `// All cross versions: Scala v1, v2, …`. Otherwise emit + * one labeled output (or compile-error / runtime-error) block per execution, in the order versions appear in + * [[crossExecutions]]. */ private def renderCrossExecutions( sb: StringBuilder, @@ -224,8 +216,7 @@ object MarkdownRenderer: val ver = Some(x.scalaVersion) x.compileResult match case Some(cr) if !cr.success => - if config.showCompileErrors then - renderDiagnostics(sb, cr.errors, config, ver) + if config.showCompileErrors then renderDiagnostics(sb, cr.errors, config, ver) case _ => if block.showWarnings(config.showCompileWarnings) then x.compileResult.foreach { cr => @@ -233,19 +224,16 @@ object MarkdownRenderer: cr.diagnostics.filter( _.severity == DiagnosticSeverity.Warning ) - if warnings.nonEmpty then - renderDiagnostics(sb, warnings, config, ver) + if warnings.nonEmpty then renderDiagnostics(sb, warnings, config, ver) } x.executionOutput.filter(_.nonEmpty).foreach { out => renderOutput(sb, out, config, ver) } x.error.foreach { - case MarklitError.RuntimeError(ex, runOut) - if config.showRuntimeErrors => + case MarklitError.RuntimeError(ex, runOut) if config.showRuntimeErrors => if runOut.nonEmpty then renderOutput(sb, runOut, config, ver) sb.append("```\n") - if config.showScalaVersion then - sb.append(s"// Scala ${x.scalaVersion}\n") + if config.showScalaVersion then sb.append(s"// Scala ${x.scalaVersion}\n") sb.append( s"${config.errorPrefix}Exception: ${ex.getClass.getSimpleName}: ${ex.getMessage}\n" ) @@ -364,8 +352,7 @@ object MarkdownRenderer: if effectiveShowWarnings then val warnings = entry.compileErrors.filter(_.severity == "warning") - if warnings.nonEmpty then - renderDiagnosticsFromEntries(sb, warnings, config, versionTag) + if warnings.nonEmpty then renderDiagnosticsFromEntries(sb, warnings, config, versionTag) entry.executionOutput.filter(_.nonEmpty).foreach { output => renderOutput(sb, output, config, versionTag) } @@ -374,8 +361,7 @@ object MarkdownRenderer: entry.runtimeError.foreach { err => if config.showRuntimeErrors then sb.append("```\n") - if config.showScalaVersion then - sb.append(s"// Scala ${entry.scalaVersion}\n") + if config.showScalaVersion then sb.append(s"// Scala ${entry.scalaVersion}\n") sb.append(s"${config.errorPrefix}$err\n") sb.append("```\n") } diff --git a/core/src/main/scala/marklit/resolver/DependencyResolver.scala b/core/src/main/scala/marklit/resolver/DependencyResolver.scala index 3767b32..cff9d12 100644 --- a/core/src/main/scala/marklit/resolver/DependencyResolver.scala +++ b/core/src/main/scala/marklit/resolver/DependencyResolver.scala @@ -32,16 +32,14 @@ object DependencyResolver: // org::name:version (cross-built) private def crossBuiltCoord[$: P]: P[MavenCoord] = - P(coordPart ~ "::" ~ coordPart ~ ":" ~ coordPart).map { - case (org, name, version) => - MavenCoord(org, name, version, crossBuilt = true) + P(coordPart ~ "::" ~ coordPart ~ ":" ~ coordPart).map { case (org, name, version) => + MavenCoord(org, name, version, crossBuilt = true) } // org:name:version (exact artifact) private def exactCoord[$: P]: P[MavenCoord] = - P(coordPart ~ ":" ~ coordPart ~ ":" ~ coordPart).map { - case (org, name, version) => - MavenCoord(org, name, version, crossBuilt = false) + P(coordPart ~ ":" ~ coordPart ~ ":" ~ coordPart).map { case (org, name, version) => + MavenCoord(org, name, version, crossBuilt = false) } // Full coordinate parser - try cross-built first @@ -123,8 +121,7 @@ object DependencyResolver: // Add custom repositories repositories.foreach { repoStr => - if repoStr.startsWith("ivy:") then - fetch.addRepositories(IvyRepository.of(repoStr.stripPrefix("ivy:"))) + if repoStr.startsWith("ivy:") then fetch.addRepositories(IvyRepository.of(repoStr.stripPrefix("ivy:"))) else fetch.addRepositories(MavenRepository.of(repoStr)) } @@ -149,10 +146,9 @@ object DependencyResolver: /** Accumulated using directives from a document. * - * `dependencies`, `repositories`, and `scalacOptions` are insertion-ordered - * sets — duplicates within or across blocks collapse to a single entry, - * which prevents dotc/nsc from emitting "set repeatedly" warnings when the - * same flag appears in multiple blocks of one file. + * `dependencies`, `repositories`, and `scalacOptions` are insertion-ordered sets — duplicates within or across + * blocks collapse to a single entry, which prevents dotc/nsc from emitting "set repeatedly" warnings when the same + * flag appears in multiple blocks of one file. */ case class UsingDirectives( dependencies: ListSet[String] = ListSet.empty, @@ -174,11 +170,11 @@ object DependencyResolver: extension (ud: UsingDirectives) def addDirective(d: Directive): UsingDirectives = d match - case Directive.Dep(v) => ud.copy(dependencies = ud.dependencies + v) - case Directive.Deps(vs) => ud.copy(dependencies = ud.dependencies ++ vs) - case Directive.Scala(v) => ud.copy(scalaVersion = Some(v)) - case Directive.Repo(v) => ud.copy(repositories = ud.repositories + v) - case Directive.Repos(vs) => ud.copy(repositories = ud.repositories ++ vs) + case Directive.Dep(v) => ud.copy(dependencies = ud.dependencies + v) + case Directive.Deps(vs) => ud.copy(dependencies = ud.dependencies ++ vs) + case Directive.Scala(v) => ud.copy(scalaVersion = Some(v)) + case Directive.Repo(v) => ud.copy(repositories = ud.repositories + v) + case Directive.Repos(vs) => ud.copy(repositories = ud.repositories ++ vs) case Directive.ScalacOption(v) => ud.copy(scalacOptions = ud.scalacOptions + v) case Directive.ScalacOptions(vs) => @@ -213,28 +209,27 @@ object DependencyResolver: // Using directive line: //> using key value(s) private def usingDirective[$: P]: P[Directive] = - P("//>" ~ ws ~ "using" ~ ws1 ~ identifier ~ ws1 ~ valueList).map { - case (key, values) => - key.toLowerCase match - case "dep" | "dependency" | "lib" | "library" => - if values.size == 1 then Directive.Dep(values.head) - else Directive.Deps(values) - case "deps" | "dependencies" | "libs" | "libraries" => - Directive.Deps(values) - case "scala" => - Directive.Scala(values.headOption.getOrElse("")) - case "repo" | "repository" => - if values.size == 1 then Directive.Repo(values.head) - else Directive.Repos(values) - case "repos" | "repositories" => - Directive.Repos(values) - case "option" | "scalac" => - if values.size == 1 then Directive.ScalacOption(values.head) - else Directive.ScalacOptions(values) - case "options" => - Directive.ScalacOptions(values) - case other => - Directive.Unknown(other, values.mkString(", ")) + P("//>" ~ ws ~ "using" ~ ws1 ~ identifier ~ ws1 ~ valueList).map { case (key, values) => + key.toLowerCase match + case "dep" | "dependency" | "lib" | "library" => + if values.size == 1 then Directive.Dep(values.head) + else Directive.Deps(values) + case "deps" | "dependencies" | "libs" | "libraries" => + Directive.Deps(values) + case "scala" => + Directive.Scala(values.headOption.getOrElse("")) + case "repo" | "repository" => + if values.size == 1 then Directive.Repo(values.head) + else Directive.Repos(values) + case "repos" | "repositories" => + Directive.Repos(values) + case "option" | "scalac" => + if values.size == 1 then Directive.ScalacOption(values.head) + else Directive.ScalacOptions(values) + case "options" => + Directive.ScalacOptions(values) + case other => + Directive.Unknown(other, values.mkString(", ")) } // Try to parse a line as a using directive @@ -260,8 +255,8 @@ object DependencyResolver: acc.merge(parseUsingDirectives(code)) } - /** Resolve Scala library jars for compilation. For Scala 3, only need - * scala3-library which transitively brings in scala-library. + /** Resolve Scala library jars for compilation. For Scala 3, only need scala3-library which transitively brings in + * scala-library. * * @param scalaVersion * Full Scala version (e.g., "3.8.2") @@ -284,26 +279,22 @@ object DependencyResolver: Vector( Dependency.of("org.scala-lang", "scala3-library_3", scalaVersion) ) - else - Vector(Dependency.of("org.scala-lang", "scala-library", scalaVersion)) + else Vector(Dependency.of("org.scala-lang", "scala-library", scalaVersion)) val fetch = Fetch.create().addDependencies(deps*) fetch.fetch().asScala.toVector.map(_.getAbsolutePath) end resolveScalaLibrarySync - /** Resolve compiler jars (with all transitive deps, including the matching - * stdlib). Used by CompilerFactory to build a per-version classloader for - * invoking either dotc (Scala 3) or nsc (Scala 2.13) reflectively. + /** Resolve compiler jars (with all transitive deps, including the matching stdlib). Used by CompilerFactory to build + * a per-version classloader for invoking either dotc (Scala 3) or nsc (Scala 2.13) reflectively. * - * Scala 2.13.x maps to `org.scala-lang:scala-compiler:`. Earlier - * 2.x lines (2.12, 2.11) are explicitly rejected — marklit only supports - * 2.13 and 3.x. + * Scala 2.13.x maps to `org.scala-lang:scala-compiler:`. Earlier 2.x lines (2.12, 2.11) are explicitly + * rejected — marklit only supports 2.13 and 3.x. * * @param scalaVersion * Full Scala version (e.g. "3.7.0", "2.13.16"). * @return - * List of JAR file paths sufficient to invoke the matching compiler. Order - * is the URLClassLoader scan order. + * List of JAR file paths sufficient to invoke the matching compiler. Order is the URLClassLoader scan order. */ def resolveScalaCompiler(scalaVersion: String): Task[Vector[String]] = ZIO.attemptBlocking { @@ -313,10 +304,8 @@ object DependencyResolver: /** Synchronous version for use in CompilerFactory initialization */ def resolveScalaCompilerSync(scalaVersion: String): Vector[String] = val dep = - if scalaVersion.startsWith("3") then - Dependency.of("org.scala-lang", "scala3-compiler_3", scalaVersion) - else if scalaVersion.startsWith("2.13") then - Dependency.of("org.scala-lang", "scala-compiler", scalaVersion) + if scalaVersion.startsWith("3") then Dependency.of("org.scala-lang", "scala3-compiler_3", scalaVersion) + else if scalaVersion.startsWith("2.13") then Dependency.of("org.scala-lang", "scala-compiler", scalaVersion) else throw new IllegalArgumentException( s"Unsupported Scala version '$scalaVersion'. Marklit supports 2.13.x and 3.x; earlier 2.x lines are not supported." diff --git a/core/src/main/scala/marklit/results/ResultsFile.scala b/core/src/main/scala/marklit/results/ResultsFile.scala index 7080b1b..e366d3d 100644 --- a/core/src/main/scala/marklit/results/ResultsFile.scala +++ b/core/src/main/scala/marklit/results/ResultsFile.scala @@ -45,8 +45,7 @@ object BlockResultEntry: def fromBlockResult(br: BlockResult, scalaVersion: String): BlockResultEntry = BlockResultEntry( - locationKey = - s"${br.block.location.file}:${br.block.location.startLine}:${br.block.location.startColumn}", + locationKey = s"${br.block.location.file}:${br.block.location.startLine}:${br.block.location.startColumn}", scalaVersion = scalaVersion, code = br.block.code, success = br.isSuccess, @@ -129,8 +128,7 @@ object MergedResults: given JsonCodec[RunResults] = RunResults.given_JsonCodec_RunResults val parsed = jsons.map(_.fromJson[RunResults]) val errors = parsed.collect { case Left(e) => e } - if errors.nonEmpty then - Left(s"Failed to parse results: ${errors.mkString(", ")}") + if errors.nonEmpty then Left(s"Failed to parse results: ${errors.mkString(", ")}") else val results = parsed.collect { case Right(r) => r } Right(merge(results)) diff --git a/core/src/main/scala/marklit/scope/ScopeManager.scala b/core/src/main/scala/marklit/scope/ScopeManager.scala index b25036e..06ea4b7 100644 --- a/core/src/main/scala/marklit/scope/ScopeManager.scala +++ b/core/src/main/scala/marklit/scope/ScopeManager.scala @@ -5,12 +5,10 @@ import zio.* /** A scope node in the scope tree. * - * `topLevel` marks a scope whose code is compiled at file scope (no - * `MarklitWrapper` wrapper). Each scope is single-kind: top-level scopes hold - * only top-level definitions, normal scopes hold run-body code. A top-level - * scope can only be extended/appended by another top-level scope; a normal - * scope may *extend* (but not append to) a top-level scope, hoisting its - * definitions above the wrapper. + * `topLevel` marks a scope whose code is compiled at file scope (no `MarklitWrapper` wrapper). Each scope is + * single-kind: top-level scopes hold only top-level definitions, normal scopes hold run-body code. A top-level scope + * can only be extended/appended by another top-level scope; a normal scope may *extend* (but not append to) a + * top-level scope, hoisting its definitions above the wrapper. */ final case class Scope( id: String, @@ -26,22 +24,20 @@ final case class Scope( end Scope object Scope: - /** Legacy single-default id, kept as the bare-version default for blocks - * resolved without an effective Scala version. New per-version default ids - * are produced by [[defaultIdFor]]. + /** Legacy single-default id, kept as the bare-version default for blocks resolved without an effective Scala version. + * New per-version default ids are produced by [[defaultIdFor]]. */ val defaultId = "__default__" - /** Default-scope id for a specific Scala version (e.g. "__default__3.7.3"). - * If `version` is empty, returns [[defaultId]] for backward compatibility. + /** Default-scope id for a specific Scala version (e.g. "__default__3.7.3"). If `version` is empty, returns + * [[defaultId]] for backward compatibility. */ def defaultIdFor(version: String): String = if version.isEmpty then defaultId else s"${defaultId}${version}" - /** Page-scope id for a specific Scala version. Used when the CLI/plugin opts - * a file into shared-by-default behavior: every anonymous block extends this - * scope (with `append`), parented to the per-version default so `shared` - * blocks still seed it. + /** Page-scope id for a specific Scala version. Used when the CLI/plugin opts a file into shared-by-default behavior: + * every anonymous block extends this scope (with `append`), parented to the per-version default so `shared` blocks + * still seed it. */ val pageId = "__page__" def pageIdFor(version: String): String = @@ -63,39 +59,36 @@ end Scope /** Result of resolving a scope for a code block. * - * `inheritedTopLevel` / `inheritedBody` are the ancestor code split by kind: - * code from top-level ancestor scopes (hoisted above the wrapper) vs. code - * from normal ancestor scopes (run-body). Because a top-level scope can only - * be extended by another top-level scope, top-level ancestors always form a - * prefix of the chain, so `inheritedTopLevel ++ inheritedBody` preserves the - * original ancestor order. + * `inheritedTopLevel` / `inheritedBody` are the ancestor code split by kind: code from top-level ancestor scopes + * (hoisted above the wrapper) vs. code from normal ancestor scopes (run-body). Because a top-level scope can only be + * extended by another top-level scope, top-level ancestors always form a prefix of the chain, so + * `inheritedTopLevel ++ inheritedBody` preserves the original ancestor order. */ final case class ResolvedScope( scope: Scope, inheritedTopLevel: Vector[String], inheritedBody: Vector[String] ): - /** Flat inherited code in ancestor order (top-level ancestors first). Kept - * for callers that don't care about hoist placement. + /** Flat inherited code in ancestor order (top-level ancestors first). Kept for callers that don't care about hoist + * placement. */ def inheritedCode: Vector[String] = inheritedTopLevel ++ inheritedBody - /** Definitions to emit at file scope: inherited top-level ancestor code, plus - * this scope's own code when it is itself a top-level scope. + /** Definitions to emit at file scope: inherited top-level ancestor code, plus this scope's own code when it is itself + * a top-level scope. */ def hoistCode: Vector[String] = if scope.topLevel then inheritedTopLevel ++ scope.priorCode else inheritedTopLevel - /** Code to emit in the wrapper body: inherited normal-ancestor code, plus - * this scope's own code when it is a normal scope. + /** Code to emit in the wrapper body: inherited normal-ancestor code, plus this scope's own code when it is a normal + * scope. */ def bodyCode: Vector[String] = if scope.topLevel then inheritedBody else inheritedBody ++ scope.priorCode - /** All code in order: inherited from ancestors + this scope's accumulated - * code + /** All code in order: inherited from ancestors + this scope's accumulated code */ def allCode: String = (inheritedCode ++ scope.priorCode).mkString("\n\n") @@ -105,10 +98,9 @@ end ResolvedScope trait ScopeManager: /** Get or create a scope for a code block based on its config. * - * `effectiveVersion` is the Scala version this block will be compiled - * against (block override > file default > CLI default). When the block has - * no explicit scope (`config` is empty of id/extends/append), we resolve to - * a per-version default scope keyed by `effectiveVersion`. + * `effectiveVersion` is the Scala version this block will be compiled against (block override > file default > CLI + * default). When the block has no explicit scope (`config` is empty of id/extends/append), we resolve to a + * per-version default scope keyed by `effectiveVersion`. */ def resolveScope( config: ScopeConfig, @@ -117,11 +109,10 @@ trait ScopeManager: topLevel: Boolean = false ): IO[MarklitError, ResolvedScope] - /** Seed a per-version default scope's prior code (used to inject `shared` / - * `shared-{mv}` blocks before any block of that version is processed). + /** Seed a per-version default scope's prior code (used to inject `shared` / `shared-{mv}` blocks before any block of + * that version is processed). * - * Idempotent against the same `(version, code)` pair: appends only if the - * exact code isn't already present. + * Idempotent against the same `(version, code)` pair: appends only if the exact code isn't already present. */ def seedDefaultPriorCode(version: String, code: String): UIO[Unit] @@ -216,8 +207,7 @@ private object ScopeState: ) /** Live implementation of ScopeManager */ -private final class ScopeManagerLive(stateRef: Ref[ScopeState]) - extends ScopeManager: +private final class ScopeManagerLive(stateRef: Ref[ScopeState]) extends ScopeManager: override def resolveScope( config: ScopeConfig, @@ -297,8 +287,7 @@ private final class ScopeManagerLive(stateRef: Ref[ScopeState]) // at creation. def sameKindError(existing: Scope): Option[MarklitError] = if existing.topLevel == topLevel then None - else - Some(MarklitError.ValidationError(location, kindMismatch(existing.id))) + else Some(MarklitError.ValidationError(location, kindMismatch(existing.id))) (config.id, config.extendsScope, config.append) match // No scope config - fresh anonymous scope per block (isolated by @@ -412,8 +401,7 @@ private final class ScopeManagerLive(stateRef: Ref[ScopeState]) case None => // Re-use of `id=foo` must not flip the version. (config.scalaVersion, existing.scalaVersion) match - case (Some(reqV), Some(existingV)) - if reqV != existingV => + case (Some(reqV), Some(existingV)) if reqV != existingV => ( Left( MarklitError.ValidationError( @@ -551,11 +539,9 @@ private final class ScopeManagerLive(stateRef: Ref[ScopeState]) end match end resolveInternal - /** Collect all code from ancestors in order (oldest first), split by scope - * kind: `(topLevelCode, bodyCode)`. Because a top-level scope can only be - * extended by another top-level scope, top-level ancestors always form a - * prefix of the chain — so concatenating `topLevelCode ++ bodyCode` recovers - * the original ancestor order. + /** Collect all code from ancestors in order (oldest first), split by scope kind: `(topLevelCode, bodyCode)`. Because + * a top-level scope can only be extended by another top-level scope, top-level ancestors always form a prefix of the + * chain — so concatenating `topLevelCode ++ bodyCode` recovers the original ancestor order. */ private def collectInheritedCode( scopeId: String, diff --git a/core/src/test/scala/marklit/model/CodeBlockSpec.scala b/core/src/test/scala/marklit/model/CodeBlockSpec.scala index 0ca06db..1476582 100644 --- a/core/src/test/scala/marklit/model/CodeBlockSpec.scala +++ b/core/src/test/scala/marklit/model/CodeBlockSpec.scala @@ -71,9 +71,7 @@ object CodeBlockSpec extends ZIOSpecDefault: Modifier.SharedMajor("3") ) assertTrue( - bad.forall(m => - blockWith(Set(Modifier.TopLevel, m)).modifierConflicts.isDefined - ) + bad.forall(m => blockWith(Set(Modifier.TopLevel, m)).modifierConflicts.isDefined) ) }, test("modifierConflicts is None for a non-top-level block") { diff --git a/core/src/test/scala/marklit/parser/MarkdownParserEdgeCasesSpec.scala b/core/src/test/scala/marklit/parser/MarkdownParserEdgeCasesSpec.scala index 9127ed3..d1c74d6 100644 --- a/core/src/test/scala/marklit/parser/MarkdownParserEdgeCasesSpec.scala +++ b/core/src/test/scala/marklit/parser/MarkdownParserEdgeCasesSpec.scala @@ -5,13 +5,11 @@ import zio.test.* /** Edge case audit suite for [[MarkdownParser]]. * - * Each test encodes the **CommonMark-correct expectation** for a known or - * suspected divergence in marklit's fence scanner. Tests prefixed with - * `// AUDIT:` are predicted to currently fail; their failure output is the - * deliverable for deciding whether to fix piecemeal or rewrite the scanner. + * Each test encodes the **CommonMark-correct expectation** for a known or suspected divergence in marklit's fence + * scanner. Tests prefixed with `// AUDIT:` are predicted to currently fail; their failure output is the deliverable + * for deciding whether to fix piecemeal or rewrite the scanner. * - * Categories track the plan in - * `~/.claude/plans/i-want-to-harden-squishy-micali.md`: + * Categories track the plan in `~/.claude/plans/i-want-to-harden-squishy-micali.md`: * - A: opening fence indentation * - B: closing fence length * - C: closing fence character match diff --git a/core/src/test/scala/marklit/processor/DocumentProcessorSpec.scala b/core/src/test/scala/marklit/processor/DocumentProcessorSpec.scala index ef6084a..5803171 100644 --- a/core/src/test/scala/marklit/processor/DocumentProcessorSpec.scala +++ b/core/src/test/scala/marklit/processor/DocumentProcessorSpec.scala @@ -11,10 +11,9 @@ object DocumentProcessorSpec extends ZIOSpecDefault: /** Test compiler that tracks calls and returns configurable results. * - * `defaultScalaVersion` is what the processor sees as the runtime default - * (used by bare-major filtering). Per-block specific-version requests (e.g. - * `scala=3.7.0`) are recorded in [[compileCallsWithVersion]] so tests can - * assert that the right version was demanded. + * `defaultScalaVersion` is what the processor sees as the runtime default (used by bare-major filtering). Per-block + * specific-version requests (e.g. `scala=3.7.0`) are recorded in [[compileCallsWithVersion]] so tests can assert + * that the right version was demanded. */ class TestCompiler( compileResults: Map[String, CompileResult] = Map.empty, @@ -280,8 +279,7 @@ object DocumentProcessorSpec extends ZIOSpecDefault: val block = makeBlock("invalid syntax", Set(Modifier.Fail)) val failResult = CompileResult( success = false, - diagnostics = - List(ScalaDiagnostic(DiagnosticSeverity.Error, "error", 1, 1, None)) + diagnostics = List(ScalaDiagnostic(DiagnosticSeverity.Error, "error", 1, 1, None)) ) val compiler = new TestCompiler(compileResults = Map("invalid syntax" -> failResult)) @@ -325,9 +323,7 @@ object DocumentProcessorSpec extends ZIOSpecDefault: ) ) ) - val compiler = new TestCompiler(compileResults = - Map("val broken: String = 42" -> failResult) - ) + val compiler = new TestCompiler(compileResults = Map("val broken: String = 42" -> failResult)) for scopeManager <- ZIO.service[ScopeManager] @@ -371,9 +367,7 @@ object DocumentProcessorSpec extends ZIOSpecDefault: ) ) ) - val compiler = new TestCompiler(compileResults = - Map("@deprecated val x = 1" -> warnResult) - ) + val compiler = new TestCompiler(compileResults = Map("@deprecated val x = 1" -> warnResult)) for scopeManager <- ZIO.service[ScopeManager] @@ -662,9 +656,7 @@ object DocumentProcessorSpec extends ZIOSpecDefault: ) ) ) - val compiler = new TestCompiler(compileResults = - Map("val x: Int = \"string\"" -> errorResult) - ) + val compiler = new TestCompiler(compileResults = Map("val x: Int = \"string\"" -> errorResult)) for scopeManager <- ZIO.service[ScopeManager] diff --git a/core/src/test/scala/marklit/resolver/DependencyResolverSpec.scala b/core/src/test/scala/marklit/resolver/DependencyResolverSpec.scala index 43307ad..7158abd 100644 --- a/core/src/test/scala/marklit/resolver/DependencyResolverSpec.scala +++ b/core/src/test/scala/marklit/resolver/DependencyResolverSpec.scala @@ -3,9 +3,8 @@ package marklit.resolver import zio.Scope import zio.test.* -/** Integration tests for DependencyResolver's Coursier-backed helpers. These - * tests hit the local Coursier cache (or fetch on first run); they don't - * depend on the test JVM's classpath layout. +/** Integration tests for DependencyResolver's Coursier-backed helpers. These tests hit the local Coursier cache (or + * fetch on first run); they don't depend on the test JVM's classpath layout. */ object DependencyResolverSpec extends ZIOSpecDefault: diff --git a/core/src/test/scala/marklit/results/ResultsFileSpec.scala b/core/src/test/scala/marklit/results/ResultsFileSpec.scala index d6f0388..d850f22 100644 --- a/core/src/test/scala/marklit/results/ResultsFileSpec.scala +++ b/core/src/test/scala/marklit/results/ResultsFileSpec.scala @@ -97,8 +97,7 @@ object ResultsFileSpec extends ZIOSpecDefault: test("serializes to and from JSON") { val docResult = DocumentResult( - blockResults = - Vector(makeBlockResult("val x = 1", output = Some("1"))), + blockResults = Vector(makeBlockResult("val x = 1", output = Some("1"))), processingTime = java.time.Duration.ofMillis(100) ) diff --git a/docs/src/test/scala/marklit/docs/Front.scala b/docs/src/test/scala/marklit/docs/Front.scala index 2c44a98..aa32397 100644 --- a/docs/src/test/scala/marklit/docs/Front.scala +++ b/docs/src/test/scala/marklit/docs/Front.scala @@ -10,28 +10,24 @@ import specular.* import specular.ziotest.DocSpecSuite import zio.test.* -/** Install coordinate. Always the last published tag. A bare next version is - * not on Central. +/** Install coordinate. Always the last published tag. A bare next version is not on Central. */ object DocsVersion: /** Last published tag (`v0.1.0`). Update this in the release commit. */ val published = "0.1.0" - /** Returns [[published]]. A bare next version, `-ci`, `+`, and `SNAPSHOT` are - * unpublished. + /** Returns [[published]]. A bare next version, `-ci`, `+`, and `SNAPSHOT` are unpublished. */ def advertise(raw: String): String = if raw.trim == published then published else published - /** `specular.meta.version` is the raw build version. Absent in unit tests, so - * those use [[published]]. + /** `specular.meta.version` is the raw build version. Absent in unit tests, so those use [[published]]. */ def version: String = sys.props.get("specular.meta.version").map(advertise).getOrElse(published) end DocsVersion -/** The site front. Specular still emits a summary index; [[BuildSite]] copies - * this page over it. +/** The site front. Specular still emits a summary index; [[BuildSite]] copies this page over it. * * Not a sidebar item. */ @@ -41,9 +37,7 @@ object Front extends DocSpecSuite: s"""addSbtPlugin("rocks.earlyeffect" % "sbt-marklit" % "${DocsVersion.version}")""" /** Narrow enough for a 390px column after the content padding. */ - private val figure = RenderConfig(layout = - LayoutConfig(hSpacing = 32, padding = 8, vSpacing = 36) - ) + private val figure = RenderConfig(layout = LayoutConfig(hSpacing = 32, padding = 8, vSpacing = 36)) private val pipeline = Mermaid("""flowchart TB @@ -67,9 +61,8 @@ object Front extends DocSpecSuite: case e: UI.Element[Any] => val here = if e.tag == "svg" then - e.attrs.collect { - case ascent.ast.Attr.StaticAttr("width", AttrValue.Str(raw)) => - raw.toDoubleOption + e.attrs.collect { case ascent.ast.Attr.StaticAttr("width", AttrValue.Str(raw)) => + raw.toDoubleOption }.flatten else Nil here.toList ++ e.children.toList.flatMap(svgWidths) diff --git a/sbt-plugin/src/main/scala/marklit/sbt/MarklitInProcess.scala b/sbt-plugin/src/main/scala/marklit/sbt/MarklitInProcess.scala index 7023473..267d5ba 100644 --- a/sbt-plugin/src/main/scala/marklit/sbt/MarklitInProcess.scala +++ b/sbt-plugin/src/main/scala/marklit/sbt/MarklitInProcess.scala @@ -4,24 +4,20 @@ import marklit.{MarklitRun, MarklitRunConfig, MarklitRunResult} import sbt.{Logger, MessageOnlyException} import zio.Unsafe -/** Bridges the sbt plugin's task values to the in-process marklit orchestrator - * ([[marklit.MarklitRun]]) and translates the structured result back into sbt - * logging + failure semantics. +/** Bridges the sbt plugin's task values to the in-process marklit orchestrator ([[marklit.MarklitRun]]) and translates + * the structured result back into sbt logging + failure semantics. * - * Execution is serialized on a single monitor: - * [[marklit.compiler.ScalaCompiler]] redirects `System.out`/`System.err` at - * the JVM level while a block executes, which is unsafe if two marklit tasks - * run concurrently inside the same sbt JVM. The old daemon serialized via its - * RPC lock; this preserves that. + * Execution is serialized on a single monitor: [[marklit.compiler.ScalaCompiler]] redirects `System.out`/`System.err` + * at the JVM level while a block executes, which is unsafe if two marklit tasks run concurrently inside the same sbt + * JVM. The old daemon serialized via its RPC lock; this preserves that. */ object MarklitInProcess { // Process-wide lock: only one marklit run touches System.out at a time. private val executionLock = new AnyRef - /** Run marklit over `config` on the warm session factory. Logs the facade's - * notices and per-file summaries, and throws [[MessageOnlyException]] (the - * sbt idiom for a clean task failure) when any file failed. + /** Run marklit over `config` on the warm session factory. Logs the facade's notices and per-file summaries, and + * throws [[MessageOnlyException]] (the sbt idiom for a clean task failure) when any file failed. * * @param taskLabel * "compilation" / "generation" — used in the failure message. diff --git a/sbt-plugin/src/main/scala/marklit/sbt/MarklitPlugin.scala b/sbt-plugin/src/main/scala/marklit/sbt/MarklitPlugin.scala index 83f9d38..7f34fb7 100644 --- a/sbt-plugin/src/main/scala/marklit/sbt/MarklitPlugin.scala +++ b/sbt-plugin/src/main/scala/marklit/sbt/MarklitPlugin.scala @@ -62,10 +62,9 @@ object MarklitPlugin extends AutoPlugin { override def requires: Plugins = plugins.JvmPlugin override def trigger: PluginTrigger = allRequirements - /** Filter scala-library / scala3-library jars out of a forwarded classpath. - * marklit resolves the per-version stdlib via Coursier; leaving the host - * project's stdlib on the classpath leaks its bin-TASTy into a different - * compile context. + /** Filter scala-library / scala3-library jars out of a forwarded classpath. marklit resolves the per-version stdlib + * via Coursier; leaving the host project's stdlib on the classpath leaks its bin-TASTy into a different compile + * context. */ private def filterForwardedClasspath(cp: Seq[File]): Seq[File] = cp.filterNot { f => @@ -75,8 +74,8 @@ object MarklitPlugin extends AutoPlugin { } /** Resolve the real on-disk paths of a Classpath. sbt 2.0 classpaths are - * `Seq[Attributed[xsbti.HashedVirtualFileRef]]`; the converter turns each - * virtual ref into a `java.nio.file.Path` (then a `File`). + * `Seq[Attributed[xsbti.HashedVirtualFileRef]]`; the converter turns each virtual ref into a `java.nio.file.Path` + * (then a `File`). */ private def classpathFiles( cp: Def.Classpath, @@ -86,15 +85,13 @@ object MarklitPlugin extends AutoPlugin { /** Resolve a dep project's per-cross-version classes directory. * - * In sbt 2.0 the build output layout is - * `/out/jvm/scala-//classes`, and the `target` - * key for the dep is already scoped to the *current* session's Scala version - * (e.g. `.../out/jvm/scala-3.8.2/marklit-example-core`). To reach a - * different cross version we substitute the `scala-` path segment. + * In sbt 2.0 the build output layout is `/out/jvm/scala-//classes`, and the `target` key + * for the dep is already scoped to the *current* session's Scala version (e.g. + * `.../out/jvm/scala-3.8.2/marklit-example-core`). To reach a different cross version we substitute the + * `scala-` path segment. * - * For robustness we also emit the sbt 1.x candidates (`target/scala-` - * and `target/scala-`); the caller filters by existence and - * picks the first that's present. + * For robustness we also emit the sbt 1.x candidates (`target/scala-` and `target/scala-`); the + * caller filters by existence and picks the first that's present. */ private def crossClassesDir(target: File, version: String): Seq[File] = { val major = version.takeWhile(_ != '.') @@ -123,18 +120,14 @@ object MarklitPlugin extends AutoPlugin { /** Auto-discover per-major classpaths from this project's dependsOn graph. * - * For each project the docs module depends on (transitively), we look at its - * `crossScalaVersions`. For every cross-version that's a *different* major - * from the docs module's own scalaVersion, we add that project's expected - * cross-build classes directory to the per-major classpath. Cross-builds are - * produced automatically by the `marklitGenerate` / `marklitCompile` - * commands (which schedule `++ ! depProj/compile` ahead of the task - * invocation). When invoking the task directly via project scoping - * (`docs/marklitGenerate`), the user is responsible for having compiled the - * cross-builds first. + * For each project the docs module depends on (transitively), we look at its `crossScalaVersions`. For every + * cross-version that's a *different* major from the docs module's own scalaVersion, we add that project's expected + * cross-build classes directory to the per-major classpath. Cross-builds are produced automatically by the + * `marklitGenerate` / `marklitCompile` commands (which schedule `++ ! depProj/compile` ahead of the task + * invocation). When invoking the task directly via project scoping (`docs/marklitGenerate`), the user is responsible + * for having compiled the cross-builds first. */ - private def autoMajorClasspaths - : Def.Initialize[Task[Map[String, Seq[File]]]] = + private def autoMajorClasspaths: Def.Initialize[Task[Map[String, Seq[File]]]] = Def.taskDyn { val docsMajor = scalaVersion.value.takeWhile(_ != '.') val deps = buildDependencies.value @@ -148,17 +141,16 @@ object MarklitPlugin extends AutoPlugin { .value val depTargets = Keys.target.all(perDepFilter).value - val entries: Seq[(String, File)] = (depCrosses zip depTargets).flatMap { - case (versions, tgt) => - versions - .filter(v => v.takeWhile(_ != '.') != docsMajor) - .flatMap { v => - val major = v.takeWhile(_ != '.') - crossClassesDir(tgt, v) - .filter(_.exists()) - .headOption - .map(major -> _) - } + val entries: Seq[(String, File)] = (depCrosses zip depTargets).flatMap { case (versions, tgt) => + versions + .filter(v => v.takeWhile(_ != '.') != docsMajor) + .flatMap { v => + val major = v.takeWhile(_ != '.') + crossClassesDir(tgt, v) + .filter(_.exists()) + .headOption + .map(major -> _) + } } entries @@ -167,19 +159,15 @@ object MarklitPlugin extends AutoPlugin { } } - /** Compute the commands needed before invoking `taskName` on every project - * that has MarklitPlugin enabled. + /** Compute the commands needed before invoking `taskName` on every project that has MarklitPlugin enabled. * * The returned sequence is ordered: - * 1. `+ /Compile/compile` for each unique cross-built dep project - * (the `+` prefix iterates the dep's own `crossScalaVersions` and is - * responsible for stashing & restoring session state — so we get back - * to the user's original `scalaVersion` per project before the docs - * task runs). + * 1. `+ /Compile/compile` for each unique cross-built dep project (the `+` prefix iterates the dep's own + * `crossScalaVersions` and is responsible for stashing & restoring session state — so we get back to the user's + * original `scalaVersion` per project before the docs task runs). * 2. `/` per marklit-enabled project. * - * Each dep project is cross-compiled at most once (de-duplicated across docs - * projects). + * Each dep project is cross-compiled at most once (de-duplicated across docs projects). */ private def buildCrossCommands( state: State, @@ -224,12 +212,11 @@ object MarklitPlugin extends AutoPlugin { } } - /** Build-level commands that schedule the necessary cross-version compiles - * before invoking each marklit-enabled project's task. + /** Build-level commands that schedule the necessary cross-version compiles before invoking each marklit-enabled + * project's task. * - * Why a command (and not a task)? sbt's `scalaVersion` is a setting, not a - * parameter to `compile`. The `+` cross-prefix is a *command-level* loop - * that re-applies the build with a different `scalaVersion` per cross + * Why a command (and not a task)? sbt's `scalaVersion` is a setting, not a parameter to `compile`. The `+` + * cross-prefix is a *command-level* loop that re-applies the build with a different `scalaVersion` per cross * version, then runs the suffix command. */ private val marklitGenerateCommand: Command = diff --git a/sbt-plugin/src/main/scala/marklit/sbt/MarklitSession.scala b/sbt-plugin/src/main/scala/marklit/sbt/MarklitSession.scala index c75bbb3..bca5318 100644 --- a/sbt-plugin/src/main/scala/marklit/sbt/MarklitSession.scala +++ b/sbt-plugin/src/main/scala/marklit/sbt/MarklitSession.scala @@ -5,16 +5,14 @@ import zio.{Exit, Runtime, Scope, Unsafe, ZIO} /** Session-scoped holder for a single [[CompilerFactory]] and a ZIO runtime. * - * This is the in-process replacement for the old out-of-process marklit - * daemon. `CompilerFactory.layer` is a *scoped* layer — it extracts the two - * shim jars to temp files and maintains a per-Scala-version cache of - * classloaders + reflective dotc invokers. By building it once and keeping its - * Scope open for the whole sbt session, every marklit task reuses the same - * warm per-version compilers; the second `marklitGenerate` skips the - * cold-start classloader/Coursier work entirely. + * This is the in-process replacement for the old out-of-process marklit daemon. `CompilerFactory.layer` is a *scoped* + * layer — it extracts the two shim jars to temp files and maintains a per-Scala-version cache of classloaders + + * reflective dotc invokers. By building it once and keeping its Scope open for the whole sbt session, every marklit + * task reuses the same warm per-version compilers; the second `marklitGenerate` skips the cold-start + * classloader/Coursier work entirely. * - * `shutdown()` is wired to the build's `Global / onUnload` so the scope (and - * its temp files) is released on sbt exit / reload. + * `shutdown()` is wired to the build's `Global / onUnload` so the scope (and its temp files) is released on sbt exit / + * reload. */ object MarklitSession { @@ -52,8 +50,8 @@ object MarklitSession { /** The runtime marklit effects are executed on. */ def runtime(): Runtime[Any] = ensure().runtime - /** Release the session's CompilerFactory scope (temp shim jars, cached - * classloaders). Safe to call when nothing was ever initialized. + /** Release the session's CompilerFactory scope (temp shim jars, cached classloaders). Safe to call when nothing was + * ever initialized. */ def shutdown(): Unit = synchronized { if (live != null) {