Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v4
with:
go-version: 1.22
go-version-file: 'go.mod'
# - name: golangci-lint
# uses: golangci/golangci-lint-action@v2
# with:
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v5
with:
go-version: 1.22
go-version-file: 'go.mod'
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
uses: goreleaser/goreleaser-action@v6
with:
version: latest
args: release --clean
Expand Down
2 changes: 2 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ builds:
- windows
goarch:
- amd64
- arm64
ldflags:
- -X 'github.com/pubgo/protobuild/version.Version={{ .Version }}'
- main: ./cmd/protoc-gen-retag/main.go
Expand All @@ -21,6 +22,7 @@ builds:
- windows
goarch:
- amd64
- arm64
archives:
- name_template: "{{ .Binary }}-{{ .Tag }}-{{ .Os }}-{{ .Arch }}"
format: binary
Expand Down
58 changes: 58 additions & 0 deletions cmd/linters/github_actions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package linters

import (
"bytes"
"fmt"
"github.com/samber/lo"
"path/filepath"
"strings"

"github.com/googleapis/api-linter/lint"
)

// formatGitHubActionOutput returns lint errors in GitHub actions format.
func formatGitHubActionOutput(responses []lint.Response) []byte {
var buf bytes.Buffer
for _, response := range responses {
for _, problem := range response.Problems {
// lint example:
// ::error file={name},line={line},endLine={endLine},title={title}::{message}
// https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-error-message

fmt.Println(lo.Must(filepath.Abs(response.FilePath)))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Consider using slog for logging instead of fmt.Println. It provides structured logging and better control over log levels.

fmt.Fprintf(&buf, "::error file=%s", response.FilePath)
if problem.Location != nil {
// Some findings are *line level* and only have start positions but no
// starting column. Construct a switch fallthrough to emit as many of
// the location indicators are included.
switch len(problem.Location.Span) {
case 4:
fmt.Fprintf(&buf, ",endColumn=%d", problem.Location.Span[3])
fallthrough
case 3:
fmt.Fprintf(&buf, ",endLine=%d", problem.Location.Span[2])
fallthrough
case 2:
fmt.Fprintf(&buf, ",col=%d", problem.Location.Span[1])
fallthrough
case 1:
fmt.Fprintf(&buf, ",line=%d", problem.Location.Span[0])
}
}

// GitHub uses :: as control characters (which are also used to delimit
// Linter rules. In order to prevent confusion, replace the double colon
// with two Armenian full stops which are indistinguishable to my eye.
runeThatLooksLikeTwoColonsButIsActuallyTwoArmenianFullStops := "։։"
title := strings.ReplaceAll(string(problem.RuleID), "::", runeThatLooksLikeTwoColonsButIsActuallyTwoArmenianFullStops)
Comment on lines +46 to +47
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This replacement of :: with Armenian full stops is clever to avoid conflicts with GitHub Actions' control characters. However, ensure this substitution is well-documented and easily discoverable for anyone debugging linting outputs.

message := strings.ReplaceAll(problem.Message, "\n", "\\n")
uri := problem.GetRuleURI()
if uri != "" {
message += "\\n\\n" + uri
}
fmt.Fprintf(&buf, ",title=%s::%s\n", title, message)
}
}

return buf.Bytes()
}
209 changes: 209 additions & 0 deletions cmd/linters/lint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package linters

import (
"encoding/json"
"errors"
"fmt"
"github.com/samber/lo"
"os"
"strings"
"sync"

"github.com/googleapis/api-linter/lint"
"github.com/jhump/protoreflect/desc/protoparse"
"github.com/pubgo/protobuild/internal/typex"
"github.com/urfave/cli/v3"
"gopkg.in/yaml.v3"
)

type CliArgs struct {
//FormatType string
//ProtoImportPaths []string
EnabledRules []string
DisabledRules []string
ListRulesFlag bool
DebugFlag bool
//IgnoreCommentDisablesFlag bool
}

func NewCli() (*CliArgs, typex.Flags) {
var cliArgs CliArgs

return &cliArgs, typex.Flags{
//&cli.BoolFlag{
// Name: "ignore-comment-disables",
// 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.",
// Value: false,
// Destination: &cliArgs.IgnoreCommentDisablesFlag,
//},

&cli.BoolFlag{
Name: "debug",
Usage: "Run in debug mode. Panics will print stack.",
Value: false,
Destination: &cliArgs.DebugFlag,
},

&cli.BoolFlag{
Name: "list-rules",
Usage: "Print the rules and exit. Honors the output-format flag.",
Value: false,
Destination: &cliArgs.ListRulesFlag,
},

//&cli.StringFlag{
// Name: "output-format",
// Usage: "The format of the linting results.\nSupported formats include \"yaml\", \"json\",\"github\" and \"summary\" table.\nYAML is the default.",
// Aliases: []string{"f"},
// Value: "",
// Destination: &cliArgs.FormatType,
//},

//&cli.StringSliceFlag{
// Name: "proto-path",
// 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.",
// Aliases: []string{"I"},
// Value: nil,
// Destination: &cliArgs.ProtoImportPaths,
//},

//&cli.StringSliceFlag{
// Name: "enable-rule",
// Usage: "Enable a rule with the given name.\nMay be specified multiple times.",
// Value: nil,
// Destination: &cliArgs.EnabledRules,
//},
//
//&cli.StringSliceFlag{
// Name: "disable-rule",
// Usage: "Disable a rule with the given name.\nMay be specified multiple times.",
// Value: nil,
// Destination: &cliArgs.DisabledRules,
//},
}

}

type LinterConfig struct {
Rules lint.Config `yaml:"rules,omitempty" hash:"-"`
FormatType string `yaml:"format_type"`
IgnoreCommentDisablesFlag bool `yaml:"ignore_comment_disables_flag"`
}

func Linter(c *CliArgs, config LinterConfig, protoImportPaths []string, protoFiles []string) error {
if c.ListRulesFlag {
return outputRules(config.FormatType)
}

// Pre-check if there are files to lint.
if len(protoFiles) == 0 {
return fmt.Errorf("no file to lint")
}

rules := lint.Configs{config.Rules}

// Add configs for the enabled rules.
rules = append(rules, lint.Config{EnabledRules: c.EnabledRules})
rules = append(rules, lint.Config{DisabledRules: c.DisabledRules})

var errorsWithPos []protoparse.ErrorWithPos
var lock sync.Mutex
// Parse proto files into `protoreflect` file descriptors.
p := protoparse.Parser{
ImportPaths: append(protoImportPaths, "."),
IncludeSourceCodeInfo: true,
ErrorReporter: func(errorWithPos protoparse.ErrorWithPos) error {
// Protoparse isn't concurrent right now but just to be safe for the future.
lock.Lock()
errorsWithPos = append(errorsWithPos, errorWithPos)
lock.Unlock()
// Continue parsing. The error returned will be protoparse.ErrInvalidSource.
return nil
},
}

var err error
// Resolve file absolute paths to relative ones.
// Using supplied import paths first.
if len(protoImportPaths) > 0 {
protoFiles, err = protoparse.ResolveFilenames(protoImportPaths, protoFiles...)
if err != nil {
return err
}
}
// Then resolve again against ".", the local directory.
// This is necessary because ResolveFilenames won't resolve a path if it
// relative to *at least one* of the given import paths, which can result
// in duplicate file parsing and compilation errors, as seen in #1465 and
// #1471. So we resolve against local (default) and flag specified import
// paths separately.
protoFiles, err = protoparse.ResolveFilenames([]string{"."}, protoFiles...)
if err != nil {
return err
}

fd, err := p.ParseFiles(protoFiles...)
if err != nil {
if err == protoparse.ErrInvalidSource {
if len(errorsWithPos) == 0 {
return errors.New("got protoparse.ErrInvalidSource but no ErrorWithPos errors")
}
// TODO: There's multiple ways to deal with this but this prints all the errors at least
errStrings := make([]string, len(errorsWithPos))
for i, errorWithPos := range errorsWithPos {
errStrings[i] = errorWithPos.Error()
}
return errors.New(strings.Join(errStrings, "\n"))
}
return err
}

// Create a Linter to lint the file descriptors.
l := lint.New(globalRules, rules, lint.Debug(c.DebugFlag), lint.IgnoreCommentDisables(config.IgnoreCommentDisablesFlag))
results, err := l.LintProtos(fd...)
if err != nil {
return err
}

// Determine the format for printing the results.
// YAML format is the default.
marshal := getOutputFormatFunc(config.FormatType)

// Print the results.
b, err := marshal(results)
if err != nil {
return err
}

fmt.Println(string(b))

filterResults := lo.Filter(results, func(item lint.Response, index int) bool { return len(item.Problems) > 0 })
if len(filterResults) > 0 {
os.Exit(1)
}

return nil
}

var outputFormatFuncs = map[string]formatFunc{
"yaml": yaml.Marshal,
"yml": yaml.Marshal,
"json": json.Marshal,
"github": func(i interface{}) ([]byte, error) {
switch v := i.(type) {
case []lint.Response:
return formatGitHubActionOutput(v), nil
default:
return json.Marshal(v)
}
},
}

type formatFunc func(interface{}) ([]byte, error)

func getOutputFormatFunc(formatType string) formatFunc {
if f, found := outputFormatFuncs[strings.ToLower(formatType)]; found {
return f
}
return yaml.Marshal
}
59 changes: 59 additions & 0 deletions cmd/linters/rules.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package linters

import (
"github.com/googleapis/api-linter/rules"
"log"
"os"
"sort"

"github.com/googleapis/api-linter/lint"
)

var (
globalRules = lint.NewRuleRegistry()
)

func init() {
if err := rules.Add(globalRules); err != nil {
log.Fatalf("error when registering rules: %v", err)
}
}

type (
listedRule struct {
Name lint.RuleName
}
listedRules []listedRule
listedRulesByName []listedRule
)

func (a listedRulesByName) Len() int { return len(a) }
func (a listedRulesByName) Less(i, j int) bool { return a[i].Name < a[j].Name }
func (a listedRulesByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

func outputRules(formatType string) error {
rules := listedRules{}
for id := range globalRules {
rules = append(rules, listedRule{
Name: id,
})
}

sort.Sort(listedRulesByName(rules))

// Determine the format for printing the results.
// YAML format is the default.
marshal := getOutputFormatFunc(formatType)

// Print the results.
b, err := marshal(rules)
if err != nil {
return err
}
w := os.Stdout
if _, err = w.Write(b); err != nil {
return err
}

return nil
}
Loading
Loading