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 }