[clang] a5fa8c3 - [Clang] Reland "Diagnose UB and emit error when identifier has both internal and external linkage" (#193567)
via cfe-commits
cfe-commits at lists.llvm.org
Tue Aug 11 13:55:14 PDT 2026
Author: Aditya Medhane
Date: 2026-08-12T02:25:08+05:30
New Revision: a5fa8c34d9a07076a6cda4d2bcb85923787ce7f8
URL: https://github.com/llvm/llvm-project/commit/a5fa8c34d9a07076a6cda4d2bcb85923787ce7f8
DIFF: https://github.com/llvm/llvm-project/commit/a5fa8c34d9a07076a6cda4d2bcb85923787ce7f8.diff
LOG: [Clang] Reland "Diagnose UB and emit error when identifier has both internal and external linkage" (#193567)
C11 6.2.2p7 makes it undefined behavior for the same identifier to
appear with both internal and external linkage in a translation unit;
C2y N3410 makes this ill-formed. Clang now diagnoses it as an error in
all C language modes, noting the undefined behavior in pre-C2y modes.
Sema::MergeVarDecl uses LookupResult::isShadowed() to detect when a
block-scope extern declaration conflicts with an internal-linkage
declaration it reached through a shadowed lookup. C++ is unaffected:
a block-scope extern declaration targets the enclosing namespace scope
([dcl.meaning.general]/3.5, P1787R6) and inherits internal linkage, so
no conflict arises.
Fixes #54215
Added:
clang/test/Sema/linkage-internal-extern.cpp
Modified:
clang/docs/ReleaseNotes.md
clang/include/clang/Basic/DiagnosticSemaKinds.td
clang/lib/Sema/SemaDecl.cpp
clang/test/C/C2y/n3410.c
clang/www/c_status.html
Removed:
################################################################################
diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index 55fca8c9a6658..da0c20487b78e 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -164,6 +164,12 @@ features cannot lower the translation-unit ABI level;
}
```
+- Clang now diagnoses the use of the same identifier with both internal and
+ external linkage within a translation unit, as made ill-formed by
+ [N3410](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3410.pdf).
+ This is also diagnosed in older C language modes as the behavior was
+ undefined prior to C2y. (#GH54215)
+
#### C23 Feature Support
### Objective-C Language Changes
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index fc30db9f16887..b314c17ad27bd 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -6590,7 +6590,11 @@ def err_inline_decl_follows_def : Error<
def err_inline_declaration_block_scope : Error<
"inline declaration of %0 not allowed in block scope">;
def err_static_non_static : Error<
- "static declaration of %0 follows non-static declaration">;
+ "static declaration of %0 follows non-static declaration"
+ "%select{|; behavior is undefined}1">;
+def err_internal_extern_mismatch : Error<
+ "variable %0%select{| cannot be}1 declared with external linkage following "
+ "a declaration with internal linkage%select{; behavior is undefined|}1">;
def err_
diff erent_language_linkage : Error<
"declaration of %0 has a
diff erent language linkage">;
def ext_retained_language_linkage : Extension<
diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp
index c5dcdee7dc5dd..032737c7a191d 100644
--- a/clang/lib/Sema/SemaDecl.cpp
+++ b/clang/lib/Sema/SemaDecl.cpp
@@ -3800,7 +3800,8 @@ bool Sema::MergeFunctionDecl(FunctionDecl *New, NamedDecl *&OldD, Scope *S,
Diag(New->getLocation(), diag::ext_static_non_static) << New;
Diag(OldLocation, PrevDiag) << Old << Old->getType();
} else {
- Diag(New->getLocation(), diag::err_static_non_static) << New;
+ Diag(New->getLocation(), diag::err_static_non_static)
+ << New << /*MixedLinkageUB=*/false;
Diag(OldLocation, PrevDiag) << Old << Old->getType();
return true;
}
@@ -4821,12 +4822,37 @@ void Sema::MergeVarDecl(VarDecl *New, LookupResult &Previous) {
<< New->getDeclName();
Diag(OldLocation, PrevDiag);
} else {
+ // This is the same internal/external linkage conflict as C2y 6.7.1p7;
+ // before C2y it was undefined behavior (C11 6.2.2p7), so note that in
+ // the older C language modes.
Diag(New->getLocation(), diag::err_static_non_static)
- << New->getDeclName();
+ << New->getDeclName()
+ << (!getLangOpts().CPlusPlus && !getLangOpts().C2y);
Diag(OldLocation, PrevDiag);
return New->setInvalidDecl();
}
}
+
+ // C2y 6.7.1p7: an identifier shall not appear with both internal and
+ // external linkage within a translation unit. Before C2y this was UB
+ // (C11 6.2.2p7).
+ //
+ // In C, a local shadow prevents a block-scope extern from inheriting the
+ // file-scope static's internal linkage (C2y 6.2.2p6), so it defaults to
+ // external linkage, creating the conflict.
+ //
+ // In C++, block-scope extern declarations target the enclosing namespace
+ // scope ([dcl.meaning.general]/3.5), bypassing local shadows entirely, so
+ // the extern always inherits internal linkage. No conflict arises.
+ if (!getLangOpts().CPlusPlus && New->isLocalVarDecl() &&
+ New->hasExternalStorage() && Previous.isShadowed() &&
+ Old->getFormalLinkage() == Linkage::Internal) {
+ Diag(New->getLocation(), diag::err_internal_extern_mismatch)
+ << New->getDeclName() << getLangOpts().C2y;
+ Diag(OldLocation, diag::note_previous_declaration);
+ return New->setInvalidDecl();
+ }
+
// C99 6.2.2p4:
// For an identifier declared with the storage-class specifier
// extern in a scope in which a prior declaration of that
diff --git a/clang/test/C/C2y/n3410.c b/clang/test/C/C2y/n3410.c
index e1cb41f375b82..60317bf1d3833 100644
--- a/clang/test/C/C2y/n3410.c
+++ b/clang/test/C/C2y/n3410.c
@@ -1,45 +1,128 @@
-// RUN: %clang_cc1 -verify -std=c2y -Wall -pedantic -Wno-unused %s
+// RUN: %clang_cc1 -verify-directives -verify=expected,c2y -std=c2y -Wall -pedantic -Wno-unused %s
+// RUN: %clang_cc1 -verify-directives -verify=expected,c89-23 -std=c23 -Wall -pedantic -Wno-unused %s
+// RUN: %clang_cc1 -verify-directives -verify=expected,c89-23 -std=c17 -Wall -pedantic -Wno-unused %s
+// RUN: %clang_cc1 -verify-directives -verify=expected,c89-23 -std=c11 -Wall -pedantic -Wno-unused %s
+// RUN: %clang_cc1 -verify-directives -verify=expected,c89-23 -std=c99 -Wall -pedantic -Wno-unused %s
+// RUN: %clang_cc1 -verify-directives -verify=expected,c89-23 -std=c89 -Wall -pedantic -Wno-unused -Wno-comment %s
-/* WG14 N3410: No
+/* WG14 N3410: Clang 24
* Slay Some Earthly Demons XI
*
* It is now ill-formed for the same identifier within a TU to have both
* internal and external linkage.
*/
-void func1() {
- extern int a; // #a
+void func1(void) {
+ extern int a; /* #a */
}
-// This 'a' is the same as the one declared extern above.
-static int a; /* expected-error {{static declaration of 'a' follows non-static declaration}}
+/* This 'a' is the same as the one declared extern above. */
+static int a; /* c2y-error {{static declaration of 'a' follows non-static declaration}}
+ c89-23-error {{static declaration of 'a' follows non-static declaration; behavior is undefined}}
expected-note@#a {{previous declaration is here}}
*/
static int b;
-void func2() {
- // This 'b' is the same as the one declaraed static above, but this is not
- // ill-formed because of C2y 6.2.2p4, which gives this variable internal
- // linkage because the previous declaration had internal linkage.
- extern int b; // Ok
+void func2(void) {
+ /* This 'b' is well-formed, because C2y 6.2.2p6 makes it "inherit" the
+ static linkage of `static int b` above, because the latter is visible.
+ */
+ extern int b; /* Ok */
}
-static int c, d;
-void func3() {
- int c; // no linkage,
diff erent object from the one declared above.
- for (int d;;) {
- // This 'c' is the same as the one declared at file scope, but because of
- // the local scope 'c', the file scope 'c' is not visible.
- // FIXME: This should be diagnosed under N3410.
- extern int c;
- // This 'd' is the same as the one declared at file scope as well, but
- // because of the 'd' declared within the for loop, the file scope 'd' is
- // also not visible, same as with 'c'.
- // FIXME: This should be diagnosed under N3410.
- extern int d;
+static int c, d; /* #c_d */
+void func3(void) {
+ int c; /* no linkage,
diff erent object from the one declared above. */
+ {
+ int d; /* no linkage,
diff erent object from the file-scope 'd'. */
+ {
+ /* This 'c' is the same as the one declared at file scope, but because
+ of the local scope 'c', the file scope 'c' is not visible. */
+ extern int c; /* c2y-error {{variable 'c' cannot be declared with external linkage following a declaration with internal linkage}}
+ c89-23-error {{variable 'c' declared with external linkage following a declaration with internal linkage; behavior is undefined}}
+ expected-note@#c_d {{previous declaration is here}}
+ */
+ /* This 'd' is the same as the one declared at file scope as well, but
+ because of the enclosing block-scope 'd', the file scope 'd' is also
+ not visible, same as with 'c'. */
+ extern int d; /* c2y-error {{variable 'd' cannot be declared with external linkage following a declaration with internal linkage}}
+ c89-23-error {{variable 'd' declared with external linkage following a declaration with internal linkage; behavior is undefined}}
+ expected-note@#c_d {{previous declaration is here}}
+ */
+ }
}
- for (static int e;;) {
- extern int e; // Ok for the same reason as 'b' above.
+ {
+ static int e;
+ {
+ extern int e; /* Ok for the same reason as 'b' above. */
+ }
}
}
+/* A function parameter shadows the file-scope 'p' the same way a local
+ variable does, so the block-scope 'extern' does not inherit internal
+ linkage and conflicts. */
+static int p; /* #p */
+void func4(int p) {
+ {
+ extern int p; /* c2y-error {{variable 'p' cannot be declared with external linkage following a declaration with internal linkage}}
+ c89-23-error {{variable 'p' declared with external linkage following a declaration with internal linkage; behavior is undefined}}
+ expected-note@#p {{previous declaration is here}}
+ */
+ }
+}
+
+static int q;
+void func5(void) {
+ /* No shadow intervenes here, so this 'q' inherits the internal linkage of
+ the file-scope 'q', which is fine. */
+ extern int q; /* #q */
+ {
+ int q; /* no linkage; shadows the declarations above. */
+ {
+ /* The file-scope 'q' is now hidden, so this 'extern' has external
+ linkage and conflicts with the internal-linkage declaration above. */
+ extern int q; /* c2y-error {{variable 'q' cannot be declared with external linkage following a declaration with internal linkage}}
+ c89-23-error {{variable 'q' declared with external linkage following a declaration with internal linkage; behavior is undefined}}
+ expected-note@#q {{previous declaration is here}}
+ */
+ }
+ }
+}
+
+void func6(void) {
+ /* No file-scope declaration of 'r' exists, so the block-scope 'extern' just
+ has external linkage and there is no conflict. */
+ {
+ int r; /* no linkage. */
+ {
+ extern int r; /* Ok */
+ }
+ }
+}
+
+static int s; /* #s */
+void func7(void) {
+ {
+ /* The file-scope 's' is visible here, so this 'extern' inherits its
+ internal linkage, which may be surprising. */
+ extern int s; /* Ok */
+ }
+ {
+ int s; /* no linkage; shadows the file-scope 's'. */
+ {
+ /* The file-scope 's' is hidden by the local 's' above, so this 'extern'
+ has external linkage and conflicts.
+
+ This tests that we do not accidentally note the internal linkage
+ declaration using the 'extern' specifier in the function scope; we
+ want the note to point to the declaration using the 'static'
+ specifier at global scope because the function scope identifier is
+ hidden at this point. */
+ extern int s; /* c2y-error {{variable 's' cannot be declared with external linkage following a declaration with internal linkage}}
+ c89-23-error {{variable 's' declared with external linkage following a declaration with internal linkage; behavior is undefined}}
+ expected-note@#s {{previous declaration is here}}
+ */
+ }
+ }
+}
diff --git a/clang/test/Sema/linkage-internal-extern.cpp b/clang/test/Sema/linkage-internal-extern.cpp
new file mode 100644
index 0000000000000..deace77566db0
--- /dev/null
+++ b/clang/test/Sema/linkage-internal-extern.cpp
@@ -0,0 +1,41 @@
+// RUN: %clang_cc1 -ast-dump %s | FileCheck %s
+
+// In C++, block-scope extern declarations target the enclosing namespace
+// scope ([dcl.meaning.general]/3.5), so they match against the namespace-scope
+// static despite local shadows and inherit internal linkage. No conflict arises.
+//
+// This
diff ers from C, where a local shadow breaks linkage inheritance,
+// causing the conflict diagnosed by err_internal_extern_mismatch.
+
+// Example adapted from [basic.link]/6.
+static void f();
+// CHECK: FunctionDecl {{.*}} f 'void ()' static internal-linkage
+static int i = 0;
+// CHECK: VarDecl {{.*}} i 'int' static cinit internal-linkage
+void g() {
+// CHECK: FunctionDecl {{.*}} g 'void ()' external-linkage
+ extern void f();
+ // CHECK: FunctionDecl {{.*}} prev {{.*}} f 'void ()' extern internal-linkage
+ int i;
+ // CHECK: VarDecl {{.*}} i 'int'{{$}}
+ {
+ extern void f();
+ // CHECK: FunctionDecl {{.*}} prev {{.*}} f 'void ()' extern internal-linkage
+ extern int i;
+ // CHECK: VarDecl {{.*}} prev {{.*}} i 'int' extern internal-linkage
+ }
+}
+
+// Block-scope function declarations behave identically without extern
+// (C11 6.2.2p5, C++ [dcl.meaning.general]/3.5).
+static void h();
+// CHECK: FunctionDecl {{.*}} h 'void ()' static internal-linkage
+void g2() {
+// CHECK: FunctionDecl {{.*}} g2 'void ()' external-linkage
+ int h;
+ // CHECK: VarDecl {{.*}} h 'int'{{$}}
+ {
+ void h();
+ // CHECK: FunctionDecl {{.*}} prev {{.*}} h 'void ()' internal-linkage
+ }
+}
diff --git a/clang/www/c_status.html b/clang/www/c_status.html
index f478a56857fa3..6984de2bd1480 100644
--- a/clang/www/c_status.html
+++ b/clang/www/c_status.html
@@ -283,7 +283,7 @@ <h2 id="c2y">C2y implementation status</h2>
<tr>
<td>Slay Some Earthly Demons XI</td>
<td><a href="https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3410.pdf">N3410</a></td>
- <td class="none" align="center">No</td>
+ <td class="unreleased" align="center">Clang 24</td>
</tr>
<tr>
<td>Slay Some Earthly Demons XII</td>
More information about the cfe-commits
mailing list