diff --git a/clang/include/clang/Basic/DiagnosticLexKinds.td b/clang/include/clang/Basic/DiagnosticLexKinds.td --- a/clang/include/clang/Basic/DiagnosticLexKinds.td +++ b/clang/include/clang/Basic/DiagnosticLexKinds.td @@ -427,7 +427,12 @@ def ext_pp_opencl_variadic_macros : Extension< "variadic macros are a Clang extension in OpenCL">; -def err_pp_invalid_directive : Error<"invalid preprocessing directive">; +def err_pp_invalid_directive : Error< + "invalid preprocessing directive%select{|, did you mean '#%1'?}0" +>; +def warn_pp_invalid_directive : Warning< + err_pp_invalid_directive.Text>, InGroup>; + def err_pp_directive_required : Error< "%0 must be used within a preprocessing directive">; def err_pp_file_not_found : Error<"'%0' file not found">, DefaultFatal; diff --git a/clang/include/clang/Lex/Preprocessor.h b/clang/include/clang/Lex/Preprocessor.h --- a/clang/include/clang/Lex/Preprocessor.h +++ b/clang/include/clang/Lex/Preprocessor.h @@ -2241,6 +2241,11 @@ /// Return true if an error occurs parsing the arg list. bool ReadMacroParameterList(MacroInfo *MI, Token& LastTok); + /// Provide a suggestion for a typoed directive. If there is no typo, then + /// just skip suggesting. + void SuggestTypoedDirective(const Token &Tok, StringRef Directive, + const SourceLocation &endLoc); + /// We just read a \#if or related directive and decided that the /// subsequent tokens are in the \#if'd out portion of the /// file. Lex the rest of the file, until we see an \#endif. If \p diff --git a/clang/lib/Lex/PPDirectives.cpp b/clang/lib/Lex/PPDirectives.cpp --- a/clang/lib/Lex/PPDirectives.cpp +++ b/clang/lib/Lex/PPDirectives.cpp @@ -433,6 +433,27 @@ return BytesToSkip - LengthDiff; } +/// SuggestTypoedDirective - Provide a suggestion for a typoed directive. If +/// there is no typo, then just skip suggesting. +void Preprocessor::SuggestTypoedDirective(const Token &Tok, StringRef Directive, + const SourceLocation &EndLoc) { + std::vector Candidates = { + "if", "ifdef", "ifndef", "elif", "else", "endif" + }; + if (LangOpts.C2x || LangOpts.CPlusPlus2b) { + Candidates.insert(Candidates.end(), {"elifdef", "elifndef"}); + } + + if (Optional Sugg = Directive.find_similar_str(Candidates)) { + CharSourceRange DirectiveRange = + CharSourceRange::getCharRange(Tok.getLocation(), EndLoc); + std::string SuggValue = Sugg.getValue(); + + auto Hint = FixItHint::CreateReplacement(DirectiveRange, "#" + SuggValue); + Diag(Tok, diag::warn_pp_invalid_directive) << 1 << SuggValue << Hint; + } +} + /// SkipExcludedConditionalBlock - We just read a \#if or related directive and /// decided that the subsequent tokens are in the \#if'd out portion of the /// file. Lex the rest of the file, until we see an \#endif. If @@ -556,6 +577,8 @@ CurPPLexer->pushConditionalLevel(Tok.getLocation(), /*wasskipping*/true, /*foundnonskip*/false, /*foundelse*/false); + } else { + SuggestTypoedDirective(Tok, Directive, endLoc); } } else if (Directive[0] == 'e') { StringRef Sub = Directive.substr(1); @@ -705,7 +728,11 @@ break; } } + } else { + SuggestTypoedDirective(Tok, Directive, endLoc); } + } else { + SuggestTypoedDirective(Tok, Directive, endLoc); } CurPPLexer->ParsingPreprocessorDirective = false; @@ -1182,7 +1209,7 @@ } // If we reached here, the preprocessing token is not valid! - Diag(Result, diag::err_pp_invalid_directive); + Diag(Result, diag::err_pp_invalid_directive) << 0; // Read the rest of the PP line. DiscardUntilEndOfDirective(); diff --git a/clang/test/Preprocessor/suggest-typoed-directive-c2x-cpp2b.c b/clang/test/Preprocessor/suggest-typoed-directive-c2x-cpp2b.c new file mode 100644 --- /dev/null +++ b/clang/test/Preprocessor/suggest-typoed-directive-c2x-cpp2b.c @@ -0,0 +1,29 @@ +// RUN: %clang_cc1 -std=c2x -fsyntax-only -verify %s +// RUN: %clang_cc1 -x c++ -std=c++2b -fsyntax-only -verify %s + +// id: not suggested to '#if' +// ifd: expected-warning@+11 {{invalid preprocessing directive, did you mean '#if'?}} +// ifde: expected-warning@+11 {{invalid preprocessing directive, did you mean '#ifdef'?}} +// elify: expected-warning@+11 {{invalid preprocessing directive, did you mean '#elif'?}} +// elsif: expected-warning@+11 {{invalid preprocessing directive, did you mean '#elif'?}} +// elseif: expected-warning@+11 {{invalid preprocessing directive, did you mean '#elif'?}} +// elfidef: expected-warning@+11 {{invalid preprocessing directive, did you mean '#elifdef'?}} +// elfindef: expected-warning@+11 {{invalid preprocessing directive, did you mean '#elifndef'?}} +// elses: expected-warning@+11 {{invalid preprocessing directive, did you mean '#else'?}} +// endi: expected-warning@+11 {{invalid preprocessing directive, did you mean '#endif'?}} +#ifdef UNDEFINED +#id +#ifd +#ifde +#elify +#elsif +#elseif +#elfidef +#elfindef +#elses +#endi +#endif + +#if special_compiler +#special_compiler_directive // no diagnostic +#endif diff --git a/clang/test/Preprocessor/suggest-typoed-directive.c b/clang/test/Preprocessor/suggest-typoed-directive.c new file mode 100644 --- /dev/null +++ b/clang/test/Preprocessor/suggest-typoed-directive.c @@ -0,0 +1,28 @@ +// RUN: %clang_cc1 -fsyntax-only -verify %s + +// id: not suggested to '#if' +// ifd: expected-warning@+11 {{invalid preprocessing directive, did you mean '#if'?}} +// ifde: expected-warning@+11 {{invalid preprocessing directive, did you mean '#ifdef'?}} +// elify: expected-warning@+11 {{invalid preprocessing directive, did you mean '#elif'?}} +// elsif: expected-warning@+11 {{invalid preprocessing directive, did you mean '#elif'?}} +// elseif: expected-warning@+11 {{invalid preprocessing directive, did you mean '#elif'?}} +// elfidef: not suggested to '#elifdef' +// elfindef: not suggested to '#elifndef' +// elses: expected-warning@+11 {{invalid preprocessing directive, did you mean '#else'?}} +// endi: expected-warning@+11 {{invalid preprocessing directive, did you mean '#endif'?}} +#ifdef UNDEFINED +#id +#ifd +#ifde +#elify +#elsif +#elseif +#elfidef +#elfindef +#elses +#endi +#endif + +#if special_compiler +#special_compiler_directive // no diagnostic +#endif diff --git a/llvm/include/llvm/ADT/StringRef.h b/llvm/include/llvm/ADT/StringRef.h --- a/llvm/include/llvm/ADT/StringRef.h +++ b/llvm/include/llvm/ADT/StringRef.h @@ -10,6 +10,7 @@ #define LLVM_ADT_STRINGREF_H #include "llvm/ADT/DenseMapInfo.h" +#include "llvm/ADT/Optional.h" #include "llvm/ADT/STLFunctionalExtras.h" #include "llvm/ADT/iterator_range.h" #include "llvm/Support/Compiler.h" @@ -24,6 +25,7 @@ #endif #include #include +#include // Declare the __builtin_strlen intrinsic for MSVC so it can be used in // constexpr context. @@ -240,6 +242,23 @@ unsigned edit_distance(StringRef Other, bool AllowReplacements = true, unsigned MaxEditDistance = 0) const; + /// Find a similar string in `Candidates`. + /// + /// \param Candidates the candidates to find a similar string. + /// + /// \param Dist Maximum distance to elect as similar strings. Eventually, + /// a string with the smallest distance (= most similar string) among them + /// will be returned. + /// If non-zero, use it as maximum distance; otherwise, if the LHS size is + /// less than 3, use the LHS size minus 1 and if not, use the LHS size + /// divided by 3. + /// + /// \returns a similar string if exists. If no similar string exists, + /// returns None. + Optional find_similar_str( + const std::vector &Candidates, + size_t Dist = 0) const; + /// str - Get the contents as an std::string. LLVM_NODISCARD std::string str() const { diff --git a/llvm/lib/Support/StringRef.cpp b/llvm/lib/Support/StringRef.cpp --- a/llvm/lib/Support/StringRef.cpp +++ b/llvm/lib/Support/StringRef.cpp @@ -98,6 +98,43 @@ AllowReplacements, MaxEditDistance); } +// Find a similar string in `Candidates`. +Optional StringRef::find_similar_str(const std::vector &Candidates, size_t Dist) const { + // We need to check if `rng` has the exact case-insensitive string because the Levenshtein distance match does not + // care about it. + for (StringRef C : Candidates) { + if (equals_insensitive(C)) { + return C.str(); + } + } + + // Keep going with the Levenshtein distance match. + // If dist is given, use the dist for maxDist; otherwise, if the LHS size is less than 3, use the LHS size minus 1 + // and if not, use the LHS size divided by 3. + size_t MaxDist = Dist != 0 ? Dist + : Length < 3 ? Length - 1 + : Length / 3; + + std::vector> Cand; + for (StringRef C : Candidates) { + size_t CurDist = edit_distance(C, false); + if (CurDist <= MaxDist) { + Cand.emplace_back(C, CurDist); + } + } + + if (Cand.empty()) { + return None; + } else if (Cand.size() == 1) { + return Cand[0].first; + } else { + auto SimilarStr = std::min_element( + Cand.cbegin(), Cand.cend(), + [](const auto &A, const auto &B) { return A.second < B.second; }); + return SimilarStr->first; + } +} + //===----------------------------------------------------------------------===// // String Operations //===----------------------------------------------------------------------===// diff --git a/llvm/unittests/ADT/StringRefTest.cpp b/llvm/unittests/ADT/StringRefTest.cpp --- a/llvm/unittests/ADT/StringRefTest.cpp +++ b/llvm/unittests/ADT/StringRefTest.cpp @@ -566,6 +566,13 @@ } TEST(StringRefTest, EditDistance) { + EXPECT_EQ(0U, StringRef("aaaa").edit_distance("aaaa")); + EXPECT_EQ(1U, StringRef("aaaa").edit_distance("aaab")); + EXPECT_EQ(2U, StringRef("aabc").edit_distance("aacb")); + EXPECT_EQ(2U, StringRef("aabc").edit_distance("abca")); + EXPECT_EQ(3U, StringRef("aabc").edit_distance("adef")); + EXPECT_EQ(4U, StringRef("abcd").edit_distance("efgh")); + StringRef Hello("hello"); EXPECT_EQ(2U, Hello.edit_distance("hill")); @@ -584,6 +591,28 @@ "people soiled our green ")); } +TEST(StringRefTest, FindSimilarStr) { + { + std::vector Candidates{"b", "c"}; + EXPECT_EQ(None, StringRef("a").find_similar_str(Candidates)); + } + { // macros + std::vector Candidates{ + "if", "ifdef", "ifndef", "elif", "elifdef", "elifndef", "else", "endif" + }; + EXPECT_EQ(std::string("elifdef"), StringRef("elfidef").find_similar_str(Candidates)); + EXPECT_EQ(std::string("elifdef"), StringRef("elifdef").find_similar_str(Candidates)); + EXPECT_EQ(None, StringRef("special_compiler_directive").find_similar_str(Candidates)); + } + { // case-insensitive + std::vector Candidates{ + "if", "ifdef", "ifndef", "elif", "elifdef", "elifndef", "else", "endif" + }; + EXPECT_EQ(std::string("elifdef"), StringRef("elifdef").find_similar_str(Candidates)); + EXPECT_EQ(std::string("elifdef"), StringRef("ELIFDEF").find_similar_str(Candidates)); + } +} + TEST(StringRefTest, Misc) { std::string Storage; raw_string_ostream OS(Storage);