Skip to content

Commit 639ab43

Browse files
committed
fix: barry quick fix, 2025-06-24 22:58:25
1 parent 2f29a1d commit 639ab43

File tree

6 files changed

+546
-25
lines changed

6 files changed

+546
-25
lines changed

cmd/linter/cli.go

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
// Copyright 2019 Google LLC
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+
// https://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+
package main
16+
17+
import (
18+
"encoding/json"
19+
"errors"
20+
"fmt"
21+
"os"
22+
"strings"
23+
"sync"
24+
25+
"github.com/googleapis/api-linter/lint"
26+
"github.com/jhump/protoreflect/desc"
27+
"github.com/jhump/protoreflect/desc/protoparse"
28+
"github.com/spf13/pflag"
29+
"google.golang.org/protobuf/proto"
30+
dpb "google.golang.org/protobuf/types/descriptorpb"
31+
"gopkg.in/yaml.v3"
32+
)
33+
34+
type cli struct {
35+
ConfigPath string
36+
FormatType string
37+
OutputPath string
38+
ExitStatusOnLintFailure bool
39+
VersionFlag bool
40+
ProtoImportPaths []string
41+
ProtoFiles []string
42+
ProtoDescPath []string
43+
EnabledRules []string
44+
DisabledRules []string
45+
ListRulesFlag bool
46+
DebugFlag bool
47+
IgnoreCommentDisablesFlag bool
48+
}
49+
50+
// ExitForLintFailure indicates that a problem was found during linting.
51+
//
52+
//lint:ignore ST1012 modifying this variable name is a breaking change.
53+
var ExitForLintFailure = errors.New("found problems during linting")
54+
55+
func newCli(args []string) *cli {
56+
// Define flag variables.
57+
var cfgFlag string
58+
var fmtFlag string
59+
var outFlag string
60+
var setExitStatusOnLintFailure bool
61+
var versionFlag bool
62+
var protoImportFlag []string
63+
var protoDescFlag []string
64+
var ruleEnableFlag []string
65+
var ruleDisableFlag []string
66+
var listRulesFlag bool
67+
var debugFlag bool
68+
var ignoreCommentDisablesFlag bool
69+
70+
// Register flag variables.
71+
fs := pflag.NewFlagSet("api-linter", pflag.ExitOnError)
72+
fs.StringVar(&cfgFlag, "config", "", "The linter config file.")
73+
fs.StringVar(&fmtFlag, "output-format", "", "The format of the linting results.\nSupported formats include \"yaml\", \"json\",\"github\" and \"summary\" table.\nYAML is the default.")
74+
fs.StringVarP(&outFlag, "output-path", "o", "", "The output file path.\nIf not given, the linting results will be printed out to STDOUT.")
75+
fs.BoolVar(&setExitStatusOnLintFailure, "set-exit-status", false, "Return exit status 1 when lint errors are found.")
76+
fs.BoolVar(&versionFlag, "version", false, "Print version and exit.")
77+
fs.StringArrayVarP(&protoImportFlag, "proto-path", "I", nil, "The folder for searching proto imports.\nMay be specified multiple times; directories will be searched in order.\nThe current working directory is always used.")
78+
fs.StringArrayVar(&protoDescFlag, "descriptor-set-in", nil, "The file containing a FileDescriptorSet for searching proto imports.\nMay be specified multiple times.")
79+
fs.StringArrayVar(&ruleEnableFlag, "enable-rule", nil, "Enable a rule with the given name.\nMay be specified multiple times.")
80+
fs.StringArrayVar(&ruleDisableFlag, "disable-rule", nil, "Disable a rule with the given name.\nMay be specified multiple times.")
81+
fs.BoolVar(&listRulesFlag, "list-rules", false, "Print the rules and exit. Honors the output-format flag.")
82+
fs.BoolVar(&debugFlag, "debug", false, "Run in debug mode. Panics will print stack.")
83+
fs.BoolVar(&ignoreCommentDisablesFlag, "ignore-comment-disables", false, "If set to true, disable comments will be ignored.\nThis is helpful when strict enforcement of AIPs are necessary and\nproto definitions should not be able to disable checks.")
84+
85+
// Parse flags.
86+
err := fs.Parse(args)
87+
if err != nil {
88+
panic(err)
89+
}
90+
91+
return &cli{
92+
ConfigPath: cfgFlag,
93+
FormatType: fmtFlag,
94+
OutputPath: outFlag,
95+
ExitStatusOnLintFailure: setExitStatusOnLintFailure,
96+
ProtoImportPaths: protoImportFlag,
97+
ProtoDescPath: protoDescFlag,
98+
EnabledRules: ruleEnableFlag,
99+
DisabledRules: ruleDisableFlag,
100+
ProtoFiles: fs.Args(),
101+
VersionFlag: versionFlag,
102+
ListRulesFlag: listRulesFlag,
103+
DebugFlag: debugFlag,
104+
IgnoreCommentDisablesFlag: ignoreCommentDisablesFlag,
105+
}
106+
}
107+
108+
func (c *cli) lint(rules lint.RuleRegistry, configs lint.Configs) error {
109+
// Print version and exit if asked.
110+
if c.VersionFlag {
111+
return nil
112+
}
113+
114+
if c.ListRulesFlag {
115+
return outputRules(c.FormatType)
116+
}
117+
118+
// Pre-check if there are files to lint.
119+
if len(c.ProtoFiles) == 0 {
120+
return fmt.Errorf("no file to lint")
121+
}
122+
// Read linter config and append it to the default.
123+
if c.ConfigPath != "" {
124+
config, err := lint.ReadConfigsFromFile(c.ConfigPath)
125+
if err != nil {
126+
return err
127+
}
128+
configs = append(configs, config...)
129+
}
130+
// Add configs for the enabled rules.
131+
configs = append(configs, lint.Config{
132+
EnabledRules: c.EnabledRules,
133+
})
134+
// Add configs for the disabled rules.
135+
configs = append(configs, lint.Config{
136+
DisabledRules: c.DisabledRules,
137+
})
138+
// Prepare proto import lookup.
139+
fs, err := loadFileDescriptors(c.ProtoDescPath...)
140+
if err != nil {
141+
return err
142+
}
143+
lookupImport := func(name string) (*desc.FileDescriptor, error) {
144+
if f, found := fs[name]; found {
145+
return f, nil
146+
}
147+
return nil, fmt.Errorf("%q is not found", name)
148+
}
149+
var errorsWithPos []protoparse.ErrorWithPos
150+
var lock sync.Mutex
151+
// Parse proto files into `protoreflect` file descriptors.
152+
p := protoparse.Parser{
153+
ImportPaths: append(c.ProtoImportPaths, "."),
154+
IncludeSourceCodeInfo: true,
155+
LookupImport: lookupImport,
156+
ErrorReporter: func(errorWithPos protoparse.ErrorWithPos) error {
157+
// Protoparse isn't concurrent right now but just to be safe for the future.
158+
lock.Lock()
159+
errorsWithPos = append(errorsWithPos, errorWithPos)
160+
lock.Unlock()
161+
// Continue parsing. The error returned will be protoparse.ErrInvalidSource.
162+
return nil
163+
},
164+
}
165+
// Resolve file absolute paths to relative ones.
166+
// Using supplied import paths first.
167+
protoFiles := c.ProtoFiles
168+
if len(c.ProtoImportPaths) > 0 {
169+
protoFiles, err = protoparse.ResolveFilenames(c.ProtoImportPaths, c.ProtoFiles...)
170+
if err != nil {
171+
return err
172+
}
173+
}
174+
// Then resolve again against ".", the local directory.
175+
// This is necessary because ResolveFilenames won't resolve a path if it
176+
// relative to *at least one* of the given import paths, which can result
177+
// in duplicate file parsing and compilation errors, as seen in #1465 and
178+
// #1471. So we resolve against local (default) and flag specified import
179+
// paths separately.
180+
protoFiles, err = protoparse.ResolveFilenames([]string{"."}, protoFiles...)
181+
if err != nil {
182+
return err
183+
}
184+
fd, err := p.ParseFiles(protoFiles...)
185+
if err != nil {
186+
if err == protoparse.ErrInvalidSource {
187+
if len(errorsWithPos) == 0 {
188+
return errors.New("got protoparse.ErrInvalidSource but no ErrorWithPos errors")
189+
}
190+
// TODO: There's multiple ways to deal with this but this prints all the errors at least
191+
errStrings := make([]string, len(errorsWithPos))
192+
for i, errorWithPos := range errorsWithPos {
193+
errStrings[i] = errorWithPos.Error()
194+
}
195+
return errors.New(strings.Join(errStrings, "\n"))
196+
}
197+
return err
198+
}
199+
200+
// Create a linter to lint the file descriptors.
201+
l := lint.New(rules, configs, lint.Debug(c.DebugFlag), lint.IgnoreCommentDisables(c.IgnoreCommentDisablesFlag))
202+
results, err := l.LintProtos(fd...)
203+
if err != nil {
204+
return err
205+
}
206+
207+
// Determine the output for writing the results.
208+
// Stdout is the default output.
209+
w := os.Stdout
210+
if c.OutputPath != "" {
211+
var err error
212+
w, err = os.Create(c.OutputPath)
213+
if err != nil {
214+
return err
215+
}
216+
defer w.Close()
217+
}
218+
219+
// Determine the format for printing the results.
220+
// YAML format is the default.
221+
marshal := getOutputFormatFunc(c.FormatType)
222+
223+
// Print the results.
224+
b, err := marshal(results)
225+
if err != nil {
226+
return err
227+
}
228+
if _, err = w.Write(b); err != nil {
229+
return err
230+
}
231+
232+
// Return error on lint failure which subsequently
233+
// exits with a non-zero status code
234+
if c.ExitStatusOnLintFailure && anyProblems(results) {
235+
return ExitForLintFailure
236+
}
237+
238+
return nil
239+
}
240+
241+
func anyProblems(results []lint.Response) bool {
242+
for i := range results {
243+
if len(results[i].Problems) > 0 {
244+
return true
245+
}
246+
}
247+
return false
248+
}
249+
250+
func loadFileDescriptors(filePaths ...string) (map[string]*desc.FileDescriptor, error) {
251+
fds := []*dpb.FileDescriptorProto{}
252+
for _, filePath := range filePaths {
253+
fs, err := readFileDescriptorSet(filePath)
254+
if err != nil {
255+
return nil, err
256+
}
257+
fds = append(fds, fs.GetFile()...)
258+
}
259+
return desc.CreateFileDescriptors(fds)
260+
}
261+
262+
func readFileDescriptorSet(filePath string) (*dpb.FileDescriptorSet, error) {
263+
in, err := os.ReadFile(filePath)
264+
if err != nil {
265+
return nil, err
266+
}
267+
fs := &dpb.FileDescriptorSet{}
268+
if err := proto.Unmarshal(in, fs); err != nil {
269+
return nil, err
270+
}
271+
return fs, nil
272+
}
273+
274+
var outputFormatFuncs = map[string]formatFunc{
275+
"yaml": yaml.Marshal,
276+
"yml": yaml.Marshal,
277+
"json": json.Marshal,
278+
"github": func(i interface{}) ([]byte, error) {
279+
switch v := i.(type) {
280+
case []lint.Response:
281+
return formatGitHubActionOutput(v), nil
282+
default:
283+
return json.Marshal(v)
284+
}
285+
},
286+
}
287+
288+
type formatFunc func(interface{}) ([]byte, error)
289+
290+
func getOutputFormatFunc(formatType string) formatFunc {
291+
if f, found := outputFormatFuncs[strings.ToLower(formatType)]; found {
292+
return f
293+
}
294+
return yaml.Marshal
295+
}

cmd/linter/github_actions.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Copyright 2022 Google LLC
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+
// https://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+
package main
16+
17+
import (
18+
"bytes"
19+
"fmt"
20+
"strings"
21+
22+
"github.com/googleapis/api-linter/lint"
23+
)
24+
25+
// formatGitHubActionOutput returns lint errors in GitHub actions format.
26+
func formatGitHubActionOutput(responses []lint.Response) []byte {
27+
var buf bytes.Buffer
28+
for _, response := range responses {
29+
for _, problem := range response.Problems {
30+
// lint example:
31+
// ::error file={name},line={line},endLine={endLine},title={title}::{message}
32+
// https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-error-message
33+
34+
fmt.Fprintf(&buf, "::error file=%s", response.FilePath)
35+
if problem.Location != nil {
36+
// Some findings are *line level* and only have start positions but no
37+
// starting column. Construct a switch fallthrough to emit as many of
38+
// the location indicators are included.
39+
switch len(problem.Location.Span) {
40+
case 4:
41+
fmt.Fprintf(&buf, ",endColumn=%d", problem.Location.Span[3])
42+
fallthrough
43+
case 3:
44+
fmt.Fprintf(&buf, ",endLine=%d", problem.Location.Span[2])
45+
fallthrough
46+
case 2:
47+
fmt.Fprintf(&buf, ",col=%d", problem.Location.Span[1])
48+
fallthrough
49+
case 1:
50+
fmt.Fprintf(&buf, ",line=%d", problem.Location.Span[0])
51+
}
52+
}
53+
54+
// GitHub uses :: as control characters (which are also used to delimit
55+
// linter rules. In order to prevent confusion, replace the double colon
56+
// with two Armenian full stops which are indistinguishable to my eye.
57+
runeThatLooksLikeTwoColonsButIsActuallyTwoArmenianFullStops := "։։"
58+
title := strings.ReplaceAll(string(problem.RuleID), "::", runeThatLooksLikeTwoColonsButIsActuallyTwoArmenianFullStops)
59+
message := strings.ReplaceAll(problem.Message, "\n", "\\n")
60+
uri := problem.GetRuleURI()
61+
if uri != "" {
62+
message += "\\n\\n" + uri
63+
}
64+
fmt.Fprintf(&buf, ",title=%s::%s\n", title, message)
65+
}
66+
}
67+
68+
return buf.Bytes()
69+
}

0 commit comments

Comments
 (0)