collect.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. /*
  2. Copyright 2019 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package markers
  14. import (
  15. "go/ast"
  16. "go/token"
  17. "strings"
  18. "sync"
  19. "sigs.k8s.io/controller-tools/pkg/loader"
  20. )
  21. // Collector collects and parses marker comments defined in the registry
  22. // from package source code. If no registry is provided, an empty one will
  23. // be initialized on the first call to MarkersInPackage.
  24. type Collector struct {
  25. *Registry
  26. byPackage map[string]map[ast.Node]MarkerValues
  27. mu sync.Mutex
  28. }
  29. // MarkerValues are all the values for some set of markers.
  30. type MarkerValues map[string][]interface{}
  31. // Get fetches the first value that for the given marker, returning
  32. // nil if no values are available.
  33. func (v MarkerValues) Get(name string) interface{} {
  34. vals := v[name]
  35. if len(vals) == 0 {
  36. return nil
  37. }
  38. return vals[0]
  39. }
  40. func (c *Collector) init() {
  41. if c.Registry == nil {
  42. c.Registry = &Registry{}
  43. }
  44. if c.byPackage == nil {
  45. c.byPackage = make(map[string]map[ast.Node]MarkerValues)
  46. }
  47. }
  48. // MarkersInPackage computes the marker values by node for the given package. Results
  49. // are cached by package ID, so this is safe to call repeatedly from different functions.
  50. // Each file in the package is treated as a distinct node.
  51. //
  52. // We consider a marker to be associated with a given AST node if either of the following are true:
  53. //
  54. // - it's in the Godoc for that AST node
  55. //
  56. // - it's in the closest non-godoc comment group above that node,
  57. // *and* that node is a type or field node, *and* [it's either
  58. // registered as type-level *or* it's not registered as being
  59. // package-level]
  60. //
  61. // - it's not in the Godoc of a node, doesn't meet the above criteria, and
  62. // isn't in a struct definition (in which case it's package-level)
  63. func (c *Collector) MarkersInPackage(pkg *loader.Package) (map[ast.Node]MarkerValues, error) {
  64. c.mu.Lock()
  65. c.init()
  66. if markers, exist := c.byPackage[pkg.ID]; exist {
  67. c.mu.Unlock()
  68. return markers, nil
  69. }
  70. // unlock early, it's ok if we do a bit extra work rather than locking while we're working
  71. c.mu.Unlock()
  72. pkg.NeedSyntax()
  73. nodeMarkersRaw := c.associatePkgMarkers(pkg)
  74. markers, err := c.parseMarkersInPackage(nodeMarkersRaw)
  75. if err != nil {
  76. return nil, err
  77. }
  78. c.mu.Lock()
  79. defer c.mu.Unlock()
  80. c.byPackage[pkg.ID] = markers
  81. return markers, nil
  82. }
  83. // parseMarkersInPackage parses the given raw marker comments into output values using the registry.
  84. func (c *Collector) parseMarkersInPackage(nodeMarkersRaw map[ast.Node][]markerComment) (map[ast.Node]MarkerValues, error) {
  85. var errors []error
  86. nodeMarkerValues := make(map[ast.Node]MarkerValues)
  87. for node, markersRaw := range nodeMarkersRaw {
  88. var target TargetType
  89. switch node.(type) {
  90. case *ast.File:
  91. target = DescribesPackage
  92. case *ast.Field:
  93. target = DescribesField
  94. default:
  95. target = DescribesType
  96. }
  97. markerVals := make(map[string][]interface{})
  98. for _, markerRaw := range markersRaw {
  99. markerText := markerRaw.Text()
  100. def := c.Registry.Lookup(markerText, target)
  101. if def == nil {
  102. continue
  103. }
  104. val, err := def.Parse(markerText)
  105. if err != nil {
  106. errors = append(errors, loader.ErrFromNode(err, markerRaw))
  107. continue
  108. }
  109. markerVals[def.Name] = append(markerVals[def.Name], val)
  110. }
  111. nodeMarkerValues[node] = markerVals
  112. }
  113. return nodeMarkerValues, loader.MaybeErrList(errors)
  114. }
  115. // associatePkgMarkers associates markers with AST nodes in the given package.
  116. func (c *Collector) associatePkgMarkers(pkg *loader.Package) map[ast.Node][]markerComment {
  117. nodeMarkers := make(map[ast.Node][]markerComment)
  118. for _, file := range pkg.Syntax {
  119. fileNodeMarkers := c.associateFileMarkers(file)
  120. for node, markers := range fileNodeMarkers {
  121. nodeMarkers[node] = append(nodeMarkers[node], markers...)
  122. }
  123. }
  124. return nodeMarkers
  125. }
  126. // associateFileMarkers associates markers with AST nodes in the given file.
  127. func (c *Collector) associateFileMarkers(file *ast.File) map[ast.Node][]markerComment {
  128. // grab all the raw marker comments by node
  129. visitor := markerSubVisitor{
  130. collectPackageLevel: true,
  131. markerVisitor: &markerVisitor{
  132. nodeMarkers: make(map[ast.Node][]markerComment),
  133. allComments: file.Comments,
  134. },
  135. }
  136. ast.Walk(visitor, file)
  137. // grab the last package-level comments at the end of the file (if any)
  138. lastFileMarkers := visitor.markersBetween(false, visitor.commentInd, len(visitor.allComments))
  139. visitor.pkgMarkers = append(visitor.pkgMarkers, lastFileMarkers...)
  140. // figure out if any type-level markers are actually package-level markers
  141. for node, markers := range visitor.nodeMarkers {
  142. _, isType := node.(*ast.TypeSpec)
  143. if !isType {
  144. continue
  145. }
  146. endOfMarkers := 0
  147. for _, marker := range markers {
  148. if marker.fromGodoc {
  149. // markers from godoc are never package level
  150. markers[endOfMarkers] = marker
  151. endOfMarkers++
  152. continue
  153. }
  154. markerText := marker.Text()
  155. typeDef := c.Registry.Lookup(markerText, DescribesType)
  156. if typeDef != nil {
  157. // prefer assuming type-level markers
  158. markers[endOfMarkers] = marker
  159. endOfMarkers++
  160. continue
  161. }
  162. def := c.Registry.Lookup(markerText, DescribesPackage)
  163. if def == nil {
  164. // assume type-level unless proven otherwise
  165. markers[endOfMarkers] = marker
  166. endOfMarkers++
  167. continue
  168. }
  169. // it's package-level, since a package-level definition exists
  170. visitor.pkgMarkers = append(visitor.pkgMarkers, marker)
  171. }
  172. visitor.nodeMarkers[node] = markers[:endOfMarkers] // re-set after trimming the package markers
  173. }
  174. visitor.nodeMarkers[file] = visitor.pkgMarkers
  175. return visitor.nodeMarkers
  176. }
  177. // markerComment is an AST comment that contains a marker.
  178. // It may or may not be from a Godoc comment, which affects
  179. // marker re-associated (from type-level to package-level)
  180. type markerComment struct {
  181. *ast.Comment
  182. fromGodoc bool
  183. }
  184. // Text returns the text of the marker, stripped of the comment
  185. // marker and leading spaces, as should be passed to Registry.Lookup
  186. // and Registry.Parse.
  187. func (c markerComment) Text() string {
  188. return strings.TrimSpace(c.Comment.Text[2:])
  189. }
  190. // markerVisistor visits AST nodes, recording markers associated with each node.
  191. type markerVisitor struct {
  192. allComments []*ast.CommentGroup
  193. commentInd int
  194. declComments []markerComment
  195. lastLineCommentGroup *ast.CommentGroup
  196. pkgMarkers []markerComment
  197. nodeMarkers map[ast.Node][]markerComment
  198. }
  199. // isMarkerComment checks that the given comment is a single-line (`//`)
  200. // comment and it's first non-space content is `+`.
  201. func isMarkerComment(comment string) bool {
  202. if comment[0:2] != "//" {
  203. return false
  204. }
  205. stripped := strings.TrimSpace(comment[2:])
  206. if len(stripped) < 1 || stripped[0] != '+' {
  207. return false
  208. }
  209. return true
  210. }
  211. // markersBetween grabs the markers between the given indicies in the list of all comments.
  212. func (v *markerVisitor) markersBetween(fromGodoc bool, start, end int) []markerComment {
  213. if start < 0 || end < 0 {
  214. return nil
  215. }
  216. var res []markerComment
  217. for i := start; i < end; i++ {
  218. commentGroup := v.allComments[i]
  219. for _, comment := range commentGroup.List {
  220. if !isMarkerComment(comment.Text) {
  221. continue
  222. }
  223. res = append(res, markerComment{Comment: comment, fromGodoc: fromGodoc})
  224. }
  225. }
  226. return res
  227. }
  228. type markerSubVisitor struct {
  229. *markerVisitor
  230. node ast.Node
  231. collectPackageLevel bool
  232. }
  233. // Visit collects markers for each node in the AST, optionally
  234. // collecting unassociated markers as package-level.
  235. func (v markerSubVisitor) Visit(node ast.Node) ast.Visitor {
  236. if node == nil {
  237. // end of the node, so we might need to advance comments beyond the end
  238. // of the block if we don't want to collect package-level markers in
  239. // this block.
  240. if !v.collectPackageLevel {
  241. if v.commentInd < len(v.allComments) {
  242. lastCommentInd := v.commentInd
  243. nextGroup := v.allComments[lastCommentInd]
  244. for nextGroup.Pos() < v.node.End() {
  245. lastCommentInd++
  246. if lastCommentInd >= len(v.allComments) {
  247. // after the increment so our decrement below still makes sense
  248. break
  249. }
  250. nextGroup = v.allComments[lastCommentInd]
  251. }
  252. v.commentInd = lastCommentInd
  253. }
  254. }
  255. return nil
  256. }
  257. // skip comments on the same line as the previous node
  258. // making sure to double-check for the case where we've gone past the end of the comments
  259. // but still have to finish up typespec-gendecl association (see below).
  260. if v.lastLineCommentGroup != nil && v.commentInd < len(v.allComments) && v.lastLineCommentGroup.Pos() == v.allComments[v.commentInd].Pos() {
  261. v.commentInd++
  262. }
  263. // stop visiting if there are no more comments in the file
  264. // NB(directxman12): we can't just stop immediately, because we
  265. // still need to check if there are typespecs associated with gendecls.
  266. var markerCommentBlock []markerComment
  267. var docCommentBlock []markerComment
  268. lastCommentInd := v.commentInd
  269. if v.commentInd < len(v.allComments) {
  270. // figure out the first comment after the node in question...
  271. nextGroup := v.allComments[lastCommentInd]
  272. for nextGroup.Pos() < node.Pos() {
  273. lastCommentInd++
  274. if lastCommentInd >= len(v.allComments) {
  275. // after the increment so our decrement below still makes sense
  276. break
  277. }
  278. nextGroup = v.allComments[lastCommentInd]
  279. }
  280. lastCommentInd-- // ...then decrement to get the last comment before the node in question
  281. // figure out the godoc comment so we can deal with it separately
  282. var docGroup *ast.CommentGroup
  283. docGroup, v.lastLineCommentGroup = associatedCommentsFor(node)
  284. // find the last comment group that's not godoc
  285. markerCommentInd := lastCommentInd
  286. if docGroup != nil && v.allComments[markerCommentInd].Pos() == docGroup.Pos() {
  287. markerCommentInd--
  288. }
  289. // check if we have freestanding package markers,
  290. // and find the markers in our "closest non-godoc" comment block,
  291. // plus our godoc comment block
  292. if markerCommentInd >= v.commentInd {
  293. if v.collectPackageLevel {
  294. // assume anything between the comment ind and the marker ind (not including it)
  295. // are package-level
  296. v.pkgMarkers = append(v.pkgMarkers, v.markersBetween(false, v.commentInd, markerCommentInd)...)
  297. }
  298. markerCommentBlock = v.markersBetween(false, markerCommentInd, markerCommentInd+1)
  299. docCommentBlock = v.markersBetween(true, markerCommentInd+1, lastCommentInd+1)
  300. } else {
  301. docCommentBlock = v.markersBetween(true, markerCommentInd+1, lastCommentInd+1)
  302. }
  303. }
  304. resVisitor := markerSubVisitor{
  305. collectPackageLevel: false, // don't collect package level by default
  306. markerVisitor: v.markerVisitor,
  307. node: node,
  308. }
  309. // associate those markers with a node
  310. switch typedNode := node.(type) {
  311. case *ast.GenDecl:
  312. // save the comments associated with the gen-decl if it's a single-line type decl
  313. if typedNode.Lparen != token.NoPos || typedNode.Tok != token.TYPE {
  314. // not a single-line type spec, treat them as free comments
  315. v.pkgMarkers = append(v.pkgMarkers, markerCommentBlock...)
  316. break
  317. }
  318. // save these, we'll need them when we encounter the actual type spec
  319. v.declComments = append(v.declComments, markerCommentBlock...)
  320. v.declComments = append(v.declComments, docCommentBlock...)
  321. case *ast.TypeSpec:
  322. // add in comments attributed to the gen-decl, if any,
  323. // as well as comments associated with the actual type
  324. v.nodeMarkers[node] = append(v.nodeMarkers[node], v.declComments...)
  325. v.nodeMarkers[node] = append(v.nodeMarkers[node], markerCommentBlock...)
  326. v.nodeMarkers[node] = append(v.nodeMarkers[node], docCommentBlock...)
  327. v.declComments = nil
  328. v.collectPackageLevel = false // don't collect package-level inside type structs
  329. case *ast.Field:
  330. v.nodeMarkers[node] = append(v.nodeMarkers[node], markerCommentBlock...)
  331. v.nodeMarkers[node] = append(v.nodeMarkers[node], docCommentBlock...)
  332. case *ast.File:
  333. v.pkgMarkers = append(v.pkgMarkers, markerCommentBlock...)
  334. v.pkgMarkers = append(v.pkgMarkers, docCommentBlock...)
  335. // collect markers in root file scope
  336. resVisitor.collectPackageLevel = true
  337. default:
  338. // assume markers before anything else are package-level markers,
  339. // *but* don't include any markers in godoc
  340. if v.collectPackageLevel {
  341. v.pkgMarkers = append(v.pkgMarkers, markerCommentBlock...)
  342. }
  343. }
  344. // increment the comment ind so that we start at the right place for the next node
  345. v.commentInd = lastCommentInd + 1
  346. return resVisitor
  347. }
  348. // associatedCommentsFor returns the doc comment group (if relevant and present) and end-of-line comment
  349. // (again if relevant and present) for the given AST node.
  350. func associatedCommentsFor(node ast.Node) (docGroup *ast.CommentGroup, lastLineCommentGroup *ast.CommentGroup) {
  351. switch typedNode := node.(type) {
  352. case *ast.Field:
  353. docGroup = typedNode.Doc
  354. lastLineCommentGroup = typedNode.Comment
  355. case *ast.File:
  356. docGroup = typedNode.Doc
  357. case *ast.FuncDecl:
  358. docGroup = typedNode.Doc
  359. case *ast.GenDecl:
  360. docGroup = typedNode.Doc
  361. case *ast.ImportSpec:
  362. docGroup = typedNode.Doc
  363. lastLineCommentGroup = typedNode.Comment
  364. case *ast.TypeSpec:
  365. docGroup = typedNode.Doc
  366. lastLineCommentGroup = typedNode.Comment
  367. case *ast.ValueSpec:
  368. docGroup = typedNode.Doc
  369. lastLineCommentGroup = typedNode.Comment
  370. default:
  371. lastLineCommentGroup = nil
  372. }
  373. return docGroup, lastLineCommentGroup
  374. }