Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement breaking lints in the LSP #3404

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open

Implement breaking lints in the LSP #3404

wants to merge 4 commits into from

Conversation

mcy
Copy link
Member

@mcy mcy commented Oct 16, 2024

This change adds breaking lints to the LSP as part of the general lint pass.

Breaking lints are interesting because they need an "against". I've chosen to expose two configuration knobs (which are set in the settings for an LSP in the standard way):

  1. buf.against sets the against strategy. For the LSP, there are three that reasonably make sense: against some remote's HEAD, against your local HEAD, and against whatever is on disk. This means that lints go away when pushing to main, committing working changes, or saving in the editor, respectively.
  2. buf.gitRemote, the remote to use for the above. "origin" is the default for almost everyone, but it need not be that, so it's a config knob.

Also, in the process of standing up loading config settings from the client, I found a deadlock in the jsonrpc2 library. To work around it, every request runs on its own goroutine (although they are still serialized wrt to the big LSP lock). This ensures that the single goroutine jsonrpc2 spawns to actually poll the IPC socket doesn't deadlock. Why do they only spawn one goroutine? Presumably because they expect users to spawn a goroutine to handle each request, but this does not appear to be visibly documented. (Much debugging of deadlocked goroutines happened today.)

@mcy mcy requested review from bufdev and doriable October 16, 2024 19:52
Copy link
Contributor

github-actions bot commented Oct 16, 2024

The latest Buf updates on your PR. Results from workflow Buf CI / buf (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedOct 24, 2024, 4:29 PM

Comment on lines 46 to 52
const (
againstTrunk againstKind = iota + 1
againstHead
againstSaved
)

const descriptorPath = "google/protobuf/descriptor.proto"
Copy link
Member

Choose a reason for hiding this comment

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

Style nit: I would group the enum value declarations alongside the type, something like:

const descriptorPath = "google/protobuf/descriptor.proto"

const (
	againstTrunk againstKind = iota + 1
	againstHead
	againstSaved
)

type againstKind int

According to our style guide, we should group related declarations, so swapping the order of the two const blocks makes sense to me.
Also we should have some docs around what each one is.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

l.lock.Lock()
defer l.lock.Unlock()
return actual(ctx, replier, req)
return func(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) error {
Copy link
Member

Choose a reason for hiding this comment

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

Discussed in DMs, but we may be able to use jsonrpc2.AsyncHandler for this https://pkg.go.dev/go.lsp.dev/jsonrpc2#AsyncHandler

Copy link
Member Author

Choose a reason for hiding this comment

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

The semantics are not precisely the same, but it's close enough!

Comment on lines 229 to 239
if errors.Is(err, git.ErrRemoteNotFound) {
// This is an invalid remote, so it means gitRemote should be
// interpreted as a local branch name instead.
f.gitRemoteBranch = f.gitRemote
f.gitRemote = ""
f.lsp.logger.Debug(
"found local branch",
slog.String("uri", string(f.uri)),
slog.String("ref", f.gitRemoteBranch),
)
} else if err != nil {
Copy link
Member

Choose a reason for hiding this comment

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

This behaviour is somewhat reasonable, but I wonder if branch + remote should just be different config fields with sane defaults for clarity. Even if this is documented behaviour, the config field being named gitRemote but pointing to a local branch might cause some confusion. This would have cascading changes through the fileOpener, etc.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think I'm going to change the semantics as follows:

  1. againstKind will be againstGit or againstSaved.
  2. There will be a single config knob for the ref to use for againstGit, with a default of origin/HEAD.

Comment on lines 616 to 617
if data == nil || errors.Is(err, git.ErrNotARepo) {
return os.Open(fileInfo.LocalPath())
Copy link
Member

Choose a reason for hiding this comment

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

For this case, should we check f.IsLocal()?

Copy link
Member Author

Choose a reason for hiding this comment

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

No: the local path is the path on disk, which is the file we need to open for e.g. things that are in cache.

Comment on lines 624 to 627
// BuildImage build Buf Images for this file, to be used with linting
// routines.
//
// This operation requires IndexImports().
Copy link
Member

Choose a reason for hiding this comment

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

Nit: %s/BuildImage/BuildImages

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

Comment on lines 383 to 396
for {
_, err := os.Stat(filepath.Join(dir, ".git"))
if err == nil {
break
} else if errors.Is(err, os.ErrNotExist) {
parent := filepath.Dir(dir)
if parent == dir {
return nil, fmt.Errorf("could not find .git directory for %s: %w", orig, ErrNotARepo)
}
dir = parent
} else {
return nil, err
}
}
Copy link
Member

Choose a reason for hiding this comment

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

We could use CheckDirectoryIsValidGitCheckout to check if this is a git repository:

func CheckDirectoryIsValidGitCheckout(

The call-site will need to handle ErrInvalidGitCheckout instead.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's not quite what we want. We want to get the repository root, which requires traversing upwards (vs IsValidCheckout, which just kicks rev-parse).

@mcy mcy requested a review from doriable October 24, 2024 15:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants