@@ -20,15 +20,11 @@ import (
2020 "fmt"
2121 "os"
2222 "strings"
23- "sync"
2423
2524 "github.com/aep-dev/api-linter/internal"
2625 "github.com/aep-dev/api-linter/lint"
27- "github.com/jhump/protoreflect/desc"
28- "github.com/jhump/protoreflect/desc/protoparse"
26+ lint_v2 "github.com/aep-dev/api-linter/lint/v2"
2927 "github.com/spf13/pflag"
30- "google.golang.org/protobuf/proto"
31- dpb "google.golang.org/protobuf/types/descriptorpb"
3228 "gopkg.in/yaml.v2"
3329)
3430
@@ -106,7 +102,7 @@ func newCli(args []string) *cli {
106102 }
107103}
108104
109- func (c * cli ) lint (rules lint.RuleRegistry , configs lint.Configs ) error {
105+ func (c * cli ) lint (rulesV1 lint. RuleRegistry , rulesV2 lint_v2 .RuleRegistry , configs lint.Configs ) error {
110106 // Print version and exit if asked.
111107 if c .VersionFlag {
112108 fmt .Printf ("api-linter %s\n " , internal .Version )
@@ -137,60 +133,30 @@ func (c *cli) lint(rules lint.RuleRegistry, configs lint.Configs) error {
137133 configs = append (configs , lint.Config {
138134 DisabledRules : c .DisabledRules ,
139135 })
140- // Prepare proto import lookup.
141- fs , err := loadFileDescriptors (c .ProtoDescPath ... )
136+
137+ // V1 Pipeline
138+ resultsV1 , err := c .runV1 (rulesV1 , configs )
142139 if err != nil {
143140 return err
144141 }
145- lookupImport := func (name string ) (* desc.FileDescriptor , error ) {
146- if f , found := fs [name ]; found {
147- return f , nil
148- }
149- return nil , fmt .Errorf ("%q is not found" , name )
150- }
151- var errorsWithPos []protoparse.ErrorWithPos
152- var lock sync.Mutex
153- // Parse proto files into `protoreflect` file descriptors.
154- p := protoparse.Parser {
155- ImportPaths : c .ProtoImportPaths ,
156- IncludeSourceCodeInfo : true ,
157- LookupImport : lookupImport ,
158- ErrorReporter : func (errorWithPos protoparse.ErrorWithPos ) error {
159- // Protoparse isn't concurrent right now but just to be safe for the future.
160- lock .Lock ()
161- errorsWithPos = append (errorsWithPos , errorWithPos )
162- lock .Unlock ()
163- // Continue parsing. The error returned will be protoparse.ErrInvalidSource.
164- return nil
165- },
166- }
167- // Resolve file absolute paths to relative ones.
168- protoFiles , err := protoparse .ResolveFilenames (c .ProtoImportPaths , c .ProtoFiles ... )
169- if err != nil {
170- return err
142+
143+ // V2 Pipeline
144+ var configsV2 lint_v2.Configs
145+ for _ , cfg := range configs {
146+ configsV2 = append (configsV2 , lint_v2.Config {
147+ IncludedPaths : cfg .IncludedPaths ,
148+ ExcludedPaths : cfg .ExcludedPaths ,
149+ EnabledRules : cfg .EnabledRules ,
150+ DisabledRules : cfg .DisabledRules ,
151+ })
171152 }
172- fd , err := p . ParseFiles ( protoFiles ... )
153+ resultsV2 , err := c . runV2 ( rulesV2 , configsV2 )
173154 if err != nil {
174- if err == protoparse .ErrInvalidSource {
175- if len (errorsWithPos ) == 0 {
176- return errors .New ("got protoparse.ErrInvalidSource but no ErrorWithPos errors" )
177- }
178- // TODO: There's multiple ways to deal with this but this prints all the errors at least
179- errStrings := make ([]string , len (errorsWithPos ))
180- for i , errorWithPos := range errorsWithPos {
181- errStrings [i ] = errorWithPos .Error ()
182- }
183- return errors .New (strings .Join (errStrings , "\n " ))
184- }
185- return err
155+ return fmt .Errorf ("V2 pipeline failed: %w" , err )
186156 }
187157
188- // Create a linter to lint the file descriptors.
189- l := lint .New (rules , configs , lint .Debug (c .DebugFlag ), lint .IgnoreCommentDisables (c .IgnoreCommentDisablesFlag ))
190- results , err := l .LintProtos (fd ... )
191- if err != nil {
192- return err
193- }
158+ // Combine results from both pipelines, preserving file order.
159+ results := combineResponses (resultsV1 , resultsV2 )
194160
195161 // Determine the output for writing the results.
196162 // Stdout is the default output.
@@ -226,37 +192,124 @@ func (c *cli) lint(rules lint.RuleRegistry, configs lint.Configs) error {
226192 return nil
227193}
228194
229- func anyProblems (results []lint. Response ) bool {
230- for i := range results {
231- if len ( results [ i ]. Problems ) > 0 {
195+ func anyProblems (results []combinedResponse ) bool {
196+ for _ , r := range results {
197+ if r . hasProblems () {
232198 return true
233199 }
234200 }
235201 return false
236202}
237203
238- func loadFileDescriptors (filePaths ... string ) (map [string ]* desc.FileDescriptor , error ) {
239- fds := []* dpb.FileDescriptorProto {}
240- for _ , filePath := range filePaths {
241- fs , err := readFileDescriptorSet (filePath )
242- if err != nil {
243- return nil , err
204+ // combinedResponse holds lint results from both V1 and V2 pipelines for a
205+ // single file. It preserves the concrete problem types so that custom
206+ // MarshalJSON/MarshalYAML methods on each Problem type are invoked correctly.
207+ type combinedResponse struct {
208+ FilePath string
209+ ProblemsV1 []lint.Problem
210+ ProblemsV2 []lint_v2.Problem
211+ }
212+
213+ // MarshalJSON produces output compatible with the original lint.Response format.
214+ func (r combinedResponse ) MarshalJSON () ([]byte , error ) {
215+ problems := make ([]interface {}, 0 , len (r .ProblemsV1 )+ len (r .ProblemsV2 ))
216+ for _ , p := range r .ProblemsV1 {
217+ problems = append (problems , p )
218+ }
219+ for _ , p := range r .ProblemsV2 {
220+ problems = append (problems , p )
221+ }
222+ return json .Marshal (struct {
223+ FilePath string `json:"file_path"`
224+ Problems []interface {} `json:"problems"`
225+ }{r .FilePath , problems })
226+ }
227+
228+ // MarshalYAML produces output compatible with the original lint.Response format.
229+ func (r combinedResponse ) MarshalYAML () (interface {}, error ) {
230+ problems := make ([]interface {}, 0 , len (r .ProblemsV1 )+ len (r .ProblemsV2 ))
231+ for _ , p := range r .ProblemsV1 {
232+ problems = append (problems , p )
233+ }
234+ for _ , p := range r .ProblemsV2 {
235+ problems = append (problems , p )
236+ }
237+ return struct {
238+ FilePath string `yaml:"file_path"`
239+ Problems []interface {} `yaml:"problems"`
240+ }{r .FilePath , problems }, nil
241+ }
242+
243+ func (r combinedResponse ) hasProblems () bool {
244+ return len (r .ProblemsV1 ) > 0 || len (r .ProblemsV2 ) > 0
245+ }
246+
247+ // problemInfo provides a unified view of a problem for format functions
248+ // (github, summary) that need to access problem fields directly.
249+ type problemInfo struct {
250+ RuleID string
251+ Message string
252+ Span []int32 // From Location.Span; nil if no location.
253+ RuleURI string
254+ }
255+
256+ // allProblems returns a unified list of problem info from both V1 and V2 problems.
257+ func (r combinedResponse ) allProblems () []problemInfo {
258+ result := make ([]problemInfo , 0 , len (r .ProblemsV1 )+ len (r .ProblemsV2 ))
259+ for _ , p := range r .ProblemsV1 {
260+ var span []int32
261+ if p .Location != nil {
262+ span = p .Location .Span
244263 }
245- fds = append (fds , fs .GetFile ()... )
264+ result = append (result , problemInfo {
265+ RuleID : string (p .RuleID ),
266+ Message : p .Message ,
267+ Span : span ,
268+ RuleURI : p .GetRuleURI (),
269+ })
246270 }
247- return desc .CreateFileDescriptors (fds )
271+ for _ , p := range r .ProblemsV2 {
272+ var span []int32
273+ if p .Location != nil {
274+ span = p .Location .Span
275+ }
276+ result = append (result , problemInfo {
277+ RuleID : string (p .RuleID ),
278+ Message : p .Message ,
279+ Span : span ,
280+ RuleURI : p .GetRuleURI (),
281+ })
282+ }
283+ return result
248284}
249285
250- func readFileDescriptorSet (filePath string ) (* dpb.FileDescriptorSet , error ) {
251- in , err := os .ReadFile (filePath )
252- if err != nil {
253- return nil , err
286+ // combineResponses merges V1 and V2 responses by file path, preserving
287+ // the input file ordering (V1 order first, then any V2-only files).
288+ func combineResponses (v1 []lint.Response , v2 []lint_v2.Response ) []combinedResponse {
289+ order := make ([]string , 0 )
290+ byFile := make (map [string ]* combinedResponse )
291+
292+ for _ , r := range v1 {
293+ if _ , ok := byFile [r .FilePath ]; ! ok {
294+ order = append (order , r .FilePath )
295+ byFile [r .FilePath ] = & combinedResponse {FilePath : r .FilePath }
296+ }
297+ byFile [r .FilePath ].ProblemsV1 = append (byFile [r .FilePath ].ProblemsV1 , r .Problems ... )
254298 }
255- fs := & dpb.FileDescriptorSet {}
256- if err := proto .Unmarshal (in , fs ); err != nil {
257- return nil , err
299+
300+ for _ , r := range v2 {
301+ if _ , ok := byFile [r .FilePath ]; ! ok {
302+ order = append (order , r .FilePath )
303+ byFile [r .FilePath ] = & combinedResponse {FilePath : r .FilePath }
304+ }
305+ byFile [r .FilePath ].ProblemsV2 = append (byFile [r .FilePath ].ProblemsV2 , r .Problems ... )
306+ }
307+
308+ result := make ([]combinedResponse , 0 , len (order ))
309+ for _ , fp := range order {
310+ result = append (result , * byFile [fp ])
258311 }
259- return fs , nil
312+ return result
260313}
261314
262315var outputFormatFuncs = map [string ]formatFunc {
@@ -265,15 +318,15 @@ var outputFormatFuncs = map[string]formatFunc{
265318 "json" : json .Marshal ,
266319 "github" : func (i interface {}) ([]byte , error ) {
267320 switch v := i .(type ) {
268- case []lint. Response :
321+ case []combinedResponse :
269322 return formatGitHubActionOutput (v ), nil
270323 default :
271324 return json .Marshal (v )
272325 }
273326 },
274327 "summary" : func (i interface {}) ([]byte , error ) {
275328 switch v := i .(type ) {
276- case []lint. Response :
329+ case []combinedResponse :
277330 return printSummaryTable (v )
278331 case listedRules :
279332 return v .printSummaryTable ()
0 commit comments