@@ -21,6 +21,7 @@ import (
2121 "fmt"
2222 "os"
2323 "path/filepath"
24+ "slices"
2425 "strings"
2526
2627 "github.com/bufbuild/protocompile"
@@ -46,6 +47,7 @@ type cli struct {
4647 ProtoImportPaths []string
4748 ProtoFiles []string
4849 ProtoDescPath []string
50+ SkipCompilationFlag bool
4951 EnabledRules []string
5052 DisabledRules []string
5153 ListRulesFlag bool
@@ -67,6 +69,7 @@ func newCli(args []string) *cli {
6769 var versionFlag bool
6870 var protoImportFlag []string
6971 var protoDescFlag []string
72+ var skipCompilationFlag bool
7073 var ruleEnableFlag []string
7174 var ruleDisableFlag []string
7275 var listRulesFlag bool
@@ -81,7 +84,8 @@ func newCli(args []string) *cli {
8184 fs .BoolVar (& setExitStatusOnLintFailure , "set-exit-status" , false , "Return exit status 1 when lint errors are found." )
8285 fs .BoolVar (& versionFlag , "version" , false , "Print version and exit." )
8386 fs .StringArrayVarP (& protoImportFlag , "proto-path" , "I" , nil , "The folder for searching proto imports.\n May be specified multiple times; directories will be searched in order.\n The current working directory is always used." )
84- fs .StringArrayVar (& protoDescFlag , "descriptor-set-in" , nil , "The file containing a FileDescriptorSet for searching proto imports.\n May be specified multiple times." )
87+ fs .StringArrayVar (& protoDescFlag , "descriptor-set-in" , nil , "The file containing a FileDescriptorSet for searching proto imports.\n May be specified multiple times.\n Also used as the source of proto files to lint when --skip-compilation is enabled." )
88+ fs .BoolVar (& skipCompilationFlag , "skip-compilation" , false , "Skip the compilation of the proto files and instead use the provided descriptor set to look up the files to lint. When using this flag, the provided descriptor set must contain the files to be linted and should have been compiled with --include_source_info and --include_imports." )
8589 fs .StringArrayVar (& ruleEnableFlag , "enable-rule" , nil , "Enable a rule with the given name.\n May be specified multiple times." )
8690 fs .StringArrayVar (& ruleDisableFlag , "disable-rule" , nil , "Disable a rule with the given name.\n May be specified multiple times." )
8791 fs .BoolVar (& listRulesFlag , "list-rules" , false , "Print the rules and exit. Honors the output-format flag." )
@@ -101,6 +105,7 @@ func newCli(args []string) *cli {
101105 ExitStatusOnLintFailure : setExitStatusOnLintFailure ,
102106 ProtoImportPaths : protoImportFlag ,
103107 ProtoDescPath : protoDescFlag ,
108+ SkipCompilationFlag : skipCompilationFlag ,
104109 EnabledRules : ruleEnableFlag ,
105110 DisabledRules : ruleDisableFlag ,
106111 ProtoFiles : fs .Args (),
@@ -144,10 +149,93 @@ func (c *cli) lint(rules lint.RuleRegistry, configs lint.Configs) error {
144149 })
145150 }
146151
152+ var fileDescriptors []protoreflect.FileDescriptor
153+ var err error
154+ if c .SkipCompilationFlag {
155+ fileDescriptors , err = c .getDescriptorsFromDescriptorSet ()
156+ } else {
157+ fileDescriptors , err = c .getDescriptorsFromSource ()
158+ }
159+ if err != nil {
160+ return err
161+ }
162+
163+ // Create a linter to lint the file descriptors.
164+ l := lint .New (rules , configs , lint .Debug (c .DebugFlag ), lint .IgnoreCommentDisables (c .IgnoreCommentDisablesFlag ))
165+ results , err := l .LintProtos (fileDescriptors ... )
166+ if err != nil {
167+ return err
168+ }
169+
170+ // Determine the output for writing the results.
171+ // Stdout is the default output.
172+ w := os .Stdout
173+ if c .OutputPath != "" {
174+ var err error
175+ w , err = os .Create (c .OutputPath )
176+ if err != nil {
177+ return err
178+ }
179+ defer w .Close ()
180+ }
181+
182+ // Determine the format for printing the results.
183+ // YAML format is the default.
184+ marshal := getOutputFormatFunc (c .FormatType )
185+
186+ // Print the results.
187+ b , err := marshal (results )
188+ if err != nil {
189+ return err
190+ }
191+ if _ , err = w .Write (b ); err != nil {
192+ return err
193+ }
194+
195+ // Return error on lint failure which subsequently
196+ // exits with a non-zero status code
197+ if c .ExitStatusOnLintFailure && anyProblems (results ) {
198+ return ExitForLintFailure
199+ }
200+
201+ return nil
202+ }
203+
204+ func (c * cli ) getDescriptorsFromDescriptorSet () ([]protoreflect.FileDescriptor , error ) {
205+ if len (c .ProtoDescPath ) == 0 {
206+ return nil , fmt .Errorf ("no descriptor set found" )
207+ }
208+
209+ files , err := createRegistryFromDescriptorSets (c .ProtoDescPath ... )
210+ if err != nil {
211+ return nil , err
212+ }
213+
214+ var fileDescriptors []protoreflect.FileDescriptor
215+ // Iterate over the files in the registry and append them to fileDescriptors.
216+ files .RangeFiles (func (fd protoreflect.FileDescriptor ) bool {
217+ if slices .Contains (c .ProtoFiles , fd .Path ()) {
218+ fileDescriptors = append (fileDescriptors , fd )
219+ }
220+ return true // continue iteration
221+ })
222+
223+ if len (fileDescriptors ) < len (c .ProtoFiles ) {
224+ var filenames []string
225+ for _ , fd := range fileDescriptors {
226+ filenames = append (filenames , fd .Path ())
227+ }
228+ return nil , fmt .Errorf ("files found in descriptors %v, files requested for linting %v" , filenames , c .ProtoFiles )
229+ }
230+
231+ return fileDescriptors , nil
232+ }
233+
234+ func (c * cli ) getDescriptorsFromSource () ([]protoreflect.FileDescriptor , error ) {
147235 // Create resolver for descriptor sets.
148236 descResolver , err := loadFileDescriptorsAsResolver (c .ProtoDescPath ... )
149237 if err != nil {
150- return err
238+ return nil , err
151239 }
152240
153241 // Create resolver for source files.
@@ -199,66 +287,27 @@ func (c *cli) lint(rules lint.RuleRegistry, configs lint.Configs) error {
199287 for i , e := range collectedErrors {
200288 errorStrings [i ] = e .Error ()
201289 }
202- return errors .New (strings .Join (errorStrings , "\n " ))
290+ return nil , errors .New (strings .Join (errorStrings , "\n " ))
203291 }
204292
205293 // If the reporter has no errors, but the compiler still returned one,
206294 // it's a fatal, non-recoverable error.
207295 if err != nil {
208- return err
296+ return nil , err
209297 }
210298 // Append the compiled file(s) to the slice.
211299 compiledFiles = append (compiledFiles , f ... )
212300 }
213301 files := compiledFiles
214302
303+ var fileDescriptors []protoreflect.FileDescriptor
215304 // The compiler returns a slice of `*linker.File`, which is the compiler's
216305 // internal representation. We convert this to a slice of the standard
217306 // `protoreflect.FileDescriptor` interface, which the linter engine expects.
218- var fileDescriptors []protoreflect.FileDescriptor
219307 for _ , f := range files {
220308 fileDescriptors = append (fileDescriptors , f )
221309 }
222-
223- // Create a linter to lint the file descriptors.
224- l := lint .New (rules , configs , lint .Debug (c .DebugFlag ), lint .IgnoreCommentDisables (c .IgnoreCommentDisablesFlag ))
225- results , err := l .LintProtos (fileDescriptors ... )
226- if err != nil {
227- return err
228- }
229-
230- // Determine the output for writing the results.
231- // Stdout is the default output.
232- w := os .Stdout
233- if c .OutputPath != "" {
234- var err error
235- w , err = os .Create (c .OutputPath )
236- if err != nil {
237- return err
238- }
239- defer w .Close ()
240- }
241-
242- // Determine the format for printing the results.
243- // YAML format is the default.
244- marshal := getOutputFormatFunc (c .FormatType )
245-
246- // Print the results.
247- b , err := marshal (results )
248- if err != nil {
249- return err
250- }
251- if _ , err = w .Write (b ); err != nil {
252- return err
253- }
254-
255- // Return error on lint failure which subsequently
256- // exits with a non-zero status code
257- if c .ExitStatusOnLintFailure && anyProblems (results ) {
258- return ExitForLintFailure
259- }
260-
261- return nil
310+ return fileDescriptors , nil
262311}
263312
264313func anyProblems (results []lint.Response ) bool {
@@ -293,6 +342,18 @@ func (r *resolver) FindFileByPath(path string) (protocompile.SearchResult, error
293342// object. It then wraps this object in our custom resolver so that it can be
294343// used by the protocompile.Compiler to resolve imports.
295344func loadFileDescriptorsAsResolver (filePaths ... string ) (protocompile.Resolver , error ) {
345+ files , err := createRegistryFromDescriptorSets (filePaths ... )
346+ if err != nil {
347+ return nil , err
348+ }
349+ // Returning nil is safe as callers check for nil before using the resolver.
350+ if files == nil {
351+ return nil , nil
352+ }
353+ return & resolver {files : files }, nil
354+ }
355+
356+ func createRegistryFromDescriptorSets (filePaths ... string ) (* protoregistry.Files , error ) {
296357 if len (filePaths ) == 0 {
297358 return nil , nil
298359 }
@@ -318,7 +379,7 @@ func loadFileDescriptorsAsResolver(filePaths ...string) (protocompile.Resolver,
318379 if err != nil {
319380 return nil , fmt .Errorf ("failed to create protoregistry.Files: %w" , err )
320381 }
321- return & resolver { files : files } , nil
382+ return files , nil
322383}
323384
324385func readFileDescriptorSet (filePath string ) (* dpb.FileDescriptorSet , error ) {
0 commit comments