@@ -43,8 +43,8 @@ import dotty.tools.dotc.util.SourceFile
4343 files.head
4444 val metacSb : StringBuilder = StringBuilder (5000 )
4545 val semanticdbPath = inputFile()
46- val doc = Tools .loadTextDocumentUnsafe(sourceSrc.toAbsolutePath, semanticdbPath)
47- Tools .metac(doc, Paths .get(doc.uri))(using metacSb)
46+ val doc = SemanticdbTests .loadTextDocumentUnsafe(sourceSrc.toAbsolutePath, semanticdbPath)
47+ SemanticdbTests .metac(doc, Paths .get(doc.uri))(using metacSb)
4848 Files .write(rootSrc.resolve(" metac.expect" ), metacSb.toString.getBytes(StandardCharsets .UTF_8 ))
4949
5050
@@ -85,8 +85,8 @@ class SemanticdbTests:
8585 .resolve(relpath)
8686 .resolveSibling(filename + " .semanticdb" )
8787 val expectPath = source.resolveSibling(filename.replace(" .scala" , " .expect.scala" ))
88- val doc = Tools .loadTextDocument(source, relpath, semanticdbPath)
89- Tools .metac(doc, rootSrc.relativize(source))(using metacSb)
88+ val doc = SemanticdbTests .loadTextDocument(source, relpath, semanticdbPath)
89+ SemanticdbTests .metac(doc, rootSrc.relativize(source))(using metacSb)
9090 val obtained = trimTrailingWhitespace(SemanticdbTests .printTextDocument(doc))
9191 collectErrorOrUpdate(expectPath, obtained)
9292 collectErrorOrUpdate(metacExpectFile, metacSb.toString)
@@ -157,4 +157,139 @@ object SemanticdbTests:
157157 out.flush()
158158 DocumentPrinter .textDocumentPrettyPrint(byteStream.toByteArray.nn)
159159 end printTextDocument
160+
161+
162+ /** Load SemanticDB TextDocument for a single Scala source file
163+ *
164+ * @param scalaAbsolutePath Absolute path to a Scala source file.
165+ * @param scalaRelativePath scalaAbsolutePath relativized by the sourceroot.
166+ * @param semanticdbAbsolutePath Absolute path to the SemanticDB file.
167+ */
168+ def loadTextDocument (
169+ scalaAbsolutePath : Path ,
170+ scalaRelativePath : Path ,
171+ semanticdbAbsolutePath : Path
172+ ): TextDocument =
173+ val reluri = Tools .mkURIstring(scalaRelativePath)
174+ val sdocs = parseTextDocuments(semanticdbAbsolutePath)
175+ sdocs.documents.find(_.uri == reluri) match
176+ case None => throw new NoSuchElementException (s " $scalaRelativePath" )
177+ case Some (document) =>
178+ val text = new String (Files .readAllBytes(scalaAbsolutePath), StandardCharsets .UTF_8 )
179+ // Assert the SemanticDB payload is in-sync with the contents of the Scala file on disk.
180+ val md5FingerprintOnDisk = internal.MD5 .compute(text)
181+ if document.md5 != md5FingerprintOnDisk then
182+ throw new IllegalArgumentException (s " stale semanticdb: $scalaRelativePath" )
183+ else
184+ // Update text document to include full text contents of the file.
185+ document.copy(text = text)
186+ end loadTextDocument
187+
188+ def loadTextDocumentUnsafe (scalaAbsolutePath : Path , semanticdbAbsolutePath : Path ): TextDocument =
189+ val docs = parseTextDocuments(semanticdbAbsolutePath).documents
190+ assert(docs.length == 1 )
191+ docs.head.copy(text = new String (Files .readAllBytes(scalaAbsolutePath), StandardCharsets .UTF_8 ))
192+
193+ /** Parses SemanticDB text documents from an absolute path to a `*.semanticdb` file. */
194+ private def parseTextDocuments (path : Path ): TextDocuments =
195+ val bytes = Files .readAllBytes(path).nn // NOTE: a semanticdb file is a TextDocuments message, not TextDocument
196+ TextDocuments .parseFrom(bytes)
197+
198+
199+ def metac (doc : TextDocument , realPath : Path )(using sb : StringBuilder ): StringBuilder =
200+ val symtab = PrinterSymtab .fromTextDocument(doc)
201+ val symPrinter = SymbolInformationPrinter (symtab)
202+ val realURI = realPath.toString
203+ given sourceFile : SourceFile = SourceFile .virtual(doc.uri, doc.text)
204+ val synthPrinter = SyntheticPrinter (symtab, sourceFile)
205+ sb.append(realURI).nl
206+ sb.append(" -" * realURI.length).nl
207+ sb.nl
208+ sb.append(" Summary:" ).nl
209+ sb.append(" Schema => " ).append(schemaString(doc.schema)).nl
210+ sb.append(" Uri => " ).append(doc.uri).nl
211+ sb.append(" Text => empty" ).nl
212+ sb.append(" Language => " ).append(languageString(doc.language)).nl
213+ sb.append(" Symbols => " ).append(doc.symbols.length).append(" entries" ).nl
214+ sb.append(" Occurrences => " ).append(doc.occurrences.length).append(" entries" ).nl
215+ if doc.diagnostics.nonEmpty then
216+ sb.append(" Diagnostics => " ).append(doc.diagnostics.length).append(" entries" ).nl
217+ if doc.synthetics.nonEmpty then
218+ sb.append(" Synthetics => " ).append(doc.synthetics.length).append(" entries" ).nl
219+ sb.nl
220+ sb.append(" Symbols:" ).nl
221+ doc.symbols.sorted.foreach(s => processSymbol(s, symPrinter))
222+ sb.nl
223+ sb.append(" Occurrences:" ).nl
224+ doc.occurrences.sorted.foreach(processOccurrence)
225+ sb.nl
226+ if doc.diagnostics.nonEmpty then
227+ sb.append(" Diagnostics:" ).nl
228+ doc.diagnostics.sorted.foreach(d => processDiag(d))
229+ sb.nl
230+ if doc.synthetics.nonEmpty then
231+ sb.append(" Synthetics:" ).nl
232+ doc.synthetics.sorted.foreach(s => processSynth(s, synthPrinter))
233+ sb.nl
234+ sb
235+ end metac
236+
237+ private def schemaString (schema : Schema ) =
238+ import Schema .*
239+ schema match
240+ case SEMANTICDB3 => " SemanticDB v3"
241+ case SEMANTICDB4 => " SemanticDB v4"
242+ case LEGACY => " SemanticDB legacy"
243+ case Unrecognized (_) => " unknown"
244+ end schemaString
245+
246+ private def languageString (language : Language ) =
247+ import Language .*
248+ language match
249+ case SCALA => " Scala"
250+ case JAVA => " Java"
251+ case UNKNOWN_LANGUAGE | Unrecognized (_) => " unknown"
252+ end languageString
253+
254+ private def processSymbol (info : SymbolInformation , printer : SymbolInformationPrinter )(using sb : StringBuilder ): Unit =
255+ sb.append(printer.pprintSymbolInformation(info)).nl
256+
257+ private def processSynth (synth : Synthetic , printer : SyntheticPrinter )(using sb : StringBuilder ): Unit =
258+ sb.append(printer.pprint(synth)).nl
259+
260+ private def processDiag (d : Diagnostic )(using sb : StringBuilder ): Unit =
261+ d.range match
262+ case Some (range) => processRange(sb, range)
263+ case _ => sb.append(" [):" )
264+ sb.append(" " )
265+ d.severity match
266+ case Diagnostic .Severity .ERROR => sb.append(" [error]" )
267+ case Diagnostic .Severity .WARNING => sb.append(" [warning]" )
268+ case Diagnostic .Severity .INFORMATION => sb.append(" [info]" )
269+ case _ => sb.append(" [unknown]" )
270+ sb.append(" " )
271+ sb.append(d.message)
272+ sb.nl
273+
274+ private def processOccurrence (occ : SymbolOccurrence )(using sb : StringBuilder , sourceFile : SourceFile ): Unit =
275+ occ.range match
276+ case Some (range) =>
277+ processRange(sb, range)
278+ if range.endLine == range.startLine
279+ && range.startCharacter != range.endCharacter
280+ && ! (occ.symbol.isConstructor && occ.role.isDefinition) then
281+ val line = sourceFile.lineContent(sourceFile.lineToOffset(range.startLine))
282+ assert(range.startCharacter <= line.length && range.endCharacter <= line.length,
283+ s " Line is only ${line.length} - start line was ${range.startLine} in source ${sourceFile.file.name}"
284+ )
285+ sb.append(" " ).append(line.substring(range.startCharacter, range.endCharacter))
286+ case _ =>
287+ sb.append(" [):" )
288+ end match
289+ sb.append(if occ.role.isReference then " -> " else " <- " ).append(occ.symbol).nl
290+ end processOccurrence
291+
292+ extension (sb : StringBuilder )
293+ private inline def nl = sb.append(System .lineSeparator)
294+
160295end SemanticdbTests
0 commit comments