Skip to content

Commit 8aed3ac

Browse files
authored
Feat: add protobuf lint (#15)
1 parent 2f29a1d commit 8aed3ac

File tree

10 files changed

+504
-62
lines changed

10 files changed

+504
-62
lines changed

.github/workflows/lint.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ jobs:
77
runs-on: ubuntu-latest
88

99
steps:
10-
- uses: actions/checkout@v2
10+
- uses: actions/checkout@v4
1111

1212
- name: Set up Go
13-
uses: actions/setup-go@v2
13+
uses: actions/setup-go@v4
1414
with:
15-
go-version: 1.22
15+
go-version-file: 'go.mod'
1616
# - name: golangci-lint
1717
# uses: golangci/golangci-lint-action@v2
1818
# with:

.github/workflows/release.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ jobs:
99
runs-on: ubuntu-latest
1010
steps:
1111
- name: Checkout
12-
uses: actions/checkout@v2
12+
uses: actions/checkout@v4
1313
with:
1414
fetch-depth: 0
1515
- name: Set up Go
16-
uses: actions/setup-go@v2
16+
uses: actions/setup-go@v5
1717
with:
18-
go-version: 1.22
18+
go-version-file: 'go.mod'
1919
- name: Run GoReleaser
20-
uses: goreleaser/goreleaser-action@v2
20+
uses: goreleaser/goreleaser-action@v6
2121
with:
2222
version: latest
2323
args: release --clean

.goreleaser.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ builds:
99
- windows
1010
goarch:
1111
- amd64
12+
- arm64
1213
ldflags:
1314
- -X 'github.com/pubgo/protobuild/version.Version={{ .Version }}'
1415
- main: ./cmd/protoc-gen-retag/main.go
@@ -21,6 +22,7 @@ builds:
2122
- windows
2223
goarch:
2324
- amd64
25+
- arm64
2426
archives:
2527
- name_template: "{{ .Binary }}-{{ .Tag }}-{{ .Os }}-{{ .Arch }}"
2628
format: binary

cmd/linters/github_actions.go

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

cmd/linters/lint.go

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
package linters
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"github.com/samber/lo"
8+
"os"
9+
"strings"
10+
"sync"
11+
12+
"github.com/googleapis/api-linter/lint"
13+
"github.com/jhump/protoreflect/desc/protoparse"
14+
"github.com/pubgo/protobuild/internal/typex"
15+
"github.com/urfave/cli/v3"
16+
"gopkg.in/yaml.v3"
17+
)
18+
19+
type CliArgs struct {
20+
//FormatType string
21+
//ProtoImportPaths []string
22+
EnabledRules []string
23+
DisabledRules []string
24+
ListRulesFlag bool
25+
DebugFlag bool
26+
//IgnoreCommentDisablesFlag bool
27+
}
28+
29+
func NewCli() (*CliArgs, typex.Flags) {
30+
var cliArgs CliArgs
31+
32+
return &cliArgs, typex.Flags{
33+
//&cli.BoolFlag{
34+
// Name: "ignore-comment-disables",
35+
// Usage: "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.",
36+
// Value: false,
37+
// Destination: &cliArgs.IgnoreCommentDisablesFlag,
38+
//},
39+
40+
&cli.BoolFlag{
41+
Name: "debug",
42+
Usage: "Run in debug mode. Panics will print stack.",
43+
Value: false,
44+
Destination: &cliArgs.DebugFlag,
45+
},
46+
47+
&cli.BoolFlag{
48+
Name: "list-rules",
49+
Usage: "Print the rules and exit. Honors the output-format flag.",
50+
Value: false,
51+
Destination: &cliArgs.ListRulesFlag,
52+
},
53+
54+
//&cli.StringFlag{
55+
// Name: "output-format",
56+
// Usage: "The format of the linting results.\nSupported formats include \"yaml\", \"json\",\"github\" and \"summary\" table.\nYAML is the default.",
57+
// Aliases: []string{"f"},
58+
// Value: "",
59+
// Destination: &cliArgs.FormatType,
60+
//},
61+
62+
//&cli.StringSliceFlag{
63+
// Name: "proto-path",
64+
// Usage: "The folder for searching proto imports.\\nMay be specified multiple times; directories will be searched in order.\\nThe current working directory is always used.",
65+
// Aliases: []string{"I"},
66+
// Value: nil,
67+
// Destination: &cliArgs.ProtoImportPaths,
68+
//},
69+
70+
//&cli.StringSliceFlag{
71+
// Name: "enable-rule",
72+
// Usage: "Enable a rule with the given name.\nMay be specified multiple times.",
73+
// Value: nil,
74+
// Destination: &cliArgs.EnabledRules,
75+
//},
76+
//
77+
//&cli.StringSliceFlag{
78+
// Name: "disable-rule",
79+
// Usage: "Disable a rule with the given name.\nMay be specified multiple times.",
80+
// Value: nil,
81+
// Destination: &cliArgs.DisabledRules,
82+
//},
83+
}
84+
85+
}
86+
87+
type LinterConfig struct {
88+
Rules lint.Config `yaml:"rules,omitempty" hash:"-"`
89+
FormatType string `yaml:"format_type"`
90+
IgnoreCommentDisablesFlag bool `yaml:"ignore_comment_disables_flag"`
91+
}
92+
93+
func Linter(c *CliArgs, config LinterConfig, protoImportPaths []string, protoFiles []string) error {
94+
if c.ListRulesFlag {
95+
return outputRules(config.FormatType)
96+
}
97+
98+
// Pre-check if there are files to lint.
99+
if len(protoFiles) == 0 {
100+
return fmt.Errorf("no file to lint")
101+
}
102+
103+
rules := lint.Configs{config.Rules}
104+
105+
// Add configs for the enabled rules.
106+
rules = append(rules, lint.Config{EnabledRules: c.EnabledRules})
107+
rules = append(rules, lint.Config{DisabledRules: c.DisabledRules})
108+
109+
var errorsWithPos []protoparse.ErrorWithPos
110+
var lock sync.Mutex
111+
// Parse proto files into `protoreflect` file descriptors.
112+
p := protoparse.Parser{
113+
ImportPaths: append(protoImportPaths, "."),
114+
IncludeSourceCodeInfo: true,
115+
ErrorReporter: func(errorWithPos protoparse.ErrorWithPos) error {
116+
// Protoparse isn't concurrent right now but just to be safe for the future.
117+
lock.Lock()
118+
errorsWithPos = append(errorsWithPos, errorWithPos)
119+
lock.Unlock()
120+
// Continue parsing. The error returned will be protoparse.ErrInvalidSource.
121+
return nil
122+
},
123+
}
124+
125+
var err error
126+
// Resolve file absolute paths to relative ones.
127+
// Using supplied import paths first.
128+
if len(protoImportPaths) > 0 {
129+
protoFiles, err = protoparse.ResolveFilenames(protoImportPaths, protoFiles...)
130+
if err != nil {
131+
return err
132+
}
133+
}
134+
// Then resolve again against ".", the local directory.
135+
// This is necessary because ResolveFilenames won't resolve a path if it
136+
// relative to *at least one* of the given import paths, which can result
137+
// in duplicate file parsing and compilation errors, as seen in #1465 and
138+
// #1471. So we resolve against local (default) and flag specified import
139+
// paths separately.
140+
protoFiles, err = protoparse.ResolveFilenames([]string{"."}, protoFiles...)
141+
if err != nil {
142+
return err
143+
}
144+
145+
fd, err := p.ParseFiles(protoFiles...)
146+
if err != nil {
147+
if err == protoparse.ErrInvalidSource {
148+
if len(errorsWithPos) == 0 {
149+
return errors.New("got protoparse.ErrInvalidSource but no ErrorWithPos errors")
150+
}
151+
// TODO: There's multiple ways to deal with this but this prints all the errors at least
152+
errStrings := make([]string, len(errorsWithPos))
153+
for i, errorWithPos := range errorsWithPos {
154+
errStrings[i] = errorWithPos.Error()
155+
}
156+
return errors.New(strings.Join(errStrings, "\n"))
157+
}
158+
return err
159+
}
160+
161+
// Create a Linter to lint the file descriptors.
162+
l := lint.New(globalRules, rules, lint.Debug(c.DebugFlag), lint.IgnoreCommentDisables(config.IgnoreCommentDisablesFlag))
163+
results, err := l.LintProtos(fd...)
164+
if err != nil {
165+
return err
166+
}
167+
168+
// Determine the format for printing the results.
169+
// YAML format is the default.
170+
marshal := getOutputFormatFunc(config.FormatType)
171+
172+
// Print the results.
173+
b, err := marshal(results)
174+
if err != nil {
175+
return err
176+
}
177+
178+
fmt.Println(string(b))
179+
180+
filterResults := lo.Filter(results, func(item lint.Response, index int) bool { return len(item.Problems) > 0 })
181+
if len(filterResults) > 0 {
182+
os.Exit(1)
183+
}
184+
185+
return nil
186+
}
187+
188+
var outputFormatFuncs = map[string]formatFunc{
189+
"yaml": yaml.Marshal,
190+
"yml": yaml.Marshal,
191+
"json": json.Marshal,
192+
"github": func(i interface{}) ([]byte, error) {
193+
switch v := i.(type) {
194+
case []lint.Response:
195+
return formatGitHubActionOutput(v), nil
196+
default:
197+
return json.Marshal(v)
198+
}
199+
},
200+
}
201+
202+
type formatFunc func(interface{}) ([]byte, error)
203+
204+
func getOutputFormatFunc(formatType string) formatFunc {
205+
if f, found := outputFormatFuncs[strings.ToLower(formatType)]; found {
206+
return f
207+
}
208+
return yaml.Marshal
209+
}

cmd/linters/rules.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package linters
2+
3+
import (
4+
"github.com/googleapis/api-linter/rules"
5+
"log"
6+
"os"
7+
"sort"
8+
9+
"github.com/googleapis/api-linter/lint"
10+
)
11+
12+
var (
13+
globalRules = lint.NewRuleRegistry()
14+
)
15+
16+
func init() {
17+
if err := rules.Add(globalRules); err != nil {
18+
log.Fatalf("error when registering rules: %v", err)
19+
}
20+
}
21+
22+
type (
23+
listedRule struct {
24+
Name lint.RuleName
25+
}
26+
listedRules []listedRule
27+
listedRulesByName []listedRule
28+
)
29+
30+
func (a listedRulesByName) Len() int { return len(a) }
31+
func (a listedRulesByName) Less(i, j int) bool { return a[i].Name < a[j].Name }
32+
func (a listedRulesByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
33+
34+
func outputRules(formatType string) error {
35+
rules := listedRules{}
36+
for id := range globalRules {
37+
rules = append(rules, listedRule{
38+
Name: id,
39+
})
40+
}
41+
42+
sort.Sort(listedRulesByName(rules))
43+
44+
// Determine the format for printing the results.
45+
// YAML format is the default.
46+
marshal := getOutputFormatFunc(formatType)
47+
48+
// Print the results.
49+
b, err := marshal(rules)
50+
if err != nil {
51+
return err
52+
}
53+
w := os.Stdout
54+
if _, err = w.Write(b); err != nil {
55+
return err
56+
}
57+
58+
return nil
59+
}

0 commit comments

Comments
 (0)