| /* |
| Copyright 2016 Google LLC |
| |
| 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 |
| |
| https://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. |
| */ |
| |
| // Rewriting of high-level (not purely syntactic) BUILD constructs. |
| |
| package build |
| |
| import ( |
| "path" |
| "path/filepath" |
| "regexp" |
| "sort" |
| "strings" |
| |
| "github.com/bazelbuild/buildtools/labels" |
| "github.com/bazelbuild/buildtools/tables" |
| ) |
| |
| // DisableRewrites disables certain rewrites (for debugging). |
| var DisableRewrites []string |
| |
| // disabled reports whether the named rewrite is disabled. |
| func disabled(name string) bool { |
| for _, x := range DisableRewrites { |
| if name == x { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // AllowSort allows sorting of these lists even with sorting otherwise disabled (for debugging). |
| var AllowSort []string |
| |
| // allowedSort reports whether sorting is allowed in the named context. |
| func allowedSort(name string) bool { |
| for _, x := range AllowSort { |
| if name == x { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // Rewriter controls the rewrites to be applied. |
| // |
| // If non-nil, the rewrites with the specified names will be run. If |
| // nil, a default set of rewrites will be used that is determined by |
| // the type (BUILD vs default starlark) of the file being rewritten. |
| type Rewriter struct { |
| RewriteSet []string |
| IsLabelArg map[string]bool |
| LabelDenyList map[string]bool |
| IsSortableListArg map[string]bool |
| SortableDenylist map[string]bool |
| SortableAllowlist map[string]bool |
| NamePriority map[string]int |
| StripLabelLeadingSlashes bool |
| ShortenAbsoluteLabelsToRelative bool |
| } |
| |
| // Rewrite applies rewrites to a file |
| func Rewrite(f *File) { |
| var rewriter = &Rewriter{ |
| IsLabelArg: tables.IsLabelArg, |
| LabelDenyList: tables.LabelDenylist, |
| IsSortableListArg: tables.IsSortableListArg, |
| SortableDenylist: tables.SortableDenylist, |
| SortableAllowlist: tables.SortableAllowlist, |
| NamePriority: tables.NamePriority, |
| StripLabelLeadingSlashes: tables.StripLabelLeadingSlashes, |
| ShortenAbsoluteLabelsToRelative: tables.ShortenAbsoluteLabelsToRelative, |
| } |
| rewriter.Rewrite(f) |
| } |
| |
| // Rewrite applies the rewrites to a file |
| func (w *Rewriter) Rewrite(f *File) { |
| for _, r := range rewrites { |
| // f.Type&r.scope is a bitwise comparison. Because starlark files result in a scope that will |
| // not be changed by rewrites, we have included another check looking on the right side. |
| // If we have an empty rewrite set, we do not want any rewrites to happen. |
| if (!disabled(r.name) && (f.Type&r.scope != 0) && w.RewriteSet == nil) || (w.RewriteSet != nil && rewriteSetContains(w, r.name)) { |
| r.fn(f, w) |
| } |
| } |
| } |
| |
| func rewriteSetContains(w *Rewriter, name string) bool { |
| for _, value := range w.RewriteSet { |
| if value == name { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // Each rewrite function can be either applied for BUILD files, other files (such as .bzl), |
| // or all files. |
| const ( |
| scopeDefault = TypeDefault | TypeBzl // .bzl and generic Starlark files |
| scopeBuild = TypeBuild | TypeWorkspace | TypeModule // BUILD, WORKSPACE, and MODULE files |
| scopeBoth = scopeDefault | scopeBuild |
| ) |
| |
| // rewrites is the list of all Buildifier rewrites, in the order in which they are applied. |
| // The order here matters: for example, label canonicalization must happen |
| // before sorting lists of strings. |
| var rewrites = []struct { |
| name string |
| fn func(*File, *Rewriter) |
| scope FileType |
| }{ |
| {"removeParens", removeParens, scopeBuild}, |
| {"callsort", sortCallArgs, scopeBuild}, |
| {"label", fixLabels, scopeBuild}, |
| {"listsort", sortStringLists, scopeBoth}, |
| {"multiplus", fixMultilinePlus, scopeBuild}, |
| {"loadTop", moveLoadOnTop, scopeBoth}, |
| {"sameOriginLoad", compressSameOriginLoads, scopeBoth}, |
| {"sortLoadStatements", sortLoadStatements, scopeBoth}, |
| {"loadsort", sortAllLoadArgs, scopeBoth}, |
| {"useRepoPositionalsSort", sortUseRepoPositionals, TypeModule}, |
| {"formatdocstrings", formatDocstrings, scopeBoth}, |
| {"reorderarguments", reorderArguments, scopeBoth}, |
| {"editoctal", editOctals, scopeBoth}, |
| {"editfloat", editFloats, scopeBoth}, |
| {"collapseEmpty", collapseEmpty, scopeBoth}, |
| } |
| |
| // leaveAlone reports whether any of the nodes on the stack are marked |
| // with a comment containing "buildifier: leave-alone". |
| func leaveAlone(stk []Expr, final Expr) bool { |
| for _, x := range stk { |
| if leaveAlone1(x) { |
| return true |
| } |
| } |
| if final != nil && leaveAlone1(final) { |
| return true |
| } |
| return false |
| } |
| |
| // hasComment reports whether x is marked with a comment that |
| // after being converted to lower case, contains the specified text. |
| func hasComment(x Expr, text string) bool { |
| if x == nil { |
| return false |
| } |
| for _, com := range x.Comment().Before { |
| if strings.Contains(strings.ToLower(com.Token), text) { |
| return true |
| } |
| } |
| for _, com := range x.Comment().After { |
| if strings.Contains(strings.ToLower(com.Token), text) { |
| return true |
| } |
| } |
| for _, com := range x.Comment().Suffix { |
| if strings.Contains(strings.ToLower(com.Token), text) { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // isCommentAnywhere checks whether there's a comment containing the given text |
| // anywhere in the file. |
| func isCommentAnywhere(f *File, text string) bool { |
| commentExists := false |
| WalkInterruptable(f, func(node Expr, stack []Expr) (err error) { |
| if hasComment(node, text) { |
| commentExists = true |
| return &StopTraversalError{} |
| } |
| return nil |
| }) |
| return commentExists |
| } |
| |
| // leaveAlone1 reports whether x is marked with a comment containing |
| // "buildifier: leave-alone", case-insensitive. |
| func leaveAlone1(x Expr) bool { |
| return hasComment(x, "buildifier: leave-alone") |
| } |
| |
| // doNotSort reports whether x is marked with a comment containing |
| // "do not sort", case-insensitive. |
| func doNotSort(x Expr) bool { |
| return hasComment(x, "do not sort") |
| } |
| |
| // keepSorted reports whether x is marked with a comment containing |
| // "keep sorted", case-insensitive. |
| func keepSorted(x Expr) bool { |
| return hasComment(x, "keep sorted") |
| } |
| |
| // fixLabels rewrites labels into a canonical form. |
| // |
| // First, it joins labels written as string addition, turning |
| // "//x" + ":y" (usually split across multiple lines) into "//x:y". |
| // |
| // Second, it removes redundant target qualifiers, turning labels like |
| // "//third_party/m4:m4" into "//third_party/m4" as well as ones like |
| // "@foo//:foo" into "@foo". |
| func fixLabels(f *File, w *Rewriter) { |
| joinLabel := func(p *Expr) { |
| add, ok := (*p).(*BinaryExpr) |
| if !ok || add.Op != "+" { |
| return |
| } |
| str1, ok := add.X.(*StringExpr) |
| if !ok || !strings.HasPrefix(str1.Value, "//") || strings.Contains(str1.Value, " ") { |
| return |
| } |
| str2, ok := add.Y.(*StringExpr) |
| if !ok || strings.Contains(str2.Value, " ") { |
| return |
| } |
| str1.Value += str2.Value |
| |
| // Deleting nodes add and str2. |
| // Merge comments from add, str1, and str2 and save in str1. |
| com1 := add.Comment() |
| com2 := str1.Comment() |
| com3 := str2.Comment() |
| com1.Before = append(com1.Before, com2.Before...) |
| com1.Before = append(com1.Before, com3.Before...) |
| com1.Suffix = append(com1.Suffix, com2.Suffix...) |
| com1.Suffix = append(com1.Suffix, com3.Suffix...) |
| *str1.Comment() = *com1 |
| |
| *p = str1 |
| } |
| |
| labelPrefix := "//" |
| if w.StripLabelLeadingSlashes { |
| labelPrefix = "" |
| } |
| // labelRE matches label strings, e.g. @r//x/y/z:abc |
| // where $1 is @r//x/y/z, $2 is @r//, $3 is r, $4 is z, $5 is abc. |
| labelRE := regexp.MustCompile(`^(((?:@(\w+))?//|` + labelPrefix + `)(?:.+/)?([^:]*))(?::([^:]+))?$`) |
| |
| shortenLabel := func(v Expr) { |
| str, ok := v.(*StringExpr) |
| if !ok { |
| return |
| } |
| |
| if w.StripLabelLeadingSlashes && strings.HasPrefix(str.Value, "//") { |
| if filepath.Dir(f.Path) == "." || !strings.HasPrefix(str.Value, "//:") { |
| str.Value = str.Value[2:] |
| } |
| } |
| if w.ShortenAbsoluteLabelsToRelative { |
| thisPackage := labelPrefix + filepath.Dir(f.Path) |
| // filepath.Dir on Windows uses backslashes as separators, while labels always have slashes. |
| if filepath.Separator != '/' { |
| thisPackage = strings.Replace(thisPackage, string(filepath.Separator), "/", -1) |
| } |
| |
| if str.Value == thisPackage { |
| str.Value = ":" + path.Base(str.Value) |
| } else if strings.HasPrefix(str.Value, thisPackage+":") { |
| str.Value = str.Value[len(thisPackage):] |
| } |
| } |
| |
| m := labelRE.FindStringSubmatch(str.Value) |
| if m == nil { |
| return |
| } |
| if m[4] != "" && m[4] == m[5] { // e.g. //foo:foo |
| str.Value = m[1] |
| } else if m[3] != "" && m[4] == "" && m[3] == m[5] { // e.g. @foo//:foo |
| str.Value = "@" + m[3] |
| } |
| } |
| |
| // Join and shorten labels within a container of labels (which can be a single |
| // label, e.g. a single string expression or a concatenation of them). |
| // Gracefully finish if the argument is of a different type. |
| fixLabelsWithinAContainer := func(e *Expr) { |
| if list, ok := (*e).(*ListExpr); ok { |
| for i := range list.List { |
| if leaveAlone1(list.List[i]) { |
| continue |
| } |
| joinLabel(&list.List[i]) |
| shortenLabel(list.List[i]) |
| } |
| } |
| if set, ok := (*e).(*SetExpr); ok { |
| for i := range set.List { |
| if leaveAlone1(set.List[i]) { |
| continue |
| } |
| joinLabel(&set.List[i]) |
| shortenLabel(set.List[i]) |
| } |
| } else { |
| joinLabel(e) |
| shortenLabel(*e) |
| } |
| } |
| |
| Walk(f, func(v Expr, stk []Expr) { |
| switch v := v.(type) { |
| case *CallExpr: |
| if leaveAlone(stk, v) { |
| return |
| } |
| for i := range v.List { |
| if leaveAlone1(v.List[i]) { |
| continue |
| } |
| as, ok := v.List[i].(*AssignExpr) |
| if !ok { |
| continue |
| } |
| key, ok := as.LHS.(*Ident) |
| if !ok || !w.IsLabelArg[key.Name] || w.LabelDenyList[callName(v)+"."+key.Name] { |
| continue |
| } |
| if leaveAlone1(as.RHS) { |
| continue |
| } |
| |
| findAndModifyStrings(&as.RHS, fixLabelsWithinAContainer) |
| } |
| } |
| }) |
| } |
| |
| // callName returns the name of the rule being called by call. |
| // If the call is not to a literal rule name or a dot expression, callName |
| // returns "". |
| func callName(call *CallExpr) string { |
| return (&Rule{call, ""}).Kind() |
| } |
| |
| // sortCallArgs sorts lists of named arguments to a call. |
| func sortCallArgs(f *File, w *Rewriter) { |
| |
| Walk(f, func(v Expr, stk []Expr) { |
| call, ok := v.(*CallExpr) |
| if !ok { |
| return |
| } |
| if leaveAlone(stk, call) { |
| return |
| } |
| rule := callName(call) |
| if rule == "" { |
| rule = "<complex rule kind>" |
| } |
| |
| // Find the tail of the argument list with named arguments. |
| start := len(call.List) |
| for start > 0 && argName(call.List[start-1]) != "" { |
| start-- |
| } |
| |
| // Record information about each arg into a sortable list. |
| var args namedArgs |
| for i, x := range call.List[start:] { |
| name := argName(x) |
| args = append(args, namedArg{ruleNamePriority(w, rule, name), name, i, x}) |
| } |
| |
| // Sort the list and put the args back in the new order. |
| if sort.IsSorted(args) { |
| return |
| } |
| sort.Sort(args) |
| for i, x := range args { |
| call.List[start+i] = x.expr |
| } |
| }) |
| } |
| |
| // ruleNamePriority maps a rule argument name to its sorting priority. |
| // It could use the auto-generated per-rule tables but for now it just |
| // falls back to the original list. |
| func ruleNamePriority(w *Rewriter, rule, arg string) int { |
| ruleArg := rule + "." + arg |
| if val, ok := w.NamePriority[ruleArg]; ok { |
| return val |
| } |
| return w.NamePriority[arg] |
| |
| /* |
| list := ruleArgOrder[rule] |
| if len(list) == 0 { |
| return tables.NamePriority[arg] |
| } |
| for i, x := range list { |
| if x == arg { |
| return i |
| } |
| } |
| return len(list) |
| */ |
| } |
| |
| // If x is of the form key=value, argName returns the string key. |
| // Otherwise argName returns "". |
| func argName(x Expr) string { |
| if as, ok := x.(*AssignExpr); ok { |
| if id, ok := as.LHS.(*Ident); ok { |
| return id.Name |
| } |
| } |
| return "" |
| } |
| |
| // A namedArg records information needed for sorting |
| // a named call argument into its proper position. |
| type namedArg struct { |
| priority int // kind of name; first sort key |
| name string // name; second sort key |
| index int // original index; final sort key |
| expr Expr // name=value argument |
| } |
| |
| // namedArgs is a slice of namedArg that implements sort.Interface |
| type namedArgs []namedArg |
| |
| func (x namedArgs) Len() int { return len(x) } |
| func (x namedArgs) Swap(i, j int) { x[i], x[j] = x[j], x[i] } |
| |
| func (x namedArgs) Less(i, j int) bool { |
| p := x[i] |
| q := x[j] |
| if p.priority != q.priority { |
| return p.priority < q.priority |
| } |
| if p.name != q.name { |
| return p.name < q.name |
| } |
| return p.index < q.index |
| } |
| |
| // sortStringLists sorts lists of string literals used as specific rule arguments. |
| func sortStringLists(f *File, w *Rewriter) { |
| sortStringList := func(x *Expr) { |
| SortStringList(*x) |
| } |
| |
| Walk(f, func(e Expr, stk []Expr) { |
| switch v := e.(type) { |
| case *CallExpr: |
| if f.Type == TypeDefault || f.Type == TypeBzl { |
| // Rule parameters, not applicable to .bzl or default file types |
| return |
| } |
| if leaveAlone(stk, v) { |
| return |
| } |
| rule := callName(v) |
| for _, arg := range v.List { |
| if leaveAlone1(arg) { |
| continue |
| } |
| as, ok := arg.(*AssignExpr) |
| if !ok || leaveAlone1(as) { |
| continue |
| } |
| key, ok := as.LHS.(*Ident) |
| if !ok { |
| continue |
| } |
| context := rule + "." + key.Name |
| if w.SortableDenylist[context] { |
| continue |
| } |
| if w.IsSortableListArg[key.Name] || |
| w.SortableAllowlist[context] || |
| (!disabled("unsafesort") && allowedSort(context)) { |
| if doNotSort(as) { |
| deduplicateStringList(as.RHS) |
| } else { |
| findAndModifyStrings(&as.RHS, sortStringList) |
| } |
| } |
| } |
| case *AssignExpr: |
| if disabled("unsafesort") { |
| return |
| } |
| // "keep sorted" comment on x = list forces sorting of list. |
| if keepSorted(v) { |
| findAndModifyStrings(&v.RHS, sortStringList) |
| } |
| case *KeyValueExpr: |
| if disabled("unsafesort") { |
| return |
| } |
| // "keep sorted" before key: list also forces sorting of list. |
| if keepSorted(v) { |
| findAndModifyStrings(&v.Value, sortStringList) |
| } |
| case *ListExpr: |
| if disabled("unsafesort") { |
| return |
| } |
| // "keep sorted" comment above first list element also forces sorting of list. |
| if len(v.List) > 0 && (keepSorted(v) || keepSorted(v.List[0])) { |
| findAndModifyStrings(&e, sortStringList) |
| } |
| } |
| }) |
| } |
| |
| // deduplicateStringList removes duplicates from a list with string expressions |
| // without reordering its elements. |
| // Any suffix-comments are lost, any before- and after-comments are preserved. |
| func deduplicateStringList(x Expr) { |
| list, ok := x.(*ListExpr) |
| if !ok { |
| return |
| } |
| |
| list.List = deduplicateStringExprs(list.List) |
| } |
| |
| // deduplicateStringExprs removes duplicate string expressions from a slice |
| // without reordering its elements. |
| // Any suffix-comments are lost, any before- and after-comments are preserved. |
| func deduplicateStringExprs(list []Expr) []Expr { |
| var comments []Comment |
| alreadySeen := make(map[string]bool) |
| var deduplicated []Expr |
| for _, value := range list { |
| str, ok := value.(*StringExpr) |
| if !ok { |
| deduplicated = append(deduplicated, value) |
| continue |
| } |
| strVal := str.Value |
| if _, ok := alreadySeen[strVal]; ok { |
| // This is a duplicate of a string above. |
| // Collect comments so that they're not lost. |
| comments = append(comments, str.Comment().Before...) |
| comments = append(comments, str.Comment().After...) |
| continue |
| } |
| alreadySeen[strVal] = true |
| if len(comments) > 0 { |
| comments = append(comments, value.Comment().Before...) |
| value.Comment().Before = comments |
| comments = nil |
| } |
| deduplicated = append(deduplicated, value) |
| } |
| return deduplicated |
| } |
| |
| // SortStringList sorts x, a list of strings. |
| // The list is broken by non-strings and by blank lines and comments into chunks. |
| // Each chunk is sorted in place. |
| func SortStringList(x Expr) { |
| list, ok := x.(*ListExpr) |
| if !ok || len(list.List) < 2 { |
| return |
| } |
| |
| if doNotSort(list.List[0]) { |
| list.List = deduplicateStringExprs(list.List) |
| return |
| } |
| |
| forceSort := keepSorted(list) || keepSorted(list.List[0]) |
| |
| // TODO(bazel-team): Decide how to recognize lists that cannot |
| // be sorted. Avoiding all lists with comments avoids sorting |
| // lists that say explicitly, in some form or another, why they |
| // cannot be sorted. For example, many cc_test rules require |
| // certain order in their deps attributes. |
| if !forceSort { |
| if line, _ := hasComments(list); line { |
| deduplicateStringList(list) |
| return |
| } |
| } |
| |
| list.List = sortStringExprs(list.List) |
| } |
| |
| // findAndModifyStrings finds and modifies string lists with a callback |
| // function recursively within the given expression. It doesn't touch all |
| // string lists it can find, but only top-level lists, lists that are parts of |
| // concatenated expressions and lists within select statements. |
| // It calls the callback on the root node and on all relevant inner lists. |
| // The callback function should gracefully return if called with not appropriate |
| // arguments. |
| func findAndModifyStrings(x *Expr, callback func(*Expr)) { |
| callback(x) |
| switch x := (*x).(type) { |
| case *BinaryExpr: |
| if x.Op != "+" { |
| return |
| } |
| findAndModifyStrings(&x.X, callback) |
| findAndModifyStrings(&x.Y, callback) |
| case *CallExpr: |
| if ident, ok := x.X.(*Ident); !ok || ident.Name != "select" { |
| return |
| } |
| if len(x.List) == 0 { |
| return |
| } |
| dict, ok := x.List[0].(*DictExpr) |
| if !ok { |
| return |
| } |
| for _, kv := range dict.List { |
| findAndModifyStrings(&kv.Value, callback) |
| } |
| } |
| } |
| |
| func sortStringExprs(list []Expr) []Expr { |
| if len(list) < 2 { |
| return list |
| } |
| |
| // Sort chunks of the list with no intervening blank lines or comments. |
| for i := 0; i < len(list); { |
| if _, ok := list[i].(*StringExpr); !ok { |
| i++ |
| continue |
| } |
| |
| j := i + 1 |
| for ; j < len(list); j++ { |
| if str, ok := list[j].(*StringExpr); !ok || len(str.Before) > 0 { |
| break |
| } |
| } |
| |
| var chunk []stringSortKey |
| for index, x := range list[i:j] { |
| chunk = append(chunk, makeSortKey(index, x.(*StringExpr))) |
| } |
| if !sort.IsSorted(byStringExpr(chunk)) || !isUniq(chunk) { |
| before := chunk[0].x.Comment().Before |
| chunk[0].x.Comment().Before = nil |
| |
| sort.Sort(byStringExpr(chunk)) |
| chunk = uniq(chunk) |
| |
| chunk[0].x.Comment().Before = before |
| for offset, key := range chunk { |
| list[i+offset] = key.x |
| } |
| list = append(list[:(i+len(chunk))], list[j:]...) |
| } |
| |
| i = j |
| } |
| |
| return list |
| } |
| |
| // uniq removes duplicates from a list, which must already be sorted. |
| // It edits the list in place. |
| func uniq(sortedList []stringSortKey) []stringSortKey { |
| out := sortedList[:0] |
| for _, sk := range sortedList { |
| if len(out) == 0 || sk.value != out[len(out)-1].value { |
| out = append(out, sk) |
| } |
| } |
| return out |
| } |
| |
| // isUniq reports whether the sorted list only contains unique elements. |
| func isUniq(list []stringSortKey) bool { |
| for i := range list { |
| if i+1 < len(list) && list[i].value == list[i+1].value { |
| return false |
| } |
| } |
| return true |
| } |
| |
| // If stk describes a call argument like rule(arg=...), callArgName |
| // returns the name of that argument, formatted as "rule.arg". |
| func callArgName(stk []Expr) string { |
| n := len(stk) |
| if n < 2 { |
| return "" |
| } |
| arg := argName(stk[n-1]) |
| if arg == "" { |
| return "" |
| } |
| call, ok := stk[n-2].(*CallExpr) |
| if !ok { |
| return "" |
| } |
| rule, ok := call.X.(*Ident) |
| if !ok { |
| return "" |
| } |
| return rule.Name + "." + arg |
| } |
| |
| // A stringSortKey records information about a single string literal to be |
| // sorted. The strings are first grouped into four phases: most strings, |
| // strings beginning with ":", strings beginning with "//", and strings |
| // beginning with "@". The next significant part of the comparison is the list |
| // of elements in the value, where elements are split at `.' and `:'. Finally |
| // we compare by value and break ties by original index. |
| type stringSortKey struct { |
| phase int |
| split []string |
| value string |
| original int |
| x Expr |
| } |
| |
| func makeSortKey(index int, x *StringExpr) stringSortKey { |
| key := stringSortKey{ |
| value: x.Value, |
| original: index, |
| x: x, |
| } |
| |
| switch { |
| case strings.HasPrefix(x.Value, ":"): |
| key.phase = 1 |
| case strings.HasPrefix(x.Value, "//") || (tables.StripLabelLeadingSlashes && !strings.HasPrefix(x.Value, "@")): |
| key.phase = 2 |
| case strings.HasPrefix(x.Value, "@"): |
| key.phase = 3 |
| } |
| |
| key.split = strings.Split(strings.Replace(x.Value, ":", ".", -1), ".") |
| return key |
| } |
| |
| // byStringExpr implements sort.Interface for a list of stringSortKey. |
| type byStringExpr []stringSortKey |
| |
| func (x byStringExpr) Len() int { return len(x) } |
| func (x byStringExpr) Swap(i, j int) { x[i], x[j] = x[j], x[i] } |
| |
| func (x byStringExpr) Less(i, j int) bool { |
| xi := x[i] |
| xj := x[j] |
| |
| if xi.phase != xj.phase { |
| return xi.phase < xj.phase |
| } |
| for k := 0; k < len(xi.split) && k < len(xj.split); k++ { |
| if xi.split[k] != xj.split[k] { |
| return xi.split[k] < xj.split[k] |
| } |
| } |
| if len(xi.split) != len(xj.split) { |
| return len(xi.split) < len(xj.split) |
| } |
| if xi.value != xj.value { |
| return xi.value < xj.value |
| } |
| return xi.original < xj.original |
| } |
| |
| // fixMultilinePlus turns |
| // |
| // ... + |
| // [ ... ] |
| // |
| // ... + |
| // call(...) |
| // |
| // into |
| // |
| // ... + [ |
| // ... |
| // ] |
| // |
| // ... + call( |
| // ... |
| // ) |
| // |
| // which typically works better with our aggressively compact formatting. |
| func fixMultilinePlus(f *File, _ *Rewriter) { |
| |
| // List manipulation helpers. |
| // As a special case, we treat f([...]) as a list, mainly |
| // for glob. |
| |
| // isList reports whether x is a list. |
| var isList func(x Expr) bool |
| isList = func(x Expr) bool { |
| switch x := x.(type) { |
| case *ListExpr: |
| return true |
| case *CallExpr: |
| if len(x.List) == 1 { |
| return isList(x.List[0]) |
| } |
| } |
| return false |
| } |
| |
| // isMultiLine reports whether x is a multiline list. |
| var isMultiLine func(Expr) bool |
| isMultiLine = func(x Expr) bool { |
| switch x := x.(type) { |
| case *ListExpr: |
| return x.ForceMultiLine || len(x.List) > 1 |
| case *CallExpr: |
| if x.ForceMultiLine || len(x.List) > 1 && !x.ForceCompact { |
| return true |
| } |
| if len(x.List) == 1 { |
| return isMultiLine(x.List[0]) |
| } |
| } |
| return false |
| } |
| |
| // forceMultiLine tries to force the list x to use a multiline form. |
| // It reports whether it was successful. |
| var forceMultiLine func(Expr) bool |
| forceMultiLine = func(x Expr) bool { |
| switch x := x.(type) { |
| case *ListExpr: |
| // Already multi line? |
| if x.ForceMultiLine { |
| return true |
| } |
| // If this is a list containing a list, force the |
| // inner list to be multiline instead. |
| if len(x.List) == 1 && forceMultiLine(x.List[0]) { |
| return true |
| } |
| x.ForceMultiLine = true |
| return true |
| |
| case *CallExpr: |
| if len(x.List) == 1 { |
| return forceMultiLine(x.List[0]) |
| } |
| } |
| return false |
| } |
| |
| skip := map[Expr]bool{} |
| Walk(f, func(v Expr, stk []Expr) { |
| if skip[v] { |
| return |
| } |
| bin, ok := v.(*BinaryExpr) |
| if !ok || bin.Op != "+" { |
| return |
| } |
| |
| // Found a +. |
| // w + x + y + z parses as ((w + x) + y) + z, |
| // so chase down the left side to make a list of |
| // all the things being added together, separated |
| // by the BinaryExprs that join them. |
| // Mark them as "skip" so that when Walk recurses |
| // into the subexpressions, we won't reprocess them. |
| var all []Expr |
| for { |
| all = append(all, bin.Y, bin) |
| bin1, ok := bin.X.(*BinaryExpr) |
| if !ok || bin1.Op != "+" { |
| break |
| } |
| bin = bin1 |
| skip[bin] = true |
| } |
| all = append(all, bin.X) |
| |
| // Because the outermost expression was the |
| // rightmost one, the list is backward. Reverse it. |
| for i, j := 0, len(all)-1; i < j; i, j = i+1, j-1 { |
| all[i], all[j] = all[j], all[i] |
| } |
| |
| // The 'all' slice is alternating addends and BinaryExpr +'s: |
| // w, +, x, +, y, +, z |
| // If there are no lists involved, don't rewrite anything. |
| haveList := false |
| for i := 0; i < len(all); i += 2 { |
| if isList(all[i]) { |
| haveList = true |
| break |
| } |
| } |
| if !haveList { |
| return |
| } |
| |
| // Okay, there are lists. |
| // Consider each + next to a line break. |
| for i := 1; i < len(all); i += 2 { |
| bin := all[i].(*BinaryExpr) |
| if !bin.LineBreak { |
| continue |
| } |
| |
| // We're going to break the line after the +. |
| // If it is followed by a list, force that to be |
| // multiline instead. |
| if forceMultiLine(all[i+1]) { |
| bin.LineBreak = false |
| continue |
| } |
| |
| // If the previous list was multiline already, |
| // don't bother with the line break after |
| // the +. |
| if isMultiLine(all[i-1]) { |
| bin.LineBreak = false |
| continue |
| } |
| } |
| }) |
| } |
| |
| // isFunctionCall checks whether expr is a call of a function with a given name |
| func isFunctionCall(expr Expr, name string) bool { |
| call, ok := expr.(*CallExpr) |
| if !ok { |
| return false |
| } |
| if ident, ok := call.X.(*Ident); ok && ident.Name == name { |
| return true |
| } |
| return false |
| } |
| |
| // moveLoadOnTop moves all load statements to the top of the file |
| func moveLoadOnTop(f *File, _ *Rewriter) { |
| if f.Type == TypeWorkspace { |
| // Moving load statements in Workspace files can break the semantics |
| return |
| } |
| if isCommentAnywhere(f, "disable=load-on-top") { |
| // For backward compatibility. This rewrite used to be a suppressible warning, |
| // in some cases it's hard to maintain the position of load statements (e.g. |
| // when the file is automatically generated or has automatic transformations |
| // applied to it). The rewrite checks for the comment anywhere in the file |
| // because it's hard to determine which statement is out of order. |
| return |
| } |
| |
| // Find the misplaced load statements |
| misplacedLoads := make(map[int]*LoadStmt) |
| firstStmtIndex := -1 // index of the first seen non-load statement |
| for i := 0; i < len(f.Stmt); i++ { |
| stmt := f.Stmt[i] |
| _, isString := stmt.(*StringExpr) // typically a docstring |
| _, isComment := stmt.(*CommentBlock) |
| isWorkspace := isFunctionCall(stmt, "workspace") |
| if isString || isComment || isWorkspace || stmt == nil { |
| continue |
| } |
| load, ok := stmt.(*LoadStmt) |
| if !ok { |
| if firstStmtIndex == -1 { |
| firstStmtIndex = i |
| } |
| continue |
| } |
| if firstStmtIndex == -1 { |
| continue |
| } |
| misplacedLoads[i] = load |
| } |
| |
| // Calculate a fix |
| if firstStmtIndex == -1 { |
| firstStmtIndex = 0 |
| } |
| offset := len(misplacedLoads) |
| if offset == 0 { |
| return |
| } |
| stmtCopy := append([]Expr{}, f.Stmt...) |
| for i := range f.Stmt { |
| if i < firstStmtIndex { |
| // Docstring or a comment in the beginning, skip |
| continue |
| } else if _, ok := misplacedLoads[i]; ok { |
| // A misplaced load statement, should be moved up to the `firstStmtIndex` position |
| f.Stmt[firstStmtIndex] = stmtCopy[i] |
| firstStmtIndex++ |
| offset-- |
| if offset == 0 { |
| // No more statements should be moved |
| break |
| } |
| } else { |
| // An actual statement (not a docstring or a comment in the beginning), should be moved |
| // `offset` positions down. |
| f.Stmt[i+offset] = stmtCopy[i] |
| } |
| } |
| } |
| |
| // compressSameOriginLoads merges loads from the same source into one load statement |
| func compressSameOriginLoads(f *File, _ *Rewriter) { |
| // Load statements grouped by their source files |
| loads := make(map[string]*LoadStmt) |
| |
| for stmtIndex := 0; stmtIndex < len(f.Stmt); stmtIndex++ { |
| load, ok := f.Stmt[stmtIndex].(*LoadStmt) |
| if !ok { |
| continue |
| } |
| |
| previousLoad, ok := loads[load.Module.Value] |
| if !ok { |
| loads[load.Module.Value] = load |
| continue |
| } |
| if hasComment(previousLoad, "disable=same-origin-load") || |
| hasComment(load, "disable=same-origin-load") { |
| continue |
| } |
| |
| // Move loaded symbols to the existing load statement |
| previousLoad.From = append(previousLoad.From, load.From...) |
| previousLoad.To = append(previousLoad.To, load.To...) |
| // Remove the current statement |
| f.Stmt[stmtIndex] = nil |
| } |
| } |
| |
| // comparePaths compares two strings as if they were paths (the path delimiter, |
| // '/', should be treated as smallest symbol). |
| func comparePaths(path1, path2 string) bool { |
| if path1 == path2 { |
| return false |
| } |
| |
| chunks1 := strings.Split(path1, "/") |
| chunks2 := strings.Split(path2, "/") |
| |
| for i, chunk1 := range chunks1 { |
| if i >= len(chunks2) { |
| return false |
| } |
| chunk2 := chunks2[i] |
| // Compare case-insensitively |
| chunk1Lower := strings.ToLower(chunk1) |
| chunk2Lower := strings.ToLower(chunk2) |
| if chunk1Lower != chunk2Lower { |
| return chunk1Lower < chunk2Lower |
| } |
| } |
| |
| // No case-insensitive difference detected. Likely the difference is just in |
| // the case of some symbols, compare case-sensitively for the determinism. |
| return path1 <= path2 |
| } |
| |
| // compareLoadLabels compares two module names |
| // If one label has explicit repository path (starts with @), it goes first |
| // If the packages are different, labels are sorted by package name (empty package goes first) |
| // If the packages are the same, labels are sorted by their name |
| // Relative labels go last |
| func compareLoadLabels(load1Label, load2Label string) bool { |
| // handle absolute labels with explicit repositories separately to |
| // make sure they precede absolute and relative labels without repos |
| isExplicitRepo1 := strings.HasPrefix(load1Label, "@") |
| isExplicitRepo2 := strings.HasPrefix(load2Label, "@") |
| |
| if isExplicitRepo1 != isExplicitRepo2 { |
| // Exactly one label has an explicit repository name, it should be the first one. |
| return isExplicitRepo1 |
| } |
| |
| // ensure that relative labels go last by giving them a fake package name |
| // that sorts after all valid package names |
| label1 := labels.ParseRelative(load1Label, string(rune(0x7F))) |
| label2 := labels.ParseRelative(load2Label, string(rune(0x7F))) |
| |
| if label1.Repository != label2.Repository { |
| return label1.Repository < label2.Repository |
| } |
| |
| // in case one of the packages is empty, the empty one goes first |
| if (len(label1.Package) == 0) != (len(label2.Package) == 0) { |
| return len(label1.Package) == 0 |
| } |
| |
| // both packages are non-empty and not equal, so compare them |
| if label1.Package != label2.Package { |
| return comparePaths(label1.Package, label2.Package) |
| } |
| |
| // in case both packages are the same, use file names to break ties |
| return comparePaths(label1.Target, label2.Target) |
| } |
| |
| // sortLoadStatements reorders sorts loads lexicographically by the source file, |
| // but absolute loads have priority over local loads. |
| func sortLoadStatements(f *File, _ *Rewriter) { |
| if isCommentAnywhere(f, "disable=out-of-order-load") { |
| // For backward compatibility. This rewrite used to be a suppressible warning, |
| // in some cases it's hard to maintain the position of load statements (e.g. |
| // when the file is automatically generated or has automatic transformations |
| // applied to it). The rewrite checks for the comment anywhere in the file |
| // because it's hard to determine which statement is out of order. |
| return |
| } |
| |
| // Consequent chunks of load statements (i.e. without statements of other types between them) |
| var loadsChunks [][]*LoadStmt |
| newChunk := true // Create a new chunk for the next seen load statement |
| |
| for _, stmt := range f.Stmt { |
| if stmt == nil { |
| // nil statements are no-op and shouldn't break consecutive chains of |
| // load statements |
| continue |
| } |
| load, ok := stmt.(*LoadStmt) |
| if !ok { |
| newChunk = true |
| continue |
| } |
| if newChunk { |
| // There's a non-load statement between this load and the previous load |
| loadsChunks = append(loadsChunks, []*LoadStmt{}) |
| newChunk = false |
| } |
| loadsChunks[len(loadsChunks)-1] = append(loadsChunks[len(loadsChunks)-1], load) |
| } |
| |
| // Sort and flatten the chunks |
| var sortedLoads []*LoadStmt |
| for _, chunk := range loadsChunks { |
| sortedChunk := append([]*LoadStmt{}, chunk...) |
| |
| sort.SliceStable(sortedChunk, func(i, j int) bool { |
| load1Label := (sortedChunk)[i].Module.Value |
| load2Label := (sortedChunk)[j].Module.Value |
| return compareLoadLabels(load1Label, load2Label) |
| }) |
| sortedLoads = append(sortedLoads, sortedChunk...) |
| } |
| |
| // Calculate the replacements |
| loadIndex := 0 |
| for stmtIndex := range f.Stmt { |
| if _, ok := f.Stmt[stmtIndex].(*LoadStmt); !ok { |
| continue |
| } |
| f.Stmt[stmtIndex] = sortedLoads[loadIndex] |
| loadIndex++ |
| } |
| } |
| |
| // sortAllLoadArgs sorts all load arguments in the file |
| func sortAllLoadArgs(f *File, _ *Rewriter) { |
| Walk(f, func(v Expr, stk []Expr) { |
| if load, ok := v.(*LoadStmt); ok { |
| SortLoadArgs(load) |
| } |
| }) |
| } |
| |
| func sortUseRepoPositionals(f *File, _ *Rewriter) { |
| Walk(f, func(v Expr, stk []Expr) { |
| if call, ok := v.(*CallExpr); ok { |
| // The first argument of a valid use_repo call is always a module extension proxy, so we |
| // do not need to sort calls with less than three arguments. |
| if ident, ok := call.X.(*Ident); !ok || ident.Name != "use_repo" || len(call.List) < 3 { |
| return |
| } |
| // Respect the "do not sort" comment on both the first argument and the first repository |
| // name. |
| if doNotSort(call) || doNotSort(call.List[0]) || doNotSort(call.List[1]) { |
| call.List = deduplicateStringExprs(call.List) |
| } else { |
| // Keyword arguments do not have to be sorted here as this has already been done by |
| // the generic callsort rewriter pass. |
| call.List = sortStringExprs(call.List) |
| } |
| } |
| }) |
| } |
| |
| // hasComments reports whether any comments are associated with |
| // the list or its elements. |
| func hasComments(list *ListExpr) (line, suffix bool) { |
| com := list.Comment() |
| if len(com.Before) > 0 || len(com.After) > 0 || len(list.End.Before) > 0 { |
| line = true |
| } |
| if len(com.Suffix) > 0 { |
| suffix = true |
| } |
| for _, elem := range list.List { |
| com := elem.Comment() |
| if len(com.Before) > 0 { |
| line = true |
| } |
| if len(com.Suffix) > 0 { |
| suffix = true |
| } |
| } |
| return |
| } |
| |
| // A wrapper for a LoadStmt's From and To slices for consistent sorting of their contents. |
| // It's assumed that the following slices have the same length. The contents are sorted by |
| // the `To` attribute, but all items with equal "From" and "To" parts are placed before the items |
| // with different parts. |
| type loadArgs struct { |
| From []*Ident |
| To []*Ident |
| modified bool |
| } |
| |
| func (args *loadArgs) Len() int { |
| return len(args.From) |
| } |
| |
| func (args *loadArgs) Swap(i, j int) { |
| args.From[i], args.From[j] = args.From[j], args.From[i] |
| args.To[i], args.To[j] = args.To[j], args.To[i] |
| args.modified = true |
| } |
| |
| func (args *loadArgs) Less(i, j int) bool { |
| // Arguments with equal "from" and "to" parts are prioritized |
| equalI := args.From[i].Name == args.To[i].Name |
| equalJ := args.From[j].Name == args.To[j].Name |
| if equalI != equalJ { |
| // If equalI and !equalJ, return true, otherwise false. |
| // Equivalently, return equalI. |
| return equalI |
| } |
| return args.To[i].Name < args.To[j].Name |
| } |
| |
| // deduplicate assumes that the args are sorted and prioritize arguments that |
| // are defined later (e.g. `a = "x", a = "y"` is shortened to `a = "y"`). |
| func (args *loadArgs) deduplicate() { |
| var from, to []*Ident |
| for i := range args.To { |
| if len(to) > 0 && to[len(to)-1].Name == args.To[i].Name { |
| // Overwrite |
| from[len(from)-1] = args.From[i] |
| to[len(to)-1] = args.To[i] |
| args.modified = true |
| } else { |
| // Just append |
| from = append(from, args.From[i]) |
| to = append(to, args.To[i]) |
| } |
| } |
| args.From = from |
| args.To = to |
| } |
| |
| // SortLoadArgs sorts a load statement arguments (lexicographically, but positional first) |
| func SortLoadArgs(load *LoadStmt) bool { |
| args := loadArgs{From: load.From, To: load.To} |
| sort.Sort(&args) |
| args.deduplicate() |
| load.From = args.From |
| load.To = args.To |
| return args.modified |
| } |
| |
| // formatDocstrings fixes the indentation and trailing whitespace of docstrings |
| func formatDocstrings(f *File, _ *Rewriter) { |
| Walk(f, func(v Expr, stk []Expr) { |
| def, ok := v.(*DefStmt) |
| if !ok || len(def.Body) == 0 { |
| return |
| } |
| docstring, ok := def.Body[0].(*StringExpr) |
| if !ok || !docstring.TripleQuote { |
| return |
| } |
| |
| oldIndentation := docstring.Start.LineRune - 1 // LineRune starts with 1 |
| newIndentation := nestedIndentation * len(stk) |
| |
| // Operate on Token, not Value, because their line breaks can be different if a line ends with |
| // a backslash. |
| updatedToken := formatString(docstring.Token, oldIndentation, newIndentation) |
| if updatedToken != docstring.Token { |
| docstring.Token = updatedToken |
| // Update the value to keep it consistent with Token |
| docstring.Value, _, _ = Unquote(updatedToken) |
| } |
| }) |
| } |
| |
| // formatString modifies a string value of a docstring to match the new indentation level and |
| // to remove trailing whitespace from its lines. |
| func formatString(value string, oldIndentation, newIndentation int) string { |
| difference := newIndentation - oldIndentation |
| lines := strings.Split(value, "\n") |
| for i, line := range lines { |
| if i == 0 { |
| // The first line shouldn't be touched because it starts right after ''' or """ |
| continue |
| } |
| if difference > 0 { |
| line = strings.Repeat(" ", difference) + line |
| } else { |
| for i, rune := range line { |
| if i == -difference || rune != ' ' { |
| line = line[i:] |
| break |
| } |
| } |
| } |
| if i != len(lines)-1 { |
| // Remove trailing space from the line unless it's the last line that's responsible |
| // for the indentation of the closing `"""` |
| line = strings.TrimRight(line, " ") |
| } |
| lines[i] = line |
| } |
| return strings.Join(lines, "\n") |
| } |
| |
| // argumentType returns an integer by which funcall arguments can be sorted: |
| // 1 for positional, 2 for named, 3 for *args, 4 for **kwargs |
| func argumentType(expr Expr) int { |
| switch expr := expr.(type) { |
| case *UnaryExpr: |
| switch expr.Op { |
| case "**": |
| return 4 |
| case "*": |
| return 3 |
| } |
| case *AssignExpr: |
| return 2 |
| } |
| return 1 |
| } |
| |
| // reorderArguments fixes the order of arguments of a function call |
| // (positional, named, *args, **kwargs) |
| func reorderArguments(f *File, _ *Rewriter) { |
| Walk(f, func(expr Expr, stack []Expr) { |
| call, ok := expr.(*CallExpr) |
| if !ok { |
| return |
| } |
| compare := func(i, j int) bool { |
| // Keep nil nodes at their place. They are no-op for the formatter but can |
| // be useful for the linter which expects them to not move. |
| if call.List[i] == nil || call.List[j] == nil { |
| return false |
| } |
| return argumentType(call.List[i]) < argumentType(call.List[j]) |
| } |
| if !sort.SliceIsSorted(call.List, compare) { |
| sort.SliceStable(call.List, compare) |
| } |
| }) |
| } |
| |
| // editOctals inserts 'o' into octal numbers to make it more obvious they are octal |
| // 0123 -> 0o123 |
| func editOctals(f *File, _ *Rewriter) { |
| Walk(f, func(expr Expr, stack []Expr) { |
| l, ok := expr.(*LiteralExpr) |
| if !ok { |
| return |
| } |
| if len(l.Token) > 1 && l.Token[0] == '0' && l.Token[1] >= '0' && l.Token[1] <= '9' { |
| l.Token = "0o" + l.Token[1:] |
| } |
| }) |
| } |
| |
| // editFloats inserts '0' before the decimal point in floats and normalizes the |
| // exponent part to lowercase, no plus sign, and no leading zero. |
| func editFloats(f *File, _ *Rewriter) { |
| Walk(f, func(expr Expr, stack []Expr) { |
| l, ok := expr.(*LiteralExpr) |
| if !ok { |
| return |
| } |
| if !strings.ContainsRune(l.Token, '.') { |
| return |
| } |
| if strings.HasPrefix(l.Token, ".") { |
| l.Token = "0" + l.Token |
| } |
| if !strings.ContainsAny(l.Token, "eE") { |
| return |
| } |
| parts := strings.SplitN(l.Token, "e", 2) |
| if len(parts) != 2 { |
| parts = strings.SplitN(l.Token, "E", 2) |
| } |
| if len(parts) != 2 { |
| // Invalid float, skip rewriting. |
| return |
| } |
| l.Token = parts[0] + "e" + strings.TrimLeft(parts[1], "0+") |
| }) |
| } |
| |
| // removeParens removes trivial parens |
| func removeParens(f *File, _ *Rewriter) { |
| var simplify func(expr Expr, stack []Expr) Expr |
| simplify = func(expr Expr, stack []Expr) Expr { |
| // Look for parenthesized expressions, ignoring those with |
| // comments and those that are intentionally multiline. |
| pa, ok := expr.(*ParenExpr) |
| if !ok || pa.ForceMultiLine { |
| return expr |
| } |
| if len(pa.Comment().Before) > 0 || len(pa.Comment().After) > 0 || len(pa.Comment().Suffix) > 0 { |
| return expr |
| } |
| |
| switch x := pa.X.(type) { |
| case *Comprehension, *DictExpr, *Ident, *ListExpr, *LiteralExpr, *ParenExpr, *SetExpr, *StringExpr: |
| // These expressions don't need parens, remove them (recursively). |
| return Edit(x, simplify) |
| case *CallExpr: |
| // Parens might be needed if the callable is multiline. |
| start, end := x.X.Span() |
| if start.Line == end.Line { |
| return Edit(x, simplify) |
| } |
| } |
| return expr |
| } |
| |
| Edit(f, simplify) |
| } |
| |
| // collapseEmpty unsets ForceMultiLine for empty call expressions. |
| func collapseEmpty(f *File, _ *Rewriter) { |
| Walk(f, func(expr Expr, stack []Expr) { |
| if c, ok := expr.(*CallExpr); ok && len(c.List) == 0 { // No arguments |
| c.ForceMultiLine = false |
| } |
| }) |
| } |