fix.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. /*
  2. Copyright 2016 Google Inc. All Rights Reserved.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. // Functions to clean and fix BUILD files
  14. package edit
  15. import (
  16. "regexp"
  17. "sort"
  18. "strings"
  19. "github.com/bazelbuild/buildtools/build"
  20. )
  21. // splitOptionsWithSpaces is a cleanup function.
  22. // It splits options strings that contain a space. This change
  23. // should be safe as Blaze is splitting those strings, but we will
  24. // eventually get rid of this misfeature.
  25. // eg. it converts from:
  26. // copts = ["-Dfoo -Dbar"]
  27. // to:
  28. // copts = ["-Dfoo", "-Dbar"]
  29. func splitOptionsWithSpaces(_ *build.File, r *build.Rule, _ string) bool {
  30. var attrToRewrite = []string{
  31. "copts",
  32. "linkopts",
  33. }
  34. fixed := false
  35. for _, attrName := range attrToRewrite {
  36. attr := r.Attr(attrName)
  37. if attr != nil {
  38. for _, li := range AllLists(attr) {
  39. fixed = splitStrings(li) || fixed
  40. }
  41. }
  42. }
  43. return fixed
  44. }
  45. func splitStrings(list *build.ListExpr) bool {
  46. var all []build.Expr
  47. fixed := false
  48. for _, e := range list.List {
  49. str, ok := e.(*build.StringExpr)
  50. if !ok {
  51. all = append(all, e)
  52. continue
  53. }
  54. if strings.Contains(str.Value, " ") && !strings.Contains(str.Value, "'\"") {
  55. fixed = true
  56. for i, substr := range strings.Fields(str.Value) {
  57. item := &build.StringExpr{Value: substr}
  58. if i == 0 {
  59. item.Comments = str.Comments
  60. }
  61. all = append(all, item)
  62. }
  63. } else {
  64. all = append(all, str)
  65. }
  66. }
  67. list.List = all
  68. return fixed
  69. }
  70. // shortenLabels rewrites the labels in the rule using the short notation.
  71. func shortenLabels(_ *build.File, r *build.Rule, pkg string) bool {
  72. fixed := false
  73. for _, attr := range r.AttrKeys() {
  74. e := r.Attr(attr)
  75. if !ContainsLabels(attr) {
  76. continue
  77. }
  78. for _, li := range AllLists(e) {
  79. for _, elem := range li.List {
  80. str, ok := elem.(*build.StringExpr)
  81. if ok && str.Value != ShortenLabel(str.Value, pkg) {
  82. str.Value = ShortenLabel(str.Value, pkg)
  83. fixed = true
  84. }
  85. }
  86. }
  87. }
  88. return fixed
  89. }
  90. // removeVisibility removes useless visibility attributes.
  91. func removeVisibility(f *build.File, r *build.Rule, pkg string) bool {
  92. pkgDecl := PackageDeclaration(f)
  93. defaultVisibility := pkgDecl.AttrStrings("default_visibility")
  94. // If no default_visibility is given, it is implicitly private.
  95. if len(defaultVisibility) == 0 {
  96. defaultVisibility = []string{"//visibility:private"}
  97. }
  98. visibility := r.AttrStrings("visibility")
  99. if len(visibility) == 0 || len(visibility) != len(defaultVisibility) {
  100. return false
  101. }
  102. sort.Strings(defaultVisibility)
  103. sort.Strings(visibility)
  104. for i, vis := range visibility {
  105. if vis != defaultVisibility[i] {
  106. return false
  107. }
  108. }
  109. r.DelAttr("visibility")
  110. return true
  111. }
  112. // removeTestOnly removes the useless testonly attributes.
  113. func removeTestOnly(f *build.File, r *build.Rule, pkg string) bool {
  114. pkgDecl := PackageDeclaration(f)
  115. def := strings.HasSuffix(r.Kind(), "_test") || r.Kind() == "test_suite"
  116. if !def {
  117. if pkgDecl.Attr("default_testonly") == nil {
  118. def = strings.HasPrefix(pkg, "javatests/")
  119. } else if pkgDecl.AttrLiteral("default_testonly") == "1" {
  120. def = true
  121. } else if pkgDecl.AttrLiteral("default_testonly") != "0" {
  122. // Non-literal value: it's not safe to do a change.
  123. return false
  124. }
  125. }
  126. testonly := r.AttrLiteral("testonly")
  127. if def && testonly == "1" {
  128. r.DelAttr("testonly")
  129. return true
  130. }
  131. if !def && testonly == "0" {
  132. r.DelAttr("testonly")
  133. return true
  134. }
  135. return false
  136. }
  137. func genruleRenameDepsTools(_ *build.File, r *build.Rule, _ string) bool {
  138. return r.Kind() == "genrule" && RenameAttribute(r, "deps", "tools") == nil
  139. }
  140. // explicitHeuristicLabels adds $(location ...) for each label in the string s.
  141. func explicitHeuristicLabels(s string, labels map[string]bool) string {
  142. // Regexp comes from LABEL_CHAR_MATCHER in
  143. // java/com/google/devtools/build/lib/analysis/LabelExpander.java
  144. re := regexp.MustCompile("[a-zA-Z0-9:/_.+-]+|[^a-zA-Z0-9:/_.+-]+")
  145. parts := re.FindAllString(s, -1)
  146. changed := false
  147. canChange := true
  148. for i, part := range parts {
  149. // We don't want to add $(location when it's already present.
  150. // So we skip the next label when we see location(s).
  151. if part == "location" || part == "locations" {
  152. canChange = false
  153. }
  154. if !labels[part] {
  155. if labels[":"+part] { // leading colon is often missing
  156. part = ":" + part
  157. } else {
  158. continue
  159. }
  160. }
  161. if !canChange {
  162. canChange = true
  163. continue
  164. }
  165. parts[i] = "$(location " + part + ")"
  166. changed = true
  167. }
  168. if changed {
  169. return strings.Join(parts, "")
  170. }
  171. return s
  172. }
  173. func addLabels(r *build.Rule, attr string, labels map[string]bool) {
  174. a := r.Attr(attr)
  175. if a == nil {
  176. return
  177. }
  178. for _, li := range AllLists(a) {
  179. for _, item := range li.List {
  180. if str, ok := item.(*build.StringExpr); ok {
  181. labels[str.Value] = true
  182. }
  183. }
  184. }
  185. }
  186. // genruleFixHeuristicLabels modifies the cmd attribute of genrules, so
  187. // that they don't rely on heuristic label expansion anymore.
  188. // Label expansion is made explicit with the $(location ...) command.
  189. func genruleFixHeuristicLabels(_ *build.File, r *build.Rule, _ string) bool {
  190. if r.Kind() != "genrule" {
  191. return false
  192. }
  193. cmd := r.Attr("cmd")
  194. if cmd == nil {
  195. return false
  196. }
  197. labels := make(map[string]bool)
  198. addLabels(r, "tools", labels)
  199. addLabels(r, "srcs", labels)
  200. fixed := false
  201. for _, str := range AllStrings(cmd) {
  202. newVal := explicitHeuristicLabels(str.Value, labels)
  203. if newVal != str.Value {
  204. fixed = true
  205. str.Value = newVal
  206. }
  207. }
  208. return fixed
  209. }
  210. // sortExportsFiles sorts the first argument of exports_files if it is a list.
  211. func sortExportsFiles(_ *build.File, r *build.Rule, _ string) bool {
  212. if r.Kind() != "exports_files" || len(r.Call.List) == 0 {
  213. return false
  214. }
  215. build.SortStringList(r.Call.List[0])
  216. return true
  217. }
  218. // removeVarref replaces all varref('x') with '$(x)'.
  219. // The goal is to eventually remove varref from the build language.
  220. func removeVarref(_ *build.File, r *build.Rule, _ string) bool {
  221. fixed := false
  222. EditFunction(r.Call, "varref", func(call *build.CallExpr, stk []build.Expr) build.Expr {
  223. if len(call.List) != 1 {
  224. return nil
  225. }
  226. str, ok := (call.List[0]).(*build.StringExpr)
  227. if !ok {
  228. return nil
  229. }
  230. fixed = true
  231. str.Value = "$(" + str.Value + ")"
  232. // Preserve suffix comments from the function call
  233. str.Comment().Suffix = append(str.Comment().Suffix, call.Comment().Suffix...)
  234. return str
  235. })
  236. return fixed
  237. }
  238. // sortGlob sorts the list argument to glob.
  239. func sortGlob(_ *build.File, r *build.Rule, _ string) bool {
  240. fixed := false
  241. EditFunction(r.Call, "glob", func(call *build.CallExpr, stk []build.Expr) build.Expr {
  242. if len(call.List) == 0 {
  243. return nil
  244. }
  245. build.SortStringList(call.List[0])
  246. fixed = true
  247. return call
  248. })
  249. return fixed
  250. }
  251. func evaluateListConcatenation(expr build.Expr) build.Expr {
  252. if _, ok := expr.(*build.ListExpr); ok {
  253. return expr
  254. }
  255. bin, ok := expr.(*build.BinaryExpr)
  256. if !ok || bin.Op != "+" {
  257. return expr
  258. }
  259. li1, ok1 := evaluateListConcatenation(bin.X).(*build.ListExpr)
  260. li2, ok2 := evaluateListConcatenation(bin.Y).(*build.ListExpr)
  261. if !ok1 || !ok2 {
  262. return expr
  263. }
  264. res := *li1
  265. res.List = append(li1.List, li2.List...)
  266. return &res
  267. }
  268. // mergeLiteralLists evaluates the concatenation of two literal lists.
  269. // e.g. [1, 2] + [3, 4] -> [1, 2, 3, 4]
  270. func mergeLiteralLists(_ *build.File, r *build.Rule, _ string) bool {
  271. fixed := false
  272. build.Edit(r.Call, func(expr build.Expr, stk []build.Expr) build.Expr {
  273. newexpr := evaluateListConcatenation(expr)
  274. fixed = fixed || (newexpr != expr)
  275. return newexpr
  276. })
  277. return fixed
  278. }
  279. // usePlusEqual replaces uses of extend and append with the += operator.
  280. // e.g. foo.extend(bar) => foo += bar
  281. // foo.append(bar) => foo += [bar]
  282. func usePlusEqual(f *build.File) bool {
  283. fixed := false
  284. for i, stmt := range f.Stmt {
  285. call, ok := stmt.(*build.CallExpr)
  286. if !ok {
  287. continue
  288. }
  289. dot, ok := call.X.(*build.DotExpr)
  290. if !ok || len(call.List) != 1 {
  291. continue
  292. }
  293. obj, ok := dot.X.(*build.LiteralExpr)
  294. if !ok {
  295. continue
  296. }
  297. var fix *build.BinaryExpr
  298. if dot.Name == "extend" {
  299. fix = &build.BinaryExpr{X: obj, Op: "+=", Y: call.List[0]}
  300. } else if dot.Name == "append" {
  301. list := &build.ListExpr{List: []build.Expr{call.List[0]}}
  302. fix = &build.BinaryExpr{X: obj, Op: "+=", Y: list}
  303. } else {
  304. continue
  305. }
  306. fix.Comments = call.Comments // Keep original comments
  307. f.Stmt[i] = fix
  308. fixed = true
  309. }
  310. return fixed
  311. }
  312. func isNonemptyComment(comment *build.Comments) bool {
  313. return len(comment.Before)+len(comment.Suffix)+len(comment.After) > 0
  314. }
  315. // Checks whether a call or any of its arguments have a comment
  316. func hasComment(call *build.CallExpr) bool {
  317. if isNonemptyComment(call.Comment()) {
  318. return true
  319. }
  320. for _, arg := range call.List {
  321. if isNonemptyComment(arg.Comment()) {
  322. return true
  323. }
  324. }
  325. return false
  326. }
  327. // cleanUnusedLoads removes symbols from load statements that are not used in the file.
  328. // It also cleans symbols loaded multiple times, sorts symbol list, and removes load
  329. // statements when the list is empty.
  330. func cleanUnusedLoads(f *build.File) bool {
  331. // If the file needs preprocessing, leave it alone.
  332. for _, stmt := range f.Stmt {
  333. if _, ok := stmt.(*build.PythonBlock); ok {
  334. return false
  335. }
  336. }
  337. symbols := UsedSymbols(f)
  338. fixed := false
  339. var all []build.Expr
  340. for _, stmt := range f.Stmt {
  341. rule, ok := ExprToRule(stmt, "load")
  342. if !ok || len(rule.Call.List) == 0 || hasComment(rule.Call) {
  343. all = append(all, stmt)
  344. continue
  345. }
  346. var args []build.Expr
  347. for _, arg := range rule.Call.List[1:] { // first argument is the path, we keep it
  348. symbol, ok := loadedSymbol(arg)
  349. if !ok || symbols[symbol] {
  350. args = append(args, arg)
  351. if ok {
  352. // If the same symbol is loaded twice, we'll remove it.
  353. delete(symbols, symbol)
  354. }
  355. } else {
  356. fixed = true
  357. }
  358. }
  359. if len(args) > 0 { // Keep the load statement if it loads at least one symbol.
  360. li := &build.ListExpr{List: args}
  361. build.SortStringList(li)
  362. rule.Call.List = append(rule.Call.List[:1], li.List...)
  363. all = append(all, rule.Call)
  364. } else {
  365. fixed = true
  366. }
  367. }
  368. f.Stmt = all
  369. return fixed
  370. }
  371. // loadedSymbol parses the symbol token from a load statement argument,
  372. // supporting aliases.
  373. func loadedSymbol(arg build.Expr) (string, bool) {
  374. symbol, ok := arg.(*build.StringExpr)
  375. if ok {
  376. return symbol.Value, ok
  377. }
  378. // try an aliased symbol
  379. if binExpr, ok := arg.(*build.BinaryExpr); ok && binExpr.Op == "=" {
  380. if keyExpr, ok := binExpr.X.(*build.LiteralExpr); ok {
  381. return keyExpr.Token, ok
  382. }
  383. }
  384. return "", false
  385. }
  386. // movePackageDeclarationToTheTop ensures that the call to package() is done
  387. // before everything else (except comments).
  388. func movePackageDeclarationToTheTop(f *build.File) bool {
  389. pkg := ExistingPackageDeclaration(f)
  390. if pkg == nil {
  391. return false
  392. }
  393. all := []build.Expr{}
  394. inserted := false // true when the package declaration has been inserted
  395. for _, stmt := range f.Stmt {
  396. _, isComment := stmt.(*build.CommentBlock)
  397. _, isBinaryExpr := stmt.(*build.BinaryExpr) // e.g. variable declaration
  398. _, isLoad := ExprToRule(stmt, "load")
  399. if isComment || isBinaryExpr || isLoad {
  400. all = append(all, stmt)
  401. continue
  402. }
  403. if stmt == pkg.Call {
  404. if inserted {
  405. // remove the old package
  406. continue
  407. }
  408. return false // the file was ok
  409. }
  410. if !inserted {
  411. all = append(all, pkg.Call)
  412. inserted = true
  413. }
  414. all = append(all, stmt)
  415. }
  416. f.Stmt = all
  417. return true
  418. }
  419. // moveToPackage is an auxilliary function used by moveLicensesAndDistribs.
  420. // The function shouldn't appear more than once in the file (depot cleanup has
  421. // been done).
  422. func moveToPackage(f *build.File, attrname string) bool {
  423. var all []build.Expr
  424. fixed := false
  425. for _, stmt := range f.Stmt {
  426. rule, ok := ExprToRule(stmt, attrname)
  427. if !ok || len(rule.Call.List) != 1 {
  428. all = append(all, stmt)
  429. continue
  430. }
  431. pkgDecl := PackageDeclaration(f)
  432. pkgDecl.SetAttr(attrname, rule.Call.List[0])
  433. pkgDecl.AttrDefn(attrname).Comments = *stmt.Comment()
  434. fixed = true
  435. }
  436. f.Stmt = all
  437. return fixed
  438. }
  439. // moveLicensesAndDistribs replaces the 'licenses' and 'distribs' functions
  440. // with an attribute in package.
  441. // Before: licenses(["notice"])
  442. // After: package(licenses = ["notice"])
  443. func moveLicensesAndDistribs(f *build.File) bool {
  444. fixed1 := moveToPackage(f, "licenses")
  445. fixed2 := moveToPackage(f, "distribs")
  446. return fixed1 || fixed2
  447. }
  448. // AllRuleFixes is a list of all Buildozer fixes that can be applied on a rule.
  449. var AllRuleFixes = []struct {
  450. Name string
  451. Fn func(file *build.File, rule *build.Rule, pkg string) bool
  452. Message string
  453. }{
  454. {"sortGlob", sortGlob,
  455. "Sort the list in a call to glob"},
  456. {"splitOptions", splitOptionsWithSpaces,
  457. "Each option should be given separately in the list"},
  458. {"shortenLabels", shortenLabels,
  459. "Style: Use the canonical label notation"},
  460. {"removeVisibility", removeVisibility,
  461. "This visibility attribute is useless (it corresponds to the default value)"},
  462. {"removeTestOnly", removeTestOnly,
  463. "This testonly attribute is useless (it corresponds to the default value)"},
  464. {"genruleRenameDepsTools", genruleRenameDepsTools,
  465. "'deps' attribute in genrule has been renamed 'tools'"},
  466. {"genruleFixHeuristicLabels", genruleFixHeuristicLabels,
  467. "$(location) should be called explicitely"},
  468. {"sortExportsFiles", sortExportsFiles,
  469. "Files in exports_files should be sorted"},
  470. {"varref", removeVarref,
  471. "All varref('foo') should be replaced with '$foo'"},
  472. {"mergeLiteralLists", mergeLiteralLists,
  473. "Remove useless list concatenation"},
  474. }
  475. // FileLevelFixes is a list of all Buildozer fixes that apply on the whole file.
  476. var FileLevelFixes = []struct {
  477. Name string
  478. Fn func(file *build.File) bool
  479. Message string
  480. }{
  481. {"movePackageToTop", movePackageDeclarationToTheTop,
  482. "The package declaration should be the first rule in a file"},
  483. {"usePlusEqual", usePlusEqual,
  484. "Prefer '+=' over 'extend' or 'append'"},
  485. {"unusedLoads", cleanUnusedLoads,
  486. "Remove unused symbols from load statements"},
  487. {"moveLicensesAndDistribs", moveLicensesAndDistribs,
  488. "Move licenses and distribs to the package function"},
  489. }
  490. // FixRule aims to fix errors in BUILD files, remove deprecated features, and
  491. // simplify the code.
  492. func FixRule(f *build.File, pkg string, rule *build.Rule, fixes []string) *build.File {
  493. fixesAsMap := make(map[string]bool)
  494. for _, fix := range fixes {
  495. fixesAsMap[fix] = true
  496. }
  497. fixed := false
  498. for _, fix := range AllRuleFixes {
  499. if len(fixes) == 0 || fixesAsMap[fix.Name] {
  500. fixed = fix.Fn(f, rule, pkg) || fixed
  501. }
  502. }
  503. if !fixed {
  504. return nil
  505. }
  506. return f
  507. }
  508. // FixFile fixes everything it can in the BUILD file.
  509. func FixFile(f *build.File, pkg string, fixes []string) *build.File {
  510. fixesAsMap := make(map[string]bool)
  511. for _, fix := range fixes {
  512. fixesAsMap[fix] = true
  513. }
  514. fixed := false
  515. for _, rule := range f.Rules("") {
  516. res := FixRule(f, pkg, rule, fixes)
  517. if res != nil {
  518. fixed = true
  519. f = res
  520. }
  521. }
  522. for _, fix := range FileLevelFixes {
  523. if len(fixes) == 0 || fixesAsMap[fix.Name] {
  524. fixed = fix.Fn(f) || fixed
  525. }
  526. }
  527. if !fixed {
  528. return nil
  529. }
  530. return f
  531. }