This is an archive of the discontinued LLVM Phabricator instance.

[clangd] Revamp handling of diagnostics.
ClosedPublic

Authored by ilya-biryukov on Mar 6 2018, 3:49 AM.

Details

Summary

The new implementation attaches notes to diagnostic message and shows
the original diagnostics in the message of the note.

Diff Detail

Event Timeline

ilya-biryukov created this revision.Mar 6 2018, 3:49 AM

This revision is still missing tests, but I wanted to get feedback for the overall design and the new diagnostic messages. So sending out for review now.

ilya-biryukov added inline comments.Mar 6 2018, 6:38 AM
clangd/Diagnostics.h
29

This could probably use a better name

Behavior looks good. I think we can do a bit better with (most) fixits - your call on whether it makes sense to do here.

As discussed i'd simplify the diagnostic containers until we know there's a strong need.

clangd/ClangdLSPServer.cpp
323

nit: while here, can we add a colon after FixIt (or if appropriate in practice, just drop the prefix altogether?). My recollection is this is a bit hard to parse.

clangd/Diagnostics.cpp
86

inside

126

mind pasting a small example as a function comment?
Logic looks good, but I had to reconstruct the message in my head.

148

or just mainMessage (this is another verbs-for-pure-functions case where the style recommendation seems to hurt readability to me, but up to you)

193

PreBuild could be FillBasicFields or something? found this name confusing.

201

(nit: if the goal with the callback function is to avoid allocations, shouldn't we reuse the DiagWithFixIts?)

323

do you need else log here? (and just log the main diag)

clangd/Diagnostics.h
29

I found it a bit confusing that this represents both "top-level" diagnostics and notes. It obscures the nature of the hierarchy a bit: there *are* a fixed number of levels with different "kinds", but the kinds are similar enough to share a type.

I'd actually consider using inheritance here to model the relationships more clearly. (Yes, gross, I know)
Like:

struct DiagBase { Message, File, Range, InMainFile }
struct Diag : DiagBase { FixIts, Notes, Severity }
struct Note : DiagBase {} // or leave this one out until needed

(I think we came to the conclusion that different severity of notes isn't interesting and potentially just confusing)

36

As discussed offline - I think fixits should probably be a different struct hanging off the main diagnostic, with a name. Following clang's example seems... less than clear here.

They could also be modeled as notes with an optional TextEdit - this seems sligthly less clear but more faithful to clang internals*.
Either way, it should be clear that they're only allowed in *one* of these locations - having notes and main diags be distinct types would help.

I think this probably affects how we should expose them through LSP - they should be named code actions attached to the original diagnostic.
Maybe they should also be separate diagnostics, but only if they contribute a unique opportunity to the user e.g. they have a non-empty range that isn't contained within the diagnostic's range.
This patch seems like a reasonable place to do that, but also OK if you want to defer.

36

Nit: can we call these "Fix"es, which is a noun (at least in common usage?)

Clang calls them FixItHint, but I think Fix is no less clear (and shorter)

59

This is cool :) but really looks like premature optimization.
Particularly, if the diagnostics were a self-contained value type, this could just be vector<diag> I think. And if we care about copying strings (I'm not sure we do), being lifetime-scoped to the ASTcontext is probably OK.

(rawDiags() has potential uses but having callers explicitly traverse is going to be clearer as your comment suggests)

114

Can we try removing this struct in favor of two callback params? It seems like a small source of confusion, considering how important the type used to be.

124

I don't think this overload pays for itself - making callers write the outer loop would be a little clearer, I think.

(I'd consider having that fn return a smallvector instead of taking a callback, but don't feel strongly)

129

brief doc.
In particular: what filtering is applied

129

Maybe just StoreDiags?

131

If there's no strong reason otherwise, DiagList take() is slightly easier/clearer for tests. compare

DiagList Diags;
StoreDiagsConsumer Cons(Diags);
...
EXPECT_THAT(Diags, ...)

vs

StoreDiagsConsumer Diags;
...
EXPECT_THAT(Diags.take(), ...)
clangd/SourceCode.cpp
53

isn't this sourceLocToPosition?

test/clangd/diagnostics.test
20–21

I think ideally this wouldn't show up at all, "change return type to int" would be the name of the code action returned for the first diag. Note the range is the same, so we're not giving the user an extra place to apply the fix.

ilya-biryukov marked 9 inline comments as done.

This is not final, there are still unadressed comments.

  • Significantly simplified the implementation by removing the DiagList, etc.
  • Addressed some of the rest of the comments.
clangd/ClangdLSPServer.cpp
323

I added a colon for now, but happy to look into removing the prefix. The use-case that's slightly confusing is:

use of undeclared identifier 'foo'. did you mean 'bar'?

Since the actual fix-it is at the end, it's might be a bit hard to understand.

clangd/Diagnostics.cpp
201

Can we actually avoid allocating some of the strings there?
We're std::moveing the result into the callback, so we don't seem to waste memory too.

clangd/Diagnostics.h
59

Using a straight-forward representation for now.

124

Still using a callback, but happy to return a SmallVector instead. It seems the pattern for using SmallVector in llvm is passing it in as an output parameter (so that the users might decide themselves what's the count of items on the stack), which is slightly ugly.
It might be fine to return SmallVector here though, this function can probably make assumptions about the number of notes in a diagnostic.

ilya-biryukov marked 8 inline comments as done.

Addressed review comments.

The only thing that's left on my todo-list is a test for toLSPDiags().
Everything else should be ready.

Not sure if you're waiting on comments from me: the changes look good.
As discussed I still think Fix should be a separate thing with a name because that's how editors treat it, and this is the right layer to decide how to do that mapping.

clangd/ClangdLSPServer.cpp
323

Yeah. I'd still be inclined to drop the prefix:

  • this case isn't ideal, but is also not terrible
  • this is a "no separate note for fixit" case, where we can consider synthesizing the message from the text edit rather than reusing the diagnostic. That would work well at least in this case.
323

nit: fixit --> fix in prefix and in lambda
(And drop the caps in the prefix I think, as this is now a regular english noun rather than a pseudo-product-name)

clangd/Diagnostics.h
41

I don't understand what "persistent" means in this context.
This does seem to be an is-a relationship rather than has-a, so I'd find DiagBase/Diag + inheritance clearer.

But with composition, this one seems rike it should be "Diag" and the type of Main could be DiagDetails or so?

Sorry, last comments were concurrent with the latest snapshot.

Except where noted, the old comments still apply I think.

clangd/ClangdLSPServer.cpp
317

inline this into the for loop?

clangd/ClangdLSPServer.h
76

getFixes?

clangd/Diagnostics.h
36

This is done, and looks good to me.

41

(this happened already, yay!)

48

nit: severity of notes can...

52

Seems a bit confusing to use "diagnostic" to mean something importantly different than "diag", and to define top-level diagnostic in terms of notes.

Maybe just "A top-level diagnostic that may have Notes and Fixes"?

55

I think these deserve some comments:

  • fixes are *alternative* fixes for this diagnostic, one should be chosen
  • notes elaborate on the problem, usually pointing to a related piece of code
58

I'm not sure what "conventional" means here?

unittests/clangd/ClangdUnitTests.cpp
23

can you make these operator<< and move to the main code? This seems a fine general-purpose debug representation (could be tweaked, but also later).

That avoids the ODR issue, and makes casual logging easier.

38

Ah, and now that you've written this... ;-)

For the specific case of "field X matches matcher M" you can use ::testing::Field (you'd keep WithNote, but make it call Field)

Messages are good as long as you pass the optional field_name param.

69

this "same range and message as the diagnostic itself" is an important property of the code being tested, and it's hidden here in a comment of a helper function.
I'd probably prefer avoiding this helper altogether (spelling the string twice in the test) so the test behavior/change is obvious when changing the code.
Failing that, DiagWithEqualFix or something?

  • Added unit test for toLSPDiags
ilya-biryukov marked 14 inline comments as done.
  • Addressed review comments
clangd/Diagnostics.h
36

We now provide a separate Fix class, but we still don't return it to LSP, keeping the behaviour as is (i.e. mapping LSP ranges to code actions on the server side).

The fixes are not reported as separate diagnostics, they are only shown as code actions when users hovers over the diagnostic.

58

Removed conventional from the comment. Other parts should make sense.

unittests/clangd/ClangdUnitTests.cpp
38

Was a useful exercise anyway.

Couldn't find the optional 'field_name' param, though, am I missing something?

69

I agree that, but spelling this in the code is so much noise. Renaming to DiagWithEqualFix makes total sense, did that.

ilya-biryukov marked 2 inline comments as done.Mar 12 2018, 4:51 AM
ilya-biryukov added inline comments.
clangd/ClangdLSPServer.cpp
323

I still find a prefix useful in some cases, e.g. the unresolved identifier foo. did you mean bar?.
Mostly for cases where a fix note uses the error message of the main diagnostic or when it's badly phrased (haven't really seen those, but I would be surprised if there are none).

sammccall accepted this revision.Mar 12 2018, 6:02 AM

Ship it! (but let's drop the extra operator== if possible?)

clangd/ClangdLSPServer.cpp
323

that's... exactly the case I was replying to :)
I don't think it's common or confusing enough, and there are other fixes when using the main diagnostic. But this isn't a big deal; let's leave it for now.

clangd/Protocol.h
169

The TextEdit/Diagnostic == operators seem odd - what do we need these for?

unittests/clangd/ClangdUnitTests.cpp
38

You're not, sorry - it was added recently to google's copy and hasn't been pushed to upstream gmock yet. So for now we have slightly bad error messages (or have the full MatcherInterface implementation)

This revision is now accepted and ready to land.Mar 12 2018, 6:02 AM
  • Replace equality comparison ops with matchers, they were only used in tests.
This revision was automatically updated to reflect the committed changes.