diff --git a/scaladoc-testcases/src/tests/snippetCompilerTest.scala b/scaladoc-testcases/src/tests/snippetCompilerTest.scala index b2d121f2323c..fc56a6fbb901 100644 --- a/scaladoc-testcases/src/tests/snippetCompilerTest.scala +++ b/scaladoc-testcases/src/tests/snippetCompilerTest.scala @@ -3,6 +3,7 @@ package snippetCompiler /** * ```scala sc:compile * def a = 2 + * val x = 1 + List() * a * ``` * @@ -11,4 +12,4 @@ package snippetCompiler * a() * ``` */ -class A { } \ No newline at end of file +class A { } diff --git a/scaladoc/src/dotty/tools/scaladoc/api.scala b/scaladoc/src/dotty/tools/scaladoc/api.scala index 649c9ffe8a2f..940decb94cb9 100644 --- a/scaladoc/src/dotty/tools/scaladoc/api.scala +++ b/scaladoc/src/dotty/tools/scaladoc/api.scala @@ -232,11 +232,15 @@ extension (s: Signature) case l: Link => l.name }.mkString -case class TastyMemberSource(val path: java.nio.file.Path, val lineNumber: Int) +case class TastyMemberSource(path: java.nio.file.Path, lineNumber: Int) + +object SnippetCompilerData: + case class Position(line: Int, column: Int) case class SnippetCompilerData( - val packageName: String, - val classType: Option[String], - val classGenerics: Option[String], - val imports: List[String] + packageName: String, + classType: Option[String], + classGenerics: Option[String], + imports: List[String], + position: SnippetCompilerData.Position ) diff --git a/scaladoc/src/dotty/tools/scaladoc/renderers/WikiDocRenderer.scala b/scaladoc/src/dotty/tools/scaladoc/renderers/WikiDocRenderer.scala index 390d8b15efbe..85649e606471 100644 --- a/scaladoc/src/dotty/tools/scaladoc/renderers/WikiDocRenderer.scala +++ b/scaladoc/src/dotty/tools/scaladoc/renderers/WikiDocRenderer.scala @@ -10,22 +10,22 @@ import dotty.tools.scaladoc.snippets._ class DocRender(signatureRenderer: SignatureRenderer, snippetChecker: SnippetChecker)(using ctx: DocContext): - private val snippetCheckingFunc: Member => (String, Option[SnippetCompilerArg]) => Unit = - (m: Member) => { - (str: String, argOverride: Option[SnippetCompilerArg]) => { - val arg = argOverride.fold( - ctx.snippetCompilerArgs.get(m).fold(SnippetCompilerArg.default)(p => p) - )(p => p) - - snippetChecker.checkSnippet(str, m.docs.map(_.snippetCompilerData), arg).foreach { _ match { - case r @ SnippetCompilationResult(None, _) => - println(s"In member ${m.name} (${m.dri.location}):") - println(r.getSummary) - case _ => + private val snippetCheckingFuncFromMember: Member => SnippetChecker.SnippetCheckingFunc = + (m: Member) => { + (str: String, lineOffset: SnippetChecker.LineOffset, argOverride: Option[SnippetCompilerArg]) => { + val arg = argOverride.getOrElse( + ctx.snippetCompilerArgs.get(m).getOrElse(SnippetCompilerArg.default) + ) + + snippetChecker.checkSnippet(str, m.docs.map(_.snippetCompilerData), arg, lineOffset).foreach { _ match { + case r @ SnippetCompilationResult(None, _) => + println(s"In member ${m.name} (${m.dri.location}):") + println(r.getSummary) + case _ => + } } - } + } } - } def renderDocPart(doc: DocPart)(using Member): AppliedTag = doc match case md: MdNode => renderMarkdown(md) @@ -37,7 +37,7 @@ class DocRender(signatureRenderer: SignatureRenderer, snippetChecker: SnippetChe raw(DocFlexmarkRenderer.render(el)( (link,name) => renderLink(link, default => text(if name.isEmpty then default else name)).toString, - snippetCheckingFunc(m) + snippetCheckingFuncFromMember(m) )) private def listItems(items: Seq[WikiDocElement])(using m: Member) = @@ -67,7 +67,7 @@ class DocRender(signatureRenderer: SignatureRenderer, snippetChecker: SnippetChe case 6 => h6(content) case Paragraph(text) => p(renderElement(text)) case Code(data: String) => - snippetCheckingFunc(m)(data, None) + snippetCheckingFuncFromMember(m)(data, 0, None) pre(code(raw(data))) // TODO add classes case HorizontalRule => hr diff --git a/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetChecker.scala b/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetChecker.scala index 97e830b5ce0c..3adfe0fb875c 100644 --- a/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetChecker.scala +++ b/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetChecker.scala @@ -10,22 +10,25 @@ class SnippetChecker()(using ctx: DocContext): Paths.get(ctx.args.classpath).toAbsolutePath + sep + ctx.args.tastyDirs.map(_.getAbsolutePath()).mkString(sep) private val compiler: SnippetCompiler = SnippetCompiler(classpath = cp) - private val wrapper: SnippetWrapper = SnippetWrapper() + var warningsCount = 0 var errorsCount = 0 def checkSnippet( snippet: String, data: Option[SnippetCompilerData], - arg: SnippetCompilerArg + arg: SnippetCompilerArg, + lineOffset: SnippetChecker.LineOffset ): Option[SnippetCompilationResult] = { if arg.is(SCFlags.Compile) then - val wrapped = wrapper.wrap( + val wrapped = WrappedSnippet( snippet, data.map(_.packageName), data.flatMap(_.classType), data.flatMap(_.classGenerics), - data.map(_.imports).getOrElse(Nil) + data.map(_.imports).getOrElse(Nil), + lineOffset + data.fold(0)(_.position.line) + 1, + data.fold(0)(_.position.column) ) val res = compiler.compile(wrapped) if !res.messages.filter(_.level == MessageLevel.Error).isEmpty then errorsCount = errorsCount + 1 @@ -38,4 +41,8 @@ class SnippetChecker()(using ctx: DocContext): |Snippet compiler summary: | Found $warningsCount warnings | Found $errorsCount errors - |""".stripMargin \ No newline at end of file + |""".stripMargin + +object SnippetChecker: + type LineOffset = Int + type SnippetCheckingFunc = (String, LineOffset, Option[SnippetCompilerArg]) => Unit diff --git a/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompiler.scala b/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompiler.scala index 1dad4af1d88a..0c92286ca3c7 100644 --- a/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompiler.scala +++ b/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompiler.scala @@ -42,14 +42,14 @@ class SnippetCompiler( private def nullableMessage(msgOrNull: String): String = if (msgOrNull == null) "" else msgOrNull - private def createReportMessage(diagnostics: Seq[Diagnostic]): Seq[SnippetCompilerMessage] = { + private def createReportMessage(diagnostics: Seq[Diagnostic], line: Int, column: Int): Seq[SnippetCompilerMessage] = { val infos = diagnostics.toSeq.sortBy(_.pos.source.path) val errorMessages = infos.map { case diagnostic if diagnostic.position.isPresent => val pos = diagnostic.position.get val msg = nullableMessage(diagnostic.message) val level = MessageLevel.fromOrdinal(diagnostic.level) - SnippetCompilerMessage(pos.line, pos.column, pos.lineContent, msg, level) + SnippetCompilerMessage(pos.line + line, pos.column + column, pos.lineContent, msg, level) case d => val level = MessageLevel.fromOrdinal(d.level) SnippetCompilerMessage(-1, -1, "", nullableMessage(d.message), level) @@ -58,7 +58,7 @@ class SnippetCompiler( } def compile( - snippets: List[String] + wrappedSnippet: WrappedSnippet ): SnippetCompilationResult = { val context = driver.currentCtx.fresh .setSetting( @@ -67,14 +67,8 @@ class SnippetCompiler( ) .setReporter(new StoreReporter) val run = newRun(using context) - run.compileFromStrings(snippets) - val messages = createReportMessage(context.reporter.pendingMessages(using context)) + run.compileFromStrings(List(wrappedSnippet.snippet)) + val messages = createReportMessage(context.reporter.pendingMessages(using context), wrappedSnippet.lineOffset, wrappedSnippet.columnOffset) val targetIfSuccessful = Option.when(!context.reporter.hasErrors)(target) SnippetCompilationResult(targetIfSuccessful, messages) } - - def compile( - snippet: String - ): SnippetCompilationResult = compile(List(snippet)) - - diff --git a/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompilerArgs.scala b/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompilerArgs.scala index d7495960e721..f880169d78e0 100644 --- a/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompilerArgs.scala +++ b/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompilerArgs.scala @@ -77,4 +77,4 @@ object SnippetCompilerArgParser extends ArgParser[SnippetCompilerArg]: }.toList if !allErrors.isEmpty then Left(allErrors.mkString("\n")) else Right(SnippetCompilerArg(checkedFlags)) - } \ No newline at end of file + } diff --git a/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetWrapper.scala b/scaladoc/src/dotty/tools/scaladoc/snippets/WrappedSnippet.scala similarity index 62% rename from scaladoc/src/dotty/tools/scaladoc/snippets/SnippetWrapper.scala rename to scaladoc/src/dotty/tools/scaladoc/snippets/WrappedSnippet.scala index 1916986ba474..91229cb148a3 100644 --- a/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetWrapper.scala +++ b/scaladoc/src/dotty/tools/scaladoc/snippets/WrappedSnippet.scala @@ -4,19 +4,30 @@ package snippets import java.io.ByteArrayOutputStream import java.io.PrintStream -class SnippetWrapper: - extension (ps: PrintStream) private def printlnWithIndent(indent: Int, str: String) = - ps.println((" " * indent) + str) - def wrap(str: String): String = +case class WrappedSnippet(snippet: String, lineOffset: Int, columnOffset: Int) + +object WrappedSnippet: + private val lineOffset = 2 + private val columnOffset = 2 + + def apply(str: String): WrappedSnippet = val baos = new ByteArrayOutputStream() val ps = new PrintStream(baos) ps.println("package snippets") ps.println("object Snippet {") str.split('\n').foreach(ps.printlnWithIndent(2, _)) ps.println("}") - baos.toString + WrappedSnippet(baos.toString, lineOffset, columnOffset) - def wrap(str:String, packageName: Option[String], className: Option[String], classGenerics: Option[String], imports: List[String]) = + def apply( + str: String, + packageName: Option[String], + className: Option[String], + classGenerics: Option[String], + imports: List[String], + lineOffset: Int, + columnOffset: Int + ): WrappedSnippet = val baos = new ByteArrayOutputStream() val ps = new PrintStream(baos) ps.println(s"package ${packageName.getOrElse("snippets")}") @@ -24,8 +35,9 @@ class SnippetWrapper: ps.println(s"trait Snippet${classGenerics.getOrElse("")} { ${className.fold("")(cn => s"self: $cn =>")}") str.split('\n').foreach(ps.printlnWithIndent(2, _)) ps.println("}") - baos.toString + WrappedSnippet(baos.toString, lineOffset, columnOffset) + + extension (ps: PrintStream) private def printlnWithIndent(indent: Int, str: String) = + ps.println((" " * indent) + str) + -object SnippetWrapper: - private val lineOffset = 2 - private val columnOffset = 2 \ No newline at end of file diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/SyntheticSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/SyntheticSupport.scala index 28440b066fa7..01c3b5eaec38 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/SyntheticSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/SyntheticSupport.scala @@ -114,4 +114,3 @@ trait SyntheticsSupport: typeForClass(c).asInstanceOf[dotc.core.Types.Type] .memberInfo(symbol.asInstanceOf[dotc.core.Symbols.Symbol]) .asInstanceOf[TypeRepr] - diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/comments/Comments.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/comments/Comments.scala index 722569ed6e24..e58305aa558b 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/comments/Comments.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/comments/Comments.scala @@ -147,10 +147,31 @@ abstract class MarkupConversion[T](val repr: Repr)(using DocContext) { createTypeConstructor(t.asInstanceOf[dotc.core.Types.TypeRef].underlying) ).mkString("[",", ","]") ) - SnippetCompilerData(packageName, classType, classGenerics, Nil) + SnippetCompilerData(packageName, classType, classGenerics, Nil, position(hackGetPositionOfDocstring(using qctx)(sym))) case _ => getSnippetCompilerData(sym.maybeOwner) - } else SnippetCompilerData(packageName, None, None, Nil) - + } else SnippetCompilerData(packageName, None, None, Nil, position(hackGetPositionOfDocstring(using qctx)(sym))) + + private def position(p: Option[qctx.reflect.Position]): SnippetCompilerData.Position = + p.fold(SnippetCompilerData.Position(0, 0))(p => SnippetCompilerData.Position(p.startLine, p.startColumn)) + + private def hackGetPositionOfDocstring(using Quotes)(s: qctx.reflect.Symbol): Option[qctx.reflect.Position] = + import dotty.tools.dotc.core.Comments.CommentsContext + import dotty.tools.dotc + given ctx: dotc.core.Contexts.Context = qctx.asInstanceOf[scala.quoted.runtime.impl.QuotesImpl].ctx + val docCtx = ctx.docCtx.getOrElse { + throw new RuntimeException( + "DocCtx could not be found and documentations are unavailable. This is a compiler-internal error." + ) + } + val span = docCtx.docstring(s.asInstanceOf[dotc.core.Symbols.Symbol]).span + s.pos.flatMap { pos => + docCtx.docstring(s.asInstanceOf[dotc.core.Symbols.Symbol]).map { docstring => + dotty.tools.dotc.util.SourcePosition( + pos.sourceFile.asInstanceOf[dotty.tools.dotc.util.SourceFile], + docstring.span + ).asInstanceOf[qctx.reflect.Position] + } + } final def parse(preparsed: PreparsedComment): Comment = val body = markupToDokkaCommentBody(stringToMarkup(preparsed.body)) diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/comments/markdown/DocFlexmarkExtension.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/comments/markdown/DocFlexmarkExtension.scala index 1c80a963aaf0..31a263555a88 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/comments/markdown/DocFlexmarkExtension.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/comments/markdown/DocFlexmarkExtension.scala @@ -12,6 +12,8 @@ import com.vladsch.flexmark.util.options._ import com.vladsch.flexmark.util.sequence.BasedSequence import com.vladsch.flexmark._ +import dotty.tools.scaladoc.snippets.SnippetChecker + class DocLinkNode( val target: DocLink, val body: String, @@ -45,7 +47,7 @@ object DocFlexmarkParser { } } -case class DocFlexmarkRenderer(renderLink: (DocLink, String) => String, snippetCheckingFunc: (String, Option[snippets.SnippetCompilerArg]) => Unit) +case class DocFlexmarkRenderer(renderLink: (DocLink, String) => String, snippetCheckingFunc: SnippetChecker.SnippetCheckingFunc) extends HtmlRenderer.HtmlRendererExtension: def rendererOptions(opt: MutableDataHolder): Unit = () // noop @@ -59,7 +61,7 @@ case class DocFlexmarkRenderer(renderLink: (DocLink, String) => String, snippetC .map(_.stripPrefix("sc:")) .map(snippets.SnippetCompilerArgParser.parse) .flatMap(_.toOption) - snippetCheckingFunc(node.getContentChars.toString, argOverride) + snippetCheckingFunc(node.getContentChars.toString, node.getStartLineNumber, argOverride) c.delegateRender() object Handler extends CustomNodeRenderer[DocLinkNode]: @@ -80,6 +82,6 @@ case class DocFlexmarkRenderer(renderLink: (DocLink, String) => String, snippetC htmlRendererBuilder.nodeRendererFactory(Factory) object DocFlexmarkRenderer: - def render(node: Node)(renderLink: (DocLink, String) => String, snippetCheckingFunc: (String, Option[snippets.SnippetCompilerArg]) => Unit) = + def render(node: Node)(renderLink: (DocLink, String) => String, snippetCheckingFunc: SnippetChecker.SnippetCheckingFunc) = val opts = MarkdownParser.mkMarkdownOptions(Seq(DocFlexmarkRenderer(renderLink, snippetCheckingFunc))) - HtmlRenderer.builder(opts).build().render(node) \ No newline at end of file + HtmlRenderer.builder(opts).build().render(node)