| /* |
| Copyright 2023 Google LLC |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| https://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| // Package bzlmod contains functions for working with MODULE.bazel files. |
| package bzlmod |
| |
| import ( |
| "path" |
| |
| "github.com/bazelbuild/buildtools/build" |
| "github.com/bazelbuild/buildtools/labels" |
| ) |
| |
| // Proxies returns the names of extension proxies (i.e. the names of variables to which the result |
| // of a use_extension call is assigned) for the given extension with the given value of the |
| // dev_dependency attribute. |
| // Extension proxies created with "isolate = True" are ignored. |
| func Proxies(f *build.File, rawExtBzlFile string, extName string, dev bool) []string { |
| apparentModuleName := getApparentModuleName(f) |
| extBzlFile := normalizeLabelString(rawExtBzlFile, apparentModuleName) |
| |
| var proxies []string |
| for _, stmt := range f.Stmt { |
| proxy, rawBzlFile, name, isDev, isIsolated := parseUseExtension(stmt) |
| if proxy == "" || isDev != dev || isIsolated { |
| continue |
| } |
| bzlFile := normalizeLabelString(rawBzlFile, apparentModuleName) |
| if bzlFile == extBzlFile && name == extName { |
| proxies = append(proxies, proxy) |
| } |
| } |
| |
| return proxies |
| } |
| |
| // AllProxies returns the names of all extension proxies (i.e. the names of variables to which the |
| // result of a use_extension call is assigned) corresponding to the same extension usage as the |
| // given proxy. |
| // For an isolated extension usage, a list containing only the given proxy is returned. |
| // For a non-isolated extension usage, the proxies of all non-isolated extension usages of the same |
| // extension with the same value for the dev_dependency parameter are returned. |
| // If the given proxy is not an extension proxy, nil is returned. |
| func AllProxies(f *build.File, proxy string) []string { |
| for _, stmt := range f.Stmt { |
| proxyCandidate, rawBzlFile, name, isDev, isIsolated := parseUseExtension(stmt) |
| if proxyCandidate == proxy { |
| if isIsolated { |
| return []string{proxy} |
| } |
| return Proxies(f, rawBzlFile, name, isDev) |
| } |
| } |
| return nil |
| } |
| |
| // UseRepos returns the use_repo calls that use the given proxies. |
| func UseRepos(f *build.File, proxies []string) []*build.CallExpr { |
| proxiesSet := make(map[string]struct{}) |
| for _, p := range proxies { |
| proxiesSet[p] = struct{}{} |
| } |
| |
| var useRepos []*build.CallExpr |
| for _, stmt := range f.Stmt { |
| if _, ok := stmt.(*build.CallExpr); !ok { |
| continue |
| } |
| call := stmt.(*build.CallExpr) |
| if _, ok := call.X.(*build.Ident); !ok { |
| continue |
| } |
| if call.X.(*build.Ident).Name != "use_repo" || len(call.List) < 1 { |
| continue |
| } |
| proxy, ok := call.List[0].(*build.Ident) |
| if !ok { |
| continue |
| } |
| if _, ok := proxiesSet[proxy.Name]; !ok { |
| continue |
| } |
| useRepos = append(useRepos, call) |
| } |
| |
| return useRepos |
| } |
| |
| // NewUseRepo inserts and returns a new use_repo call after the last usage of any of the given |
| // proxies, where a usage is either a use_extension call or a tag definition. |
| func NewUseRepo(f *build.File, proxies []string) (*build.File, *build.CallExpr) { |
| lastUsage, proxy := lastProxyUsage(f, proxies) |
| if lastUsage == -1 { |
| return f, nil |
| } |
| |
| useRepo := &build.CallExpr{ |
| X: &build.Ident{Name: "use_repo"}, |
| List: []build.Expr{ |
| &build.Ident{Name: proxy}, |
| }, |
| } |
| stmt := append(f.Stmt[:lastUsage+1], append([]build.Expr{useRepo}, f.Stmt[lastUsage+1:]...)...) |
| |
| return &build.File{Path: f.Path, Comments: f.Comments, Stmt: stmt, Type: build.TypeModule}, useRepo |
| } |
| |
| // AddRepoUsages adds the given repos to the given use_repo calls without introducing duplicate |
| // arguments. |
| // useRepos must not be empty. |
| // Keyword arguments are preserved but adding them is currently not supported. |
| func AddRepoUsages(useRepos []*build.CallExpr, repos ...string) { |
| if len(repos) == 0 { |
| return |
| } |
| if len(useRepos) == 0 { |
| panic("useRepos must not be empty") |
| } |
| |
| seen := make(map[string]struct{}) |
| for _, useRepo := range useRepos { |
| if len(useRepo.List) == 0 { |
| // Invalid use_repo call, skip. |
| continue |
| } |
| for _, arg := range useRepo.List[1:] { |
| seen[repoFromUseRepoArg(arg)] = struct{}{} |
| } |
| } |
| |
| lastUseRepo := getLastUseRepo(useRepos) |
| for _, repo := range repos { |
| if _, ok := seen[repo]; ok { |
| continue |
| } |
| // Sorting of use_repo arguments is handled by Buildify. |
| // TODO: Add a keyword argument instead if repo is of the form "key=value". |
| lastUseRepo.List = append(lastUseRepo.List, &build.StringExpr{Value: repo}) |
| } |
| } |
| |
| // RemoveRepoUsages removes the given repos from the given use_repo calls. |
| // Repositories are identified via their names as exported by the module extension (i.e. the value |
| // rather than the key in the case of keyword arguments). |
| func RemoveRepoUsages(useRepos []*build.CallExpr, repos ...string) { |
| if len(useRepos) == 0 || len(repos) == 0 { |
| return |
| } |
| |
| toRemove := make(map[string]struct{}) |
| for _, repo := range repos { |
| toRemove[repo] = struct{}{} |
| } |
| |
| for _, useRepo := range useRepos { |
| if len(useRepo.List) == 0 { |
| // Invalid use_repo call, skip. |
| continue |
| } |
| var args []build.Expr |
| // Skip over ext in use_repo(ext, ...). |
| for _, arg := range useRepo.List[1:] { |
| repo := repoFromUseRepoArg(arg) |
| if _, remove := toRemove[repo]; !remove { |
| args = append(args, arg) |
| } |
| } |
| useRepo.List = append(useRepo.List[:1], args...) |
| } |
| } |
| |
| func getLastUseRepo(useRepos []*build.CallExpr) *build.CallExpr { |
| var lastUseRepo *build.CallExpr |
| for _, useRepo := range useRepos { |
| if lastUseRepo == nil || useRepo.Pos.Byte > lastUseRepo.Pos.Byte { |
| lastUseRepo = useRepo |
| } |
| } |
| return lastUseRepo |
| } |
| |
| // repoFromUseRepoArg returns the repository name used by the module extension itself from a |
| // use_repo argument. |
| func repoFromUseRepoArg(arg build.Expr) string { |
| switch arg := arg.(type) { |
| case *build.StringExpr: |
| // use_repo(ext, "repo") --> repo |
| return arg.Value |
| case *build.AssignExpr: |
| // use_repo(ext, my_repo = "repo") --> repo |
| if repo, ok := arg.RHS.(*build.StringExpr); ok { |
| return repo.Value |
| } |
| } |
| return "" |
| } |
| |
| // getApparentModuleName returns the apparent name used for the repository of the module defined |
| // in the given MODULE.bazel file. |
| func getApparentModuleName(f *build.File) string { |
| apparentName := "" |
| |
| for _, module := range f.Rules("module") { |
| if repoName := module.AttrString("repo_name"); repoName != "" { |
| apparentName = repoName |
| } else if name := module.AttrString("name"); name != "" { |
| apparentName = name |
| } |
| } |
| |
| return apparentName |
| } |
| |
| // normalizeLabelString converts a label string into the form @apparent_name//path/to:target. |
| func normalizeLabelString(rawLabel, apparentModuleName string) string { |
| label := labels.ParseRelative(rawLabel, "") |
| if label.Repository == "" { |
| // This branch is taken in two different cases: |
| // 1. The label is relative. In this case, labels.ParseRelative populates the Package field |
| // but not the Repository field. |
| // 2. The label is of the form "@//pkg:extension.bzl". Normalize to spelling out the |
| // apparent name of the root module. Note that this syntax is only allowed in the root |
| // module, but since we are inspecting its module file as a tool, we can assume that the |
| // current module is the root module. |
| label.Repository = apparentModuleName |
| } |
| return label.Format() |
| } |
| |
| func parseUseExtension(stmt build.Expr) (proxy string, bzlFile string, name string, dev bool, isolate bool) { |
| assign, ok := stmt.(*build.AssignExpr) |
| if !ok { |
| return |
| } |
| if _, ok = assign.LHS.(*build.Ident); !ok { |
| return |
| } |
| if _, ok = assign.RHS.(*build.CallExpr); !ok { |
| return |
| } |
| call := assign.RHS.(*build.CallExpr) |
| if _, ok = call.X.(*build.Ident); !ok { |
| return |
| } |
| if call.X.(*build.Ident).Name != "use_extension" { |
| return |
| } |
| if len(call.List) < 2 { |
| // Missing required positional arguments. |
| return |
| } |
| bzlFileExpr, ok := call.List[0].(*build.StringExpr) |
| if !ok { |
| return |
| } |
| nameExpr, ok := call.List[1].(*build.StringExpr) |
| if !ok { |
| return |
| } |
| // Check for the optional dev_dependency keyword argument. |
| if len(call.List) > 2 { |
| for _, arg := range call.List[2:] { |
| dev = dev || parseBooleanKeywordArg(arg, "dev_dependency") |
| isolate = isolate || parseBooleanKeywordArg(arg, "isolate") |
| } |
| } |
| return assign.LHS.(*build.Ident).Name, bzlFileExpr.Value, nameExpr.Value, dev, isolate |
| } |
| |
| // parseBooleanKeywordArg parses a keyword argument of type bool that is assumed to default to |
| // False. |
| func parseBooleanKeywordArg(arg build.Expr, name string) bool { |
| keywordArg, ok := arg.(*build.AssignExpr) |
| if !ok { |
| return false |
| } |
| argName, ok := keywordArg.LHS.(*build.Ident) |
| if !ok || argName.Name != name { |
| return false |
| } |
| argValue, ok := keywordArg.RHS.(*build.Ident) |
| // We assume that any expression other than "False" evaluates to True as otherwise there would |
| // be no reason to specify the argument - MODULE.bazel files are entirely static with no |
| // external inputs, so every expression always evaluates to the same value. |
| if ok && argValue.Name == "False" { |
| return false |
| } |
| return true |
| } |
| |
| func parseTag(stmt build.Expr) string { |
| call, ok := stmt.(*build.CallExpr) |
| if !ok { |
| return "" |
| } |
| if _, ok := call.X.(*build.DotExpr); !ok { |
| return "" |
| } |
| dot := call.X.(*build.DotExpr) |
| if _, ok := dot.X.(*build.Ident); !ok { |
| return "" |
| } |
| return dot.X.(*build.Ident).Name |
| } |
| |
| // lastProxyUsage returns the index of the last statement in the given file that uses one of the |
| // given extension proxies (either in a use_extension assignment or tag call). If no such statement |
| // exists, -1 is returned. |
| func lastProxyUsage(f *build.File, proxies []string) (lastUsage int, lastProxy string) { |
| proxiesSet := make(map[string]struct{}) |
| for _, p := range proxies { |
| proxiesSet[p] = struct{}{} |
| } |
| |
| lastUsage = -1 |
| for i, stmt := range f.Stmt { |
| proxy, _, _, _, _ := parseUseExtension(stmt) |
| if proxy != "" { |
| _, isUsage := proxiesSet[proxy] |
| if isUsage { |
| lastUsage = i |
| lastProxy = proxy |
| continue |
| } |
| } |
| |
| proxy = parseTag(stmt) |
| if proxy != "" { |
| _, isUsage := proxiesSet[proxy] |
| if isUsage { |
| lastUsage = i |
| lastProxy = proxy |
| continue |
| } |
| } |
| } |
| |
| return lastUsage, lastProxy |
| } |
| |
| // ExtractModuleToApparentNameMapping collects the mapping of module names (e.g. "rules_go") to |
| // user-configured apparent names (e.g. "my_rules_go") from the repo's MODULE.bazel, if it exists. |
| // The given function is called with a repo-relative, slash-separated path and should return the |
| // content of the MODULE.bazel or *.MODULE.bazel file at that path, or nil if the file does not |
| // exist. |
| // See https://bazel.build/external/module#repository_names_and_strict_deps for more information on |
| // apparent names. |
| func ExtractModuleToApparentNameMapping(fileReader func(relPath string) *build.File) func(string) string { |
| moduleToApparentName := collectApparentNames(fileReader, "MODULE.bazel") |
| |
| return func(moduleName string) string { |
| return moduleToApparentName[moduleName] |
| } |
| } |
| |
| // Collects the mapping of module names (e.g. "rules_go") to user-configured apparent names (e.g. |
| // "my_rules_go"). See https://bazel.build/external/module#repository_names_and_strict_deps for more |
| // information on apparent names. |
| func collectApparentNames(fileReader func(relPath string) *build.File, relPath string) map[string]string { |
| apparentNames := make(map[string]string) |
| seenFiles := make(map[string]struct{}) |
| filesToProcess := []string{relPath} |
| |
| for len(filesToProcess) > 0 { |
| f := filesToProcess[0] |
| filesToProcess = filesToProcess[1:] |
| if _, seen := seenFiles[f]; seen { |
| continue |
| } |
| seenFiles[f] = struct{}{} |
| bf := fileReader(f) |
| if bf == nil { |
| return nil |
| } |
| names, includeLabels := collectApparentNamesAndIncludes(bf) |
| for name, apparentName := range names { |
| apparentNames[name] = apparentName |
| } |
| for _, includeLabel := range includeLabels { |
| l := labels.Parse(includeLabel) |
| p := path.Join(l.Package, l.Target) |
| filesToProcess = append(filesToProcess, p) |
| } |
| } |
| |
| return apparentNames |
| } |
| |
| func collectApparentNamesAndIncludes(f *build.File) (map[string]string, []string) { |
| apparentNames := make(map[string]string) |
| var includeLabels []string |
| |
| for _, dep := range f.Rules("") { |
| if dep.ExplicitName() == "" { |
| if ident, ok := dep.Call.X.(*build.Ident); !ok || ident.Name != "include" { |
| continue |
| } |
| if len(dep.Call.List) != 1 { |
| continue |
| } |
| if str, ok := dep.Call.List[0].(*build.StringExpr); ok { |
| includeLabels = append(includeLabels, str.Value) |
| } |
| continue |
| } |
| if dep.Kind() != "module" && dep.Kind() != "bazel_dep" { |
| continue |
| } |
| // We support module in addition to bazel_dep to handle language repos that use Gazelle to |
| // manage their own BUILD files. |
| if name := dep.AttrString("name"); name != "" { |
| if repoName := dep.AttrString("repo_name"); repoName != "" { |
| apparentNames[name] = repoName |
| } else { |
| apparentNames[name] = name |
| } |
| } |
| } |
| |
| return apparentNames, includeLabels |
| } |