...

Source file src/github.com/bazelbuild/rules_go/go/tools/builders/nogo_main.go

Documentation: github.com/bazelbuild/rules_go/go/tools/builders

     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  // Loads and runs registered analyses on a well-typed Go package.
    17  // The code in this file is combined with the code generated by
    18  // generate_nogo_main.go.
    19  
    20  package main
    21  
    22  import (
    23  	"bytes"
    24  	"encoding/gob"
    25  	"errors"
    26  	"flag"
    27  	"fmt"
    28  	"go/ast"
    29  	"go/parser"
    30  	"go/token"
    31  	"go/types"
    32  	"io/ioutil"
    33  	"log"
    34  	"os"
    35  	"reflect"
    36  	"regexp"
    37  	"sort"
    38  	"strings"
    39  	"sync"
    40  
    41  	"golang.org/x/tools/go/analysis"
    42  	"golang.org/x/tools/go/gcexportdata"
    43  	"golang.org/x/tools/internal/facts"
    44  )
    45  
    46  const nogoBaseConfigName = "_base"
    47  
    48  func init() {
    49  	if err := analysis.Validate(analyzers); err != nil {
    50  		log.Fatal(err)
    51  	}
    52  }
    53  
    54  var typesSizes = types.SizesFor("gc", os.Getenv("GOARCH"))
    55  
    56  func main() {
    57  	log.SetFlags(0) // no timestamp
    58  	log.SetPrefix("nogo: ")
    59  	if err := run(os.Args[1:]); err != nil {
    60  		log.Fatal(err)
    61  	}
    62  }
    63  
    64  // run returns an error if there is a problem loading the package or if any
    65  // analysis fails.
    66  func run(args []string) error {
    67  	args, _, err := expandParamsFiles(args)
    68  	if err != nil {
    69  		return fmt.Errorf("error reading paramfiles: %v", err)
    70  	}
    71  
    72  	factMap := factMultiFlag{}
    73  	flags := flag.NewFlagSet("nogo", flag.ExitOnError)
    74  	flags.Var(&factMap, "fact", "Import path and file containing facts for that library, separated by '=' (may be repeated)'")
    75  	importcfg := flags.String("importcfg", "", "The import configuration file")
    76  	packagePath := flags.String("p", "", "The package path (importmap) of the package being compiled")
    77  	xPath := flags.String("x", "", "The archive file where serialized facts should be written")
    78  	flags.Parse(args)
    79  	srcs := flags.Args()
    80  
    81  	packageFile, importMap, err := readImportCfg(*importcfg)
    82  	if err != nil {
    83  		return fmt.Errorf("error parsing importcfg: %v", err)
    84  	}
    85  
    86  	diagnostics, facts, err := checkPackage(analyzers, *packagePath, packageFile, importMap, factMap, srcs)
    87  	if err != nil {
    88  		return fmt.Errorf("error running analyzers: %v", err)
    89  	}
    90  	if diagnostics != "" {
    91  		return fmt.Errorf("errors found by nogo during build-time code analysis:\n%s\n", diagnostics)
    92  	}
    93  	if *xPath != "" {
    94  		if err := ioutil.WriteFile(abs(*xPath), facts, 0o666); err != nil {
    95  			return fmt.Errorf("error writing facts: %v", err)
    96  		}
    97  	}
    98  
    99  	return nil
   100  }
   101  
   102  // Adapted from go/src/cmd/compile/internal/gc/main.go. Keep in sync.
   103  func readImportCfg(file string) (packageFile map[string]string, importMap map[string]string, err error) {
   104  	packageFile, importMap = make(map[string]string), make(map[string]string)
   105  	data, err := ioutil.ReadFile(file)
   106  	if err != nil {
   107  		return nil, nil, fmt.Errorf("-importcfg: %v", err)
   108  	}
   109  
   110  	for lineNum, line := range strings.Split(string(data), "\n") {
   111  		lineNum++ // 1-based
   112  		line = strings.TrimSpace(line)
   113  		if line == "" || strings.HasPrefix(line, "#") {
   114  			continue
   115  		}
   116  
   117  		var verb, args string
   118  		if i := strings.Index(line, " "); i < 0 {
   119  			verb = line
   120  		} else {
   121  			verb, args = line[:i], strings.TrimSpace(line[i+1:])
   122  		}
   123  		var before, after string
   124  		if i := strings.Index(args, "="); i >= 0 {
   125  			before, after = args[:i], args[i+1:]
   126  		}
   127  		switch verb {
   128  		default:
   129  			return nil, nil, fmt.Errorf("%s:%d: unknown directive %q", file, lineNum, verb)
   130  		case "importmap":
   131  			if before == "" || after == "" {
   132  				return nil, nil, fmt.Errorf(`%s:%d: invalid importmap: syntax is "importmap old=new"`, file, lineNum)
   133  			}
   134  			importMap[before] = after
   135  		case "packagefile":
   136  			if before == "" || after == "" {
   137  				return nil, nil, fmt.Errorf(`%s:%d: invalid packagefile: syntax is "packagefile path=filename"`, file, lineNum)
   138  			}
   139  			packageFile[before] = after
   140  		}
   141  	}
   142  	return packageFile, importMap, nil
   143  }
   144  
   145  // checkPackage runs all the given analyzers on the specified package and
   146  // returns the source code diagnostics that the must be printed in the build log.
   147  // It returns an empty string if no source code diagnostics need to be printed.
   148  //
   149  // This implementation was adapted from that of golang.org/x/tools/go/checker/internal/checker.
   150  func checkPackage(analyzers []*analysis.Analyzer, packagePath string, packageFile, importMap map[string]string, factMap map[string]string, filenames []string) (string, []byte, error) {
   151  	// Register fact types and establish dependencies between analyzers.
   152  	actions := make(map[*analysis.Analyzer]*action)
   153  	var visit func(a *analysis.Analyzer) *action
   154  	visit = func(a *analysis.Analyzer) *action {
   155  		act, ok := actions[a]
   156  		if !ok {
   157  			act = &action{a: a}
   158  			actions[a] = act
   159  			for _, f := range a.FactTypes {
   160  				act.usesFacts = true
   161  				gob.Register(f)
   162  			}
   163  			act.deps = make([]*action, len(a.Requires))
   164  			for i, req := range a.Requires {
   165  				dep := visit(req)
   166  				if dep.usesFacts {
   167  					act.usesFacts = true
   168  				}
   169  				act.deps[i] = dep
   170  			}
   171  		}
   172  		return act
   173  	}
   174  
   175  	roots := make([]*action, 0, len(analyzers))
   176  	for _, a := range analyzers {
   177  		if cfg, ok := configs[a.Name]; ok {
   178  			for flagKey, flagVal := range cfg.analyzerFlags {
   179  				if strings.HasPrefix(flagKey, "-") {
   180  					return "", nil, fmt.Errorf(
   181  						"%s: flag should not begin with '-': %s", a.Name, flagKey)
   182  				}
   183  				if flag := a.Flags.Lookup(flagKey); flag == nil {
   184  					return "", nil, fmt.Errorf("%s: unrecognized flag: %s", a.Name, flagKey)
   185  				}
   186  				if err := a.Flags.Set(flagKey, flagVal); err != nil {
   187  					return "", nil, fmt.Errorf(
   188  						"%s: invalid value for flag: %s=%s: %w", a.Name, flagKey, flagVal, err)
   189  				}
   190  			}
   191  		}
   192  		roots = append(roots, visit(a))
   193  	}
   194  
   195  	// Load the package, including AST, types, and facts.
   196  	imp := newImporter(importMap, packageFile, factMap)
   197  	pkg, err := load(packagePath, imp, filenames)
   198  	if err != nil {
   199  		return "", nil, fmt.Errorf("error loading package: %v", err)
   200  	}
   201  	for _, act := range actions {
   202  		act.pkg = pkg
   203  	}
   204  
   205  	// Process nolint directives similar to golangci-lint.
   206  	for _, f := range pkg.syntax {
   207  		// CommentMap will correctly associate comments to the largest node group
   208  		// applicable. This handles inline comments that might trail a large
   209  		// assignment and will apply the comment to the entire assignment.
   210  		commentMap := ast.NewCommentMap(pkg.fset, f, f.Comments)
   211  		for node, groups := range commentMap {
   212  			rng := &Range{
   213  				from: pkg.fset.Position(node.Pos()),
   214  				to:   pkg.fset.Position(node.End()).Line,
   215  			}
   216  			for _, group := range groups {
   217  				for _, comm := range group.List {
   218  					linters, ok := parseNolint(comm.Text)
   219  					if !ok {
   220  						continue
   221  					}
   222  					for analyzer, act := range actions {
   223  						if linters == nil || linters[analyzer.Name] {
   224  							act.nolint = append(act.nolint, rng)
   225  						}
   226  					}
   227  				}
   228  			}
   229  		}
   230  	}
   231  
   232  	// Execute the analyzers.
   233  	execAll(roots)
   234  
   235  	// Process diagnostics and encode facts for importers of this package.
   236  	diagnostics := checkAnalysisResults(roots, pkg)
   237  	facts := pkg.facts.Encode()
   238  	return diagnostics, facts, nil
   239  }
   240  
   241  type Range struct {
   242  	from token.Position
   243  	to   int
   244  }
   245  
   246  // An action represents one unit of analysis work: the application of
   247  // one analysis to one package. Actions form a DAG within a
   248  // package (as different analyzers are applied, either in sequence or
   249  // parallel).
   250  type action struct {
   251  	once        sync.Once
   252  	a           *analysis.Analyzer
   253  	pass        *analysis.Pass
   254  	pkg         *goPackage
   255  	deps        []*action
   256  	inputs      map[*analysis.Analyzer]interface{}
   257  	result      interface{}
   258  	diagnostics []analysis.Diagnostic
   259  	usesFacts   bool
   260  	err         error
   261  	nolint      []*Range
   262  }
   263  
   264  func (act *action) String() string {
   265  	return fmt.Sprintf("%s@%s", act.a, act.pkg)
   266  }
   267  
   268  func execAll(actions []*action) {
   269  	var wg sync.WaitGroup
   270  	wg.Add(len(actions))
   271  	for _, act := range actions {
   272  		go func(act *action) {
   273  			defer wg.Done()
   274  			act.exec()
   275  		}(act)
   276  	}
   277  	wg.Wait()
   278  }
   279  
   280  func (act *action) exec() { act.once.Do(act.execOnce) }
   281  
   282  func (act *action) execOnce() {
   283  	// Analyze dependencies.
   284  	execAll(act.deps)
   285  
   286  	// Report an error if any dependency failed.
   287  	var failed []string
   288  	for _, dep := range act.deps {
   289  		if dep.err != nil {
   290  			failed = append(failed, dep.String())
   291  		}
   292  	}
   293  	if failed != nil {
   294  		sort.Strings(failed)
   295  		act.err = fmt.Errorf("failed prerequisites: %s", strings.Join(failed, ", "))
   296  		return
   297  	}
   298  
   299  	// Plumb the output values of the dependencies
   300  	// into the inputs of this action.
   301  	inputs := make(map[*analysis.Analyzer]interface{})
   302  	for _, dep := range act.deps {
   303  		// Same package, different analysis (horizontal edge):
   304  		// in-memory outputs of prerequisite analyzers
   305  		// become inputs to this analysis pass.
   306  		inputs[dep.a] = dep.result
   307  	}
   308  
   309  	ignoreNolintReporter := func(d analysis.Diagnostic) {
   310  		pos := act.pkg.fset.Position(d.Pos)
   311  		for _, rng := range act.nolint {
   312  			// The list of nolint ranges is built for the entire package. Make sure we
   313  			// only apply ranges to the correct file.
   314  			if pos.Filename != rng.from.Filename {
   315  				continue
   316  			}
   317  			if pos.Line < rng.from.Line || pos.Line > rng.to {
   318  				continue
   319  			}
   320  			// Found a nolint range. Ignore the issue.
   321  			return
   322  		}
   323  		act.diagnostics = append(act.diagnostics, d)
   324  	}
   325  
   326  	// Run the analysis.
   327  	factFilter := make(map[reflect.Type]bool)
   328  	for _, f := range act.a.FactTypes {
   329  		factFilter[reflect.TypeOf(f)] = true
   330  	}
   331  	pass := &analysis.Pass{
   332  		Analyzer:          act.a,
   333  		Fset:              act.pkg.fset,
   334  		Files:             act.pkg.syntax,
   335  		Pkg:               act.pkg.types,
   336  		TypesInfo:         act.pkg.typesInfo,
   337  		ResultOf:          inputs,
   338  		Report:            ignoreNolintReporter,
   339  		ImportPackageFact: act.pkg.facts.ImportPackageFact,
   340  		ExportPackageFact: act.pkg.facts.ExportPackageFact,
   341  		ImportObjectFact:  act.pkg.facts.ImportObjectFact,
   342  		ExportObjectFact:  act.pkg.facts.ExportObjectFact,
   343  		AllPackageFacts:   func() []analysis.PackageFact { return act.pkg.facts.AllPackageFacts(factFilter) },
   344  		AllObjectFacts:    func() []analysis.ObjectFact { return act.pkg.facts.AllObjectFacts(factFilter) },
   345  		TypesSizes:        typesSizes,
   346  	}
   347  	act.pass = pass
   348  
   349  	var err error
   350  	if act.pkg.illTyped && !pass.Analyzer.RunDespiteErrors {
   351  		err = fmt.Errorf("analysis skipped due to type-checking error: %v", act.pkg.typeCheckError)
   352  	} else {
   353  		act.result, err = pass.Analyzer.Run(pass)
   354  		if err == nil {
   355  			if got, want := reflect.TypeOf(act.result), pass.Analyzer.ResultType; got != want {
   356  				err = fmt.Errorf(
   357  					"internal error: on package %s, analyzer %s returned a result of type %v, but declared ResultType %v",
   358  					pass.Pkg.Path(), pass.Analyzer, got, want)
   359  			}
   360  		}
   361  	}
   362  	act.err = err
   363  }
   364  
   365  // load parses and type checks the source code in each file in filenames.
   366  // load also deserializes facts stored for imported packages.
   367  func load(packagePath string, imp *importer, filenames []string) (*goPackage, error) {
   368  	if len(filenames) == 0 {
   369  		return nil, errors.New("no filenames")
   370  	}
   371  	var syntax []*ast.File
   372  	for _, file := range filenames {
   373  		s, err := parser.ParseFile(imp.fset, file, nil, parser.ParseComments)
   374  		if err != nil {
   375  			return nil, err
   376  		}
   377  		syntax = append(syntax, s)
   378  	}
   379  	pkg := &goPackage{fset: imp.fset, syntax: syntax}
   380  
   381  	config := types.Config{Importer: imp}
   382  	info := &types.Info{
   383  		Types:      make(map[ast.Expr]types.TypeAndValue),
   384  		Uses:       make(map[*ast.Ident]types.Object),
   385  		Defs:       make(map[*ast.Ident]types.Object),
   386  		Implicits:  make(map[ast.Node]types.Object),
   387  		Scopes:     make(map[ast.Node]*types.Scope),
   388  		Selections: make(map[*ast.SelectorExpr]*types.Selection),
   389  	}
   390  
   391  	initInstanceInfo(info)
   392  
   393  	types, err := config.Check(packagePath, pkg.fset, syntax, info)
   394  	if err != nil {
   395  		pkg.illTyped, pkg.typeCheckError = true, err
   396  	}
   397  	pkg.types, pkg.typesInfo = types, info
   398  
   399  	pkg.facts, err = facts.NewDecoder(pkg.types).Decode(imp.readFacts)
   400  	if err != nil {
   401  		return nil, fmt.Errorf("internal error decoding facts: %v", err)
   402  	}
   403  
   404  	return pkg, nil
   405  }
   406  
   407  // A goPackage describes a loaded Go package.
   408  type goPackage struct {
   409  	// fset provides position information for types, typesInfo, and syntax.
   410  	// It is set only when types is set.
   411  	fset *token.FileSet
   412  	// syntax is the package's syntax trees.
   413  	syntax []*ast.File
   414  	// types provides type information for the package.
   415  	types *types.Package
   416  	// facts contains information saved by the analysis framework. Passes may
   417  	// import facts for imported packages and may also export facts for this
   418  	// package to be consumed by analyses in downstream packages.
   419  	facts *facts.Set
   420  	// illTyped indicates whether the package or any dependency contains errors.
   421  	// It is set only when types is set.
   422  	illTyped bool
   423  	// typeCheckError contains any error encountered during type-checking. It is
   424  	// only set when illTyped is true.
   425  	typeCheckError error
   426  	// typesInfo provides type information about the package's syntax trees.
   427  	// It is set only when syntax is set.
   428  	typesInfo *types.Info
   429  }
   430  
   431  func (g *goPackage) String() string {
   432  	return g.types.Path()
   433  }
   434  
   435  // checkAnalysisResults checks the analysis diagnostics in the given actions
   436  // and returns a string containing all the diagnostics that should be printed
   437  // to the build log.
   438  func checkAnalysisResults(actions []*action, pkg *goPackage) string {
   439  	type entry struct {
   440  		analysis.Diagnostic
   441  		*analysis.Analyzer
   442  	}
   443  	var diagnostics []entry
   444  	var errs []error
   445  	for _, act := range actions {
   446  		if act.err != nil {
   447  			// Analyzer failed.
   448  			errs = append(errs, fmt.Errorf("analyzer %q failed: %v", act.a.Name, act.err))
   449  			continue
   450  		}
   451  		if len(act.diagnostics) == 0 {
   452  			continue
   453  		}
   454  		var currentConfig config
   455  		// Use the base config if it exists.
   456  		if baseConfig, ok := configs[nogoBaseConfigName]; ok {
   457  			currentConfig = baseConfig
   458  		}
   459  		// Overwrite the config with the desired config. Any unset fields
   460  		// in the config will default to the base config.
   461  		if actionConfig, ok := configs[act.a.Name]; ok {
   462  			if actionConfig.analyzerFlags != nil {
   463  				currentConfig.analyzerFlags = actionConfig.analyzerFlags
   464  			}
   465  			if actionConfig.onlyFiles != nil {
   466  				currentConfig.onlyFiles = actionConfig.onlyFiles
   467  			}
   468  			if actionConfig.excludeFiles != nil {
   469  				currentConfig.excludeFiles = actionConfig.excludeFiles
   470  			}
   471  		}
   472  
   473  		if currentConfig.onlyFiles == nil && currentConfig.excludeFiles == nil {
   474  			for _, diag := range act.diagnostics {
   475  				diagnostics = append(diagnostics, entry{Diagnostic: diag, Analyzer: act.a})
   476  			}
   477  			continue
   478  		}
   479  		// Discard diagnostics based on the analyzer configuration.
   480  		for _, d := range act.diagnostics {
   481  			// NOTE(golang.org/issue/31008): nilness does not set positions,
   482  			// so don't assume the position is valid.
   483  			p := pkg.fset.Position(d.Pos)
   484  			filename := "-"
   485  			if p.IsValid() {
   486  				filename = p.Filename
   487  			}
   488  			include := true
   489  			if len(currentConfig.onlyFiles) > 0 {
   490  				// This analyzer emits diagnostics for only a set of files.
   491  				include = false
   492  				for _, pattern := range currentConfig.onlyFiles {
   493  					if pattern.MatchString(filename) {
   494  						include = true
   495  						break
   496  					}
   497  				}
   498  			}
   499  			if include {
   500  				for _, pattern := range currentConfig.excludeFiles {
   501  					if pattern.MatchString(filename) {
   502  						include = false
   503  						break
   504  					}
   505  				}
   506  			}
   507  			if include {
   508  				diagnostics = append(diagnostics, entry{Diagnostic: d, Analyzer: act.a})
   509  			}
   510  		}
   511  	}
   512  	if len(diagnostics) == 0 && len(errs) == 0 {
   513  		return ""
   514  	}
   515  
   516  	sort.Slice(diagnostics, func(i, j int) bool {
   517  		return diagnostics[i].Pos < diagnostics[j].Pos
   518  	})
   519  	errMsg := &bytes.Buffer{}
   520  	sep := ""
   521  	for _, err := range errs {
   522  		errMsg.WriteString(sep)
   523  		sep = "\n"
   524  		errMsg.WriteString(err.Error())
   525  	}
   526  	for _, d := range diagnostics {
   527  		errMsg.WriteString(sep)
   528  		sep = "\n"
   529  		fmt.Fprintf(errMsg, "%s: %s (%s)", pkg.fset.Position(d.Pos), d.Message, d.Name)
   530  	}
   531  	return errMsg.String()
   532  }
   533  
   534  // config determines which source files an analyzer will emit diagnostics for.
   535  // config values are generated in another file that is compiled with
   536  // nogo_main.go by the nogo rule.
   537  type config struct {
   538  	// onlyFiles is a list of regular expressions that match files an analyzer
   539  	// will emit diagnostics for. When empty, the analyzer will emit diagnostics
   540  	// for all files.
   541  	onlyFiles []*regexp.Regexp
   542  
   543  	// excludeFiles is a list of regular expressions that match files that an
   544  	// analyzer will not emit diagnostics for.
   545  	excludeFiles []*regexp.Regexp
   546  
   547  	// analyzerFlags is a map of flag names to flag values which will be passed
   548  	// to Analyzer.Flags. Note that no leading '-' should be present in a flag
   549  	// name
   550  	analyzerFlags map[string]string
   551  }
   552  
   553  // importer is an implementation of go/types.Importer that imports type
   554  // information from the export data in compiled .a files.
   555  type importer struct {
   556  	fset         *token.FileSet
   557  	importMap    map[string]string         // map import path in source code to package path
   558  	packageCache map[string]*types.Package // cache of previously imported packages
   559  	packageFile  map[string]string         // map package path to .a file with export data
   560  	factMap      map[string]string         // map import path in source code to file containing serialized facts
   561  }
   562  
   563  func newImporter(importMap, packageFile map[string]string, factMap map[string]string) *importer {
   564  	return &importer{
   565  		fset:         token.NewFileSet(),
   566  		importMap:    importMap,
   567  		packageCache: make(map[string]*types.Package),
   568  		packageFile:  packageFile,
   569  		factMap:      factMap,
   570  	}
   571  }
   572  
   573  func (i *importer) Import(path string) (*types.Package, error) {
   574  	if imp, ok := i.importMap[path]; ok {
   575  		// Translate import path if necessary.
   576  		path = imp
   577  	}
   578  	if path == "unsafe" {
   579  		// Special case: go/types has pre-defined type information for unsafe.
   580  		// See https://github.com/golang/go/issues/13882.
   581  		return types.Unsafe, nil
   582  	}
   583  	if pkg, ok := i.packageCache[path]; ok && pkg.Complete() {
   584  		return pkg, nil // cache hit
   585  	}
   586  
   587  	archive, ok := i.packageFile[path]
   588  	if !ok {
   589  		return nil, fmt.Errorf("could not import %q", path)
   590  	}
   591  	// open file
   592  	f, err := os.Open(archive)
   593  	if err != nil {
   594  		return nil, err
   595  	}
   596  	defer func() {
   597  		f.Close()
   598  		if err != nil {
   599  			// add file name to error
   600  			err = fmt.Errorf("reading export data: %s: %v", archive, err)
   601  		}
   602  	}()
   603  
   604  	r, err := gcexportdata.NewReader(f)
   605  	if err != nil {
   606  		return nil, err
   607  	}
   608  
   609  	return gcexportdata.Read(r, i.fset, i.packageCache, path)
   610  }
   611  
   612  func (i *importer) readFacts(pkgPath string) ([]byte, error) {
   613  	facts := i.factMap[pkgPath]
   614  	if facts == "" {
   615  		// Packages that were not built with the nogo toolchain will not be
   616  		// analyzed, so there's no opportunity to store facts. This includes
   617  		// packages in the standard library and packages built with go_tool_library,
   618  		// such as coverdata. Analyzers are expected to hard code information
   619  		// about standard library definitions and must gracefully handle packages
   620  		// that don't have facts. For example, the "printf" analyzer must know
   621  		// fmt.Printf accepts a format string.
   622  		return nil, nil
   623  	}
   624  	return os.ReadFile(facts)
   625  }
   626  
   627  type factMultiFlag map[string]string
   628  
   629  func (m *factMultiFlag) String() string {
   630  	if m == nil || len(*m) == 0 {
   631  		return ""
   632  	}
   633  	return fmt.Sprintf("%v", *m)
   634  }
   635  
   636  func (m *factMultiFlag) Set(v string) error {
   637  	parts := strings.Split(v, "=")
   638  	if len(parts) != 2 {
   639  		return fmt.Errorf("badly formatted -fact flag: %s", v)
   640  	}
   641  	(*m)[parts[0]] = parts[1]
   642  	return nil
   643  }
   644  

View as plain text