...

Source file src/github.com/bazelbuild/buildtools/edit/bzlmod/bzlmod.go

Documentation: github.com/bazelbuild/buildtools/edit/bzlmod

     1  /*
     2  Copyright 2023 Google LLC
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      https://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Package bzlmod contains functions for working with MODULE.bazel files.
    18  package bzlmod
    19  
    20  import (
    21  	"strings"
    22  
    23  	"github.com/bazelbuild/buildtools/build"
    24  )
    25  
    26  // Proxies returns the names of extension proxies (i.e. the names of variables to which the result
    27  // of a use_extension call is assigned) for the given extension with the given value of the
    28  // dev_dependency attribute.
    29  // Extension proxies created with "isolate = True" are ignored.
    30  func Proxies(f *build.File, rawExtBzlFile string, extName string, dev bool) []string {
    31  	apparentModuleName := getApparentModuleName(f)
    32  	extBzlFile := normalizeLabelString(rawExtBzlFile, apparentModuleName)
    33  
    34  	var proxies []string
    35  	for _, stmt := range f.Stmt {
    36  		proxy, rawBzlFile, name, isDev, isIsolated := parseUseExtension(stmt)
    37  		if proxy == "" || isDev != dev || isIsolated {
    38  			continue
    39  		}
    40  		bzlFile := normalizeLabelString(rawBzlFile, apparentModuleName)
    41  		if bzlFile == extBzlFile && name == extName {
    42  			proxies = append(proxies, proxy)
    43  		}
    44  	}
    45  
    46  	return proxies
    47  }
    48  
    49  // AllProxies returns the names of all extension proxies (i.e. the names of variables to which the
    50  // result of a use_extension call is assigned) corresponding to the same extension usage as the
    51  // given proxy.
    52  // For an isolated extension usage, a list containing only the given proxy is returned.
    53  // For a non-isolated extension usage, the proxies of all non-isolated extension usages of the same
    54  // extension with the same value for the dev_dependency parameter are returned.
    55  // If the given proxy is not an extension proxy, nil is returned.
    56  func AllProxies(f *build.File, proxy string) []string {
    57  	for _, stmt := range f.Stmt {
    58  		proxyCandidate, rawBzlFile, name, isDev, isIsolated := parseUseExtension(stmt)
    59  		if proxyCandidate == proxy {
    60  			if isIsolated {
    61  				return []string{proxy}
    62  			}
    63  			return Proxies(f, rawBzlFile, name, isDev)
    64  		}
    65  	}
    66  	return nil
    67  }
    68  
    69  // UseRepos returns the use_repo calls that use the given proxies.
    70  func UseRepos(f *build.File, proxies []string) []*build.CallExpr {
    71  	proxiesSet := make(map[string]struct{})
    72  	for _, p := range proxies {
    73  		proxiesSet[p] = struct{}{}
    74  	}
    75  
    76  	var useRepos []*build.CallExpr
    77  	for _, stmt := range f.Stmt {
    78  		if _, ok := stmt.(*build.CallExpr); !ok {
    79  			continue
    80  		}
    81  		call := stmt.(*build.CallExpr)
    82  		if _, ok := call.X.(*build.Ident); !ok {
    83  			continue
    84  		}
    85  		if call.X.(*build.Ident).Name != "use_repo" || len(call.List) < 1 {
    86  			continue
    87  		}
    88  		proxy, ok := call.List[0].(*build.Ident)
    89  		if !ok {
    90  			continue
    91  		}
    92  		if _, ok := proxiesSet[proxy.Name]; !ok {
    93  			continue
    94  		}
    95  		useRepos = append(useRepos, call)
    96  	}
    97  
    98  	return useRepos
    99  }
   100  
   101  // NewUseRepo inserts and returns a new use_repo call after the last usage of any of the given
   102  // proxies, where a usage is either a use_extension call or a tag definition.
   103  func NewUseRepo(f *build.File, proxies []string) (*build.File, *build.CallExpr) {
   104  	lastUsage, proxy := lastProxyUsage(f, proxies)
   105  	if lastUsage == -1 {
   106  		return f, nil
   107  	}
   108  
   109  	useRepo := &build.CallExpr{
   110  		X: &build.Ident{Name: "use_repo"},
   111  		List: []build.Expr{
   112  			&build.Ident{Name: proxy},
   113  		},
   114  	}
   115  	stmt := append(f.Stmt[:lastUsage+1], append([]build.Expr{useRepo}, f.Stmt[lastUsage+1:]...)...)
   116  
   117  	return &build.File{Path: f.Path, Comments: f.Comments, Stmt: stmt, Type: build.TypeModule}, useRepo
   118  }
   119  
   120  // AddRepoUsages adds the given repos to the given use_repo calls without introducing duplicate
   121  // arguments.
   122  // useRepos must not be empty.
   123  // Keyword arguments are preserved but adding them is currently not supported.
   124  func AddRepoUsages(useRepos []*build.CallExpr, repos ...string) {
   125  	if len(repos) == 0 {
   126  		return
   127  	}
   128  	if len(useRepos) == 0 {
   129  		panic("useRepos must not be empty")
   130  	}
   131  
   132  	seen := make(map[string]struct{})
   133  	for _, useRepo := range useRepos {
   134  		if len(useRepo.List) == 0 {
   135  			// Invalid use_repo call, skip.
   136  			continue
   137  		}
   138  		for _, arg := range useRepo.List[1:] {
   139  			seen[repoFromUseRepoArg(arg)] = struct{}{}
   140  		}
   141  	}
   142  
   143  	lastUseRepo := getLastUseRepo(useRepos)
   144  	for _, repo := range repos {
   145  		if _, ok := seen[repo]; ok {
   146  			continue
   147  		}
   148  		// Sorting of use_repo arguments is handled by Buildify.
   149  		// TODO: Add a keyword argument instead if repo is of the form "key=value".
   150  		lastUseRepo.List = append(lastUseRepo.List, &build.StringExpr{Value: repo})
   151  	}
   152  }
   153  
   154  // RemoveRepoUsages removes the given repos from the given use_repo calls.
   155  // Repositories are identified via their names as exported by the module extension (i.e. the value
   156  // rather than the key in the case of keyword arguments).
   157  func RemoveRepoUsages(useRepos []*build.CallExpr, repos ...string) {
   158  	if len(useRepos) == 0 || len(repos) == 0 {
   159  		return
   160  	}
   161  
   162  	toRemove := make(map[string]struct{})
   163  	for _, repo := range repos {
   164  		toRemove[repo] = struct{}{}
   165  	}
   166  
   167  	for _, useRepo := range useRepos {
   168  		if len(useRepo.List) == 0 {
   169  			// Invalid use_repo call, skip.
   170  			continue
   171  		}
   172  		var args []build.Expr
   173  		// Skip over ext in use_repo(ext, ...).
   174  		for _, arg := range useRepo.List[1:] {
   175  			repo := repoFromUseRepoArg(arg)
   176  			if _, remove := toRemove[repo]; !remove {
   177  				args = append(args, arg)
   178  			}
   179  		}
   180  		useRepo.List = append(useRepo.List[:1], args...)
   181  	}
   182  }
   183  
   184  func getLastUseRepo(useRepos []*build.CallExpr) *build.CallExpr {
   185  	var lastUseRepo *build.CallExpr
   186  	for _, useRepo := range useRepos {
   187  		if lastUseRepo == nil || useRepo.Pos.Byte > lastUseRepo.Pos.Byte {
   188  			lastUseRepo = useRepo
   189  		}
   190  	}
   191  	return lastUseRepo
   192  }
   193  
   194  // repoFromUseRepoArg returns the repository name used by the module extension itself from a
   195  // use_repo argument.
   196  func repoFromUseRepoArg(arg build.Expr) string {
   197  	switch arg := arg.(type) {
   198  	case *build.StringExpr:
   199  		// use_repo(ext, "repo") --> repo
   200  		return arg.Value
   201  	case *build.AssignExpr:
   202  		// use_repo(ext, my_repo = "repo") --> repo
   203  		if repo, ok := arg.RHS.(*build.StringExpr); ok {
   204  			return repo.Value
   205  		}
   206  	}
   207  	return ""
   208  }
   209  
   210  // getApparentModuleName returns the apparent name used for the repository of the module defined
   211  // in the given MODULE.bazel file.
   212  func getApparentModuleName(f *build.File) string {
   213  	apparentName := ""
   214  
   215  	for _, module := range f.Rules("module") {
   216  		if repoName := module.AttrString("repo_name"); repoName != "" {
   217  			apparentName = repoName
   218  		} else if name := module.AttrString("name"); name != "" {
   219  			apparentName = name
   220  		}
   221  	}
   222  
   223  	return apparentName
   224  }
   225  
   226  // normalizeLabelString converts a label string into the form @apparent_name//path/to:target.
   227  func normalizeLabelString(rawLabel, apparentModuleName string) string {
   228  	// This implements
   229  	// https://github.com/bazelbuild/bazel/blob/dd822392db96bb7bccdb673414a20c4b91e3dbc1/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileGlobals.java#L416
   230  	// with the assumption that the current module is the root module.
   231  	if strings.HasPrefix(rawLabel, "//") {
   232  		// Relative labels always refer to the current module.
   233  		return "@" + apparentModuleName + rawLabel
   234  	} else if strings.HasPrefix(rawLabel, "@//") {
   235  		// In the root module only, this syntax refer to the module. Since we are inspecting its
   236  		// module file as a tool, we can assume that the current module is the root module.
   237  		return "@" + apparentModuleName + rawLabel[1:]
   238  	} else {
   239  		return rawLabel
   240  	}
   241  }
   242  
   243  func parseUseExtension(stmt build.Expr) (proxy string, bzlFile string, name string, dev bool, isolate bool) {
   244  	assign, ok := stmt.(*build.AssignExpr)
   245  	if !ok {
   246  		return
   247  	}
   248  	if _, ok = assign.LHS.(*build.Ident); !ok {
   249  		return
   250  	}
   251  	if _, ok = assign.RHS.(*build.CallExpr); !ok {
   252  		return
   253  	}
   254  	call := assign.RHS.(*build.CallExpr)
   255  	if call.X.(*build.Ident).Name != "use_extension" {
   256  		return
   257  	}
   258  	if len(call.List) < 2 {
   259  		// Missing required positional arguments.
   260  		return
   261  	}
   262  	bzlFileExpr, ok := call.List[0].(*build.StringExpr)
   263  	if !ok {
   264  		return
   265  	}
   266  	nameExpr, ok := call.List[1].(*build.StringExpr)
   267  	if !ok {
   268  		return
   269  	}
   270  	// Check for the optional dev_dependency keyword argument.
   271  	if len(call.List) > 2 {
   272  		for _, arg := range call.List[2:] {
   273  			dev = dev || parseBooleanKeywordArg(arg, "dev_dependency")
   274  			isolate = isolate || parseBooleanKeywordArg(arg, "isolate")
   275  		}
   276  	}
   277  	return assign.LHS.(*build.Ident).Name, bzlFileExpr.Value, nameExpr.Value, dev, isolate
   278  }
   279  
   280  // parseBooleanKeywordArg parses a keyword argument of type bool that is assumed to default to
   281  // False.
   282  func parseBooleanKeywordArg(arg build.Expr, name string) bool {
   283  	keywordArg, ok := arg.(*build.AssignExpr)
   284  	if !ok {
   285  		return false
   286  	}
   287  	argName, ok := keywordArg.LHS.(*build.Ident)
   288  	if !ok || argName.Name != name {
   289  		return false
   290  	}
   291  	argValue, ok := keywordArg.RHS.(*build.Ident)
   292  	// We assume that any expression other than "False" evaluates to True as otherwise there would
   293  	// be no reason to specify the argument - MODULE.bazel files are entirely static with no
   294  	// external inputs, so every expression always evaluates to the same value.
   295  	if ok && argValue.Name == "False" {
   296  		return false
   297  	}
   298  	return true
   299  }
   300  
   301  func parseTag(stmt build.Expr) string {
   302  	call, ok := stmt.(*build.CallExpr)
   303  	if !ok {
   304  		return ""
   305  	}
   306  	if _, ok := call.X.(*build.DotExpr); !ok {
   307  		return ""
   308  	}
   309  	dot := call.X.(*build.DotExpr)
   310  	if _, ok := dot.X.(*build.Ident); !ok {
   311  		return ""
   312  	}
   313  	return dot.X.(*build.Ident).Name
   314  }
   315  
   316  // lastProxyUsage returns the index of the last statement in the given file that uses one of the
   317  // given extension proxies (either in a use_extension assignment or tag call). If no such statement
   318  // exists, -1 is returned.
   319  func lastProxyUsage(f *build.File, proxies []string) (lastUsage int, lastProxy string) {
   320  	proxiesSet := make(map[string]struct{})
   321  	for _, p := range proxies {
   322  		proxiesSet[p] = struct{}{}
   323  	}
   324  
   325  	lastUsage = -1
   326  	for i, stmt := range f.Stmt {
   327  		proxy, _, _, _, _ := parseUseExtension(stmt)
   328  		if proxy != "" {
   329  			_, isUsage := proxiesSet[proxy]
   330  			if isUsage {
   331  				lastUsage = i
   332  				lastProxy = proxy
   333  				continue
   334  			}
   335  		}
   336  
   337  		proxy = parseTag(stmt)
   338  		if proxy != "" {
   339  			_, isUsage := proxiesSet[proxy]
   340  			if isUsage {
   341  				lastUsage = i
   342  				lastProxy = proxy
   343  				continue
   344  			}
   345  		}
   346  	}
   347  
   348  	return lastUsage, lastProxy
   349  }
   350  

View as plain text