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

     1  /* Copyright 2017 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 merger
    17  
    18  import (
    19  	"fmt"
    20  	"strings"
    21  
    22  	"github.com/bazelbuild/bazel-gazelle/rule"
    23  	bzl "github.com/bazelbuild/buildtools/build"
    24  )
    25  
    26  // FixLoads removes loads of unused go rules and adds loads of newly used rules.
    27  // This should be called after FixFile and MergeFile, since symbols
    28  // may be introduced that aren't loaded.
    29  //
    30  // This function calls File.Sync before processing loads.
    31  func FixLoads(f *rule.File, knownLoads []rule.LoadInfo) {
    32  	knownFiles := make(map[string]bool)
    33  	knownSymbols := make(map[string]string)
    34  	for _, l := range knownLoads {
    35  		knownFiles[l.Name] = true
    36  		for _, k := range l.Symbols {
    37  			knownSymbols[k] = l.Name
    38  		}
    39  	}
    40  
    41  	// Sync the file. We need File.Loads and File.Rules to contain inserted
    42  	// statements and not deleted statements.
    43  	f.Sync()
    44  
    45  	// Scan load statements in the file. Keep track of loads of known files,
    46  	// since these may be changed. Keep track of symbols loaded from unknown
    47  	// files; we will not add loads for these.
    48  	var loads []*rule.Load
    49  	otherLoadedKinds := make(map[string]bool)
    50  	for _, l := range f.Loads {
    51  		if knownFiles[l.Name()] {
    52  			loads = append(loads, l)
    53  			continue
    54  		}
    55  		for _, sym := range l.Symbols() {
    56  			otherLoadedKinds[sym] = true
    57  		}
    58  	}
    59  
    60  	// Make a map of all the symbols from known files used in this file.
    61  	usedSymbols := make(map[string]map[string]bool)
    62  	bzl.Walk(f.File, func(x bzl.Expr, stk []bzl.Expr) {
    63  		ce, ok := x.(*bzl.CallExpr)
    64  		if !ok {
    65  			return
    66  		}
    67  
    68  		var functionIdent *bzl.Ident
    69  
    70  		d, ok := ce.X.(*bzl.DotExpr)
    71  		if ok {
    72  			functionIdent, ok = d.X.(*bzl.Ident)
    73  		} else {
    74  			functionIdent, ok = ce.X.(*bzl.Ident)
    75  		}
    76  
    77  		if !ok {
    78  			return
    79  		}
    80  
    81  		idents := []*bzl.Ident{functionIdent}
    82  
    83  		for _, arg := range ce.List {
    84  			if argIdent, ok := arg.(*bzl.Ident); ok {
    85  				idents = append(idents, argIdent)
    86  			}
    87  		}
    88  
    89  		for _, id := range idents {
    90  			file, ok := knownSymbols[id.Name]
    91  			if !ok || otherLoadedKinds[id.Name] {
    92  				continue
    93  			}
    94  
    95  			if usedSymbols[file] == nil {
    96  				usedSymbols[file] = make(map[string]bool)
    97  			}
    98  			usedSymbols[file][id.Name] = true
    99  		}
   100  	})
   101  
   102  	// Fix the load statements. The order is important, so we iterate over
   103  	// knownLoads instead of knownFiles.
   104  	for _, known := range knownLoads {
   105  		file := known.Name
   106  		first := true
   107  		for _, l := range loads {
   108  			if l.Name() != file {
   109  				continue
   110  			}
   111  			if first {
   112  				fixLoad(l, file, usedSymbols[file], knownSymbols)
   113  				first = false
   114  			} else {
   115  				fixLoad(l, file, nil, knownSymbols)
   116  			}
   117  			if l.IsEmpty() {
   118  				l.Delete()
   119  			}
   120  		}
   121  		if first {
   122  			load := fixLoad(nil, file, usedSymbols[file], knownSymbols)
   123  			if load != nil {
   124  				index := newLoadIndex(f, known.After)
   125  				load.Insert(f, index)
   126  			}
   127  		}
   128  	}
   129  }
   130  
   131  // fixLoad updates a load statement with the given symbols. If load is nil,
   132  // a new load may be created and returned. Symbols in symbols will be added
   133  // to the load if they're not already present. Known symbols not in symbols
   134  // will be removed if present. Other symbols will be preserved. If load is
   135  // empty, nil is returned.
   136  func fixLoad(load *rule.Load, file string, symbols map[string]bool, knownSymbols map[string]string) *rule.Load {
   137  	if load == nil {
   138  		if len(symbols) == 0 {
   139  			return nil
   140  		}
   141  		load = rule.NewLoad(file)
   142  	}
   143  
   144  	for k := range symbols {
   145  		load.Add(k)
   146  	}
   147  	for _, k := range load.Symbols() {
   148  		if knownSymbols[k] != "" && !symbols[k] {
   149  			load.Remove(k)
   150  		}
   151  	}
   152  
   153  	return load
   154  }
   155  
   156  // newLoadIndex returns the index in stmts where a new load statement should
   157  // be inserted. after is a list of function names that the load should not
   158  // be inserted before.
   159  func newLoadIndex(f *rule.File, after []string) int {
   160  	if len(after) == 0 {
   161  		return 0
   162  	}
   163  	index := 0
   164  	for _, r := range f.Rules {
   165  		for _, a := range after {
   166  			if r.Kind() == a && r.Index() >= index {
   167  				index = r.Index() + 1
   168  			}
   169  		}
   170  	}
   171  	return index
   172  }
   173  
   174  // CheckGazelleLoaded searches the given WORKSPACE file for a repository named
   175  // "bazel_gazelle". If no such repository is found *and* the repo is not
   176  // declared with a directive *and* at least one load statement mentions
   177  // the repository, a descriptive error will be returned.
   178  //
   179  // This should be called after modifications have been made to WORKSPACE
   180  // (i.e., after FixLoads) before writing it to disk.
   181  func CheckGazelleLoaded(f *rule.File) error {
   182  	needGazelle := false
   183  	for _, l := range f.Loads {
   184  		if strings.HasPrefix(l.Name(), "@bazel_gazelle//") {
   185  			needGazelle = true
   186  		}
   187  	}
   188  	if !needGazelle {
   189  		return nil
   190  	}
   191  	for _, r := range f.Rules {
   192  		if r.Name() == "bazel_gazelle" {
   193  			return nil
   194  		}
   195  	}
   196  	for _, d := range f.Directives {
   197  		if d.Key != "repo" {
   198  			continue
   199  		}
   200  		if fs := strings.Fields(d.Value); len(fs) > 0 && fs[0] == "bazel_gazelle" {
   201  			return nil
   202  		}
   203  	}
   204  	return fmt.Errorf(`%s: error: bazel_gazelle is not declared in WORKSPACE.
   205  Without this repository, Gazelle cannot safely modify the WORKSPACE file.
   206  See the instructions at https://github.com/bazelbuild/bazel-gazelle.
   207  If the bazel_gazelle is declared inside a macro, you can suppress this error
   208  by adding a comment like this to WORKSPACE:
   209      # gazelle:repo bazel_gazelle
   210  `, f.Path)
   211  }