Files
clice/tests/unit/syntax/module_scan_tests.cpp
ykiko a536865fca feat: add scan_module_decl() fallback for conditional module declarations (#373)
## Summary

- Add `scan_module_decl()` — a lightweight preprocessor-based fallback
that resolves module declarations inside `#if`/`#ifdef` conditionals.
When `scan()` detects `need_preprocess=true`, this function runs clang's
preprocessor to evaluate conditions and extract the actual module name.
It stops lexing as soon as the module declaration is found, making it
much cheaper than `scan_precise()`.
- Integrate the fallback into `scan_dependency_graph()` for wave 0
source files, so conditional module declarations (e.g. `#ifdef
USE_MODULES / export module M; / #endif`) are correctly registered in
the dependency graph.
- Add comprehensive test cases covering all C++20 module declaration
forms from cppreference, including `scan_module_decl()` tests for
conditional resolution and `scan_precise()` tests for module import
semantics.

## Test plan

- [x] All 310 unit tests pass (0 failures, 9 skipped)
- [x] `scan()` tests cover: primary interface, implementation, dotted
names, partitions, GMF, conditional module declarations, private module
fragment
- [x] `scan_module_decl()` tests cover: basic, conditional with `-D`,
conditional with `#if` expression, GMF with conditional, implementation
unit, dotted name, partition, no-module file
- [x] `scan_precise()` tests cover: named import, multiple imports,
dotted import, partition import, export-import, export-import partition,
implementation import, GMF with import, mixed includes/imports,
no-module file

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Improved detection of module declarations hidden by conditional
compilation via a lightweight fallback scan. Resolved module vs.
interface classification is cached to avoid repeated work and is used
consistently in dependency mapping.
* Better handling and classification of module imports, partitions, and
global-fragment includes when building module relationships.

* **Tests**
* Added comprehensive unit tests covering module declaration extraction,
fallback resolution under preprocessor guards, imports, partitions,
includes, and macro-driven cases.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 22:27:49 +08:00

251 lines
6.9 KiB
C++

#include "module_scan_fixture.h"
#include "test/test.h"
#include "syntax/scan.h"
namespace clice::testing {
namespace {
// =============================================================================
// scan() — module declaration extraction (lexer-based, cppref coverage)
// =============================================================================
TEST_SUITE(ModuleScan) {
// Primary module interface: export module M;
TEST_CASE(PrimaryModuleInterface) {
auto result = scan("export module mylib;");
EXPECT_EQ(result.module_name, "mylib");
EXPECT_TRUE(result.is_interface_unit);
EXPECT_FALSE(result.need_preprocess);
}
// Module implementation unit: module M;
TEST_CASE(ModuleImplementationUnit) {
auto result = scan("module mylib;");
EXPECT_EQ(result.module_name, "mylib");
EXPECT_FALSE(result.is_interface_unit);
EXPECT_FALSE(result.need_preprocess);
}
// Dotted module name: export module std.io;
TEST_CASE(DottedModuleName) {
auto result = scan("export module std.io;");
EXPECT_EQ(result.module_name, "std.io");
EXPECT_TRUE(result.is_interface_unit);
}
// Deeply dotted module name: export module a.b.c.d;
TEST_CASE(DeeplyDottedModuleName) {
auto result = scan("export module a.b.c.d;");
EXPECT_EQ(result.module_name, "a.b.c.d");
EXPECT_TRUE(result.is_interface_unit);
}
// Module partition interface: export module M:P;
TEST_CASE(PartitionInterface) {
auto result = scan("export module mylib:core;");
EXPECT_EQ(result.module_name, "mylib:core");
EXPECT_TRUE(result.is_interface_unit);
}
// Module partition implementation: module M:P;
TEST_CASE(PartitionImplementation) {
auto result = scan("module mylib:core;");
EXPECT_EQ(result.module_name, "mylib:core");
EXPECT_FALSE(result.is_interface_unit);
}
// Dotted module name + partition: export module a.b:p;
TEST_CASE(DottedModuleWithPartition) {
auto result = scan("export module a.b:p;");
EXPECT_EQ(result.module_name, "a.b:p");
EXPECT_TRUE(result.is_interface_unit);
}
// Global module fragment with includes before module declaration.
TEST_CASE(GlobalModuleFragmentWithIncludes) {
auto result = scan(R"(
module;
#include <stdlib.h>
#include "config.h"
export module mylib;
)");
EXPECT_EQ(result.module_name, "mylib");
EXPECT_TRUE(result.is_interface_unit);
ASSERT_EQ(result.includes.size(), 2u);
EXPECT_EQ(result.includes[0].path, "stdlib.h");
EXPECT_TRUE(result.includes[0].is_angled);
EXPECT_EQ(result.includes[1].path, "config.h");
EXPECT_FALSE(result.includes[1].is_angled);
}
// Conditional module declaration with #ifdef.
TEST_CASE(ConditionalModuleIfdef) {
auto result = scan(R"(
#ifdef USE_MODULES
export module mylib;
#endif
)");
EXPECT_TRUE(result.module_name.empty());
EXPECT_TRUE(result.need_preprocess);
}
// Conditional module declaration with #if __cpp_modules.
TEST_CASE(ConditionalModuleCppModules) {
auto result = scan(R"(
#if __cpp_modules >= 201907L
export module mylib;
#endif
)");
EXPECT_TRUE(result.module_name.empty());
EXPECT_TRUE(result.need_preprocess);
}
// Conditional module declaration in global module fragment.
TEST_CASE(ConditionalModuleInGMF) {
auto result = scan(R"(
module;
#include <stdlib.h>
#ifdef USE_MODULES
export module mylib;
#endif
)");
EXPECT_TRUE(result.module_name.empty());
EXPECT_TRUE(result.need_preprocess);
ASSERT_EQ(result.includes.size(), 1u);
EXPECT_EQ(result.includes[0].path, "stdlib.h");
}
// Module declaration NOT inside conditional (after a closed conditional block).
TEST_CASE(ModuleAfterClosedConditional) {
auto result = scan(R"(
module;
#ifdef FOO
#include <optional.h>
#endif
export module mylib;
)");
EXPECT_EQ(result.module_name, "mylib");
EXPECT_TRUE(result.is_interface_unit);
EXPECT_FALSE(result.need_preprocess);
}
// Private module fragment marker should not override the real module declaration.
TEST_CASE(PrivateModuleFragment) {
auto result = scan(R"(
export module mylib;
export int f();
module : private;
int f() { return 42; }
)");
EXPECT_EQ(result.module_name, "mylib");
EXPECT_TRUE(result.is_interface_unit);
}
}; // TEST_SUITE(ModuleScan)
// =============================================================================
// scan_module_decl() — lightweight preprocessor fallback
// =============================================================================
TEST_SUITE(ModuleDeclFallback) {
TEST_CASE(Basic) {
ModuleScanFixture f("main.cppm", "export module mylib;");
auto result = f.decl();
EXPECT_EQ(result.module_name, "mylib");
EXPECT_TRUE(result.is_interface_unit);
}
TEST_CASE(ConditionalWithDefine) {
// Without -DUSE_MODULES: no module declaration.
ModuleScanFixture f1("main.cppm", R"(
#ifdef USE_MODULES
export module mylib;
#endif
)");
EXPECT_TRUE(f1.decl().module_name.empty());
// With -DUSE_MODULES: module declaration found.
ModuleScanFixture f2("main.cppm",
R"(
#ifdef USE_MODULES
export module mylib;
#endif
)",
{"-DUSE_MODULES"});
auto result = f2.decl();
EXPECT_EQ(result.module_name, "mylib");
EXPECT_TRUE(result.is_interface_unit);
}
TEST_CASE(ConditionalIfExpr) {
// Without the define: no module.
ModuleScanFixture f1("main.cppm", R"(
#if ENABLE_MODULES >= 1
export module mylib;
#endif
)");
EXPECT_TRUE(f1.decl().module_name.empty());
// With the define: module found.
ModuleScanFixture f2("main.cppm",
R"(
#if ENABLE_MODULES >= 1
export module mylib;
#endif
)",
{"-DENABLE_MODULES=1"});
auto result = f2.decl();
EXPECT_EQ(result.module_name, "mylib");
EXPECT_TRUE(result.is_interface_unit);
}
TEST_CASE(GMFWithConditional) {
ModuleScanFixture f("main.cppm", R"(
module;
#include "config.h"
#ifdef USE_MODULES
export module mylib;
#endif
)");
f.add_file("config.h", "#define USE_MODULES 1\n");
auto result = f.decl();
EXPECT_EQ(result.module_name, "mylib");
EXPECT_TRUE(result.is_interface_unit);
}
TEST_CASE(ImplementationUnit) {
ModuleScanFixture f("main.cpp", "module mylib;");
auto result = f.decl();
EXPECT_EQ(result.module_name, "mylib");
EXPECT_FALSE(result.is_interface_unit);
}
TEST_CASE(DottedName) {
ModuleScanFixture f("main.cppm", "export module std.io;");
auto result = f.decl();
EXPECT_EQ(result.module_name, "std.io");
EXPECT_TRUE(result.is_interface_unit);
}
TEST_CASE(Partition) {
ModuleScanFixture f("main.cppm", "export module mylib:core;");
auto result = f.decl();
EXPECT_EQ(result.module_name, "mylib:core");
EXPECT_TRUE(result.is_interface_unit);
}
TEST_CASE(NoModule) {
ModuleScanFixture f("main.cpp", "int main() { return 0; }");
auto result = f.decl();
EXPECT_TRUE(result.module_name.empty());
EXPECT_FALSE(result.is_interface_unit);
EXPECT_TRUE(result.modules.empty());
}
}; // TEST_SUITE(ModuleDeclFallback)
} // namespace
} // namespace clice::testing