265 lines
5.9 KiB
Go
265 lines
5.9 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/olekukonko/tablewriter"
|
|
)
|
|
|
|
const (
|
|
// a lifted comment line shall be like `+lifted:key=value`
|
|
liftedPrefix = "+lifted:"
|
|
liftedKeyValueSeparator = "="
|
|
|
|
liftedKeySource = "source"
|
|
liftedKeyChanged = "changed"
|
|
|
|
docPrefix = "// Code generated by hack/update-lifted. DO NOT EDIT.\n\n" +
|
|
"// Package lifted contains the files lifted from other projects.\n" +
|
|
"package lifted\n\n/*\n"
|
|
docSuffix = "*/\n"
|
|
)
|
|
|
|
var (
|
|
// TODO: add to flag
|
|
liftedDir = "pkg/util/lifted"
|
|
output = "pkg/util/lifted/doc.go"
|
|
excludeFiles = []string{"doc.go"}
|
|
)
|
|
|
|
func main() {
|
|
a := newAnalyzer()
|
|
a.collect(liftedDir)
|
|
a.dump(output)
|
|
|
|
fmt.Println()
|
|
if a.failed {
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
type analyzer struct {
|
|
fset *token.FileSet
|
|
failed bool
|
|
items []item
|
|
}
|
|
|
|
func newAnalyzer() *analyzer {
|
|
a := &analyzer{
|
|
fset: token.NewFileSet(),
|
|
}
|
|
return a
|
|
}
|
|
|
|
func (a *analyzer) collect(dir string) {
|
|
pkgs, err := parser.ParseDir(a.fset, dir, a.filter, parser.AllErrors|parser.ParseComments)
|
|
if err != nil {
|
|
a.errorf("syntax error: %v", err)
|
|
return
|
|
}
|
|
|
|
for _, pkg := range pkgs {
|
|
sortedFilenames := a.sortFilenames(pkg.Files)
|
|
for _, filename := range sortedFilenames {
|
|
file := pkg.Files[filename]
|
|
basename := filepath.Base(filename)
|
|
a.collectFile(basename, file)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (a *analyzer) filter(info fs.FileInfo) bool {
|
|
for _, pattern := range excludeFiles {
|
|
match, err := filepath.Match(pattern, info.Name())
|
|
if err != nil {
|
|
a.errorf("match %v %v error: %v", pattern, info.Name(), err)
|
|
return false
|
|
}
|
|
if match {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (a *analyzer) sortFilenames(m map[string]*ast.File) []string {
|
|
files := make([]string, 0, len(m))
|
|
for n := range m {
|
|
files = append(files, n)
|
|
}
|
|
sort.Strings(files)
|
|
return files
|
|
}
|
|
|
|
func (a *analyzer) collectFile(filename string, file *ast.File) {
|
|
cmap := ast.NewCommentMap(a.fset, file, file.Comments)
|
|
for _, decl := range file.Decls {
|
|
switch decl := decl.(type) {
|
|
case *ast.FuncDecl:
|
|
a.collectFuncDecl(cmap, filename, decl)
|
|
case *ast.GenDecl:
|
|
a.collectGenDecl(cmap, filename, decl)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (a *analyzer) collectGenDecl(cmap ast.CommentMap, file string, decl *ast.GenDecl) {
|
|
for _, spec := range decl.Specs {
|
|
switch spec := spec.(type) {
|
|
case *ast.TypeSpec:
|
|
a.collectTypeSpec(cmap, file, decl, spec)
|
|
case *ast.ValueSpec:
|
|
a.collectValueSpec(cmap, file, decl, spec)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (a *analyzer) collectTypeSpec(cmap ast.CommentMap, file string, decl *ast.GenDecl, spec *ast.TypeSpec) {
|
|
// for GenDecl, comments are in decl, not spec
|
|
item, ok := a.parseComments(cmap[decl])
|
|
if !ok {
|
|
return
|
|
}
|
|
item.file = file
|
|
item.kindName += decl.Tok.String() /* type */ + " " + spec.Name.Name
|
|
a.items = append(a.items, item)
|
|
}
|
|
|
|
func (a *analyzer) collectValueSpec(cmap ast.CommentMap, file string, decl *ast.GenDecl, spec *ast.ValueSpec) {
|
|
// for GenDecl, comments are in decl, not spec
|
|
item, ok := a.parseComments(cmap[decl])
|
|
if !ok {
|
|
return
|
|
}
|
|
item.file = file
|
|
// One spec may contains one or more name. we combine then to one item.
|
|
// e.g.
|
|
// ```
|
|
// var a, b = 1, 2
|
|
// ```
|
|
// spec.Names will be [a, b]. And generate `var a b` as item's kindName.
|
|
item.kindName = decl.Tok.String() /* var/const */
|
|
for _, name := range spec.Names {
|
|
item.kindName += " " + name.Name
|
|
}
|
|
a.items = append(a.items, item)
|
|
}
|
|
|
|
func (a *analyzer) collectFuncDecl(cmap ast.CommentMap, file string, decl *ast.FuncDecl) {
|
|
item, ok := a.parseComments(cmap[decl])
|
|
if !ok {
|
|
return
|
|
}
|
|
item.file = file
|
|
item.kindName = "func " + decl.Name.Name
|
|
a.items = append(a.items, item)
|
|
}
|
|
|
|
func (a *analyzer) parseComments(comments []*ast.CommentGroup) (item item, ok bool) {
|
|
for _, comment := range comments {
|
|
for _, c := range comment.List {
|
|
key, value, isLifted := parseLiftedLine(c.Text)
|
|
if !isLifted {
|
|
continue
|
|
}
|
|
|
|
switch key {
|
|
case liftedKeySource:
|
|
item.source = value
|
|
ok = true
|
|
case liftedKeyChanged:
|
|
item.changed = parseBool(value, true)
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (a *analyzer) dump(path string) {
|
|
file, err := os.Create(path)
|
|
if err != nil {
|
|
a.errorf("open %s error: %v", path, err)
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
fmt.Fprint(file, docPrefix)
|
|
defer fmt.Fprint(file, docSuffix)
|
|
|
|
table := newTableWriter(file)
|
|
for _, lifted := range a.items {
|
|
row := []string{lifted.file, lifted.source, lifted.kindName, "N"}
|
|
if lifted.changed {
|
|
row[3] = "Y"
|
|
}
|
|
table.Append(row)
|
|
}
|
|
table.Render()
|
|
}
|
|
|
|
func newTableWriter(w io.Writer) *tablewriter.Table {
|
|
table := tablewriter.NewWriter(w)
|
|
table.SetHeader([]string{"lifted file ", "source file", "const/var/type/func", "changed"})
|
|
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
|
|
table.SetAutoFormatHeaders(false)
|
|
table.SetAutoWrapText(false)
|
|
table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false})
|
|
table.SetCenterSeparator("|")
|
|
return table
|
|
}
|
|
|
|
func (a *analyzer) errorf(format string, args ...interface{}) {
|
|
fmt.Fprintf(os.Stderr, "ERROR "+format, args...)
|
|
fmt.Fprintln(os.Stderr)
|
|
a.failed = true
|
|
}
|
|
|
|
type item struct {
|
|
file string
|
|
source string
|
|
kindName string
|
|
changed bool
|
|
}
|
|
|
|
func parseLiftedLine(line string) (key, value string, isLifted bool) {
|
|
// a lifted lie is like: `// +lifted:key=value`
|
|
line = strings.TrimSpace(line)
|
|
line = strings.TrimPrefix(line, "//")
|
|
line = strings.TrimSpace(line)
|
|
|
|
isLifted = strings.HasPrefix(line, liftedPrefix)
|
|
if !isLifted {
|
|
return
|
|
}
|
|
line = strings.TrimPrefix(line, liftedPrefix)
|
|
parts := strings.SplitN(line, liftedKeyValueSeparator, 2)
|
|
if len(parts) > 0 {
|
|
key = strings.TrimSpace(parts[0])
|
|
}
|
|
if len(parts) > 1 {
|
|
value = strings.TrimSpace(parts[1])
|
|
}
|
|
return
|
|
}
|
|
|
|
func parseBool(s string, defaultValue bool) bool {
|
|
s = strings.ToLower(s)
|
|
switch s {
|
|
case "true":
|
|
return true
|
|
case "false":
|
|
return false
|
|
default:
|
|
return defaultValue
|
|
}
|
|
}
|