github.com/bazelbuild/bazel-gazelle@v0.36.1-0.20240520142334-61b277ba6fed/rule/rule_test.go (about)

     1  /* Copyright 2018 The Bazel Authors. All rights reserved.
     2  
     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  
     7     http://www.apache.org/licenses/LICENSE-2.0
     8  
     9  Unless required by applicable law or agreed to in writing, software
    10  distributed under the License is distributed on an "AS IS" BASIS,
    11  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  See the License for the specific language governing permissions and
    13  limitations under the License.
    14  */
    15  
    16  package rule
    17  
    18  import (
    19  	"path/filepath"
    20  	"reflect"
    21  	"sort"
    22  	"strings"
    23  	"testing"
    24  
    25  	bzl "github.com/bazelbuild/buildtools/build"
    26  )
    27  
    28  // This file contains tests for File, Load, Rule, and related functions.
    29  // Tests only cover some basic functionality and a few non-obvious cases.
    30  // Most test coverage will come from clients of this package.
    31  
    32  func TestEditAndSync(t *testing.T) {
    33  	old := []byte(`
    34  load("a.bzl", "x_library")
    35  
    36  x_library(name = "foo")
    37  
    38  load("b.bzl", y_library = "y")
    39  
    40  y_library(name = "bar")
    41  `)
    42  	f, err := LoadData(filepath.Join("old", "BUILD.bazel"), "", old)
    43  	if err != nil {
    44  		t.Fatal(err)
    45  	}
    46  
    47  	loadA := f.Loads[0]
    48  	loadA.Delete()
    49  	loadB := f.Loads[1]
    50  	loadB.Add("x_library")
    51  	loadB.Remove("y_library")
    52  	loadC := NewLoad("c.bzl")
    53  	loadC.AddAlias("z_library", "z")
    54  	loadC.Add("y_library")
    55  	loadC.Insert(f, 3)
    56  
    57  	foo := f.Rules[0]
    58  	foo.Delete()
    59  	bar := f.Rules[1]
    60  	bar.SetAttr("srcs", []string{"bar.y"})
    61  	loadMaybe := NewLoad("//some:maybe.bzl")
    62  	loadMaybe.Add("maybe")
    63  	loadMaybe.Insert(f, 0)
    64  	baz := NewRule("maybe", "baz")
    65  	baz.AddArg(&bzl.LiteralExpr{Token: "z"})
    66  	baz.AddArg(&bzl.LiteralExpr{Token: "z"})
    67  	if err := baz.UpdateArg(0, &bzl.LiteralExpr{Token: "z0"}); err != nil {
    68  		t.Fatal(err)
    69  	}
    70  	if err := baz.UpdateArg(10, &bzl.LiteralExpr{Token: "blah"}); err == nil {
    71  		t.Fatalf("want error because tried to modify an arg outside of arg bounds, got nil")
    72  	}
    73  	baz.SetAttr("srcs", GlobValue{
    74  		Patterns: []string{"**"},
    75  		Excludes: []string{"*.pem"},
    76  	})
    77  	baz.Insert(f)
    78  
    79  	got := strings.TrimSpace(string(f.Format()))
    80  	want := strings.TrimSpace(`
    81  load("//some:maybe.bzl", "maybe")
    82  load("b.bzl", "x_library")
    83  load(
    84      "c.bzl",
    85      "y_library",
    86      z = "z_library",
    87  )
    88  
    89  y_library(
    90      name = "bar",
    91      srcs = ["bar.y"],
    92  )
    93  
    94  maybe(
    95      z0,
    96      z,
    97      name = "baz",
    98      srcs = glob(
    99          ["**"],
   100          exclude = ["*.pem"],
   101      ),
   102  )
   103  `)
   104  	if got != want {
   105  		t.Errorf("got:\n%s\nwant:\n%s", got, want)
   106  	}
   107  }
   108  
   109  func TestPassInserted(t *testing.T) {
   110  	old := []byte(`
   111  load("a.bzl", "baz")
   112  
   113  def foo():
   114      go_repository(name = "bar")
   115  `)
   116  	f, err := LoadMacroData(filepath.Join("old", "repo.bzl"), "", "foo", old)
   117  	if err != nil {
   118  		t.Fatal(err)
   119  	}
   120  
   121  	f.Rules[0].Delete()
   122  	f.Sync()
   123  	got := strings.TrimSpace(string(f.Format()))
   124  	want := strings.TrimSpace(`
   125  load("a.bzl", "baz")
   126  
   127  def foo():
   128      pass
   129  `)
   130  
   131  	if got != want {
   132  		t.Errorf("got:\n%s\nwant:%s", got, want)
   133  	}
   134  }
   135  
   136  func TestPassRemoved(t *testing.T) {
   137  	old := []byte(`
   138  load("a.bzl", "baz")
   139  
   140  def foo():
   141      pass
   142  `)
   143  	f, err := LoadMacroData(filepath.Join("old", "repo.bzl"), "", "foo", old)
   144  	if err != nil {
   145  		t.Fatal(err)
   146  	}
   147  
   148  	bar := NewRule("go_repository", "bar")
   149  	bar.Insert(f)
   150  	f.Sync()
   151  	got := strings.TrimSpace(string(f.Format()))
   152  	want := strings.TrimSpace(`
   153  load("a.bzl", "baz")
   154  
   155  def foo():
   156      go_repository(name = "bar")
   157  `)
   158  
   159  	if got != want {
   160  		t.Errorf("got:\n%s\nwant:%s", got, want)
   161  	}
   162  }
   163  
   164  func TestFunctionInserted(t *testing.T) {
   165  	f, err := LoadMacroData(filepath.Join("old", "repo.bzl"), "", "foo", nil)
   166  	if err != nil {
   167  		t.Fatal(err)
   168  	}
   169  
   170  	bar := NewRule("go_repository", "bar")
   171  	bar.Insert(f)
   172  	f.Sync()
   173  	got := strings.TrimSpace(string(f.Format()))
   174  	want := strings.TrimSpace(`
   175  def foo():
   176      go_repository(name = "bar")
   177  `)
   178  
   179  	if got != want {
   180  		t.Errorf("got:\n%s\nwant:%s", got, want)
   181  	}
   182  }
   183  
   184  func TestArgsAlwaysEndUpBeforeKwargs(t *testing.T) {
   185  	f, err := LoadData(filepath.Join("old", "BUILD.bazel"), "", nil)
   186  	if err != nil {
   187  		t.Fatal(err)
   188  	}
   189  
   190  	bar := NewRule("maybe", "bar")
   191  	bar.SetAttr("url", "https://doesnotexist.com")
   192  	bar.AddArg(&bzl.Ident{Name: "http_archive"})
   193  	bar.Insert(f)
   194  	f.Sync()
   195  	got := strings.TrimSpace(string(f.Format()))
   196  	want := strings.TrimSpace(`
   197  maybe(
   198      http_archive,
   199      name = "bar",
   200      url = "https://doesnotexist.com",
   201  )
   202  `)
   203  
   204  	if got != want {
   205  		t.Errorf("got:\n%s\nwant:%s", got, want)
   206  	}
   207  }
   208  
   209  func TestDeleteSyncDelete(t *testing.T) {
   210  	old := []byte(`
   211  x_library(name = "foo")
   212  
   213  # comment
   214  
   215  x_library(name = "bar")
   216  `)
   217  	f, err := LoadData(filepath.Join("old", "BUILD.bazel"), "", old)
   218  	if err != nil {
   219  		t.Fatal(err)
   220  	}
   221  
   222  	foo := f.Rules[0]
   223  	bar := f.Rules[1]
   224  	foo.Delete()
   225  	f.Sync()
   226  	bar.Delete()
   227  	f.Sync()
   228  	got := strings.TrimSpace(string(f.Format()))
   229  	want := strings.TrimSpace(`# comment`)
   230  	if got != want {
   231  		t.Errorf("got:\n%s\nwant:%s", got, want)
   232  	}
   233  }
   234  
   235  func TestInsertDeleteSync(t *testing.T) {
   236  	old := []byte("")
   237  	f, err := LoadData(filepath.Join("old", "BUILD.bazel"), "", old)
   238  	if err != nil {
   239  		t.Fatal(err)
   240  	}
   241  
   242  	foo := NewRule("filegroup", "test")
   243  	foo.Insert(f)
   244  	foo.Delete()
   245  	f.Sync()
   246  	got := strings.TrimSpace(string(f.Format()))
   247  	want := ""
   248  	if got != want {
   249  		t.Errorf("got:\n%s\nwant:%s", got, want)
   250  	}
   251  }
   252  
   253  func TestSymbolsReturnsKeys(t *testing.T) {
   254  	f, err := LoadData(filepath.Join("load", "BUILD.bazel"), "", []byte(`load("a.bzl", "y", z = "a")`))
   255  	if err != nil {
   256  		t.Fatal(err)
   257  	}
   258  	got := f.Loads[0].Symbols()
   259  	want := []string{"y", "z"}
   260  	if !reflect.DeepEqual(got, want) {
   261  		t.Errorf("got %#v; want %#v", got, want)
   262  	}
   263  }
   264  
   265  func TestLoadCommentsAreRetained(t *testing.T) {
   266  	f, err := LoadData(filepath.Join("load", "BUILD.bazel"), "", []byte(`
   267  load(
   268      "a.bzl",
   269      # Comment for a symbol that will be deleted.
   270      "baz",
   271      # Some comment without remapping.
   272      "foo",
   273      # Some comment with remapping.
   274      my_bar = "bar",
   275  )
   276  `))
   277  	if err != nil {
   278  		t.Fatal(err)
   279  	}
   280  	l := f.Loads[0]
   281  	l.Remove("baz")
   282  	f.Sync()
   283  	l.Add("new_baz")
   284  	f.Sync()
   285  
   286  	got := strings.TrimSpace(string(f.Format()))
   287  	want := strings.TrimSpace(`
   288  load(
   289      "a.bzl",
   290      # Some comment without remapping.
   291      "foo",
   292      "new_baz",
   293      # Some comment with remapping.
   294      my_bar = "bar",
   295  )
   296  `)
   297  
   298  	if got != want {
   299  		t.Errorf("got:\n%s\nwant:%s", got, want)
   300  	}
   301  }
   302  
   303  func TestKeepRule(t *testing.T) {
   304  	for _, tc := range []struct {
   305  		desc, src string
   306  		want      bool
   307  	}{
   308  		{
   309  			desc: "prefix",
   310  			src: `
   311  # keep
   312  x_library(name = "x")
   313  `,
   314  			want: true,
   315  		}, {
   316  			desc: "prefix with description",
   317  			src: `
   318  # keep: hack, see more in ticket #42
   319  x_library(name = "x")
   320  `,
   321  			want: true,
   322  		}, {
   323  			desc: "compact_suffix",
   324  			src: `
   325  x_library(name = "x") # keep
   326  `,
   327  			want: true,
   328  		}, {
   329  			desc: "multiline_internal",
   330  			src: `
   331  x_library( # keep
   332      name = "x",
   333  )
   334  `,
   335  			want: false,
   336  		}, {
   337  			desc: "multiline_suffix",
   338  			src: `
   339  x_library(
   340      name = "x",
   341  ) # keep
   342  `,
   343  			want: true,
   344  		}, {
   345  			desc: "after",
   346  			src: `
   347  x_library(name = "x")
   348  # keep
   349  `,
   350  			want: false,
   351  		},
   352  	} {
   353  		t.Run(tc.desc, func(t *testing.T) {
   354  			f, err := LoadData(filepath.Join(tc.desc, "BUILD.bazel"), "", []byte(tc.src))
   355  			if err != nil {
   356  				t.Fatal(err)
   357  			}
   358  			if got := f.Rules[0].ShouldKeep(); got != tc.want {
   359  				t.Errorf("got %v; want %v", got, tc.want)
   360  			}
   361  		})
   362  	}
   363  }
   364  
   365  func TestShouldKeepExpr(t *testing.T) {
   366  	for _, tc := range []struct {
   367  		desc, src string
   368  		path      func(e bzl.Expr) bzl.Expr
   369  		want      bool
   370  	}{
   371  		{
   372  			desc: "before",
   373  			src: `
   374  # keep
   375  "s"
   376  `,
   377  			want: true,
   378  		}, {
   379  			desc: "before with description",
   380  			src: `
   381  # keep: we need it for the ninja feature
   382  "s"
   383  `,
   384  			want: true,
   385  		}, {
   386  			desc: "before but not the correct prefix (keeping)",
   387  			src: `
   388  			# keeping this for now
   389  "s"
   390  `,
   391  			want: false,
   392  		}, {
   393  			desc: "before but not the correct prefix (no colon)",
   394  			src: `
   395  			# keep this around for the time being
   396  "s"
   397  `,
   398  			want: false,
   399  		}, {
   400  			desc: "suffix",
   401  			src: `
   402  "s" # keep
   403  `,
   404  			want: true,
   405  		}, {
   406  			desc: "after",
   407  			src: `
   408  "s"
   409  # keep
   410  `,
   411  			want: false,
   412  		}, {
   413  			desc: "list_elem_prefix",
   414  			src: `
   415  [
   416      # keep
   417      "s",
   418  ]
   419  `,
   420  			path: func(e bzl.Expr) bzl.Expr { return e.(*bzl.ListExpr).List[0] },
   421  			want: true,
   422  		}, {
   423  			desc: "list_elem_suffix",
   424  			src: `
   425  [
   426      "s", # keep
   427  ]
   428  `,
   429  			path: func(e bzl.Expr) bzl.Expr { return e.(*bzl.ListExpr).List[0] },
   430  			want: true,
   431  		},
   432  	} {
   433  		t.Run(tc.desc, func(t *testing.T) {
   434  			ast, err := bzl.Parse(tc.desc, []byte(tc.src))
   435  			if err != nil {
   436  				t.Fatal(err)
   437  			}
   438  			expr := ast.Stmt[0]
   439  			if tc.path != nil {
   440  				expr = tc.path(expr)
   441  			}
   442  			got := ShouldKeep(expr)
   443  			if got != tc.want {
   444  				t.Errorf("got %v; want %v", got, tc.want)
   445  			}
   446  		})
   447  	}
   448  }
   449  
   450  func TestInternalVisibility(t *testing.T) {
   451  	tests := []struct {
   452  		rel      string
   453  		expected string
   454  	}{
   455  		{rel: "internal", expected: "//:__subpackages__"},
   456  		{rel: "a/b/internal", expected: "//a/b:__subpackages__"},
   457  		{rel: "a/b/internal/c", expected: "//a/b:__subpackages__"},
   458  		{rel: "a/b/internal/c/d", expected: "//a/b:__subpackages__"},
   459  		{rel: "a/b/internal/c/internal", expected: "//a/b/internal/c:__subpackages__"},
   460  		{rel: "a/b/internal/c/internal/d", expected: "//a/b/internal/c:__subpackages__"},
   461  	}
   462  
   463  	for _, tt := range tests {
   464  		if actual := CheckInternalVisibility(tt.rel, "default"); actual != tt.expected {
   465  			t.Errorf("got %v; want %v", actual, tt.expected)
   466  		}
   467  	}
   468  }
   469  
   470  func TestSortLoadsByName(t *testing.T) {
   471  	f, err := LoadMacroData(
   472  		filepath.Join("third_party", "repos.bzl"),
   473  		"", "repos",
   474  		[]byte(`load("@bazel_gazelle//:deps.bzl", "go_repository")
   475  load("//some:maybe.bzl", "maybe")
   476  load("//some2:maybe.bzl", "maybe2")
   477  load("//some1:maybe4.bzl", "maybe1")
   478  load("b.bzl", "x_library")
   479  def repos():
   480      go_repository(
   481          name = "com_github_bazelbuild_bazel_gazelle",
   482      )
   483  `))
   484  	if err != nil {
   485  		t.Error(err)
   486  	}
   487  	sort.Stable(loadsByName{
   488  		loads: f.Loads,
   489  		exprs: f.File.Stmt,
   490  	})
   491  	f.Sync()
   492  
   493  	got := strings.TrimSpace(string(f.Format()))
   494  	want := strings.TrimSpace(`
   495  load("@bazel_gazelle//:deps.bzl", "go_repository")
   496  load("//some:maybe.bzl", "maybe")
   497  load("//some1:maybe4.bzl", "maybe1")
   498  load("//some2:maybe.bzl", "maybe2")
   499  load("b.bzl", "x_library")
   500  
   501  def repos():
   502      go_repository(
   503          name = "com_github_bazelbuild_bazel_gazelle",
   504      )
   505  `)
   506  
   507  	if got != want {
   508  		t.Errorf("got:\n%s\nwant:%s", got, want)
   509  	}
   510  }
   511  
   512  func TestSortRulesByKindAndName(t *testing.T) {
   513  	f, err := LoadMacroData(
   514  		filepath.Join("third_party", "repos.bzl"),
   515  		"", "repos",
   516  		[]byte(`load("@bazel_gazelle//:deps.bzl", "go_repository")
   517  def repos():
   518      some_other_call_rule()
   519      go_repository(
   520          name = "com_github_andybalholm_cascadia",
   521      )
   522      go_repository(
   523          name = "com_github_bazelbuild_buildtools",
   524      )
   525      go_repository(
   526          name = "com_github_bazelbuild_rules_go",
   527      )
   528      go_repository(
   529          name = "com_github_bazelbuild_bazel_gazelle",
   530      )
   531      a_rule(
   532          name = "name1",
   533      )
   534  `))
   535  	if err != nil {
   536  		t.Error(err)
   537  	}
   538  	sort.Stable(rulesByKindAndName{
   539  		rules: f.Rules,
   540  		exprs: f.function.stmt.Body,
   541  	})
   542  	repos := []string{
   543  		"name1",
   544  		"com_github_andybalholm_cascadia",
   545  		"com_github_bazelbuild_bazel_gazelle",
   546  		"com_github_bazelbuild_buildtools",
   547  		"com_github_bazelbuild_rules_go",
   548  	}
   549  	for i, r := range repos {
   550  		rule := f.Rules[i]
   551  		if rule.Name() != r {
   552  			t.Errorf("expect rule %s at %d, got %s", r, i, rule.Name())
   553  		}
   554  		if rule.Index() != i {
   555  			t.Errorf("expect rule %s with index %d, got %d", r, i, rule.Index())
   556  		}
   557  		if f.function.stmt.Body[i] != rule.expr {
   558  			t.Errorf("underlying syntax tree of rule %s not sorted", r)
   559  		}
   560  	}
   561  
   562  	got := strings.TrimSpace(string(f.Format()))
   563  	want := strings.TrimSpace(`
   564  load("@bazel_gazelle//:deps.bzl", "go_repository")
   565  
   566  def repos():
   567      a_rule(
   568          name = "name1",
   569      )
   570      go_repository(
   571          name = "com_github_andybalholm_cascadia",
   572      )
   573  
   574      go_repository(
   575          name = "com_github_bazelbuild_bazel_gazelle",
   576      )
   577      go_repository(
   578          name = "com_github_bazelbuild_buildtools",
   579      )
   580      go_repository(
   581          name = "com_github_bazelbuild_rules_go",
   582      )
   583      some_other_call_rule()
   584  `)
   585  
   586  	if got != want {
   587  		t.Errorf("got:%s\nwant:%s", got, want)
   588  	}
   589  }
   590  
   591  func TestCheckFile(t *testing.T) {
   592  	f := File{Rules: []*Rule{
   593  		NewRule("go_repository", "com_google_cloud_go_pubsub"),
   594  		NewRule("go_repository", "com_google_cloud_go_pubsub"),
   595  	}}
   596  	err := checkFile(&f)
   597  	if err == nil {
   598  		t.Errorf("muliple rules with the same name should not be tolerated")
   599  	}
   600  
   601  	f = File{Rules: []*Rule{
   602  		NewRule("go_rules_dependencies", ""),
   603  		NewRule("go_register_toolchains", ""),
   604  	}}
   605  	err = checkFile(&f)
   606  	if err != nil {
   607  		t.Errorf("unexpected error: %v", err)
   608  	}
   609  }
   610  
   611  func TestAttributeComment(t *testing.T) {
   612  	f, err := LoadData(filepath.Join("old", "BUILD.bazel"), "", nil)
   613  	if err != nil {
   614  		t.Fatal(err)
   615  	}
   616  
   617  	r := NewRule("a_rule", "name1")
   618  	r.SetAttr("deps", []string{"foo", "bar", "baz"})
   619  	r.SetAttr("hdrs", []string{"foo", "bar", "baz"})
   620  	hdrComments := r.AttrComments("hdrs")
   621  	hdrComments.Before = append(hdrComments.Before, bzl.Comment{
   622  		Token: "# do not sort",
   623  	})
   624  
   625  	r.Insert(f)
   626  
   627  	got := strings.TrimSpace(string(f.Format()))
   628  	want := strings.TrimSpace(`
   629  a_rule(
   630      name = "name1",
   631      # do not sort
   632      hdrs = [
   633          "foo",
   634          "bar",
   635          "baz",
   636      ],
   637      deps = [
   638          "bar",
   639          "baz",
   640          "foo",
   641      ],
   642  )
   643  `)
   644  
   645  	if got != want {
   646  		t.Errorf("got:%s\nwant:%s", got, want)
   647  	}
   648  }
   649  
   650  func TestSimpleArgument(t *testing.T) {
   651  	f := EmptyFile("foo", "bar")
   652  
   653  	r := NewRule("export_files", "")
   654  	r.AddArg(&bzl.CallExpr{
   655  		X: &bzl.Ident{Name: "glob"},
   656  		List: []bzl.Expr{
   657  			&bzl.ListExpr{
   658  				List: []bzl.Expr{
   659  					&bzl.StringExpr{Value: "**"},
   660  				},
   661  			},
   662  		},
   663  	})
   664  
   665  	r.Insert(f)
   666  	f.Sync()
   667  
   668  	got := strings.TrimSpace(string(bzl.FormatWithoutRewriting(f.File)))
   669  	want := strings.TrimSpace(`
   670  export_files(glob(["**"]))
   671  `)
   672  
   673  	if got != want {
   674  		t.Errorf("got:%s\nwant:%s", got, want)
   675  	}
   676  }
   677  
   678  func TestAttributeValueSorting(t *testing.T) {
   679  	f := EmptyFile("foo", "bar")
   680  
   681  	r := NewRule("a_rule", "")
   682  	r.SetAttr("deps", []string{"foo", "bar", "baz"})
   683  	r.SetAttr("srcs", UnsortedStrings{"foo", "bar", "baz"})
   684  	r.SetAttr("hdrs", []string{"foo", "bar", "baz"})
   685  
   686  	r.Insert(f)
   687  	f.Sync()
   688  
   689  	got := strings.TrimSpace(string(bzl.FormatWithoutRewriting(f.File)))
   690  	want := strings.TrimSpace(`
   691  a_rule(
   692      srcs = [
   693          "foo",
   694          "bar",
   695          "baz",
   696      ],
   697      hdrs = [
   698          "foo",
   699          "bar",
   700          "baz",
   701      ],
   702      deps = [
   703          "bar",
   704          "baz",
   705          "foo",
   706      ],
   707  )
   708  `)
   709  
   710  	if got != want {
   711  		t.Errorf("got:%s\nwant:%s", got, want)
   712  	}
   713  }
   714  
   715  func TestAttributeValueSortingOverride(t *testing.T) {
   716  	f := EmptyFile("foo", "bar")
   717  
   718  	r := NewRule("a_rule", "")
   719  	r.SetAttr("deps", []string{"foo", "bar", "baz"})
   720  	r.SetAttr("srcs", UnsortedStrings{"foo", "bar", "baz"})
   721  	r.SetAttr("hdrs", []string{"foo", "bar", "baz"})
   722  	r.SetSortedAttrs([]string{"srcs", "hdrs"})
   723  
   724  	r.Insert(f)
   725  	f.Sync()
   726  
   727  	got := strings.TrimSpace(string(bzl.FormatWithoutRewriting(f.File)))
   728  	want := strings.TrimSpace(`
   729  a_rule(
   730      srcs = [
   731          "foo",
   732          "bar",
   733          "baz",
   734      ],
   735      hdrs = [
   736          "bar",
   737          "baz",
   738          "foo",
   739      ],
   740      deps = [
   741          "foo",
   742          "bar",
   743          "baz",
   744      ],
   745  )
   746  `)
   747  
   748  	if got != want {
   749  		t.Errorf("got:%s\nwant:%s", got, want)
   750  	}
   751  
   752  	if !reflect.DeepEqual(r.SortedAttrs(), []string{"srcs", "hdrs"}) {
   753  		t.Errorf("Unexpected r.SortedAttrs(): %v", r.SortedAttrs())
   754  	}
   755  }