@@ -26,6 +26,7 @@ import (
26
26
"io"
27
27
"log"
28
28
"os"
29
+ "path"
29
30
"path/filepath"
30
31
"regexp"
31
32
"runtime"
@@ -1183,11 +1184,9 @@ func rewrite(opts *Options, commandsForFile commandsForFile) *rewriteResult {
1183
1184
}
1184
1185
var errs []error
1185
1186
changed := false
1186
- for _ , commands := range commandsForFile .commands {
1187
- target := commands .target
1188
- commands := commands .commands
1189
- _ , _ , absPkg , rule := InterpretLabelForWorkspaceLocation (opts .RootDir , target )
1190
- if label := labels .Parse (target ); label .Package == stdinPackageName {
1187
+ for _ , cft := range commandsForFile .commands {
1188
+ _ , _ , absPkg , rule := InterpretLabelForWorkspaceLocation (opts .RootDir , cft .target )
1189
+ if label := labels .Parse (cft .target ); label .Package == stdinPackageName {
1191
1190
// Special-case: This is already absolute
1192
1191
absPkg = stdinPackageName
1193
1192
}
@@ -1198,48 +1197,27 @@ func rewrite(opts *Options, commandsForFile commandsForFile) *rewriteResult {
1198
1197
1199
1198
targets , err := expandTargets (f , rule )
1200
1199
if err != nil {
1201
- cerr := commandError (commands , target , err )
1200
+ cerr := commandError (cft . commands , cft . target , err )
1202
1201
errs = append (errs , cerr )
1203
1202
if ! opts .KeepGoing {
1204
1203
return & rewriteResult {file : name , errs : errs , records : records }
1205
1204
}
1206
1205
}
1207
1206
targets = filterRules (opts , targets )
1208
- for _ , cmd := range commands {
1209
- cmdInfo := AllCommands [cmd .tokens [0 ]]
1210
- // Depending on whether a transformation is rule-specific or not, it should be applied to
1211
- // every rule that satisfies the filter or just once to the file.
1212
- cmdTargets := targets
1213
- if ! cmdInfo .PerRule {
1214
- cmdTargets = []* build.Rule {nil }
1215
- }
1216
- for _ , r := range cmdTargets {
1217
- record := & apipb.Output_Record {}
1218
- newf , err := cmdInfo .Fn (opts , CmdEnvironment {f , r , vars , absPkg , cmd .tokens [1 :], record })
1219
- if len (record .Fields ) != 0 {
1220
- records = append (records , record )
1221
- }
1222
- if err != nil {
1223
- cerr := commandError ([]command {cmd }, target , err )
1224
- if opts .KeepGoing {
1225
- errs = append (errs , cerr )
1226
- } else {
1227
- return & rewriteResult {file : name , errs : []error {cerr }, records : records }
1228
- }
1229
- }
1230
- if newf != nil {
1231
- changed = true
1232
- f = newf
1233
- }
1234
- }
1207
+
1208
+ newf , err := executeCommandsInFile (opts , f , cft , targets , & records , vars , absPkg , & errs )
1209
+ if err != nil {
1210
+ return & rewriteResult {file : name , errs : []error {err }, records : records }
1211
+ }
1212
+ if newf != nil {
1213
+ changed = true
1214
+ f = newf
1235
1215
}
1236
1216
}
1237
1217
if ! changed {
1238
1218
return & rewriteResult {file : name , errs : errs , records : records }
1239
1219
}
1240
- f = RemoveEmptyPackage (f )
1241
- f = RemoveEmptyUseRepoCalls (f )
1242
- ndata , err := buildifier .Buildify (opts , f )
1220
+ ndata , err := cleanAndBuildify (opts , f )
1243
1221
if err != nil {
1244
1222
return & rewriteResult {file : name , errs : []error {fmt .Errorf ("running buildifier: %v" , err )}, records : records }
1245
1223
}
@@ -1264,6 +1242,58 @@ func rewrite(opts *Options, commandsForFile commandsForFile) *rewriteResult {
1264
1242
return & rewriteResult {file : name , errs : errs , modified : true , records : records }
1265
1243
}
1266
1244
1245
+ // executeCommandsInFile executes the provided commandsForTarget in the provided build.File.
1246
+ func executeCommandsInFile (
1247
+ opts * Options ,
1248
+ f * build.File ,
1249
+ cft commandsForTarget ,
1250
+ rules []* build.Rule ,
1251
+ records * []* apipb.Output_Record ,
1252
+ vars map [string ]* build.AssignExpr ,
1253
+ absPkg string ,
1254
+ errs * []error ,
1255
+ ) (* build.File , error ) {
1256
+ changed := false
1257
+ for _ , cmd := range cft .commands {
1258
+ cmdInfo := AllCommands [cmd .tokens [0 ]]
1259
+ // Depending on whether a transformation is rule-specific or not, it should be applied to
1260
+ // every rule that satisfies the filter or just once to the file.
1261
+ cmdTargets := rules
1262
+ if ! cmdInfo .PerRule {
1263
+ cmdTargets = []* build.Rule {nil }
1264
+ }
1265
+ for _ , r := range cmdTargets {
1266
+ record := & apipb.Output_Record {}
1267
+ newf , err := cmdInfo .Fn (opts , CmdEnvironment {f , r , vars , absPkg , cmd .tokens [1 :], record })
1268
+ if len (record .Fields ) != 0 {
1269
+ * records = append (* records , record )
1270
+ }
1271
+ if err != nil {
1272
+ cerr := commandError ([]command {cmd }, cft .target , err )
1273
+ if opts .KeepGoing {
1274
+ * errs = append (* errs , cerr )
1275
+ } else {
1276
+ return nil , cerr
1277
+ }
1278
+ }
1279
+ if newf != nil {
1280
+ f = newf
1281
+ changed = true
1282
+ }
1283
+ }
1284
+ }
1285
+ if changed {
1286
+ return f , nil
1287
+ }
1288
+ return nil , nil
1289
+ }
1290
+
1291
+ func cleanAndBuildify (opts * Options , f * build.File ) ([]byte , error ) {
1292
+ f = RemoveEmptyPackage (f )
1293
+ f = RemoveEmptyUseRepoCalls (f )
1294
+ return buildifier .Buildify (opts , f )
1295
+ }
1296
+
1267
1297
// EditFile is a function that does any prework needed before editing a file.
1268
1298
// e.g. "checking out for write" from a locking source control repo.
1269
1299
var EditFile = func (fi os.FileInfo , name string ) error {
@@ -1547,3 +1577,82 @@ func Buildozer(opts *Options, args []string) int {
1547
1577
}
1548
1578
return 0
1549
1579
}
1580
+
1581
+ // ExecuteCommandsOnInlineFile executes the given commands on the given file content.
1582
+ // Returns the new file content after applying the commands.
1583
+ func ExecuteCommandsOnInlineFile (fileContent []byte , commands []string ) ([]byte , error ) {
1584
+ opts := Options {}
1585
+ commandsByTargetName , filename , err := groupCommandsForInlineFile (commands , opts )
1586
+ if err != nil {
1587
+ return nil , err
1588
+ }
1589
+ f , err := build .Parse (* filename , fileContent )
1590
+ if err != nil {
1591
+ return nil , err
1592
+ }
1593
+ if f .Type == build .TypeDefault {
1594
+ // Buildozer is unable to infer the file type, fall back to BUILD by default.
1595
+ f .Type = build .TypeBuild
1596
+ }
1597
+ for _ , cft := range commandsByTargetName {
1598
+ rules , err := expandTargets (f , cft .target )
1599
+ if err != nil {
1600
+ return nil , err
1601
+ }
1602
+ newf , err := executeCommandsInFile (
1603
+ & opts ,
1604
+ f ,
1605
+ cft ,
1606
+ rules ,
1607
+ // Output records are ignored in inline file execution.
1608
+ nil ,
1609
+ // Global variables not supported in inline file execution.
1610
+ nil ,
1611
+ f .Pkg ,
1612
+ // Errors-list is ignored since opts.keepGoing is always false.
1613
+ nil ,
1614
+ )
1615
+ if err != nil {
1616
+ return nil , err
1617
+ }
1618
+ if newf != nil {
1619
+ f = newf
1620
+ }
1621
+ }
1622
+ outputFileContent , err := cleanAndBuildify (& opts , f )
1623
+ if err != nil {
1624
+ return nil , err
1625
+ }
1626
+ return outputFileContent , nil
1627
+ }
1628
+
1629
+ // groupCommandsForInlineFile groups the given commands by file and returns the commands for a
1630
+ // single file. Returns an error if the commands modify multiple files or if commands are invalid.
1631
+ func groupCommandsForInlineFile (commands []string , opts Options ) ([]commandsForTarget , * string , error ) {
1632
+ commandsByFile := make (map [string ][]commandsForTarget )
1633
+ commandReader := strings .NewReader (strings .Join (commands , "\n " ))
1634
+ err := appendCommandsFromReader (& opts , commandReader , commandsByFile , nil )
1635
+ if err != nil {
1636
+ return nil , nil , fmt .Errorf ("error parsing commands %s" , err )
1637
+ }
1638
+ if len (commandsByFile ) != 1 {
1639
+ return nil , nil , fmt .Errorf ("invalid input commands, expected all commands to reference a single file" )
1640
+ }
1641
+ for filepath , commandsForTargets := range commandsByFile {
1642
+ for i := range commandsForTargets {
1643
+ cft := & commandsForTargets [i ]
1644
+ splitTarget := strings .Split (cft .target , ":" )
1645
+ switch len (splitTarget ) {
1646
+ case 1 : // No-op
1647
+ case 2 :
1648
+ // Only keeps target (what is after the ":" character) since path is redundant.
1649
+ cft .target = splitTarget [1 ]
1650
+ default :
1651
+ return nil , nil , fmt .Errorf ("invalid target name %q" , cft .target )
1652
+ }
1653
+ }
1654
+ _ , filename := path .Split (filepath )
1655
+ return commandsForTargets , & filename , nil
1656
+ }
1657
+ panic ("unreachable" )
1658
+ }
0 commit comments