Skip to content

Commit 92b1d9c

Browse files
committed
fix(scanner): the identity refusal quotes what was read, not what was recorded
The tokeniser stops at the first character it does not accept, so a declaration it cannot read at all yields an EMPTY name -- and `'' is not a module name` names nothing the author can find. Measured on a non-ASCII name, where `is_module_name_char` tests bytes with std::isalnum and every byte of a UTF-8 sequence is false: the message read `''` where the source said `模块`. GCC 16.1 refuses that declaration too (`unrecognized 'MODULE-EXPORT ...'`), so this is a clearer sentence for a refusal that already existed rather than a new restriction. The message now also states what a name is.
1 parent 0b283ac commit 92b1d9c

2 files changed

Lines changed: 38 additions & 1 deletion

File tree

src/modgraph/scanner.cppm

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -827,11 +827,24 @@ std::expected<SourceUnit, ScanError> scan_file(const std::filesystem::path& file
827827
// no source could have declared: a recorded non-name propagates
828828
// into the build graph as a BMI path and is reported by nothing.
829829
if (!is_well_formed_module_name(name)) {
830+
// QUOTE WHAT WAS READ, NOT WHAT WAS RECORDED. The tokeniser
831+
// stops at the first character it does not accept, so a
832+
// declaration it cannot read at all yields an EMPTY name — and
833+
// `'' is not a module name` names nothing the author can find.
834+
// The measured case is a non-ASCII name: `is_module_name_char`
835+
// tests bytes with std::isalnum, which is false for every byte
836+
// of a UTF-8 sequence, so `export module <non-ascii>;` produced
837+
// the empty string. (GCC 16.1 refuses that declaration too,
838+
// with `unrecognized 'MODULE-EXPORT ...'`, so this is a clearer
839+
// sentence for the same refusal rather than a new restriction.)
840+
const auto decl = trim(r.substr(0, r.find(';')));
830841
return std::unexpected(ScanError{file, lineno,
831842
std::format("'{}' is not a module name. A module "
832843
"declaration is `module <name>;`, "
833844
"`module <name>:<partition>;` or "
834-
"`module : private;`.", name)});
845+
"`module : private;`, and a name is a "
846+
"dot-separated sequence of ASCII identifiers.",
847+
name.empty() ? std::string(decl) : name)});
835848
}
836849
if (is_export) {
837850
if (u.provides) {

tests/unit/test_modgraph.cpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,6 +1102,30 @@ TEST(Scanner, AModuleIdentityThatIsNotANameIsRefused) {
11021102
<< u.error().message;
11031103
}
11041104

1105+
// The message quotes WHAT WAS READ, not what was recorded.
1106+
//
1107+
// The tokeniser stops at the first character it does not accept, so a
1108+
// declaration it cannot read at all yields an EMPTY name, and `'' is not a
1109+
// module name` names nothing the author can find. A non-ASCII name is the
1110+
// measured case: `is_module_name_char` tests bytes with std::isalnum, false for
1111+
// every byte of a UTF-8 sequence. GCC 16.1 refuses the same declaration with
1112+
// `unrecognized 'MODULE-EXPORT ...'`, so this is a clearer sentence for a
1113+
// refusal that already existed, not a new restriction.
1114+
TEST(Scanner, AnUnreadableModuleNameIsQuotedAsWritten) {
1115+
auto dir = make_tempdir("mcpp-nonascii");
1116+
write(dir / "src" / "u.cppm", "export module \u6a21\u5757;\n");
1117+
1118+
auto u = scan_file(dir / "src" / "u.cppm", "pkg",
1119+
mcpp::builtin_extension_table());
1120+
ASSERT_FALSE(u.has_value());
1121+
EXPECT_NE(u.error().message.find("is not a module name"), std::string::npos)
1122+
<< u.error().message;
1123+
EXPECT_EQ(u.error().message.find("'' is not"), std::string::npos)
1124+
<< "the message quoted an empty name: " << u.error().message;
1125+
EXPECT_NE(u.error().message.find("\u6a21\u5757"), std::string::npos)
1126+
<< u.error().message;
1127+
}
1128+
11051129
// The guard above must not refuse what the language allows. A partition, a
11061130
// dotted name and a dotted partition are all names.
11071131
TEST(Scanner, WellFormedNamesSurviveTheIdentityGuard) {

0 commit comments

Comments
 (0)