kilo/vendor/sigs.k8s.io/controller-tools/pkg/markers/collect.go

423 lines
14 KiB
Go
Raw Normal View History

/*
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
}