-
Notifications
You must be signed in to change notification settings - Fork 1k
/
TastyParser.scala
223 lines (190 loc) · 8.36 KB
/
TastyParser.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
package dotty.tools.scaladoc
package tasty
import java.util.regex.Pattern
import scala.util.{Try, Success, Failure}
import scala.tasty.inspector.DocTastyInspector
import scala.quoted._
import dotty.tools.dotc
import dotty.tools.scaladoc.tasty.comments.MemberLookup
import dotty.tools.scaladoc.tasty.comments.QueryParser
import dotty.tools.scaladoc.tasty.comments.Comment
import java.nio.file.Paths
import java.nio.file.Files
import SymOps._
import ScaladocSupport._
/** Responsible for collectively inspecting all the Tasty files we're interested in.
*
* Delegates most of the work to [[TastyParser]] [[dotty.tools.scaladoc.tasty.TastyParser]].
*/
case class ScaladocTastyInspector()(using ctx: DocContext) extends DocTastyInspector:
private val topLevels = Seq.newBuilder[(String, Member)]
private var rootDoc: Option[Comment] = None
def processCompilationUnit(using Quotes)(root: reflect.Tree): Unit = ()
override def postProcess(using Quotes): Unit =
// hack into the compiler to get a list of all top-level trees
// in principle, to do this, one would collect trees in processCompilationUnit
// however, path-dependent types disallow doing so w/o using casts
inline def hackForeachTree(thunk: reflect.Tree => Unit): Unit =
given dctx: dotc.core.Contexts.Context = quotes.asInstanceOf[scala.quoted.runtime.impl.QuotesImpl].ctx
dctx.run.units.foreach { compilationUnit =>
// mirrors code from TastyInspector
thunk(compilationUnit.tpdTree.asInstanceOf[reflect.Tree])
}
val symbolsToSkip: Set[reflect.Symbol] =
ctx.args.identifiersToSkip.flatMap { ref =>
val qrSymbol = reflect.Symbol
Try(qrSymbol.requiredPackage(ref)).orElse(Try(qrSymbol.requiredClass(ref))) match {
case Success(sym) => Some(sym)
case Failure(err) =>
report.warning(
s"Failed to resolve identifier to skip - $ref - because: ${throwableToString(err)}",
dotc.util.NoSourcePosition,
)
None
}
}.toSet
val patternsToSkip: List[Pattern] =
ctx.args.regexesToSkip.flatMap { regexString =>
Try(Pattern.compile(regexString)) match
case Success(pat) => Some(pat)
case Failure(err) =>
report.warning(
s"Failed to compile regex to skip - $regexString - because: ${throwableToString(err)}",
dotc.util.NoSourcePosition,
)
None
}
def isSkipped(sym: reflect.Symbol): Boolean =
def isSkippedById(sym: reflect.Symbol): Boolean =
if !sym.exists then false else
symbolsToSkip.contains(sym) || isSkipped(sym.owner)
def isSkippedByRx(sym: reflect.Symbol): Boolean =
val symStr = sym.fullName
patternsToSkip.exists(p => p.matcher(symStr).matches())
isSkippedById(sym) || isSkippedByRx(sym)
val parser = new TastyParser(quotes, this)(isSkipped)
def driFor(link: String): Option[DRI] =
val symOps = new SymOpsWithLinkCache
import symOps._
Try(QueryParser(link).readQuery()).toOption.flatMap(query =>
MemberLookup.lookupOpt(query, None).map {
case (sym, _, inheritingParent) => inheritingParent match
case Some(parent) => sym.driInContextOfInheritingParent(parent)
case None => sym.dri
}
)
ctx.staticSiteContext.foreach(_.memberLinkResolver = driFor)
var alreadyProcessed = false
def processRootDocIfNeeded(tree: parser.qctx.reflect.Tree) =
def readFile(pathStr: String)(using CompilerContext): Option[String] =
try
val path = Paths.get(pathStr)
if Files.exists(path) then Some(util.IO.read(path))
else
report.inform("Rootdoc at $pathStr does not exisits")
None
catch
case e: RuntimeException =>
report.warning(s"Unable to read root package doc from $pathStr: ${throwableToString(e)}")
None
if !alreadyProcessed then
alreadyProcessed = true
ctx.args.rootDocPath.flatMap(readFile).map { content =>
import parser.qctx.reflect._
def root(s: Symbol): Symbol = if s.owner.isNoSymbol then s else root(s.owner)
val topLevelPck = root(tree.symbol)
rootDoc = Some(parseCommentString(using parser.qctx, summon[DocContext])(content, topLevelPck, None))
}
hackForeachTree { root =>
if !isSkipped(root.symbol) then
val treeRoot = root.asInstanceOf[parser.qctx.reflect.Tree]
processRootDocIfNeeded(treeRoot)
topLevels ++= parser.parseRootTree(treeRoot)
}
val defn = ctx.compilerContext.definitions
if ctx.args.documentSyntheticTypes then
val intrinsicClassDefs = Seq(
defn.AnyClass,
defn.MatchableClass,
defn.AnyKindClass,
defn.AnyValClass,
defn.NullClass,
defn.NothingClass,
defn.SingletonClass,
defn.andType,
defn.orType,
).map { s =>
"scala" -> s.asInstanceOf[parser.qctx.reflect.Symbol].tree.match {
case cd: parser.qctx.reflect.ClassDef => parser.parseClasslike(cd)
case td: parser.qctx.reflect.TypeDef => parser.parseTypeDef(td)
}
}
topLevels ++= intrinsicClassDefs
val scalaPckg = defn.ScalaPackageVal.asInstanceOf[parser.qctx.reflect.Symbol]
given parser.qctx.type = parser.qctx
topLevels += "scala" -> Member(scalaPckg.fullName, scalaPckg.dri, Kind.Package)
topLevels += mergeAnyRefAliasAndObject(parser)
def result(): (List[Member], Option[Comment]) =
topLevels.clear()
rootDoc = None
val filePaths = ctx.args.tastyFiles.map(_.getAbsolutePath).toList
val classpath = ctx.args.classpath.split(java.io.File.pathSeparator).toList
if filePaths.nonEmpty then inspectFilesInContext(classpath, filePaths)
val all = topLevels.result()
all.groupBy(_._1).map { case (pckName, members) =>
val (pcks, rest) = members.map(_._2).partition(_.kind == Kind.Package)
val basePck = pcks.reduce( (p1, p2) =>
val withNewMembers = p1.withNewMembers(p2.members)
if withNewMembers.docs.isEmpty then withNewMembers.withDocs(p2.docs) else withNewMembers
)
basePck.withMembers((basePck.members ++ rest).sortBy(_.name))
}.toList -> rootDoc
def mergeAnyRefAliasAndObject(parser: TastyParser) =
val defn = ctx.compilerContext.definitions
val oM = parser.parseClasslike(defn.ObjectClass.asInstanceOf[parser.qctx.reflect.Symbol].tree.asInstanceOf[parser.qctx.reflect.ClassDef])
val aM = parser.parseTypeDef(defn.AnyRefAlias.asInstanceOf[parser.qctx.reflect.Symbol].tree.asInstanceOf[parser.qctx.reflect.TypeDef])
"scala" -> aM.copy(
kind = oM.kind,
members = oM.members
)
/** Parses a single Tasty compilation unit. */
case class TastyParser(
qctx: Quotes,
inspector: ScaladocTastyInspector,
)(
isSkipped: qctx.reflect.Symbol => Boolean
)(
using val ctx: DocContext
) extends BasicSupport with TypesSupport with ClassLikeSupport with PackageSupport with InkuireSupport:
import qctx.reflect._
private given qctx.type = qctx
def processTree[T](tree: Tree)(op: => T): Option[T] = try Option(op) catch
case e: Exception =>
report.warning(throwableToString(e), tree.pos)
None
def processTreeOpt[T](tree: Tree)(op: => Option[T]): Option[T] = try op catch
case e: Exception =>
report.warning(throwableToString(e), tree.pos)
None
def parseRootTree(root: Tree): Seq[(String, Member)] =
val docs = Seq.newBuilder[(String, Member)]
object Traverser extends TreeTraverser:
var seen: List[Tree] = Nil
override def traverseTree(tree: Tree)(owner: Symbol): Unit =
seen = tree :: seen
if !isSkipped(tree.symbol) then tree match {
case pck: PackageClause =>
docs += parsePackage(pck)
super.traverseTree(tree)(owner)
case packageObject: ClassDef if(packageObject.symbol.name.contains("package$")) =>
docs += parsePackageObject(packageObject)
case clazz: ClassDef if clazz.symbol.shouldDocumentClasslike =>
docs += clazz.symbol.packageName -> parseClasslike(clazz)
case _ =>
}
seen = seen.tail
try Traverser.traverseTree(root)(Symbol.spliceOwner)
catch case e: Throwable =>
println(s"Problem parsing ${root.pos}, documentation may not be generated.")
e.printStackTrace()
docs.result()