kubernetes/vendor/github.com/bazelbuild/buildtools/edit/edit.go
2019-02-12 18:38:36 -08:00

824 lines
23 KiB
Go

/*
Copyright 2016 Google Inc. All Rights Reserved.
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 edit provides high-level auxiliary functions for AST manipulation
// on BUILD files.
package edit
import (
"fmt"
"os"
"path"
"path/filepath"
"sort"
"strconv"
"strings"
"github.com/bazelbuild/buildtools/build"
"github.com/bazelbuild/buildtools/wspace"
)
var (
// ShortenLabelsFlag if true converts added labels to short form , e.g. //foo:bar => :bar
ShortenLabelsFlag = true
// DeleteWithComments if true a list attribute will be be deleted in ListDelete, even if there is a comment attached to it
DeleteWithComments = true
)
// ParseLabel parses a Blaze label (eg. //devtools/buildozer:rule), and returns
// the repo name ("" for the main repo), package (with leading slashes trimmed)
// and rule name (e.g. ["", "devtools/buildozer", "rule"]).
func ParseLabel(target string) (string, string, string) {
repo := ""
if strings.HasPrefix(target, "@") {
target = strings.TrimLeft(target, "@")
parts := strings.SplitN(target, "/", 2)
if len(parts) == 1 {
// "@foo" -> "foo", "", "foo" (ie @foo//:foo)
return target, "", target
}
repo = parts[0]
target = "/" + parts[1]
}
// TODO(bazel-team): check if the next line can now be deleted
target = strings.TrimRight(target, ":") // labels can end with ':'
parts := strings.SplitN(target, ":", 2)
parts[0] = strings.TrimPrefix(parts[0], "//")
if len(parts) == 1 {
if strings.HasPrefix(target, "//") {
// "//absolute/pkg" -> "absolute/pkg", "pkg"
return repo, parts[0], path.Base(parts[0])
}
// "relative/label" -> "", "relative/label"
return repo, "", parts[0]
}
return repo, parts[0], parts[1]
}
// ShortenLabel rewrites labels to use the canonical form (the form
// recommended by build-style). This behavior can be disabled using the
// --noshorten_labels flag for projects that consistently use long-form labels.
// "//foo/bar:bar" => "//foo/bar", or ":bar" when possible.
func ShortenLabel(label string, pkg string) string {
if !ShortenLabelsFlag {
return label
}
if !strings.HasPrefix(label, "//") {
// It doesn't look like a long label, so we preserve it.
return label
}
repo, labelPkg, rule := ParseLabel(label)
if repo == "" && labelPkg == pkg { // local label
return ":" + rule
}
slash := strings.LastIndex(labelPkg, "/")
if (slash >= 0 && labelPkg[slash+1:] == rule) || labelPkg == rule {
return "//" + labelPkg
}
return label
}
// LabelsEqual returns true if label1 and label2 are equal. The function
// takes care of the optional ":" prefix and differences between long-form
// labels and local labels.
func LabelsEqual(label1, label2, pkg string) bool {
str1 := strings.TrimPrefix(ShortenLabel(label1, pkg), ":")
str2 := strings.TrimPrefix(ShortenLabel(label2, pkg), ":")
return str1 == str2
}
// isFile returns true if the path refers to a regular file after following
// symlinks.
func isFile(path string) bool {
path, err := filepath.EvalSymlinks(path)
if err != nil {
return false
}
info, err := os.Stat(path)
if err != nil {
return false
}
return info.Mode().IsRegular()
}
// InterpretLabelForWorkspaceLocation returns the name of the BUILD file to
// edit, the full package name, and the rule. It takes a workspace-rooted
// directory to use.
func InterpretLabelForWorkspaceLocation(root string, target string) (buildFile string, pkg string, rule string) {
repo, pkg, rule := ParseLabel(target)
rootDir, relativePath := wspace.FindWorkspaceRoot(root)
if repo != "" {
files, err := wspace.FindRepoBuildFiles(rootDir)
if err == nil {
if buildFile, ok := files[repo]; ok {
return buildFile, pkg, rule
}
}
// TODO(rodrigoq): report error for other repos
}
if strings.HasPrefix(target, "//") {
buildFile = path.Join(rootDir, pkg, "BUILD")
return
}
if isFile(pkg) {
// allow operation on other files like WORKSPACE
buildFile = pkg
pkg = path.Join(relativePath, filepath.Dir(pkg))
return
}
if pkg != "" {
buildFile = pkg + "/BUILD"
} else {
buildFile = "BUILD"
}
pkg = path.Join(relativePath, pkg)
return
}
// InterpretLabel returns the name of the BUILD file to edit, the full
// package name, and the rule. It uses the pwd for resolving workspace file paths.
func InterpretLabel(target string) (buildFile string, pkg string, rule string) {
return InterpretLabelForWorkspaceLocation("", target)
}
// ExprToRule returns a Rule from an Expr.
// The boolean is false iff the Expr is not a function call, or does not have
// the expected kind.
func ExprToRule(expr build.Expr, kind string) (*build.Rule, bool) {
call, ok := expr.(*build.CallExpr)
if !ok {
return nil, false
}
k, ok := call.X.(*build.LiteralExpr)
if !ok || k.Token != kind {
return nil, false
}
return &build.Rule{Call: call}, true
}
// ExistingPackageDeclaration returns the package declaration, or nil if there is none.
func ExistingPackageDeclaration(f *build.File) *build.Rule {
for _, stmt := range f.Stmt {
if rule, ok := ExprToRule(stmt, "package"); ok {
return rule
}
}
return nil
}
// PackageDeclaration returns the package declaration. If it doesn't
// exist, it is created at the top of the BUILD file, after leading
// comments.
func PackageDeclaration(f *build.File) *build.Rule {
if pkg := ExistingPackageDeclaration(f); pkg != nil {
return pkg
}
all := []build.Expr{}
added := false
call := &build.CallExpr{X: &build.LiteralExpr{Token: "package"}}
// Skip CommentBlocks and find a place to insert the package declaration.
for _, stmt := range f.Stmt {
_, ok := stmt.(*build.CommentBlock)
if !ok && !added {
all = append(all, call)
added = true
}
all = append(all, stmt)
}
if !added { // In case the file is empty.
all = append(all, call)
}
f.Stmt = all
return &build.Rule{Call: call}
}
// RemoveEmptyPackage removes empty package declarations from the file, i.e.:
// package()
// This might appear because of a buildozer transformation (e.g. when removing a package
// attribute). Removing it is required for the file to be valid.
func RemoveEmptyPackage(f *build.File) *build.File {
var all []build.Expr
for _, stmt := range f.Stmt {
if call, ok := stmt.(*build.CallExpr); ok {
functionName, ok := call.X.(*build.LiteralExpr)
if ok && functionName.Token == "package" && len(call.List) == 0 {
continue
}
}
all = append(all, stmt)
}
return &build.File{Path: f.Path, Comments: f.Comments, Stmt: all}
}
// InsertAfter inserts an expression after index i.
func InsertAfter(i int, stmt []build.Expr, expr build.Expr) []build.Expr {
i = i + 1 // index after the element at i
result := make([]build.Expr, len(stmt)+1)
copy(result[0:i], stmt[0:i])
result[i] = expr
copy(result[i+1:], stmt[i:])
return result
}
// IndexOfLast finds the index of the last expression of a specific kind.
func IndexOfLast(stmt []build.Expr, Kind string) int {
lastIndex := -1
for i, s := range stmt {
sAsCallExpr, ok := s.(*build.CallExpr)
if !ok {
continue
}
literal, ok := sAsCallExpr.X.(*build.LiteralExpr)
if ok && literal.Token == Kind {
lastIndex = i
}
}
return lastIndex
}
// InsertAfterLastOfSameKind inserts an expression after the last expression of the same kind.
func InsertAfterLastOfSameKind(stmt []build.Expr, expr *build.CallExpr) []build.Expr {
index := IndexOfLast(stmt, expr.X.(*build.LiteralExpr).Token)
if index == -1 {
return InsertAtEnd(stmt, expr)
}
return InsertAfter(index, stmt, expr)
}
// InsertAtEnd inserts an expression at the end of a list, before trailing comments.
func InsertAtEnd(stmt []build.Expr, expr build.Expr) []build.Expr {
var i int
for i = len(stmt) - 1; i >= 0; i-- {
_, ok := stmt[i].(*build.CommentBlock)
if !ok {
break
}
}
return InsertAfter(i, stmt, expr)
}
// FindRuleByName returns the rule in the file that has the given name.
// If the name is "__pkg__", it returns the global package declaration.
func FindRuleByName(f *build.File, name string) *build.Rule {
if name == "__pkg__" {
return PackageDeclaration(f)
}
i := IndexOfRuleByName(f, name)
if i != -1 {
return &build.Rule{Call: f.Stmt[i].(*build.CallExpr)}
}
return nil
}
// UseImplicitName returns the rule in the file if it meets these conditions:
// - It is the only unnamed rule in the file.
// - The file path's ending directory name and the passed rule name match.
// In the Pants Build System, by pantsbuild, the use of an implicit name makes
// creating targets easier. This function implements such names.
func UseImplicitName(f *build.File, rule string) *build.Rule {
// We disallow empty names
if f.Path == "BUILD" {
return nil
}
ruleCount := 0
var temp, found *build.Rule
pkg := filepath.Base(filepath.Dir(f.Path))
for _, stmt := range f.Stmt {
call, ok := stmt.(*build.CallExpr)
if !ok {
continue
}
temp = &build.Rule{Call: call}
if temp.Kind() != "" && temp.Name() == "" {
ruleCount++
found = temp
}
}
if ruleCount == 1 {
if rule == pkg {
return found
}
}
return nil
}
// IndexOfRuleByName returns the index (in f.Stmt) of the CallExpr which defines a rule named `name`, or -1 if it doesn't exist.
func IndexOfRuleByName(f *build.File, name string) int {
linenum := -1
if strings.HasPrefix(name, "%") {
// "%<LINENUM>" will match the rule which begins at LINENUM.
// This is for convenience, "%" is not a valid character in bazel targets.
if result, err := strconv.Atoi(name[1:]); err == nil {
linenum = result
}
}
for i, stmt := range f.Stmt {
call, ok := stmt.(*build.CallExpr)
if !ok {
continue
}
r := &build.Rule{Call: call}
start, _ := call.X.Span()
if r.Name() == name || start.Line == linenum {
return i
}
}
return -1
}
// FindExportedFile returns the first exports_files call which contains the
// file 'name', or nil if not found
func FindExportedFile(f *build.File, name string) *build.Rule {
for _, r := range f.Rules("exports_files") {
if len(r.Call.List) == 0 {
continue
}
pkg := "" // Files are not affected by the package name
if ListFind(r.Call.List[0], name, pkg) != nil {
return r
}
}
return nil
}
// DeleteRule returns the AST without the specified rule
func DeleteRule(f *build.File, rule *build.Rule) *build.File {
var all []build.Expr
for _, stmt := range f.Stmt {
if stmt == rule.Call {
continue
}
all = append(all, stmt)
}
return &build.File{Path: f.Path, Comments: f.Comments, Stmt: all}
}
// DeleteRuleByName returns the AST without the rules that have the
// given name.
func DeleteRuleByName(f *build.File, name string) *build.File {
var all []build.Expr
for _, stmt := range f.Stmt {
call, ok := stmt.(*build.CallExpr)
if !ok {
all = append(all, stmt)
continue
}
r := &build.Rule{Call: call}
if r.Name() != name {
all = append(all, stmt)
}
}
return &build.File{Path: f.Path, Comments: f.Comments, Stmt: all}
}
// DeleteRuleByKind removes the rules of the specified kind from the AST.
// Returns an updated copy of f.
func DeleteRuleByKind(f *build.File, kind string) *build.File {
var all []build.Expr
for _, stmt := range f.Stmt {
call, ok := stmt.(*build.CallExpr)
if !ok {
all = append(all, stmt)
continue
}
k, ok := call.X.(*build.LiteralExpr)
if !ok || k.Token != kind {
all = append(all, stmt)
}
}
return &build.File{Path: f.Path, Comments: f.Comments, Stmt: all}
}
// AllLists returns all the lists concatenated in an expression.
// For example, in: glob(["*.go"]) + [":rule"]
// the function will return [[":rule"]].
func AllLists(e build.Expr) []*build.ListExpr {
switch e := e.(type) {
case *build.ListExpr:
return []*build.ListExpr{e}
case *build.BinaryExpr:
if e.Op == "+" {
return append(AllLists(e.X), AllLists(e.Y)...)
}
}
return nil
}
// FirstList works in the same way as AllLists, except that it
// returns only one list, or nil.
func FirstList(e build.Expr) *build.ListExpr {
switch e := e.(type) {
case *build.ListExpr:
return e
case *build.BinaryExpr:
if e.Op == "+" {
li := FirstList(e.X)
if li == nil {
return FirstList(e.Y)
}
return li
}
}
return nil
}
// AllStrings returns all the string literals concatenated in an expression.
// For example, in: "foo" + x + "bar"
// the function will return ["foo", "bar"].
func AllStrings(e build.Expr) []*build.StringExpr {
switch e := e.(type) {
case *build.StringExpr:
return []*build.StringExpr{e}
case *build.BinaryExpr:
if e.Op == "+" {
return append(AllStrings(e.X), AllStrings(e.Y)...)
}
}
return nil
}
// ListFind looks for a string in the list expression (which may be a
// concatenation of lists). It returns the element if it is found. nil
// otherwise.
func ListFind(e build.Expr, item string, pkg string) *build.StringExpr {
item = ShortenLabel(item, pkg)
for _, li := range AllLists(e) {
for _, elem := range li.List {
str, ok := elem.(*build.StringExpr)
if ok && LabelsEqual(str.Value, item, pkg) {
return str
}
}
}
return nil
}
// hasComments returns whether the StringExpr literal has a comment attached to it.
func hasComments(literal *build.StringExpr) bool {
return len(literal.Before) > 0 || len(literal.Suffix) > 0
}
// ContainsComments returns whether the expr has a comment that includes str.
func ContainsComments(expr build.Expr, str string) bool {
str = strings.ToLower(str)
com := expr.Comment()
comments := append(com.Before, com.Suffix...)
comments = append(comments, com.After...)
for _, c := range comments {
if strings.Contains(strings.ToLower(c.Token), str) {
return true
}
}
return false
}
// ListDelete deletes the item from a list expression in e and returns
// the StringExpr deleted, or nil otherwise.
func ListDelete(e build.Expr, item, pkg string) (deleted *build.StringExpr) {
deleted = nil
item = ShortenLabel(item, pkg)
for _, li := range AllLists(e) {
var all []build.Expr
for _, elem := range li.List {
if str, ok := elem.(*build.StringExpr); ok {
if LabelsEqual(str.Value, item, pkg) && (DeleteWithComments || !hasComments(str)) {
deleted = str
continue
}
}
all = append(all, elem)
}
li.List = all
}
return deleted
}
// ListAttributeDelete deletes string item from list attribute attr, deletes attr if empty,
// and returns the StringExpr deleted, or nil otherwise.
func ListAttributeDelete(rule *build.Rule, attr, item, pkg string) *build.StringExpr {
deleted := ListDelete(rule.Attr(attr), item, pkg)
if deleted != nil {
if listExpr, ok := rule.Attr(attr).(*build.ListExpr); ok && len(listExpr.List) == 0 {
rule.DelAttr(attr)
}
}
return deleted
}
// ListReplace replaces old with value in all lists in e and returns a Boolean
// to indicate whether the replacement was successful.
func ListReplace(e build.Expr, old, value, pkg string) bool {
replaced := false
old = ShortenLabel(old, pkg)
for _, li := range AllLists(e) {
for k, elem := range li.List {
str, ok := elem.(*build.StringExpr)
if !ok || !LabelsEqual(str.Value, old, pkg) {
continue
}
li.List[k] = &build.StringExpr{Value: ShortenLabel(value, pkg), Comments: *elem.Comment()}
replaced = true
}
}
return replaced
}
// isExprLessThan compares two Expr statements. Currently, only labels are supported.
func isExprLessThan(x1, x2 build.Expr) bool {
str1, ok1 := x1.(*build.StringExpr)
str2, ok2 := x2.(*build.StringExpr)
if ok1 != ok2 {
return ok2
}
if ok1 && ok2 {
// Labels starting with // are put at the end.
pre1 := strings.HasPrefix(str1.Value, "//")
pre2 := strings.HasPrefix(str2.Value, "//")
if pre1 != pre2 {
return pre2
}
return str1.Value < str2.Value
}
return false
}
func sortedInsert(list []build.Expr, item build.Expr) []build.Expr {
i := 0
for ; i < len(list); i++ {
if isExprLessThan(item, list[i]) {
break
}
}
res := make([]build.Expr, 0, len(list)+1)
res = append(res, list[:i]...)
res = append(res, item)
res = append(res, list[i:]...)
return res
}
// attributeMustNotBeSorted returns true if the list in the attribute cannot be
// sorted. For some attributes, it makes sense to try to do a sorted insert
// (e.g. deps), even when buildifier will not sort it for conservative reasons.
// For a few attributes, sorting will never make sense.
func attributeMustNotBeSorted(rule, attr string) bool {
// TODO(bazel-team): Come up with a more complete list.
return attr == "args"
}
// getVariable returns the binary expression that assignes a variable to expr, if expr is
// an identifier of a variable that vars contains a mapping for.
func getVariable(expr build.Expr, vars *map[string]*build.BinaryExpr) (varAssignment *build.BinaryExpr) {
if vars == nil {
return nil
}
if literal, ok := expr.(*build.LiteralExpr); ok {
if varAssignment = (*vars)[literal.Token]; varAssignment != nil {
return varAssignment
}
}
return nil
}
// AddValueToList adds a value to a list. If the expression is
// not a list, a list with a single element is appended to the original
// expression.
func AddValueToList(oldList build.Expr, pkg string, item build.Expr, sorted bool) build.Expr {
if oldList == nil {
return &build.ListExpr{List: []build.Expr{item}}
}
str, ok := item.(*build.StringExpr)
if ok && ListFind(oldList, str.Value, pkg) != nil {
// The value is already in the list.
return oldList
}
li := FirstList(oldList)
if li != nil {
if sorted {
li.List = sortedInsert(li.List, item)
} else {
li.List = append(li.List, item)
}
return oldList
}
list := &build.ListExpr{List: []build.Expr{item}}
concat := &build.BinaryExpr{Op: "+", X: oldList, Y: list}
return concat
}
// AddValueToListAttribute adds the given item to the list attribute identified by name and pkg.
func AddValueToListAttribute(r *build.Rule, name string, pkg string, item build.Expr, vars *map[string]*build.BinaryExpr) {
old := r.Attr(name)
sorted := !attributeMustNotBeSorted(r.Kind(), name)
if varAssignment := getVariable(old, vars); varAssignment != nil {
varAssignment.Y = AddValueToList(varAssignment.Y, pkg, item, sorted)
} else {
r.SetAttr(name, AddValueToList(old, pkg, item, sorted))
}
}
// MoveAllListAttributeValues moves all values from list attribute oldAttr to newAttr,
// and deletes oldAttr.
func MoveAllListAttributeValues(rule *build.Rule, oldAttr, newAttr, pkg string, vars *map[string]*build.BinaryExpr) error {
if rule.Attr(oldAttr) == nil {
return fmt.Errorf("no attribute %s found in %s", oldAttr, rule.Name())
}
if rule.Attr(newAttr) == nil {
RenameAttribute(rule, oldAttr, newAttr)
return nil
}
if listExpr, ok := rule.Attr(oldAttr).(*build.ListExpr); ok {
for _, val := range listExpr.List {
AddValueToListAttribute(rule, newAttr, pkg, val, vars)
}
rule.DelAttr(oldAttr)
return nil
}
return fmt.Errorf("%s already exists and %s is not a simple list", newAttr, oldAttr)
}
// DictionarySet looks for the key in the dictionary expression. If value is not nil,
// it replaces the current value with it. In all cases, it returns the current value.
func DictionarySet(dict *build.DictExpr, key string, value build.Expr) build.Expr {
for _, e := range dict.List {
kv, _ := e.(*build.KeyValueExpr)
if k, ok := kv.Key.(*build.StringExpr); ok && k.Value == key {
if value != nil {
kv.Value = value
}
return kv.Value
}
}
if value != nil {
kv := &build.KeyValueExpr{Key: &build.StringExpr{Value: key}, Value: value}
dict.List = append(dict.List, kv)
}
return nil
}
// RenameAttribute renames an attribute in a rule.
func RenameAttribute(r *build.Rule, oldName, newName string) error {
if r.Attr(newName) != nil {
return fmt.Errorf("attribute %s already exists in rule %s", newName, r.Name())
}
for _, kv := range r.Call.List {
as, ok := kv.(*build.BinaryExpr)
if !ok || as.Op != "=" {
continue
}
k, ok := as.X.(*build.LiteralExpr)
if !ok || k.Token != oldName {
continue
}
k.Token = newName
return nil
}
return fmt.Errorf("no attribute %s found in rule %s", oldName, r.Name())
}
// EditFunction is a wrapper around build.Edit. The callback is called only on
// functions 'name'.
func EditFunction(v build.Expr, name string, f func(x *build.CallExpr, stk []build.Expr) build.Expr) build.Expr {
return build.Edit(v, func(expr build.Expr, stk []build.Expr) build.Expr {
call, ok := expr.(*build.CallExpr)
if !ok {
return nil
}
fct, ok := call.X.(*build.LiteralExpr)
if !ok || fct.Token != name {
return nil
}
return f(call, stk)
})
}
// UsedSymbols returns the set of symbols used in the BUILD file (variables, function names).
func UsedSymbols(f *build.File) map[string]bool {
symbols := make(map[string]bool)
build.Walk(f, func(expr build.Expr, stack []build.Expr) {
literal, ok := expr.(*build.LiteralExpr)
if !ok {
return
}
// Check if we are on the left-side of an assignment
for _, e := range stack {
if as, ok := e.(*build.BinaryExpr); ok {
if as.Op == "=" && as.X == expr {
return
}
}
}
symbols[literal.Token] = true
})
return symbols
}
func newLoad(args []string) *build.CallExpr {
load := &build.CallExpr{
X: &build.LiteralExpr{
Token: "load",
},
List: []build.Expr{},
ForceCompact: true,
}
for _, a := range args {
load.List = append(load.List, &build.StringExpr{Value: a})
}
return load
}
// appendLoad tries to find an existing load location and append symbols to it.
func appendLoad(stmts []build.Expr, args []string) bool {
if len(args) == 0 {
return false
}
location := args[0]
symbolsToLoad := make(map[string]bool)
for _, s := range args[1:] {
symbolsToLoad[s] = true
}
var lastLoad *build.CallExpr
for _, s := range stmts {
call, ok := s.(*build.CallExpr)
if !ok {
continue
}
if l, ok := call.X.(*build.LiteralExpr); !ok || l.Token != "load" {
continue
}
if len(call.List) < 2 {
continue
}
if s, ok := call.List[0].(*build.StringExpr); !ok || s.Value != location {
continue // Loads a different file.
}
for _, arg := range call.List[1:] {
if s, ok := arg.(*build.StringExpr); ok {
delete(symbolsToLoad, s.Value) // Already loaded.
}
}
// Remember the last insert location, but potentially remove more symbols
// that are already loaded in other subsequent calls.
lastLoad = call
}
if lastLoad == nil {
return false
}
// Append the remaining loads to the last load location.
sortedSymbols := []string{}
for s := range symbolsToLoad {
sortedSymbols = append(sortedSymbols, s)
}
sort.Strings(sortedSymbols)
for _, s := range sortedSymbols {
lastLoad.List = append(lastLoad.List, &build.StringExpr{Value: s})
}
return true
}
// InsertLoad inserts a load statement at the top of the list of statements.
// The load statement is constructed using args. Symbols that are already loaded
// from the given filepath are ignored. If stmts already contains a load for the
// location in arguments, appends the symbols to load to it.
func InsertLoad(stmts []build.Expr, args []string) []build.Expr {
if appendLoad(stmts, args) {
return stmts
}
load := newLoad(args)
var all []build.Expr
added := false
for _, stmt := range stmts {
_, isComment := stmt.(*build.CommentBlock)
if isComment || added {
all = append(all, stmt)
continue
}
all = append(all, load)
all = append(all, stmt)
added = true
}
if !added { // Empty file or just comments.
all = append(all, load)
}
return all
}