| /* 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. A File represents an |
| // individual build file. It comprises a list of Rules and a list of Loads. |
| // Rules and Loads may be inserted, modified, or deleted. When all changes |
| // are done, File.Save() may be called to write changes back to a file. |
| package rule |
| |
| import ( |
| "fmt" |
| "io/fs" |
| "os" |
| "path/filepath" |
| "sort" |
| "strings" |
| |
| bzl "github.com/bazelbuild/buildtools/build" |
| bt "github.com/bazelbuild/buildtools/tables" |
| ) |
| |
| // File provides editing functionality for a build file. You can create a |
| // new file with EmptyFile or load an existing file with LoadFile. After |
| // changes have been made, call Save to write changes back to a file. |
| 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 |
| |
| // function is the underlying syntax tree of a bzl file function. |
| // This is used for editing the bzl file function specified by the |
| // update-repos -to_macro option. |
| function *function |
| |
| // Pkg is the Bazel package this build file defines. |
| Pkg string |
| |
| // Path is the file system path to the build file (same as File.Path). |
| Path string |
| |
| // DefName is the name of the function definition this File refers to |
| // if loaded with LoadMacroFile or a similar function. Normally empty. |
| DefName 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 |
| |
| // Content is the file's underlying disk content, which is recorded when the |
| // file is initially loaded and whenever it is saved back to disk. If the file |
| // is modified outside of Rule methods, Content must be manually updated in |
| // order to keep it in sync. |
| Content []byte |
| } |
| |
| // EmptyFile creates a File wrapped around an empty syntax tree. |
| func EmptyFile(path, pkg string) *File { |
| return &File{ |
| File: &bzl.File{Path: path, Type: bzl.TypeBuild}, |
| Path: path, |
| Pkg: pkg, |
| } |
| } |
| |
| // 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, pkg string) (*File, error) { |
| data, err := os.ReadFile(path) |
| if err != nil { |
| return nil, err |
| } |
| return LoadData(path, pkg, data) |
| } |
| |
| // LoadWorkspaceFile is similar to LoadFile but parses the file as a WORKSPACE |
| // file. |
| func LoadWorkspaceFile(path, pkg string) (*File, error) { |
| data, err := os.ReadFile(path) |
| if err != nil { |
| return nil, err |
| } |
| return LoadWorkspaceData(path, pkg, data) |
| } |
| |
| // LoadMacroFile loads a bzl file from disk, parses it, then scans for the load |
| // statements and the rules called from the given Starlark function. If there is |
| // no matching function name, then a new function with that name will be created. |
| // The function's syntax tree will be returned within File and can be modified by |
| // Sync and Save calls. |
| func LoadMacroFile(path, pkg, defName string) (*File, error) { |
| data, err := os.ReadFile(path) |
| if err != nil { |
| return nil, err |
| } |
| return LoadMacroData(path, pkg, defName, data) |
| } |
| |
| // EmptyMacroFile creates a bzl file at the given path and within the file creates |
| // a Starlark function with the provided name. The function can then be modified |
| // by Sync and Save calls. |
| func EmptyMacroFile(path, pkg, defName string) (*File, error) { |
| _, err := os.Create(path) |
| if err != nil { |
| return nil, err |
| } |
| return LoadMacroData(path, pkg, defName, nil) |
| } |
| |
| // 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, pkg string, data []byte) (*File, error) { |
| ast, err := bzl.ParseBuild(path, data) |
| if err != nil { |
| return nil, err |
| } |
| f := ScanAST(pkg, ast) |
| if err := checkFile(f); err != nil { |
| return nil, err |
| } |
| f.Content = data |
| return f, nil |
| } |
| |
| // LoadWorkspaceData is similar to LoadData but parses the data as a |
| // WORKSPACE file. |
| func LoadWorkspaceData(path, pkg string, data []byte) (*File, error) { |
| ast, err := bzl.ParseWorkspace(path, data) |
| if err != nil { |
| return nil, err |
| } |
| f := ScanAST(pkg, ast) |
| if err := checkFile(f); err != nil { |
| return nil, err |
| } |
| f.Content = data |
| return f, nil |
| } |
| |
| // LoadMacroData parses a bzl file from a byte slice and scans for the load |
| // statements and the rules called from the given Starlark function. If there is |
| // no matching function name, then a new function will be created, and added to the |
| // File the next time Sync is called. The function's syntax tree will be returned |
| // within File and can be modified by Sync and Save calls. |
| func LoadMacroData(path, pkg, defName string, data []byte) (*File, error) { |
| ast, err := bzl.ParseBzl(path, data) |
| if err != nil { |
| return nil, err |
| } |
| f := ScanASTBody(pkg, defName, ast) |
| if err := checkFile(f); err != nil { |
| return nil, err |
| } |
| f.Content = data |
| return f, nil |
| } |
| |
| // ScanAST creates a File wrapped around the given syntax tree. This tree |
| // will be modified by editing methods. |
| func ScanAST(pkg string, bzlFile *bzl.File) *File { |
| return ScanASTBody(pkg, "", bzlFile) |
| } |
| |
| type function struct { |
| stmt *bzl.DefStmt |
| inserted, hasPass bool |
| } |
| |
| // ScanASTBody creates a File wrapped around the given syntax tree. It will also |
| // scan the AST for a function matching the given defName, and if the function |
| // does not exist it will create a new one and mark it to be added to the File |
| // the next time Sync is called. |
| func ScanASTBody(pkg, defName string, bzlFile *bzl.File) *File { |
| f := &File{ |
| File: bzlFile, |
| Pkg: pkg, |
| Path: bzlFile.Path, |
| DefName: defName, |
| } |
| var defStmt *bzl.DefStmt |
| f.Rules, f.Loads, defStmt = scanExprs(defName, bzlFile.Stmt) |
| if defStmt != nil { |
| f.Rules, _, _ = scanExprs("", defStmt.Body) |
| f.function = &function{ |
| stmt: defStmt, |
| inserted: true, |
| } |
| if len(defStmt.Body) == 1 { |
| if v, ok := defStmt.Body[0].(*bzl.BranchStmt); ok && v.Token == "pass" { |
| f.function.hasPass = true |
| } |
| } |
| } else if defName != "" { |
| f.function = &function{ |
| stmt: &bzl.DefStmt{Name: defName}, |
| inserted: false, |
| } |
| } |
| if f.function != nil { |
| f.Directives = ParseDirectivesFromMacro(f.function.stmt) |
| } else { |
| f.Directives = ParseDirectives(bzlFile) |
| } |
| return f |
| } |
| |
| func scanExprs(defName string, stmt []bzl.Expr) (rules []*Rule, loads []*Load, fn *bzl.DefStmt) { |
| for i, expr := range stmt { |
| switch expr := expr.(type) { |
| case *bzl.LoadStmt: |
| l := loadFromExpr(i, expr) |
| loads = append(loads, l) |
| case *bzl.CallExpr: |
| if r := ruleFromExpr(i, expr); r != nil { |
| rules = append(rules, r) |
| } |
| case *bzl.DefStmt: |
| if expr.Name == defName { |
| fn = expr |
| } |
| } |
| } |
| return rules, loads, fn |
| } |
| |
| // MatchBuildFile looks for a file in files that has a name from names. |
| // If there is at least one matching file, a path will be returned by joining |
| // dir and the first matching name. If there are no matching files, the |
| // empty string is returned. |
| func MatchBuildFile(dir string, names []string, ents []fs.DirEntry) string { |
| for _, name := range names { |
| for _, ent := range ents { |
| if ent.Name() == name && !ent.IsDir() { |
| return filepath.Join(dir, name) |
| } |
| } |
| } |
| return "" |
| } |
| |
| // Deprecated: Prefer MatchBuildFile, it's more efficient to fetch a []fs.DirEntry |
| func MatchBuildFileName(dir string, names []string, files []os.FileInfo) string { |
| for _, name := range names { |
| for _, fi := range files { |
| if fi.Name() == name && !fi.IsDir() { |
| return filepath.Join(dir, name) |
| } |
| } |
| } |
| return "" |
| } |
| |
| // SyncMacroFile syncs the file's syntax tree with another file's. This is |
| // useful for keeping multiple macro definitions from the same .bzl file in sync. |
| func (f *File) SyncMacroFile(from *File) { |
| fromFunc := *from.function.stmt |
| _, _, toFunc := scanExprs(from.function.stmt.Name, f.File.Stmt) |
| if toFunc != nil { |
| *toFunc = fromFunc |
| } else { |
| f.File.Stmt = append(f.File.Stmt, &fromFunc) |
| } |
| } |
| |
| // MacroName returns the name of the macro function that this file is editing, |
| // or an empty string if a macro function is not being edited. |
| func (f *File) MacroName() string { |
| if f.function != nil && f.function.stmt != nil { |
| return f.function.stmt.Name |
| } |
| return "" |
| } |
| |
| // 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 loadInserts, loadDeletes, loadStmts []*stmt |
| var r, w int |
| for r, w = 0, 0; r < len(f.Loads); r++ { |
| s := f.Loads[r] |
| s.sync() |
| if s.deleted { |
| loadDeletes = append(loadDeletes, &s.stmt) |
| continue |
| } |
| if s.inserted { |
| loadInserts = append(loadInserts, &s.stmt) |
| s.inserted = false |
| } else { |
| loadStmts = append(loadStmts, &s.stmt) |
| } |
| f.Loads[w] = s |
| w++ |
| } |
| f.Loads = f.Loads[:w] |
| var ruleInserts, ruleDeletes, ruleStmts []*stmt |
| for r, w = 0, 0; r < len(f.Rules); r++ { |
| s := f.Rules[r] |
| s.sync() |
| if s.deleted { |
| ruleDeletes = append(ruleDeletes, &s.stmt) |
| continue |
| } |
| if s.inserted { |
| ruleInserts = append(ruleInserts, &s.stmt) |
| s.inserted = false |
| } else { |
| ruleStmts = append(ruleStmts, &s.stmt) |
| } |
| f.Rules[w] = s |
| w++ |
| } |
| f.Rules = f.Rules[:w] |
| |
| if f.function == nil { |
| deletes := append(loadDeletes, ruleDeletes...) |
| inserts := append(loadInserts, ruleInserts...) |
| stmts := append(loadStmts, ruleStmts...) |
| updateStmt(&f.File.Stmt, inserts, deletes, stmts) |
| } else { |
| updateStmt(&f.File.Stmt, loadInserts, loadDeletes, loadStmts) |
| if f.function.hasPass && len(ruleInserts) > 0 { |
| f.function.stmt.Body = []bzl.Expr{} |
| f.function.hasPass = false |
| } |
| updateStmt(&f.function.stmt.Body, ruleInserts, ruleDeletes, ruleStmts) |
| if len(f.function.stmt.Body) == 0 { |
| f.function.stmt.Body = append(f.function.stmt.Body, &bzl.BranchStmt{Token: "pass"}) |
| f.function.hasPass = true |
| } |
| if !f.function.inserted { |
| f.File.Stmt = append(f.File.Stmt, f.function.stmt) |
| f.function.inserted = true |
| } |
| } |
| } |
| |
| func updateStmt(oldStmt *[]bzl.Expr, inserts, deletes, stmts []*stmt) { |
| sort.Stable(byIndex(deletes)) |
| sort.Stable(byIndex(inserts)) |
| sort.Stable(byIndex(stmts)) |
| cap := len(*oldStmt) - len(deletes) + len(inserts) |
| if cap < 0 { |
| cap = 0 |
| } |
| newStmt := make([]bzl.Expr, 0, cap) |
| var ii, di, si int |
| for i, stmt := range *oldStmt { |
| for ii < len(inserts) && inserts[ii].index == i { |
| inserts[ii].index = len(newStmt) |
| newStmt = append(newStmt, inserts[ii].expr) |
| ii++ |
| } |
| if di < len(deletes) && deletes[di].index == i { |
| di++ |
| continue |
| } |
| if si < len(stmts) && stmts[si].expr == stmt { |
| stmts[si].index = len(newStmt) |
| si++ |
| } |
| newStmt = append(newStmt, stmt) |
| } |
| for ii < len(inserts) { |
| inserts[ii].index = len(newStmt) |
| newStmt = append(newStmt, inserts[ii].expr) |
| ii++ |
| } |
| *oldStmt = newStmt |
| } |
| |
| // 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) |
| } |
| |
| // SortMacro sorts rules and loads in the macro of this File. It doesn't sort the rules if |
| // this File does not have a macro, e.g., WORKSPACE. |
| // This method calls Sync internally. |
| func (f *File) SortMacro() { |
| f.Sync() |
| |
| if f.function == nil { |
| panic(fmt.Sprintf("%s: not loaded as macro file", f.Path)) |
| } |
| |
| sort.Stable(loadsByName{f.Loads, f.File.Stmt}) |
| sort.Stable(rulesByKindAndName{f.Rules, f.function.stmt.Body}) |
| } |
| |
| // Save writes the build file to disk. This method calls Sync internally. |
| func (f *File) Save(path string) error { |
| f.Sync() |
| f.Content = bzl.Format(f.File) |
| return os.WriteFile(path, f.Content, 0o666) |
| } |
| |
| // HasDefaultVisibility returns whether the File contains a "package" rule with |
| // a "default_visibility" attribute. Rules generated by Gazelle should not |
| // have their own visibility attributes if this is the case. |
| func (f *File) HasDefaultVisibility() bool { |
| for _, r := range f.Rules { |
| if r.Kind() == "package" && r.Attr("default_visibility") != nil { |
| return true |
| } |
| } |
| return false |
| } |
| |
| type stmt struct { |
| index int |
| deleted, inserted, updated bool |
| comments []string |
| commentsUpdated bool |
| expr bzl.Expr |
| } |
| |
| // 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 } |
| |
| // Comments returns the text of the comments that appear before the statement. |
| // Each comment includes the leading "#". |
| func (s *stmt) Comments() []string { |
| return s.comments |
| } |
| |
| // AddComment adds a new comment above the statement, after other comments. |
| // The new comment must start with "#". |
| func (s *stmt) AddComment(token string) { |
| if !strings.HasPrefix(token, "#") { |
| panic(fmt.Sprintf("comment must start with '#': got %q", token)) |
| } |
| s.comments = append(s.comments, token) |
| s.commentsUpdated = true |
| } |
| |
| func commentsFromExpr(e bzl.Expr) []string { |
| before := e.Comment().Before |
| tokens := make([]string, len(before)) |
| for i, c := range before { |
| tokens[i] = c.Token |
| } |
| return tokens |
| } |
| |
| func (s *stmt) syncComments() { |
| if !s.commentsUpdated { |
| return |
| } |
| s.commentsUpdated = false |
| before := make([]bzl.Comment, len(s.comments)) |
| for i, token := range s.comments { |
| before[i].Token = token |
| } |
| s.expr.Comment().Before = before |
| } |
| |
| 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] |
| } |
| |
| type rulesByKindAndName struct { |
| rules []*Rule |
| exprs []bzl.Expr |
| } |
| |
| // type checking |
| var _ sort.Interface = (*rulesByKindAndName)(nil) |
| |
| func (s rulesByKindAndName) Len() int { |
| return len(s.rules) |
| } |
| |
| func (s rulesByKindAndName) Less(i, j int) bool { |
| if s.rules[i].Kind() == s.rules[j].Kind() { |
| return s.rules[i].Name() < s.rules[j].Name() |
| } |
| return s.rules[i].Kind() < s.rules[j].Kind() |
| } |
| |
| func (s rulesByKindAndName) Swap(i, j int) { |
| s.exprs[s.rules[i].index], s.exprs[s.rules[j].index] = s.exprs[s.rules[j].index], s.exprs[s.rules[i].index] |
| s.rules[i].index, s.rules[j].index = s.rules[j].index, s.rules[i].index |
| s.rules[i], s.rules[j] = s.rules[j], s.rules[i] |
| } |
| |
| type loadsByName struct { |
| loads []*Load |
| exprs []bzl.Expr |
| } |
| |
| // type checking |
| var _ sort.Interface = (*loadsByName)(nil) |
| |
| func (s loadsByName) Len() int { |
| return len(s.loads) |
| } |
| |
| func (s loadsByName) Less(i, j int) bool { |
| return s.loads[i].Name() < s.loads[j].Name() |
| } |
| |
| func (s loadsByName) Swap(i, j int) { |
| s.exprs[s.loads[i].index], s.exprs[s.loads[j].index] = s.exprs[s.loads[j].index], s.exprs[s.loads[i].index] |
| s.loads[i].index, s.loads[j].index = s.loads[j].index, s.loads[i].index |
| s.loads[i], s.loads[j] = s.loads[j], s.loads[i] |
| } |
| |
| // identPair represents one symbol, with or without remapping, in a load |
| // statement within a build file. |
| type identPair struct { |
| to, from *bzl.Ident |
| } |
| |
| // Load represents a load statement within a build file. |
| type Load struct { |
| stmt |
| name string |
| symbols map[string]identPair |
| } |
| |
| // NewLoad creates a new, empty load statement for the given file name. |
| func NewLoad(name string) *Load { |
| return &Load{ |
| stmt: stmt{ |
| expr: &bzl.LoadStmt{ |
| Module: &bzl.StringExpr{Value: name}, |
| ForceCompact: true, |
| }, |
| }, |
| name: name, |
| symbols: make(map[string]identPair), |
| } |
| } |
| |
| func loadFromExpr(index int, loadStmt *bzl.LoadStmt) *Load { |
| l := &Load{ |
| stmt: stmt{ |
| index: index, |
| expr: loadStmt, |
| comments: commentsFromExpr(loadStmt), |
| }, |
| name: loadStmt.Module.Value, |
| symbols: make(map[string]identPair), |
| } |
| for i := range loadStmt.From { |
| to, from := loadStmt.To[i], loadStmt.From[i] |
| l.symbols[to.Name] = identPair{to: to, from: from} |
| } |
| return l |
| } |
| |
| // Name returns the name of the file this statement loads. |
| func (l *Load) Name() string { |
| return l.name |
| } |
| |
| // Symbols returns a sorted list of symbols this statement loads. |
| // If the symbol is loaded with a name different from its definition, the |
| // loaded name is returned, not the original name. |
| 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 |
| } |
| |
| // SymbolPairs returns a list of symbol pairs loaded by this statement. |
| // Each pair contains the symbol defined in the loaded module (From) and the |
| // symbol declared in the loading module (To). The pairs are sorted by To |
| // (same order as Symbols). |
| func (l *Load) SymbolPairs() []struct{ From, To string } { |
| toSyms := l.Symbols() |
| pairs := make([]struct{ From, To string }, 0, len(toSyms)) |
| for _, toSym := range toSyms { |
| pairs = append(pairs, struct{ From, To string }{l.symbols[toSym].from.Name, toSym}) |
| } |
| return pairs |
| } |
| |
| // Has returns true if sym is loaded by this statement. |
| func (l *Load) Has(sym string) bool { |
| _, ok := l.symbols[sym] |
| return ok |
| } |
| |
| // Unalias returns the original (from) name of a (to) Symbol from a load. |
| func (l *Load) Unalias(sym string) string { |
| return l.symbols[sym].from.Name |
| } |
| |
| // 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 { |
| i := &bzl.Ident{Name: sym} |
| l.symbols[sym] = identPair{to: i, from: i} |
| l.updated = true |
| } |
| } |
| |
| // AddAlias inserts a new aliased 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) AddAlias(sym, to string) { |
| if _, ok := l.symbols[sym]; !ok { |
| l.symbols[sym] = identPair{ |
| to: &bzl.Ident{Name: to}, |
| from: &bzl.Ident{Name: 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() { |
| l.syncComments() |
| if !l.updated { |
| return |
| } |
| l.updated = false |
| |
| // args1 and args2 are two different sort groups based on whether a remap of the identifier is present. |
| var args1, args2, args []string |
| for sym, pair := range l.symbols { |
| if pair.from.Name == pair.to.Name { |
| args1 = append(args1, sym) |
| } else { |
| args2 = append(args2, sym) |
| } |
| } |
| sort.Strings(args1) |
| sort.Strings(args2) |
| args = append(args, args1...) |
| args = append(args, args2...) |
| |
| loadStmt := l.expr.(*bzl.LoadStmt) |
| loadStmt.Module.Value = l.name |
| loadStmt.From = make([]*bzl.Ident, 0, len(args)) |
| loadStmt.To = make([]*bzl.Ident, 0, len(args)) |
| for _, sym := range args { |
| pair := l.symbols[sym] |
| loadStmt.From = append(loadStmt.From, pair.from) |
| loadStmt.To = append(loadStmt.To, pair.to) |
| if pair.from.Name != pair.to.Name { |
| loadStmt.ForceCompact = false |
| } |
| } |
| } |
| |
| // Rule represents a rule statement within a build file. |
| type Rule struct { |
| stmt |
| kind string |
| args []bzl.Expr |
| attrs map[string]attrValue |
| private map[string]interface{} |
| sortedAttrs []string |
| } |
| |
| type attrValue struct { |
| // expr is the expression that defines the attribute assignment. If mergeable |
| // this will be replaced with a call to the merge function. |
| expr *bzl.AssignExpr |
| // val is the value of the attribute. If the attribute is mergeable |
| // the value must implement the Merger interface. could be nil. |
| val interface{} |
| } |
| |
| // NewRule creates a new, empty rule with the given kind and name. |
| func NewRule(kind, name string) *Rule { |
| kindIdent := createDotExpr(kind) |
| call := &bzl.CallExpr{X: kindIdent} |
| |
| r := &Rule{ |
| stmt: stmt{expr: call}, |
| kind: kind, |
| attrs: map[string]attrValue{}, |
| private: map[string]interface{}{}, |
| sortedAttrs: []string{"deps", "srcs"}, |
| } |
| if name != "" { |
| nameAttr := attrValue{ |
| expr: &bzl.AssignExpr{ |
| LHS: &bzl.Ident{Name: "name"}, |
| RHS: &bzl.StringExpr{Value: name}, |
| Op: "=", |
| }, |
| val: name} |
| call.List = []bzl.Expr{nameAttr.expr} |
| r.attrs["name"] = nameAttr |
| } |
| return r |
| } |
| |
| // Creates `bzl.Expr` for a kind which |
| // is either `*bzl.DotExpr` or `*bzl.Ident`. |
| // |
| // For `myKind` kind it returns: |
| // |
| // &bzl.Ident{ |
| // Name: "myKind" |
| // } |
| // |
| // For `myKind.inner` kind it returns: |
| // |
| // &bzl.DotExpr{ |
| // Name: "inner", |
| // X: &bzl.Ident { |
| // Name: "myKind" |
| // } |
| // } |
| func createDotExpr(kind string) bzl.Expr { |
| var expr bzl.Expr |
| parts := strings.Split(kind, ".") |
| |
| if len(parts) > 1 { |
| // last `parts` element is the main bzl.DotExpr |
| var dotExpr *bzl.DotExpr = &bzl.DotExpr{Name: parts[len(parts)-1]} |
| |
| _pDot := dotExpr |
| |
| for idx := len(parts) - 2; idx > 0; idx-- { |
| d := &bzl.DotExpr{Name: parts[idx]} |
| _pDot.X = d |
| _pDot = d |
| } |
| |
| // first `parts` element is the identifier |
| _pDot.X = &bzl.Ident{Name: parts[0]} |
| expr = dotExpr |
| } else { |
| expr = &bzl.Ident{Name: kind} |
| } |
| |
| return expr |
| } |
| |
| func isNestedDotOrIdent(expr bzl.Expr) bool { |
| if _, ok := expr.(*bzl.Ident); ok { |
| return true |
| } |
| |
| dot, ok := expr.(*bzl.DotExpr) |
| if !ok { |
| return false |
| } |
| |
| return isNestedDotOrIdent(dot.X) |
| } |
| |
| func ruleFromExpr(index int, expr bzl.Expr) *Rule { |
| call, ok := expr.(*bzl.CallExpr) |
| if !ok { |
| return nil |
| } |
| |
| kind := call.X |
| if !isNestedDotOrIdent(kind) { |
| return nil |
| } |
| |
| var args []bzl.Expr |
| attrs := make(map[string]attrValue, len(call.List)) |
| for _, arg := range call.List { |
| if attr, ok := arg.(*bzl.AssignExpr); ok { |
| key := attr.LHS.(*bzl.Ident) // required by parser |
| attrs[key.Name] = attrValue{expr: attr} |
| } else { |
| args = append(args, arg) |
| } |
| } |
| return &Rule{ |
| stmt: stmt{ |
| index: index, |
| expr: call, |
| comments: commentsFromExpr(expr), |
| }, |
| kind: bzl.FormatString(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.expr) |
| } |
| |
| // Kind returns the kind of rule this is (for example, "go_library"). |
| func (r *Rule) Kind() string { |
| return r.kind |
| } |
| |
| // SetKind changes the kind of rule this is. |
| func (r *Rule) SetKind(kind string) { |
| r.kind = kind |
| r.updated = true |
| } |
| |
| // Name returns the value of the rule's "name" attribute if it is a string |
| // or "" if the attribute does not exist or is not a string. |
| func (r *Rule) Name() string { |
| return r.AttrString("name") |
| } |
| |
| // SetName sets the value of the rule's "name" attribute. |
| 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.expr.RHS |
| } |
| |
| // 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.expr.RHS.(*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.expr.RHS.(*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 value. If the attribute is |
| // mergeable, then the value must implement the Merger interface, or an error will |
| // be returned. |
| func (r *Rule) SetAttr(key string, value interface{}) { |
| rhs := ExprFromValue(value) |
| if attr, ok := r.attrs[key]; ok { |
| attr.expr.RHS = rhs |
| attr.val = value |
| r.attrs[key] = attr |
| } else { |
| r.attrs[key] = attrValue{ |
| expr: &bzl.AssignExpr{ |
| LHS: &bzl.Ident{Name: key}, |
| RHS: rhs, |
| Op: "=", |
| }, |
| val: value, |
| } |
| } |
| r.updated = true |
| } |
| |
| // AttrComments returns the comments for an attribute. |
| // It can be used to attach comments like "do not sort". |
| func (r *Rule) AttrComments(key string) *bzl.Comments { |
| attr, ok := r.attrs[key] |
| if !ok { |
| return nil |
| } |
| return attr.expr.Comment() |
| } |
| |
| // 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 |
| } |
| |
| // AddArg adds a positional argument to the rule. |
| func (r *Rule) AddArg(value bzl.Expr) { |
| r.args = append(r.args, value) |
| r.updated = true |
| } |
| |
| // UpdateArg replaces an existing arg with a new value. |
| func (r *Rule) UpdateArg(index int, value bzl.Expr) error { |
| if len(r.args) < index { |
| return fmt.Errorf("can't update argument at index %d, only have %d args", index, len(r.args)) |
| } |
| r.args[index] = value |
| r.updated = true |
| return nil |
| } |
| |
| // SortedAttrs returns the keys of attributes whose values will be sorted |
| func (r *Rule) SortedAttrs() []string { |
| return r.sortedAttrs |
| } |
| |
| // SetSortedAttrs sets the keys of attributes whose values will be sorted |
| func (r *Rule) SetSortedAttrs(keys []string) { |
| r.sortedAttrs = keys |
| r.updated = true |
| } |
| |
| // 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) { |
| var stmt []bzl.Expr |
| if f.function == nil { |
| stmt = f.File.Stmt |
| } else { |
| stmt = f.function.stmt.Body |
| } |
| r.InsertAt(f, len(stmt)) |
| } |
| |
| // InsertAt marks this statement for insertion before the statement at index. |
| // Multiple rules inserted at the same index will be inserted in the order |
| // Insert is called. Loads inserted at the same index will be inserted first. |
| func (r *Rule) InsertAt(f *File, index int) { |
| r.index = index |
| 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) sync() { |
| r.syncComments() |
| if !r.updated { |
| return |
| } |
| r.updated = false |
| |
| for _, k := range r.sortedAttrs { |
| attr, ok := r.attrs[k] |
| _, isUnsorted := attr.val.(UnsortedStrings) |
| if ok && !isUnsorted { |
| bzl.Walk(attr.expr.RHS, sortExprLabels) |
| } |
| } |
| |
| call := r.expr.(*bzl.CallExpr) |
| |
| // update `call.X` (e.g.: "# gazelle:map_kind") |
| call.X = createDotExpr(r.Kind()) |
| |
| if len(r.attrs) > 1 { |
| call.ForceMultiLine = true |
| } |
| |
| 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.expr) |
| } |
| sortedAttrs := list[len(r.args):] |
| key := func(e bzl.Expr) string { return e.(*bzl.AssignExpr).LHS.(*bzl.Ident).Name } |
| 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 |
| }) |
| |
| 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" || strings.HasPrefix(text, "keep: ") { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // CheckInternalVisibility overrides the given visibility if the package is |
| // internal. |
| func CheckInternalVisibility(rel, visibility string) string { |
| if strings.HasSuffix(rel, "/internal") { |
| visibility = fmt.Sprintf("//%s:__subpackages__", rel[:len(rel)-len("/internal")]) |
| } else if i := strings.LastIndex(rel, "/internal/"); i >= 0 { |
| visibility = fmt.Sprintf("//%s:__subpackages__", rel[:i]) |
| } else if strings.HasPrefix(rel, "internal/") || rel == "internal" { |
| visibility = "//:__subpackages__" |
| } |
| return visibility |
| } |
| |
| type byAttrName []KeyValue |
| |
| var _ sort.Interface = (*byAttrName)(nil) |
| |
| 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] |
| } |
| |
| func checkFile(f *File) error { |
| names := make(map[string]bool) |
| for _, r := range f.Rules { |
| name := r.Name() |
| if name == "" { |
| continue |
| } |
| if names[name] { |
| return fmt.Errorf("%s: multiple rules have the name %q", f.Path, name) |
| } |
| names[name] = true |
| } |
| return nil |
| } |