diff --git a/clang/include/clang/ASTMatchers/Dynamic/Diagnostics.h b/clang/include/clang/ASTMatchers/Dynamic/Diagnostics.h --- a/clang/include/clang/ASTMatchers/Dynamic/Diagnostics.h +++ b/clang/include/clang/ASTMatchers/Dynamic/Diagnostics.h @@ -60,14 +60,16 @@ ET_None = 0, ET_RegistryMatcherNotFound = 1, - ET_RegistryWrongArgCount = 2, - ET_RegistryWrongArgType = 3, - ET_RegistryNotBindable = 4, - ET_RegistryAmbiguousOverload = 5, - ET_RegistryValueNotFound = 6, - ET_RegistryUnknownEnumWithReplace = 7, - ET_RegistryNonNodeMatcher = 8, - ET_RegistryMatcherNoWithSupport = 9, + ET_RegistryMatcherNotFoundWithReplace = 2, + ET_RegistryWrongArgCount = 3, + ET_RegistryWrongArgType = 4, + ET_RegistryNotBindable = 5, + ET_RegistryAmbiguousOverload = 6, + ET_RegistryValueNotFound = 7, + ET_RegistryValueNotFoundWithReplace = 8, + ET_RegistryUnknownEnumWithReplace = 9, + ET_RegistryNonNodeMatcher = 10, + ET_RegistryMatcherNoWithSupport = 11, ET_ParserStringError = 100, ET_ParserNoOpenParen = 101, diff --git a/clang/include/clang/ASTMatchers/Dynamic/Parser.h b/clang/include/clang/ASTMatchers/Dynamic/Parser.h --- a/clang/include/clang/ASTMatchers/Dynamic/Parser.h +++ b/clang/include/clang/ASTMatchers/Dynamic/Parser.h @@ -95,10 +95,16 @@ /// /// \param MatcherName The matcher name found by the parser. /// - /// \return The matcher constructor, or Optional() if not - /// found. + /// \param SuggestedTypo If non-null and no matcher constructor was found, + /// This will be set to the closest matching name if any exists. + /// + /// \return The matcher constructor if found. If match not found but \p + /// SuggestedTypo was non-null and a close match was found, the + /// corresponding match constructor will be returned to let further error + /// checking continue. virtual llvm::Optional - lookupMatcherCtor(StringRef MatcherName) = 0; + lookupMatcherCtor(StringRef MatcherName, + StringRef *SuggestedTypo = nullptr) = 0; virtual bool isBuilderMatcher(MatcherCtor) const = 0; @@ -139,7 +145,7 @@ ~RegistrySema() override; llvm::Optional - lookupMatcherCtor(StringRef MatcherName) override; + lookupMatcherCtor(StringRef MatcherName, StringRef *SuggestedTypo) override; VariantMatcher actOnMatcherExpression(MatcherCtor Ctor, SourceRange NameRange, diff --git a/clang/include/clang/ASTMatchers/Dynamic/Registry.h b/clang/include/clang/ASTMatchers/Dynamic/Registry.h --- a/clang/include/clang/ASTMatchers/Dynamic/Registry.h +++ b/clang/include/clang/ASTMatchers/Dynamic/Registry.h @@ -95,7 +95,8 @@ /// /// \return An opaque value which may be used to refer to the matcher /// constructor, or Optional() if not found. - static llvm::Optional lookupMatcherCtor(StringRef MatcherName); + static llvm::Optional lookupMatcherCtor(StringRef MatcherName, + StringRef *SuggestTypo); /// Compute the list of completion types for \p Context. /// diff --git a/clang/lib/ASTMatchers/Dynamic/CMakeLists.txt b/clang/lib/ASTMatchers/Dynamic/CMakeLists.txt --- a/clang/lib/ASTMatchers/Dynamic/CMakeLists.txt +++ b/clang/lib/ASTMatchers/Dynamic/CMakeLists.txt @@ -8,6 +8,7 @@ Marshallers.cpp Parser.cpp Registry.cpp + TypoSuggester.cpp VariantValue.cpp LINK_LIBS diff --git a/clang/lib/ASTMatchers/Dynamic/Diagnostics.cpp b/clang/lib/ASTMatchers/Dynamic/Diagnostics.cpp --- a/clang/lib/ASTMatchers/Dynamic/Diagnostics.cpp +++ b/clang/lib/ASTMatchers/Dynamic/Diagnostics.cpp @@ -87,6 +87,8 @@ switch (Type) { case Diagnostics::ET_RegistryMatcherNotFound: return "Matcher not found: $0"; + case Diagnostics::ET_RegistryMatcherNotFoundWithReplace: + return "Matcher not found: $0; did you mean $1?"; case Diagnostics::ET_RegistryWrongArgCount: return "Incorrect argument count. (Expected = $0) != (Actual = $1)"; case Diagnostics::ET_RegistryWrongArgType: @@ -98,6 +100,8 @@ return "Ambiguous matcher overload."; case Diagnostics::ET_RegistryValueNotFound: return "Value not found: $0"; + case Diagnostics::ET_RegistryValueNotFoundWithReplace: + return "Value not found: $0; did you mean $1?"; case Diagnostics::ET_RegistryUnknownEnumWithReplace: return "Unknown value '$1' for arg $0; did you mean '$2'"; case Diagnostics::ET_RegistryNonNodeMatcher: diff --git a/clang/lib/ASTMatchers/Dynamic/Parser.cpp b/clang/lib/ASTMatchers/Dynamic/Parser.cpp --- a/clang/lib/ASTMatchers/Dynamic/Parser.cpp +++ b/clang/lib/ASTMatchers/Dynamic/Parser.cpp @@ -12,6 +12,7 @@ //===----------------------------------------------------------------------===// #include "clang/ASTMatchers/Dynamic/Parser.h" +#include "TypoSuggester.h" #include "clang/ASTMatchers/ASTMatchersInternal.h" #include "clang/ASTMatchers/Dynamic/Diagnostics.h" #include "clang/ASTMatchers/Dynamic/Registry.h" @@ -354,7 +355,7 @@ /// In case of failure it will try to determine the user's intent to give /// an appropriate error message. bool Parser::parseIdentifierPrefixImpl(VariantValue *Value) { - const TokenInfo NameToken = Tokenizer->consumeNextToken(); + TokenInfo NameToken = Tokenizer->consumeNextToken(); if (Tokenizer->nextTokenKind() != TokenInfo::TK_OpenParen) { // Parse as a named value. @@ -421,8 +422,20 @@ Tokenizer->nextTokenKind() == TokenInfo::TK_NewLine || Tokenizer->nextTokenKind() == TokenInfo::TK_Eof) && !S->lookupMatcherCtor(NameToken.Text)) { - Error->addError(NameToken.Range, Error->ET_RegistryValueNotFound) - << NameToken.Text; + TypoSuggester T(NameToken.Text, TypoSuggester::AutoMaxEdit); + if (NamedValues) { + for (const auto &NamedValue : *NamedValues) { + if (T.addSuggestion(NamedValue.getKey())) + break; + } + } + if (T.foundSuggestion()) + Error->addError(NameToken.Range, + Error->ET_RegistryValueNotFoundWithReplace) + << NameToken.Text << T.getSuggestion(); + else + Error->addError(NameToken.Range, Error->ET_RegistryValueNotFound) + << NameToken.Text; return false; } // Otherwise, fallback to the matcher parser. @@ -438,10 +451,21 @@ return false; } - llvm::Optional Ctor = S->lookupMatcherCtor(NameToken.Text); + StringRef Typo; + llvm::Optional Ctor = + S->lookupMatcherCtor(NameToken.Text, &Typo); + + if (!Typo.empty()) { + Error->addError(NameToken.Range, + Error->ET_RegistryMatcherNotFoundWithReplace) + << NameToken.Text << Typo; + // Update the NameToken so diagnostics are better. + NameToken.Text = Typo; + } // Parse as a matcher expression. - return parseMatcherExpressionImpl(NameToken, OpenToken, Ctor, Value); + return parseMatcherExpressionImpl(NameToken, OpenToken, Ctor, Value) && + Typo.empty(); } bool Parser::parseBindID(std::string &BindID) { @@ -474,6 +498,8 @@ std::vector Args; TokenInfo EndToken; + bool HasError = false; + Tokenizer->SkipNewlines(); { @@ -517,8 +543,10 @@ ArgValue.Text = NodeMatcherToken.Text; ArgValue.Range = NodeMatcherToken.Range; + StringRef Typo; + llvm::Optional MappedMatcher = - S->lookupMatcherCtor(ArgValue.Text); + S->lookupMatcherCtor(ArgValue.Text, &Typo); if (!MappedMatcher) { Error->addError(NodeMatcherToken.Range, @@ -526,6 +554,12 @@ << NodeMatcherToken.Text; return false; } + if (!Typo.empty()) { + Error->addError(NodeMatcherToken.Range, + Error->ET_RegistryMatcherNotFoundWithReplace) + << NodeMatcherToken.Text << Typo; + HasError = true; + } ASTNodeKind NK = S->nodeMatcherType(*MappedMatcher); @@ -588,7 +622,7 @@ return false; *Value = Result; - return true; + return !HasError; } else if (ChainCallToken.Text == TokenInfo::ID_With) { Tokenizer->SkipNewlines(); @@ -605,7 +639,8 @@ TokenInfo WithOpenToken = Tokenizer->consumeNextToken(); return parseMatcherExpressionImpl(NameToken, WithOpenToken, - BuiltCtor.get(), Value); + BuiltCtor.get(), Value) && + !HasError; } } @@ -619,7 +654,7 @@ return false; *Value = Result; - return true; + return !HasError; } /// Parse and validate a matcher expression. @@ -829,8 +864,9 @@ Parser::RegistrySema::~RegistrySema() = default; llvm::Optional -Parser::RegistrySema::lookupMatcherCtor(StringRef MatcherName) { - return Registry::lookupMatcherCtor(MatcherName); +Parser::RegistrySema::lookupMatcherCtor(StringRef MatcherName, + StringRef *SuggestedTypo) { + return Registry::lookupMatcherCtor(MatcherName, SuggestedTypo); } VariantMatcher Parser::RegistrySema::actOnMatcherExpression( diff --git a/clang/lib/ASTMatchers/Dynamic/Registry.cpp b/clang/lib/ASTMatchers/Dynamic/Registry.cpp --- a/clang/lib/ASTMatchers/Dynamic/Registry.cpp +++ b/clang/lib/ASTMatchers/Dynamic/Registry.cpp @@ -13,6 +13,7 @@ #include "clang/ASTMatchers/Dynamic/Registry.h" #include "Marshallers.h" +#include "TypoSuggester.h" #include "clang/AST/ASTTypeTraits.h" #include "clang/ASTMatchers/ASTMatchers.h" #include "clang/ASTMatchers/Dynamic/Diagnostics.h" @@ -586,11 +587,25 @@ } // static -llvm::Optional Registry::lookupMatcherCtor(StringRef MatcherName) { +llvm::Optional +Registry::lookupMatcherCtor(StringRef MatcherName, StringRef *SuggestedTypo) { auto it = RegistryData->constructors().find(MatcherName); - return it == RegistryData->constructors().end() - ? llvm::Optional() - : it->second.get(); + if (it != RegistryData->constructors().end()) + return it->second.get(); + if (SuggestedTypo) { + TypoSuggester T(MatcherName, TypoSuggester::AutoMaxEdit); + for (const auto &Ctor : RegistryData->constructors()) { + if (T.addSuggestion(Ctor.getKey())) + break; + } + if (T.foundSuggestion()) { + *SuggestedTypo = T.getSuggestion(); + // If we found a good suggestion, return the Ctor so parsing can continue, + // The error state is conveyed by SuggestedTypo being written to. + return RegistryData->constructors().find(T.getSuggestion())->second.get(); + } + } + return llvm::None; } static llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, diff --git a/clang/lib/ASTMatchers/Dynamic/TypoSuggester.h b/clang/lib/ASTMatchers/Dynamic/TypoSuggester.h new file mode 100644 --- /dev/null +++ b/clang/lib/ASTMatchers/Dynamic/TypoSuggester.h @@ -0,0 +1,54 @@ +//===- TypoSuggester.h ------------------------------------------*- 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_LIB_ASTMATCHERS_DYNAMIC_TYPOSUGGESTER_H +#define LLVM_CLANG_LIB_ASTMATCHERS_DYNAMIC_TYPOSUGGESTER_H + +#include "llvm/ADT/StringRef.h" + +namespace clang { +namespace ast_matchers { +namespace dynamic { +// Helper class for getting the closest match for a typo string. +// If OwnSuggestions is true, temporary strings can be passed to addSuggestion. +class TypoSuggester { + llvm::StringRef Current; + const llvm::StringRef Value; + unsigned BestDistance; + bool HasMatch = false; + +public: + enum AutoMaxEditT { AutoMaxEdit }; + TypoSuggester(llvm::StringRef Value, AutoMaxEditT) + : Value(Value), BestDistance((Value.size() + 3U) / 5U) { + // This BestDistance is rather arbitrary but prevents generating suggestions + // for single character values and grows more lenient as the value grows. + } + TypoSuggester(llvm::StringRef Target, + unsigned MaxEdit = std::numeric_limits::max()) + : Value(Target), BestDistance(MaxEdit) { + assert(MaxEdit > 0); + } + + // Returns true if we reach a state where we can never find a unique closest + // match. + bool addSuggestion(llvm::StringRef Suggest); + // If true, we are in a state where we can never find a unique closest match. + bool hasFailed() const { return BestDistance == 0; } + bool foundSuggestion() const { return HasMatch; } + llvm::StringRef getSuggestion() const { + assert(foundSuggestion()); + return Current; + } +}; + +} // namespace dynamic +} // namespace ast_matchers +} // namespace clang + +#endif // LLVM_CLANG_LIB_ASTMATCHERS_DYNAMIC_TYPOSUGGESTER_H diff --git a/clang/lib/ASTMatchers/Dynamic/TypoSuggester.cpp b/clang/lib/ASTMatchers/Dynamic/TypoSuggester.cpp new file mode 100644 --- /dev/null +++ b/clang/lib/ASTMatchers/Dynamic/TypoSuggester.cpp @@ -0,0 +1,39 @@ +//===- TypoSuggester.cpp --------------------------------------------------===// +// +// 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 "TypoSuggester.h" + +namespace clang { +namespace ast_matchers { +namespace dynamic { +bool TypoSuggester::addSuggestion(llvm::StringRef Suggest) { + unsigned Dist = Value.edit_distance(Suggest, true, BestDistance); + if (Dist > BestDistance) + return false; + if (Dist == BestDistance) { + if (HasMatch) { + // If we already have a completion candidate with the same edit + // distance, we can't be sure so disregard both. + HasMatch = false; + --BestDistance; + // As we assume Target will never appear in Suggest, once we reach a + // case where we need 0 edits, we can stop work. + return BestDistance == 0; + } + Current = Suggest; + HasMatch = true; + return false; + } + Current = Suggest; + HasMatch = true; + BestDistance = Dist; + return false; +} +} // namespace dynamic +} // namespace ast_matchers +} // namespace clang \ No newline at end of file