This took a lot longer than it looks to get right. It's not perfect, but it now reliably generates documentation which we can put into gohugo.
789 lines
20 KiB
Go
789 lines
20 KiB
Go
// Mgmt
|
|
// Copyright (C) 2013-2024+ James Shubin and the project contributors
|
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
//
|
|
// Additional permission under GNU GPL version 3 section 7
|
|
//
|
|
// If you modify this program, or any covered work, by linking or combining it
|
|
// with embedded mcl code and modules (and that the embedded mcl code and
|
|
// modules which link with this program, contain a copy of their source code in
|
|
// the authoritative form) containing parts covered by the terms of any other
|
|
// license, the licensors of this program grant you additional permission to
|
|
// convey the resulting work. Furthermore, the licensors of this program grant
|
|
// the original author, James Shubin, additional permission to update this
|
|
// additional permission if he deems it necessary to achieve the goals of this
|
|
// additional permission.
|
|
|
|
package docs
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
cliUtil "github.com/purpleidea/mgmt/cli/util"
|
|
docsUtil "github.com/purpleidea/mgmt/docs/util"
|
|
"github.com/purpleidea/mgmt/engine"
|
|
engineUtil "github.com/purpleidea/mgmt/engine/util"
|
|
"github.com/purpleidea/mgmt/lang/funcs"
|
|
"github.com/purpleidea/mgmt/lang/interfaces"
|
|
"github.com/purpleidea/mgmt/util"
|
|
)
|
|
|
|
const (
|
|
// JSONSuffix is the output extension for the generated documentation.
|
|
JSONSuffix = ".json"
|
|
)
|
|
|
|
// Generate is the main entrypoint for this command. It generates everything.
|
|
type Generate struct {
|
|
*cliUtil.DocsGenerateArgs // embedded config
|
|
Config // embedded Config
|
|
|
|
// Program is the name of this program, usually set at compile time.
|
|
Program string
|
|
|
|
// Version is the version of this program, usually set at compile time.
|
|
Version string
|
|
|
|
// Debug represents if we're running in debug mode or not.
|
|
Debug bool
|
|
|
|
// Logf is a logger which should be used.
|
|
Logf func(format string, v ...interface{})
|
|
}
|
|
|
|
// Main runs everything for this setup item.
|
|
func (obj *Generate) Main(ctx context.Context) error {
|
|
if err := obj.Validate(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := obj.Run(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Validate verifies that the structure has acceptable data stored within.
|
|
func (obj *Generate) Validate() error {
|
|
if obj == nil {
|
|
return fmt.Errorf("data is nil")
|
|
}
|
|
if obj.Program == "" {
|
|
return fmt.Errorf("program is empty")
|
|
}
|
|
if obj.Version == "" {
|
|
return fmt.Errorf("version is empty")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Run performs the desired actions to generate the documentation.
|
|
func (obj *Generate) Run(ctx context.Context) error {
|
|
|
|
outputFile := obj.DocsGenerateArgs.Output
|
|
if outputFile == "" || !strings.HasSuffix(outputFile, JSONSuffix) {
|
|
return fmt.Errorf("must specify output")
|
|
}
|
|
// support relative paths too!
|
|
if !strings.HasPrefix(outputFile, "/") {
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
outputFile = filepath.Join(wd, outputFile)
|
|
}
|
|
|
|
if obj.Debug {
|
|
obj.Logf("output: %s", outputFile)
|
|
}
|
|
|
|
// Ensure the directory exists.
|
|
//d := filepath.Dir(outputFile)
|
|
//if err := os.MkdirAll(d, 0750); err != nil {
|
|
// return fmt.Errorf("could not make output dir at: %s", d)
|
|
//}
|
|
|
|
resources, err := obj.genResources()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
functions, err := obj.genFunctions()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
data := &Output{
|
|
Version: safeVersion(obj.Version),
|
|
Resources: resources,
|
|
Functions: functions,
|
|
}
|
|
|
|
b, err := json.Marshal(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b = append(b, '\n') // needs a trailing newline
|
|
|
|
if err := os.WriteFile(outputFile, b, 0600); err != nil {
|
|
return err
|
|
}
|
|
obj.Logf("wrote: %s", outputFile)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (obj *Generate) getResourceInfo(kind, filename, structName string) (*ResourceInfo, error) {
|
|
rootDir := obj.DocsGenerateArgs.RootDir
|
|
if rootDir == "" {
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rootDir = wd + "/" // add a trailing slash
|
|
}
|
|
if !strings.HasPrefix(rootDir, "/") || !strings.HasSuffix(rootDir, "/") {
|
|
return nil, fmt.Errorf("bad root dir: %s", rootDir)
|
|
}
|
|
|
|
// filename might be "noop.go" for example
|
|
p := filepath.Join(rootDir, engine.ResourcesRelDir, filename)
|
|
|
|
fset := token.NewFileSet()
|
|
|
|
// f is a: https://golang.org/pkg/go/ast/#File
|
|
f, err := parser.ParseFile(fset, p, nil, parser.ParseComments)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// mcl field name to golang field name
|
|
mapping, err := engineUtil.LangFieldNameToStructFieldName(kind)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// golang field name to mcl field name
|
|
nameMap, err := util.MapSwap(mapping)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// mcl field name to mcl type
|
|
typMap, err := engineUtil.LangFieldNameToStructType(kind)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ri := &ResourceInfo{}
|
|
// Populate the fields, even if they don't have a comment.
|
|
ri.Name = structName // golang name
|
|
ri.Kind = kind // duplicate data
|
|
ri.File = filename
|
|
ri.Fields = make(map[string]*ResourceFieldInfo)
|
|
for mclFieldName, fieldName := range mapping {
|
|
typ, exists := typMap[mclFieldName]
|
|
if !exists {
|
|
continue
|
|
}
|
|
|
|
ri.Fields[mclFieldName] = &ResourceFieldInfo{
|
|
Name: fieldName,
|
|
Type: typ.String(),
|
|
Desc: "", // empty for now
|
|
}
|
|
}
|
|
|
|
var previousComment *ast.CommentGroup
|
|
|
|
// Walk through the AST...
|
|
ast.Inspect(f, func(node ast.Node) bool {
|
|
|
|
// Comments above the struct appear as a node right _before_ we
|
|
// find the struct, so if we see one, save it for later...
|
|
if cg, ok := node.(*ast.CommentGroup); ok {
|
|
previousComment = cg
|
|
return true
|
|
}
|
|
|
|
typeSpec, ok := node.(*ast.TypeSpec)
|
|
if !ok {
|
|
return true
|
|
}
|
|
name := typeSpec.Name.Name // name is now known!
|
|
|
|
// If the struct isn't what we're expecting, then move on...
|
|
if name != structName {
|
|
return true
|
|
}
|
|
|
|
// Check if the TypeSpec is a named struct type that we want...
|
|
st, ok := typeSpec.Type.(*ast.StructType)
|
|
if !ok {
|
|
return true
|
|
}
|
|
|
|
// At this point, we have the struct we want...
|
|
|
|
var comment *ast.CommentGroup
|
|
if typeSpec.Doc != nil {
|
|
// I don't know how to even get here...
|
|
comment = typeSpec.Doc // found!
|
|
|
|
} else if previousComment != nil {
|
|
comment = previousComment // found!
|
|
previousComment = nil
|
|
}
|
|
|
|
ri.Desc = commentCleaner(comment)
|
|
|
|
// Iterate over the fields of the struct
|
|
for _, field := range st.Fields.List {
|
|
// Check if the field has a comment associated with it
|
|
if field.Doc == nil {
|
|
continue
|
|
}
|
|
|
|
if len(field.Names) < 1 { // XXX: why does this happen?
|
|
continue
|
|
}
|
|
|
|
fieldName := field.Names[0].Name
|
|
if fieldName == "" { // Can this happen?
|
|
continue
|
|
}
|
|
if isPrivate(fieldName) {
|
|
continue
|
|
}
|
|
|
|
mclFieldName, exists := nameMap[fieldName]
|
|
if !exists {
|
|
continue
|
|
}
|
|
|
|
ri.Fields[mclFieldName].Desc = commentCleaner(field.Doc)
|
|
}
|
|
|
|
return true
|
|
})
|
|
|
|
return ri, nil
|
|
}
|
|
|
|
func (obj *Generate) genResources() (map[string]*ResourceInfo, error) {
|
|
resources := make(map[string]*ResourceInfo)
|
|
if obj.DocsGenerateArgs.NoResources {
|
|
return resources, nil
|
|
}
|
|
|
|
r := engine.RegisteredResourcesNames()
|
|
sort.Strings(r)
|
|
for _, kind := range r {
|
|
metadata, err := docsUtil.LookupResource(kind)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if strings.HasPrefix(kind, "_") {
|
|
// TODO: Should we display these somehow?
|
|
// built-in resource
|
|
continue
|
|
}
|
|
|
|
ri, err := obj.getResourceInfo(kind, metadata.Filename, metadata.Typename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if ri.Name == "" {
|
|
return nil, fmt.Errorf("empty resource name: %s", kind)
|
|
}
|
|
if ri.File == "" {
|
|
return nil, fmt.Errorf("empty resource file: %s", kind)
|
|
}
|
|
if ri.Desc == "" {
|
|
obj.Logf("empty resource desc: %s", kind)
|
|
}
|
|
fields := []string{}
|
|
for field := range ri.Fields {
|
|
fields = append(fields, field)
|
|
}
|
|
sort.Strings(fields)
|
|
for _, field := range fields {
|
|
if ri.Fields[field].Desc == "" {
|
|
obj.Logf("empty resource (%s) field desc: %s", kind, field)
|
|
}
|
|
}
|
|
|
|
resources[kind] = ri
|
|
}
|
|
|
|
return resources, nil
|
|
}
|
|
|
|
func (obj *Generate) getFunctionInfo(pkg, name string, metadata *docsUtil.Metadata) (*FunctionInfo, error) {
|
|
rootDir := obj.DocsGenerateArgs.RootDir
|
|
if rootDir == "" {
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rootDir = wd + "/" // add a trailing slash
|
|
}
|
|
if !strings.HasPrefix(rootDir, "/") || !strings.HasSuffix(rootDir, "/") {
|
|
return nil, fmt.Errorf("bad root dir: %s", rootDir)
|
|
}
|
|
if metadata.Filename == "" {
|
|
return nil, fmt.Errorf("empty filename for: %s.%s", pkg, name)
|
|
}
|
|
|
|
// filename might be "pow.go" for example and contain a rel dir
|
|
p := filepath.Join(rootDir, funcs.FunctionsRelDir, metadata.Filename)
|
|
|
|
fset := token.NewFileSet()
|
|
|
|
// f is a: https://golang.org/pkg/go/ast/#File
|
|
f, err := parser.ParseFile(fset, p, nil, parser.ParseComments)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fi := &FunctionInfo{}
|
|
fi.Name = metadata.Typename
|
|
fi.File = metadata.Filename
|
|
|
|
var previousComment *ast.CommentGroup
|
|
found := false
|
|
|
|
rawFunc := func(node ast.Node) (*ast.CommentGroup, string) {
|
|
fd, ok := node.(*ast.FuncDecl)
|
|
if !ok {
|
|
return nil, ""
|
|
}
|
|
return fd.Doc, fd.Name.Name // name is now known!
|
|
}
|
|
|
|
rawStruct := func(node ast.Node) (*ast.CommentGroup, string) {
|
|
typeSpec, ok := node.(*ast.TypeSpec)
|
|
if !ok {
|
|
return nil, ""
|
|
}
|
|
|
|
// Check if the TypeSpec is a named struct type that we want...
|
|
if _, ok := typeSpec.Type.(*ast.StructType); !ok {
|
|
return nil, ""
|
|
}
|
|
|
|
return typeSpec.Doc, typeSpec.Name.Name // name is now known!
|
|
}
|
|
|
|
// Walk through the AST...
|
|
ast.Inspect(f, func(node ast.Node) bool {
|
|
|
|
// Comments above the struct appear as a node right _before_ we
|
|
// find the struct, so if we see one, save it for later...
|
|
if cg, ok := node.(*ast.CommentGroup); ok {
|
|
previousComment = cg
|
|
return true
|
|
}
|
|
|
|
doc, name := rawFunc(node) // First see if it's a raw func.
|
|
if name == "" {
|
|
doc, name = rawStruct(node) // Otherwise it's a struct.
|
|
}
|
|
|
|
// If the func isn't what we're expecting, then move on...
|
|
if name != metadata.Typename {
|
|
return true
|
|
}
|
|
|
|
var comment *ast.CommentGroup
|
|
if doc != nil {
|
|
// I don't know how to even get here...
|
|
comment = doc // found!
|
|
|
|
} else if previousComment != nil {
|
|
comment = previousComment // found!
|
|
previousComment = nil
|
|
}
|
|
|
|
fi.Desc = commentCleaner(comment)
|
|
found = true
|
|
|
|
return true
|
|
})
|
|
|
|
if !found {
|
|
//return nil, nil
|
|
}
|
|
|
|
return fi, nil
|
|
}
|
|
|
|
func (obj *Generate) genFunctions() (map[string]*FunctionInfo, error) {
|
|
functions := make(map[string]*FunctionInfo)
|
|
if obj.DocsGenerateArgs.NoFunctions {
|
|
return functions, nil
|
|
}
|
|
|
|
m := funcs.Map() // map[string]func() interfaces.Func
|
|
names := []string{}
|
|
for name := range m {
|
|
names = append(names, name)
|
|
}
|
|
sort.Slice(names, func(i, j int) bool {
|
|
a := names[i]
|
|
b := names[j]
|
|
// TODO: do a sorted-by-package order.
|
|
return a < b
|
|
})
|
|
|
|
for _, name := range names {
|
|
//v := m[name]
|
|
//fn := v()
|
|
fn := m[name]()
|
|
|
|
// eg: golang/strings.has_suffix
|
|
sp := strings.Split(name, ".")
|
|
if len(sp) == 0 {
|
|
return nil, fmt.Errorf("unexpected empty function")
|
|
}
|
|
if len(sp) > 2 {
|
|
return nil, fmt.Errorf("unexpected function name: %s", name)
|
|
}
|
|
n := sp[0]
|
|
p := sp[0]
|
|
if len(sp) == 1 { // built-in
|
|
p = "" // no package!
|
|
}
|
|
if len(sp) == 2 { // normal import
|
|
n = sp[1]
|
|
}
|
|
|
|
if strings.HasPrefix(n, "_") {
|
|
// TODO: Should we display these somehow?
|
|
// built-in function
|
|
continue
|
|
}
|
|
|
|
var sig *string
|
|
//iface := ""
|
|
if x := fn.Info().Sig; x != nil {
|
|
s := x.String()
|
|
sig = &s
|
|
//iface = "simple"
|
|
}
|
|
|
|
metadata := &docsUtil.Metadata{}
|
|
|
|
// XXX: maybe we need a better way to get this?
|
|
mdFunc, ok := fn.(interfaces.MetadataFunc)
|
|
if !ok {
|
|
// Function doesn't tell us what the data is, let's try
|
|
// to get it automatically...
|
|
metadata.Typename = funcs.GetFunctionName(fn) // works!
|
|
metadata.Filename = "" // XXX: How can we get this?
|
|
|
|
// XXX: We only need this back-channel metadata store
|
|
// because we don't know how to get the filename without
|
|
// manually writing code in each function. Alternatively
|
|
// we could add a New() method to each struct and then
|
|
// we could modify the struct instead of having it be
|
|
// behind a copy which is needed to get new copies!
|
|
var err error
|
|
metadata, err = docsUtil.LookupFunction(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
} else if mdFunc == nil {
|
|
// programming error
|
|
return nil, fmt.Errorf("unexpected empty metadata for function: %s", name)
|
|
|
|
} else {
|
|
metadata = mdFunc.GetMetadata()
|
|
}
|
|
|
|
if metadata == nil {
|
|
return nil, fmt.Errorf("unexpected nil metadata for function: %s", name)
|
|
}
|
|
|
|
// This may be an empty func name if the function did not know
|
|
// how to get it. (This is normal for automatic regular funcs.)
|
|
if metadata.Typename == "" {
|
|
metadata.Typename = funcs.GetFunctionName(fn) // works!
|
|
}
|
|
|
|
fi, err := obj.getFunctionInfo(p, n, metadata)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// We may not get any fields added if we can't find anything...
|
|
fi.Name = metadata.Typename
|
|
fi.Package = p
|
|
fi.Func = n
|
|
fi.File = metadata.Filename
|
|
//fi.Desc = desc
|
|
fi.Signature = sig
|
|
|
|
if fi.Func == "" {
|
|
return nil, fmt.Errorf("empty function name: %s", name)
|
|
}
|
|
if fi.File == "" {
|
|
return nil, fmt.Errorf("empty function file: %s", name)
|
|
}
|
|
if fi.Desc == "" {
|
|
obj.Logf("empty function desc: %s", name)
|
|
}
|
|
if fi.Signature == nil {
|
|
obj.Logf("empty function sig: %s", name)
|
|
}
|
|
|
|
functions[name] = fi
|
|
}
|
|
|
|
return functions, nil
|
|
}
|
|
|
|
// Output is the type of the final data that will be for the json output.
|
|
type Output struct {
|
|
// Version is the sha1 or ref name of this specific version. This is
|
|
// used if we want to generate documentation with links matching the
|
|
// correct version. If unspecified then this assumes git master.
|
|
Version string `json:"version"`
|
|
|
|
// Resources contains the collection of every available resource!
|
|
// FIXME: should this be a list instead?
|
|
Resources map[string]*ResourceInfo `json:"resources"`
|
|
|
|
// Functions contains the collection of every available function!
|
|
// FIXME: should this be a list instead?
|
|
Functions map[string]*FunctionInfo `json:"functions"`
|
|
}
|
|
|
|
// ResourceInfo stores some information about each resource.
|
|
type ResourceInfo struct {
|
|
// Name is the golang name of this resource.
|
|
Name string `json:"name"`
|
|
|
|
// Kind is the kind of this resource.
|
|
Kind string `json:"kind"`
|
|
|
|
// File is the file name where this resource exists.
|
|
File string `json:"file"`
|
|
|
|
// Desc explains what this resource does.
|
|
Desc string `json:"description"`
|
|
|
|
// Fields is a collection of each resource field and corresponding info.
|
|
Fields map[string]*ResourceFieldInfo `json:"fields"`
|
|
}
|
|
|
|
// ResourceFieldInfo stores some information about each field in each resource.
|
|
type ResourceFieldInfo struct {
|
|
// Name is what this field is called in golang format.
|
|
Name string `json:"name"`
|
|
|
|
// Type is the mcl type for this field.
|
|
Type string `json:"type"`
|
|
|
|
// Desc explains what this field does.
|
|
Desc string `json:"description"`
|
|
}
|
|
|
|
// FunctionInfo stores some information about each function.
|
|
type FunctionInfo struct {
|
|
// Name is the golang name of this function. This may be an actual
|
|
// function if used by the simple API, or the name of a struct.
|
|
Name string `json:"name"`
|
|
|
|
// Package is the import name to use to get to this function.
|
|
Package string `json:"package"`
|
|
|
|
// Func is the name of the function in that package.
|
|
Func string `json:"func"`
|
|
|
|
// File is the file name where this function exists.
|
|
File string `json:"file"`
|
|
|
|
// Desc explains what this function does.
|
|
Desc string `json:"description"`
|
|
|
|
// Signature is the type signature of this function. If empty then the
|
|
// signature is not known statically and it may be polymorphic.
|
|
Signature *string `json:"signature,omitempty"`
|
|
}
|
|
|
|
// commentCleaner takes a comment group and returns it as a clean string. It
|
|
// removes the spurious newlines and programmer-focused comments. If there are
|
|
// blank lines, it replaces them with a single newline. The idea is that the
|
|
// webpage formatter would replace the newline with a <br /> or similar. This
|
|
// code is a modified alternative of the ast.CommentGroup.Text() function.
|
|
func commentCleaner(g *ast.CommentGroup) string {
|
|
if g == nil {
|
|
return ""
|
|
}
|
|
comments := make([]string, len(g.List))
|
|
for i, c := range g.List {
|
|
comments[i] = c.Text
|
|
}
|
|
|
|
lines := make([]string, 0, 10) // most comments are less than 10 lines
|
|
for _, c := range comments {
|
|
// Remove comment markers.
|
|
// The parser has given us exactly the comment text.
|
|
switch c[1] {
|
|
case '/':
|
|
//-style comment (no newline at the end)
|
|
c = c[2:]
|
|
if len(c) == 0 {
|
|
// empty line
|
|
break
|
|
}
|
|
if isDevComment(c[1:]) { // get rid of one space
|
|
continue
|
|
}
|
|
if c[0] == ' ' {
|
|
// strip first space - required for Example tests
|
|
c = c[1:]
|
|
break
|
|
}
|
|
//if isDirective(c) {
|
|
// // Ignore //go:noinline, //line, and so on.
|
|
// continue
|
|
//}
|
|
case '*':
|
|
/*-style comment */
|
|
c = c[2 : len(c)-2]
|
|
}
|
|
|
|
// Split on newlines.
|
|
cl := strings.Split(c, "\n")
|
|
|
|
// Walk lines, stripping trailing white space and adding to list.
|
|
for _, l := range cl {
|
|
lines = append(lines, stripTrailingWhitespace(l))
|
|
}
|
|
}
|
|
|
|
// Remove leading blank lines; convert runs of interior blank lines to a
|
|
// single blank line.
|
|
n := 0
|
|
for _, line := range lines {
|
|
if line != "" || n > 0 && lines[n-1] != "" {
|
|
lines[n] = line
|
|
n++
|
|
}
|
|
}
|
|
lines = lines[0:n]
|
|
|
|
// Concatenate all of these together. Blank lines should be a newline.
|
|
s := ""
|
|
for i, line := range lines {
|
|
if line == "" {
|
|
continue
|
|
}
|
|
s += line
|
|
if i < len(lines)-1 { // Is there another line?
|
|
if lines[i+1] == "" {
|
|
s += "\n" // Will eventually be a line break.
|
|
} else {
|
|
s += " "
|
|
}
|
|
}
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
// TODO: should we use unicode.IsSpace instead?
|
|
func isWhitespace(ch byte) bool { return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' }
|
|
|
|
// TODO: should we replace with a strings package stdlib function?
|
|
func stripTrailingWhitespace(s string) string {
|
|
i := len(s)
|
|
for i > 0 && isWhitespace(s[i-1]) {
|
|
i--
|
|
}
|
|
return s[0:i]
|
|
}
|
|
|
|
// isPrivate specifies if a field name is "private" or not.
|
|
func isPrivate(fieldName string) bool {
|
|
if fieldName == "" {
|
|
panic("invalid field name")
|
|
}
|
|
x := fieldName[0:1]
|
|
|
|
if strings.ToLower(x) == x {
|
|
return true // it was already private
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// isDevComment tells us that the comment is for developers only!
|
|
func isDevComment(comment string) bool {
|
|
if strings.HasPrefix(comment, "TODO:") {
|
|
return true
|
|
}
|
|
if strings.HasPrefix(comment, "FIXME:") {
|
|
return true
|
|
}
|
|
if strings.HasPrefix(comment, "XXX:") {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// safeVersion parses the main version string and returns a short hash for us.
|
|
// For example, we might get a string of 0.0.26-176-gabcdef012-dirty as input,
|
|
// and we'd want to return abcdef012.
|
|
func safeVersion(version string) string {
|
|
const dirty = "-dirty"
|
|
|
|
s := version
|
|
if strings.HasSuffix(s, dirty) { // helpful dirty remover
|
|
s = s[0 : len(s)-len(dirty)]
|
|
}
|
|
|
|
ix := strings.LastIndex(s, "-")
|
|
if ix == -1 { // assume we have a standalone version (future proofing?)
|
|
return s
|
|
}
|
|
s = s[ix+1:]
|
|
|
|
// From the `git describe` man page: The "g" prefix stands for "git" and
|
|
// is used to allow describing the version of a software depending on
|
|
// the SCM the software is managed with. This is useful in an
|
|
// environment where people may use different SCMs.
|
|
const g = "g"
|
|
if strings.HasPrefix(s, g) {
|
|
s = s[len(g):]
|
|
}
|
|
|
|
return s
|
|
}
|