| //===-- ClangOptionDocEmitter.cpp - Documentation for command line flags --===// | |
| // | |
| // 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 | |
| // | |
| // FIXME: Once this has stabilized, consider moving it to LLVM. | |
| // | |
| //===----------------------------------------------------------------------===// | |
| #include "TableGenBackends.h" | |
| #include "llvm/ADT/STLExtras.h" | |
| #include "llvm/ADT/StringSwitch.h" | |
| #include "llvm/ADT/Twine.h" | |
| #include "llvm/TableGen/Error.h" | |
| #include "llvm/TableGen/Record.h" | |
| #include "llvm/TableGen/TableGenBackend.h" | |
| #include <cctype> | |
| #include <cstring> | |
| #include <map> | |
| using namespace llvm; | |
| namespace { | |
| struct DocumentedOption { | |
| const Record *Option; | |
| std::vector<const Record *> Aliases; | |
| }; | |
| struct DocumentedGroup; | |
| struct Documentation { | |
| std::vector<DocumentedGroup> Groups; | |
| std::vector<DocumentedOption> Options; | |
| bool empty() { | |
| return Groups.empty() && Options.empty(); | |
| } | |
| }; | |
| struct DocumentedGroup : Documentation { | |
| const Record *Group; | |
| }; | |
| static bool hasFlag(const Record *Option, StringRef OptionFlag, | |
| StringRef FlagsField) { | |
| for (const Record *Flag : Option->getValueAsListOfDefs(FlagsField)) | |
| if (Flag->getName() == OptionFlag) | |
| return true; | |
| if (const DefInit *DI = dyn_cast<DefInit>(Option->getValueInit("Group"))) | |
| for (const Record *Flag : DI->getDef()->getValueAsListOfDefs(FlagsField)) | |
| if (Flag->getName() == OptionFlag) | |
| return true; | |
| return false; | |
| } | |
| static bool isOptionVisible(const Record *Option, const Record *DocInfo) { | |
| for (StringRef IgnoredFlag : DocInfo->getValueAsListOfStrings("IgnoreFlags")) | |
| if (hasFlag(Option, IgnoredFlag, "Flags")) | |
| return false; | |
| for (StringRef Mask : DocInfo->getValueAsListOfStrings("VisibilityMask")) | |
| if (hasFlag(Option, Mask, "Visibility")) | |
| return true; | |
| return false; | |
| } | |
| // Reorganize the records into a suitable form for emitting documentation. | |
| Documentation extractDocumentation(const RecordKeeper &Records, | |
| const Record *DocInfo) { | |
| Documentation Result; | |
| // Build the tree of groups. The root in the tree is the fake option group | |
| // (Record*)nullptr, which contains all top-level groups and options. | |
| std::map<const Record *, std::vector<const Record *>> OptionsInGroup; | |
| std::map<const Record *, std::vector<const Record *>> GroupsInGroup; | |
| std::map<const Record *, std::vector<const Record *>> Aliases; | |
| std::map<std::string, const Record *> OptionsByName; | |
| for (const Record *R : Records.getAllDerivedDefinitions("Option")) | |
| OptionsByName[std::string(R->getValueAsString("Name"))] = R; | |
| auto Flatten = [](const Record *R) { | |
| return R->getValue("DocFlatten") && R->getValueAsBit("DocFlatten"); | |
| }; | |
| auto SkipFlattened = [&](const Record *R) -> const Record * { | |
| while (R && Flatten(R)) { | |
| auto *G = dyn_cast<DefInit>(R->getValueInit("Group")); | |
| if (!G) | |
| return nullptr; | |
| R = G->getDef(); | |
| } | |
| return R; | |
| }; | |
| for (const Record *R : Records.getAllDerivedDefinitions("OptionGroup")) { | |
| if (Flatten(R)) | |
| continue; | |
| const Record *Group = nullptr; | |
| if (auto *G = dyn_cast<DefInit>(R->getValueInit("Group"))) | |
| Group = SkipFlattened(G->getDef()); | |
| GroupsInGroup[Group].push_back(R); | |
| } | |
| for (const Record *R : Records.getAllDerivedDefinitions("Option")) { | |
| if (auto *A = dyn_cast<DefInit>(R->getValueInit("Alias"))) { | |
| Aliases[A->getDef()].push_back(R); | |
| continue; | |
| } | |
| // Pretend no-X and Xno-Y options are aliases of X and XY. | |
| std::string Name = std::string(R->getValueAsString("Name")); | |
| if (Name.size() >= 4) { | |
| if (Name.substr(0, 3) == "no-") { | |
| if (const Record *Opt = OptionsByName[Name.substr(3)]) { | |
| Aliases[Opt].push_back(R); | |
| continue; | |
| } | |
| } | |
| if (Name.substr(1, 3) == "no-") { | |
| if (const Record *Opt = OptionsByName[Name[0] + Name.substr(4)]) { | |
| Aliases[Opt].push_back(R); | |
| continue; | |
| } | |
| } | |
| } | |
| const Record *Group = nullptr; | |
| if (auto *G = dyn_cast<DefInit>(R->getValueInit("Group"))) | |
| Group = SkipFlattened(G->getDef()); | |
| OptionsInGroup[Group].push_back(R); | |
| } | |
| auto CompareByName = [](const Record *A, const Record *B) { | |
| return A->getValueAsString("Name") < B->getValueAsString("Name"); | |
| }; | |
| auto CompareByLocation = [](const Record *A, const Record *B) { | |
| return A->getLoc()[0].getPointer() < B->getLoc()[0].getPointer(); | |
| }; | |
| auto DocumentationForOption = [&](const Record *R) -> DocumentedOption { | |
| auto &A = Aliases[R]; | |
| sort(A, CompareByName); | |
| return {R, std::move(A)}; | |
| }; | |
| std::function<Documentation(const Record *)> DocumentationForGroup = | |
| [&](const Record *R) -> Documentation { | |
| Documentation D; | |
| auto &Groups = GroupsInGroup[R]; | |
| sort(Groups, CompareByLocation); | |
| for (const Record *G : Groups) { | |
| D.Groups.emplace_back(); | |
| D.Groups.back().Group = G; | |
| Documentation &Base = D.Groups.back(); | |
| Base = DocumentationForGroup(G); | |
| if (Base.empty()) | |
| D.Groups.pop_back(); | |
| } | |
| auto &Options = OptionsInGroup[R]; | |
| sort(Options, CompareByName); | |
| for (const Record *O : Options) | |
| if (isOptionVisible(O, DocInfo)) | |
| D.Options.push_back(DocumentationForOption(O)); | |
| return D; | |
| }; | |
| return DocumentationForGroup(nullptr); | |
| } | |
| // Get the first and successive separators to use for an OptionKind. | |
| std::pair<StringRef,StringRef> getSeparatorsForKind(const Record *OptionKind) { | |
| return StringSwitch<std::pair<StringRef, StringRef>>(OptionKind->getName()) | |
| .Cases({"KIND_JOINED", "KIND_JOINED_OR_SEPARATE", | |
| "KIND_JOINED_AND_SEPARATE", "KIND_REMAINING_ARGS_JOINED"}, | |
| {"", " "}) | |
| .Case("KIND_COMMAJOINED", {"", ","}) | |
| .Default({" ", " "}); | |
| } | |
| const unsigned UnlimitedArgs = unsigned(-1); | |
| // Get the number of arguments expected for an option, or -1 if any number of | |
| // arguments are accepted. | |
| unsigned getNumArgsForKind(const Record *OptionKind, const Record *Option) { | |
| return StringSwitch<unsigned>(OptionKind->getName()) | |
| .Cases({"KIND_JOINED", "KIND_JOINED_OR_SEPARATE", "KIND_SEPARATE"}, 1) | |
| .Cases({"KIND_REMAINING_ARGS", "KIND_REMAINING_ARGS_JOINED", | |
| "KIND_COMMAJOINED"}, | |
| UnlimitedArgs) | |
| .Case("KIND_JOINED_AND_SEPARATE", 2) | |
| .Case("KIND_MULTIARG", Option->getValueAsInt("NumArgs")) | |
| .Default(0); | |
| } | |
| std::string escapePlainTextForMarkdown(StringRef Str) { | |
| std::string Out; | |
| for (char C : Str) { | |
| if (StringRef("*[]\\<>").count(C)) | |
| Out.push_back('\\'); | |
| Out.push_back(C); | |
| } | |
| return Out; | |
| } | |
| StringRef getSphinxOptionID(StringRef OptionName) { | |
| return OptionName.take_while([](char C) { return isalnum(C) || C == '-'; }); | |
| } | |
| bool canSphinxCopeWithOption(const Record *Option) { | |
| // HACK: Work arond sphinx's inability to cope with punctuation-only options | |
| // such as /? by suppressing them from the option list. | |
| for (char C : Option->getValueAsString("Name")) | |
| if (isalnum(C)) | |
| return true; | |
| return false; | |
| } | |
| void emitHeading(int Depth, const std::string &Heading, raw_ostream &OS) { | |
| assert(Depth < 5 && "groups nested too deeply"); | |
| OS << std::string(Depth + 2, '#') << ' ' << Heading << "\n\n"; | |
| } | |
| /// Get the value of field \p Primary, if possible. If \p Primary does not | |
| /// exist, get the value of \p Fallback and escape it for Markdown emission. | |
| std::string getMarkdownStringWithTextFallback(const Record *R, | |
| StringRef Primary, | |
| StringRef Fallback) { | |
| for (auto Field : {Primary, Fallback}) { | |
| if (auto *V = R->getValue(Field)) { | |
| StringRef Value; | |
| if (auto *SV = dyn_cast_or_null<StringInit>(V->getValue())) | |
| Value = SV->getValue(); | |
| if (!Value.empty()) | |
| return Field == Primary ? Value.str() | |
| : escapePlainTextForMarkdown(Value); | |
| } | |
| } | |
| return std::string(StringRef()); | |
| } | |
| // The Sphinx option directive contents do not need to be escaped. They use | |
| // standard usage conventions, where angle brackets are values, and square | |
| // brackets are optional arguments. See | |
| // https://www.sphinx-doc.org/en/master/usage/domains/standard.html#directive-option | |
| void emitOptionWithArgs(StringRef Prefix, const Record *Option, | |
| ArrayRef<StringRef> Args, raw_ostream &OS) { | |
| OS << Prefix << Option->getValueAsString("Name"); | |
| std::pair<StringRef, StringRef> Separators = | |
| getSeparatorsForKind(Option->getValueAsDef("Kind")); | |
| StringRef Separator = Separators.first; | |
| for (auto Arg : Args) { | |
| OS << Separator << Arg; | |
| Separator = Separators.second; | |
| } | |
| } | |
| constexpr StringLiteral DefaultMetaVarName = "<arg>"; | |
| void emitOptionName(StringRef Prefix, const Record *Option, raw_ostream &OS) { | |
| // Find the arguments to list after the option. | |
| unsigned NumArgs = getNumArgsForKind(Option->getValueAsDef("Kind"), Option); | |
| bool HasMetaVarName = !Option->isValueUnset("MetaVarName"); | |
| std::vector<std::string> Args; | |
| if (HasMetaVarName) | |
| Args.push_back(std::string(Option->getValueAsString("MetaVarName"))); | |
| else if (NumArgs == 1) | |
| Args.push_back(DefaultMetaVarName.str()); | |
| // Fill up arguments if this option didn't provide a meta var name or it | |
| // supports an unlimited number of arguments. We can't see how many arguments | |
| // already are in a meta var name, so assume it has right number. This is | |
| // needed for JoinedAndSeparate options so that there arent't too many | |
| // arguments. | |
| if (!HasMetaVarName || NumArgs == UnlimitedArgs) { | |
| while (Args.size() < NumArgs) { | |
| Args.push_back(("<arg" + Twine(Args.size() + 1) + ">").str()); | |
| // Use '--args <arg1> <arg2>...' if any number of args are allowed. | |
| if (Args.size() == 2 && NumArgs == UnlimitedArgs) { | |
| Args.back() += "..."; | |
| break; | |
| } | |
| } | |
| } | |
| emitOptionWithArgs(Prefix, Option, | |
| std::vector<StringRef>(Args.begin(), Args.end()), OS); | |
| auto AliasArgs = Option->getValueAsListOfStrings("AliasArgs"); | |
| if (!AliasArgs.empty()) { | |
| const Record *Alias = Option->getValueAsDef("Alias"); | |
| OS << " (equivalent to "; | |
| emitOptionWithArgs( | |
| Alias->getValueAsListOfStrings("Prefixes").front(), Alias, | |
| AliasArgs, OS); | |
| OS << ")"; | |
| } | |
| } | |
| bool emitOptionNames(const Record *Option, raw_ostream &OS, bool EmittedAny) { | |
| for (auto &Prefix : Option->getValueAsListOfStrings("Prefixes")) { | |
| if (EmittedAny) | |
| OS << ", "; | |
| emitOptionName(Prefix, Option, OS); | |
| EmittedAny = true; | |
| } | |
| return EmittedAny; | |
| } | |
| template <typename Fn> | |
| void forEachOptionName(const DocumentedOption &Option, const Record *DocInfo, | |
| Fn F) { | |
| F(Option.Option); | |
| for (auto *Alias : Option.Aliases) | |
| if (isOptionVisible(Alias, DocInfo) && | |
| canSphinxCopeWithOption(Option.Option)) | |
| F(Alias); | |
| } | |
| void emitOption(const DocumentedOption &Option, const Record *DocInfo, | |
| raw_ostream &OS) { | |
| if (Option.Option->getValueAsDef("Kind")->getName() == "KIND_UNKNOWN" || | |
| Option.Option->getValueAsDef("Kind")->getName() == "KIND_INPUT") | |
| return; | |
| if (!canSphinxCopeWithOption(Option.Option)) | |
| return; | |
| // HACK: Emit a different program name with each option to work around | |
| // sphinx's inability to cope with options that differ only by punctuation | |
| // (eg -ObjC vs -ObjC++, -G vs -G=). | |
| std::vector<std::string> SphinxOptionIDs; | |
| forEachOptionName(Option, DocInfo, [&](const Record *Option) { | |
| for (auto &Prefix : Option->getValueAsListOfStrings("Prefixes")) | |
| SphinxOptionIDs.push_back(std::string(getSphinxOptionID( | |
| (Prefix + Option->getValueAsString("Name")).str()))); | |
| }); | |
| assert(!SphinxOptionIDs.empty() && "no flags for option"); | |
| static std::map<std::string, int> NextSuffix; | |
| int SphinxWorkaroundSuffix = NextSuffix[*llvm::max_element( | |
| SphinxOptionIDs, [&](const std::string &A, const std::string &B) { | |
| return NextSuffix[A] < NextSuffix[B]; | |
| })]; | |
| for (auto &S : SphinxOptionIDs) | |
| NextSuffix[S] = SphinxWorkaroundSuffix + 1; | |
| std::string Program = DocInfo->getValueAsString("Program").lower(); | |
| if (SphinxWorkaroundSuffix) | |
| OS << ":::{program} " << Program << SphinxWorkaroundSuffix << "\n:::\n\n"; | |
| // Emit the names of the option. | |
| OS << ":::{option} "; | |
| bool EmittedAny = false; | |
| forEachOptionName(Option, DocInfo, [&](const Record *Option) { | |
| EmittedAny = emitOptionNames(Option, OS, EmittedAny); | |
| }); | |
| OS << "\n:::\n\n"; | |
| // Emit the description, if we have one. | |
| const Record *R = Option.Option; | |
| std::string Description; | |
| // Prefer a program specific help string. | |
| // This is a list of (visibilities, string) pairs. | |
| for (const Record *VisibilityHelp : | |
| R->getValueAsListOfDefs("HelpTextsForVariants")) { | |
| // This is a list of visibilities. | |
| ArrayRef<const Init *> Visibilities = | |
| VisibilityHelp->getValueAsListInit("Visibilities")->getElements(); | |
| // See if any of the program's visibilities are in the list. | |
| for (StringRef DocInfoMask : | |
| DocInfo->getValueAsListOfStrings("VisibilityMask")) { | |
| for (const Init *Visibility : Visibilities) { | |
| if (Visibility->getAsUnquotedString() == DocInfoMask) { | |
| // Use the first one we find. | |
| Description = escapePlainTextForMarkdown( | |
| VisibilityHelp->getValueAsString("Text")); | |
| break; | |
| } | |
| } | |
| if (!Description.empty()) | |
| break; | |
| } | |
| if (!Description.empty()) | |
| break; | |
| } | |
| // If there's not a program specific string, use the default one. | |
| if (Description.empty()) | |
| Description = getMarkdownStringWithTextFallback(R, "DocBrief", "HelpText"); | |
| if (!isa<UnsetInit>(R->getValueInit("Values"))) { | |
| if (!Description.empty() && Description.back() != '.') | |
| Description.push_back('.'); | |
| StringRef MetaVarName; | |
| if (!isa<UnsetInit>(R->getValueInit("MetaVarName"))) | |
| MetaVarName = R->getValueAsString("MetaVarName"); | |
| else | |
| MetaVarName = DefaultMetaVarName; | |
| SmallVector<StringRef> Values; | |
| SplitString(R->getValueAsString("Values"), Values, ","); | |
| Description += " " + escapePlainTextForMarkdown(MetaVarName) + " must be '"; | |
| if (Values.size() > 1) { | |
| for (auto [I, Value] : enumerate(drop_end(Values))) { | |
| if (I) | |
| Description += "', '"; | |
| Description += escapePlainTextForMarkdown(Value); | |
| } | |
| Description += "' or '"; | |
| } | |
| Description += escapePlainTextForMarkdown(Values.back()) + "'."; | |
| } | |
| if (!Description.empty()) | |
| OS << Description << "\n\n"; | |
| if (SphinxWorkaroundSuffix) | |
| OS << ":::{program} " << Program << "\n:::\n\n"; | |
| } | |
| void emitDocumentation(int Depth, const Documentation &Doc, | |
| const Record *DocInfo, raw_ostream &OS); | |
| void emitGroup(int Depth, const DocumentedGroup &Group, const Record *DocInfo, | |
| raw_ostream &OS) { | |
| emitHeading(Depth, | |
| getMarkdownStringWithTextFallback(Group.Group, "DocName", "Name"), | |
| OS); | |
| // Emit the description, if we have one. | |
| std::string Description = | |
| getMarkdownStringWithTextFallback(Group.Group, "DocBrief", "HelpText"); | |
| if (!Description.empty()) | |
| OS << Description << "\n\n"; | |
| // Emit contained options and groups. | |
| emitDocumentation(Depth + 1, Group, DocInfo, OS); | |
| } | |
| void emitDocumentation(int Depth, const Documentation &Doc, | |
| const Record *DocInfo, raw_ostream &OS) { | |
| for (auto &O : Doc.Options) | |
| emitOption(O, DocInfo, OS); | |
| for (auto &G : Doc.Groups) | |
| emitGroup(Depth, G, DocInfo, OS); | |
| } | |
| } // namespace | |
| void clang::EmitClangOptDocs(const RecordKeeper &Records, raw_ostream &OS) { | |
| const Record *DocInfo = Records.getDef("GlobalDocumentation"); | |
| if (!DocInfo) { | |
| PrintFatalError("The GlobalDocumentation top-level definition is missing, " | |
| "no documentation will be generated."); | |
| return; | |
| } | |
| OS << DocInfo->getValueAsString("Intro") << "\n"; | |
| OS << ":::{program} " << DocInfo->getValueAsString("Program").lower() | |
| << "\n:::\n\n"; | |
| emitDocumentation(0, extractDocumentation(Records, DocInfo), DocInfo, OS); | |
| } |