[clang] [clang][Modules] Respect case sensitivity when hashing implicit module map path (PR #226339)

Cyndy Ishida via cfe-commits cfe-commits at lists.llvm.org
Fri Sep 25 09:51:05 PDT 2026


https://github.com/cyndyishida updated https://github.com/llvm/llvm-project/pull/226339

>From 551f45047d34085ccd5e543a7ce21fa7b22c3e6d Mon Sep 17 00:00:00 2001
From: Cyndy Ishida <cyndy_ishida at apple.com>
Date: Thu, 24 Sep 2026 18:36:12 -0700
Subject: [PATCH 1/2] [clang][Modules] Respect case sensitivity when hashing
 module map path

Before this patch, the implicit module hash normalized the module map
path, however explicit modules always respects the case in paths. This
worked by depending on FileEntry caching & relocation checking to
resolve correctly. This behavior was both subtle and difficult to understand. Instead,
respect the casing in paths for hashing to match explicitly built
modules so they can never diverage.

This patch also updates `modules-context-hash-module-map-path.c` to
match a downstream version that already tested for the case where vfs
overlay enteries differ by case.
---
 clang/lib/Lex/HeaderSearch.cpp                |   5 +-
 ...es-context-hash-case-sensitive-vfs-paths.c | 130 ++++++++++++++++++
 .../modules-context-hash-module-map-path.c    |  86 +++++++++---
 3 files changed, 201 insertions(+), 20 deletions(-)
 create mode 100644 clang/test/ClangScanDeps/modules-context-hash-case-sensitive-vfs-paths.c

diff --git a/clang/lib/Lex/HeaderSearch.cpp b/clang/lib/Lex/HeaderSearch.cpp
index 824ed8936621a..243604c3b2da0 100644
--- a/clang/lib/Lex/HeaderSearch.cpp
+++ b/clang/lib/Lex/HeaderSearch.cpp
@@ -283,13 +283,12 @@ ModuleFileName HeaderSearch::getCachedModuleFileNameImpl(
     // in the hash are safe (because any translation unit can only import one
     // module with each name), but result in a loss of caching.
     //
-    // To avoid false-negatives, we form as canonical a path as we can, and map
-    // to lower-case in case we're on a case-insensitive file system.
+    // To avoid false-negatives, we form as canonical a path as we can.
     SmallString<128> CanonicalPath(ModuleMapPath);
     if (getModuleMap().canonicalizeModuleMapPath(CanonicalPath))
       return {};
 
-    auto Hash = llvm::xxh3_64bits(CanonicalPath.str().lower());
+    auto Hash = llvm::xxh3_64bits(CanonicalPath.str());
 
     SmallString<128> HashStr;
     llvm::APInt(64, Hash).toStringUnsigned(HashStr, /*Radix*/36);
diff --git a/clang/test/ClangScanDeps/modules-context-hash-case-sensitive-vfs-paths.c b/clang/test/ClangScanDeps/modules-context-hash-case-sensitive-vfs-paths.c
new file mode 100644
index 0000000000000..431595fed26ca
--- /dev/null
+++ b/clang/test/ClangScanDeps/modules-context-hash-case-sensitive-vfs-paths.c
@@ -0,0 +1,130 @@
+// Ensure the path to the modulemap input is included in the context hash.
+// This test uses different vfs overlay entries that only differ by case, 
+// but to point to different input paths.
+
+// RUN: rm -rf %t
+// RUN: split-file %s %t
+// RUN: sed "s|DIR|%/t|g" %t/cdb0.json.template > %t/cdb0.json
+// RUN: sed "s|DIR|%/t|g" %t/cdb1.json.template > %t/cdb1.json
+// RUN: sed "s|DIR|%/t|g" %t/overlay.json.template > %t/overlay.json
+// RUN: mkdir -p %t/foo
+// RUN: cp %t/Mod.h %t/foo/
+// RUN: cp %t/m.m %t/foo/
+
+// RUN: clang-scan-deps -compilation-database %t/cdb0.json -j 1 \
+// RUN:   -format experimental-full > %t/deps.json
+
+// RUN: echo 'DIFFERENT_PATH' >> %t/deps.json
+
+// RUN: clang-scan-deps -compilation-database %t/cdb1.json -j 1 \
+// RUN:   -format experimental-full >> %t/deps.json
+
+// RUN: cat %t/deps.json | sed 's:\\\\\?:/:g' | FileCheck -DPREFIX=%/t %s
+
+
+// CHECK:      {
+// CHECK-NEXT:   "modules": [
+// CHECK:          {
+// CHECK:            "command-line": [
+// CHECK:              "{{.*}}Dir/module.modulemap"
+// CHECK:            ]
+// CHECK:            "context-hash": "[[HASH1:.*]]"
+// CHECK:            "name": "Mod"
+// CHECK-NEXT:     }
+// CHECK-NEXT:   ]
+// CHECK-NEXT:   "translation-units": [
+// CHECK-NEXT:     {
+// CHECK:            "clang-module-deps": [
+// CHECK-NEXT:         {
+// CHECK-NEXT:           "context-hash": "[[HASH1]]"
+// CHECK-NEXT:            "module-name": "Mod"
+// CHECK-NEXT:         }
+// CHECK-NEXT:       ]
+// CHECK-LABEL: DIFFERENT_PATH
+// CHECK:      {
+// CHECK-NEXT:   "modules": [
+// CHECK:          {
+// CHECK-NOT: [[HASH1]]
+// CHECK:            "command-line": [
+// CHECK:              "{{.*}}dir/module.modulemap"
+// CHECK:            ]
+// CHECK-NOT: [[HASH1]]
+// CHECK:            "name": "Mod"
+// CHECK-NEXT:     }
+// CHECK-NEXT:   ]
+// CHECK-NEXT:   "translation-units": [
+// CHECK-NEXT:     {
+// CHECK:            "clang-module-deps": [
+// CHECK-NEXT:         {
+// CHECK-NEXT:           "context-hash":
+// CHECK-NOT: [[HASH1]]
+// CHECK-NEXT:            "module-name": "Mod"
+// CHECK-NEXT:         }
+// CHECK-NEXT:       ]
+
+//--- overlay.json.template
+{
+  "version": 0,
+  "case-sensitive": true,
+  "roots": [
+  {
+     "contents": [
+     {
+        "external-contents": "DIR/m.m",
+        "name": "module.modulemap",
+        "type": "file"
+     },
+     {
+        "external-contents": "DIR/Mod.h",
+        "name": "Mod.h",
+        "type": "file"
+     }],
+     "name": "DIR/Dir",
+     "type": "directory"
+  },
+  {
+     "contents": [
+     {
+        "external-contents": "DIR/foo/m.m",
+        "name": "module.modulemap",
+        "type": "file"
+     },
+     {
+        "external-contents": "DIR/foo/Mod.h",
+        "name": "Mod.h",
+        "type": "file"
+     }],
+     "name": "DIR/dir",
+     "type": "directory"
+  }
+  ]
+}
+
+//--- cdb0.json.template
+[
+  {
+    "directory": "DIR",
+    "command": "clang -fsyntax-only -Idir -IDir DIR/tu0.c -fmodules -fimplicit-module-maps -fmodules-cache-path=DIR/cache -ivfsoverlay overlay.json",
+    "file": "DIR/tu0.c"
+  }
+]
+
+//--- cdb1.json.template
+[
+  {
+    "directory": "DIR",
+    "command": "clang -fsyntax-only -Idir -IDir DIR/tu1.c -fmodules -fimplicit-module-maps -fmodules-cache-path=DIR/cache -ivfsoverlay overlay.json",
+    "file": "DIR/tu1.c"
+  }
+]
+
+//--- m.m
+module Mod { header "Mod.h" }
+
+//--- Mod.h
+
+//--- tu0.c
+#include "Dir/Mod.h"
+
+//--- tu1.c
+#include "dir/Mod.h"
diff --git a/clang/test/ClangScanDeps/modules-context-hash-module-map-path.c b/clang/test/ClangScanDeps/modules-context-hash-module-map-path.c
index 066bcdf8f93b7..69bcc9f6b42a3 100644
--- a/clang/test/ClangScanDeps/modules-context-hash-module-map-path.c
+++ b/clang/test/ClangScanDeps/modules-context-hash-module-map-path.c
@@ -1,20 +1,19 @@
 // Ensure the path to the modulemap input is included in the context hash
 // irrespective of other TU command-line arguments, as it effects the canonical
-// module build command. In this test we use the difference in spelling between
-// module.modulemap and module.map, but it also applies to situations such as
-// differences in case-insensitive paths if they are not canonicalized away.
+// module build command. In this test we use the difference in case of a path.
 
 // RUN: rm -rf %t
 // RUN: split-file %s %t
-// RUN: sed "s|DIR|%/t|g" %t/cdb.json.template > %t/cdb.json
+// RUN: sed "s|DIR|%/t|g" %t/cdb0.json.template > %t/cdb0.json
+// RUN: sed "s|DIR|%/t|g" %t/cdb1.json.template > %t/cdb1.json
+// RUN: sed "s|DIR|%/t|g" %t/overlay.json.template > %t/overlay.json
 
-// RUN: clang-scan-deps -compilation-database %t/cdb.json -j 1 \
+// RUN: clang-scan-deps -compilation-database %t/cdb0.json -j 1 \
 // RUN:   -format experimental-full > %t/deps.json
 
-// RUN: mv %t/module.modulemap %t/module.map
-// RUN: echo 'AFTER_MOVE' >> %t/deps.json
+// RUN: echo 'DIFFERENT_PATH' >> %t/deps.json
 
-// RUN: clang-scan-deps -compilation-database %t/cdb.json -j 1 \
+// RUN: clang-scan-deps -compilation-database %t/cdb1.json -j 1 \
 // RUN:   -format experimental-full >> %t/deps.json
 
 // RUN: cat %t/deps.json | sed 's:\\\\\?:/:g' | FileCheck -DPREFIX=%/t %s
@@ -23,7 +22,7 @@
 // CHECK-NEXT:   "modules": [
 // CHECK:          {
 // CHECK:            "command-line": [
-// CHECK:              "{{.*}}module.modulemap"
+// CHECK:              "{{.*}}Dir/module.modulemap"
 // CHECK:            ]
 // CHECK:            "context-hash": "[[HASH1:.*]]"
 // CHECK:            "name": "Mod"
@@ -37,13 +36,13 @@
 // CHECK-NEXT:            "module-name": "Mod"
 // CHECK-NEXT:         }
 // CHECK-NEXT:       ]
-// CHECK-LABEL: AFTER_MOVE
+// CHECK-LABEL: DIFFERENT_PATH
 // CHECK:      {
 // CHECK-NEXT:   "modules": [
 // CHECK:          {
 // CHECK-NOT: [[HASH1]]
 // CHECK:            "command-line": [
-// CHECK:              "{{.*}}module.map"
+// CHECK:              "{{.*}}dir/module.modulemap"
 // CHECK:            ]
 // CHECK-NOT: [[HASH1]]
 // CHECK:            "name": "Mod"
@@ -59,19 +58,72 @@
 // CHECK-NEXT:         }
 // CHECK-NEXT:       ]
 
-//--- cdb.json.template
+// This overlay is used just to force the filesystem to be case sensitive to
+// simulate case insensitive access via diffierent capitalizations.
+//--- overlay.json.template
+
+{
+  "version": 0,
+  "case-sensitive": true,
+  "roots": [
+  {
+     "contents": [
+     {
+        "external-contents": "DIR/m.m",
+        "name": "module.modulemap",
+        "type": "file"
+     },
+     {
+        "external-contents": "DIR/Mod.h",
+        "name": "Mod.h",
+        "type": "file"
+     }],
+     "name": "DIR/Dir",
+     "type": "directory"
+  },
+  {
+     "contents": [
+     {
+        "external-contents": "DIR/m.m",
+        "name": "module.modulemap",
+        "type": "file"
+     },
+     {
+        "external-contents": "DIR/Mod.h",
+        "name": "Mod.h",
+        "type": "file"
+     }],
+     "name": "DIR/dir",
+     "type": "directory"
+  }
+  ]
+}
+
+//--- cdb0.json.template
+[
+  {
+    "directory": "DIR",
+    "command": "clang -fsyntax-only -Idir -IDir DIR/tu0.c -fmodules -fimplicit-module-maps -fmodules-cache-path=DIR/cache -ivfsoverlay overlay.json",
+    "file": "DIR/tu0.c"
+  }
+]
+
+//--- cdb1.json.template
 [
   {
     "directory": "DIR",
-    "command": "clang -fsyntax-only DIR/tu.c -fmodules -fimplicit-module-maps -fmodules-cache-path=DIR/cache",
-    "file": "DIR/tu.c"
+    "command": "clang -fsyntax-only -Idir -IDir DIR/tu1.c -fmodules -fimplicit-module-maps -fmodules-cache-path=DIR/cache -ivfsoverlay overlay.json",
+    "file": "DIR/tu1.c"
   }
 ]
 
-//--- module.modulemap
+//--- m.m
 module Mod { header "Mod.h" }
 
 //--- Mod.h
 
-//--- tu.c
-#include "Mod.h"
+//--- tu0.c
+#include "Dir/Mod.h"
+
+//--- tu1.c
+#include "dir/Mod.h"

>From f3bba61d627f7569be4dfc61d3c06149afed11a8 Mon Sep 17 00:00:00 2001
From: Cyndy Ishida <cyndy_ishida at apple.com>
Date: Fri, 25 Sep 2026 09:23:40 -0700
Subject: [PATCH 2/2] Address review comments

---
 clang/lib/Lex/HeaderSearch.cpp                |  2 +-
 ...es-context-hash-case-sensitive-vfs-paths.c | 32 +++------
 .../modules-context-hash-module-map-path.c    | 32 +++------
 .../implicit-module-map-path-case-sensitive.c | 70 +++++++++++++++++++
 4 files changed, 87 insertions(+), 49 deletions(-)
 create mode 100644 clang/test/Modules/implicit-module-map-path-case-sensitive.c

diff --git a/clang/lib/Lex/HeaderSearch.cpp b/clang/lib/Lex/HeaderSearch.cpp
index 243604c3b2da0..906ce07b96a79 100644
--- a/clang/lib/Lex/HeaderSearch.cpp
+++ b/clang/lib/Lex/HeaderSearch.cpp
@@ -288,7 +288,7 @@ ModuleFileName HeaderSearch::getCachedModuleFileNameImpl(
     if (getModuleMap().canonicalizeModuleMapPath(CanonicalPath))
       return {};
 
-    auto Hash = llvm::xxh3_64bits(CanonicalPath.str());
+    auto Hash = llvm::xxh3_64bits(CanonicalPath);
 
     SmallString<128> HashStr;
     llvm::APInt(64, Hash).toStringUnsigned(HashStr, /*Radix*/36);
diff --git a/clang/test/ClangScanDeps/modules-context-hash-case-sensitive-vfs-paths.c b/clang/test/ClangScanDeps/modules-context-hash-case-sensitive-vfs-paths.c
index 431595fed26ca..42a8a76e78dc8 100644
--- a/clang/test/ClangScanDeps/modules-context-hash-case-sensitive-vfs-paths.c
+++ b/clang/test/ClangScanDeps/modules-context-hash-case-sensitive-vfs-paths.c
@@ -4,20 +4,22 @@
 
 // RUN: rm -rf %t
 // RUN: split-file %s %t
-// RUN: sed "s|DIR|%/t|g" %t/cdb0.json.template > %t/cdb0.json
-// RUN: sed "s|DIR|%/t|g" %t/cdb1.json.template > %t/cdb1.json
 // RUN: sed "s|DIR|%/t|g" %t/overlay.json.template > %t/overlay.json
 // RUN: mkdir -p %t/foo
 // RUN: cp %t/Mod.h %t/foo/
 // RUN: cp %t/m.m %t/foo/
 
-// RUN: clang-scan-deps -compilation-database %t/cdb0.json -j 1 \
-// RUN:   -format experimental-full > %t/deps.json
+// RUN: clang-scan-deps -format experimental-full -- \
+// RUN:   %clang -I %t/dir -I %t/Dir -c %t/tu0.c -ivfsoverlay %t/overlay.json \
+// RUN:     -fmodules -fimplicit-module-maps -fmodules-cache-path=%t/cache \
+// RUN:   > %t/deps.json
 
 // RUN: echo 'DIFFERENT_PATH' >> %t/deps.json
 
-// RUN: clang-scan-deps -compilation-database %t/cdb1.json -j 1 \
-// RUN:   -format experimental-full >> %t/deps.json
+// RUN: clang-scan-deps -format experimental-full -- \
+// RUN:   %clang -I %t/dir -I %t/Dir -c %t/tu1.c -ivfsoverlay %t/overlay.json \
+// RUN:     -fmodules -fimplicit-module-maps -fmodules-cache-path=%t/cache \
+// RUN:   >> %t/deps.json
 
 // RUN: cat %t/deps.json | sed 's:\\\\\?:/:g' | FileCheck -DPREFIX=%/t %s
 
@@ -100,24 +102,6 @@
   ]
 }
 
-//--- cdb0.json.template
-[
-  {
-    "directory": "DIR",
-    "command": "clang -fsyntax-only -Idir -IDir DIR/tu0.c -fmodules -fimplicit-module-maps -fmodules-cache-path=DIR/cache -ivfsoverlay overlay.json",
-    "file": "DIR/tu0.c"
-  }
-]
-
-//--- cdb1.json.template
-[
-  {
-    "directory": "DIR",
-    "command": "clang -fsyntax-only -Idir -IDir DIR/tu1.c -fmodules -fimplicit-module-maps -fmodules-cache-path=DIR/cache -ivfsoverlay overlay.json",
-    "file": "DIR/tu1.c"
-  }
-]
-
 //--- m.m
 module Mod { header "Mod.h" }
 
diff --git a/clang/test/ClangScanDeps/modules-context-hash-module-map-path.c b/clang/test/ClangScanDeps/modules-context-hash-module-map-path.c
index 69bcc9f6b42a3..01a9dd3acbd03 100644
--- a/clang/test/ClangScanDeps/modules-context-hash-module-map-path.c
+++ b/clang/test/ClangScanDeps/modules-context-hash-module-map-path.c
@@ -4,17 +4,19 @@
 
 // RUN: rm -rf %t
 // RUN: split-file %s %t
-// RUN: sed "s|DIR|%/t|g" %t/cdb0.json.template > %t/cdb0.json
-// RUN: sed "s|DIR|%/t|g" %t/cdb1.json.template > %t/cdb1.json
 // RUN: sed "s|DIR|%/t|g" %t/overlay.json.template > %t/overlay.json
 
-// RUN: clang-scan-deps -compilation-database %t/cdb0.json -j 1 \
-// RUN:   -format experimental-full > %t/deps.json
+// RUN: clang-scan-deps -format experimental-full -- \
+// RUN:   %clang -I %t/dir -I %t/Dir -c %t/tu0.c -ivfsoverlay %t/overlay.json \
+// RUN:     -fmodules -fimplicit-module-maps -fmodules-cache-path=%t/cache \
+// RUN:   > %t/deps.json
 
 // RUN: echo 'DIFFERENT_PATH' >> %t/deps.json
 
-// RUN: clang-scan-deps -compilation-database %t/cdb1.json -j 1 \
-// RUN:   -format experimental-full >> %t/deps.json
+// RUN: clang-scan-deps -format experimental-full -- \
+// RUN:   %clang -I %t/dir -I %t/Dir -c %t/tu1.c -ivfsoverlay %t/overlay.json \
+// RUN:     -fmodules -fimplicit-module-maps -fmodules-cache-path=%t/cache \
+// RUN:   >> %t/deps.json
 
 // RUN: cat %t/deps.json | sed 's:\\\\\?:/:g' | FileCheck -DPREFIX=%/t %s
 
@@ -99,24 +101,6 @@
   ]
 }
 
-//--- cdb0.json.template
-[
-  {
-    "directory": "DIR",
-    "command": "clang -fsyntax-only -Idir -IDir DIR/tu0.c -fmodules -fimplicit-module-maps -fmodules-cache-path=DIR/cache -ivfsoverlay overlay.json",
-    "file": "DIR/tu0.c"
-  }
-]
-
-//--- cdb1.json.template
-[
-  {
-    "directory": "DIR",
-    "command": "clang -fsyntax-only -Idir -IDir DIR/tu1.c -fmodules -fimplicit-module-maps -fmodules-cache-path=DIR/cache -ivfsoverlay overlay.json",
-    "file": "DIR/tu1.c"
-  }
-]
-
 //--- m.m
 module Mod { header "Mod.h" }
 
diff --git a/clang/test/Modules/implicit-module-map-path-case-sensitive.c b/clang/test/Modules/implicit-module-map-path-case-sensitive.c
new file mode 100644
index 0000000000000..998dd615bb4ba
--- /dev/null
+++ b/clang/test/Modules/implicit-module-map-path-case-sensitive.c
@@ -0,0 +1,70 @@
+// Ensure that module variant hashes in the PCM paths differ when module map paths 
+// differ by case. This relies on the canonical path of each
+// module map resolving to the path specified by the case-sensitive VFS
+// overlay, which is not dependent on underlying file system's resolution.
+
+// RUN: rm -rf %t
+// RUN: split-file %s %t
+// RUN: sed "s|DIR|%/t|g" %t/overlay.json.template > %t/overlay.json
+
+// RUN: %clang_cc1 -fsyntax-only %t/tu0.c -ivfsoverlay %t/overlay.json \
+// RUN:   -fmodules -fimplicit-module-maps -fmodules-cache-path=%t/cache \
+// RUN:   -Rmodule-build 2> %t/remarks_tu0.txt
+// RUN: find %t/cache -name "Mod-*.pcm" | count 1
+
+// RUN: %clang_cc1 -fsyntax-only %t/tu1.c -ivfsoverlay %t/overlay.json \
+// RUN:   -fmodules -fimplicit-module-maps -fmodules-cache-path=%t/cache \
+// RUN:   -Rmodule-build 2> %t/remarks_tu1.txt
+// RUN: cat %t/remarks_tu0.txt %t/remarks_tu1.txt | FileCheck %s
+// RUN: find %t/cache -name "Mod-*.pcm" | count 2
+
+// CHECK:     tu0.c:{{.*}} remark: building module 'Mod' as '{{.*}}Mod-[[HASH:[A-Z0-9]+]].pcm'
+// CHECK-NOT: Mod-[[HASH]].pcm
+
+//--- overlay.json.template
+{
+  "version": 0,
+  "case-sensitive": true,
+  "roots": [
+  {
+     "contents": [
+     {
+        "external-contents": "DIR/real/module.modulemap",
+        "name": "module.modulemap",
+        "type": "file"
+     },
+     {
+        "external-contents": "DIR/real/Mod.h",
+        "name": "Mod.h",
+        "type": "file"
+     }],
+     "name": "DIR/Dir",
+     "type": "directory"
+  },
+  {
+     "contents": [
+     {
+        "external-contents": "DIR/real/module.modulemap",
+        "name": "module.modulemap",
+        "type": "file"
+     },
+     {
+        "external-contents": "DIR/real/Mod.h",
+        "name": "Mod.h",
+        "type": "file"
+     }],
+     "name": "DIR/dir",
+     "type": "directory"
+  }
+  ]
+}
+
+//--- real/module.modulemap
+module Mod { header "Mod.h" }
+//--- real/Mod.h
+
+//--- tu0.c
+#include "Dir/Mod.h"
+
+//--- tu1.c
+#include "dir/Mod.h"



More information about the cfe-commits mailing list