Previously clangd would jump to forward declarations for protocols
and classes instead of their definition/implementation.
Details
Diff Detail
- Repository
- rG LLVM Github Monorepo
Event Timeline
clang-tools-extra/clangd/XRefs.cpp | ||
---|---|---|
317 | Think it would make sense to special case ObjCInterfaceDecl here to get at both the interface definition + implementation if available? |
clang-tools-extra/clangd/XRefs.cpp | ||
---|---|---|
83 | This is a really useful comment, thanks! | |
88–92 | I think there might be (what you've done seems reasonable in isolation but I'm not sure it'll yield the best behavior). Consider this code: int foo(); // A int foo(); // B int foo(){} // C We have 3 declarations. A will be chosen as the canonical declaration (because it's first, though things get more complicated when the index is involved). C is the definition. So go-to-definition will land on C, and then triggering it again will take you to A, and then back to C. go-to-declaration is the opposite. B is basically just a reference for our purposes, we won't navigate you there (except by find-refs). Now let's look at your example again: @class MyClass; // A @interface MyClass ... @end // B @implementation MyClass ... @end // C Thinking about where you might want to navigate to, A is certainly the least important of these, right? So I think:
Some code will only see the forward declaration and that's OK. The index's merge rules are that a candidate "canonical declaration" which has an associated definition is preferred over one that doesn't. Since the implementation can always see the interface, the interface will end up being the canonical declaration after merge. | |
317 | Rather than returning both results, I think it's more consistent to return them as a declaration/definition pair. (This means special-casing ObjCImplDecl in namedDecl or at least getDeclAsPosition, so you always end up with the ObjCInterfaceDecl instead) | |
clang-tools-extra/clangd/unittests/XRefsTests.cpp | ||
743 | Hm, I quite like this. You can probably also just do TU.ExtraArgs.push_back("-xobjective-c++") though, it probably won't break anything. |
clang-tools-extra/clangd/XRefs.cpp | ||
---|---|---|
88–92 | Thanks, that makes sense - I agree that B should be the actual canonical declaration for our case (even though Clang picks A) and C is the definition. I think that part was confusing me, since Clang seems to use the forward declaration as the canonical one when it exists, but it seems like that's intentional/OK and shouldn't change (e.g. for ReferenceFinder in the same file). |
I think without index changes this will still give the wrong answer for go-to-definition if the @implementation is in a different file.
Do you want to include those too or will that work ok in a separate patch?
(I'm not 100% sure of the interactions here, possible it'll seem a bit glitchy)
clang-tools-extra/clangd/XRefs.cpp | ||
---|---|---|
88–92 |
This is super confusing. Clang has a concept of canonical declaration that's basically arbitrary: you pick one so that e.g. if you have canonical decls you can compare their pointers. It picks the first one seen, which is nice because it's stable over the life of the parse. Clangd has a concept of canonical declaration that's not arbitrary: it's meant to be the declaration the user wants to see. And yet, clangd's canonical is initialized to clang's canonical by default, because we've got to pick something. | |
263 | nit: real definition -> primary declaration | |
265 | I'd consider getPreferredDecl here. |
I'll look into the index changes as well in this change, just wanted to get the base functionality down for xrefs first. If it seems large I'll probably send a follow up diff.
Looking further into the indexing support, I've questions:
- How targetDecl should behave for these objc container types
- Similarly for SymbolCollector (guessing in SymbolCollector::handleDeclOccurrence ?)
clang-tools-extra/clangd/XRefs.cpp | ||
---|---|---|
317 | Whoops, meant to comment here but it was lost. I'm not sure what you meant here. Should this be done here inside the for loop or in getDeclAtPosition? Are you saying that given: @interface Foo // A @end @implementation Foo // B @end B --> A here and similarly @interface Foo // A @end @interface Foo (Ext) // B @end @implementation Foo (Ext) // C @end B --> A |
clang-tools-extra/clangd/FindTarget.cpp | ||
---|---|---|
305 | I think this is what you had in mind for findTarget, right? I guess though to be safe we should check D for nullptr before reporting below? |
(Hmm, targetDecl doesn't seem to canonicalize decls anywhere, which might be a bug, but it's unrelated to objc)
The main thing is that AIUI @implementation is one thing, as far as clang's concerned, while @class+@interface are another.
i.e. getCanonicalDecl() and USR generation both split them into equivalence classes {@implementation} and {@class, @interface}. Is that right?
In that case the thing targetDecl needs to do is "jump" from the implementation to the interface, because we're pretending they're the same decl.
It doesn't need to jump from the @class to the @interface, semantic canonicalization doesn't belong there. (In fact it may need to do the opposite, since targetDecl might be relying on implicitly canonicalizing with clang's semantics).
- Similarly for SymbolCollector (guessing in SymbolCollector::handleDeclOccurrence ?)
Right - this is where most of the real canonicalization happens.
You need to ensure that:
- isPreferred is true for @interface, so that when we see both @class and @interface we'll take the latter as our preferred declaration
- addDefinition gets called for @implementation, so that the definition location is set correctly. Also the SymbolID used in this case must be based on the USR of the interface, so we're considering them a single symbol. (Either addDeclaration should not be called in this case, or it should be called with the @implementation again somehow).
I think that should be enough...
clang-tools-extra/clangd/XRefs.cpp | ||
---|---|---|
83 | ... and it's gone. | |
317 | In the first example, selecting either A or B should yield one LocatedSymbol with {Decl=A, Def=B}. This shouldn't require any special-casing. The second example is very similar to template specialization, with exceptions:
I'd suggest, given @interface Foo (Ext):
And @implementation Foo(Ext) should behave in exactly the same way. |
clang-tools-extra/clangd/XRefs.cpp | ||
---|---|---|
83 | I had moved it to getPreferredDecl, will add it back here as well with more info (need to special case one more special category: a class extension) | |
317 | Trying this out now, two problems:
|
clang-tools-extra/clangd/XRefs.cpp | ||
---|---|---|
317 |
Oh right... yes, this seems fine to me (for the ObjCContainerDecl subclasses only, and with a comment)
I'm not sure what "this" in "this is actually a DeclRelation::Underlying" refers to. Do you have a failing testcase? |
Fixes for ObjC in SymbolCollector as well as XRefs
- Make sure ObjC indexing doesn't use the forward decls and doesn't treat the impl as the canonical decl
- Don't treat ObjC protocol forward declarations as true declarations
- Also AddDecl for ObjC interfaces when the interface name in a category is selected
I believe they share the same USR but besides that, yes that's correct.
In that case the thing targetDecl needs to do is "jump" from the implementation to the interface, because we're pretending they're the same decl.
It doesn't need to jump from the @class to the @interface, semantic canonicalization doesn't belong there. (In fact it may need to do the opposite, since targetDecl might be relying on implicitly canonicalizing with clang's semantics).
SGTM, done.
- Similarly for SymbolCollector (guessing in SymbolCollector::handleDeclOccurrence ?)
Right - this is where most of the real canonicalization happens.
You need to ensure that:
- isPreferred is true for @interface, so that when we see both @class and @interface we'll take the latter as our preferred declaration
- addDefinition gets called for @implementation, so that the definition location is set correctly. Also the SymbolID used in this case must be based on the USR of the interface, so we're considering them a single symbol. (Either addDeclaration should not be called in this case, or it should be called with the @implementation again somehow).
I think that should be enough...
Gone ahead and implemented this. It seems OK. I think the only thing that might be missing here is class extensions:
@interface Foo @end @interface Foo () - (void)bar; @end @implementation Foo - (void)bar {} @end
I implemented goto-definition from Foo() --> Foo impl in Xrefs, on the symbolcollector side I don't think there's anything to do?
clang-tools-extra/clangd/XRefs.cpp | ||
---|---|---|
317 |
Not besides what was mentioned above, so I think it's OK at the moment.
I meant that for ObjC, I used DeclRelation::Underlying so I have to include it to get the ObjCCategoryImplDecl. I guess I can later filter out the DeclRelation::Underlying unless it's for the ObjC case. Failing test, I think only returning callback is intentional? LocateSymbol.TemplateTypedefs Value of: locateSymbolAt(AST, T.point()) Expected: has 1 element that sym "callback" Actual: { function: 1:30-1:38@file:///clangd-test/TestTU.cpp def=1:30-1:38@file:///clangd-test/TestTU.cpp, callback: 2:29-2:37@file:///clangd-test/TestTU.cpp }, which has 2 elements Code: template <class T> struct function {}; template <class T> using callback = function<T()>; c^allback<int> foo; |
This can't be done purely in xrefs as the AST may not be available.
A.m: @class Foo; ^Foo x; <-- go-to-definition at ^
B.m: @interface Foo...@end @implementation Foo...@end
The index needs to know that for the USR associated with the @class (found by targetDecl), the canonical decl is the @interface in B and the definition is the @implementation in B.
So SymbolCollector needs to see it as a definition. The tests seem to show it does already, but it's not obvious why from the code. Do you know? Maybe it's the fact that they share a USR and thus a symbol ID. This is worth making explicit somewhere.
Thanks, this looks pretty good, I think there are a couple of adjustments...
clang-tools-extra/clangd/XRefs.cpp | ||
---|---|---|
317 |
Underlying isn't appropriate here - it's used for situations like typedefs where there's clearly two symbols (two names) and an asymmetrical synonym relation (e.g. int32 is a synonym for int, but not the other way around). Can you just stop setting Underlying in FindTargets? | |
clang-tools-extra/clangd/index/SymbolCollector.cpp | ||
159–160 | "preferred" declaration is just the method name, and "true" declaration doesn't seem to clarify much. For this overview paragraph I'd suggest: Checks whether \p ND is a good candidate to be the *canonical* declaration of its symbol (e.g. a go-to-declaration target). This overrides the default of using Clang's canonical declaration, which is the first in the TU. | |
162 | I'm not sure the for C++/for ObjC split in the doc makes sense - these special cases are more to do with the node types than the underlying languages (which can overlap). | |
179 | this isn't really an example, it's the only case. I think it can be moved inside the function, above the case itself. "We treat ObjCImplDecl as a definition but not a declaration - the corresponding ObjcInterfaceDecl is the declaration" | |
368 | Hmm, not sure about this. What if the @implementation is in a different file we're not indexing? Then we'll end up with no declaration. Probably instead we should treat this as if it were a non-preferred non-canonical declaration whose canonical declaration is the @interface. if (auto *OCID = llvm::dyn_cast<ObjCImplDecl>(D)) D = OCID->getClassInterface(); // hmm, but maybe not if it's an implicit @interface | |
373 | you're now clobbering BasicSymbol every time, even if it was set and this isn't a preferred declaration | |
clang-tools-extra/clangd/unittests/FindTargetTests.cpp | ||
611 | can we add a case here and in symbolcollector testing the "implicit interface decl" case? (ObjCInterfaceDecl::isImplicitInterfaceDecl()) |
Think we're talking about different things. See ObjCClassExtensions in SymbolCollectorTests which I added, the idea is that the Cat () can link to the implementation like I added to XRefs, but I'm not sure how this should be represented. As for @class -> @interface/@implementation those should have the same USR, yes.
clang-tools-extra/clangd/XRefs.cpp | ||
---|---|---|
317 | If I do that it'll be filtered out since it's not a DeclRelation::TemplatePattern | DeclRelation::Alias. Should I add a new DeclRelation for this? | |
clang-tools-extra/clangd/index/SymbolCollector.cpp | ||
368 | Ok, I've done something like this but it gets odd since we need to keep track that it is canonical. LMK if I should swap this to be later to replace OriginalDecl or isPreferredDeclaration. |
clang-tools-extra/clangd/unittests/SymbolCollectorTests.cpp | ||
---|---|---|
614 | Here's the ClassExtension that I was talking about. Ideally we can map each Cat () --> @implementation Cat like I did in XRefs But as you said the Cat () could be in a different file and I think it has a different USR. |
Oh, sorry I indeed missed the parens.
Yes, this is a go-to-def special case, SymbolCollector shouldn't need anything. (I'm assuming that a reference to the class is reported already - could add a test that the ref exists if you like).
clang-tools-extra/clangd/XRefs.cpp | ||
---|---|---|
317 | I think one of us is confused about what these bitmasks do :-)
Not all targets found have a relation set, e.g. a DeclRefExpr to a variable doesn't have any of these bits. This means it's universal - it'll always be considered the target. |
if I understand this correctly I think MultipleDeclsWithSameDefinition already tests that, although it relies upon locateASTReferent 's TouchedIdentifier being the interface. Should it always be added instead?
clang-tools-extra/clangd/XRefs.cpp | ||
---|---|---|
317 | Haha yeah, should have just tried it. Didn't realize the masking style in targetDecl which filters from allTargetDecls always includes 0 Relations sets (since it's a not a normal filter, it's inverting). It works =) |
clang-tools-extra/clangd/FindTarget.cpp | ||
---|---|---|
307 | This just describes what the code is doing, say why instead? | |
clang-tools-extra/clangd/unittests/FindTargetTests.cpp | ||
615 | Hmm, do we want to use the @interface or @implementation for this case? The interface is implicit but probably still has a valid location. | |
clang-tools-extra/clangd/unittests/SymbolCollectorTests.cpp | ||
614 |
I'm not sure there's anything that would ideally be done differently here. The thing to test would be that we're emitting *refs* from @interface [[Cat]] () to catdecl. | |
clang-tools-extra/clangd/unittests/XRefsTests.cpp | ||
710 | maybe add a comment like // prefer interface definition over forward declaration | |
803 | this seems to be copy/pasted from the test above. | |
838 | and again here Desire to split these tables up into named tests is something we want to address somehow, but we don't have a good answer right now and it's important for maintenance that the logic/annotation conventions don't diverge across different tests that could be the same. |
(Sorry this has been pending a while - I think it's basically there. Only things we really need to address to land this is have a consistent view of what the canonical decl is for the no-@interface case, and avoid too much duplication of mechanisms in the tests)
No problem, thanks for the review
clang-tools-extra/clangd/unittests/FindTargetTests.cpp | ||
---|---|---|
615 | Good catch, don't think this is a common case but yeah I think the impl makes more sense then. swapped over | |
clang-tools-extra/clangd/unittests/SymbolCollectorTests.cpp | ||
614 | Hmm, it looks like at the moment it either shares the same QName or doesn't have one. This might be good to look into a follow up patch? | |
clang-tools-extra/clangd/unittests/XRefsTests.cpp | ||
803 | I could merge them but I figured it would be better to separate tests with multi def/decls from those with just one. WDYT? | |
838 | This one is split because you can't annotate one symbol with multiple annotations. I can instead make this a regular non generic test like the following, WDYT? @interface $interfacedecl[[Cat]] @end @interface $classextensiondecl[[Ca^t]] () - (void)meow; @end @implementation $implementationdecl[[Cat]] - (void)meow {} @end |
Ugh, I forgot to hit submit and went on vacation :-\ Really sorry again.
As much as we can simplify/unify the tests that helps, but let's not block on this anymore, up to you.
clang-tools-extra/clangd/unittests/SymbolCollectorTests.cpp | ||
---|---|---|
614 | Sorry, I don't know what you mean about QName. USR? Yeah, fine to defer out of this patch. This is just "does find references on an @interface find extensions of that interface". | |
clang-tools-extra/clangd/unittests/XRefsTests.cpp | ||
803 | I think it would be better to split each decl/def pair into its own testcase, and combine with the above (even if it means a bit of duplication between test cases) | |
838 |
Not sure I quite see what you mean here, but $foo[[$bar[[symbol]]]] should work. Anyway, fine if you want to leave this one separate. I'd avoid the tests array+loop if there's just one. |
- Final touches
clang-tools-extra/clangd/unittests/SymbolCollectorTests.cpp | ||
---|---|---|
614 | QualifiedName, will follow up later | |
clang-tools-extra/clangd/unittests/XRefsTests.cpp | ||
803 | These tests are odd since one point (^) can trigger multiple decls, so I think it's worth keeping separate vs. the other tests which assume 1 decl/def pair. | |
838 | 🤦 Ah thanks, that works! |
I think this is what you had in mind for findTarget, right? I guess though to be safe we should check D for nullptr before reporting below?