These can be invoked at different stages while building an AST to let
FeatureModules implement features on top of it. The patch also
introduces a sawDiagnostic hook, which can mutate the final clangd::Diag
while reading a clang::Diagnostic.
Details
- Reviewers
sammccall - Commits
- rGbce3ac4f224a: [clangd] Introduce ASTHooks to FeatureModules
Diff Detail
- Repository
- rG LLVM Github Monorepo
Event Timeline
I'm getting a little nervous about the amount of stuff we're packing into modules without in-tree examples.
I should split out some of the "standard" features into modules as that's possible already.
My model for modules using this diagnostic stuff (apart from the build-system stuff which sadly can't be meaningfully upstreamed) are IncludeFixer, ClangTidy, and IWYU - worth thinking about how we'd build those on top of this model. (Doesn't mean we need to add a hook to emit diagnostics, but it probably means we should know where it would go)
clang-tools-extra/clangd/FeatureModule.h | ||
---|---|---|
104 | naming: giving this a plural name and having a collection of them is a little confusing (is Hooks the hooks from one module, or from all of them?). What about ASTListener? | |
112 | This comment hints at what I *thought* was the idea: astHooks() is called each time we parse a file, the returned object has methods called on it while the file is being parsed, and is then destroyed. But the code suggests we call once globally and it has effectively the same lifetime as the module. (Lots of natural extensions here like providing ParseInputs to astHooks(), but YAGNI for now) |
Agreed. I believe they all would need extra end points to ASTHooks though.
ClangTidy:
- needs extra hooks to register PP callbacks, and take in a diagnostics engine
- needs a new endpoint to traverse ast and *emit* diags
- CTContext needs to be alive until the parsing is done: so we can:
- make ASTHooks own it and instantiate a new one on every parse (i think the cleanest and most explicit)
- make the module own them and control the lifetime with entry/exit calls on the asthooks. (there's more burden on the modules now, and they'll need extra synchronisation on enter/exit calls)
- return some other object on parsing entry hook that'll be kept alive by the caller (needs design around semantics of that object).
IncludeFixer:
- needs a new hook to act as an external sema source, so that it can fix unresolved names.
- similar to CTContext issue, it references per-tu state like HeaderSearchInfo, so will need to incorporate these into entry hook, and somehow disambiguate hooks for different TUs (all the options proposed for tidy).
IWYU:
- I'd expect this to make use of ~same API as IncludeFixer.
PreamblePatching:
- can mutate compiler instance via a hook
- drop diagnostics from ParsedAST on exit (requires some mutation on parsedast though)
clang-tools-extra/clangd/FeatureModule.h | ||
---|---|---|
112 |
This was the intent actually (to enable modularization of other features like clangtidy, includefixer, etc. as mentioned above), but looks like i got confused while integrating this into clangdserver :/ While trying to resolve my confusion i actually noticed that we cannot uphold the contract of "hooks being called synchronously", because we actually have both a preamblethread and astworker that can invoke hooks (embarrassing of me to forget that 😓). So we can:
I am leaning towards 4, but unsure (mostly hesitant about dependency schema, as featuremodules also depend on TUScheduler..). WDYT? |
Yup. Let's not bite off more for now.
- make ASTHooks own it and instantiate a new one on every parse (i think the cleanest and most explicit)
Agreed. (And this only one that overlaps this patch a lot)
clang-tools-extra/clangd/FeatureModule.h | ||
---|---|---|
112 |
Ha! And yes, in our motivating case of fixing build system rules, the diagnostics are mostly going to be in the preamble. The options that seem most tempting to me are:
These are roughly in order of complexity so we should probably start toward the top of the list somewhere. Up to you. (I don't like the #1 or #3 in your list above much at all.) |
clang-tools-extra/clangd/FeatureModule.h | ||
---|---|---|
112 |
yes this is what i meant by #4.
I'd lean towards the second option(creating separate ASTHooks with the same interface for preamble and astworker). As discussed before first one (i.e. scraping diag message) is a limited solution that's likely to get us into a corner in the future. let me know if you have any concerns around this approach. |
clang-tools-extra/clangd/ClangdServer.cpp | ||
---|---|---|
237 | Now we're creating two ASTListeners for each version of the file seen, but most won't be parsed twice (and some won't even be parsed once). This seems like it's going to be a weird wart for e.g. hooks that expect to do something interesting when a file finishes, or those that allocate something heavyweight. I'd suggest sinking the ModuleSet into ParsedAST::build etc, or at least moving the astHooks() calls into TUScheduler. (And documenting that astHooks() should be threadsafe) Otherwise, need to document that sometimes hooks are created and then never used. | |
clang-tools-extra/clangd/Compiler.h | ||
60 | I think having these in ParseInputs is conceptually confusing and shared_ptr is risky. I think these listeners should be clearly either:
You could consider the ModuleSet to be part of ParseInputs, though... | |
clang-tools-extra/clangd/Diagnostics.h | ||
147 | ArrayRef<shared_ptr<ASTListener>> seems like we're leaking a lot. And even mentioning AST hooks seems dubious layering-wise. | |
clang-tools-extra/clangd/FeatureModule.h | ||
101 | nit: this describes the interaction more but not what it's for. /// Extension point that allows modules to observe and modify an AST build. /// One instance is created each time clangd produces a ParsedAST or PrecompiledPreamble. /// For a given instance, lifecycle methods are always called on a single thread. | |
113 | the "hooks" name is still used here and throughout |
- Pass FeatureModuleSet rather than astListeners in ParseInputs.
- Create listeners only when building ASTs.
- Use a callback function in StoreDiags to notify about diagnostics.
Now we're creating two ASTListeners for each version of the file seen, but most won't be parsed twice (and some won't even be parsed once).
This seems like it's going to be a weird wart for e.g. hooks that expect to do something interesting when a file finishes, or those that allocate something heavyweight.
I'd suggest sinking the ModuleSet into ParsedAST::build etc, or at least moving the astHooks() calls into TUScheduler. (And documenting that astHooks() should be threadsafe)
Otherwise, need to document that sometimes hooks are created and then never used.