Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP : Support Tabular Data formatting in Buildifier #985

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a0f8f33
Add testcases
elenaodimitrova Mar 31, 2021
5aeb63d
TODO revert : Test commit
rtabassum Mar 31, 2021
d59c7f5
Revert last test commit
rtabassum Mar 31, 2021
3700471
Introducing Tabwriter in print.go
HariPreethiS Mar 31, 2021
90d6ff0
TabWriter - Version 2
HariPreethiS Mar 31, 2021
38e9ede
Add a placeholder method for where the metaData on the List and Tuple…
SKashyap Mar 31, 2021
9e5a59c
Add tags for table formatting and sorting: #buildifier: table sort 1 …
rtabassum Mar 31, 2021
78308e4
Update tags in the UnitTests
rtabassum Mar 31, 2021
1c1705e
Fix 6 failing tests. Solution : Hook up tabWrite logic only when we d…
SKashyap Mar 31, 2021
254df91
Fix Panics while reading tags - also test failures cause by it.
SKashyap Mar 31, 2021
6d157e1
Update UnitTests to apply tags at the tabletop
rtabassum Mar 31, 2021
4703c9d
Add more UnitTests for testcases with - long column width, variable c…
rtabassum Mar 31, 2021
bfbc5c4
Fix issue with table sort tag - panic: runtime error: index out of ra…
rtabassum Mar 31, 2021
f794bd2
First version of Table sorting with tag #buildifier: table sort N
rtabassum Apr 1, 2021
f642928
Attempt fix sort bug
rtabassum Apr 1, 2021
49185cb
Fix sorting
elenaodimitrova Apr 1, 2021
7dcad2b
Changes done -
HariPreethiS Apr 1, 2021
d087cb5
Revise golden files with sorting logic.
HariPreethiS Apr 1, 2021
f1c491d
Test fix for 067 + Code clean up
SKashyap Apr 1, 2021
15e04bb
More code clean-up , remove unnecessary variables - standardize the t…
SKashyap Apr 1, 2021
45d2326
Format changes, code cleanup
HariPreethiS Apr 1, 2021
e866e9c
Adding README.md for MagicTable
HariPreethiS Apr 1, 2021
495a548
Additional documentation of usage in Readme
SKashyap Apr 1, 2021
9727036
Cleanup README.md
rtabassum Jun 18, 2021
376034e
Add unit tests for buildifier sorting tag
rtabassum Jun 19, 2021
636d6bd
Bug Fix - Invalid column number specified in sorting tag.
rtabassum Jun 19, 2021
8b787fb
Handle case when duplicate keys for the table column to be sorted.
rtabassum Jun 20, 2021
563207f
Fix the "missing space" issue with some design changes
SKashyap Jun 21, 2021
3ceb917
Refactoring `formatTables` function
SKashyap Jun 21, 2021
dc76563
Completion of Design 1:
SKashyap Jun 22, 2021
9ba01a4
Code cleanup- Add back mode `modeTable` instead of extra param in `se…
SKashyap Jun 22, 2021
bb4a4f5
Handling invalid or missing "column number" for sort.
SKashyap Mar 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 77 additions & 12 deletions build/print.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"bytes"
"fmt"
"strings"
"text/tabwriter"
)

const (
Expand Down Expand Up @@ -63,18 +64,26 @@ func FormatString(x Expr) string {

// A printer collects the state during printing of a file or expression.
type printer struct {
fileType FileType // different rules can be applied to different file types.
bytes.Buffer // output buffer
comment []Comment // pending end-of-line comments
margin int // left margin (indent), a number of spaces
depth int // nesting depth inside ( ) [ ] { }
level int // nesting level of def-, if-else- and for-blocks
needsNewLine bool // true if the next statement needs a new line before it
fileType FileType // different rules can be applied to different file types.
bytes.Buffer // output buffer
comment []Comment // pending end-of-line comments
margin int // left margin (indent), a number of spaces
depth int // nesting depth inside ( ) [ ] { }
level int // nesting level of def-, if-else- and for-blocks
needsNewLine bool // true if the next statement needs a new line before it
tabWriterOn bool // when this mode is on ,use the tabwriter to write to buffer
tWriter *tabwriter.Writer // tab writer
}

// printf prints to the buffer.
// printf prints to the buffer either directly or via TabWriter based on the mode
func (p *printer) printf(format string, args ...interface{}) {
fmt.Fprintf(p, format, args...)
if !p.tabWriterOn {
fmt.Fprintf(p, format, args...)
return
}
if p.tWriter != nil {
fmt.Fprintf(p.tWriter, format, args...)
}
}

// indent returns the position on the current line, in bytes, 0-indexed.
Expand Down Expand Up @@ -374,7 +383,9 @@ func (p *printer) expr(v Expr, outerPrec int) {
p.printf("\n")
}
// Re-indent to margin.
p.printf("%*s", p.margin, "")
if !p.tabWriterOn {
p.printf("%*s", p.margin, "")
}
for _, com := range before {
p.printf("%s", strings.TrimSpace(com.Token))
p.newline()
Expand Down Expand Up @@ -607,7 +618,11 @@ func (p *printer) expr(v Expr, outerPrec int) {
p.seq("()", &v.Load, &args, &v.Rparen, modeLoad, v.ForceCompact, false)

case *ListExpr:
p.seq("[]", &v.Start, &v.List, &v.End, modeList, false, v.ForceMultiLine)
mode := modeList
if v.FormatAsTable {
mode = modeTable
}
p.seq("[]", &v.Start, &v.List, &v.End, mode, false, v.ForceMultiLine)

case *SetExpr:
p.seq("{}", &v.Start, &v.List, &v.End, modeList, false, v.ForceMultiLine)
Expand All @@ -617,7 +632,12 @@ func (p *printer) expr(v Expr, outerPrec int) {
if v.NoBrackets {
mode = modeSeq
}
p.seq("()", &v.Start, &v.List, &v.End, mode, v.ForceCompact, v.ForceMultiLine)

if v.FormatAsTableRow {
p.tabbedSeq("()", &v.Start, &v.List, &v.End)
} else {
p.seq("()", &v.Start, &v.List, &v.End, mode, v.ForceCompact, v.ForceMultiLine)
}

case *DictExpr:
var list []Expr
Expand Down Expand Up @@ -747,6 +767,7 @@ const (
modeSeq // x, y
modeDef // def f(x, y)
modeLoad // load(a, b, c)
modeTable // [(x,y)] : List that prints as a table
)

// useCompactMode reports whether a sequence should be formatted in a compact mode
Expand All @@ -762,6 +783,10 @@ func (p *printer) useCompactMode(start *Position, list *[]Expr, end *End, mode s
return false
}

// Tables are always multiline
if mode == modeTable {
return false
}
// Implicit tuples are always compact
if mode == modeSeq {
return true
Expand Down Expand Up @@ -821,7 +846,19 @@ func (p *printer) seq(brack string, start *Position, list *[]Expr, end *End, mod
if mode != modeSeq {
p.printf("%s", brack[:1])
}

p.depth++
if mode == modeTable {
p.tabWriterOn = true
p.tWriter = new(tabwriter.Writer)
p.tWriter.Init(p, 0, 0, 4, ' ', tabwriter.TabIndent)
}
defer func() {
if mode == modeTable {
p.tWriter.Flush()
}
}()

defer func() {
p.depth--
if mode != modeSeq {
Expand Down Expand Up @@ -882,6 +919,34 @@ func (p *printer) seq(brack string, start *Position, list *[]Expr, end *End, mod
}
}

// Formats a list of values inside a given bracket pair (brack = "()", "[]")
// by adding a tabspace in between, so that the tabwriter prints them out in table format.
func (p *printer) tabbedSeq(brack string, start *Position, list *[]Expr, end *End) {

// Print starting braces
p.printf("%s", brack[:1])

p.depth++
defer func() {
p.depth--
p.printf("%s", brack[1:])

}()

// Tablerows entries in each column must end with a tab, to table format it.
for i, x := range *list {
if i > 0 {
p.printf(",\t")
}
p.expr(x, precLow)
}

// Single-element tuple must end with comma, to mark it as a tuple.
if len(*list) == 1 {
p.printf(",")
}
}

func needsTrailingComma(mode seqMode, v Expr) bool {
switch mode {
case modeDef:
Expand Down
131 changes: 131 additions & 0 deletions build/rewrite.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"

"github.com/bazelbuild/buildtools/tables"
Expand Down Expand Up @@ -84,6 +85,7 @@ var rewrites = []struct {
{"removeParens", removeParens, scopeBuild},
{"callsort", sortCallArgs, scopeBuild},
{"label", fixLabels, scopeBuild},
{"formatTables", formatTables, scopeBoth},
{"listsort", sortStringLists, scopeBoth},
{"multiplus", fixMultilinePlus, scopeBuild},
{"loadsort", sortAllLoadArgs, scopeBoth},
Expand Down Expand Up @@ -117,6 +119,35 @@ func hasComment(x Expr, text string) bool {
return false
}

// getColumnNumber reports the column number specified in the x for
// "buildifier: table sort <n>" in comment.
func getColumnNumber(x Expr) int {
for _, com := range x.Comment().Before {
tokens := strings.Split(strings.ToLower(com.Token), " ")
colNum, err := strconv.Atoi(tokens[4])
if err == nil && colNum > 0 {
return colNum
}
}
return -1
}

// tableFormat reports whether x is marked with a comment containing
// "buildifier: table", case-insensitive.
func tableFormat(x Expr) bool {
return hasComment(x, "buildifier: table")
}

// tableSort reports whether x is marked with a comment containing
// "buildifier: table sort <n>", case-insensitive and returns the
// column number.
func tableSort(x Expr) int {
if hasComment(x, "buildifier: table sort") {
return getColumnNumber(x)
}
return -1
}

// leaveAlone1 reports whether x is marked with a comment containing
// "buildifier: leave-alone", case-insensitive.
func leaveAlone1(x Expr) bool {
Expand Down Expand Up @@ -376,6 +407,106 @@ func (x namedArgs) Less(i, j int) bool {
return p.index < q.index
}

// Detects if a data is marked as tablular based on the tag
// `buildifier: table` and sets the correct flags on the expression to
// identify those nodes in the AST.
func formatTables(f *File) {

// Function that sets a bit on the child nodes of type (list) to indicate
// that they are a part of a row
markTableRowNodes := func(x Expr, stk2 []Expr) Expr {
if x, ok := x.(*TupleExpr); ok {
x.FormatAsTableRow = true
}
return x
}

markTableNodes := func(v Expr, stk []Expr) {
switch v := v.(type) {
// Tabular formatting is currently supported only for lists
case *ListExpr:
// Handle when "#buildifier: table" tag is set
if len(v.List) > 0 && tableFormat(v.List[0]) {
v.FormatAsTable = true
EditChildren(v, markTableRowNodes)
sortTableRows(v)
}
}
}

Walk(f, markTableNodes)
}

// If the tags necessitate a sorting of the tabular data, then this takes care of sorting.
// TODO : Change to in-place sorting later.
func sortTableRows(v *ListExpr) {
// Handle when "#buildifier: table sort N" tag is set
colNumber := tableSort(v.List[0])
if len(v.List) > 0 && colNumber > 0 {
// less than 2 rows, nothing to sort
if len(v.List) < 2 {
return
}

var comments []Comment
keys := make([]string, 0, len(v.List))
tableMap := map[string][]*TupleExpr{}

// column number specified in tag is larger than the max number of columns
columns, ok := v.List[1].(*TupleExpr)
if !ok || colNumber > len(columns.List) {
return
}

// Iterate over the list of tuples(tablerows)
for _, row := range v.List {
tupleRow, ok := row.(*TupleExpr)
if !ok {
continue
}
// keys are the values from tuple[colNumber position]
// Table column starts at 1, where list starts at 0
str, ok := tupleRow.List[colNumber-1].(*StringExpr)
key := str.Value
keys = append(keys, key)

// Collect comments so that they're not lost.
comments = append(comments, tupleRow.Comment().Before...)

// Create a map with <key, tuple> values
tableMap[key] = append(tableMap[key], tupleRow)
}

// Store the comment so its not lost
var commentKey []Comment
for i, key := range keys {
if i == 0 {
commentKey = tableMap[key][0].Comment().Before[0:1]
tableMap[key][0].Comment().Before = tableMap[key][0].Comment().Before[1:]
}
}
// Sort the keys
sort.Strings(keys)

// Rewrite the sorted list to v.list
var sortedList []Expr
alreadySeen := make(map[string]bool)
for i, key := range keys {
if i == 0 {
tableMap[key][0].Comment().Before = commentKey
}
if _, ok := alreadySeen[key]; !ok {
for _, value := range tableMap[key] {
sortedList = append(sortedList, value)
}
alreadySeen[key] = true
}
}
v.List = sortedList
}
}


// sortStringLists sorts lists of string literals used as specific rule arguments.
func sortStringLists(f *File) {
Walk(f, func(v Expr, stk []Expr) {
Expand Down
10 changes: 6 additions & 4 deletions build/syntax.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,8 @@ func (x *Ident) asString() *StringExpr {
// An TypedIdent represents an identifier with type annotation: "foo: int".
type TypedIdent struct {
Comments
Ident *Ident
Type Expr
Ident *Ident
Type Expr
}

// Span returns the start and end positions of the node
Expand Down Expand Up @@ -445,6 +445,7 @@ type ListExpr struct {
List []Expr
End
ForceMultiLine bool // force multiline form when printing
FormatAsTable bool // flag to indicate that this list needs to print tabular data.
}

// Span returns the start and end positions of the node
Expand Down Expand Up @@ -485,8 +486,9 @@ type TupleExpr struct {
Start Position
List []Expr
End
ForceCompact bool // force compact (non-multiline) form when printing
ForceMultiLine bool // force multiline form when printing
ForceCompact bool // force compact (non-multiline) form when printing
ForceMultiLine bool // force multiline form when printing
FormatAsTableRow bool // flag that indicates that this tuple will be printed as a row of a table
}

// Span returns the start and end positions of the node
Expand Down
4 changes: 4 additions & 0 deletions build/testdata/067.build.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
manifest = [
("a", "b", "c"),
("ab", "bc", "cd"),
]
1 change: 1 addition & 0 deletions build/testdata/067.bzl.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
manifest = [("a", "b", "c"), ("ab", "bc", "cd")]
1 change: 1 addition & 0 deletions build/testdata/067.in
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
manifest = [("a", "b", "c"), ("ab", "bc", "cd"),]
6 changes: 6 additions & 0 deletions build/testdata/068.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
manifest = [
# buildifier: table sort 1
("", "", ""),
("a", "b", "c"),
("ab", "bc", "cd"),
]
6 changes: 6 additions & 0 deletions build/testdata/068.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
manifest = [
# buildifier: table sort 1
("ab", "bc", "cd"),
("a", "b", "c"),
("", "", ""),
]
6 changes: 6 additions & 0 deletions build/testdata/069.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
manifest = [
# buildifier: table
("a", "b", "c"),
("ab", "bc", "cd"),
("", "", ""),
]
3 changes: 3 additions & 0 deletions build/testdata/069.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
manifest = [
# buildifier: table
("a", "b", "c"), ("ab", "bc", "cd"), ("", "", ""),]
6 changes: 6 additions & 0 deletions build/testdata/070.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
manifest = [
# buildifier: table
("", "", ""),
("ab", "bc", "cd"),
("a", "b", "c"),
]
3 changes: 3 additions & 0 deletions build/testdata/070.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
manifest = [
# buildifier: table
("", "", ""), ("ab", "bc", "cd"),("a", "b", "c")]
6 changes: 6 additions & 0 deletions build/testdata/071.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
manifest = [
# buildifier: table
("", "", ""),
("ab", "bc", "cd"),
("a", "b", "c"),
]
Loading