Index: clang/include/clang/Basic/DiagnosticLexKinds.td =================================================================== --- clang/include/clang/Basic/DiagnosticLexKinds.td +++ clang/include/clang/Basic/DiagnosticLexKinds.td @@ -320,6 +320,8 @@ "whitespace required after macro name">; def warn_missing_whitespace_after_macro_name : Warning< "whitespace recommended after macro name">; +def warn_include_cycle : Warning<"#include cycle">, + InGroup>, DefaultIgnore; class NonportablePath : Warning< "non-portable path to file '%0'; specified path differs in case from file" Index: clang/include/clang/Lex/Preprocessor.h =================================================================== --- clang/include/clang/Lex/Preprocessor.h +++ clang/include/clang/Lex/Preprocessor.h @@ -862,6 +862,9 @@ /// The files that have been included. IncludedFilesSet IncludedFiles; + /// The currently being lexed included files + IncludedFilesSet ActiveIncludedFiles; + /// The set of top-level modules that affected preprocessing, but were not /// imported. llvm::SmallSetVector AffectingModules; Index: clang/include/clang/Lex/PreprocessorLexer.h =================================================================== --- clang/include/clang/Lex/PreprocessorLexer.h +++ clang/include/clang/Lex/PreprocessorLexer.h @@ -67,6 +67,9 @@ /// Note that in raw mode that the PP pointer may be null. bool LexingRawMode = false; + /// Record whether this lexer is for a recursive include. + bool IsCircularInclude = false; + /// A state machine that detects the \#ifndef-wrapping a file /// idiom for the multiple-include optimization. MultipleIncludeOpt MIOpt; @@ -141,6 +144,11 @@ /// Return true if this lexer is in raw mode or not. bool isLexingRawMode() const { return LexingRawMode; } + void setCircularInclude() { IsCircularInclude = true; } + + /// Retur true iff this lexer is a circular include. + bool isCircularInclude() const { return IsCircularInclude; } + /// Return the preprocessor object for this lexer. Preprocessor *getPP() const { return PP; } Index: clang/lib/Lex/PPDirectives.cpp =================================================================== --- clang/lib/Lex/PPDirectives.cpp +++ clang/lib/Lex/PPDirectives.cpp @@ -2318,6 +2318,13 @@ FileCharacter = std::max(HeaderInfo.getFileDirFlavor(&File->getFileEntry()), FileCharacter); + // Check if we're considering entering a file that is already active in the + // include stack. Even if File is idempotent, these cycles are problematic + // for modularization. + if (Action == Enter && File && + ActiveIncludedFiles.contains(&File->getFileEntry())) + Diag(FilenameTok.getLocation(), diag::warn_include_cycle); + // If this is a '#import' or an import-declaration, don't re-enter the file. // // FIXME: If we have a suggested module for a '#include', and we've already Index: clang/lib/Lex/PPLexerChange.cpp =================================================================== --- clang/lib/Lex/PPLexerChange.cpp +++ clang/lib/Lex/PPLexerChange.cpp @@ -103,6 +103,12 @@ } } + if (const FileEntry *FE = SourceMgr.getFileEntryForID(FID)) { + const auto &[I, Added] = ActiveIncludedFiles.insert(FE); + if (!Added) + TheLexer->setCircularInclude(); + } + EnterSourceFileWithLexer(TheLexer, CurDir); return false; } @@ -432,6 +438,11 @@ // lexing the #includer file. if (!IncludeMacroStack.empty()) { + if (CurPPLexer && !CurPPLexer->isCircularInclude()) + if (const FileEntry *FE = + SourceMgr.getFileEntryForID(CurPPLexer->getFileID())) + ActiveIncludedFiles.erase(FE); + // If we lexed the code-completion file, act as if we reached EOF. if (isCodeCompletionEnabled() && CurPPLexer && SourceMgr.getLocForStartOfFile(CurPPLexer->getFileID()) == Index: clang/test/Preprocessor/warn-loop-import-1.h =================================================================== --- /dev/null +++ clang/test/Preprocessor/warn-loop-import-1.h @@ -0,0 +1,2 @@ +// expected-warning@+1 {{#include cycle}} +#import "warn-loop-import-1.h" Index: clang/test/Preprocessor/warn-loop-macro-1.h =================================================================== --- /dev/null +++ clang/test/Preprocessor/warn-loop-macro-1.h @@ -0,0 +1,5 @@ +#ifndef LOOP_MACRO_1 +#define LOOP_MACRO_1 +// expected-warning@+1 {{#include cycle}} +#include "warn-loop-macro-1.h" +#endif Index: clang/test/Preprocessor/warn-loop-macro-2.h =================================================================== --- /dev/null +++ clang/test/Preprocessor/warn-loop-macro-2.h @@ -0,0 +1,6 @@ +#ifndef LOOP_MACRO_2 +#define LOOP_MACRO_2 +#include "warn-loop-macro-2a.h" +// expected-warning@+1 {{#include cycle}} +#include "warn-loop-macro-2.h" +#endif Index: clang/test/Preprocessor/warn-loop-macro-2a.h =================================================================== --- /dev/null +++ clang/test/Preprocessor/warn-loop-macro-2a.h @@ -0,0 +1,5 @@ +#ifndef LOOP_MACRO_2a +#define LOOP_MACRO_2a +// expected-warning@+1 {{#include cycle}} +#include "warn-loop-macro-2.h" +#endif Index: clang/test/Preprocessor/warn-loop-main.c =================================================================== --- /dev/null +++ clang/test/Preprocessor/warn-loop-main.c @@ -0,0 +1,7 @@ +// RUN: %clang_cc1 -Winclude-cycle -fsyntax-only -verify %s + +#include "warn-loop-macro-1.h" +#include "warn-loop-macro-2.h" +#include "warn-loop-macro-1.h" +#include "warn-loop-import-1.h" +#include "warn-loop-pragma-1.h" Index: clang/test/Preprocessor/warn-loop-pragma-1.h =================================================================== --- /dev/null +++ clang/test/Preprocessor/warn-loop-pragma-1.h @@ -0,0 +1,3 @@ +#pragma once +// expected-warning@+1 {{#include cycle}} +#include "warn-loop-pragma-1.h"