blob: d5a6601921bbea450563bdbfc53134833126fc21 [file] [log] [blame] [edit]
/* Copyright 2018 The Bazel Authors. 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 rule provides tools for editing Bazel build files. It is intended to
// be a more powerful replacement for
// github.com/bazelbuild/buildtools/build.Rule, adapted for Gazelle's usage. It
// is language agnostic, but it may be used for language-specific rules by
// providing configuration.
//
// File is the primary interface to this package. Rule and Load are used to
// create, read, update, and delete rules. Once modifications are performed,
// File.Sync() may be called to write the changes back to the original AST,
// which may then be formatted and written back to a file.
package rule
import (
"io/ioutil"
"log"
"path/filepath"
"sort"
"strings"
bzl "github.com/bazelbuild/buildtools/build"
bt "github.com/bazelbuild/buildtools/tables"
)
// File provides editing functionality on top of a Skylark syntax tree. This
// is the primary interface Gazelle uses for reading and updating build files.
// To use, create a new file with EmptyFile or wrap a syntax tree with
// LoadFile. Perform edits on Loads and Rules, then call Sync() to write
// changes back to the AST.
type File struct {
// File is the underlying build file syntax tree. Some editing operations
// may modify this, but editing is not complete until Sync() is called.
File *bzl.File
// Path is the file system path to the build file (same as File.Path).
Path string
// Directives is a list of configuration directives found in top-level
// comments in the file. This should not be modified after the file is read.
Directives []Directive
// Loads is a list of load statements within the file. This should not
// be modified directly; use Load methods instead.
Loads []*Load
// Rules is a list of rules within the file (or function calls that look like
// rules). This should not be modified directly; use Rule methods instead.
Rules []*Rule
}
// EmptyFile creates a File wrapped around an empty syntax tree.
func EmptyFile(path string) *File {
return &File{
File: &bzl.File{Path: path},
Path: path,
}
}
// LoadFile loads a build file from disk, parses it, and scans for rules and
// load statements. The syntax tree within the returned File will be modified
// by editing methods.
//
// This function returns I/O and parse errors without modification. It's safe
// to use os.IsNotExist and similar predicates.
func LoadFile(path string) (*File, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
return LoadData(path, data)
}
// LoadData parses a build file from a byte slice and scans it for rules and
// load statements. The syntax tree within the returned File will be modified
// by editing methods.
func LoadData(path string, data []byte) (*File, error) {
ast, err := bzl.Parse(path, data)
if err != nil {
return nil, err
}
return ScanAST(ast), nil
}
// ScanAST creates a File wrapped around the given syntax tree. This tree
// will be modified by editing methods.
func ScanAST(bzlFile *bzl.File) *File {
f := &File{
File: bzlFile,
Path: bzlFile.Path,
}
for i, stmt := range f.File.Stmt {
call, ok := stmt.(*bzl.CallExpr)
if !ok {
continue
}
x, ok := call.X.(*bzl.LiteralExpr)
if !ok {
continue
}
if x.Token == "load" {
if l := loadFromExpr(i, call); l != nil {
f.Loads = append(f.Loads, l)
}
} else {
if r := ruleFromExpr(i, call); r != nil {
f.Rules = append(f.Rules, r)
}
}
}
f.Directives = ParseDirectives(bzlFile)
return f
}
// Rel returns the slash-separated relative path from the given absolute path to
// the directory containing this file. If the file is in the root directory, Rel
// returns "". This string may be used as a Bazel package name.
func (f *File) Rel(root string) string {
rel, err := filepath.Rel(root, filepath.Dir(f.Path))
if err != nil {
log.Panicf("%s is not a parent of %s", root, f.Path)
}
if rel == "." {
rel = ""
}
return filepath.ToSlash(rel)
}
// Sync writes all changes back to the wrapped syntax tree. This should be
// called after editing operations, before reading the syntax tree again.
func (f *File) Sync() {
var inserts, deletes, stmts []*stmt
var r, w int
for r, w = 0, 0; r < len(f.Loads); r++ {
s := f.Loads[r]
s.sync()
if s.deleted {
deletes = append(deletes, &s.stmt)
continue
}
if s.inserted {
inserts = append(inserts, &s.stmt)
s.inserted = false
} else {
stmts = append(stmts, &s.stmt)
}
f.Loads[w] = s
w++
}
f.Loads = f.Loads[:w]
for r, w = 0, 0; r < len(f.Rules); r++ {
s := f.Rules[r]
s.sync()
if s.deleted {
deletes = append(deletes, &s.stmt)
continue
}
if s.inserted {
inserts = append(inserts, &s.stmt)
s.inserted = false
} else {
stmts = append(stmts, &s.stmt)
}
f.Rules[w] = s
w++
}
f.Rules = f.Rules[:w]
sort.Stable(byIndex(deletes))
sort.Stable(byIndex(inserts))
sort.Stable(byIndex(stmts))
oldStmt := f.File.Stmt
f.File.Stmt = make([]bzl.Expr, 0, len(oldStmt)-len(deletes)+len(inserts))
var ii, di, si int
for i, stmt := range oldStmt {
for ii < len(inserts) && inserts[ii].index == i {
inserts[ii].index = len(f.File.Stmt)
f.File.Stmt = append(f.File.Stmt, inserts[ii].call)
ii++
}
if di < len(deletes) && deletes[di].index == i {
di++
continue
}
if si < len(stmts) && stmts[si].call == stmt {
stmts[si].index = len(f.File.Stmt)
si++
}
f.File.Stmt = append(f.File.Stmt, stmt)
}
for ii < len(inserts) {
inserts[ii].index = len(f.File.Stmt)
f.File.Stmt = append(f.File.Stmt, inserts[ii].call)
ii++
}
}
// Format formats the build file in a form that can be written to disk.
// This method calls Sync internally.
func (f *File) Format() []byte {
f.Sync()
return bzl.Format(f.File)
}
// Save writes the build file to disk at the same path it was loaded from.
// This method calls Sync internally.
func (f *File) Save() error {
f.Sync()
data := bzl.Format(f.File)
return ioutil.WriteFile(f.Path, data, 0666)
}
type stmt struct {
index int
deleted, inserted, updated bool
call *bzl.CallExpr
}
// Index returns the index for this statement within the build file. For
// inserted rules, this is where the rule will be inserted (rules with the
// same index will be inserted in the order Insert was called). For existing
// rules, this is the index of the original statement.
func (s *stmt) Index() int { return s.index }
// Delete marks this statement for deletion. It will be removed from the
// syntax tree when File.Sync is called.
func (s *stmt) Delete() { s.deleted = true }
type byIndex []*stmt
func (s byIndex) Len() int {
return len(s)
}
func (s byIndex) Less(i, j int) bool {
return s[i].index < s[j].index
}
func (s byIndex) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// Load represents a load statement within a build file.
type Load struct {
stmt
name string
symbols map[string]bzl.Expr
}
// NewLoad creates a new, empty load statement for the given file name.
func NewLoad(name string) *Load {
return &Load{
stmt: stmt{
call: &bzl.CallExpr{
X: &bzl.LiteralExpr{Token: "load"},
List: []bzl.Expr{&bzl.StringExpr{Value: name}},
ForceCompact: true,
},
},
name: name,
symbols: make(map[string]bzl.Expr),
}
}
func loadFromExpr(index int, call *bzl.CallExpr) *Load {
l := &Load{
stmt: stmt{index: index, call: call},
symbols: make(map[string]bzl.Expr),
}
if len(call.List) == 0 {
return nil
}
name, ok := call.List[0].(*bzl.StringExpr)
if !ok {
return nil
}
l.name = name.Value
for _, arg := range call.List[1:] {
switch arg := arg.(type) {
case *bzl.StringExpr:
l.symbols[arg.Value] = arg
case *bzl.BinaryExpr:
x, ok := arg.X.(*bzl.LiteralExpr)
if !ok {
return nil
}
if _, ok := arg.Y.(*bzl.StringExpr); !ok {
return nil
}
l.symbols[x.Token] = arg
default:
return nil
}
}
return l
}
// Name returns the name of the file this statement loads.
func (l *Load) Name() string {
return l.name
}
// Symbols returns a list of symbols this statement loads.
func (l *Load) Symbols() []string {
syms := make([]string, 0, len(l.symbols))
for sym := range l.symbols {
syms = append(syms, sym)
}
sort.Strings(syms)
return syms
}
// Has returns true if sym is loaded by this statement.
func (l *Load) Has(sym string) bool {
_, ok := l.symbols[sym]
return ok
}
// Add inserts a new symbol into the load statement. This has no effect if
// the symbol is already loaded. Symbols will be sorted, so the order
// doesn't matter.
func (l *Load) Add(sym string) {
if _, ok := l.symbols[sym]; !ok {
l.symbols[sym] = &bzl.StringExpr{Value: sym}
l.updated = true
}
}
// Remove deletes a symbol from the load statement. This has no effect if
// the symbol is not loaded.
func (l *Load) Remove(sym string) {
if _, ok := l.symbols[sym]; ok {
delete(l.symbols, sym)
l.updated = true
}
}
// IsEmpty returns whether this statement loads any symbols.
func (l *Load) IsEmpty() bool {
return len(l.symbols) == 0
}
// Insert marks this statement for insertion at the given index. If multiple
// statements are inserted at the same index, they will be inserted in the
// order Insert is called.
func (l *Load) Insert(f *File, index int) {
l.index = index
l.inserted = true
f.Loads = append(f.Loads, l)
}
func (l *Load) sync() {
if !l.updated {
return
}
l.updated = false
args := make([]*bzl.StringExpr, 0, len(l.symbols))
kwargs := make([]*bzl.BinaryExpr, 0, len(l.symbols))
for _, e := range l.symbols {
if a, ok := e.(*bzl.StringExpr); ok {
args = append(args, a)
} else {
kwargs = append(kwargs, e.(*bzl.BinaryExpr))
}
}
sort.Slice(args, func(i, j int) bool {
return args[i].Value < args[j].Value
})
sort.Slice(kwargs, func(i, j int) bool {
return kwargs[i].X.(*bzl.StringExpr).Value < kwargs[j].Y.(*bzl.StringExpr).Value
})
list := make([]bzl.Expr, 0, 1+len(l.symbols))
list = append(list, l.call.List[0])
for _, a := range args {
list = append(list, a)
}
for _, a := range kwargs {
list = append(list, a)
}
l.call.List = list
l.call.ForceCompact = len(kwargs) == 0
}
// Rule represents a rule statement within a build file.
type Rule struct {
stmt
kind string
args []bzl.Expr
attrs map[string]*bzl.BinaryExpr
private map[string]interface{}
}
// NewRule creates a new, empty rule with the given kind and name.
func NewRule(kind, name string) *Rule {
nameAttr := &bzl.BinaryExpr{
X: &bzl.LiteralExpr{Token: "name"},
Y: &bzl.StringExpr{Value: name},
Op: "=",
}
r := &Rule{
stmt: stmt{
call: &bzl.CallExpr{
X: &bzl.LiteralExpr{Token: kind},
List: []bzl.Expr{nameAttr},
},
},
kind: kind,
attrs: map[string]*bzl.BinaryExpr{"name": nameAttr},
private: map[string]interface{}{},
}
return r
}
func ruleFromExpr(index int, expr bzl.Expr) *Rule {
call, ok := expr.(*bzl.CallExpr)
if !ok {
return nil
}
x, ok := call.X.(*bzl.LiteralExpr)
if !ok {
return nil
}
kind := x.Token
var args []bzl.Expr
attrs := make(map[string]*bzl.BinaryExpr)
for _, arg := range call.List {
attr, ok := arg.(*bzl.BinaryExpr)
if ok && attr.Op == "=" {
key := attr.X.(*bzl.LiteralExpr) // required by parser
attrs[key.Token] = attr
} else {
args = append(args, arg)
}
}
return &Rule{
stmt: stmt{
index: index,
call: call,
},
kind: kind,
args: args,
attrs: attrs,
private: map[string]interface{}{},
}
}
// ShouldKeep returns whether the rule is marked with a "# keep" comment. Rules
// that are kept should not be modified. This does not check whether
// subexpressions within the rule should be kept.
func (r *Rule) ShouldKeep() bool {
return ShouldKeep(r.call)
}
func (r *Rule) Kind() string {
return r.kind
}
func (r *Rule) SetKind(kind string) {
r.kind = kind
r.updated = true
}
func (r *Rule) Name() string {
return r.AttrString("name")
}
func (r *Rule) SetName(name string) {
r.SetAttr("name", name)
}
// AttrKeys returns a sorted list of attribute keys used in this rule.
func (r *Rule) AttrKeys() []string {
keys := make([]string, 0, len(r.attrs))
for k := range r.attrs {
keys = append(keys, k)
}
sort.SliceStable(keys, func(i, j int) bool {
if cmp := bt.NamePriority[keys[i]] - bt.NamePriority[keys[j]]; cmp != 0 {
return cmp < 0
}
return keys[i] < keys[j]
})
return keys
}
// Attr returns the value of the named attribute. nil is returned when the
// attribute is not set.
func (r *Rule) Attr(key string) bzl.Expr {
attr, ok := r.attrs[key]
if !ok {
return nil
}
return attr.Y
}
// AttrString returns the value of the named attribute if it is a scalar string.
// "" is returned if the attribute is not set or is not a string.
func (r *Rule) AttrString(key string) string {
attr, ok := r.attrs[key]
if !ok {
return ""
}
str, ok := attr.Y.(*bzl.StringExpr)
if !ok {
return ""
}
return str.Value
}
// AttrStrings returns the string values of an attribute if it is a list.
// nil is returned if the attribute is not set or is not a list. Non-string
// values within the list won't be returned.
func (r *Rule) AttrStrings(key string) []string {
attr, ok := r.attrs[key]
if !ok {
return nil
}
list, ok := attr.Y.(*bzl.ListExpr)
if !ok {
return nil
}
strs := make([]string, 0, len(list.List))
for _, e := range list.List {
if str, ok := e.(*bzl.StringExpr); ok {
strs = append(strs, str.Value)
}
}
return strs
}
// DelAttr removes the named attribute from the rule.
func (r *Rule) DelAttr(key string) {
delete(r.attrs, key)
r.updated = true
}
// SetAttr adds or replaces the named attribute with an expression produced
// by ExprFromValue.
func (r *Rule) SetAttr(key string, value interface{}) {
y := ExprFromValue(value)
if attr, ok := r.attrs[key]; ok {
attr.Y = y
} else {
r.attrs[key] = &bzl.BinaryExpr{
X: &bzl.LiteralExpr{Token: key},
Y: y,
Op: "=",
}
}
r.updated = true
}
// PrivateAttrKeys returns a sorted list of private attribute names.
func (r *Rule) PrivateAttrKeys() []string {
keys := make([]string, 0, len(r.private))
for k := range r.private {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
// PrivateAttr return the private value associated with a key.
func (r *Rule) PrivateAttr(key string) interface{} {
return r.private[key]
}
// SetPrivateAttr associates a value with a key. Unlike SetAttr, this value
// is not converted to a build syntax tree and will not be written to a build
// file.
func (r *Rule) SetPrivateAttr(key string, value interface{}) {
r.private[key] = value
}
// Args returns positional arguments passed to a rule.
func (r *Rule) Args() []bzl.Expr {
return r.args
}
// Insert marks this statement for insertion at the end of the file. Multiple
// statements will be inserted in the order Insert is called.
func (r *Rule) Insert(f *File) {
// TODO(jayconrod): should rules always be inserted at the end? Should there
// be some sort order?
r.index = len(f.File.Stmt)
r.inserted = true
f.Rules = append(f.Rules, r)
}
// IsEmpty returns true when the rule contains none of the attributes in attrs
// for its kind. attrs should contain attributes that make the rule buildable
// like srcs or deps and not descriptive attributes like name or visibility.
func (r *Rule) IsEmpty(info KindInfo) bool {
if info.NonEmptyAttrs == nil {
return false
}
for k := range info.NonEmptyAttrs {
if _, ok := r.attrs[k]; ok {
return false
}
}
return true
}
func (r *Rule) IsEmptyOld(attrs map[string]bool) bool {
if attrs == nil {
return false
}
for k := range attrs {
if _, ok := r.attrs[k]; ok {
return false
}
}
return true
}
func (r *Rule) sync() {
if !r.updated {
return
}
r.updated = false
for _, k := range []string{"srcs", "deps"} {
if attr, ok := r.attrs[k]; ok {
bzl.Walk(attr.Y, sortExprLabels)
}
}
call := r.call
call.X.(*bzl.LiteralExpr).Token = r.kind
list := make([]bzl.Expr, 0, len(r.args)+len(r.attrs))
list = append(list, r.args...)
for _, attr := range r.attrs {
list = append(list, attr)
}
sortedAttrs := list[len(r.args):]
key := func(e bzl.Expr) string { return e.(*bzl.BinaryExpr).X.(*bzl.LiteralExpr).Token }
sort.SliceStable(sortedAttrs, func(i, j int) bool {
ki := key(sortedAttrs[i])
kj := key(sortedAttrs[j])
if cmp := bt.NamePriority[ki] - bt.NamePriority[kj]; cmp != 0 {
return cmp < 0
}
return ki < kj
})
r.call.List = list
r.updated = false
}
// ShouldKeep returns whether e is marked with a "# keep" comment. Kept
// expressions should not be removed or modified.
func ShouldKeep(e bzl.Expr) bool {
for _, c := range append(e.Comment().Before, e.Comment().Suffix...) {
text := strings.TrimSpace(strings.TrimPrefix(c.Token, "#"))
if text == "keep" {
return true
}
}
return false
}
type byAttrName []KeyValue
var _ sort.Interface = byAttrName{}
func (s byAttrName) Len() int {
return len(s)
}
func (s byAttrName) Less(i, j int) bool {
if cmp := bt.NamePriority[s[i].Key] - bt.NamePriority[s[j].Key]; cmp != 0 {
return cmp < 0
}
return s[i].Key < s[j].Key
}
func (s byAttrName) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}