github.com/yoheimuta/protolint@v0.49.8-0.20240515023657-4ecaebb7575d/internal/cmd/subcmds/lint/cmdLint.go (about)

     1  package lint
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"log"
     7  	"path/filepath"
     8  
     9  	"github.com/hashicorp/go-plugin"
    10  
    11  	"github.com/yoheimuta/go-protoparser/v4/parser"
    12  
    13  	"github.com/yoheimuta/protolint/internal/linter/config"
    14  
    15  	"github.com/yoheimuta/protolint/internal/linter"
    16  	"github.com/yoheimuta/protolint/internal/linter/file"
    17  	"github.com/yoheimuta/protolint/internal/osutil"
    18  	"github.com/yoheimuta/protolint/linter/report"
    19  )
    20  
    21  // CmdLint is a lint command.
    22  type CmdLint struct {
    23  	l          *linter.Linter
    24  	stdout     io.Writer
    25  	stderr     io.Writer
    26  	protoFiles []file.ProtoFile
    27  	config     CmdLintConfig
    28  	output     io.Writer
    29  }
    30  
    31  // NewCmdLint creates a new CmdLint.
    32  func NewCmdLint(
    33  	flags Flags,
    34  	stdout io.Writer,
    35  	stderr io.Writer,
    36  ) (*CmdLint, error) {
    37  	protoSet, err := file.NewProtoSet(flags.FilePaths)
    38  	if err != nil {
    39  		return nil, err
    40  	}
    41  
    42  	externalConfig, err := config.GetExternalConfig(flags.ConfigPath, flags.ConfigDirPath)
    43  	if err != nil {
    44  		return nil, err
    45  	}
    46  	if flags.Verbose {
    47  		if externalConfig != nil {
    48  			log.Printf("[INFO] protolint loads a config file at %s\n", externalConfig.SourcePath)
    49  		} else {
    50  			log.Println("[INFO] protolint doesn't load a config file")
    51  		}
    52  	}
    53  	if externalConfig == nil {
    54  		externalConfig = &(config.ExternalConfig{})
    55  	}
    56  	lintConfig := NewCmdLintConfig(
    57  		*externalConfig,
    58  		flags,
    59  	)
    60  
    61  	output := stderr
    62  
    63  	return &CmdLint{
    64  		l:          linter.NewLinter(),
    65  		stdout:     stdout,
    66  		stderr:     stderr,
    67  		protoFiles: protoSet.ProtoFiles(),
    68  		config:     lintConfig,
    69  		output:     output,
    70  	}, nil
    71  }
    72  
    73  // Run lints to proto files.
    74  func (c *CmdLint) Run() osutil.ExitCode {
    75  	defer plugin.CleanupClients()
    76  
    77  	failures, err := c.run()
    78  	if err != nil {
    79  		_, _ = fmt.Fprintln(c.stderr, err)
    80  		return osutil.ExitInternalFailure
    81  	}
    82  
    83  	err = c.config.reporters.ReportWithFallback(c.output, failures)
    84  	if err != nil {
    85  		_, _ = fmt.Fprintln(c.stderr, err)
    86  		return osutil.ExitInternalFailure
    87  	}
    88  
    89  	if 0 < len(failures) {
    90  		return osutil.ExitLintFailure
    91  	}
    92  
    93  	return osutil.ExitSuccess
    94  }
    95  
    96  func (c *CmdLint) run() ([]report.Failure, error) {
    97  	var allFailures []report.Failure
    98  
    99  	for _, f := range c.protoFiles {
   100  		failures, err := c.runOneFile(f)
   101  		if err != nil {
   102  			return nil, err
   103  		}
   104  		allFailures = append(allFailures, failures...)
   105  	}
   106  	return allFailures, nil
   107  }
   108  
   109  // ParseError represents the error returned through a parsing exception.
   110  type ParseError struct {
   111  	Message string
   112  }
   113  
   114  func (p ParseError) Error() string {
   115  	return p.Message
   116  }
   117  
   118  func (c *CmdLint) runOneFile(
   119  	f file.ProtoFile,
   120  ) ([]report.Failure, error) {
   121  	// Gen rules first
   122  	// If there is no rule, we can skip parse proto file
   123  	rs, err := c.config.GenRules(f)
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  	if len(rs) == 0 {
   128  		return []report.Failure{}, nil
   129  	}
   130  
   131  	return c.l.Run(func(p *parser.Proto) (*parser.Proto, error) {
   132  		// Recreate a protoFile if the previous rule changed the filename.
   133  		if p != nil && p.Meta.Filename != f.DisplayPath() {
   134  			newFilename := p.Meta.Filename
   135  			newBase := filepath.Base(newFilename)
   136  			f = file.NewProtoFile(filepath.Join(filepath.Dir(f.Path()), newBase), newFilename)
   137  		}
   138  
   139  		proto, err := f.Parse(c.config.verbose)
   140  		if err != nil {
   141  			if c.config.verbose {
   142  				return nil, ParseError{Message: err.Error()}
   143  			}
   144  			return nil, ParseError{Message: fmt.Sprintf("%s. Use -v for more details", err)}
   145  		}
   146  		return proto, nil
   147  	}, rs)
   148  }