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>
This commit is contained in:
ykiko
2026-03-28 22:27:49 +08:00
committed by GitHub
parent f8a39147a7
commit a536865fca
6 changed files with 677 additions and 0 deletions

View File

@@ -513,6 +513,35 @@ et::task<> scan_impl(CompilationDatabase& cdb,
}
// Record module interface unit mapping.
// When the module declaration is inside a conditional directive
// (need_preprocess=true), fall back to scan_module_decl() which
// runs a lightweight preprocessor pass to resolve the actual
// module name. This only applies to source files (wave 0) since
// headers cannot contain module declarations.
if(scan_result.scan_result.need_preprocess && wave_num == 0) {
auto file_path = llvm::StringRef(scan_result.path);
auto contexts =
cdb.lookup(file_path, {.query_toolchain = true, .suppress_logging = true});
if(!contexts.empty()) {
auto& ctx = contexts[0];
auto fallback = scan_module_decl(ctx.arguments, ctx.directory, /*content=*/{});
if(!fallback.module_name.empty()) {
scan_result.scan_result.module_name = std::move(fallback.module_name);
scan_result.scan_result.is_interface_unit = fallback.is_interface_unit;
// Update cache so warm runs don't re-trigger fallback.
if(ext_cache) {
auto cache_it = ext_cache->scan_results.find(scan_result.path_id);
if(cache_it != ext_cache->scan_results.end()) {
cache_it->second.module_name = scan_result.scan_result.module_name;
cache_it->second.is_interface_unit =
scan_result.scan_result.is_interface_unit;
cache_it->second.need_preprocess = false;
}
}
}
}
}
if(scan_result.scan_result.is_interface_unit) {
graph.add_module(scan_result.scan_result.module_name, scan_result.path_id);
}