sourcegraph/dev/sg/linters/linters.go
Noah S-C 19d9cfc73b
bazel: native go-mockgen in Bazel (#60386)
Adds a new:
- gazelle generator
- rule + rule targets + catchall target
for generating go-mockgen mocks & testing for their being up-to-date.

Each go_mockgen macro invocation adds targets for generating mocks, copying to the source tree, as well as testing whether the current source tree mocks are up-to-date.

How to use this: `bazel run //dev:go_mockgen` for the catch-all, or `bazel run //some/target:generate_mocks` for an individual package, and `bazel test //some/target:generate_mocks_tests` to test for up-to-date-ness. There is no catch-all for testing

This currently uses a fork of go-mockgen, with an open PR for upstream here: https://github.com/derision-test/go-mockgen/pull/50.

Closes https://github.com/sourcegraph/sourcegraph/issues/60099

## Test plan

Extensive testing during development, including the following cases:
- Deleting a generated file and its entry in a go_library/go_test `srcs` attribute list and then re-running `sg bazel configure`
- Adding a non-existent output directory to mockgen.test.yaml and running the bash one-liner emitted to prepare the workspace for rerunning `sg bazel configure`

The existing config tests a lot of existing paths anyway (creating mocks for a 3rd party library's interface, entries for a given output file in >1 config file etc)
2024-02-16 13:26:48 +00:00

209 lines
6.2 KiB
Go

// Package linters defines all available linters.
package linters
import (
"bytes"
"context"
"os"
"strings"
"sync"
"time"
"github.com/sourcegraph/run"
"go.bobheadxi.dev/streamline/pipeline"
"github.com/sourcegraph/sourcegraph/dev/sg/internal/check"
"github.com/sourcegraph/sourcegraph/dev/sg/internal/repo"
"github.com/sourcegraph/sourcegraph/dev/sg/internal/std"
"github.com/sourcegraph/sourcegraph/dev/sg/root"
"github.com/sourcegraph/sourcegraph/internal/honey"
"github.com/sourcegraph/sourcegraph/lib/errors"
)
type Target = check.Category[*repo.State]
type linter = check.Check[*repo.State]
// Targets lists all available linter targets. Each target consists of multiple linters.
//
// These should align with the names in 'dev/ci/internal/ci/changed'
var Targets = []Target{
{
Name: "urls",
Description: "Check for broken urls in the codebase",
Checks: []*linter{
runScript("Broken urls", "dev/check/broken-urls.bash"),
},
},
{
Name: "go",
Description: "Check go code for linting errors, forbidden imports, generated files, etc",
Checks: []*linter{
onlyLocal(goGenerateLinter),
onlyLocal(goDBConnImport),
onlyLocal(noLocalHost),
timeCheck(lintGoDirectives()),
timeCheck(lintLoggingLibraries()),
onlyLocal(lintTracingLibraries()),
timeCheck(goModGuards()),
onlyLocal(lintSGExit()),
},
},
{
Name: "graphql",
Description: "Checks the graphql code for linting errors [bazel]",
Checks: []*linter{
onlyLocal(bazelExec("graphql schema lint (bazel)", "test //cmd/frontend/graphqlbackend:graphql_schema_lint_test")),
},
},
{
Name: "docs",
Description: "Documentation checks",
Checks: []*linter{
onlyLocal(bazelExec("Docsite lint (bazel)", "test //doc:test")),
timeCheck(docChangesLint()),
},
},
{
Name: "dockerfiles",
Description: "Check Dockerfiles for Sourcegraph best practices",
Checks: []*linter{
// TODO move to pre-commit
timeCheck(hadolint()),
},
},
{
Name: "client",
Description: "Check client code for linting errors, forbidden imports, etc",
Checks: []*linter{
timeCheck(inlineTemplates),
// we only run this linter locally, since on CI it has it's own job
onlyLocal(runScriptSerialized("pnpm lint:js:web", "dev/ci/pnpm-run.sh lint:js:web")),
timeCheck(checkUnversionedDocsLinks()),
},
},
{
Name: "pnpm",
Description: "Check pnpm lockfiles for optimality",
Checks: []*linter{
timeCheck(runScriptSerialized("pnpm dedupe", "dev/check/pnpm-deduplicate.sh")),
},
},
{
Name: "shell",
Description: "Check shell code for linting errors, formatting, etc",
Checks: []*linter{
timeCheck(shFmt),
timeCheck(shellCheck),
timeCheck(bashSyntax),
},
},
{
Name: "protobuf",
Description: "Check protobuf code for linting errors, formatting, etc",
Checks: []*linter{
timeCheck(bufFormat),
timeCheck(bufGenerate),
timeCheck(bufLint),
},
},
{
Name: "bazel generated",
Description: "Ensures documentation and source generated by Bazel is up to date",
Checks: []*linter{
onlyLocal(bazelExec("bazel generate files", "run //:write_all_generated")),
},
},
Formatting,
}
var Formatting = Target{
Name: "format",
Description: "Check client code and docs for formatting errors",
Checks: []*linter{
timeCheck(prettier),
},
}
func onlyLocal(l *linter) *linter {
if os.Getenv("CI") == "true" {
l.Enabled = func(ctx context.Context, args *repo.State) error {
return errors.New("check is disabled in CI")
}
}
return l
}
// runScript creates check that runs the given script from the root of sourcegraph/sourcegraph.
func runScript(name string, script string) *linter {
return &linter{
Name: name,
Check: func(ctx context.Context, out *std.Output, args *repo.State) error {
return root.Run(run.Bash(ctx, script)).StreamLines(out.Write)
},
}
}
var runScriptSerializedMu sync.Mutex
// runScriptSerialized is exactly like runScript, but ensure that all the check functions
// are run serially by acquiring a lock.
//
// This is useful for pnpm for examples, as some tasks might end up writing to the same files
// concurrently, leading to race conditions and thus CI failures.
func runScriptSerialized(name string, script string) *linter {
return &linter{
Name: name,
Check: func(ctx context.Context, out *std.Output, args *repo.State) error {
event := honey.FromContext(ctx)
t1 := time.Now()
runScriptSerializedMu.Lock()
t2 := time.Since(t1)
event.AddField("pnpm_lock_duration", t2.Seconds())
event.AddField("pnpm_lock_duration_ms", t2.Milliseconds())
defer runScriptSerializedMu.Unlock()
return root.Run(run.Bash(ctx, script)).StreamLines(out.Write)
},
}
}
// runCheck creates a check that runs the given check func.
func runCheck(name string, check check.CheckAction[*repo.State]) *linter {
return &linter{
Name: name,
Check: check,
}
}
func bazelExec(name, args string) *linter {
cmd := []string{"bazel"}
cmd = append(cmd, strings.Split(args, " ")...)
return &linter{
Name: name,
Check: func(ctx context.Context, out *std.Output, args *repo.State) error {
return root.Run(run.Cmd(ctx, cmd...)).StreamLines(out.Write)
},
}
}
// pnpmInstallFilter is a pipeline that filters out all the warning junk that pnpm install
// emits that seem inconsequential, for example:
//
// warning "@storybook/addon-foo > react-test-renderer@16.14.0" has incorrect peer dependency "react@^16.14.0".
// warning " > @storybook/react@6.5.9" has unmet peer dependency "require-from-string@^2.0.2".
// warning "@storybook/react > react-element-to-jsx-string@14.3.4" has incorrect peer dependency "react@^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1".
// warning " > @testing-library/react-hooks@8.0.0" has incorrect peer dependency "react@^16.9.0 || ^17.0.0".
// warning Workspaces can only be enabled in private projects.
// warning Workspaces can only be enabled in private projects.
func pnpmInstallFilter() pipeline.Pipeline {
return pipeline.Filter(func(line []byte) bool { return !bytes.Contains(line, []byte("warning")) })
}
// disabled can be used to mark a category or check as disabled.
func disabled(reason string) check.EnableFunc[*repo.State] {
return func(context.Context, *repo.State) error {
return errors.Newf("disabled: %s", reason)
}
}