karmada/hack/tools/lifted-gen/lifted-gen.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
}
}