[clang] 14192ad - [clang][Sema] Fix ObjC file-scope literal diagnostic (#209688)

via cfe-commits cfe-commits at lists.llvm.org
Thu Jul 30 23:01:40 PDT 2026


Author: Peter Rong
Date: 2026-07-30T23:01:34-07:00
New Revision: 14192adddc39cb28b7bcb1e6d50dc6ef87f4d54b

URL: https://github.com/llvm/llvm-project/commit/14192adddc39cb28b7bcb1e6d50dc6ef87f4d54b
DIFF: https://github.com/llvm/llvm-project/commit/14192adddc39cb28b7bcb1e6d50dc6ef87f4d54b.diff

LOG: [clang][Sema] Fix ObjC file-scope literal diagnostic (#209688)

A non-constant ObjC collection literal used as a file-scope initializer
should get the targeted err_objc_literal_nonconstant_at_file_scope
pointing at the offending element. CheckForConstantInitializer instead
inspected the wrapped Init/Culprit, so under -fobjc-arc, for id-typed
variables, or with parentheses it skipped the per-element loop and fell
back to the generic "initializer element is not a compile-time constant"
on the whole @{...}/@[...] -- or blamed the wrong element (e.g. a valid
string key rather than the non-constant value).

Unwrap the culprit with IgnoreParenImpCasts and drive classification and
per-element reporting off it, mirroring BuildObjC{Array,Dictionary}Literal
(unwrap before each isa<>; keys must be string literals, values/elements
constant object literals). Also treat CK_ARCReclaimReturnedObject as
transparent in Expr::isConstantInitializer so ARC-wrapped literals are
recognized and the reported culprit is the literal itself.

Add objc-constant-literal-dict-key-restrictions.m (macOS no-ARC, iOS ARC,
ObjC++), with each culprit on its own line so -verify checks the caret.

[Assisted-by](https://t.ly/Dkjjk): Opus 4.8

Added: 
    clang/test/SemaObjC/objc-constant-literal-dict-key-restrictions.m

Modified: 
    clang/lib/AST/Expr.cpp
    clang/lib/Sema/SemaDecl.cpp

Removed: 
    


################################################################################
diff  --git a/clang/lib/AST/Expr.cpp b/clang/lib/AST/Expr.cpp
index 9a9a76e265f6a..5d7ee4710481c 100644
--- a/clang/lib/AST/Expr.cpp
+++ b/clang/lib/AST/Expr.cpp
@@ -3558,6 +3558,7 @@ bool Expr::isConstantInitializer(ASTContext &Ctx, bool IsForRef,
         CE->getCastKind() == CK_NonAtomicToAtomic ||
         CE->getCastKind() == CK_AtomicToNonAtomic ||
         CE->getCastKind() == CK_NullToPointer ||
+        CE->getCastKind() == CK_ARCReclaimReturnedObject ||
         CE->getCastKind() == CK_IntToOCLSampler)
       return CE->getSubExpr()->isConstantInitializer(Ctx, false, Culprit);
 

diff  --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp
index a553c6994f0ed..c46d2d780fad7 100644
--- a/clang/lib/Sema/SemaDecl.cpp
+++ b/clang/lib/Sema/SemaDecl.cpp
@@ -12936,14 +12936,31 @@ bool Sema::CheckForConstantInitializer(Expr *Init, unsigned DiagID) {
   if (Init->isConstantInitializer(Context, /*ForRef=*/false, &Culprit))
     return false;
 
-  // Emit ObjC-specific diagnostics for non-constant literals at file scope.
-  if (getLangOpts().ObjCConstantLiterals && isa<ObjCObjectLiteral>(Culprit)) {
+  // The culprit reported by isConstantInitializer() may be wrapped in implicit
+  // casts and parentheses that it does not look through: under ARC an
+  // object-pointer initializer is an `ImplicitCastExpr
+  // <ARCReclaimReturnedObject>`, an `id`-typed (or otherwise 
diff erently-typed)
+  // variable adds an `ImplicitCastExpr <BitCast>` on top, and a parenthesized
+  // initializer such as `(@{...})` adds a `ParenExpr`. Strip all of these so
+  // the ObjC-specific classification and per-element reporting below can see
+  // the underlying literal regardless of how it is wrapped.
+  const Expr *CulpritLiteral = Culprit->IgnoreParenImpCasts();
 
-    // For collection literals iterate the elements to highlight which one is
-    // the offender.
-    if (auto ALE = dyn_cast<ObjCArrayLiteral>(Init)) {
-      for (auto *Elm : ALE->elements()) {
-        if (!Elm->isConstantInitializer(Context)) {
+  // Emit ObjC-specific diagnostics for non-constant literals at file scope.
+  if (getLangOpts().ObjCConstantLiterals &&
+      isa<ObjCObjectLiteral>(CulpritLiteral)) {
+
+    // For collection literals, iterate the elements to point at the specific
+    // offender. These per-element checks mirror the constant-initializer rules
+    // applied when the literal was built (see SemaObjC::BuildObjCArrayLiteral
+    // and SemaObjC::BuildObjCDictionaryLiteral): each element must itself be a
+    // constant object literal, and dictionary keys must additionally be string
+    // literals. Elements, keys and values are wrapped in an implicit BitCast to
+    // `id`, so the isa<> classification is done on the unwrapped expression.
+    if (const auto *ALE = dyn_cast<ObjCArrayLiteral>(CulpritLiteral)) {
+      for (const Expr *Elm : ALE->elements()) {
+        if (!isa<ObjCObjectLiteral>(Elm->IgnoreImpCasts()) ||
+            !Elm->isConstantInitializer(Context)) {
           Diag(Elm->getExprLoc(),
                diag::err_objc_literal_nonconstant_at_file_scope)
               << ObjC().CheckLiteralKind(Init) << Elm->getSourceRange();
@@ -12952,12 +12969,12 @@ bool Sema::CheckForConstantInitializer(Expr *Init, unsigned DiagID) {
       }
     }
 
-    if (auto DLE = dyn_cast<ObjCDictionaryLiteral>(Init)) {
+    if (const auto *DLE = dyn_cast<ObjCDictionaryLiteral>(CulpritLiteral)) {
       for (size_t I = 0, N = DLE->getNumElements(); I != N; ++I) {
         const ObjCDictionaryElement Elm = DLE->getKeyValueElement(I);
 
-        // Check that the key is a string literal and is constant.
-        if (!isa<ObjCStringLiteral>(Elm.Key) ||
+        // Keys must be constant string literals.
+        if (!isa<ObjCStringLiteral>(Elm.Key->IgnoreImpCasts()) ||
             !Elm.Key->isConstantInitializer(Context)) {
           Diag(Elm.Key->getExprLoc(),
                diag::err_objc_literal_nonconstant_at_file_scope)
@@ -12965,7 +12982,9 @@ bool Sema::CheckForConstantInitializer(Expr *Init, unsigned DiagID) {
           return true;
         }
 
-        if (!Elm.Value->isConstantInitializer(Context)) {
+        // Values must be constant object literals.
+        if (!isa<ObjCObjectLiteral>(Elm.Value->IgnoreImpCasts()) ||
+            !Elm.Value->isConstantInitializer(Context)) {
           Diag(Elm.Value->getExprLoc(),
                diag::err_objc_literal_nonconstant_at_file_scope)
               << ObjC().CheckLiteralKind(Init) << Elm.Value->getSourceRange();
@@ -12974,9 +12993,9 @@ bool Sema::CheckForConstantInitializer(Expr *Init, unsigned DiagID) {
       }
     }
 
-    Diag(Culprit->getExprLoc(),
+    Diag(CulpritLiteral->getExprLoc(),
          diag::err_objc_literal_nonconstant_at_file_scope)
-        << ObjC().CheckLiteralKind(Init) << Culprit->getSourceRange();
+        << ObjC().CheckLiteralKind(Init) << CulpritLiteral->getSourceRange();
     return true;
   }
 

diff  --git a/clang/test/SemaObjC/objc-constant-literal-dict-key-restrictions.m b/clang/test/SemaObjC/objc-constant-literal-dict-key-restrictions.m
new file mode 100644
index 0000000000000..a00352fb48f27
--- /dev/null
+++ b/clang/test/SemaObjC/objc-constant-literal-dict-key-restrictions.m
@@ -0,0 +1,145 @@
+// RUN: %clang_cc1 -fsyntax-only -verify=objc \
+// RUN:   -triple x86_64-apple-macosx11.0.0 -fobjc-runtime=macosx-11.0.0 \
+// RUN:   -fobjc-constant-literals -fconstant-nsnumber-literals \
+// RUN:   -fconstant-nsarray-literals -fconstant-nsdictionary-literals %s
+
+// RUN: %clang_cc1 -fsyntax-only -verify=objc \
+// RUN:   -fobjc-arc \
+// RUN:   -triple arm64-apple-ios15.1.0-simulator -fobjc-runtime=ios-15.1.0 \
+// RUN:   -fobjc-constant-literals -fconstant-nsnumber-literals \
+// RUN:   -fconstant-nsarray-literals -fconstant-nsdictionary-literals %s
+
+// RUN: %clang_cc1 -fsyntax-only -verify=objcxx \
+// RUN:   -fobjc-arc -x objective-c++ \
+// RUN:   -triple arm64-apple-ios15.1.0-simulator -fobjc-runtime=ios-15.1.0 \
+// RUN:   -fobjc-constant-literals -fconstant-nsnumber-literals \
+// RUN:   -fconstant-nsarray-literals -fconstant-nsdictionary-literals %s
+
+// In C++ these file-scope initializers are dynamic initializers rather than
+// constant ones, so none of the diagnostics below apply.
+// objcxx-no-diagnostics
+
+#if __LP64__ || (TARGET_OS_EMBEDDED && !TARGET_OS_IPHONE) || TARGET_OS_WIN32 || NS_BUILD_32_LIKE_64
+typedef unsigned long NSUInteger;
+#else
+typedef unsigned int NSUInteger;
+#endif
+
+ at class NSString;
+
+ at interface NSNumber
++ (NSNumber *)numberWithInt:(int)value;
++ (NSNumber *)numberWithBool:(unsigned char)value;
+ at end
+
+ at interface NSArray
++ (id)arrayWithObjects:(const id[])objects count:(NSUInteger)cnt;
+ at end
+
+ at interface NSDictionary
++ (id)dictionaryWithObjects:(const id[])objects forKeys:(const id[])keys count:(NSUInteger)cnt;
+ at end
+
+int foo(void);
+
+static NSString *const someStringConstantVar = @"foo";
+
+// ---- Accepted: string-literal keys with constant literal values -----------
+// These are constant, so they must be diagnosed under neither triple. The `id`
+// case additionally wraps the literal in an implicit BitCast, which must not
+// stop it from being recognized as a constant literal.
+
+static NSDictionary *const dASCII = @{@"a" : @1, @"m" : @2, @"z" : @3};
+static NSDictionary *const dEmpty = @{};
+static NSArray *const aOK = @[@1, @2, @3];
+static id const dOKAsId = @{@"a" : @1};
+
+// ---- Rejected: non-string-literal key -------------------------------------
+// The error must point at the offending key, not the whole `@{...}` and not the
+// value. Splitting each element across lines lets -verify confirm the caret
+// lands on the key. This must hold with and without ARC.
+
+static NSDictionary *const dNumberKey = @{
+    @5 // objc-error {{its keys are string literals}}
+    : @1};
+
+static NSDictionary *const dBoxedKey = @{
+    @(1 + 2) // objc-error {{its keys are string literals}}
+    : @1};
+
+static NSDictionary *const dBoolKey = @{
+    @__objc_yes // objc-error {{its keys are string literals}}
+    : @1};
+
+// The offending key is the second entry; the first (valid) entry is not blamed.
+static NSDictionary *const dSecondKeyBad = @{
+    @"ok" : @1,
+    @2 // objc-error {{its keys are string literals}}
+    : @2};
+
+// A constant NSString variable is not a string *literal*, so it is rejected as
+// a key and the caret points at it.
+static NSDictionary *const dConstVarKey = @{
+    someStringConstantVar // objc-error {{its keys are string literals}}
+    : @1};
+
+// ---- Rejected: valid string key but non-constant value --------------------
+// The key is a valid string literal; the value is the culprit, so the caret
+// must point at the value and the key must not be blamed.
+
+static NSDictionary *const dValueBad = @{
+    @"a" :
+    @(foo())}; // objc-error {{its keys are string literals}}
+
+// Even when an earlier entry is fully valid, the caret points at the first bad
+// value rather than the whole literal or the good entry.
+static NSDictionary *const dMixedValueBad = @{
+    @"a" : @1,
+    @"b" :
+    @(foo())}; // objc-error {{its keys are string literals}}
+
+// A value that is constant but not itself a literal (a reference to another
+// constant) is likewise rejected, pointing at the value.
+static NSDictionary *const dNonLiteralValue = @{
+    @"a" :
+    someStringConstantVar}; // objc-error {{its keys are string literals}}
+
+// ---- Rejected: id-typed variable under ARC --------------------------------
+// Declaring the variable `id` wraps the literal in an implicit BitCast. The
+// diagnostic must still be the dictionary-specific one (not the generic C
+// "initializer element is not a compile-time constant") and must point at the
+// offending key.
+
+static id const dNumberKeyAsId = @{
+    @5 // objc-error {{its keys are string literals}}
+    : @1};
+
+// ---- Rejected: non-constant array element ---------------------------------
+// The array diagnostic points at the specific offending element.
+
+static NSArray *const aValueBad = @[
+    @1,
+    @(foo())]; // objc-error {{an array literal can only be used at file scope}}
+
+// ---- Rejected: parenthesized literal --------------------------------------
+// A parenthesized initializer such as `(@{...})` adds a `ParenExpr`; combined
+// with the implicit BitCast for an `id`-typed variable it would otherwise hide
+// the literal and fall back to the generic diagnostic. The specific-culprit
+// reporting must still fire through parentheses (including nested ones) for both
+// dictionary- and id-typed variables.
+
+static NSDictionary *const dParenValueBad = (@{
+    @"a" :
+    @(foo())}); // objc-error {{its keys are string literals}}
+
+static id const dParenIdValueBad = (@{
+    @"a" :
+    @(foo())}); // objc-error {{its keys are string literals}}
+
+static id const dNestedParenIdValueBad = ((@{
+    @"a" :
+    @(foo())})); // objc-error {{its keys are string literals}}
+
+static id const aParenIdValueBad = (@[
+    @1,
+    @(foo())]); // objc-error {{an array literal can only be used at file scope}}


        


More information about the cfe-commits mailing list