-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathvalidate.cppm
More file actions
220 lines (197 loc) · 8.95 KB
/
validate.cppm
File metadata and controls
220 lines (197 loc) · 8.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
// mcpp.modgraph.validate — naming/lint + topology checks.
export module mcpp.modgraph.validate;
import std;
import mcpp.manifest;
import mcpp.modgraph.graph;
import mcpp.modgraph.scanner;
export namespace mcpp::modgraph {
struct ValidateError {
std::filesystem::path path; // optional
std::string message;
};
struct ValidateReport {
std::vector<ValidateError> errors;
std::vector<ValidateError> warnings;
std::vector<std::size_t> topoOrder; // valid only if errors empty
bool ok() const { return errors.empty(); }
};
ValidateReport validate(const Graph& g,
const mcpp::manifest::Manifest& manifest);
// Same as `validate` plus a project-root path used to verify that the
// lib-root convention file actually exists on disk. Pass an empty path
// to disable the on-disk check (used by unit tests that build a Graph
// in memory without writing source files).
ValidateReport validate(const Graph& g,
const mcpp::manifest::Manifest& manifest,
const std::filesystem::path& projectRoot);
bool is_public_package_name(std::string_view name);
bool is_forbidden_top_module(std::string_view name);
} // namespace mcpp::modgraph
namespace mcpp::modgraph {
bool is_public_package_name(std::string_view name) {
return name.find('.') != std::string_view::npos;
}
bool is_forbidden_top_module(std::string_view name) {
static constexpr std::string_view blacklist[] = {
"core", "util", "common", "std", "detail", "internal", "base"
};
// top = before first '.'
auto p = name.find('.');
auto top = (p == std::string_view::npos) ? name : name.substr(0, p);
for (auto b : blacklist) if (top == b) return true;
return false;
}
ValidateReport validate(const Graph& g,
const mcpp::manifest::Manifest& manifest)
{
return validate(g, manifest, /*projectRoot=*/{});
}
ValidateReport validate(const Graph& g,
const mcpp::manifest::Manifest& manifest,
const std::filesystem::path& projectRoot)
{
ValidateReport r;
// 1. Naming: public packages' exported modules must be prefixed by package name,
// and the top-level segment must not be in the forbidden list (for public pkgs).
//
// Each SourceUnit carries its OWN package name (set by the scanner) so this
// works transparently for multi-package builds (path deps): each unit is
// validated against its own manifest's package name, not the primary's.
for (auto& u : g.units) {
if (!u.provides) continue;
const auto& m = u.provides->logicalName;
auto base = m.substr(0, m.find(':')); // strip partition suffix
const auto& pkg_name = u.packageName; // ← unit's own package
const bool is_public = is_public_package_name(pkg_name);
if (is_public) {
if (base != pkg_name && !base.starts_with(pkg_name + ".")) {
r.errors.push_back({u.path,
std::format("public module '{}' must be prefixed by package name '{}'",
m, pkg_name)});
}
if (is_forbidden_top_module(base)) {
r.errors.push_back({u.path,
std::format("module '{}' uses a forbidden top-level name (core/util/common/...)", m)});
}
}
}
// 2. modules.exports vs scanner result.
// Only checks the primary manifest's package — dep packages have their
// own [modules].exports validated by their own builds.
if (!manifest.modules.exports_.empty()) {
std::set<std::string> declared(manifest.modules.exports_.begin(),
manifest.modules.exports_.end());
std::set<std::string> actual;
for (auto& u : g.units) {
if (u.provides && u.packageName == manifest.package.name) {
auto base = u.provides->logicalName.substr(0, u.provides->logicalName.find(':'));
actual.insert(base);
}
}
// Any actual module that isn't in declared is a violation.
for (auto& m : actual) {
if (!declared.contains(m)) {
r.errors.push_back({{},
std::format("module '{}' is exported by code but not listed in [modules].exports", m)});
}
}
if (manifest.modules.strict) {
for (auto& m : declared) {
if (!actual.contains(m)) {
r.errors.push_back({{},
std::format("module '{}' declared in [modules].exports but never exported (strict)", m)});
}
}
}
}
// 2.5 Lib-root convention (M5.x+).
//
// For projects that ship a `kind = "lib"` target, expect a primary
// module-interface file at either `[lib].path` (explicit override) or
// `src/<package-tail>.cppm` (default convention). The file must
// declare `export module <full-package-name>;` (no partition suffix);
// partitions go in sibling files and are aggregated by re-exporting
// from the lib root, à la `lib.rs` in cargo.
//
// Pure-binary projects (mcpp itself, scaffolded `mcpp new`) skip this
// check — they have no lib-root concept.
if (mcpp::manifest::has_lib_target(manifest)) {
auto lib_root_rel = mcpp::manifest::resolve_lib_root_path(manifest);
const bool was_explicit = !manifest.lib.path.empty();
// On-disk existence check (skipped when projectRoot is empty —
// unit tests can build Graphs in memory without writing files).
if (!projectRoot.empty()) {
auto lib_root_abs = lib_root_rel.is_absolute()
? lib_root_rel
: (projectRoot / lib_root_rel);
std::error_code ec;
const bool exists = std::filesystem::exists(lib_root_abs, ec);
if (!exists) {
if (was_explicit) {
// Explicit `[lib].path` pointing at a missing file is
// always an error.
r.errors.push_back({lib_root_rel, std::format(
"[lib].path '{}' does not exist", lib_root_rel.string())});
} else {
// Convention miss is a warning — gives existing projects
// a soft on-ramp before they rename / move files.
r.warnings.push_back({lib_root_rel, std::format(
"lib target without conventional lib root '{}' "
"(create the file or set [lib].path)",
lib_root_rel.string())});
}
}
}
// Even without on-disk verification we can still cross-check the
// graph: if a unit at the lib-root path is present, it must
// export `<package-name>` exactly (no partition).
const mcpp::modgraph::SourceUnit* lib_unit = nullptr;
for (auto& u : g.units) {
// Match relative or absolute — projectRoot may be empty in
// tests, so we just compare path tails.
auto u_rel = u.path.is_absolute() && !projectRoot.empty()
? std::filesystem::relative(u.path, projectRoot)
: u.path;
if (u_rel == lib_root_rel || u.path == lib_root_rel) {
lib_unit = &u;
break;
}
}
if (lib_unit) {
if (!lib_unit->provides) {
r.errors.push_back({lib_unit->path, std::format(
"lib root '{}' must declare `export module {};`",
lib_root_rel.string(), manifest.package.name)});
} else {
const auto& m = lib_unit->provides->logicalName;
if (m.find(':') != std::string::npos) {
r.errors.push_back({lib_unit->path, std::format(
"lib root '{}' exports a partition '{}' — must be the "
"primary module '{}' (no `:partition` suffix)",
lib_root_rel.string(), m, manifest.package.name)});
} else if (m != manifest.package.name) {
r.errors.push_back({lib_unit->path, std::format(
"lib root '{}' exports module '{}', expected '{}' "
"(must match [package].name)",
lib_root_rel.string(), m, manifest.package.name)});
}
}
}
}
// 3. Topology
auto topo = topo_sort(g);
if (!topo) {
std::string names;
for (auto i : topo.error().cycle) {
if (i < g.units.size()) {
names += g.units[i].path.string() + " ";
}
}
r.errors.push_back({{},
std::format("circular module dependency among: {}", names)});
} else {
r.topoOrder = std::move(*topo);
}
return r;
}
} // namespace mcpp::modgraph