| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422 |
- /*
- Copyright 2019 The Kubernetes Authors.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package markers
- import (
- "go/ast"
- "go/token"
- "strings"
- "sync"
- "sigs.k8s.io/controller-tools/pkg/loader"
- )
- // Collector collects and parses marker comments defined in the registry
- // from package source code. If no registry is provided, an empty one will
- // be initialized on the first call to MarkersInPackage.
- type Collector struct {
- *Registry
- byPackage map[string]map[ast.Node]MarkerValues
- mu sync.Mutex
- }
- // MarkerValues are all the values for some set of markers.
- type MarkerValues map[string][]interface{}
- // Get fetches the first value that for the given marker, returning
- // nil if no values are available.
- func (v MarkerValues) Get(name string) interface{} {
- vals := v[name]
- if len(vals) == 0 {
- return nil
- }
- return vals[0]
- }
- func (c *Collector) init() {
- if c.Registry == nil {
- c.Registry = &Registry{}
- }
- if c.byPackage == nil {
- c.byPackage = make(map[string]map[ast.Node]MarkerValues)
- }
- }
- // MarkersInPackage computes the marker values by node for the given package. Results
- // are cached by package ID, so this is safe to call repeatedly from different functions.
- // Each file in the package is treated as a distinct node.
- //
- // We consider a marker to be associated with a given AST node if either of the following are true:
- //
- // - it's in the Godoc for that AST node
- //
- // - it's in the closest non-godoc comment group above that node,
- // *and* that node is a type or field node, *and* [it's either
- // registered as type-level *or* it's not registered as being
- // package-level]
- //
- // - it's not in the Godoc of a node, doesn't meet the above criteria, and
- // isn't in a struct definition (in which case it's package-level)
- func (c *Collector) MarkersInPackage(pkg *loader.Package) (map[ast.Node]MarkerValues, error) {
- c.mu.Lock()
- c.init()
- if markers, exist := c.byPackage[pkg.ID]; exist {
- c.mu.Unlock()
- return markers, nil
- }
- // unlock early, it's ok if we do a bit extra work rather than locking while we're working
- c.mu.Unlock()
- pkg.NeedSyntax()
- nodeMarkersRaw := c.associatePkgMarkers(pkg)
- markers, err := c.parseMarkersInPackage(nodeMarkersRaw)
- if err != nil {
- return nil, err
- }
- c.mu.Lock()
- defer c.mu.Unlock()
- c.byPackage[pkg.ID] = markers
- return markers, nil
- }
- // parseMarkersInPackage parses the given raw marker comments into output values using the registry.
- func (c *Collector) parseMarkersInPackage(nodeMarkersRaw map[ast.Node][]markerComment) (map[ast.Node]MarkerValues, error) {
- var errors []error
- nodeMarkerValues := make(map[ast.Node]MarkerValues)
- for node, markersRaw := range nodeMarkersRaw {
- var target TargetType
- switch node.(type) {
- case *ast.File:
- target = DescribesPackage
- case *ast.Field:
- target = DescribesField
- default:
- target = DescribesType
- }
- markerVals := make(map[string][]interface{})
- for _, markerRaw := range markersRaw {
- markerText := markerRaw.Text()
- def := c.Registry.Lookup(markerText, target)
- if def == nil {
- continue
- }
- val, err := def.Parse(markerText)
- if err != nil {
- errors = append(errors, loader.ErrFromNode(err, markerRaw))
- continue
- }
- markerVals[def.Name] = append(markerVals[def.Name], val)
- }
- nodeMarkerValues[node] = markerVals
- }
- return nodeMarkerValues, loader.MaybeErrList(errors)
- }
- // associatePkgMarkers associates markers with AST nodes in the given package.
- func (c *Collector) associatePkgMarkers(pkg *loader.Package) map[ast.Node][]markerComment {
- nodeMarkers := make(map[ast.Node][]markerComment)
- for _, file := range pkg.Syntax {
- fileNodeMarkers := c.associateFileMarkers(file)
- for node, markers := range fileNodeMarkers {
- nodeMarkers[node] = append(nodeMarkers[node], markers...)
- }
- }
- return nodeMarkers
- }
- // associateFileMarkers associates markers with AST nodes in the given file.
- func (c *Collector) associateFileMarkers(file *ast.File) map[ast.Node][]markerComment {
- // grab all the raw marker comments by node
- visitor := markerSubVisitor{
- collectPackageLevel: true,
- markerVisitor: &markerVisitor{
- nodeMarkers: make(map[ast.Node][]markerComment),
- allComments: file.Comments,
- },
- }
- ast.Walk(visitor, file)
- // grab the last package-level comments at the end of the file (if any)
- lastFileMarkers := visitor.markersBetween(false, visitor.commentInd, len(visitor.allComments))
- visitor.pkgMarkers = append(visitor.pkgMarkers, lastFileMarkers...)
- // figure out if any type-level markers are actually package-level markers
- for node, markers := range visitor.nodeMarkers {
- _, isType := node.(*ast.TypeSpec)
- if !isType {
- continue
- }
- endOfMarkers := 0
- for _, marker := range markers {
- if marker.fromGodoc {
- // markers from godoc are never package level
- markers[endOfMarkers] = marker
- endOfMarkers++
- continue
- }
- markerText := marker.Text()
- typeDef := c.Registry.Lookup(markerText, DescribesType)
- if typeDef != nil {
- // prefer assuming type-level markers
- markers[endOfMarkers] = marker
- endOfMarkers++
- continue
- }
- def := c.Registry.Lookup(markerText, DescribesPackage)
- if def == nil {
- // assume type-level unless proven otherwise
- markers[endOfMarkers] = marker
- endOfMarkers++
- continue
- }
- // it's package-level, since a package-level definition exists
- visitor.pkgMarkers = append(visitor.pkgMarkers, marker)
- }
- visitor.nodeMarkers[node] = markers[:endOfMarkers] // re-set after trimming the package markers
- }
- visitor.nodeMarkers[file] = visitor.pkgMarkers
- return visitor.nodeMarkers
- }
- // markerComment is an AST comment that contains a marker.
- // It may or may not be from a Godoc comment, which affects
- // marker re-associated (from type-level to package-level)
- type markerComment struct {
- *ast.Comment
- fromGodoc bool
- }
- // Text returns the text of the marker, stripped of the comment
- // marker and leading spaces, as should be passed to Registry.Lookup
- // and Registry.Parse.
- func (c markerComment) Text() string {
- return strings.TrimSpace(c.Comment.Text[2:])
- }
- // markerVisistor visits AST nodes, recording markers associated with each node.
- type markerVisitor struct {
- allComments []*ast.CommentGroup
- commentInd int
- declComments []markerComment
- lastLineCommentGroup *ast.CommentGroup
- pkgMarkers []markerComment
- nodeMarkers map[ast.Node][]markerComment
- }
- // isMarkerComment checks that the given comment is a single-line (`//`)
- // comment and it's first non-space content is `+`.
- func isMarkerComment(comment string) bool {
- if comment[0:2] != "//" {
- return false
- }
- stripped := strings.TrimSpace(comment[2:])
- if len(stripped) < 1 || stripped[0] != '+' {
- return false
- }
- return true
- }
- // markersBetween grabs the markers between the given indicies in the list of all comments.
- func (v *markerVisitor) markersBetween(fromGodoc bool, start, end int) []markerComment {
- if start < 0 || end < 0 {
- return nil
- }
- var res []markerComment
- for i := start; i < end; i++ {
- commentGroup := v.allComments[i]
- for _, comment := range commentGroup.List {
- if !isMarkerComment(comment.Text) {
- continue
- }
- res = append(res, markerComment{Comment: comment, fromGodoc: fromGodoc})
- }
- }
- return res
- }
- type markerSubVisitor struct {
- *markerVisitor
- node ast.Node
- collectPackageLevel bool
- }
- // Visit collects markers for each node in the AST, optionally
- // collecting unassociated markers as package-level.
- func (v markerSubVisitor) Visit(node ast.Node) ast.Visitor {
- if node == nil {
- // end of the node, so we might need to advance comments beyond the end
- // of the block if we don't want to collect package-level markers in
- // this block.
- if !v.collectPackageLevel {
- if v.commentInd < len(v.allComments) {
- lastCommentInd := v.commentInd
- nextGroup := v.allComments[lastCommentInd]
- for nextGroup.Pos() < v.node.End() {
- lastCommentInd++
- if lastCommentInd >= len(v.allComments) {
- // after the increment so our decrement below still makes sense
- break
- }
- nextGroup = v.allComments[lastCommentInd]
- }
- v.commentInd = lastCommentInd
- }
- }
- return nil
- }
- // skip comments on the same line as the previous node
- // making sure to double-check for the case where we've gone past the end of the comments
- // but still have to finish up typespec-gendecl association (see below).
- if v.lastLineCommentGroup != nil && v.commentInd < len(v.allComments) && v.lastLineCommentGroup.Pos() == v.allComments[v.commentInd].Pos() {
- v.commentInd++
- }
- // stop visiting if there are no more comments in the file
- // NB(directxman12): we can't just stop immediately, because we
- // still need to check if there are typespecs associated with gendecls.
- var markerCommentBlock []markerComment
- var docCommentBlock []markerComment
- lastCommentInd := v.commentInd
- if v.commentInd < len(v.allComments) {
- // figure out the first comment after the node in question...
- nextGroup := v.allComments[lastCommentInd]
- for nextGroup.Pos() < node.Pos() {
- lastCommentInd++
- if lastCommentInd >= len(v.allComments) {
- // after the increment so our decrement below still makes sense
- break
- }
- nextGroup = v.allComments[lastCommentInd]
- }
- lastCommentInd-- // ...then decrement to get the last comment before the node in question
- // figure out the godoc comment so we can deal with it separately
- var docGroup *ast.CommentGroup
- docGroup, v.lastLineCommentGroup = associatedCommentsFor(node)
- // find the last comment group that's not godoc
- markerCommentInd := lastCommentInd
- if docGroup != nil && v.allComments[markerCommentInd].Pos() == docGroup.Pos() {
- markerCommentInd--
- }
- // check if we have freestanding package markers,
- // and find the markers in our "closest non-godoc" comment block,
- // plus our godoc comment block
- if markerCommentInd >= v.commentInd {
- if v.collectPackageLevel {
- // assume anything between the comment ind and the marker ind (not including it)
- // are package-level
- v.pkgMarkers = append(v.pkgMarkers, v.markersBetween(false, v.commentInd, markerCommentInd)...)
- }
- markerCommentBlock = v.markersBetween(false, markerCommentInd, markerCommentInd+1)
- docCommentBlock = v.markersBetween(true, markerCommentInd+1, lastCommentInd+1)
- } else {
- docCommentBlock = v.markersBetween(true, markerCommentInd+1, lastCommentInd+1)
- }
- }
- resVisitor := markerSubVisitor{
- collectPackageLevel: false, // don't collect package level by default
- markerVisitor: v.markerVisitor,
- node: node,
- }
- // associate those markers with a node
- switch typedNode := node.(type) {
- case *ast.GenDecl:
- // save the comments associated with the gen-decl if it's a single-line type decl
- if typedNode.Lparen != token.NoPos || typedNode.Tok != token.TYPE {
- // not a single-line type spec, treat them as free comments
- v.pkgMarkers = append(v.pkgMarkers, markerCommentBlock...)
- break
- }
- // save these, we'll need them when we encounter the actual type spec
- v.declComments = append(v.declComments, markerCommentBlock...)
- v.declComments = append(v.declComments, docCommentBlock...)
- case *ast.TypeSpec:
- // add in comments attributed to the gen-decl, if any,
- // as well as comments associated with the actual type
- v.nodeMarkers[node] = append(v.nodeMarkers[node], v.declComments...)
- v.nodeMarkers[node] = append(v.nodeMarkers[node], markerCommentBlock...)
- v.nodeMarkers[node] = append(v.nodeMarkers[node], docCommentBlock...)
- v.declComments = nil
- v.collectPackageLevel = false // don't collect package-level inside type structs
- case *ast.Field:
- v.nodeMarkers[node] = append(v.nodeMarkers[node], markerCommentBlock...)
- v.nodeMarkers[node] = append(v.nodeMarkers[node], docCommentBlock...)
- case *ast.File:
- v.pkgMarkers = append(v.pkgMarkers, markerCommentBlock...)
- v.pkgMarkers = append(v.pkgMarkers, docCommentBlock...)
- // collect markers in root file scope
- resVisitor.collectPackageLevel = true
- default:
- // assume markers before anything else are package-level markers,
- // *but* don't include any markers in godoc
- if v.collectPackageLevel {
- v.pkgMarkers = append(v.pkgMarkers, markerCommentBlock...)
- }
- }
- // increment the comment ind so that we start at the right place for the next node
- v.commentInd = lastCommentInd + 1
- return resVisitor
- }
- // associatedCommentsFor returns the doc comment group (if relevant and present) and end-of-line comment
- // (again if relevant and present) for the given AST node.
- func associatedCommentsFor(node ast.Node) (docGroup *ast.CommentGroup, lastLineCommentGroup *ast.CommentGroup) {
- switch typedNode := node.(type) {
- case *ast.Field:
- docGroup = typedNode.Doc
- lastLineCommentGroup = typedNode.Comment
- case *ast.File:
- docGroup = typedNode.Doc
- case *ast.FuncDecl:
- docGroup = typedNode.Doc
- case *ast.GenDecl:
- docGroup = typedNode.Doc
- case *ast.ImportSpec:
- docGroup = typedNode.Doc
- lastLineCommentGroup = typedNode.Comment
- case *ast.TypeSpec:
- docGroup = typedNode.Doc
- lastLineCommentGroup = typedNode.Comment
- case *ast.ValueSpec:
- docGroup = typedNode.Doc
- lastLineCommentGroup = typedNode.Comment
- default:
- lastLineCommentGroup = nil
- }
- return docGroup, lastLineCommentGroup
- }
|