Clangd / source migration walkthrough

Moving clang-tidy into a FeatureModule

Before 101584320859 → after c64f1fca38c6 (squashed). This compares only the migration commit with its parent, which already includes the interface extension and baseline tests.

17 changed files · +624 / −408

The main change is ownership: clang-tidy logic formerly embedded in ParsedAST.cpp and Diagnostics.cpp moves into ClangTidyFeatureModule and its per-build TidyASTListener. Core parsing orchestration stays in place.

Code and comments move together. Original explanatory comments and existing FIXMEs move together with their code. Their wording is retained; indentation and line wrapping may change. New comments explain module ownership and wiring. See the five paired examples in figure 19.

Matching remains a direct MatchFinder call: no consumer wrapper, takeover or reinitialization is introduced. The parent already contains four relevant regression tests; this patch adds the provider-swap test and adapts existing setup. No lit test is added.

Complete change on GitHub · Markdown on GitHub · Download document and images · Download patch

Amber (−) marks lines removed at the shown location; green (+) marks added lines; gray marks unchanged context. Colors follow the actual git diff. Arrows map the responsibility described by the figure, not every line in its excerpts. Click filenames for exact source links or open a diagram at full size.

0. Clang-tidy moves behind existing FeatureModule hooks

Tidy responsibilities move with their original comments. Core parsing orchestration stays in place; matching remains a direct MatchFinder call.

Clang-tidy moves behind existing FeatureModule hooksTidy responsibilities move with their original comments. Core parsing orchestration stays in place; matching remains a direct MatchFinder call.CLANGD / CLANG-TIDY MIGRATIONMoving clang-tidy into a FeatureModule101584320859 → c64f1fca38c6 | 17 changed files | +624 / −408BEFORE: TIDY EMBEDDED IN COREAFTER: CLANGTIDYFEATUREMODULEParsedAST.cppRead .clang-tidy and apply warning optionsConstruct context, checks and matchersRun MatchFinder after tokens and traversal scopeFilter diagnostics: suppression, NOLINT, severityDiagnostics.cppTidy names, message cleanup and tagsPer-build TidyASTListenerbeforeBeginSourceFile → options and warning flagsbeforePPCallbacks → checks, PP callbacks, matchersafterExecute → Finder.matchAST(CI.getASTContext())sawDiagnostic → tidy suppression and severityfinalizeDiagnostic → names, message cleanup, tagsMOVE+ commentsSTAYS IN COREFrontend startup • token collection • traversal scope • IncludeFixer • lifecycle dispatchNo new AST-consumer wrapper. FeatureModule.h, Preamble.cpp and TestTU.h are unchanged.Existing regression tests are reused. One provider-swap test is added; no lit test is added.Original explanatory comments and existing FIXMEs move with the code; see figure 19.
SVGPNG
  • The comparison includes only the migration commit, not the earlier feature-interface or test additions.

1. Configuration and compiler warning options

Only tidy options and warning handling move into the listener; frontend startup stays in ParsedAST. Action, MainInput, BeginSourceFile and the beforeBeginSourceFile hook loop remain unchanged. Use clangd's remapped main-file spelling; normalize only a relative fallback before configuration lookup.

Configuration and compiler warning optionsOnly tidy options and warning handling move into the listener; frontend startup stays in ParsedAST. Action, MainInput, BeginSourceFile and the beforeBeginSourceFile hook loop remain unchanged. Use clangd's remapped main-file spelling; normalize only a relative fallback before configuration lookup. Frontend startup and beforeBeginSourceFile dispatch remain in ParsedAST. The remapped main-file buffer preserves the requested filename for configuration lookup.CLANGD / CLANG-TIDY MIGRATION 01 / 19Configuration and compiler warning optionsOnly tidy options and warning handling move into the listener; frontend startup stays in ParsedAST. Action, MainInput, BeginSourceFile and thebeforeBeginSourceFile hook loop remain unchanged. Use clangd's remapped main-file spelling; normalize only a relative fallback beforeconfiguration lookup.MOVE + ADAPTRemoved here (diff)Added here (diff)Unchanged contextBEFORE / 101584320859Open exact source on GitHubParsedAST.cpp−512 tidy::ClangTidyOptions ClangTidyOpts;−513 {−514 trace::Span Tracer("ClangTidyOpts");−515 ClangTidyOpts = getTidyOptionsForFile(Inputs.ClangTidyProvider, Filename);−516 dlog("ClangTidy configuration for file {0}: {1}", Filename,−517 tidy::configurationAsText(ClangTidyOpts));⋯−538 TidyDiagnosticGroups TidyGroups(ClangTidyOpts.Checks ? *ClangTidyOpts.Checks−539 : llvm::StringRef());−540 if (ClangTidyOpts.ExtraArgsBefore)−541 applyWarningOptions(*ClangTidyOpts.ExtraArgsBefore, TidyGroups, Diags);−542 if (ClangTidyOpts.ExtraArgs)−543 applyWarningOptions(*ClangTidyOpts.ExtraArgs, TidyGroups, Diags);−544 }−545546 auto Action = std::make_unique<ClangdFrontendAction>();547 for (const auto &L : ASTListeners)548 L->beforeBeginSourceFile(*Clang);549 const FrontendInputFile &MainInput = Clang->getFrontendOpts().Inputs[0];550 if (!Action->BeginSourceFile(*Clang, MainInput)) {AFTER / c64f1fca38c6Open exact source on GitHubClangTidyFeatureModule.cpp+172 void beforeBeginSourceFile(CompilerInstance &CI) override {+173 // Warning options must precede frontend initialization: module loading in+174 // BeginSourceFile() can consult their severity to decide whether to fail.+175 const auto &Inputs = CI.getFrontendOpts().Inputs;+176 if (Inputs.empty() || !Inputs.front().isFile()) {+177 elog("Cannot configure clang-tidy without a main-file input");+178 return;+179 }+180 llvm::SmallString<256> Path(Inputs.front().getFile());+181 // The remapped buffer carries clangd's original main-file spelling.+182 for (const auto &Remap :+183 llvm::reverse(CI.getPreprocessorOpts().RemappedFileBuffers)) {+184 if (Remap.first == Inputs.front().getFile()) {+185 Path = Remap.second->getBufferIdentifier();+186 break;+187 }+188 }+189 // Providers search parent directories for .clang-tidy and require an+190 // absolute path. Respect the compiler's working directory, not clangd's.+191 if (!llvm::sys::path::is_absolute(Path))+192 CI.getFileManager().makeAbsolutePath(Path, /*Canonicalize=*/true);+193 if (!llvm::sys::path::is_absolute(Path)) {+194 elog("Cannot resolve clang-tidy input path: {0}", Path);+195 return;+196 }+197 Filename = Path.str().str();+198 trace::Span Tracer("ClangTidyOpts");+199 Options = getTidyOptionsForFile(*Provider, Filename);⋯+221 TidyDiagnosticGroups EnabledGroups(Options->Checks ? *Options->Checks+222 : llvm::StringRef());+223 if (Options->ExtraArgsBefore)+224 applyWarningOptions(*Options->ExtraArgsBefore, EnabledGroups,+225 CI.getDiagnostics());+226 if (Options->ExtraArgs)+227 applyWarningOptions(*Options->ExtraArgs, EnabledGroups,+228 CI.getDiagnostics());+229 }Frontend startup and beforeBeginSourceFile dispatch remain in ParsedAST.The remapped main-file buffer preserves the requested filename for configuration lookup.Source: 101584320859 → c64f1fca38c6 • Exact snapshots; arrows map responsibilities, not every line
SVGPNG
  • Frontend startup and beforeBeginSourceFile dispatch remain in ParsedAST.
  • The remapped main-file buffer preserves the requested filename for configuration lookup.
  • Only configuration lookup and warning handling move. Action, MainInput, BeginSourceFile and lifecycle dispatch remain core responsibilities.

2. Tidy-specific helpers and linker dependencies

The warning-group parser, warning mapper, fast-check filter and force-linker leave ParsedAST.cpp and live with the module. Selected excerpts represent the helper bodies; the full diff includes every moved line. filterFastTidyChecks becomes filterFastChecks; original helper comments and FIXMEs move with the code.

Tidy-specific helpers and linker dependenciesThe warning-group parser, warning mapper, fast-check filter and force-linker leave ParsedAST.cpp and live with the module. Selected excerpts represent the helper bodies; the full diff includes every moved line. filterFastTidyChecks becomes filterFastChecks; original helper comments and FIXMEs move with the code. The warning-group parser, warning mapper, fast-check filter and force-linker leave ParsedAST.cpp and live with the module. Selected excerpts represent the helper bodies; the full diff includes every moved line. filterFastTidyChecks becomes filterFastChecks; original helper comments and FIXMEs move with the code.CLANGD / CLANG-TIDY MIGRATION 02 / 19Tidy-specific helpers and linker dependenciesThe warning-group parser, warning mapper, fast-check filter and force-linker leave ParsedAST.cpp and live with the module. Selected excerptsrepresent the helper bodies; the full diff includes every moved line. filterFastTidyChecks becomes filterFastChecks; original helper commentsand FIXMEs move with the code.MOVE + CLEANUPRemoved here (diff)Added here (diff)Unchanged contextBEFORE / 101584320859Open exact source on GitHubParsedAST.cpp−79// Force the linker to link in Clang-tidy modules.−80// clangd doesn't support the static analyzer.−81#if CLANGD_TIDY_CHECKS−82#define CLANG_TIDY_DISABLE_STATIC_ANALYZER_CHECKS−83#include "../clang-tidy/ClangTidyForceLinker.h"−84#endif⋯−304 TidyDiagnosticGroups(llvm::StringRef Checks) {−305 constexpr llvm::StringLiteral CDPrefix = "clang-diagnostic-";−306−307 llvm::StringRef Check;−308 while (!Checks.empty()) {−309 std::tie(Check, Checks) = Checks.split(',');−310 Check = Check.trim();⋯−367 bool NeedsWerrorExclusion = false;−368 for (diag::kind ID : Members) {−369 if (Enable) {−370 if (Diags.getDiagnosticLevel(ID, SourceLocation()) <−371 DiagnosticsEngine::Warning) {−372 auto Group = Diags.getDiagnosticIDs()->getGroupForDiag(ID);−373 if (!Group || !EnabledGroups(*Group))−374 continue;−375 Diags.setSeverity(ID, diag::Severity::Warning, SourceLocation());−376 if (Diags.getWarningsAsErrors())−377 NeedsWerrorExclusion = true;−378 }−379 } else {−380 Diags.setSeverity(ID, diag::Severity::Ignored, SourceLocation());−381 }⋯−416tidy::ClangTidyCheckFactories−417filterFastTidyChecks(const tidy::ClangTidyCheckFactories &All,−418 Config::FastCheckPolicy Policy) {−419 if (Policy == Config::FastCheckPolicy::None)−420 return All;−421 bool AllowUnknown = Policy == Config::FastCheckPolicy::Loose;−422 tidy::ClangTidyCheckFactories Fast;−423 for (const auto &Factory : All) {−424 if (isFastTidyCheck(Factory.getKey()).value_or(AllowUnknown))−425 Fast.registerCheckFactory(Factory.first(), Factory.second);−426 }−427 return Fast;−428}AFTER / c64f1fca38c6Open exact source on GitHubClangTidyFeatureModule.cpp+40// Force the linker to link in Clang-tidy modules.+41// clangd doesn't support the static analyzer.+42#if CLANGD_TIDY_CHECKS+43#define CLANG_TIDY_DISABLE_STATIC_ANALYZER_CHECKS+44#include "../clang-tidy/ClangTidyForceLinker.h"+45#endif⋯+73 TidyDiagnosticGroups(llvm::StringRef Checks) {+74 constexpr llvm::StringLiteral Prefix = "clang-diagnostic-";+75 llvm::StringRef Check;+76 while (!Checks.empty()) {+77 std::tie(Check, Checks) = Checks.split(',');+78 Check = Check.trim();+79 if (Check.empty())+80 continue;+81+82 bool Enable = !Check.consume_front("-");+83 bool Glob = Check.consume_back("*");⋯+130 bool NeedsWerrorExclusion = false;+131 for (diag::kind ID : Members) {+132 if (Enable) {+133 if (Diags.getDiagnosticLevel(ID, SourceLocation()) >=+134 DiagnosticsEngine::Warning)+135 continue;+136 auto DiagGroup = Diags.getDiagnosticIDs()->getGroupForDiag(ID);+137 if (!DiagGroup || !EnabledGroups(*DiagGroup))+138 continue;+139 Diags.setSeverity(ID, diag::Severity::Warning, SourceLocation());+140 NeedsWerrorExclusion |= Diags.getWarningsAsErrors();+141 } else {+142 Diags.setSeverity(ID, diag::Severity::Ignored, SourceLocation());+143 }⋯+154tidy::ClangTidyCheckFactories+155filterFastChecks(const tidy::ClangTidyCheckFactories &All,+156 Config::FastCheckPolicy Policy) {+157 if (Policy == Config::FastCheckPolicy::None)+158 return All;+159 bool AllowUnknown = Policy == Config::FastCheckPolicy::Loose;+160 tidy::ClangTidyCheckFactories Fast;+161 for (const auto &Factory : All)+162 if (isFastTidyCheck(Factory.getKey()).value_or(AllowUnknown))+163 Fast.registerCheckFactory(Factory.first(), Factory.second);+164 return Fast;+165}The warning-group parser, warning mapper, fast-check filter and force-linker leave ParsedAST.cpp and live with the module. Selected excerpts represent the helperbodies; the full diff includes every moved line. filterFastTidyChecks becomes filterFastChecks; original helper comments and FIXMEs move with the code.Source: 101584320859 → c64f1fca38c6 • Exact snapshots; arrows map responsibilities, not every line
SVGPNG
  • The warning-group parser, warning mapper, fast-check filter and force-linker leave ParsedAST.cpp and live with the module. Selected excerpts represent the helper bodies; the full diff includes every moved line. filterFastTidyChecks becomes filterFastChecks; original helper comments and FIXMEs move with the code.
  • Selected excerpts show the helper responsibilities; migration.patch contains every moved line. filterFastTidyChecks is renamed filterFastChecks.

3. Per-build state and ClangTidyContext

Stack-local tidy state becomes per-build listener state. Context outlives its checks and MatchFinder; matching is invoked directly after parsing.

Per-build state and ClangTidyContextStack-local tidy state becomes per-build listener state. Context outlives its checks and MatchFinder; matching is invoked directly after parsing. Options move into ClangTidyContext; member order preserves dependent lifetimes. Preamble listeners remain inactive. No deferred-consumer state is introduced.CLANGD / CLANG-TIDY MIGRATION 03 / 19Per-build state and ClangTidyContextStack-local tidy state becomes per-build listener state. Context outlives its checks and MatchFinder; matching is invoked directly afterparsing.MOVE + OWNERSHIPRemoved here (diff)Added here (diff)Unchanged contextBEFORE / 101584320859Open exact source on GitHubParsedAST.cpp−572 std::vector<std::unique_ptr<tidy::ClangTidyCheck>> CTChecks;−573 ast_matchers::MatchFinder CTFinder;−574 std::optional<tidy::ClangTidyContext> CTContext;⋯−589 CTContext.emplace(std::make_unique<tidy::DefaultOptionsProvider>(−590 tidy::ClangTidyGlobalOptions(), ClangTidyOpts),−591 /*AllowEnablingAnalyzerAlphaCheckers=*/false,−592 /*EnableModuleHeadersParsing=*/false,−593 Cfg.Diagnostics.ClangTidy.ExperimentalCustomChecks);−594 // The lifetime of DiagnosticOptions is managed by \c Clang.−595 CTContext->setDiagnosticsEngine(nullptr, &Clang->getDiagnostics());−596 CTContext->setASTContext(&Clang->getASTContext());−597 CTContext->setCurrentFile(Filename);−598 CTContext->setSelfContainedDiags(true);−599 tidy::ClangTidyCheckFactories CTCheckFactories = *AllCTFactories;AFTER / c64f1fca38c6Open exact source on GitHubClangTidyFeatureModule.cpp+252 tidy::ClangTidyGlobalOptions(), std::move(*Options)),+253 /*AllowEnablingAnalyzerAlphaCheckers=*/false,+254 /*EnableModuleHeadersParsing=*/false,+255 Config::current().Diagnostics.ClangTidy.ExperimentalCustomChecks);+256 Options.reset();+257 // The lifetime of DiagnosticOptions is managed by \c Clang.+258 Context->setDiagnosticsEngine(nullptr, &CI.getDiagnostics());+259 Context->setASTContext(&CI.getASTContext());+260 Context->setCurrentFile(Filename);+261 Context->setSelfContainedDiags(true);+262 tidy::ClangTidyCheckFactories Factories = *AllFactories;+263#if CLANG_TIDY_ENABLE_QUERY_BASED_CUSTOM_CHECKS⋯+377private:+378 std::shared_ptr<const TidyProvider> Provider;+379 std::string Filename;+380 std::optional<tidy::ClangTidyOptions> Options;+381 // Destruction order matters: Finder and checks refer to Context.+382 std::optional<tidy::ClangTidyContext> Context;+383 std::vector<std::unique_ptr<tidy::ClangTidyCheck>> Checks;+384 ast_matchers::MatchFinder Finder;Options move into ClangTidyContext; member order preserves dependent lifetimes.Preamble listeners remain inactive. No deferred-consumer state is introduced.Source: 101584320859 → c64f1fca38c6 • Exact snapshots; arrows map responsibilities, not every line
SVGPNG
  • Options move into ClangTidyContext; member order preserves dependent lifetimes.
  • Preamble listeners remain inactive. No deferred-consumer state is introduced.
  • Context, checks and finder keep their original roles. Their member declaration order ensures dependent state is destroyed first.

4. Factories, custom checks and callback registration

Check setup moves into beforePPCallbacks(), after BeginSourceFile and before clangd captures callbacks for preamble replay. The registry here is ClangTidyModuleRegistry (check factories), not FeatureModuleRegistry. Custom-check registration, its mutex, fast-check filtering and PP/matcher registration are retained.

Factories, custom checks and callback registrationCheck setup moves into beforePPCallbacks(), after BeginSourceFile and before clangd captures callbacks for preamble replay. The registry here is ClangTidyModuleRegistry (check factories), not FeatureModuleRegistry. Custom-check registration, its mutex, fast-check filtering and PP/matcher registration are retained. Check factories still come from ClangTidyModuleRegistry, not FeatureModuleRegistry. Custom-check registration, synchronization, fast filtering and callback registration are retained.CLANGD / CLANG-TIDY MIGRATION 04 / 19Factories, custom checks and callback registrationCheck setup moves into beforePPCallbacks(), after BeginSourceFile and before clangd captures callbacks for preamble replay. The registry hereis ClangTidyModuleRegistry (check factories), not FeatureModuleRegistry. Custom-check registration, its mutex, fast-check filtering andPP/matcher registration are retained.MOVE + ADAPTRemoved here (diff)Added here (diff)Unchanged contextBEFORE / 101584320859Open exact source on GitHubParsedAST.cpp−582 trace::Span Tracer("ClangTidyInit");−583 static const auto *AllCTFactories = [] {−584 auto *CTFactories = new tidy::ClangTidyCheckFactories;−585 for (const auto &E : tidy::ClangTidyModuleRegistry::entries())−586 E.instantiate()->addCheckFactories(*CTFactories);−587 return CTFactories;−588 }();⋯−600#if CLANG_TIDY_ENABLE_QUERY_BASED_CUSTOM_CHECKS−601 if (CTContext->canExperimentalCustomChecks() &&−602 tidy::custom::RegisterCustomChecks) {−603 // RegisterCustomChecks tracks names in process-wide mutable state.−604 // Serializing its use keeps concurrent AST builds independent.−605 static std::mutex CustomChecksMu;−606 std::lock_guard<std::mutex> Lock(CustomChecksMu);−607 tidy::custom::RegisterCustomChecks(CTContext->getOptions(),−608 CTCheckFactories);−609 }−610#endif−611 tidy::ClangTidyCheckFactories FastFactories = filterFastTidyChecks(−612 CTCheckFactories, Cfg.Diagnostics.ClangTidy.FastCheckFilter);−613 CTChecks = FastFactories.createChecksForLanguage(&*CTContext);−614 Preprocessor *PP = &Clang->getPreprocessor();−615 for (const auto &Check : CTChecks) {−616 Check->registerPPCallbacks(Clang->getSourceManager(), PP, PP);−617 Check->registerMatchers(&CTFinder);−618 }AFTER / c64f1fca38c6Open exact source on GitHubClangTidyFeatureModule.cpp+231 void beforePPCallbacks(CompilerInstance &CI) override {+232 // Preamble builds don't call beforeBeginSourceFile(). Running tidy there+233 // would duplicate diagnostics and interfere with PCH generation.+234 if (!Options)+235 return;⋯+245 auto *Factories = new tidy::ClangTidyCheckFactories;+246 for (const auto &Entry : tidy::ClangTidyModuleRegistry::entries())+247 Entry.instantiate()->addCheckFactories(*Factories);+248 return Factories;+249 }();+250 Context.emplace(+251 std::make_unique<tidy::DefaultOptionsProvider>(⋯+264 if (Context->canExperimentalCustomChecks() &&+265 tidy::custom::RegisterCustomChecks) {+266 // RegisterCustomChecks tracks names in process-wide mutable state.+267 // Serializing its use keeps concurrent AST builds independent.+268 static std::mutex CustomChecksMu;+269 std::lock_guard<std::mutex> Lock(CustomChecksMu);+270 tidy::custom::RegisterCustomChecks(Context->getOptions(), Factories);+271 }+272#endif+273 auto FastFactories = filterFastChecks(+274 Factories, Config::current().Diagnostics.ClangTidy.FastCheckFilter);+275 Checks = FastFactories.createChecksForLanguage(&*Context);+276+277 Preprocessor *PP = &CI.getPreprocessor();+278 for (const auto &Check : Checks) {+279 Check->registerPPCallbacks(CI.getSourceManager(), PP, PP);+280 Check->registerMatchers(&Finder);+281 }Check factories still come from ClangTidyModuleRegistry, not FeatureModuleRegistry.Custom-check registration, synchronization, fast filtering and callback registration are retained.Source: 101584320859 → c64f1fca38c6 • Exact snapshots; arrows map responsibilities, not every line
SVGPNG
  • Check factories still come from ClangTidyModuleRegistry, not FeatureModuleRegistry.
  • Custom-check registration, synchronization, fast filtering and callback registration are retained.
  • The registry shown here is ClangTidyModuleRegistry for check factories, not FeatureModuleRegistry.

5. AST matching is a direct afterExecute call

The same MatchFinder call moves from ParsedAST into afterExecute(). Tokens are collected and traversal scope is restricted first, as before.

AST matching is a direct afterExecute callThe same MatchFinder call moves from ParsedAST into afterExecute(). Tokens are collected and traversal scope is restricted first, as before. Token finalization, traversal restriction and hook dispatch stay in ParsedAST. There is no deferred consumer, consumer replacement, or reinitialization.CLANGD / CLANG-TIDY MIGRATION 05 / 19AST matching is a direct afterExecute callThe same MatchFinder call moves from ParsedAST into afterExecute(). Tokens are collected and traversal scope is restricted first, as before.MOVE + ADAPTRemoved here (diff)Added here (diff)Unchanged contextBEFORE / 101584320859Open exact source on GitHubParsedAST.cpp770 // We have to consume the tokens before running clang-tidy to avoid collecting771 // tokens from running the preprocessor inside the checks (only772 // modernize-use-trailing-return-type does that today).773 syntax::TokenBuffer Tokens = std::move(CollectTokens).consume();774 // Makes SelectionTree build much faster.775 Tokens.indexExpandedTokens();776 std::vector<Decl *> ParsedDecls = Action->takeTopLevelDecls();777 // AST traversals should exclude the preamble, to avoid performance cliffs.778 Clang->getASTContext().setTraversalScope(ParsedDecls);−779 if (!CTChecks.empty()) {−780 // Run the AST-dependent part of the clang-tidy checks.−781 // (The preprocessor part ran already, via PPCallbacks).−782 trace::Span Tracer("ClangTidyMatch");−783 CTFinder.matchAST(Clang->getASTContext());−784 }785 for (const auto &L : ASTListeners)786 L->afterExecute(*Clang);AFTER / c64f1fca38c6Open exact source on GitHubParsedAST.cpp504 // We have to consume the tokens before running clang-tidy to avoid collecting505 // tokens from running the preprocessor inside the checks (only506 // modernize-use-trailing-return-type does that today).507 syntax::TokenBuffer Tokens = std::move(CollectTokens).consume();508 // Makes SelectionTree build much faster.509 Tokens.indexExpandedTokens();510 std::vector<Decl *> ParsedDecls = Action->takeTopLevelDecls();511 // AST traversals should exclude the preamble, to avoid performance cliffs.512 Clang->getASTContext().setTraversalScope(ParsedDecls);513 for (const auto &L : ASTListeners)514 L->afterExecute(*Clang);Open exact source on GitHubClangTidyFeatureModule.cpp+284 void afterExecute(CompilerInstance &CI) override {+285 if (Checks.empty())+286 return;+287 // Run the AST-dependent part of the clang-tidy checks.+288 // (The preprocessor part ran already, via PPCallbacks).+289 trace::Span Tracer("ClangTidyMatch");+290 Finder.matchAST(CI.getASTContext());+291 }Token finalization, traversal restriction and hook dispatch stay in ParsedAST.There is no deferred consumer, consumer replacement, or reinitialization.Source: 101584320859 → c64f1fca38c6 • Exact snapshots; arrows map responsibilities, not every line
SVGPNG
  • Token finalization, traversal restriction and hook dispatch stay in ParsedAST.
  • There is no deferred consumer, consumer replacement, or reinitialization.
  • afterExecute runs after token collection and traversal restriction, before PP.EndSourceFile(). This preserves the original matching order.

6. Existing hooks; no consumer replacement

The listener implements the existing FeatureModule interface. Matching runs directly in afterExecute; no new AST-consumer adapter is needed.

Existing hooks; no consumer replacementThe listener implements the existing FeatureModule interface. Matching runs directly in afterExecute; no new AST-consumer adapter is needed. FeatureModule.h is unchanged; its lifecycle hooks are already in the parent. The tidy module neither takes nor replaces the existing ASTConsumer.CLANGD / CLANG-TIDY MIGRATION 06 / 19Existing hooks; no consumer replacementThe listener implements the existing FeatureModule interface. Matching runs directly in afterExecute; no new AST-consumer adapter is needed.EXISTING INTERFACERemoved here (diff)Added here (diff)Unchanged contextBEFORE / 101584320859Open exact source on GitHubFeatureModule.h119 /// remain consistent with the preamble, which may already have been built.120 virtual void beforeBeginSourceFile(CompilerInstance &CI) {}⋯129 /// called.130 virtual void beforePPCallbacks(CompilerInstance &CI) {}⋯139 /// The preprocessor has not received EndSourceFile() yet.140 virtual void afterExecute(CompilerInstance &CI) {}⋯144 /// actions later on.145 virtual void sawDiagnostic(const clang::Diagnostic &, clangd::Diag &) {}⋯148 /// fixes, and before it is returned to the caller.149 virtual void finalizeDiagnostic(clangd::Diag &) {}AFTER / c64f1fca38c6Open exact source on GitHubClangTidyFeatureModule.cpp+172 void beforeBeginSourceFile(CompilerInstance &CI) override {+173 // Warning options must precede frontend initialization: module loading in+174 // BeginSourceFile() can consult their severity to decide whether to fail.Open exact source on GitHubClangTidyFeatureModule.cpp+231 void beforePPCallbacks(CompilerInstance &CI) override {+232 // Preamble builds don't call beforeBeginSourceFile(). Running tidy there+233 // would duplicate diagnostics and interfere with PCH generation.+234 if (!Options)+235 return;+236Open exact source on GitHubClangTidyFeatureModule.cpp+284 void afterExecute(CompilerInstance &CI) override {+285 if (Checks.empty())+286 return;+287 // Run the AST-dependent part of the clang-tidy checks.+288 // (The preprocessor part ran already, via PPCallbacks).+289 trace::Span Tracer("ClangTidyMatch");+290 Finder.matchAST(CI.getASTContext());+291 }Open exact source on GitHubClangTidyFeatureModule.cpp+293 void sawDiagnostic(const clang::Diagnostic &Info, clangd::Diag &D) override {+294 // Preserve the previous suppression policy when no checks are active.+295 if (Checks.empty())+296 return;+297 std::string CheckName = Context->getCheckName(Info.getID());Open exact source on GitHubClangTidyFeatureModule.cpp+344 void finalizeDiagnostic(clangd::Diag &D) override {+345 // Constructor diagnostics precede check initialization.+346 if (D.Source == Diag::Unknown && Context &&+347 !Context->isCompilerDiagnostic(D.ID)) {+348 auto Name = Context->getCheckName(D.ID);FeatureModule.h is unchanged; its lifecycle hooks are already in the parent.The tidy module neither takes nor replaces the existing ASTConsumer.Source: 101584320859 → c64f1fca38c6 • Exact snapshots; arrows map responsibilities, not every line
SVGPNG
  • FeatureModule.h is unchanged; its lifecycle hooks are already in the parent.
  • The tidy module neither takes nor replaces the existing ASTConsumer.
  • The historical figure anchor is retained, but consumer multiplexing has been removed from the implementation.

7. Diagnostic suppression and severity policy

The tidy-specific level adjuster moves to sawDiagnostic(), which edits the assembled diagnostic. Retains clangd suppression, NOLINT, system-macro filtering and warnings-as-errors. The nonempty-check guard is preserved: without active checks, tidy suppression and promotion remain disabled.

Diagnostic suppression and severity policyThe tidy-specific level adjuster moves to sawDiagnostic(), which edits the assembled diagnostic. Retains clangd suppression, NOLINT, system-macro filtering and warnings-as-errors. The nonempty-check guard is preserved: without active checks, tidy suppression and promotion remain disabled. Suppression, NOLINT, system-macro policy and warnings-as-errors retain their previous guard. With no active tidy checks, compiler-warning suppression and promotion remain unchanged.CLANGD / CLANG-TIDY MIGRATION 07 / 19Diagnostic suppression and severity policyThe tidy-specific level adjuster moves to sawDiagnostic(), which edits the assembled diagnostic. Retains clangd suppression, NOLINT,system-macro filtering and warnings-as-errors. The nonempty-check guard is preserved: without active checks, tidy suppression and promotionremain disabled.MOVE + POLICY PRESERVEDRemoved here (diff)Added here (diff)Unchanged contextBEFORE / 101584320859Open exact source on GitHubParsedAST.cpp−641 if (!CTChecks.empty()) {−642 std::string CheckName = CTContext->getCheckName(Info.getID());−643 bool IsClangTidyDiag = !CheckName.empty();−644 if (IsClangTidyDiag) {−645 if (Cfg.Diagnostics.Suppress.contains(CheckName))−646 return DiagnosticsEngine::Ignored;⋯−653 bool IsInsideMainFile =−654 Info.hasSourceManager() &&−655 isInsideMainFile(Info.getLocation(), Info.getSourceManager());−656 SmallVector<tooling::Diagnostic, 1> TidySuppressedErrors;−657 if (IsInsideMainFile && CTContext->shouldSuppressDiagnostic(−658 DiagLevel, Info, TidySuppressedErrors,−659 /*AllowIO=*/false,−660 /*EnableNolintBlocks=*/true)) {−661 // FIXME: should we expose the suppression error (invalid use of−662 // NOLINT comments)?⋯−667 Info.getSourceManager().isInSystemMacro(Info.getLocation()))−668 return DiagnosticsEngine::Ignored;−669−670 // Check for warning-as-error.−671 if (DiagLevel == DiagnosticsEngine::Warning &&−672 CTContext->treatAsError(CheckName)) {−673 return DiagnosticsEngine::Error;−674 }−675 }−676 }−677 return DiagLevel;678 });AFTER / c64f1fca38c6Open exact source on GitHubClangTidyFeatureModule.cpp+293 void sawDiagnostic(const clang::Diagnostic &Info, clangd::Diag &D) override {+294 // Preserve the previous suppression policy when no checks are active.+295 if (Checks.empty())+296 return;+297 std::string CheckName = Context->getCheckName(Info.getID());+298 if (CheckName.empty())+299 return;+300+301 if (Config::current().Diagnostics.Suppress.contains(CheckName)) {+302 D.Severity = DiagnosticsEngine::Ignored;+303 return;⋯+312 bool InsideMainFile =+313 Info.hasSourceManager() &&+314 isInsideMainFile(Info.getLocation(), Info.getSourceManager());+315 llvm::SmallVector<tooling::Diagnostic, 1> SuppressionErrors;+316 if (InsideMainFile &&+317 Context->shouldSuppressDiagnostic(D.Severity, Info, SuppressionErrors,+318 /*AllowIO=*/false,+319 /*EnableNolintBlocks=*/true)) {+320 // FIXME: should we expose the suppression error (invalid use of+321 // NOLINT comments)?+322 D.Severity = DiagnosticsEngine::Ignored;+323 return;+324 }+325 if (!Context->getOptions().SystemHeaders.value_or(false) &&+326 Info.hasSourceManager() &&+327 Info.getSourceManager().isInSystemMacro(Info.getLocation())) {+328 D.Severity = DiagnosticsEngine::Ignored;+329 return;+330 }+331 // Check for warning-as-error.+332 if (D.Severity == DiagnosticsEngine::Warning &&+333 Context->treatAsError(CheckName))+334 D.Severity = DiagnosticsEngine::Error;Suppression, NOLINT, system-macro policy and warnings-as-errors retain their previous guard.With no active tidy checks, compiler-warning suppression and promotion remain unchanged.Source: 101584320859 → c64f1fca38c6 • Exact snapshots; arrows map responsibilities, not every line
SVGPNG
  • Suppression, NOLINT, system-macro policy and warnings-as-errors retain their previous guard.
  • With no active tidy checks, compiler-warning suppression and promotion remain unchanged.
  • The nonempty-check guard preserves the pre-migration policy; changing it to a context-only guard would change compiler-warning behavior.

8. Diagnostic names and source move out of StoreDiags

The listener identifies tidy diagnostics; StoreDiags no longer receives a ClangTidyContext and preserves supplied metadata. Compiler diagnostics retain their Clang source and -W name. Flushing, compiler metadata fallback, generic finalizer dispatch and deduplication stay in StoreDiags.

Diagnostic names and source move out of StoreDiagsThe listener identifies tidy diagnostics; StoreDiags no longer receives a ClangTidyContext and preserves supplied metadata. Compiler diagnostics retain their Clang source and -W name. Flushing, compiler metadata fallback, generic finalizer dispatch and deduplication stay in StoreDiags. StoreDiags keeps compiler metadata fallback and generic finalizer dispatch. The generic callback contract for location-less diagnostics is unchanged.CLANGD / CLANG-TIDY MIGRATION 08 / 19Diagnostic names and source move out of StoreDiagsThe listener identifies tidy diagnostics; StoreDiags no longer receives a ClangTidyContext and preserves supplied metadata. Compilerdiagnostics retain their Clang source and -W name. Flushing, compiler metadata fallback, generic finalizer dispatch and deduplication stay inStoreDiags.MOVE + GENERIC FALLBACKRemoved here (diff)Added here (diff)Unchanged contextBEFORE / 101584320859Open exact source on GitHubDiagnostics.cpp−574std::vector<Diag> StoreDiags::take(const clang::tidy::ClangTidyContext *Tidy) {⋯−602 Diag.Source = Diag::Clang;−603 } else if (Tidy != nullptr) {−604 std::string TidyDiag = Tidy->getCheckName(Diag.ID);−605 if (!TidyDiag.empty()) {−606 Diag.Name = std::move(TidyDiag);−607 Diag.Source = Diag::ClangTidy;Open exact source on GitHubDiagnostics.h139public:−140 // The ClangTidyContext populates Source and Name for clang-tidy diagnostics.−141 std::vector<Diag> take(const clang::tidy::ClangTidyContext *Tidy = nullptr);Open exact source on GitHubParsedAST.cpp−806 std::vector<Diag> D = ASTDiags.take(&*CTContext);807 Diags.insert(Diags.end(), D.begin(), D.end());AFTER / c64f1fca38c6Open exact source on GitHubClangTidyFeatureModule.cpp+336 // Compiler warnings keep their clang source and -W name, even when their+337 // severity or suppression is controlled by clang-tidy options.+338 if (!Context->isCompilerDiagnostic(Info.getID())) {+339 D.Name = std::move(CheckName);+340 D.Source = Diag::ClangTidy;+341 }Open exact source on GitHubDiagnostics.cpp+567std::vector<Diag> StoreDiags::take() {568 // Do not forget to emit a pending diagnostic if there is one.569 flushLastDiag();570571 // Fill in name/source now that we have all the context needed to map them.572 for (auto &Diag : Output) {573 if (const char *ClangDiag = getDiagnosticCode(Diag.ID)) {+574 // Feature modules may already have supplied diagnostic metadata.+575 if (Diag.Source == Diag::Unknown)+576 Diag.Source = Diag::Clang;+577 if (Diag.Name.empty()) {+578 // Warnings controlled by -Wfoo are better recognized by that name.Open exact source on GitHubDiagnostics.h135class StoreDiags : public DiagnosticConsumer {136public:+137 std::vector<Diag> take();Open exact source on GitHubParsedAST.cpp+534 std::vector<Diag> D = ASTDiags.take();535 Diags.insert(Diags.end(), D.begin(), D.end());StoreDiags keeps compiler metadata fallback and generic finalizer dispatch.The generic callback contract for location-less diagnostics is unchanged.Source: 101584320859 → c64f1fca38c6 • Exact snapshots; arrows map responsibilities, not every line
SVGPNG
  • StoreDiags keeps compiler metadata fallback and generic finalizer dispatch.
  • The generic callback contract for location-less diagnostics is unchanged.
  • StoreDiags::take() no longer needs a tidy context. Flushing, compiler metadata fallback, generic finalizers and deduplication remain in core code.

9. Message cleanup and tidy-specific tags

Message cleanup and tidy tags move into finalizeDiagnostic(), after notes and fixes are assembled. Constructor errors get tidy metadata here because they precede check initialization. Cleanup still covers messages, notes and fixes; containment checks avoid duplicate tags.

Message cleanup and tidy-specific tagsMessage cleanup and tidy tags move into finalizeDiagnostic(), after notes and fixes are assembled. Constructor errors get tidy metadata here because they precede check initialization. Cleanup still covers messages, notes and fixes; containment checks avoid duplicate tags. Message cleanup covers the diagnostic, its notes and fixes. Fallback names handle constructor diagnostics; containment checks avoid duplicate tags.CLANGD / CLANG-TIDY MIGRATION 09 / 19Message cleanup and tidy-specific tagsMessage cleanup and tidy tags move into finalizeDiagnostic(), after notes and fixes are assembled. Constructor errors get tidy metadata herebecause they precede check initialization. Cleanup still covers messages, notes and fixes; containment checks avoid duplicate tags.MOVE + SMALL GUARDRemoved here (diff)Added here (diff)Unchanged contextBEFORE / 101584320859Open exact source on GitHubDiagnostics.cpp−392 if (D.Source == Diag::ClangTidy) {−393 if (llvm::StringRef(D.Name).starts_with("misc-unused-"))−394 D.Tags.push_back(DiagnosticTag::Unnecessary);−395 if (llvm::StringRef(D.Name).starts_with("modernize-"))−396 D.Tags.push_back(DiagnosticTag::Deprecated);−397 }398}399} // namespace⋯−610 auto CleanMessage = [&](std::string &Msg) {−611 StringRef Rest(Msg);−612 if (Rest.consume_back("]") && Rest.consume_back(Diag.Name) &&−613 Rest.consume_back(" ["))−614 Msg.resize(Rest.size());−615 };−616 CleanMessage(Diag.Message);−617 for (auto &Note : Diag.Notes)−618 CleanMessage(Note.Message);−619 for (auto &Fix : Diag.Fixes)−620 CleanMessage(Fix.Message);AFTER / c64f1fca38c6Open exact source on GitHubClangTidyFeatureModule.cpp+344 void finalizeDiagnostic(clangd::Diag &D) override {+345 // Constructor diagnostics precede check initialization.+346 if (D.Source == Diag::Unknown && Context &&+347 !Context->isCompilerDiagnostic(D.ID)) {+348 auto Name = Context->getCheckName(D.ID);+349 if (!Name.empty()) {+350 D.Name = std::move(Name);+351 D.Source = Diag::ClangTidy;+352 }+353 }+354 if (D.Source != Diag::ClangTidy)+355 return;+356 // clang-tidy bakes the name into diagnostic messages. Strip it out.+357 // It would be much nicer to make clang-tidy not do this.+358 auto CleanMessage = [&](std::string &Message) {+359 llvm::StringRef Rest(Message);+360 if (Rest.consume_back("]") && Rest.consume_back(D.Name) &&+361 Rest.consume_back(" ["))+362 Message.resize(Rest.size());+363 };+364 CleanMessage(D.Message);+365 for (auto &Note : D.Notes)+366 CleanMessage(Note.Message);+367 for (auto &Fix : D.Fixes)+368 CleanMessage(Fix.Message);+369 if (llvm::StringRef(D.Name).starts_with("misc-unused-") &&+370 !llvm::is_contained(D.Tags, DiagnosticTag::Unnecessary))+371 D.Tags.push_back(DiagnosticTag::Unnecessary);+372 if (llvm::StringRef(D.Name).starts_with("modernize-") &&+373 !llvm::is_contained(D.Tags, DiagnosticTag::Deprecated))+374 D.Tags.push_back(DiagnosticTag::Deprecated);Message cleanup covers the diagnostic, its notes and fixes.Fallback names handle constructor diagnostics; containment checks avoid duplicate tags.Source: 101584320859 → c64f1fca38c6 • Exact snapshots; arrows map responsibilities, not every line
SVGPNG
  • Message cleanup covers the diagnostic, its notes and fixes.
  • Fallback names handle constructor diagnostics; containment checks avoid duplicate tags.
  • Existing cleanup comments and FIXMEs move with the corresponding logic.

10. Application wiring replaces the provider plumbing

ClangdMain explicitly creates the tidy module only when EnableClangTidy is true. Server and ParseInputs retain their existing generic FeatureModules forwarding.

Application wiring replaces the provider plumbingClangdMain explicitly creates the tidy module only when EnableClangTidy is true. Server and ParseInputs retain their existing generic FeatureModules forwarding. Module-set construction and generic forwarding already existed. Check mode no longer forces the tidy module to load when clang-tidy is disabled.CLANGD / CLANG-TIDY MIGRATION 10 / 19Application wiring replaces the provider plumbingClangdMain explicitly creates the tidy module only when EnableClangTidy is true. Server and ParseInputs retain their existing genericFeatureModules forwarding.APPLICATION WIRINGRemoved here (diff)Added here (diff)Unchanged contextBEFORE / 101584320859Open exact source on GitHubtool/ClangdMain.cpp1024 Providers.push_back(provideClangdConfig());1025 Providers.push_back(provideDefaultChecks());1026 Providers.push_back(disableUnusableChecks());1027 ClangTidyOptProvider = combine(std::move(Providers));−1028 Opts.ClangTidyProvider = ClangTidyOptProvider;1029 }Open exact source on GitHubClangdServer.h−148 /// The Options provider to use when running clang-tidy. If null, clang-tidy−149 /// checks will be disabled.−150 TidyProviderRef ClangTidyProvider;⋯−510 // When set, provides clang-tidy options for a specific file.−511 TidyProviderRef ClangTidyProvider;Open exact source on GitHubClangdServer.cpp314 Inputs.ForceRebuild = ForceRebuild;315 Inputs.Opts = std::move(Opts);316 Inputs.Index = Index;−317 Inputs.ClangTidyProvider = ClangTidyProvider;318 Inputs.FeatureModules = FeatureModules;319 Inputs.ModulesManager = ModulesManager;Open exact source on GitHubCompiler.h61 const SymbolIndex *Index = nullptr;62 ParseOptions Opts = ParseOptions();−63 TidyProviderRef ClangTidyProvider = {};64 // Used to acquire ASTListeners when parsing files.65 FeatureModuleSet *FeatureModules = nullptr;66 // Used to build and manage (C++) modules.AFTER / c64f1fca38c6Open exact source on GitHubtool/ClangdMain.cpp1045 FeatureModuleSet ModuleSet = FeatureModuleSet::fromRegistry();+1046 if (EnableClangTidy)+1047 ModuleSet.add(std::make_unique<ClangTidyFeatureModule>(+1048 std::move(ClangTidyOptProvider)));1049 if (ModuleSet.begin() != ModuleSet.end())1050 Opts.FeatureModules = &ModuleSet;1051Open exact source on GitHubClangdServer.cpp313 Inputs.ForceRebuild = ForceRebuild;314 Inputs.Opts = std::move(Opts);315 Inputs.Index = Index;316 Inputs.FeatureModules = FeatureModules;317 Inputs.ModulesManager = ModulesManager;318 adjustParseInputs(Inputs, File);Open exact source on GitHubCompiler.h60 const SymbolIndex *Index = nullptr;61 ParseOptions Opts = ParseOptions();62 // Used to acquire ASTListeners when parsing files.63 FeatureModuleSet *FeatureModules = nullptr;64 // Used to build and manage (C++) modules.65 ModulesBuilder *ModulesManager = nullptr;Open exact source on GitHubtool/Check.cpp208 Inputs.CompileCommand = Cmd;209 Inputs.TFS = &TFS;+210 Inputs.FeatureModules = Opts.FeatureModules;211 Inputs.Opts.PreambleParseForwardingFunctions =212 Opts.PreambleParseForwardingFunctions;Module-set construction and generic forwarding already existed.Check mode no longer forces the tidy module to load when clang-tidy is disabled.Source: 101584320859 → c64f1fca38c6 • Exact snapshots; arrows map responsibilities, not every line
SVGPNG
  • Module-set construction and generic forwarding already existed.
  • Check mode no longer forces the tidy module to load when clang-tidy is disabled.
  • --check alone does not force the module to load. Combining --clang-tidy=0 with --check-tidy-time is no longer supported.

11. Module-owned provider snapshots

A module owns the options provider. Each listener captures a shared snapshot, allowing later provider replacement. An empty provider disables new listeners. Existing listeners keep their captured provider. This ownership and synchronization code is new, not relocated from ParsedAST.

Module-owned provider snapshotsA module owns the options provider. Each listener captures a shared snapshot, allowing later provider replacement. An empty provider disables new listeners. Existing listeners keep their captured provider. This ownership and synchronization code is new, not relocated from ParsedAST. A module owns the options provider. Each listener captures a shared snapshot, allowing later provider replacement. An empty provider disables new listeners. Existing listeners keep their captured provider. This ownership and synchronization code is new, not relocated from ParsedAST.CLANGD / CLANG-TIDY MIGRATION 11 / 19Module-owned provider snapshotsA module owns the options provider. Each listener captures a shared snapshot, allowing later provider replacement. An empty provider disablesnew listeners. Existing listeners keep their captured provider. This ownership and synchronization code is new, not relocated from ParsedAST.NEW OWNERSHIPRemoved here (diff)Added here (diff)Unchanged contextBEFORE / 101584320859Open exact source on GitHubClangdServer.h−148 /// The Options provider to use when running clang-tidy. If null, clang-tidy−149 /// checks will be disabled.−150 TidyProviderRef ClangTidyProvider;Open exact source on GitHubCompiler.h−63 TidyProviderRef ClangTidyProvider = {};64 // Used to acquire ASTListeners when parsing files.65 FeatureModuleSet *FeatureModules = nullptr;AFTER / c64f1fca38c6Open exact source on GitHubClangTidyFeatureModule.h+22class ClangTidyFeatureModule final : public FeatureModule {+23public:+24 /// Construct explicitly with the application's clang-tidy options provider.+25 /// Takes ownership of the provider; an empty provider disables checks.+26 explicit ClangTidyFeatureModule(TidyProvider OptionsProvider) {+27 setProvider(std::move(OptionsProvider));+28 }+29+30 /// Swap provider snapshots, e.g. to temporarily override the configuration+31 /// for benchmarking and restore it afterwards. Existing listeners retain+32 /// their snapshot.+33 void swapProvider(std::shared_ptr<const TidyProvider> &OptionsProvider);+34+35 std::unique_ptr<ASTListener> astListeners() override;+36+37private:+38 void setProvider(TidyProvider OptionsProvider);+39+40 std::mutex ProviderMu;+41 std::shared_ptr<const TidyProvider> Provider;Open exact source on GitHubClangTidyFeatureModule.cpp+389std::unique_ptr<FeatureModule::ASTListener>+390ClangTidyFeatureModule::astListeners() {+391 std::lock_guard<std::mutex> Lock(ProviderMu);+392 if (!Provider)+393 return nullptr;+394 return std::make_unique<TidyASTListener>(Provider);+395}+396+397void ClangTidyFeatureModule::setProvider(TidyProvider OptionsProvider) {+398 std::shared_ptr<const TidyProvider> Replacement;+399 if (OptionsProvider)+400 Replacement =+401 std::make_shared<const TidyProvider>(std::move(OptionsProvider));+402 swapProvider(Replacement);+403}+404+405void ClangTidyFeatureModule::swapProvider(+406 std::shared_ptr<const TidyProvider> &OptionsProvider) {+407 std::lock_guard<std::mutex> Lock(ProviderMu);+408 Provider.swap(OptionsProvider);+409}A module owns the options provider. Each listener captures a shared snapshot, allowing later provider replacement. An empty provider disables new listeners. Existinglisteners keep their captured provider. This ownership and synchronization code is new, not relocated from ParsedAST.Source: 101584320859 → c64f1fca38c6 • Exact snapshots; arrows map responsibilities, not every line
SVGPNG
  • A module owns the options provider. Each listener captures a shared snapshot, allowing later provider replacement. An empty provider disables new listeners. Existing listeners keep their captured provider. This ownership and synchronization code is new, not relocated from ParsedAST.
  • A private construction helper and public snapshot swap support temporary overrides; no default registry construction is introduced.

12. Check-mode timing swaps only tidy's provider

Timing temporarily swaps only tidy's provider, leaving other modules active. Clang-tidy must be enabled; a missing module produces an error.

Check-mode timing swaps only tidy's providerTiming temporarily swaps only tidy's provider, leaving other modules active. Clang-tidy must be enabled; a missing module produces an error. Provider allocation and both swaps are outside the measured Time() block. The local variable saves the original provider; scope exit restores it.CLANGD / CLANG-TIDY MIGRATION 12 / 19Check-mode timing swaps only tidy's providerTiming temporarily swaps only tidy's provider, leaving other modules active. Clang-tidy must be enabled; a missing module produces an error.TIMING ADAPTERRemoved here (diff)Added here (diff)Unchanged contextBEFORE / 101584320859Open exact source on GitHubtool/Check.cpp321 TidyProvider CTProvider = [&](tidy::ClangTidyOptions &Opts,322 llvm::StringRef) {323 Opts.Checks = Checks.str();324 };−325 Inputs.ClangTidyProvider = CTProvider;326 // Sigh, can't reuse the CompilerInvocation.327 IgnoringDiagConsumer IgnoreDiags;328 auto Invocation = buildCompilerInvocation(Inputs, IgnoreDiags);329 Duration Val = Time([&] {330 ParsedAST::build(File, Inputs, std::move(Invocation), {}, Preamble);331 });332 vlog(" Measured {0} ==> {1}", Checks, Val);333 return Val;⋯−371 // Restore old options.−372 Inputs.ClangTidyProvider = Opts.ClangTidyProvider;AFTER / c64f1fca38c6Open exact source on GitHubtool/Check.cpp+303 bool checkTidyTimes() {+304 // Keep all other modules active while varying only tidy's configuration.+305 auto *TidyModule =+306 Inputs.FeatureModules+307 ? Inputs.FeatureModules->get<ClangTidyFeatureModule>()+308 : nullptr;+309 if (!TidyModule) {+310 elog("Cannot time clang-tidy checks without a clang-tidy feature module");+311 return false;+312 }⋯333 TidyProvider CTProvider = [&](tidy::ClangTidyOptions &Opts,334 llvm::StringRef) {335 Opts.Checks = Checks.str();336 };+337 auto Provider =+338 std::make_shared<const TidyProvider>(std::move(CTProvider));+339 TidyModule->swapProvider(Provider);+340 // Provider now holds the original configuration; restore it on exit.+341 llvm::scope_exit RestoreProvider(+342 [&] { TidyModule->swapProvider(Provider); });343 // Sigh, can't reuse the CompilerInvocation.344 IgnoringDiagConsumer IgnoreDiags;345 auto Invocation = buildCompilerInvocation(Inputs, IgnoreDiags);346 Duration Val = Time([&] {347 ParsedAST::build(File, Inputs, std::move(Invocation), {}, Preamble);348 });349 vlog(" Measured {0} ==> {1}", Checks, Val);350 return Val;Provider allocation and both swaps are outside the measured Time() block.The local variable saves the original provider; scope exit restores it.Source: 101584320859 → c64f1fca38c6 • Exact snapshots; arrows map responsibilities, not every line
SVGPNG
  • Provider allocation and both swaps are outside the measured Time() block.
  • The local variable saves the original provider; scope exit restores it.
  • The missing-module guard reports an error instead of inserting a module into a caller-owned set. Ordinary timing still works with clang-tidy enabled.

13. TestTU keeps the short test configuration

Tests may still assign TU.ClangTidyProvider. build() wraps it in a local module; inputs() only forwards explicit modules. ClangTidyProvider and FeatureModules are mutually exclusive in TestTU::build(). Mixed-module tests and direct inputs() callers use an explicit module set. This is a deliberate test-helper boundary.

TestTU keeps the short test configurationTests may still assign TU.ClangTidyProvider. build() wraps it in a local module; inputs() only forwards explicit modules. ClangTidyProvider and FeatureModules are mutually exclusive in TestTU::build(). Mixed-module tests and direct inputs() callers use an explicit module set. This is a deliberate test-helper boundary. TU.ClangTidyProvider is a build()-only convenience adapter. Mixed modules and direct inputs() calls use an explicit set; TestTU.h is unchanged.CLANGD / CLANG-TIDY MIGRATION 13 / 19TestTU keeps the short test configurationTests may still assign TU.ClangTidyProvider. build() wraps it in a local module; inputs() only forwards explicit modules. ClangTidyProviderand FeatureModules are mutually exclusive in TestTU::build(). Mixed-module tests and direct inputs() callers use an explicit module set. Thisis a deliberate test-helper boundary.TEST HELPER ADAPTERRemoved here (diff)Added here (diff)Unchanged contextBEFORE / 101584320859Open exact source on GitHubunittests/TestTU.cpp73 FS.OverlayRealFileSystemForModules = true;74 Inputs.TFS = &FS;75 Inputs.Opts = ParseOptions();−76 if (ClangTidyProvider)−77 Inputs.ClangTidyProvider = ClangTidyProvider;78 Inputs.Index = ExternalIndex;79 return Inputs;⋯115ParsedAST TestTU::build() const {116 MockFS FS;117 auto Inputs = inputs(FS);118 Inputs.Opts = ParseOpts;119 StoreDiags Diags;120 auto CI = buildCompilerInvocation(Inputs, Diags);AFTER / c64f1fca38c6Open exact source on GitHubunittests/TestTU.cpp114ParsedAST TestTU::build() const {+115 if (ClangTidyProvider && FeatureModules) {+116 llvm::errs() << "Use either ClangTidyProvider or FeatureModules\n";+117 std::abort();+118 }119 MockFS FS;120 auto Inputs = inputs(FS);121 Inputs.Opts = ParseOpts;+122 FeatureModuleSet TidyModules;+123 if (ClangTidyProvider) {+124 TidyModules.add(std::make_unique<ClangTidyFeatureModule>(+125 [this](tidy::ClangTidyOptions &Opts, llvm::StringRef Filename) {+126 ClangTidyProvider(Opts, Filename);+127 }));+128 Inputs.FeatureModules = &TidyModules;+129 }130 StoreDiags Diags;131 auto CI = buildCompilerInvocation(Inputs, Diags);132 assert(CI && "Failed to build compilation invocation.");133 if (OverlayRealFileSystemForModules)TU.ClangTidyProvider is a build()-only convenience adapter.Mixed modules and direct inputs() calls use an explicit set; TestTU.h is unchanged.Source: 101584320859 → c64f1fca38c6 • Exact snapshots; arrows map responsibilities, not every line
SVGPNG
  • TU.ClangTidyProvider is a build()-only convenience adapter.
  • Mixed modules and direct inputs() calls use an explicit set; TestTU.h is unchanged.
  • The convenience fields ClangTidyProvider and FeatureModules remain mutually exclusive in TestTU::build(). Direct inputs() calls must supply modules explicitly.

14. Server and LSP tests register a feature module

Tests embedding the server replace the removed options field with an explicitly constructed tidy module. The existing test inputs and assertions are retained; only setup changes. The LSP fixture already supplies its FeatureModuleSet to the server.

Server and LSP tests register a feature moduleTests embedding the server replace the removed options field with an explicitly constructed tidy module. The existing test inputs and assertions are retained; only setup changes. The LSP fixture already supplies its FeatureModuleSet to the server. Tests embedding the server replace the removed options field with an explicitly constructed tidy module. The existing test inputs and assertions are retained; only setup changes. The LSP fixture already supplies its FeatureModuleSet to the server.CLANGD / CLANG-TIDY MIGRATION 14 / 19Server and LSP tests register a feature moduleTests embedding the server replace the removed options field with an explicitly constructed tidy module. The existing test inputs andassertions are retained; only setup changes. The LSP fixture already supplies its FeatureModuleSet to the server.TEST SETUP ADAPTERSRemoved here (diff)Added here (diff)Unchanged contextBEFORE / 101584320859Open exact source on GitHubunittests/ClangdTests.cpp1228 TidyProvider Provider = combine(std::move(Stack));1229 CDB.ExtraClangFlags = {"-xc++"};1230 auto Opts = ClangdServer::optsForTest();−1231 Opts.ClangTidyProvider = Provider;1232 ClangdServer Server(CDB, FS, Opts, &DiagConsumer);1233 const char *SourceContents = R"cpp(Open exact source on GitHubunittests/ClangdLSPServerTests.cpp212 llvm::StringRef) {213 ClangTidyOpts.Checks = {"-*,readability-identifier-naming"};214 ClangTidyOpts.CheckOptions["readability-identifier-naming.FunctionCase"] =215 "CamelCase";216 };−217 Opts.ClangTidyProvider = ClangTidyProvider;218 auto &Client = start();219 Client.didOpen("foo.hpp", Header.code());⋯272 llvm::StringRef) {273 ClangTidyOpts.Checks = {"-*,boost-use-ranges"};274 };−275 Opts.ClangTidyProvider = ClangTidyProvider;276 // Check that registering the boost-use-ranges checker's matchers277 // on two different threads does not cause a crash.278 auto &Client = start();AFTER / c64f1fca38c6Open exact source on GitHubunittests/ClangdTests.cpp1229 TidyProvider Provider = combine(std::move(Stack));1230 CDB.ExtraClangFlags = {"-xc++"};1231 auto Opts = ClangdServer::optsForTest();+1232 FeatureModuleSet Modules;+1233 Modules.add(std::make_unique<ClangTidyFeatureModule>(std::move(Provider)));+1234 Opts.FeatureModules = &Modules;1235 ClangdServer Server(CDB, FS, Opts, &DiagConsumer);1236 const char *SourceContents = R"cpp(Open exact source on GitHubunittests/ClangdLSPServerTests.cpp212 constexpr auto ClangTidyProvider = [](tidy::ClangTidyOptions &ClangTidyOpts,213 llvm::StringRef) {214 ClangTidyOpts.Checks = {"-*,readability-identifier-naming"};215 ClangTidyOpts.CheckOptions["readability-identifier-naming.FunctionCase"] =216 "CamelCase";217 };+218 FeatureModules.add(+219 std::make_unique<ClangTidyFeatureModule>(ClangTidyProvider));220 auto &Client = start();221 Client.didOpen("foo.hpp", Header.code());⋯273 constexpr auto ClangTidyProvider = [](tidy::ClangTidyOptions &ClangTidyOpts,274 llvm::StringRef) {275 ClangTidyOpts.Checks = {"-*,boost-use-ranges"};276 };+277 FeatureModules.add(+278 std::make_unique<ClangTidyFeatureModule>(ClangTidyProvider));279 // Check that registering the boost-use-ranges checker's matchers280 // on two different threads does not cause a crash.281 auto &Client = start();Tests embedding the server replace the removed options field with an explicitly constructed tidy module. The existing test inputs and assertions are retained; onlysetup changes. The LSP fixture already supplies its FeatureModuleSet to the server.Source: 101584320859 → c64f1fca38c6 • Exact snapshots; arrows map responsibilities, not every line
SVGPNG
  • Tests embedding the server replace the removed options field with an explicitly constructed tidy module. The existing test inputs and assertions are retained; only setup changes. The LSP fixture already supplies its FeatureModuleSet to the server.
  • Existing test code and assertions are retained; setup changes to the new configuration mechanism.

15. IncludeFixer and orchestration stay in the core

IncludeFixer stays in ParsedAST; colors show reindentation and rewrites within the same core file. Clang's typo-correction adjustment, include fixes, token collection and lifecycle dispatch remain in the core. Preamble.cpp and FeatureModule.h are unchanged; the existing lifecycle hooks are reused.

IncludeFixer and orchestration stay in the coreIncludeFixer stays in ParsedAST; colors show reindentation and rewrites within the same core file. Clang's typo-correction adjustment, include fixes, token collection and lifecycle dispatch remain in the core. Preamble.cpp and FeatureModule.h are unchanged; the existing lifecycle hooks are reused. IncludeFixer and typo-correction severity handling remain in the core. Frontend execution, token collection, traversal restriction and lifecycle dispatch remain in place.CLANGD / CLANG-TIDY MIGRATION 15 / 19IncludeFixer and orchestration stay in the coreIncludeFixer stays in ParsedAST; colors show reindentation and rewrites within the same core file. Clang's typo-correction adjustment, includefixes, token collection and lifecycle dispatch remain in the core. Preamble.cpp and FeatureModule.h are unchanged; the existing lifecyclehooks are reused.STAYS IN CORERemoved here (diff)Added here (diff)Unchanged contextBEFORE / 101584320859Open exact source on GitHubParsedAST.cpp−625 for (auto ID : {diag::ext_implicit_function_decl_c99,−626 diag::ext_implicit_lib_function_decl,−627 diag::ext_implicit_lib_function_decl_c99,−628 diag::warn_implicit_function_decl}) {−629 OverriddenSeverity.try_emplace(−630 ID, Clang->getDiagnostics().getDiagnosticLevel(ID, SourceLocation()));−631 Clang->getDiagnostics().setSeverity(ID, diag::Severity::Error,−632 SourceLocation());633 }−634−635 ASTDiags.setLevelAdjuster([&](DiagnosticsEngine::Level DiagLevel,−636 const clang::Diagnostic &Info) {−637 auto It = OverriddenSeverity.find(Info.getID());−638 if (It != OverriddenSeverity.end())−639 DiagLevel = It->second;⋯−702 FixIncludes.emplace(Filename, Inserter, *Inputs.Index,−703 /*IndexRequestLimit=*/5, Directive);−704 ASTDiags.contributeFixes([&FixIncludes](DiagnosticsEngine::Level DiagLevl,−705 const clang::Diagnostic &Info) {−706 return FixIncludes->fix(DiagLevl, Info);−707 });−708 Clang->setExternalSemaSource(FixIncludes->unresolvedNameRecorder());⋯776 std::vector<Decl *> ParsedDecls = Action->takeTopLevelDecls();777 // AST traversals should exclude the preamble, to avoid performance cliffs.778 Clang->getASTContext().setTraversalScope(ParsedDecls);−779 if (!CTChecks.empty()) {−780 // Run the AST-dependent part of the clang-tidy checks.−781 // (The preprocessor part ran already, via PPCallbacks).−782 trace::Span Tracer("ClangTidyMatch");−783 CTFinder.matchAST(Clang->getASTContext());−784 }785 for (const auto &L : ASTListeners)786 L->afterExecute(*Clang);787AFTER / c64f1fca38c6Open exact source on GitHubParsedAST.cpp+395 // Clang only corrects typos for use of undeclared functions in C if that+396 // use is an error. Include fixer relies on typo correction, so pretend+397 // this is an error. (The actual typo correction is nice too).+398 // We restore the original severity in the level adjuster.+399 // FIXME: It would be better to have a real API for this, but what?+400 for (auto ID : {diag::ext_implicit_function_decl_c99,+401 diag::ext_implicit_lib_function_decl,+402 diag::ext_implicit_lib_function_decl_c99,+403 diag::warn_implicit_function_decl}) {+404 OverriddenSeverity.try_emplace(+405 ID, Clang->getDiagnostics().getDiagnosticLevel(ID, SourceLocation()));+406 Clang->getDiagnostics().setSeverity(ID, diag::Severity::Error,+407 SourceLocation());+408 }+409 ASTDiags.setLevelAdjuster(+410 [&](DiagnosticsEngine::Level Level, const clang::Diagnostic &Info) {+411 auto It = OverriddenSeverity.find(Info.getID());+412 return It == OverriddenSeverity.end() ? Level : It->second;+413 });⋯+430 // FIXME: Consider piping through ASTSignals to fetch this to handle the+431 // case where a header file contains ObjC decls but no #imports.+432 Symbol::IncludeDirective Directive =+433 Inputs.Opts.ImportInsertions+434 ? preferredIncludeDirective(Filename, Clang->getLangOpts(),+435 MainFileIncludes, {})+436 : Symbol::Include;+437 FixIncludes.emplace(Filename, Inserter, *Inputs.Index,+438 /*IndexRequestLimit=*/5, Directive);+439 ASTDiags.contributeFixes([&FixIncludes](DiagnosticsEngine::Level Level,+440 const clang::Diagnostic &Info) {+441 return FixIncludes->fix(Level, Info);442 });+443 Clang->setExternalSemaSource(FixIncludes->unresolvedNameRecorder());444 }⋯507 syntax::TokenBuffer Tokens = std::move(CollectTokens).consume();508 // Makes SelectionTree build much faster.509 Tokens.indexExpandedTokens();510 std::vector<Decl *> ParsedDecls = Action->takeTopLevelDecls();511 // AST traversals should exclude the preamble, to avoid performance cliffs.512 Clang->getASTContext().setTraversalScope(ParsedDecls);513 for (const auto &L : ASTListeners)514 L->afterExecute(*Clang);IncludeFixer and typo-correction severity handling remain in the core.Frontend execution, token collection, traversal restriction and lifecycle dispatch remain in place.Source: 101584320859 → c64f1fca38c6 • Exact snapshots; arrows map responsibilities, not every line
SVGPNG
  • IncludeFixer and typo-correction severity handling remain in the core.
  • Frontend execution, token collection, traversal restriction and lifecycle dispatch remain in place.
  • FeatureModule.h, Preamble.cpp and TestTU.h are unchanged by this commit. Colored IncludeFixer lines represent same-file reindentation/rewiring, not extraction.

16. Existing regressions are reused, not re-added

The parent already contains the zero-check, warning-option, relative-input and consumer-initialization tests. The migration adapts their setup.

Existing regressions are reused, not re-addedThe parent already contains the zero-check, warning-option, relative-input and consumer-initialization tests. The migration adapts their setup. Gray code is baseline coverage, not tests added by this migration. Direct inputs() tests construct an explicit module; build() retains its short provider adapter.CLANGD / CLANG-TIDY MIGRATION 16 / 19Existing regressions are reused, not re-addedThe parent already contains the zero-check, warning-option, relative-input and consumer-initialization tests. The migration adapts theirsetup.TEST SETUP ADAPTERSRemoved here (diff)Added here (diff)Unchanged contextBEFORE / 101584320859Open exact source on GitHubunittests/DiagnosticsTests.cpp831TEST(DiagnosticTest, ClangTidyCompilerWarningsWithoutChecks) {832 auto TU = TestTU::withCode("static void foo() {}");833 TU.ClangTidyProvider = [](tidy::ClangTidyOptions &Opts, llvm::StringRef) {834 Opts.Checks = "-*,clang-diagnostic-*";835 Opts.ExtraArgs = {"-Wunused-function"};836 Opts.WarningsAsErrors = "clang-diagnostic-unused-function";837 };838 auto Warning = AllOf(diagName("-Wunused-function"), diagSource(Diag::Clang),839 diagSeverity(DiagnosticsEngine::Warning));840 EXPECT_THAT(TU.build().getDiagnostics(), ElementsAre(Warning));841842 // Without tidy checks, NOLINT and WarningsAsErrors don't affect warnings.843 TU.Code = "static void foo() {} // NOLINT(clang-diagnostic-unused-function)";844 EXPECT_THAT(TU.build().getDiagnostics(), ElementsAre(Warning));845}Open exact source on GitHubunittests/DiagnosticsTests.cpp847TEST(DiagnosticTest, ClangTidyWarningOptionsBeforeBeginSourceFile) {848 auto Build = [](bool Suppress, bool ExtraArgsBefore) {849 auto TU = TestTU::withCode("int value;");−850 TU.ClangTidyProvider = [=](tidy::ClangTidyOptions &Opts, llvm::StringRef) {−851 Opts.Checks = "-*";−852 if (Suppress) {−853 auto &Args = ExtraArgsBefore ? Opts.ExtraArgsBefore : Opts.ExtraArgs;−854 Args = {"-Wno-incomplete-umbrella"};−855 }−856 };857 TU.ExtraArgs = {"-fmodules",858 "-fmodule-map-file=" + testPath("module.modulemap")};859 TU.AdditionalFiles["module.modulemap"] =AFTER / c64f1fca38c6Open exact source on GitHubunittests/DiagnosticsTests.cpp832TEST(DiagnosticTest, ClangTidyCompilerWarningsWithoutChecks) {833 auto TU = TestTU::withCode("static void foo() {}");834 TU.ClangTidyProvider = [](tidy::ClangTidyOptions &Opts, llvm::StringRef) {835 Opts.Checks = "-*,clang-diagnostic-*";836 Opts.ExtraArgs = {"-Wunused-function"};837 Opts.WarningsAsErrors = "clang-diagnostic-unused-function";838 };839 auto Warning = AllOf(diagName("-Wunused-function"), diagSource(Diag::Clang),840 diagSeverity(DiagnosticsEngine::Warning));841 EXPECT_THAT(TU.build().getDiagnostics(), ElementsAre(Warning));842843 // Without tidy checks, NOLINT and WarningsAsErrors don't affect warnings.844 TU.Code = "static void foo() {} // NOLINT(clang-diagnostic-unused-function)";845 EXPECT_THAT(TU.build().getDiagnostics(), ElementsAre(Warning));846}Open exact source on GitHubunittests/DiagnosticsTests.cpp848TEST(DiagnosticTest, ClangTidyWarningOptionsBeforeBeginSourceFile) {849 auto Build = [](bool Suppress, bool ExtraArgsBefore) {850 auto TU = TestTU::withCode("int value;");+851 FeatureModuleSet Modules;+852 Modules.add(std::make_unique<ClangTidyFeatureModule>(+853 [=](tidy::ClangTidyOptions &Opts, llvm::StringRef) {+854 Opts.Checks = "-*";+855 if (Suppress) {+856 auto &Args =+857 ExtraArgsBefore ? Opts.ExtraArgsBefore : Opts.ExtraArgs;+858 Args = {"-Wno-incomplete-umbrella"};+859 }+860 }));+861 TU.FeatureModules = &Modules;862 TU.ExtraArgs = {"-fmodules",863 "-fmodule-map-file=" + testPath("module.modulemap")};864 TU.AdditionalFiles["module.modulemap"] =Gray code is baseline coverage, not tests added by this migration.Direct inputs() tests construct an explicit module; build() retains its short provider adapter.Source: 101584320859 → c64f1fca38c6 • Exact snapshots; arrows map responsibilities, not every line
SVGPNG
  • Gray code is baseline coverage, not tests added by this migration.
  • Direct inputs() tests construct an explicit module; build() retains its short provider adapter.
  • All four baseline regressions were added before the migration. They are not new functionality or newly introduced tests in this patch.

17. Filename identity and temporary provider overrides

Relative-input coverage already exists in the parent; its provider is wired through a module. The provider-swap regression is new.

Filename identity and temporary provider overridesRelative-input coverage already exists in the parent; its provider is wired through a module. The provider-swap regression is new. Lookup uses the requested document identity even when the frontend input name differs. The new swap test disables checks, restores the provider, and checks the original diagnostic.CLANGD / CLANG-TIDY MIGRATION 17 / 19Filename identity and temporary provider overridesRelative-input coverage already exists in the parent; its provider is wired through a module. The provider-swap regression is new.ADAPT + ONE NEW TESTRemoved here (diff)Added here (diff)Unchanged contextBEFORE / 101584320859Open exact source on GitHubunittests/DiagnosticsTests.cpp879TEST(DiagnosticTest, ClangTidyRelativeInput) {880 if (!CLANGD_TIDY_CHECKS)881 GTEST_SKIP() << "Requires clang-tidy checks";882 MockFS FS;883 auto TU = TestTU::withCode("int *p = 0;");884 TU.Filename = "project/src/test.cpp";885 TU.AdditionalFiles["project/src/.clang-tidy"] =886 "Checks: '-*,modernize-use-nullptr'";887 auto Provider = provideClangTidyFiles(FS);−888 TU.ClangTidyProvider = [&](tidy::ClangTidyOptions &Opts,−889 llvm::StringRef Filename) {−890 EXPECT_EQ(Filename, testPath(TU.Filename));−891 Provider(Opts, Filename);−892 };893 auto Inputs = TU.inputs(FS);894 Inputs.CompileCommand.Directory = testPath("project");895 // Configuration lookup must use the requested file, not the frontend input.896 for (const auto *InputFile : {"src/./test.cpp", "../other/test.cpp"}) {897 SCOPED_TRACE(InputFile);898 IgnoringDiagConsumer Diags;899 auto CI = buildCompilerInvocation(Inputs, Diags);900 ASSERT_TRUE(CI);901 auto Kind = CI->getFrontendOpts().Inputs.front().getKind();902 CI->getFrontendOpts().Inputs = {FrontendInputFile(InputFile, Kind)};903 auto AST = ParsedAST::build(testPath(TU.Filename), Inputs, std::move(CI),904 {}, nullptr);905 ASSERT_TRUE(AST);906 ASSERT_THAT(AST->getDiagnostics(), SizeIs(1));907 EXPECT_EQ(AST->getDiagnostics().front().Name, "modernize-use-nullptr");908 }909}AFTER / c64f1fca38c6Open exact source on GitHubunittests/DiagnosticsTests.cpp884TEST(DiagnosticTest, ClangTidyRelativeInput) {885 if (!CLANGD_TIDY_CHECKS)886 GTEST_SKIP() << "Requires clang-tidy checks";887 MockFS FS;888 auto TU = TestTU::withCode("int *p = 0;");889 TU.Filename = "project/src/test.cpp";890 TU.AdditionalFiles["project/src/.clang-tidy"] =891 "Checks: '-*,modernize-use-nullptr'";892 auto Provider = provideClangTidyFiles(FS);+893 FeatureModuleSet Modules;+894 Modules.add(std::make_unique<ClangTidyFeatureModule>(+895 [&](tidy::ClangTidyOptions &Opts, llvm::StringRef Filename) {+896 EXPECT_EQ(Filename, testPath(TU.Filename));+897 Provider(Opts, Filename);+898 }));+899 TU.FeatureModules = &Modules;900 auto Inputs = TU.inputs(FS);901 Inputs.CompileCommand.Directory = testPath("project");902 // Configuration lookup must use the requested file, not the frontend input.903 for (const auto *InputFile : {"src/./test.cpp", "../other/test.cpp"}) {904 SCOPED_TRACE(InputFile);905 IgnoringDiagConsumer Diags;906 auto CI = buildCompilerInvocation(Inputs, Diags);907 ASSERT_TRUE(CI);908 auto Kind = CI->getFrontendOpts().Inputs.front().getKind();909 CI->getFrontendOpts().Inputs = {FrontendInputFile(InputFile, Kind)};910 auto AST = ParsedAST::build(testPath(TU.Filename), Inputs, std::move(CI),911 {}, nullptr);912 ASSERT_TRUE(AST);913 ASSERT_THAT(AST->getDiagnostics(), SizeIs(1));914 EXPECT_EQ(AST->getDiagnostics().front().Name, "modernize-use-nullptr");915 }916}Open exact source on GitHubunittests/FeatureModulesTests.cpp+310TEST(FeatureModulesTest, ClangTidyTemporaryProvider) {+311 if (!CLANGD_TIDY_CHECKS)+312 GTEST_SKIP() << "Requires clang-tidy checks";+313 FeatureModuleSet Modules;+314 Modules.add(std::make_unique<ClangTidyFeatureModule>(+315 addTidyChecks("modernize-use-nullptr")));+316 auto TU = TestTU::withCode("int *p = 0;");+317 TU.FeatureModules = &Modules;+318 auto *Tidy = Modules.get<ClangTidyFeatureModule>();+319 auto Provider = std::make_shared<const TidyProvider>(addTidyChecks("-*"));+320 // Check-mode timing temporarily overrides tidy's configuration, then+321 // restores the original provider for subsequent builds.+322 Tidy->swapProvider(Provider);+323 EXPECT_THAT(TU.build().getDiagnostics(), testing::IsEmpty());+324 Tidy->swapProvider(Provider);+325 auto AST = TU.build();+326 ASSERT_THAT(AST.getDiagnostics(), testing::SizeIs(1));+327 EXPECT_EQ(AST.getDiagnostics().front().Name, "modernize-use-nullptr");+328}Lookup uses the requested document identity even when the frontend input name differs.The new swap test disables checks, restores the provider, and checks the original diagnostic.Source: 101584320859 → c64f1fca38c6 • Exact snapshots; arrows map responsibilities, not every line
SVGPNG
  • Lookup uses the requested document identity even when the frontend input name differs.
  • The new swap test disables checks, restores the provider, and checks the original diagnostic.
  • The differing frontend input is an artificial low-level test. Normal CommandMangler handling already aligns the input with the requested document.

18. CMake and GN compile the new module

Both build descriptions add the same ClangTidyFeatureModule.cpp source. No lit test is introduced by this commit.

CMake and GN compile the new moduleBoth build descriptions add the same ClangTidyFeatureModule.cpp source. No lit test is introduced by this commit. The removed check-tidy-time lit test is not part of the migration. Timing with ordinary clang-tidy disabled is no longer preserved.CLANGD / CLANG-TIDY MIGRATION 18 / 19CMake and GN compile the new moduleBoth build descriptions add the same ClangTidyFeatureModule.cpp source. No lit test is introduced by this commit.BUILD WIRINGRemoved here (diff)Added here (diff)Unchanged contextBEFORE / 101584320859Open exact source on GitHubCMakeLists.txt66 ASTSignals.cpp67 ClangdLSPServer.cpp68 ClangdServer.cpp69 CodeComplete.cpp70 CodeCompletionStrings.cpp71 CollectMacros.cppOpen exact source on GitHubllvm/utils/gn/secondary/clang-tools-extra/clangd/BUILD.gn82 "ASTSignals.cpp",83 "ClangdLSPServer.cpp",84 "ClangdServer.cpp",85 "CodeComplete.cpp",86 "CodeCompletionStrings.cpp",87 "CollectMacros.cpp",AFTER / c64f1fca38c6Open exact source on GitHubCMakeLists.txt66 ASTSignals.cpp67 ClangdLSPServer.cpp68 ClangdServer.cpp+69 ClangTidyFeatureModule.cpp70 CodeComplete.cpp71 CodeCompletionStrings.cpp72 CollectMacros.cppOpen exact source on GitHubllvm/utils/gn/secondary/clang-tools-extra/clangd/BUILD.gn82 "ASTSignals.cpp",83 "ClangdLSPServer.cpp",84 "ClangdServer.cpp",+85 "ClangTidyFeatureModule.cpp",86 "CodeComplete.cpp",87 "CodeCompletionStrings.cpp",88 "CollectMacros.cpp",The removed check-tidy-time lit test is not part of the migration.Timing with ordinary clang-tidy disabled is no longer preserved.Source: 101584320859 → c64f1fca38c6 • Exact snapshots; arrows map responsibilities, not every line
SVGPNG
  • The removed check-tidy-time lit test is not part of the migration.
  • Timing with ordinary clang-tidy disabled is no longer preserved.
  • There is no check-tidy-time.test in either selected snapshot.

19. Code moves together with its original comments

Original explanatory comments and existing FIXMEs move with their code. Comment wording is preserved, allowing indentation and line wrapping changes. New adapter comments are separate.

Code moves together with its original commentsOriginal explanatory comments and existing FIXMEs move with their code. Comment wording is preserved, allowing indentation and line wrapping changes. New adapter comments are separate. Original comments and FIXMEs move with the code; wording is preserved. Only indentation and line wrapping may change. New glue comments are separate.CLANGD / CLANG-TIDY MIGRATION 19 / 19Code moves together with its original commentsOriginal explanatory comments and existing FIXMEs move with their code. Comment wording is preserved, allowing indentation and line wrappingchanges. New adapter comments are separate.CODE + ORIGINAL COMMENTSRemoved here (diff)Added here (diff)Unchanged contextBEFORE / 101584320859Open exact source on GitHubParsedAST.cpp−287// Filter for clang diagnostics groups enabled by CTOptions.Checks.−288//−289// These are check names like clang-diagnostics-unused.−290// Note that unlike -Wunused, clang-diagnostics-unused does not imply−291// subcategories like clang-diagnostics-unused-function.−292//−293// This is used to determine which diagnostics can be enabled by ExtraArgs in−294// the clang-tidy configuration.−295class TidyDiagnosticGroups {Open exact source on GitHubParsedAST.cpp−519 // If clang-tidy is configured to emit clang warnings, we should too.−520 //−521 // Such clang-tidy configuration consists of two parts:−522 // - ExtraArgs: ["-Wfoo"] causes clang to produce the warnings−523 // - Checks: "clang-diagnostic-foo" prevents clang-tidy filtering them out−524 //−525 // In clang-tidy, diagnostics are emitted if they pass both checks.−526 // When groups contain subgroups, -Wparent includes the child, but−527 // clang-diagnostic-parent does not.−528 //−529 // We *don't* want to change the compile command directly. This can have−530 // too many unexpected effects: breaking the command, interactions with−531 // -- and -Werror, etc. Besides, we've already parsed the command.−532 // Instead we parse the -W<group> flags and handle them directly.−533 //−534 // Similarly, we don't want to use Checks to filter clang diagnostics after−535 // they are generated, as this spreads clang-tidy emulation everywhere.−536 // Instead, we just use these to filter which extra diagnostics we enable.−537 auto &Diags = Clang->getDiagnostics();−538 TidyDiagnosticGroups TidyGroups(ClangTidyOpts.Checks ? *ClangTidyOpts.Checks−539 : llvm::StringRef());Open exact source on GitHubParsedAST.cpp−566 // Set up ClangTidy. Must happen after BeginSourceFile() so ASTContext exists.−567 // Clang-tidy has some limitations to ensure reasonable performance:−568 // - checks don't see all preprocessor events in the preamble−569 // - matchers run only over the main-file top-level decls (and can't see−570 // ancestors outside this scope).−571 // In practice almost all checks work well without modifications.−572 std::vector<std::unique_ptr<tidy::ClangTidyCheck>> CTChecks;Open exact source on GitHubParsedAST.cpp−383 if (NeedsWerrorExclusion) {−384 // FIXME: there's no API to suppress -Werror for single diagnostics.−385 // In some cases with sub-groups, we may end up erroneously−386 // downgrading diagnostics that were -Werror in the compile command.−387 Diags.setDiagnosticGroupWarningAsError(Group, false);Open exact source on GitHubDiagnostics.cpp−608 // clang-tidy bakes the name into diagnostic messages. Strip it out.−609 // It would be much nicer to make clang-tidy not do this.−610 auto CleanMessage = [&](std::string &Msg) {AFTER / c64f1fca38c6Open exact source on GitHubClangTidyFeatureModule.cpp+56// Filter for clang diagnostics groups enabled by CTOptions.Checks.+57//+58// These are check names like clang-diagnostics-unused.+59// Note that unlike -Wunused, clang-diagnostics-unused does not imply+60// subcategories like clang-diagnostics-unused-function.+61//+62// This is used to determine which diagnostics can be enabled by ExtraArgs in+63// the clang-tidy configuration.+64class TidyDiagnosticGroups {Open exact source on GitHubClangTidyFeatureModule.cpp+203 // If clang-tidy is configured to emit clang warnings, we should too.+204 //+205 // Such clang-tidy configuration consists of two parts:+206 // - ExtraArgs: ["-Wfoo"] causes clang to produce the warnings+207 // - Checks: "clang-diagnostic-foo" prevents clang-tidy filtering them out+208 //+209 // In clang-tidy, diagnostics are emitted if they pass both checks.+210 // When groups contain subgroups, -Wparent includes the child, but+211 // clang-diagnostic-parent does not.+212 //+213 // We *don't* want to change the compile command directly. This can have+214 // too many unexpected effects: breaking the command, interactions with+215 // -- and -Werror, etc. Besides, we've already parsed the command.+216 // Instead we parse the -W<group> flags and handle them directly.+217 //+218 // Similarly, we don't want to use Checks to filter clang diagnostics after+219 // they are generated, as this spreads clang-tidy emulation everywhere.+220 // Instead, we just use these to filter which extra diagnostics we enable.+221 TidyDiagnosticGroups EnabledGroups(Options->Checks ? *Options->Checks+222 : llvm::StringRef());Open exact source on GitHubClangTidyFeatureModule.cpp+237 // Set up ClangTidy. Must happen after BeginSourceFile() so ASTContext+238 // exists. Clang-tidy has some limitations to ensure reasonable performance:+239 // - checks don't see all preprocessor events in the preamble+240 // - matchers run only over the main-file top-level decls (and can't see+241 // ancestors outside this scope).+242 // In practice almost all checks work well without modifications.+243 trace::Span Tracer("ClangTidyInit");+244 static const auto *AllFactories = [] {+245 auto *Factories = new tidy::ClangTidyCheckFactories;Open exact source on GitHubClangTidyFeatureModule.cpp+145 if (NeedsWerrorExclusion) {+146 // FIXME: there's no API to suppress -Werror for single diagnostics.+147 // In some cases with sub-groups, we may end up erroneously+148 // downgrading diagnostics that were -Werror in the compile command.+149 Diags.setDiagnosticGroupWarningAsError(Group, false);Open exact source on GitHubClangTidyFeatureModule.cpp+356 // clang-tidy bakes the name into diagnostic messages. Strip it out.+357 // It would be much nicer to make clang-tidy not do this.+358 auto CleanMessage = [&](std::string &Message) {Original comments and FIXMEs move with the code; wording is preserved.Only indentation and line wrapping may change. New glue comments are separate.Source: 101584320859 → c64f1fca38c6 • Exact snapshots; arrows map responsibilities, not every line
SVGPNG
  • Original comments and FIXMEs move with the code; wording is preserved.
  • Only indentation and line wrapping may change. New glue comments are separate.
  • Five paired examples show helper documentation, warning-option rationale, existing limitations, a FIXME and diagnostic cleanup. Their wording matches after ignoring whitespace and line wrapping.

Lifecycle map

Existing hookTidy module responsibility
beforeBeginSourceFile()Read configuration and apply warning options before frontend initialization
beforePPCallbacks()Create the context/checks and register PP callbacks and matchers
afterExecute()Call Finder.matchAST() directly after tokens and traversal scope are finalized
sawDiagnostic()Apply tidy suppression/severity policy and identify tidy diagnostics
finalizeDiagnostic()Supply fallback metadata, clean messages and attach tidy tags

The module no longer overrides beforeExecute(). FeatureModule.h and Preamble.cpp are unchanged. Preamble builds do not run tidy checks, as before.

Core code retained by the migration

Retained blockBeforeAfter
Frontend startup and beforeBeginSourceFile dispatchL546L371
beforeExecute dispatchL760L494
Token finalization and traversal restrictionL770L504
afterExecute dispatchL786L514

Coverage of all 17 changed files

The figures group related responsibilities; the complete patch below contains every changed line.

File (under clang-tools-extra/clangd/)ChangeFigure(s)
CMakeLists.txtCompile the module source18
ClangTidyFeatureModule.cppMove tidy options, checks, direct matching, diagnostic policy and original comments; add provider ownership1–12
ClangTidyFeatureModule.hExplicit module API and provider snapshots11
ClangdServer.cppRemove tidy-provider forwarding; retain generic module forwarding10
ClangdServer.hRemove dedicated tidy-provider fields10–11
Compiler.hRemove the tidy-specific ParseInputs field/include10–11
Diagnostics.cppMove tidy metadata, cleanup and tags; retain generic diagnostics handling8–9
Diagnostics.hRemove the ClangTidyContext parameter from take()8
ParsedAST.cppExtract tidy logic; retain frontend orchestration and IncludeFixer1–7, 15
tool/Check.cppForward the module set and temporarily swap the tidy provider for timing10, 12
tool/ClangdMain.cppConstruct the tidy module only when clang-tidy is enabled10
unittests/ClangdLSPServerTests.cppAdapt two LSP tests to explicitly add the tidy module14
unittests/ClangdTests.cppAdapt server-test setup to explicitly add the tidy module14
unittests/DiagnosticsTests.cppAdapt existing warning-option and filename tests; zero-check coverage already exists16–17
unittests/FeatureModulesTests.cppAdd provider-swap coverage and adapt existing consumer-test setup17
unittests/TestTU.cppRetain the build-only provider convenience adapter13
llvm/utils/gn/secondary/clang-tools-extra/clangd/BUILD.gnCompile the module source in GN18

The generic diagnostic callback contract is unchanged. Location-less errors from custom tidy checks still bypass sawDiagnostic, so tidy-specific suppression of those errors is not preserved. Built-in location-less configuration warnings were already filtered out.

Complete patch · 101584320859 → c64f1fca38c6
diff --git a/clang-tools-extra/clangd/CMakeLists.txt b/clang-tools-extra/clangd/CMakeLists.txt
index 3aa1b7bddb99..1f839a571c46 100644
--- a/clang-tools-extra/clangd/CMakeLists.txt
+++ b/clang-tools-extra/clangd/CMakeLists.txt
@@ -66,6 +66,7 @@ add_clang_library(clangDaemon STATIC
   ASTSignals.cpp
   ClangdLSPServer.cpp
   ClangdServer.cpp
+  ClangTidyFeatureModule.cpp
   CodeComplete.cpp
   CodeCompletionStrings.cpp
   CollectMacros.cpp
diff --git a/clang-tools-extra/clangd/ClangTidyFeatureModule.cpp b/clang-tools-extra/clangd/ClangTidyFeatureModule.cpp
new file mode 100644
index 000000000000..413d89f3777c
--- /dev/null
+++ b/clang-tools-extra/clangd/ClangTidyFeatureModule.cpp
@@ -0,0 +1,412 @@
+//===--- ClangTidyFeatureModule.cpp - clang-tidy integration -------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "ClangTidyFeatureModule.h"
+#include "../clang-tidy/ClangTidyCheck.h"
+#include "../clang-tidy/ClangTidyDiagnosticConsumer.h"
+#include "../clang-tidy/ClangTidyModule.h"
+#include "../clang-tidy/ClangTidyOptions.h"
+#include "Config.h"
+#include "Diagnostics.h"
+#include "Feature.h"
+#include "SourceCode.h"
+#include "support/Logger.h"
+#include "support/Trace.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "clang/Basic/Diagnostic.h"
+#include "clang/Basic/DiagnosticIDs.h"
+#include "clang/Basic/FileManager.h"
+#include "clang/Frontend/CompilerInstance.h"
+#include "clang/Lex/Preprocessor.h"
+#include "clang/Lex/PreprocessorOptions.h"
+#include "clang/Tooling/Core/Diagnostic.h"
+#include "llvm/ADT/DenseSet.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/SmallString.h"
+#include "llvm/Support/MemoryBuffer.h"
+#include "llvm/Support/Path.h"
+#include <memory>
+#include <optional>
+#include <string>
+#include <tuple>
+#include <utility>
+#include <vector>
+
+// Force the linker to link in Clang-tidy modules.
+// clangd doesn't support the static analyzer.
+#if CLANGD_TIDY_CHECKS
+#define CLANG_TIDY_DISABLE_STATIC_ANALYZER_CHECKS
+#include "../clang-tidy/ClangTidyForceLinker.h"
+#endif
+
+#if CLANG_TIDY_ENABLE_QUERY_BASED_CUSTOM_CHECKS
+#include "../clang-tidy/ClangTidy.h"
+#include <mutex>
+#endif
+
+namespace clang {
+namespace clangd {
+namespace {
+
+// Filter for clang diagnostics groups enabled by CTOptions.Checks.
+//
+// These are check names like clang-diagnostics-unused.
+// Note that unlike -Wunused, clang-diagnostics-unused does not imply
+// subcategories like clang-diagnostics-unused-function.
+//
+// This is used to determine which diagnostics can be enabled by ExtraArgs in
+// the clang-tidy configuration.
+class TidyDiagnosticGroups {
+  // Whether all diagnostic groups are enabled by default.
+  // True if we've seen clang-diagnostic-*.
+  bool Default = false;
+  // Set of diag::Group whose enablement != Default.
+  // If Default is false, this is foo where we've seen clang-diagnostic-foo.
+  llvm::DenseSet<unsigned> Exceptions;
+
+public:
+  TidyDiagnosticGroups(llvm::StringRef Checks) {
+    constexpr llvm::StringLiteral Prefix = "clang-diagnostic-";
+    llvm::StringRef Check;
+    while (!Checks.empty()) {
+      std::tie(Check, Checks) = Checks.split(',');
+      Check = Check.trim();
+      if (Check.empty())
+        continue;
+
+      bool Enable = !Check.consume_front("-");
+      bool Glob = Check.consume_back("*");
+      if (Glob) {
+        // Is this clang-diagnostic-*, or *, or so?
+        // (We ignore all other types of globs).
+        if (Prefix.starts_with(Check)) {
+          Default = Enable;
+          Exceptions.clear();
+        }
+        continue;
+      }
+      // In "*,clang-diagnostic-foo", the latter is a no-op.
+      // The only non-glob entries we care about are clang-diagnostic-foo.
+      if (Default == Enable || !Check.consume_front(Prefix))
+        continue;
+      if (auto Group = DiagnosticIDs::getGroupForWarningOption(Check))
+        Exceptions.insert(static_cast<unsigned>(*Group));
+    }
+  }
+
+  bool operator()(diag::Group Group) const {
+    return Exceptions.contains(static_cast<unsigned>(Group)) ? !Default
+                                                             : Default;
+  }
+};
+
+// Find -W<group> and -Wno-<group> options in ExtraArgs and apply them to Diags.
+//
+// This is used to handle ExtraArgs in clang-tidy configuration.
+// We don't use clang's standard handling of this as we want slightly different
+// behavior (e.g. we want to exclude these from -Wno-error).
+void applyWarningOptions(llvm::ArrayRef<std::string> ExtraArgs,
+                         llvm::function_ref<bool(diag::Group)> EnabledGroups,
+                         DiagnosticsEngine &Diags) {
+  for (llvm::StringRef Group : ExtraArgs) {
+    // Only handle args that are of the form -W[no-]<group>.
+    // Other flags are possible but rare and deliberately out of scope.
+    llvm::SmallVector<diag::kind> Members;
+    if (!Group.consume_front("-W") || Group.empty())
+      continue;
+    bool Enable = !Group.consume_front("no-");
+    if (Diags.getDiagnosticIDs()->getDiagnosticsInGroup(
+            diag::Flavor::WarningOrError, Group, Members))
+      continue;
+
+    // Upgrade (or downgrade) the severity of each diagnostic in the group.
+    // If -Werror is on, newly added warnings will be treated as errors.
+    // We don't want this, so keep track of them to fix afterwards.
+    bool NeedsWerrorExclusion = false;
+    for (diag::kind ID : Members) {
+      if (Enable) {
+        if (Diags.getDiagnosticLevel(ID, SourceLocation()) >=
+            DiagnosticsEngine::Warning)
+          continue;
+        auto DiagGroup = Diags.getDiagnosticIDs()->getGroupForDiag(ID);
+        if (!DiagGroup || !EnabledGroups(*DiagGroup))
+          continue;
+        Diags.setSeverity(ID, diag::Severity::Warning, SourceLocation());
+        NeedsWerrorExclusion |= Diags.getWarningsAsErrors();
+      } else {
+        Diags.setSeverity(ID, diag::Severity::Ignored, SourceLocation());
+      }
+    }
+    if (NeedsWerrorExclusion) {
+      // FIXME: there's no API to suppress -Werror for single diagnostics.
+      // In some cases with sub-groups, we may end up erroneously
+      // downgrading diagnostics that were -Werror in the compile command.
+      Diags.setDiagnosticGroupWarningAsError(Group, false);
+    }
+  }
+}
+
+tidy::ClangTidyCheckFactories
+filterFastChecks(const tidy::ClangTidyCheckFactories &All,
+                 Config::FastCheckPolicy Policy) {
+  if (Policy == Config::FastCheckPolicy::None)
+    return All;
+  bool AllowUnknown = Policy == Config::FastCheckPolicy::Loose;
+  tidy::ClangTidyCheckFactories Fast;
+  for (const auto &Factory : All)
+    if (isFastTidyCheck(Factory.getKey()).value_or(AllowUnknown))
+      Fast.registerCheckFactory(Factory.first(), Factory.second);
+  return Fast;
+}
+
+class TidyASTListener final : public FeatureModule::ASTListener {
+public:
+  explicit TidyASTListener(std::shared_ptr<const TidyProvider> OptionsProvider)
+      : Provider(std::move(OptionsProvider)) {}
+
+  void beforeBeginSourceFile(CompilerInstance &CI) override {
+    // Warning options must precede frontend initialization: module loading in
+    // BeginSourceFile() can consult their severity to decide whether to fail.
+    const auto &Inputs = CI.getFrontendOpts().Inputs;
+    if (Inputs.empty() || !Inputs.front().isFile()) {
+      elog("Cannot configure clang-tidy without a main-file input");
+      return;
+    }
+    llvm::SmallString<256> Path(Inputs.front().getFile());
+    // The remapped buffer carries clangd's original main-file spelling.
+    for (const auto &Remap :
+         llvm::reverse(CI.getPreprocessorOpts().RemappedFileBuffers)) {
+      if (Remap.first == Inputs.front().getFile()) {
+        Path = Remap.second->getBufferIdentifier();
+        break;
+      }
+    }
+    // Providers search parent directories for .clang-tidy and require an
+    // absolute path. Respect the compiler's working directory, not clangd's.
+    if (!llvm::sys::path::is_absolute(Path))
+      CI.getFileManager().makeAbsolutePath(Path, /*Canonicalize=*/true);
+    if (!llvm::sys::path::is_absolute(Path)) {
+      elog("Cannot resolve clang-tidy input path: {0}", Path);
+      return;
+    }
+    Filename = Path.str().str();
+    trace::Span Tracer("ClangTidyOpts");
+    Options = getTidyOptionsForFile(*Provider, Filename);
+    dlog("ClangTidy configuration for file {0}: {1}", Filename,
+         tidy::configurationAsText(*Options));
+
+    // If clang-tidy is configured to emit clang warnings, we should too.
+    //
+    // Such clang-tidy configuration consists of two parts:
+    //   - ExtraArgs: ["-Wfoo"] causes clang to produce the warnings
+    //   - Checks: "clang-diagnostic-foo" prevents clang-tidy filtering them out
+    //
+    // In clang-tidy, diagnostics are emitted if they pass both checks.
+    // When groups contain subgroups, -Wparent includes the child, but
+    // clang-diagnostic-parent does not.
+    //
+    // We *don't* want to change the compile command directly. This can have
+    // too many unexpected effects: breaking the command, interactions with
+    // -- and -Werror, etc. Besides, we've already parsed the command.
+    // Instead we parse the -W<group> flags and handle them directly.
+    //
+    // Similarly, we don't want to use Checks to filter clang diagnostics after
+    // they are generated, as this spreads clang-tidy emulation everywhere.
+    // Instead, we just use these to filter which extra diagnostics we enable.
+    TidyDiagnosticGroups EnabledGroups(Options->Checks ? *Options->Checks
+                                                       : llvm::StringRef());
+    if (Options->ExtraArgsBefore)
+      applyWarningOptions(*Options->ExtraArgsBefore, EnabledGroups,
+                          CI.getDiagnostics());
+    if (Options->ExtraArgs)
+      applyWarningOptions(*Options->ExtraArgs, EnabledGroups,
+                          CI.getDiagnostics());
+  }
+
+  void beforePPCallbacks(CompilerInstance &CI) override {
+    // Preamble builds don't call beforeBeginSourceFile(). Running tidy there
+    // would duplicate diagnostics and interfere with PCH generation.
+    if (!Options)
+      return;
+
+    // Set up ClangTidy. Must happen after BeginSourceFile() so ASTContext
+    // exists. Clang-tidy has some limitations to ensure reasonable performance:
+    //  - checks don't see all preprocessor events in the preamble
+    //  - matchers run only over the main-file top-level decls (and can't see
+    //    ancestors outside this scope).
+    // In practice almost all checks work well without modifications.
+    trace::Span Tracer("ClangTidyInit");
+    static const auto *AllFactories = [] {
+      auto *Factories = new tidy::ClangTidyCheckFactories;
+      for (const auto &Entry : tidy::ClangTidyModuleRegistry::entries())
+        Entry.instantiate()->addCheckFactories(*Factories);
+      return Factories;
+    }();
+    Context.emplace(
+        std::make_unique<tidy::DefaultOptionsProvider>(
+            tidy::ClangTidyGlobalOptions(), std::move(*Options)),
+        /*AllowEnablingAnalyzerAlphaCheckers=*/false,
+        /*EnableModuleHeadersParsing=*/false,
+        Config::current().Diagnostics.ClangTidy.ExperimentalCustomChecks);
+    Options.reset();
+    // The lifetime of DiagnosticOptions is managed by \c Clang.
+    Context->setDiagnosticsEngine(nullptr, &CI.getDiagnostics());
+    Context->setASTContext(&CI.getASTContext());
+    Context->setCurrentFile(Filename);
+    Context->setSelfContainedDiags(true);
+    tidy::ClangTidyCheckFactories Factories = *AllFactories;
+#if CLANG_TIDY_ENABLE_QUERY_BASED_CUSTOM_CHECKS
+    if (Context->canExperimentalCustomChecks() &&
+        tidy::custom::RegisterCustomChecks) {
+      // RegisterCustomChecks tracks names in process-wide mutable state.
+      // Serializing its use keeps concurrent AST builds independent.
+      static std::mutex CustomChecksMu;
+      std::lock_guard<std::mutex> Lock(CustomChecksMu);
+      tidy::custom::RegisterCustomChecks(Context->getOptions(), Factories);
+    }
+#endif
+    auto FastFactories = filterFastChecks(
+        Factories, Config::current().Diagnostics.ClangTidy.FastCheckFilter);
+    Checks = FastFactories.createChecksForLanguage(&*Context);
+
+    Preprocessor *PP = &CI.getPreprocessor();
+    for (const auto &Check : Checks) {
+      Check->registerPPCallbacks(CI.getSourceManager(), PP, PP);
+      Check->registerMatchers(&Finder);
+    }
+  }
+
+  void afterExecute(CompilerInstance &CI) override {
+    if (Checks.empty())
+      return;
+    // Run the AST-dependent part of the clang-tidy checks.
+    // (The preprocessor part ran already, via PPCallbacks).
+    trace::Span Tracer("ClangTidyMatch");
+    Finder.matchAST(CI.getASTContext());
+  }
+
+  void sawDiagnostic(const clang::Diagnostic &Info, clangd::Diag &D) override {
+    // Preserve the previous suppression policy when no checks are active.
+    if (Checks.empty())
+      return;
+    std::string CheckName = Context->getCheckName(Info.getID());
+    if (CheckName.empty())
+      return;
+
+    if (Config::current().Diagnostics.Suppress.contains(CheckName)) {
+      D.Severity = DiagnosticsEngine::Ignored;
+      return;
+    }
+
+    // Check for suppression comment. Skip the check for diagnostics not
+    // in the main file, because we don't want that function to query the
+    // source buffer for preamble files. For the same reason, we ask
+    // shouldSuppressDiagnostic to avoid I/O.
+    // We let suppression comments take precedence over warning-as-error
+    // to match clang-tidy's behaviour.
+    bool InsideMainFile =
+        Info.hasSourceManager() &&
+        isInsideMainFile(Info.getLocation(), Info.getSourceManager());
+    llvm::SmallVector<tooling::Diagnostic, 1> SuppressionErrors;
+    if (InsideMainFile &&
+        Context->shouldSuppressDiagnostic(D.Severity, Info, SuppressionErrors,
+                                          /*AllowIO=*/false,
+                                          /*EnableNolintBlocks=*/true)) {
+      // FIXME: should we expose the suppression error (invalid use of
+      // NOLINT comments)?
+      D.Severity = DiagnosticsEngine::Ignored;
+      return;
+    }
+    if (!Context->getOptions().SystemHeaders.value_or(false) &&
+        Info.hasSourceManager() &&
+        Info.getSourceManager().isInSystemMacro(Info.getLocation())) {
+      D.Severity = DiagnosticsEngine::Ignored;
+      return;
+    }
+    // Check for warning-as-error.
+    if (D.Severity == DiagnosticsEngine::Warning &&
+        Context->treatAsError(CheckName))
+      D.Severity = DiagnosticsEngine::Error;
+
+    // Compiler warnings keep their clang source and -W name, even when their
+    // severity or suppression is controlled by clang-tidy options.
+    if (!Context->isCompilerDiagnostic(Info.getID())) {
+      D.Name = std::move(CheckName);
+      D.Source = Diag::ClangTidy;
+    }
+  }
+
+  void finalizeDiagnostic(clangd::Diag &D) override {
+    // Constructor diagnostics precede check initialization.
+    if (D.Source == Diag::Unknown && Context &&
+        !Context->isCompilerDiagnostic(D.ID)) {
+      auto Name = Context->getCheckName(D.ID);
+      if (!Name.empty()) {
+        D.Name = std::move(Name);
+        D.Source = Diag::ClangTidy;
+      }
+    }
+    if (D.Source != Diag::ClangTidy)
+      return;
+    // clang-tidy bakes the name into diagnostic messages. Strip it out.
+    // It would be much nicer to make clang-tidy not do this.
+    auto CleanMessage = [&](std::string &Message) {
+      llvm::StringRef Rest(Message);
+      if (Rest.consume_back("]") && Rest.consume_back(D.Name) &&
+          Rest.consume_back(" ["))
+        Message.resize(Rest.size());
+    };
+    CleanMessage(D.Message);
+    for (auto &Note : D.Notes)
+      CleanMessage(Note.Message);
+    for (auto &Fix : D.Fixes)
+      CleanMessage(Fix.Message);
+    if (llvm::StringRef(D.Name).starts_with("misc-unused-") &&
+        !llvm::is_contained(D.Tags, DiagnosticTag::Unnecessary))
+      D.Tags.push_back(DiagnosticTag::Unnecessary);
+    if (llvm::StringRef(D.Name).starts_with("modernize-") &&
+        !llvm::is_contained(D.Tags, DiagnosticTag::Deprecated))
+      D.Tags.push_back(DiagnosticTag::Deprecated);
+  }
+
+private:
+  std::shared_ptr<const TidyProvider> Provider;
+  std::string Filename;
+  std::optional<tidy::ClangTidyOptions> Options;
+  // Destruction order matters: Finder and checks refer to Context.
+  std::optional<tidy::ClangTidyContext> Context;
+  std::vector<std::unique_ptr<tidy::ClangTidyCheck>> Checks;
+  ast_matchers::MatchFinder Finder;
+};
+
+} // namespace
+
+std::unique_ptr<FeatureModule::ASTListener>
+ClangTidyFeatureModule::astListeners() {
+  std::lock_guard<std::mutex> Lock(ProviderMu);
+  if (!Provider)
+    return nullptr;
+  return std::make_unique<TidyASTListener>(Provider);
+}
+
+void ClangTidyFeatureModule::setProvider(TidyProvider OptionsProvider) {
+  std::shared_ptr<const TidyProvider> Replacement;
+  if (OptionsProvider)
+    Replacement =
+        std::make_shared<const TidyProvider>(std::move(OptionsProvider));
+  swapProvider(Replacement);
+}
+
+void ClangTidyFeatureModule::swapProvider(
+    std::shared_ptr<const TidyProvider> &OptionsProvider) {
+  std::lock_guard<std::mutex> Lock(ProviderMu);
+  Provider.swap(OptionsProvider);
+}
+
+} // namespace clangd
+} // namespace clang
diff --git a/clang-tools-extra/clangd/ClangTidyFeatureModule.h b/clang-tools-extra/clangd/ClangTidyFeatureModule.h
new file mode 100644
index 000000000000..9ad5a87f3f8d
--- /dev/null
+++ b/clang-tools-extra/clangd/ClangTidyFeatureModule.h
@@ -0,0 +1,47 @@
+//===--- ClangTidyFeatureModule.h - clang-tidy integration -----*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_CLANGTIDYFEATUREMODULE_H
+#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_CLANGTIDYFEATUREMODULE_H
+
+#include "FeatureModule.h"
+#include "TidyProvider.h"
+#include <memory>
+#include <mutex>
+#include <utility>
+
+namespace clang {
+namespace clangd {
+
+/// Runs clang-tidy checks as part of clangd's AST build.
+class ClangTidyFeatureModule final : public FeatureModule {
+public:
+  /// Construct explicitly with the application's clang-tidy options provider.
+  /// Takes ownership of the provider; an empty provider disables checks.
+  explicit ClangTidyFeatureModule(TidyProvider OptionsProvider) {
+    setProvider(std::move(OptionsProvider));
+  }
+
+  /// Swap provider snapshots, e.g. to temporarily override the configuration
+  /// for benchmarking and restore it afterwards. Existing listeners retain
+  /// their snapshot.
+  void swapProvider(std::shared_ptr<const TidyProvider> &OptionsProvider);
+
+  std::unique_ptr<ASTListener> astListeners() override;
+
+private:
+  void setProvider(TidyProvider OptionsProvider);
+
+  std::mutex ProviderMu;
+  std::shared_ptr<const TidyProvider> Provider;
+};
+
+} // namespace clangd
+} // namespace clang
+
+#endif // LLVM_CLANG_TOOLS_EXTRA_CLANGD_CLANGTIDYFEATUREMODULE_H
diff --git a/clang-tools-extra/clangd/ClangdServer.cpp b/clang-tools-extra/clangd/ClangdServer.cpp
index 8535823abc52..8e5b0938242d 100644
--- a/clang-tools-extra/clangd/ClangdServer.cpp
+++ b/clang-tools-extra/clangd/ClangdServer.cpp
@@ -219,7 +219,6 @@ ClangdServer::ClangdServer(const GlobalCompilationDatabase &CDB,
                      ? new FileIndex(Opts.EnableOutgoingCalls)
                      : nullptr),
       ModulesManager(Opts.ModulesManager),
-      ClangTidyProvider(Opts.ClangTidyProvider),
       UseDirtyHeaders(Opts.UseDirtyHeaders),
       LineFoldingOnly(Opts.LineFoldingOnly),
       PreambleParseForwardingFunctions(Opts.PreambleParseForwardingFunctions),
@@ -314,7 +313,6 @@ void ClangdServer::addDocument(PathRef File, llvm::StringRef Contents,
   Inputs.ForceRebuild = ForceRebuild;
   Inputs.Opts = std::move(Opts);
   Inputs.Index = Index;
-  Inputs.ClangTidyProvider = ClangTidyProvider;
   Inputs.FeatureModules = FeatureModules;
   Inputs.ModulesManager = ModulesManager;
   adjustParseInputs(Inputs, File);
diff --git a/clang-tools-extra/clangd/ClangdServer.h b/clang-tools-extra/clangd/ClangdServer.h
index ce3e66e4eb28..e5eb660d4684 100644
--- a/clang-tools-extra/clangd/ClangdServer.h
+++ b/clang-tools-extra/clangd/ClangdServer.h
@@ -145,10 +145,6 @@ public:
     /// If there is no particular file (e.g. project loading) then it is empty.
     std::function<Context(PathRef)> ContextProvider;
 
-    /// The Options provider to use when running clang-tidy. If null, clang-tidy
-    /// checks will be disabled.
-    TidyProviderRef ClangTidyProvider;
-
     /// Clangd's workspace root. Relevant for "workspace" operations not bound
     /// to a particular file.
     /// FIXME: If not set, should use the current working directory.
@@ -507,9 +503,6 @@ private:
   // Manage module files.
   ModulesBuilder *ModulesManager = nullptr;
 
-  // When set, provides clang-tidy options for a specific file.
-  TidyProviderRef ClangTidyProvider;
-
   bool UseDirtyHeaders = false;
 
   // Whether the client supports folding only complete lines.
diff --git a/clang-tools-extra/clangd/Compiler.h b/clang-tools-extra/clangd/Compiler.h
index 5e5e23d5b968..95dd6726bd1a 100644
--- a/clang-tools-extra/clangd/Compiler.h
+++ b/clang-tools-extra/clangd/Compiler.h
@@ -17,7 +17,6 @@
 
 #include "FeatureModule.h"
 #include "ModulesBuilder.h"
-#include "TidyProvider.h"
 #include "index/Index.h"
 #include "support/ThreadsafeFS.h"
 #include "clang/Frontend/CompilerInstance.h"
@@ -60,7 +59,6 @@ struct ParseInputs {
   // Used to recover from diagnostics (e.g. find missing includes for symbol).
   const SymbolIndex *Index = nullptr;
   ParseOptions Opts = ParseOptions();
-  TidyProviderRef ClangTidyProvider = {};
   // Used to acquire ASTListeners when parsing files.
   FeatureModuleSet *FeatureModules = nullptr;
   // Used to build and manage (C++) modules.
diff --git a/clang-tools-extra/clangd/Diagnostics.cpp b/clang-tools-extra/clangd/Diagnostics.cpp
index 28351c7cc224..66e2d4c55cfa 100644
--- a/clang-tools-extra/clangd/Diagnostics.cpp
+++ b/clang-tools-extra/clangd/Diagnostics.cpp
@@ -7,7 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 #include "Diagnostics.h"
-#include "../clang-tidy/ClangTidyDiagnosticConsumer.h"
 #include "Compiler.h"
 #include "Config.h"
 #include "Protocol.h"
@@ -389,12 +388,6 @@ void setTags(clangd::Diag &D) {
   } else if (UnusedDiags->contains(D.ID)) {
     D.Tags.push_back(DiagnosticTag::Unnecessary);
   }
-  if (D.Source == Diag::ClangTidy) {
-    if (llvm::StringRef(D.Name).starts_with("misc-unused-"))
-      D.Tags.push_back(DiagnosticTag::Unnecessary);
-    if (llvm::StringRef(D.Name).starts_with("modernize-"))
-      D.Tags.push_back(DiagnosticTag::Deprecated);
-  }
 }
 } // namespace
 
@@ -571,53 +564,38 @@ int getSeverity(DiagnosticsEngine::Level L) {
   llvm_unreachable("Unknown diagnostic level!");
 }
 
-std::vector<Diag> StoreDiags::take(const clang::tidy::ClangTidyContext *Tidy) {
+std::vector<Diag> StoreDiags::take() {
   // Do not forget to emit a pending diagnostic if there is one.
   flushLastDiag();
 
   // Fill in name/source now that we have all the context needed to map them.
   for (auto &Diag : Output) {
     if (const char *ClangDiag = getDiagnosticCode(Diag.ID)) {
-      // Warnings controlled by -Wfoo are better recognized by that name.
-      StringRef Warning = [&] {
-        if (OrigSrcMgr) {
-          return OrigSrcMgr->getDiagnostics()
-              .getDiagnosticIDs()
-              ->getWarningOptionForDiag(Diag.ID);
+      // Feature modules may already have supplied diagnostic metadata.
+      if (Diag.Source == Diag::Unknown)
+        Diag.Source = Diag::Clang;
+      if (Diag.Name.empty()) {
+        // Warnings controlled by -Wfoo are better recognized by that name.
+        StringRef Warning = [&] {
+          if (OrigSrcMgr) {
+            return OrigSrcMgr->getDiagnostics()
+                .getDiagnosticIDs()
+                ->getWarningOptionForDiag(Diag.ID);
+          }
+          if (!DiagnosticIDs::IsCustomDiag(Diag.ID))
+            return DiagnosticIDs{}.getWarningOptionForDiag(Diag.ID);
+          return StringRef{};
+        }();
+
+        if (!Warning.empty()) {
+          Diag.Name = ("-W" + Warning).str();
+        } else {
+          StringRef Name(ClangDiag);
+          // Almost always an error, with a name like err_enum_class_reference.
+          // Drop the err_ prefix for brevity.
+          Name.consume_front("err_");
+          Diag.Name = std::string(Name);
         }
-        if (!DiagnosticIDs::IsCustomDiag(Diag.ID))
-          return DiagnosticIDs{}.getWarningOptionForDiag(Diag.ID);
-        return StringRef{};
-      }();
-
-      if (!Warning.empty()) {
-        Diag.Name = ("-W" + Warning).str();
-      } else {
-        StringRef Name(ClangDiag);
-        // Almost always an error, with a name like err_enum_class_reference.
-        // Drop the err_ prefix for brevity.
-        Name.consume_front("err_");
-        Diag.Name = std::string(Name);
-      }
-      Diag.Source = Diag::Clang;
-    } else if (Tidy != nullptr) {
-      std::string TidyDiag = Tidy->getCheckName(Diag.ID);
-      if (!TidyDiag.empty()) {
-        Diag.Name = std::move(TidyDiag);
-        Diag.Source = Diag::ClangTidy;
-        // clang-tidy bakes the name into diagnostic messages. Strip it out.
-        // It would be much nicer to make clang-tidy not do this.
-        auto CleanMessage = [&](std::string &Msg) {
-          StringRef Rest(Msg);
-          if (Rest.consume_back("]") && Rest.consume_back(Diag.Name) &&
-              Rest.consume_back(" ["))
-            Msg.resize(Rest.size());
-        };
-        CleanMessage(Diag.Message);
-        for (auto &Note : Diag.Notes)
-          CleanMessage(Note.Message);
-        for (auto &Fix : Diag.Fixes)
-          CleanMessage(Fix.Message);
       }
     }
     setTags(Diag);
diff --git a/clang-tools-extra/clangd/Diagnostics.h b/clang-tools-extra/clangd/Diagnostics.h
index ae710f351dbf..992709813fba 100644
--- a/clang-tools-extra/clangd/Diagnostics.h
+++ b/clang-tools-extra/clangd/Diagnostics.h
@@ -28,9 +28,6 @@
 #include <vector>
 
 namespace clang {
-namespace tidy {
-class ClangTidyContext;
-} // namespace tidy
 namespace clangd {
 
 struct ClangdDiagnosticOptions {
@@ -137,8 +134,7 @@ std::optional<std::string> getDiagnosticDocURI(Diag::DiagSource, unsigned ID,
 /// the diag itself nor its notes are in the main file).
 class StoreDiags : public DiagnosticConsumer {
 public:
-  // The ClangTidyContext populates Source and Name for clang-tidy diagnostics.
-  std::vector<Diag> take(const clang::tidy::ClangTidyContext *Tidy = nullptr);
+  std::vector<Diag> take();
 
   void BeginSourceFile(const LangOptions &Opts,
                        const Preprocessor *PP) override;
diff --git a/clang-tools-extra/clangd/ParsedAST.cpp b/clang-tools-extra/clangd/ParsedAST.cpp
index 434138bcdfa5..52db921f660a 100644
--- a/clang-tools-extra/clangd/ParsedAST.cpp
+++ b/clang-tools-extra/clangd/ParsedAST.cpp
@@ -7,23 +7,17 @@
 //===----------------------------------------------------------------------===//
 
 #include "ParsedAST.h"
-#include "../clang-tidy/ClangTidyCheck.h"
-#include "../clang-tidy/ClangTidyDiagnosticConsumer.h"
-#include "../clang-tidy/ClangTidyModule.h"
-#include "../clang-tidy/ClangTidyOptions.h"
 #include "AST.h"
 #include "CollectMacros.h"
 #include "Compiler.h"
 #include "Config.h"
 #include "Diagnostics.h"
-#include "Feature.h"
 #include "FeatureModule.h"
 #include "Headers.h"
 #include "IncludeCleaner.h"
 #include "IncludeFixer.h"
 #include "Preamble.h"
 #include "SourceCode.h"
-#include "TidyProvider.h"
 #include "clang-include-cleaner/Record.h"
 #include "index/Symbol.h"
 #include "support/Logger.h"
@@ -33,7 +27,6 @@
 #include "clang/AST/Decl.h"
 #include "clang/AST/DeclGroup.h"
 #include "clang/AST/ExternalASTSource.h"
-#include "clang/ASTMatchers/ASTMatchFinder.h"
 #include "clang/Basic/Diagnostic.h"
 #include "clang/Basic/DiagnosticIDs.h"
 #include "clang/Basic/DiagnosticSema.h"
@@ -55,11 +48,9 @@
 #include "clang/Sema/HeuristicResolver.h"
 #include "clang/Serialization/ASTWriter.h"
 #include "clang/Tooling/CompilationDatabase.h"
-#include "clang/Tooling/Core/Diagnostic.h"
 #include "clang/Tooling/Syntax/Tokens.h"
 #include "llvm/ADT/ArrayRef.h"
 #include "llvm/ADT/DenseMap.h"
-#include "llvm/ADT/DenseSet.h"
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/ADT/STLFunctionalExtras.h"
 #include "llvm/ADT/SmallVector.h"
@@ -72,22 +63,9 @@
 #include <memory>
 #include <optional>
 #include <string>
-#include <tuple>
 #include <utility>
 #include <vector>
 
-// Force the linker to link in Clang-tidy modules.
-// clangd doesn't support the static analyzer.
-#if CLANGD_TIDY_CHECKS
-#define CLANG_TIDY_DISABLE_STATIC_ANALYZER_CHECKS
-#include "../clang-tidy/ClangTidyForceLinker.h"
-#endif
-
-#if CLANG_TIDY_ENABLE_QUERY_BASED_CUSTOM_CHECKS
-#include "../clang-tidy/ClangTidy.h"
-#include <mutex>
-#endif
-
 namespace clang {
 namespace clangd {
 namespace {
@@ -284,111 +262,6 @@ private:
   std::vector<syntax::Token> MainFileTokens;
 };
 
-// Filter for clang diagnostics groups enabled by CTOptions.Checks.
-//
-// These are check names like clang-diagnostics-unused.
-// Note that unlike -Wunused, clang-diagnostics-unused does not imply
-// subcategories like clang-diagnostics-unused-function.
-//
-// This is used to determine which diagnostics can be enabled by ExtraArgs in
-// the clang-tidy configuration.
-class TidyDiagnosticGroups {
-  // Whether all diagnostic groups are enabled by default.
-  // True if we've seen clang-diagnostic-*.
-  bool Default = false;
-  // Set of diag::Group whose enablement != Default.
-  // If Default is false, this is foo where we've seen clang-diagnostic-foo.
-  llvm::DenseSet<unsigned> Exceptions;
-
-public:
-  TidyDiagnosticGroups(llvm::StringRef Checks) {
-    constexpr llvm::StringLiteral CDPrefix = "clang-diagnostic-";
-
-    llvm::StringRef Check;
-    while (!Checks.empty()) {
-      std::tie(Check, Checks) = Checks.split(',');
-      Check = Check.trim();
-
-      if (Check.empty())
-        continue;
-
-      bool Enable = !Check.consume_front("-");
-      bool Glob = Check.consume_back("*");
-      if (Glob) {
-        // Is this clang-diagnostic-*, or *, or so?
-        // (We ignore all other types of globs).
-        if (CDPrefix.starts_with(Check)) {
-          Default = Enable;
-          Exceptions.clear();
-        }
-        continue;
-      }
-
-      // In "*,clang-diagnostic-foo", the latter is a no-op.
-      if (Default == Enable)
-        continue;
-      // The only non-glob entries we care about are clang-diagnostic-foo.
-      if (!Check.consume_front(CDPrefix))
-        continue;
-
-      if (auto Group = DiagnosticIDs::getGroupForWarningOption(Check))
-        Exceptions.insert(static_cast<unsigned>(*Group));
-    }
-  }
-
-  bool operator()(diag::Group GroupID) const {
-    return Exceptions.contains(static_cast<unsigned>(GroupID)) ? !Default
-                                                               : Default;
-  }
-};
-
-// Find -W<group> and -Wno-<group> options in ExtraArgs and apply them to Diags.
-//
-// This is used to handle ExtraArgs in clang-tidy configuration.
-// We don't use clang's standard handling of this as we want slightly different
-// behavior (e.g. we want to exclude these from -Wno-error).
-void applyWarningOptions(llvm::ArrayRef<std::string> ExtraArgs,
-                         llvm::function_ref<bool(diag::Group)> EnabledGroups,
-                         DiagnosticsEngine &Diags) {
-  for (llvm::StringRef Group : ExtraArgs) {
-    // Only handle args that are of the form -W[no-]<group>.
-    // Other flags are possible but rare and deliberately out of scope.
-    llvm::SmallVector<diag::kind> Members;
-    if (!Group.consume_front("-W") || Group.empty())
-      continue;
-    bool Enable = !Group.consume_front("no-");
-    if (Diags.getDiagnosticIDs()->getDiagnosticsInGroup(
-            diag::Flavor::WarningOrError, Group, Members))
-      continue;
-
-    // Upgrade (or downgrade) the severity of each diagnostic in the group.
-    // If -Werror is on, newly added warnings will be treated as errors.
-    // We don't want this, so keep track of them to fix afterwards.
-    bool NeedsWerrorExclusion = false;
-    for (diag::kind ID : Members) {
-      if (Enable) {
-        if (Diags.getDiagnosticLevel(ID, SourceLocation()) <
-            DiagnosticsEngine::Warning) {
-          auto Group = Diags.getDiagnosticIDs()->getGroupForDiag(ID);
-          if (!Group || !EnabledGroups(*Group))
-            continue;
-          Diags.setSeverity(ID, diag::Severity::Warning, SourceLocation());
-          if (Diags.getWarningsAsErrors())
-            NeedsWerrorExclusion = true;
-        }
-      } else {
-        Diags.setSeverity(ID, diag::Severity::Ignored, SourceLocation());
-      }
-    }
-    if (NeedsWerrorExclusion) {
-      // FIXME: there's no API to suppress -Werror for single diagnostics.
-      // In some cases with sub-groups, we may end up erroneously
-      // downgrading diagnostics that were -Werror in the compile command.
-      Diags.setDiagnosticGroupWarningAsError(Group, false);
-    }
-  }
-}
-
 std::vector<Diag> getIncludeCleanerDiags(ParsedAST &AST, llvm::StringRef Code,
                                          const ThreadsafeFS &TFS) {
   auto &Cfg = Config::current();
@@ -413,20 +286,6 @@ std::vector<Diag> getIncludeCleanerDiags(ParsedAST &AST, llvm::StringRef Code,
       Cfg.Style.AngledHeaders, Cfg.Style.QuotedHeaders);
 }
 
-tidy::ClangTidyCheckFactories
-filterFastTidyChecks(const tidy::ClangTidyCheckFactories &All,
-                     Config::FastCheckPolicy Policy) {
-  if (Policy == Config::FastCheckPolicy::None)
-    return All;
-  bool AllowUnknown = Policy == Config::FastCheckPolicy::Loose;
-  tidy::ClangTidyCheckFactories Fast;
-  for (const auto &Factory : All) {
-    if (isFastTidyCheck(Factory.getKey()).value_or(AllowUnknown))
-      Fast.registerCheckFactory(Factory.first(), Factory.second);
-  }
-  return Fast;
-}
-
 } // namespace
 
 std::optional<ParsedAST>
@@ -509,40 +368,6 @@ ParsedAST::build(llvm::StringRef Filename, const ParseInputs &Inputs,
                         : "unknown error");
     return std::nullopt;
   }
-  tidy::ClangTidyOptions ClangTidyOpts;
-  {
-    trace::Span Tracer("ClangTidyOpts");
-    ClangTidyOpts = getTidyOptionsForFile(Inputs.ClangTidyProvider, Filename);
-    dlog("ClangTidy configuration for file {0}: {1}", Filename,
-         tidy::configurationAsText(ClangTidyOpts));
-
-    // If clang-tidy is configured to emit clang warnings, we should too.
-    //
-    // Such clang-tidy configuration consists of two parts:
-    //   - ExtraArgs: ["-Wfoo"] causes clang to produce the warnings
-    //   - Checks: "clang-diagnostic-foo" prevents clang-tidy filtering them out
-    //
-    // In clang-tidy, diagnostics are emitted if they pass both checks.
-    // When groups contain subgroups, -Wparent includes the child, but
-    // clang-diagnostic-parent does not.
-    //
-    // We *don't* want to change the compile command directly. This can have
-    // too many unexpected effects: breaking the command, interactions with
-    // -- and -Werror, etc. Besides, we've already parsed the command.
-    // Instead we parse the -W<group> flags and handle them directly.
-    //
-    // Similarly, we don't want to use Checks to filter clang diagnostics after
-    // they are generated, as this spreads clang-tidy emulation everywhere.
-    // Instead, we just use these to filter which extra diagnostics we enable.
-    auto &Diags = Clang->getDiagnostics();
-    TidyDiagnosticGroups TidyGroups(ClangTidyOpts.Checks ? *ClangTidyOpts.Checks
-                                                         : llvm::StringRef());
-    if (ClangTidyOpts.ExtraArgsBefore)
-      applyWarningOptions(*ClangTidyOpts.ExtraArgsBefore, TidyGroups, Diags);
-    if (ClangTidyOpts.ExtraArgs)
-      applyWarningOptions(*ClangTidyOpts.ExtraArgs, TidyGroups, Diags);
-  }
-
   auto Action = std::make_unique<ClangdFrontendAction>();
   for (const auto &L : ASTListeners)
     L->beforeBeginSourceFile(*Clang);
@@ -563,150 +388,59 @@ ParsedAST::build(llvm::StringRef Filename, const ParseInputs &Inputs,
     Clang->getPreprocessor().getHeaderSearchInfo().MarkFileIncludeOnce(*MainFE);
   }
 
-  // Set up ClangTidy. Must happen after BeginSourceFile() so ASTContext exists.
-  // Clang-tidy has some limitations to ensure reasonable performance:
-  //  - checks don't see all preprocessor events in the preamble
-  //  - matchers run only over the main-file top-level decls (and can't see
-  //    ancestors outside this scope).
-  // In practice almost all checks work well without modifications.
-  std::vector<std::unique_ptr<tidy::ClangTidyCheck>> CTChecks;
-  ast_matchers::MatchFinder CTFinder;
-  std::optional<tidy::ClangTidyContext> CTContext;
   // Must outlive FixIncludes.
   auto BuildDir = VFS->getCurrentWorkingDirectory();
   std::optional<IncludeFixer> FixIncludes;
   llvm::DenseMap<diag::kind, DiagnosticsEngine::Level> OverriddenSeverity;
-  // No need to run clang-tidy or IncludeFixerif we are not going to surface
-  // diagnostics.
-  {
-    trace::Span Tracer("ClangTidyInit");
-    static const auto *AllCTFactories = [] {
-      auto *CTFactories = new tidy::ClangTidyCheckFactories;
-      for (const auto &E : tidy::ClangTidyModuleRegistry::entries())
-        E.instantiate()->addCheckFactories(*CTFactories);
-      return CTFactories;
-    }();
-    CTContext.emplace(std::make_unique<tidy::DefaultOptionsProvider>(
-                          tidy::ClangTidyGlobalOptions(), ClangTidyOpts),
-                      /*AllowEnablingAnalyzerAlphaCheckers=*/false,
-                      /*EnableModuleHeadersParsing=*/false,
-                      Cfg.Diagnostics.ClangTidy.ExperimentalCustomChecks);
-    // The lifetime of DiagnosticOptions is managed by \c Clang.
-    CTContext->setDiagnosticsEngine(nullptr, &Clang->getDiagnostics());
-    CTContext->setASTContext(&Clang->getASTContext());
-    CTContext->setCurrentFile(Filename);
-    CTContext->setSelfContainedDiags(true);
-    tidy::ClangTidyCheckFactories CTCheckFactories = *AllCTFactories;
-#if CLANG_TIDY_ENABLE_QUERY_BASED_CUSTOM_CHECKS
-    if (CTContext->canExperimentalCustomChecks() &&
-        tidy::custom::RegisterCustomChecks) {
-      // RegisterCustomChecks tracks names in process-wide mutable state.
-      // Serializing its use keeps concurrent AST builds independent.
-      static std::mutex CustomChecksMu;
-      std::lock_guard<std::mutex> Lock(CustomChecksMu);
-      tidy::custom::RegisterCustomChecks(CTContext->getOptions(),
-                                         CTCheckFactories);
-    }
-#endif
-    tidy::ClangTidyCheckFactories FastFactories = filterFastTidyChecks(
-        CTCheckFactories, Cfg.Diagnostics.ClangTidy.FastCheckFilter);
-    CTChecks = FastFactories.createChecksForLanguage(&*CTContext);
-    Preprocessor *PP = &Clang->getPreprocessor();
-    for (const auto &Check : CTChecks) {
-      Check->registerPPCallbacks(Clang->getSourceManager(), PP, PP);
-      Check->registerMatchers(&CTFinder);
-    }
+  // Clang only corrects typos for use of undeclared functions in C if that
+  // use is an error. Include fixer relies on typo correction, so pretend
+  // this is an error. (The actual typo correction is nice too).
+  // We restore the original severity in the level adjuster.
+  // FIXME: It would be better to have a real API for this, but what?
+  for (auto ID : {diag::ext_implicit_function_decl_c99,
+                  diag::ext_implicit_lib_function_decl,
+                  diag::ext_implicit_lib_function_decl_c99,
+                  diag::warn_implicit_function_decl}) {
+    OverriddenSeverity.try_emplace(
+        ID, Clang->getDiagnostics().getDiagnosticLevel(ID, SourceLocation()));
+    Clang->getDiagnostics().setSeverity(ID, diag::Severity::Error,
+                                        SourceLocation());
+  }
+  ASTDiags.setLevelAdjuster(
+      [&](DiagnosticsEngine::Level Level, const clang::Diagnostic &Info) {
+        auto It = OverriddenSeverity.find(Info.getID());
+        return It == OverriddenSeverity.end() ? Level : It->second;
+      });
 
-    // Clang only corrects typos for use of undeclared functions in C if that
-    // use is an error. Include fixer relies on typo correction, so pretend
-    // this is an error. (The actual typo correction is nice too).
-    // We restore the original severity in the level adjuster.
-    // FIXME: It would be better to have a real API for this, but what?
-    for (auto ID : {diag::ext_implicit_function_decl_c99,
-                    diag::ext_implicit_lib_function_decl,
-                    diag::ext_implicit_lib_function_decl_c99,
-                    diag::warn_implicit_function_decl}) {
-      OverriddenSeverity.try_emplace(
-          ID, Clang->getDiagnostics().getDiagnosticLevel(ID, SourceLocation()));
-      Clang->getDiagnostics().setSeverity(ID, diag::Severity::Error,
-                                          SourceLocation());
+  // Add IncludeFixer which can recover diagnostics caused by missing includes
+  // (e.g. incomplete type) and attach include insertion fixes to diagnostics.
+  if (Inputs.Index && !BuildDir.getError()) {
+    auto Style =
+        getFormatStyleForFile(Filename, Inputs.Contents, *Inputs.TFS, false);
+    auto Inserter = std::make_shared<IncludeInserter>(
+        Filename, Inputs.Contents, Style, BuildDir.get(),
+        &Clang->getPreprocessor().getHeaderSearchInfo(),
+        Cfg.Style.QuotedHeaders, Cfg.Style.AngledHeaders);
+    ArrayRef<Inclusion> MainFileIncludes;
+    if (Preamble) {
+      MainFileIncludes = Preamble->Includes.MainFileIncludes;
+      for (const auto &Inc : Preamble->Includes.MainFileIncludes)
+        Inserter->addExisting(Inc);
     }
-
-    ASTDiags.setLevelAdjuster([&](DiagnosticsEngine::Level DiagLevel,
-                                  const clang::Diagnostic &Info) {
-      auto It = OverriddenSeverity.find(Info.getID());
-      if (It != OverriddenSeverity.end())
-        DiagLevel = It->second;
-
-      if (!CTChecks.empty()) {
-        std::string CheckName = CTContext->getCheckName(Info.getID());
-        bool IsClangTidyDiag = !CheckName.empty();
-        if (IsClangTidyDiag) {
-          if (Cfg.Diagnostics.Suppress.contains(CheckName))
-            return DiagnosticsEngine::Ignored;
-          // Check for suppression comment. Skip the check for diagnostics not
-          // in the main file, because we don't want that function to query the
-          // source buffer for preamble files. For the same reason, we ask
-          // shouldSuppressDiagnostic to avoid I/O.
-          // We let suppression comments take precedence over warning-as-error
-          // to match clang-tidy's behaviour.
-          bool IsInsideMainFile =
-              Info.hasSourceManager() &&
-              isInsideMainFile(Info.getLocation(), Info.getSourceManager());
-          SmallVector<tooling::Diagnostic, 1> TidySuppressedErrors;
-          if (IsInsideMainFile && CTContext->shouldSuppressDiagnostic(
-                                      DiagLevel, Info, TidySuppressedErrors,
-                                      /*AllowIO=*/false,
-                                      /*EnableNolintBlocks=*/true)) {
-            // FIXME: should we expose the suppression error (invalid use of
-            // NOLINT comments)?
-            return DiagnosticsEngine::Ignored;
-          }
-          if (!CTContext->getOptions().SystemHeaders.value_or(false) &&
-              Info.hasSourceManager() &&
-              Info.getSourceManager().isInSystemMacro(Info.getLocation()))
-            return DiagnosticsEngine::Ignored;
-
-          // Check for warning-as-error.
-          if (DiagLevel == DiagnosticsEngine::Warning &&
-              CTContext->treatAsError(CheckName)) {
-            return DiagnosticsEngine::Error;
-          }
-        }
-      }
-      return DiagLevel;
+    // FIXME: Consider piping through ASTSignals to fetch this to handle the
+    // case where a header file contains ObjC decls but no #imports.
+    Symbol::IncludeDirective Directive =
+        Inputs.Opts.ImportInsertions
+            ? preferredIncludeDirective(Filename, Clang->getLangOpts(),
+                                        MainFileIncludes, {})
+            : Symbol::Include;
+    FixIncludes.emplace(Filename, Inserter, *Inputs.Index,
+                        /*IndexRequestLimit=*/5, Directive);
+    ASTDiags.contributeFixes([&FixIncludes](DiagnosticsEngine::Level Level,
+                                            const clang::Diagnostic &Info) {
+      return FixIncludes->fix(Level, Info);
     });
-
-    // Add IncludeFixer which can recover diagnostics caused by missing includes
-    // (e.g. incomplete type) and attach include insertion fixes to diagnostics.
-    if (Inputs.Index && !BuildDir.getError()) {
-      auto Style =
-          getFormatStyleForFile(Filename, Inputs.Contents, *Inputs.TFS, false);
-      auto Inserter = std::make_shared<IncludeInserter>(
-          Filename, Inputs.Contents, Style, BuildDir.get(),
-          &Clang->getPreprocessor().getHeaderSearchInfo(),
-          Cfg.Style.QuotedHeaders, Cfg.Style.AngledHeaders);
-      ArrayRef<Inclusion> MainFileIncludes;
-      if (Preamble) {
-        MainFileIncludes = Preamble->Includes.MainFileIncludes;
-        for (const auto &Inc : Preamble->Includes.MainFileIncludes)
-          Inserter->addExisting(Inc);
-      }
-      // FIXME: Consider piping through ASTSignals to fetch this to handle the
-      // case where a header file contains ObjC decls but no #imports.
-      Symbol::IncludeDirective Directive =
-          Inputs.Opts.ImportInsertions
-              ? preferredIncludeDirective(Filename, Clang->getLangOpts(),
-                                          MainFileIncludes, {})
-              : Symbol::Include;
-      FixIncludes.emplace(Filename, Inserter, *Inputs.Index,
-                          /*IndexRequestLimit=*/5, Directive);
-      ASTDiags.contributeFixes([&FixIncludes](DiagnosticsEngine::Level DiagLevl,
-                                              const clang::Diagnostic &Info) {
-        return FixIncludes->fix(DiagLevl, Info);
-      });
-      Clang->setExternalSemaSource(FixIncludes->unresolvedNameRecorder());
-    }
+    Clang->setExternalSemaSource(FixIncludes->unresolvedNameRecorder());
   }
 
   // ReplayPreamble must capture callbacks installed by feature modules.
@@ -776,12 +510,6 @@ ParsedAST::build(llvm::StringRef Filename, const ParseInputs &Inputs,
   std::vector<Decl *> ParsedDecls = Action->takeTopLevelDecls();
   // AST traversals should exclude the preamble, to avoid performance cliffs.
   Clang->getASTContext().setTraversalScope(ParsedDecls);
-  if (!CTChecks.empty()) {
-    // Run the AST-dependent part of the clang-tidy checks.
-    // (The preprocessor part ran already, via PPCallbacks).
-    trace::Span Tracer("ClangTidyMatch");
-    CTFinder.matchAST(Clang->getASTContext());
-  }
   for (const auto &L : ASTListeners)
     L->afterExecute(*Clang);
 
@@ -803,7 +531,7 @@ ParsedAST::build(llvm::StringRef Filename, const ParseInputs &Inputs,
     llvm::append_range(Diags, Patch->patchedDiags());
   // Finally, add diagnostics coming from the AST.
   {
-    std::vector<Diag> D = ASTDiags.take(&*CTContext);
+    std::vector<Diag> D = ASTDiags.take();
     Diags.insert(Diags.end(), D.begin(), D.end());
   }
   ParsedAST Result(Filename, Inputs.Version, std::move(Preamble),
diff --git a/clang-tools-extra/clangd/tool/Check.cpp b/clang-tools-extra/clangd/tool/Check.cpp
index 8284454c6cb5..a22f36ca3a10 100644
--- a/clang-tools-extra/clangd/tool/Check.cpp
+++ b/clang-tools-extra/clangd/tool/Check.cpp
@@ -27,6 +27,7 @@
 #include "../clang-tidy/ClangTidyModule.h"
 #include "../clang-tidy/ClangTidyOptions.h"
 #include "../clang-tidy/GlobList.h"
+#include "ClangTidyFeatureModule.h"
 #include "ClangdLSPServer.h"
 #include "ClangdServer.h"
 #include "CodeComplete.h"
@@ -63,6 +64,7 @@
 #include "clang/Tooling/CompilationDatabase.h"
 #include "llvm/ADT/ArrayRef.h"
 #include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/ScopeExit.h"
 #include "llvm/ADT/SmallString.h"
 #include "llvm/Support/Chrono.h"
 #include "llvm/Support/CommandLine.h"
@@ -205,7 +207,7 @@ public:
     std::vector<std::string> CC1Args;
     Inputs.CompileCommand = Cmd;
     Inputs.TFS = &TFS;
-    Inputs.ClangTidyProvider = Opts.ClangTidyProvider;
+    Inputs.FeatureModules = Opts.FeatureModules;
     Inputs.Opts.PreambleParseForwardingFunctions =
         Opts.PreambleParseForwardingFunctions;
     if (Contents) {
@@ -284,7 +286,8 @@ public:
       #ifndef NDEBUG
       elog("Timing clang-tidy checks in asserts-mode is not representative!");
       #endif
-      checkTidyTimes();
+      if (!checkTidyTimes())
+        return false;
     }
 
     return true;
@@ -297,7 +300,16 @@ public:
   // step-function slowdowns due to CPU scaling.
   // We take the median of 5 measurements, and after every check discard the
   // measurement if the baseline changed by >3%.
-  void checkTidyTimes() {
+  bool checkTidyTimes() {
+    // Keep all other modules active while varying only tidy's configuration.
+    auto *TidyModule =
+        Inputs.FeatureModules
+            ? Inputs.FeatureModules->get<ClangTidyFeatureModule>()
+            : nullptr;
+    if (!TidyModule) {
+      elog("Cannot time clang-tidy checks without a clang-tidy feature module");
+      return false;
+    }
     double Stability = 0.03;
     log("Timing AST build with individual clang-tidy checks (target accuracy "
         "{0:P0})",
@@ -322,7 +334,12 @@ public:
                                     llvm::StringRef) {
         Opts.Checks = Checks.str();
       };
-      Inputs.ClangTidyProvider = CTProvider;
+      auto Provider =
+          std::make_shared<const TidyProvider>(std::move(CTProvider));
+      TidyModule->swapProvider(Provider);
+      // Provider now holds the original configuration; restore it on exit.
+      llvm::scope_exit RestoreProvider(
+          [&] { TidyModule->swapProvider(Provider); });
       // Sigh, can't reuse the CompilerInvocation.
       IgnoringDiagConsumer IgnoreDiags;
       auto Invocation = buildCompilerInvocation(Inputs, IgnoreDiags);
@@ -367,9 +384,7 @@ public:
       log("  {0} = {1:P0}", Check, Fraction);
     }
     log("Finished individual clang-tidy checks");
-
-    // Restore old options.
-    Inputs.ClangTidyProvider = Opts.ClangTidyProvider;
+    return true;
   }
 
   // Build Inlay Hints for the entire AST or the specified range
diff --git a/clang-tools-extra/clangd/tool/ClangdMain.cpp b/clang-tools-extra/clangd/tool/ClangdMain.cpp
index 451495e1dbf1..9bd7f0ef4cf6 100644
--- a/clang-tools-extra/clangd/tool/ClangdMain.cpp
+++ b/clang-tools-extra/clangd/tool/ClangdMain.cpp
@@ -7,6 +7,7 @@
 //===----------------------------------------------------------------------===//
 
 #include "ClangdMain.h"
+#include "ClangTidyFeatureModule.h"
 #include "ClangdLSPServer.h"
 #include "CodeComplete.h"
 #include "Compiler.h"
@@ -1025,7 +1026,6 @@ clangd accepts flags on the commandline, and in the CLANGD_FLAGS environment var
     Providers.push_back(provideDefaultChecks());
     Providers.push_back(disableUnusableChecks());
     ClangTidyOptProvider = combine(std::move(Providers));
-    Opts.ClangTidyProvider = ClangTidyOptProvider;
   }
   Opts.UseDirtyHeaders = UseDirtyHeaders;
   Opts.PreambleParseForwardingFunctions = PreambleParseForwardingFunctions;
@@ -1043,6 +1043,9 @@ clangd accepts flags on the commandline, and in the CLANGD_FLAGS environment var
     Opts.Encoding = ForceOffsetEncoding;
 
   FeatureModuleSet ModuleSet = FeatureModuleSet::fromRegistry();
+  if (EnableClangTidy)
+    ModuleSet.add(std::make_unique<ClangTidyFeatureModule>(
+        std::move(ClangTidyOptProvider)));
   if (ModuleSet.begin() != ModuleSet.end())
     Opts.FeatureModules = &ModuleSet;
 
diff --git a/clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp b/clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp
index 460d6201d5a5..28eb48f4bca2 100644
--- a/clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp
+++ b/clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp
@@ -7,6 +7,7 @@
 //===----------------------------------------------------------------------===//
 
 #include "Annotations.h"
+#include "ClangTidyFeatureModule.h"
 #include "ClangdLSPServer.h"
 #include "ClangdServer.h"
 #include "ConfigProvider.h"
@@ -214,7 +215,8 @@ TEST_F(LSPTest, ClangTidyRename) {
     ClangTidyOpts.CheckOptions["readability-identifier-naming.FunctionCase"] =
         "CamelCase";
   };
-  Opts.ClangTidyProvider = ClangTidyProvider;
+  FeatureModules.add(
+      std::make_unique<ClangTidyFeatureModule>(ClangTidyProvider));
   auto &Client = start();
   Client.didOpen("foo.hpp", Header.code());
   Client.didOpen("foo.cpp", Source.code());
@@ -272,7 +274,8 @@ TEST_F(LSPTest, ClangTidyCrash_Issue109367) {
                                         llvm::StringRef) {
     ClangTidyOpts.Checks = {"-*,boost-use-ranges"};
   };
-  Opts.ClangTidyProvider = ClangTidyProvider;
+  FeatureModules.add(
+      std::make_unique<ClangTidyFeatureModule>(ClangTidyProvider));
   // Check that registering the boost-use-ranges checker's matchers
   // on two different threads does not cause a crash.
   auto &Client = start();
diff --git a/clang-tools-extra/clangd/unittests/ClangdTests.cpp b/clang-tools-extra/clangd/unittests/ClangdTests.cpp
index 9ea7c3e02411..0b3b3485f2a7 100644
--- a/clang-tools-extra/clangd/unittests/ClangdTests.cpp
+++ b/clang-tools-extra/clangd/unittests/ClangdTests.cpp
@@ -7,6 +7,7 @@
 //===----------------------------------------------------------------------===//
 
 #include "Annotations.h"
+#include "ClangTidyFeatureModule.h"
 #include "ClangdServer.h"
 #include "CodeComplete.h"
 #include "CompileCommands.h"
@@ -1228,7 +1229,9 @@ TEST(ClangdServer, TidyOverrideTest) {
   TidyProvider Provider = combine(std::move(Stack));
   CDB.ExtraClangFlags = {"-xc++"};
   auto Opts = ClangdServer::optsForTest();
-  Opts.ClangTidyProvider = Provider;
+  FeatureModuleSet Modules;
+  Modules.add(std::make_unique<ClangTidyFeatureModule>(std::move(Provider)));
+  Opts.FeatureModules = &Modules;
   ClangdServer Server(CDB, FS, Opts, &DiagConsumer);
   const char *SourceContents = R"cpp(
     struct Foo { Foo(); Foo(Foo&); Foo(Foo&&); };
diff --git a/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp b/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp
index 381ba05ccfea..982854c1e5c1 100644
--- a/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp
+++ b/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp
@@ -8,6 +8,7 @@
 
 #include "../clang-tidy/ClangTidyOptions.h"
 #include "Annotations.h"
+#include "ClangTidyFeatureModule.h"
 #include "Compiler.h"
 #include "Config.h"
 #include "Diagnostics.h"
@@ -847,13 +848,17 @@ TEST(DiagnosticTest, ClangTidyCompilerWarningsWithoutChecks) {
 TEST(DiagnosticTest, ClangTidyWarningOptionsBeforeBeginSourceFile) {
   auto Build = [](bool Suppress, bool ExtraArgsBefore) {
     auto TU = TestTU::withCode("int value;");
-    TU.ClangTidyProvider = [=](tidy::ClangTidyOptions &Opts, llvm::StringRef) {
-      Opts.Checks = "-*";
-      if (Suppress) {
-        auto &Args = ExtraArgsBefore ? Opts.ExtraArgsBefore : Opts.ExtraArgs;
-        Args = {"-Wno-incomplete-umbrella"};
-      }
-    };
+    FeatureModuleSet Modules;
+    Modules.add(std::make_unique<ClangTidyFeatureModule>(
+        [=](tidy::ClangTidyOptions &Opts, llvm::StringRef) {
+          Opts.Checks = "-*";
+          if (Suppress) {
+            auto &Args =
+                ExtraArgsBefore ? Opts.ExtraArgsBefore : Opts.ExtraArgs;
+            Args = {"-Wno-incomplete-umbrella"};
+          }
+        }));
+    TU.FeatureModules = &Modules;
     TU.ExtraArgs = {"-fmodules",
                     "-fmodule-map-file=" + testPath("module.modulemap")};
     TU.AdditionalFiles["module.modulemap"] =
@@ -885,11 +890,13 @@ TEST(DiagnosticTest, ClangTidyRelativeInput) {
   TU.AdditionalFiles["project/src/.clang-tidy"] =
       "Checks: '-*,modernize-use-nullptr'";
   auto Provider = provideClangTidyFiles(FS);
-  TU.ClangTidyProvider = [&](tidy::ClangTidyOptions &Opts,
-                             llvm::StringRef Filename) {
-    EXPECT_EQ(Filename, testPath(TU.Filename));
-    Provider(Opts, Filename);
-  };
+  FeatureModuleSet Modules;
+  Modules.add(std::make_unique<ClangTidyFeatureModule>(
+      [&](tidy::ClangTidyOptions &Opts, llvm::StringRef Filename) {
+        EXPECT_EQ(Filename, testPath(TU.Filename));
+        Provider(Opts, Filename);
+      }));
+  TU.FeatureModules = &Modules;
   auto Inputs = TU.inputs(FS);
   Inputs.CompileCommand.Directory = testPath("project");
   // Configuration lookup must use the requested file, not the frontend input.
diff --git a/clang-tools-extra/clangd/unittests/FeatureModulesTests.cpp b/clang-tools-extra/clangd/unittests/FeatureModulesTests.cpp
index 46571cfda4e4..826c926b37de 100644
--- a/clang-tools-extra/clangd/unittests/FeatureModulesTests.cpp
+++ b/clang-tools-extra/clangd/unittests/FeatureModulesTests.cpp
@@ -7,6 +7,7 @@
 //===----------------------------------------------------------------------===//
 
 #include "Annotations.h"
+#include "ClangTidyFeatureModule.h"
 #include "Feature.h"
 #include "FeatureModule.h"
 #include "Selection.h"
@@ -306,6 +307,26 @@ TEST(FeatureModulesTest, FinalizeDiagnostic) {
   EXPECT_EQ(Fixes, 1u);
 }
 
+TEST(FeatureModulesTest, ClangTidyTemporaryProvider) {
+  if (!CLANGD_TIDY_CHECKS)
+    GTEST_SKIP() << "Requires clang-tidy checks";
+  FeatureModuleSet Modules;
+  Modules.add(std::make_unique<ClangTidyFeatureModule>(
+      addTidyChecks("modernize-use-nullptr")));
+  auto TU = TestTU::withCode("int *p = 0;");
+  TU.FeatureModules = &Modules;
+  auto *Tidy = Modules.get<ClangTidyFeatureModule>();
+  auto Provider = std::make_shared<const TidyProvider>(addTidyChecks("-*"));
+  // Check-mode timing temporarily overrides tidy's configuration, then
+  // restores the original provider for subsequent builds.
+  Tidy->swapProvider(Provider);
+  EXPECT_THAT(TU.build().getDiagnostics(), testing::IsEmpty());
+  Tidy->swapProvider(Provider);
+  auto AST = TU.build();
+  ASSERT_THAT(AST.getDiagnostics(), testing::SizeIs(1));
+  EXPECT_EQ(AST.getDiagnostics().front().Name, "modernize-use-nullptr");
+}
+
 TEST(FeatureModulesTest, ClangTidyDoesNotReinitializeConsumer) {
   if (!CLANGD_TIDY_CHECKS)
     GTEST_SKIP() << "Requires clang-tidy checks";
@@ -331,7 +352,8 @@ TEST(FeatureModulesTest, ClangTidyDoesNotReinitializeConsumer) {
   Modules.add(std::move(Module));
   auto TU = TestTU::withCode("int *p = 0;");
   TU.FeatureModules = &Modules;
-  TU.ClangTidyProvider = addTidyChecks("modernize-use-nullptr");
+  Modules.add(std::make_unique<ClangTidyFeatureModule>(
+      addTidyChecks("modernize-use-nullptr")));
   // Running tidy must not reinitialize the existing AST consumer.
   EXPECT_THAT(TU.build().getDiagnostics(), testing::SizeIs(1));
 }
diff --git a/clang-tools-extra/clangd/unittests/TestTU.cpp b/clang-tools-extra/clangd/unittests/TestTU.cpp
index e2c2e3f000b6..fb959c7c8dcb 100644
--- a/clang-tools-extra/clangd/unittests/TestTU.cpp
+++ b/clang-tools-extra/clangd/unittests/TestTU.cpp
@@ -7,6 +7,7 @@
 //===----------------------------------------------------------------------===//
 
 #include "TestTU.h"
+#include "ClangTidyFeatureModule.h"
 #include "CompileCommands.h"
 #include "Compiler.h"
 #include "Diagnostics.h"
@@ -73,8 +74,6 @@ ParseInputs TestTU::inputs(MockFS &FS) const {
     FS.OverlayRealFileSystemForModules = true;
   Inputs.TFS = &FS;
   Inputs.Opts = ParseOptions();
-  if (ClangTidyProvider)
-    Inputs.ClangTidyProvider = ClangTidyProvider;
   Inputs.Index = ExternalIndex;
   return Inputs;
 }
@@ -113,9 +112,21 @@ TestTU::preamble(PreambleParsedCallback PreambleCallback) const {
 }
 
 ParsedAST TestTU::build() const {
+  if (ClangTidyProvider && FeatureModules) {
+    llvm::errs() << "Use either ClangTidyProvider or FeatureModules\n";
+    std::abort();
+  }
   MockFS FS;
   auto Inputs = inputs(FS);
   Inputs.Opts = ParseOpts;
+  FeatureModuleSet TidyModules;
+  if (ClangTidyProvider) {
+    TidyModules.add(std::make_unique<ClangTidyFeatureModule>(
+        [this](tidy::ClangTidyOptions &Opts, llvm::StringRef Filename) {
+          ClangTidyProvider(Opts, Filename);
+        }));
+    Inputs.FeatureModules = &TidyModules;
+  }
   StoreDiags Diags;
   auto CI = buildCompilerInvocation(Inputs, Diags);
   assert(CI && "Failed to build compilation invocation.");
diff --git a/llvm/utils/gn/secondary/clang-tools-extra/clangd/BUILD.gn b/llvm/utils/gn/secondary/clang-tools-extra/clangd/BUILD.gn
index f30aac6add17..430b86c61941 100644
--- a/llvm/utils/gn/secondary/clang-tools-extra/clangd/BUILD.gn
+++ b/llvm/utils/gn/secondary/clang-tools-extra/clangd/BUILD.gn
@@ -82,6 +82,7 @@ static_library("clangd") {
     "ASTSignals.cpp",
     "ClangdLSPServer.cpp",
     "ClangdServer.cpp",
+    "ClangTidyFeatureModule.cpp",
     "CodeComplete.cpp",
     "CodeCompletionStrings.cpp",
     "CollectMacros.cpp",

Audit: 1004 displayed source lines, zero source/highlight mismatches; five paired original-comment examples. Source map · Snapshot.

All diagrams and the patch are embedded in this HTML. Site repository · Download Markdown · Download all files.