1 | //===--- Record.cpp - Record compiler events ------------------------------===// |
---|---|
2 | // |
3 | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
4 | // See https://llvm.org/LICENSE.txt for license information. |
5 | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
6 | // |
7 | //===----------------------------------------------------------------------===// |
8 | |
9 | #include "clang-include-cleaner/Record.h" |
10 | #include "clang-include-cleaner/Types.h" |
11 | #include "clang/AST/ASTConsumer.h" |
12 | #include "clang/AST/ASTContext.h" |
13 | #include "clang/AST/DeclGroup.h" |
14 | #include "clang/Basic/FileEntry.h" |
15 | #include "clang/Basic/FileManager.h" |
16 | #include "clang/Basic/LLVM.h" |
17 | #include "clang/Basic/SourceLocation.h" |
18 | #include "clang/Basic/SourceManager.h" |
19 | #include "clang/Basic/Specifiers.h" |
20 | #include "clang/Frontend/CompilerInstance.h" |
21 | #include "clang/Lex/DirectoryLookup.h" |
22 | #include "clang/Lex/MacroInfo.h" |
23 | #include "clang/Lex/PPCallbacks.h" |
24 | #include "clang/Lex/Preprocessor.h" |
25 | #include "clang/Tooling/Inclusions/HeaderAnalysis.h" |
26 | #include "clang/Tooling/Inclusions/StandardLibrary.h" |
27 | #include "llvm/ADT/ArrayRef.h" |
28 | #include "llvm/ADT/DenseMap.h" |
29 | #include "llvm/ADT/STLExtras.h" |
30 | #include "llvm/ADT/SmallSet.h" |
31 | #include "llvm/ADT/SmallVector.h" |
32 | #include "llvm/ADT/StringRef.h" |
33 | #include "llvm/ADT/iterator_range.h" |
34 | #include "llvm/Support/Allocator.h" |
35 | #include "llvm/Support/Error.h" |
36 | #include "llvm/Support/FileSystem/UniqueID.h" |
37 | #include "llvm/Support/Path.h" |
38 | #include "llvm/Support/StringSaver.h" |
39 | #include <algorithm> |
40 | #include <assert.h> |
41 | #include <memory> |
42 | #include <optional> |
43 | #include <set> |
44 | #include <utility> |
45 | #include <vector> |
46 | |
47 | namespace clang::include_cleaner { |
48 | namespace { |
49 | |
50 | class PPRecorder : public PPCallbacks { |
51 | public: |
52 | PPRecorder(RecordedPP &Recorded, const Preprocessor &PP) |
53 | : Recorded(Recorded), PP(PP), SM(PP.getSourceManager()) { |
54 | for (const auto &Dir : PP.getHeaderSearchInfo().search_dir_range()) |
55 | if (Dir.getLookupType() == DirectoryLookup::LT_NormalDir) |
56 | Recorded.Includes.addSearchDirectory(Dir.getDirRef()->getName()); |
57 | } |
58 | |
59 | void FileChanged(SourceLocation Loc, FileChangeReason Reason, |
60 | SrcMgr::CharacteristicKind FileType, |
61 | FileID PrevFID) override { |
62 | Active = SM.isWrittenInMainFile(Loc); |
63 | } |
64 | |
65 | void InclusionDirective(SourceLocation Hash, const Token &IncludeTok, |
66 | StringRef SpelledFilename, bool IsAngled, |
67 | CharSourceRange FilenameRange, |
68 | OptionalFileEntryRef File, StringRef SearchPath, |
69 | StringRef RelativePath, const Module *SuggestedModule, |
70 | bool ModuleImported, |
71 | SrcMgr::CharacteristicKind) override { |
72 | if (!Active) |
73 | return; |
74 | |
75 | Include I; |
76 | I.HashLocation = Hash; |
77 | I.Resolved = File; |
78 | I.Line = SM.getSpellingLineNumber(Loc: Hash); |
79 | I.Spelled = SpelledFilename; |
80 | I.Angled = IsAngled; |
81 | Recorded.Includes.add(I); |
82 | } |
83 | |
84 | void MacroExpands(const Token &MacroName, const MacroDefinition &MD, |
85 | SourceRange Range, const MacroArgs *Args) override { |
86 | if (!Active) |
87 | return; |
88 | recordMacroRef(Tok: MacroName, MI: *MD.getMacroInfo()); |
89 | } |
90 | |
91 | void MacroDefined(const Token &MacroName, const MacroDirective *MD) override { |
92 | if (!Active) |
93 | return; |
94 | |
95 | const auto *MI = MD->getMacroInfo(); |
96 | // The tokens of a macro definition could refer to a macro. |
97 | // Formally this reference isn't resolved until this macro is expanded, |
98 | // but we want to treat it as a reference anyway. |
99 | for (const auto &Tok : MI->tokens()) { |
100 | auto *II = Tok.getIdentifierInfo(); |
101 | // Could this token be a reference to a macro? (Not param to this macro). |
102 | if (!II || !II->hadMacroDefinition() || |
103 | llvm::is_contained(Range: MI->params(), Element: II)) |
104 | continue; |
105 | if (const MacroInfo *MI = PP.getMacroInfo(II)) |
106 | recordMacroRef(Tok, MI: *MI); |
107 | } |
108 | } |
109 | |
110 | void MacroUndefined(const Token &MacroName, const MacroDefinition &MD, |
111 | const MacroDirective *) override { |
112 | if (!Active) |
113 | return; |
114 | if (const auto *MI = MD.getMacroInfo()) |
115 | recordMacroRef(Tok: MacroName, MI: *MI); |
116 | } |
117 | |
118 | void Ifdef(SourceLocation Loc, const Token &MacroNameTok, |
119 | const MacroDefinition &MD) override { |
120 | if (!Active) |
121 | return; |
122 | if (const auto *MI = MD.getMacroInfo()) |
123 | recordMacroRef(Tok: MacroNameTok, MI: *MI, RT: RefType::Ambiguous); |
124 | } |
125 | |
126 | void Ifndef(SourceLocation Loc, const Token &MacroNameTok, |
127 | const MacroDefinition &MD) override { |
128 | if (!Active) |
129 | return; |
130 | if (const auto *MI = MD.getMacroInfo()) |
131 | recordMacroRef(Tok: MacroNameTok, MI: *MI, RT: RefType::Ambiguous); |
132 | } |
133 | |
134 | using PPCallbacks::Elifdef; |
135 | using PPCallbacks::Elifndef; |
136 | void Elifdef(SourceLocation Loc, const Token &MacroNameTok, |
137 | const MacroDefinition &MD) override { |
138 | if (!Active) |
139 | return; |
140 | if (const auto *MI = MD.getMacroInfo()) |
141 | recordMacroRef(Tok: MacroNameTok, MI: *MI, RT: RefType::Ambiguous); |
142 | } |
143 | void Elifndef(SourceLocation Loc, const Token &MacroNameTok, |
144 | const MacroDefinition &MD) override { |
145 | if (!Active) |
146 | return; |
147 | if (const auto *MI = MD.getMacroInfo()) |
148 | recordMacroRef(Tok: MacroNameTok, MI: *MI, RT: RefType::Ambiguous); |
149 | } |
150 | |
151 | void Defined(const Token &MacroNameTok, const MacroDefinition &MD, |
152 | SourceRange Range) override { |
153 | if (!Active) |
154 | return; |
155 | if (const auto *MI = MD.getMacroInfo()) |
156 | recordMacroRef(Tok: MacroNameTok, MI: *MI, RT: RefType::Ambiguous); |
157 | } |
158 | |
159 | private: |
160 | void recordMacroRef(const Token &Tok, const MacroInfo &MI, |
161 | RefType RT = RefType::Explicit) { |
162 | if (MI.isBuiltinMacro()) |
163 | return; // __FILE__ is not a reference. |
164 | Recorded.MacroReferences.push_back( |
165 | x: SymbolReference{.Target: Macro{.Name: Tok.getIdentifierInfo(), .Definition: MI.getDefinitionLoc()}, |
166 | .RefLocation: Tok.getLocation(), .RT: RT}); |
167 | } |
168 | |
169 | bool Active = false; |
170 | RecordedPP &Recorded; |
171 | const Preprocessor &PP; |
172 | const SourceManager &SM; |
173 | }; |
174 | |
175 | } // namespace |
176 | |
177 | class PragmaIncludes::RecordPragma : public PPCallbacks, public CommentHandler { |
178 | public: |
179 | RecordPragma(const CompilerInstance &CI, PragmaIncludes *Out) |
180 | : RecordPragma(CI.getPreprocessor(), Out) {} |
181 | RecordPragma(const Preprocessor &P, PragmaIncludes *Out) |
182 | : SM(P.getSourceManager()), HeaderInfo(P.getHeaderSearchInfo()), Out(Out), |
183 | Arena(std::make_shared<llvm::BumpPtrAllocator>()), |
184 | UniqueStrings(*Arena), |
185 | MainFileStem(llvm::sys::path::stem( |
186 | path: SM.getNonBuiltinFilenameForID(FID: SM.getMainFileID()).value_or(u: ""))) {} |
187 | |
188 | void FileChanged(SourceLocation Loc, FileChangeReason Reason, |
189 | SrcMgr::CharacteristicKind FileType, |
190 | FileID PrevFID) override { |
191 | InMainFile = SM.isWrittenInMainFile(Loc); |
192 | |
193 | if (Reason == PPCallbacks::ExitFile) { |
194 | // At file exit time HeaderSearchInfo is valid and can be used to |
195 | // determine whether the file was a self-contained header or not. |
196 | if (OptionalFileEntryRef FE = SM.getFileEntryRefForID(FID: PrevFID)) { |
197 | if (tooling::isSelfContainedHeader(FE: *FE, SM, HeaderInfo)) |
198 | Out->NonSelfContainedFiles.erase(V: FE->getUniqueID()); |
199 | else |
200 | Out->NonSelfContainedFiles.insert(V: FE->getUniqueID()); |
201 | } |
202 | } |
203 | } |
204 | |
205 | void EndOfMainFile() override { |
206 | for (auto &It : Out->IWYUExportBy) { |
207 | llvm::sort(C&: It.getSecond()); |
208 | It.getSecond().erase(CS: llvm::unique(R&: It.getSecond()), CE: It.getSecond().end()); |
209 | } |
210 | Out->Arena.emplace_back(args: std::move(Arena)); |
211 | } |
212 | |
213 | void InclusionDirective(SourceLocation HashLoc, const Token &IncludeTok, |
214 | llvm::StringRef FileName, bool IsAngled, |
215 | CharSourceRange /*FilenameRange*/, |
216 | OptionalFileEntryRef File, |
217 | llvm::StringRef /*SearchPath*/, |
218 | llvm::StringRef /*RelativePath*/, |
219 | const clang::Module * /*SuggestedModule*/, |
220 | bool /*ModuleImported*/, |
221 | SrcMgr::CharacteristicKind FileKind) override { |
222 | FileID HashFID = SM.getFileID(SpellingLoc: HashLoc); |
223 | int HashLine = SM.getLineNumber(FID: HashFID, FilePos: SM.getFileOffset(SpellingLoc: HashLoc)); |
224 | std::optional<Header> IncludedHeader; |
225 | if (IsAngled) |
226 | if (auto StandardHeader = |
227 | tooling::stdlib::Header::named(Name: "<"+ FileName.str() + ">")) { |
228 | IncludedHeader = *StandardHeader; |
229 | } |
230 | if (!IncludedHeader && File) |
231 | IncludedHeader = *File; |
232 | checkForExport(IncludingFile: HashFID, HashLine, IncludedHeader, IncludedFile: File); |
233 | checkForKeep(HashLine, IncludedFile: File); |
234 | checkForDeducedAssociated(H: IncludedHeader); |
235 | } |
236 | |
237 | void checkForExport(FileID IncludingFile, int HashLine, |
238 | std::optional<Header> IncludedHeader, |
239 | OptionalFileEntryRef IncludedFile) { |
240 | if (ExportStack.empty()) |
241 | return; |
242 | auto &Top = ExportStack.back(); |
243 | if (Top.SeenAtFile != IncludingFile) |
244 | return; |
245 | // Make sure current include is covered by the export pragma. |
246 | if ((Top.Block && HashLine > Top.SeenAtLine) || |
247 | Top.SeenAtLine == HashLine) { |
248 | if (IncludedFile) |
249 | Out->IWYUExportBy[IncludedFile->getUniqueID()].push_back(Elt: Top.Path); |
250 | if (IncludedHeader && IncludedHeader->kind() == Header::Standard) |
251 | Out->StdIWYUExportBy[IncludedHeader->standard()].push_back(Elt: Top.Path); |
252 | // main-file #include with export pragma should never be removed. |
253 | if (Top.SeenAtFile == SM.getMainFileID() && IncludedFile) |
254 | Out->ShouldKeep.insert(V: IncludedFile->getUniqueID()); |
255 | } |
256 | if (!Top.Block) // Pop immediately for single-line export pragma. |
257 | ExportStack.pop_back(); |
258 | } |
259 | |
260 | void checkForKeep(int HashLine, OptionalFileEntryRef IncludedFile) { |
261 | if (!InMainFile || KeepStack.empty()) |
262 | return; |
263 | KeepPragma &Top = KeepStack.back(); |
264 | // Check if the current include is covered by a keep pragma. |
265 | if (IncludedFile && ((Top.Block && HashLine > Top.SeenAtLine) || |
266 | Top.SeenAtLine == HashLine)) { |
267 | Out->ShouldKeep.insert(V: IncludedFile->getUniqueID()); |
268 | } |
269 | |
270 | if (!Top.Block) |
271 | KeepStack.pop_back(); // Pop immediately for single-line keep pragma. |
272 | } |
273 | |
274 | // Consider marking H as the "associated header" of the main file. |
275 | // |
276 | // Our heuristic: |
277 | // - it must be the first #include in the main file |
278 | // - it must have the same name stem as the main file (foo.h and foo.cpp) |
279 | // (IWYU pragma: associated is also supported, just not by this function). |
280 | // |
281 | // We consider the associated header as if it had a keep pragma. |
282 | // (Unlike IWYU, we don't treat #includes inside the associated header as if |
283 | // they were written in the main file.) |
284 | void checkForDeducedAssociated(std::optional<Header> H) { |
285 | namespace path = llvm::sys::path; |
286 | if (!InMainFile || SeenAssociatedCandidate) |
287 | return; |
288 | SeenAssociatedCandidate = true; // Only the first #include is our candidate. |
289 | if (!H || H->kind() != Header::Physical) |
290 | return; |
291 | if (path::stem(path: H->physical().getName(), style: path::Style::posix) == MainFileStem) |
292 | Out->ShouldKeep.insert(V: H->physical().getUniqueID()); |
293 | } |
294 | |
295 | bool HandleComment(Preprocessor &PP, SourceRange Range) override { |
296 | auto &SM = PP.getSourceManager(); |
297 | auto Pragma = |
298 | tooling::parseIWYUPragma(Text: SM.getCharacterData(SL: Range.getBegin())); |
299 | if (!Pragma) |
300 | return false; |
301 | |
302 | auto [CommentFID, CommentOffset] = SM.getDecomposedLoc(Loc: Range.getBegin()); |
303 | int CommentLine = SM.getLineNumber(FID: CommentFID, FilePos: CommentOffset); |
304 | |
305 | if (InMainFile) { |
306 | if (Pragma->starts_with(Prefix: "keep") || |
307 | // Limited support for associated headers: never consider unused. |
308 | Pragma->starts_with(Prefix: "associated")) { |
309 | KeepStack.push_back(x: {.SeenAtLine: CommentLine, .Block: false}); |
310 | } else if (Pragma->starts_with(Prefix: "begin_keep")) { |
311 | KeepStack.push_back(x: {.SeenAtLine: CommentLine, .Block: true}); |
312 | } else if (Pragma->starts_with(Prefix: "end_keep") && !KeepStack.empty()) { |
313 | assert(KeepStack.back().Block); |
314 | KeepStack.pop_back(); |
315 | } |
316 | } |
317 | |
318 | auto FE = SM.getFileEntryRefForID(FID: CommentFID); |
319 | if (!FE) { |
320 | // This can only happen when the buffer was registered virtually into |
321 | // SourceManager and FileManager has no idea about it. In such a scenario, |
322 | // that file cannot be discovered by HeaderSearch, therefore no "explicit" |
323 | // includes for that file. |
324 | return false; |
325 | } |
326 | auto CommentUID = FE->getUniqueID(); |
327 | if (Pragma->consume_front(Prefix: "private")) { |
328 | StringRef PublicHeader; |
329 | if (Pragma->consume_front(Prefix: ", include ")) { |
330 | // We always insert using the spelling from the pragma. |
331 | PublicHeader = |
332 | save(S: Pragma->starts_with(Prefix: "<") || Pragma->starts_with(Prefix: "\"") |
333 | ? (*Pragma) |
334 | : ("\""+ *Pragma + "\"").str()); |
335 | } |
336 | Out->IWYUPublic.insert(KV: {CommentUID, PublicHeader}); |
337 | return false; |
338 | } |
339 | if (Pragma->consume_front(Prefix: "always_keep")) { |
340 | Out->ShouldKeep.insert(V: CommentUID); |
341 | return false; |
342 | } |
343 | auto Filename = FE->getName(); |
344 | // Record export pragma. |
345 | if (Pragma->starts_with(Prefix: "export")) { |
346 | ExportStack.push_back(x: {.SeenAtLine: CommentLine, .SeenAtFile: CommentFID, .Path: save(S: Filename), .Block: false}); |
347 | } else if (Pragma->starts_with(Prefix: "begin_exports")) { |
348 | ExportStack.push_back(x: {.SeenAtLine: CommentLine, .SeenAtFile: CommentFID, .Path: save(S: Filename), .Block: true}); |
349 | } else if (Pragma->starts_with(Prefix: "end_exports")) { |
350 | // FIXME: be robust on unmatching cases. We should only pop the stack if |
351 | // the begin_exports and end_exports is in the same file. |
352 | if (!ExportStack.empty()) { |
353 | assert(ExportStack.back().Block); |
354 | ExportStack.pop_back(); |
355 | } |
356 | } |
357 | return false; |
358 | } |
359 | |
360 | private: |
361 | StringRef save(llvm::StringRef S) { return UniqueStrings.save(S); } |
362 | |
363 | bool InMainFile = false; |
364 | const SourceManager &SM; |
365 | const HeaderSearch &HeaderInfo; |
366 | PragmaIncludes *Out; |
367 | std::shared_ptr<llvm::BumpPtrAllocator> Arena; |
368 | /// Intern table for strings. Contents are on the arena. |
369 | llvm::StringSaver UniqueStrings; |
370 | // Used when deducing associated header. |
371 | llvm::StringRef MainFileStem; |
372 | bool SeenAssociatedCandidate = false; |
373 | |
374 | struct ExportPragma { |
375 | // The line number where we saw the begin_exports or export pragma. |
376 | int SeenAtLine = 0; // 1-based line number. |
377 | // The file where we saw the pragma. |
378 | FileID SeenAtFile; |
379 | // Name (per FileEntry::getName()) of the file SeenAtFile. |
380 | StringRef Path; |
381 | // true if it is a block begin/end_exports pragma; false if it is a |
382 | // single-line export pragma. |
383 | bool Block = false; |
384 | }; |
385 | // A stack for tracking all open begin_exports or single-line export. |
386 | std::vector<ExportPragma> ExportStack; |
387 | |
388 | struct KeepPragma { |
389 | // The line number where we saw the begin_keep or keep pragma. |
390 | int SeenAtLine = 0; // 1-based line number. |
391 | // true if it is a block begin/end_keep pragma; false if it is a |
392 | // single-line keep pragma. |
393 | bool Block = false; |
394 | }; |
395 | // A stack for tracking all open begin_keep pragmas or single-line keeps. |
396 | std::vector<KeepPragma> KeepStack; |
397 | }; |
398 | |
399 | void PragmaIncludes::record(const CompilerInstance &CI) { |
400 | auto Record = std::make_unique<RecordPragma>(args: CI, args: this); |
401 | CI.getPreprocessor().addCommentHandler(Handler: Record.get()); |
402 | CI.getPreprocessor().addPPCallbacks(C: std::move(Record)); |
403 | } |
404 | |
405 | void PragmaIncludes::record(Preprocessor &P) { |
406 | auto Record = std::make_unique<RecordPragma>(args&: P, args: this); |
407 | P.addCommentHandler(Handler: Record.get()); |
408 | P.addPPCallbacks(C: std::move(Record)); |
409 | } |
410 | |
411 | llvm::StringRef PragmaIncludes::getPublic(const FileEntry *F) const { |
412 | auto It = IWYUPublic.find(Val: F->getUniqueID()); |
413 | if (It == IWYUPublic.end()) |
414 | return ""; |
415 | return It->getSecond(); |
416 | } |
417 | |
418 | static llvm::SmallVector<FileEntryRef> |
419 | toFileEntries(llvm::ArrayRef<StringRef> FileNames, FileManager &FM) { |
420 | llvm::SmallVector<FileEntryRef> Results; |
421 | |
422 | for (auto FName : FileNames) { |
423 | // FIMXE: log the failing cases? |
424 | if (auto FE = FM.getOptionalFileRef(Filename: FName)) |
425 | Results.push_back(Elt: *FE); |
426 | } |
427 | return Results; |
428 | } |
429 | llvm::SmallVector<FileEntryRef> |
430 | PragmaIncludes::getExporters(const FileEntry *File, FileManager &FM) const { |
431 | auto It = IWYUExportBy.find(Val: File->getUniqueID()); |
432 | if (It == IWYUExportBy.end()) |
433 | return {}; |
434 | |
435 | return toFileEntries(FileNames: It->getSecond(), FM); |
436 | } |
437 | llvm::SmallVector<FileEntryRef> |
438 | PragmaIncludes::getExporters(tooling::stdlib::Header StdHeader, |
439 | FileManager &FM) const { |
440 | auto It = StdIWYUExportBy.find(Val: StdHeader); |
441 | if (It == StdIWYUExportBy.end()) |
442 | return {}; |
443 | return toFileEntries(FileNames: It->getSecond(), FM); |
444 | } |
445 | |
446 | bool PragmaIncludes::isSelfContained(const FileEntry *FE) const { |
447 | return !NonSelfContainedFiles.contains(V: FE->getUniqueID()); |
448 | } |
449 | |
450 | bool PragmaIncludes::isPrivate(const FileEntry *FE) const { |
451 | return IWYUPublic.contains(Val: FE->getUniqueID()); |
452 | } |
453 | |
454 | bool PragmaIncludes::shouldKeep(const FileEntry *FE) const { |
455 | return ShouldKeep.contains(V: FE->getUniqueID()) || |
456 | NonSelfContainedFiles.contains(V: FE->getUniqueID()); |
457 | } |
458 | |
459 | namespace { |
460 | template <typename T> bool isImplicitTemplateSpecialization(const Decl *D) { |
461 | if (const auto *TD = dyn_cast<T>(D)) |
462 | return TD->getTemplateSpecializationKind() == TSK_ImplicitInstantiation; |
463 | return false; |
464 | } |
465 | } // namespace |
466 | |
467 | std::unique_ptr<ASTConsumer> RecordedAST::record() { |
468 | class Recorder : public ASTConsumer { |
469 | RecordedAST *Out; |
470 | |
471 | public: |
472 | Recorder(RecordedAST *Out) : Out(Out) {} |
473 | void Initialize(ASTContext &Ctx) override { Out->Ctx = &Ctx; } |
474 | bool HandleTopLevelDecl(DeclGroupRef DG) override { |
475 | const auto &SM = Out->Ctx->getSourceManager(); |
476 | for (Decl *D : DG) { |
477 | if (!SM.isWrittenInMainFile(Loc: SM.getExpansionLoc(Loc: D->getLocation()))) |
478 | continue; |
479 | if (isImplicitTemplateSpecialization<FunctionDecl>(D) || |
480 | isImplicitTemplateSpecialization<CXXRecordDecl>(D) || |
481 | isImplicitTemplateSpecialization<VarDecl>(D)) |
482 | continue; |
483 | // FIXME: Filter out certain Obj-C as well. |
484 | Out->Roots.push_back(x: D); |
485 | } |
486 | return ASTConsumer::HandleTopLevelDecl(D: DG); |
487 | } |
488 | }; |
489 | |
490 | return std::make_unique<Recorder>(args: this); |
491 | } |
492 | |
493 | std::unique_ptr<PPCallbacks> RecordedPP::record(const Preprocessor &PP) { |
494 | return std::make_unique<PPRecorder>(args&: *this, args: PP); |
495 | } |
496 | |
497 | } // namespace clang::include_cleaner |
498 |
Definitions
- PPRecorder
- PPRecorder
- FileChanged
- InclusionDirective
- MacroExpands
- MacroDefined
- MacroUndefined
- Ifdef
- Ifndef
- Elifdef
- Elifndef
- Defined
- recordMacroRef
- RecordPragma
- RecordPragma
- RecordPragma
- FileChanged
- EndOfMainFile
- InclusionDirective
- checkForExport
- checkForKeep
- checkForDeducedAssociated
- HandleComment
- save
- ExportPragma
- KeepPragma
- record
- record
- getPublic
- toFileEntries
- getExporters
- getExporters
- isSelfContained
- isPrivate
- shouldKeep
- isImplicitTemplateSpecialization
- record
Update your C++ knowledge – Modern C++11/14/17 Training
Find out more