diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst --- a/clang/docs/ReleaseNotes.rst +++ b/clang/docs/ReleaseNotes.rst @@ -170,6 +170,14 @@ ``-Wno-implicit-function-declaration``. As of C2x, support for implicit function declarations has been removed, and the warning options will have no effect. +- Typoed preprocessor directives, which are in a skipped block, now can be + suggested as a warning (grouped under the ``-Wunknown-directives`` flag) + to a similar directive. To avoid providing typo correction for any other + directives, suggestion occurs only when a similar pre-implemented directive + is found. Not only for skipped directives but also errored directives, + suggestions can be provided if found. If we are not in C2x/C++2b mode, then + no suggestion for `#elifdef` & `#elifndef` provides. + Fixes `Issue 51598 `_. - ``-Wmisexpect`` warns when the branch weights collected during profiling conflict with those added by ``llvm.expect``. 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,11 @@ 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,21 @@ /// 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. + /// + /// \param DiagID - generally, diag::warn_pp_invalid_directive for a warning + /// or diag::err_pp_invalid_directive for an error + /// \param Tok - Token that represents the directive + /// \param Directive - String reference for the directive name + /// \param EndLoc - End location for fixit + /// + /// \returns true - diagnosed, false - not diagnosed. + bool SuggestTypoedDirective(unsigned DiagID, + const Token &Tok, + StringRef Directive, + const SourceLocation &EndLoc = SourceLocation()) const; + /// 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 @@ -266,6 +266,51 @@ .Default(false); } +/// Find a similar string in `Candidates`. +/// +/// \param LHS a string for a similar string in `Candidates` +/// +/// \param Candidates the candidates to find a similar string. +/// +/// \returns a similar string if exists. If no similar string exists, +/// returns None. +static Optional findSimilarStr( + StringRef LHS, const std::vector &Candidates) { + // We need to check if `Candidates` has the exact case-insensitive string + // because the Levenshtein distance match does not care about it. + for (StringRef C : Candidates) { + if (LHS.equals_insensitive(C)) { + return C; + } + } + + // Keep going with the Levenshtein distance match. + // 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 Length = LHS.size(); + size_t MaxDist = Length < 3 ? Length - 1 : Length / 3; + + Optional> SimilarStr = None; + for (StringRef C : Candidates) { + size_t CurDist = LHS.edit_distance(C, true); + if (CurDist <= MaxDist) { + if (!SimilarStr.hasValue()) { + // The first similar string found. + SimilarStr = {C, CurDist}; + } else if (CurDist < SimilarStr->second) { + // More similar string found. + SimilarStr = {C, CurDist}; + } + } + } + + if (SimilarStr.hasValue()) { + return SimilarStr->first; + } else { + return None; + } +} + bool Preprocessor::CheckMacroName(Token &MacroNameTok, MacroUse isDefineUndef, bool *ShadowFlag) { // Missing macro name? @@ -433,6 +478,30 @@ return BytesToSkip - LengthDiff; } +bool Preprocessor::SuggestTypoedDirective( + unsigned DiagID, + const Token &Tok, + StringRef Directive, + const SourceLocation &EndLoc) const { + std::vector Candidates = { + "if", "ifdef", "ifndef", "elif", "else", "endif" + }; + if (LangOpts.C2x || LangOpts.CPlusPlus2b) + Candidates.insert(Candidates.end(), {"elifdef", "elifndef"}); + + if (Optional Sugg = findSimilarStr(Directive, Candidates)) { + CharSourceRange DirectiveRange = + CharSourceRange::getCharRange(Tok.getLocation(), EndLoc); + std::string SuggValue = Sugg.getValue().str(); + + auto Hint = FixItHint::CreateReplacement(DirectiveRange, "#" + SuggValue); + Diag(Tok, DiagID) << 1 << SuggValue << Hint; + return true; + } else { + return false; + } +} + /// 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 +625,8 @@ CurPPLexer->pushConditionalLevel(Tok.getLocation(), /*wasskipping*/true, /*foundnonskip*/false, /*foundelse*/false); + } else { + SuggestTypoedDirective(diag::warn_pp_invalid_directive, Tok, Directive, endLoc); } } else if (Directive[0] == 'e') { StringRef Sub = Directive.substr(1); @@ -705,7 +776,11 @@ break; } } + } else { + SuggestTypoedDirective(diag::warn_pp_invalid_directive, Tok, Directive, endLoc); } + } else { + SuggestTypoedDirective(diag::warn_pp_invalid_directive, Tok, Directive, endLoc); } CurPPLexer->ParsingPreprocessorDirective = false; @@ -1182,7 +1257,14 @@ } // If we reached here, the preprocessing token is not valid! - Diag(Result, diag::err_pp_invalid_directive); + // Start suggesting if a similar directive found. + unsigned DiagID = diag::err_pp_invalid_directive; + if (IdentifierInfo *II = Result.getIdentifierInfo()) { + if (!SuggestTypoedDirective(DiagID,Result,II->getName())) + Diag(Result, DiagID) << 0; + } else { + Diag(Result, DiagID) << 0; + } // Read the rest of the PP line. DiscardUntilEndOfDirective(); 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,54 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=pre-c2x-cpp2b %s +// RUN: %clang_cc1 -std=c2x -fsyntax-only -verify=c2x-cpp2b %s +// RUN: %clang_cc1 -x c++ -std=c++2b -fsyntax-only -verify=c2x-cpp2b %s + +// id: pre-c2x-cpp2b-warning@+12 {{invalid preprocessing directive, did you mean '#if'?}} +// ifd: pre-c2x-cpp2b-warning@+12 {{invalid preprocessing directive, did you mean '#if'?}} +// ifde: pre-c2x-cpp2b-warning@+12 {{invalid preprocessing directive, did you mean '#ifdef'?}} +// elf: pre-c2x-cpp2b-warning@+12 {{invalid preprocessing directive, did you mean '#elif'?}} +// elsif: pre-c2x-cpp2b-warning@+12 {{invalid preprocessing directive, did you mean '#elif'?}} +// elseif: pre-c2x-cpp2b-warning@+12 {{invalid preprocessing directive, did you mean '#elif'?}} +// elfidef: not suggested to '#elifdef' +// elfindef: not suggested to '#elifdef' +// elfinndef: not suggested to '#elifndef' +// els: pre-c2x-cpp2b-warning@+12 {{invalid preprocessing directive, did you mean '#else'?}} +// endi: pre-c2x-cpp2b-warning@+12 {{invalid preprocessing directive, did you mean '#endif'?}} +#ifdef UNDEFINED +#id +#ifd +#ifde +#elf +#elsif +#elseif +#elfidef +#elfindef +#elfinndef +#els +#endi +#endif +// id: c2x-cpp2b-warning@-12 {{invalid preprocessing directive, did you mean '#if'?}} +// ifd: c2x-cpp2b-warning@-12 {{invalid preprocessing directive, did you mean '#if'?}} +// ifde: c2x-cpp2b-warning@-12 {{invalid preprocessing directive, did you mean '#ifdef'?}} +// elf: c2x-cpp2b-warning@-12 {{invalid preprocessing directive, did you mean '#elif'?}} +// elsif: c2x-cpp2b-warning@-12 {{invalid preprocessing directive, did you mean '#elif'?}} +// elseif: c2x-cpp2b-warning@-12 {{invalid preprocessing directive, did you mean '#elif'?}} +// elfidef: c2x-cpp2b-warning@-12 {{invalid preprocessing directive, did you mean '#elifdef'?}} +// elfindef: c2x-cpp2b-warning@-12 {{invalid preprocessing directive, did you mean '#elifdef'?}} +// elfinndef: c2x-cpp2b-warning@-12 {{invalid preprocessing directive, did you mean '#elifndef'?}} +// els: c2x-cpp2b-warning@-12 {{invalid preprocessing directive, did you mean '#else'?}} +// endi: c2x-cpp2b-warning@-12 {{invalid preprocessing directive, did you mean '#endif'?}} + +#ifdef UNDEFINED +#i // no diagnostic +#endif + +#if special_compiler +#special_compiler_directive // no diagnostic +#endif + +// pre-c2x-cpp2b-error@+1 {{invalid preprocessing directive, did you mean '#if'?}} +#id UNDEFINED +// c2x-cpp2b-error@-1 {{invalid preprocessing directive, did you mean '#if'?}} +// pre-c2x-cpp2b-error@+1 {{#endif without #if}} +#endif +// c2x-cpp2b-error@-1 {{#endif without #if}}