Skip to content

Commit 6c0cbce

Browse files
committed
refactor(cli): centralize module graph validation
1 parent ecdcf78 commit 6c0cbce

8 files changed

Lines changed: 311 additions & 147 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#ifndef VIX_CLI_MODULES_MODULE_GRAPH_HPP
2+
#define VIX_CLI_MODULES_MODULE_GRAPH_HPP
3+
4+
#include <filesystem>
5+
#include <string>
6+
#include <vector>
7+
8+
#include <vix/cli/app/AppManifest.hpp>
9+
10+
namespace vix::cli::modules
11+
{
12+
struct ModuleNode
13+
{
14+
std::string name;
15+
std::string identity;
16+
std::filesystem::path path;
17+
std::string kind;
18+
bool enabled{true};
19+
std::vector<std::string> dependencies;
20+
};
21+
22+
class ModuleGraph
23+
{
24+
public:
25+
static std::string canonical_identity(const std::string &name);
26+
static std::string case_folded_identity(const std::string &name);
27+
static ModuleGraph from_app_modules(
28+
const std::vector<vix::cli::app::AppModule> &modules,
29+
std::string &error);
30+
31+
bool valid() const { return error_.empty(); }
32+
const std::string &error() const { return error_; }
33+
bool contains(const std::string &name) const;
34+
const ModuleNode *find(const std::string &name) const;
35+
const std::vector<ModuleNode> &nodes() const { return nodes_; }
36+
37+
// Validates paths against a project root without modifying project files.
38+
bool validate_paths(const std::filesystem::path &projectRoot,
39+
bool requireDirectories,
40+
std::string &error) const;
41+
bool topological_order(std::vector<std::string> &order,
42+
bool activeOnly = false,
43+
std::string *error = nullptr) const;
44+
bool dependency_closure(const std::string &name,
45+
std::vector<std::string> &closure,
46+
std::string *error = nullptr) const;
47+
bool active_closure(std::vector<std::string> &closure,
48+
std::string *error = nullptr) const;
49+
50+
private:
51+
std::vector<ModuleNode> nodes_;
52+
std::string error_;
53+
};
54+
}
55+
#endif

src/app/AppCMakeGenerator.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
#include <vix/cli/app/AppCMakeGenerator.hpp>
1818
#include <vix/cli/modules/ModuleManifest.hpp>
19+
#include <vix/cli/modules/ModuleGraph.hpp>
1920

2021
#include <algorithm>
2122
#include <cctype>
@@ -1477,6 +1478,16 @@ namespace vix::cli::app
14771478
const fs::path normalizedProjectDir =
14781479
fs::absolute(projectDir).lexically_normal();
14791480

1481+
std::string graphError;
1482+
const auto graph = vix::cli::modules::ModuleGraph::from_app_modules(
1483+
manifest.appModules, graphError);
1484+
if (!graph.valid() ||
1485+
!graph.validate_paths(normalizedProjectDir, true, graphError))
1486+
{
1487+
result.error = "Invalid module graph: " + graphError;
1488+
return result;
1489+
}
1490+
14801491
const std::vector<AppModule> runtimeModules =
14811492
enabled_runtime_app_modules(manifest, normalizedProjectDir);
14821493

src/app/AppManifest.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,10 @@ namespace vix::cli::app
192192
std::string("[module.").size(),
193193
value.size() - std::string("[module.").size() - 1);
194194

195-
moduleName = normalize_module_id(rawName);
195+
// Preserve the spelling supplied by the user. ModuleGraph owns the
196+
// CMake identity normalization and must be able to diagnose collisions
197+
// such as foo-bar versus foo_bar.
198+
moduleName = trim_copy(rawName);
196199

197200
return is_valid_module_name(moduleName);
198201
}

src/app/AppProjectResolver.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include <vix/cli/app/AppCMakeGenerator.hpp>
2020
#include <vix/cli/app/AppManifest.hpp>
2121
#include <vix/cli/modules/ModuleManifest.hpp>
22+
#include <vix/cli/modules/ModuleGraph.hpp>
2223

2324
#include <vix/cli/util/Lockfile.hpp>
2425
#include <vix/cli/util/Manifest.hpp>
@@ -274,6 +275,15 @@ namespace vix::cli::app
274275

275276
AppManifest manifest = loadResult.manifest;
276277

278+
std::string graphError;
279+
const auto graph = vix::cli::modules::ModuleGraph::from_app_modules(
280+
manifest.appModules, graphError);
281+
if (!graph.valid() || !graph.validate_paths(projectDir, true, graphError))
282+
{
283+
result.error = "Invalid module graph: " + graphError;
284+
return result;
285+
}
286+
277287
merge_enabled_module_registry_deps(
278288
manifest,
279289
projectDir);

src/commands/modules/ModulesCommands.cpp

Lines changed: 11 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include <vix/cli/commands/modules/ModulesUtils.hpp>
1414
#include <vix/cli/app/AppManifest.hpp>
1515
#include <vix/cli/modules/ModuleManifest.hpp>
16+
#include <vix/cli/modules/ModuleGraph.hpp>
1617
#include <vix/cli/Style.hpp>
1718
#include <vix/cli/util/Ui.hpp>
1819

@@ -1357,109 +1358,6 @@ namespace vix::commands::modules_cmd::commands
13571358
return set_module_enabled_in_vix_app(root, module, false);
13581359
}
13591360

1360-
static bool check_dependency_cycle_visit(
1361-
const std::string &module,
1362-
const std::unordered_map<std::string, std::vector<std::string>> &graph,
1363-
std::unordered_set<std::string> &visiting,
1364-
std::unordered_set<std::string> &visited,
1365-
std::vector<std::string> &stack,
1366-
std::vector<std::string> &cycle)
1367-
{
1368-
if (visited.find(module) != visited.end())
1369-
return false;
1370-
1371-
if (visiting.find(module) != visiting.end())
1372-
{
1373-
cycle.clear();
1374-
1375-
bool capture = false;
1376-
1377-
for (const std::string &item : stack)
1378-
{
1379-
if (item == module)
1380-
capture = true;
1381-
1382-
if (capture)
1383-
cycle.push_back(item);
1384-
}
1385-
1386-
cycle.push_back(module);
1387-
return true;
1388-
}
1389-
1390-
visiting.insert(module);
1391-
stack.push_back(module);
1392-
1393-
const auto it = graph.find(module);
1394-
1395-
if (it != graph.end())
1396-
{
1397-
for (const std::string &dep : it->second)
1398-
{
1399-
if (check_dependency_cycle_visit(
1400-
dep,
1401-
graph,
1402-
visiting,
1403-
visited,
1404-
stack,
1405-
cycle))
1406-
{
1407-
return true;
1408-
}
1409-
}
1410-
}
1411-
1412-
stack.pop_back();
1413-
visiting.erase(module);
1414-
visited.insert(module);
1415-
1416-
return false;
1417-
}
1418-
1419-
static bool check_dependency_cycles(
1420-
const std::unordered_map<std::string, std::vector<std::string>> &graph,
1421-
std::vector<std::string> &cycle)
1422-
{
1423-
std::unordered_set<std::string> visiting;
1424-
std::unordered_set<std::string> visited;
1425-
std::vector<std::string> stack;
1426-
1427-
for (const auto &entry : graph)
1428-
{
1429-
if (check_dependency_cycle_visit(
1430-
entry.first,
1431-
graph,
1432-
visiting,
1433-
visited,
1434-
stack,
1435-
cycle))
1436-
{
1437-
return true;
1438-
}
1439-
}
1440-
1441-
return false;
1442-
}
1443-
1444-
static std::string join_cycle(
1445-
const std::vector<std::string> &cycle)
1446-
{
1447-
if (cycle.empty())
1448-
return "-";
1449-
1450-
std::ostringstream out;
1451-
1452-
for (std::size_t i = 0; i < cycle.size(); ++i)
1453-
{
1454-
if (i > 0)
1455-
out << " -> ";
1456-
1457-
out << cycle[i];
1458-
}
1459-
1460-
return out.str();
1461-
}
1462-
14631361
bool cmd_check(const fs::path &root, const std::string &project)
14641362
{
14651363
const fs::path modulesDir = root / "modules";
@@ -1481,7 +1379,7 @@ namespace vix::commands::modules_cmd::commands
14811379
std::unordered_map<std::string, bool> enabledInApp;
14821380
std::unordered_map<std::string, std::string> kindInApp;
14831381
std::unordered_map<std::string, std::string> pathInApp;
1484-
std::unordered_map<std::string, std::vector<std::string>> appDeps;
1382+
vix::cli::modules::ModuleGraph graph;
14851383

14861384
if (hasVixApp)
14871385
{
@@ -1495,6 +1393,15 @@ namespace vix::commands::modules_cmd::commands
14951393
return false;
14961394
}
14971395

1396+
std::string graphError;
1397+
graph = vix::cli::modules::ModuleGraph::from_app_modules(
1398+
loadResult.manifest.appModules, graphError);
1399+
if (!graph.valid() || !graph.validate_paths(root, false, graphError))
1400+
{
1401+
ui::err_line(std::cout, "Invalid module graph: " + graphError);
1402+
return false;
1403+
}
1404+
14981405
for (const auto &module : loadResult.manifest.appModules)
14991406
{
15001407
const std::string name =
@@ -1504,11 +1411,6 @@ namespace vix::commands::modules_cmd::commands
15041411
enabledInApp[name] = module.enabled;
15051412
kindInApp[name] = module.kind.empty() ? "module" : module.kind;
15061413
pathInApp[name] = module.path.empty() ? ("modules/" + name) : module.path;
1507-
1508-
for (const std::string &depRaw : module.depends)
1509-
{
1510-
appDeps[name].push_back(cnt::normalize_module_id(depRaw));
1511-
}
15121414
}
15131415
}
15141416

@@ -1587,43 +1489,6 @@ namespace vix::commands::modules_cmd::commands
15871489
}
15881490
}
15891491

1590-
for (const auto &entry : appDeps)
1591-
{
1592-
const std::string &module = entry.first;
1593-
1594-
for (const std::string &dep : entry.second)
1595-
{
1596-
if (declaredInApp.find(dep) == declaredInApp.end())
1597-
{
1598-
ok = false;
1599-
++violations;
1600-
1601-
ui::err_line(std::cout, "Module depends on an undeclared module");
1602-
ui::kv(std::cout, "module", module, 12);
1603-
ui::kv(std::cout, "depends", dep, 12);
1604-
}
1605-
else if (enabledInApp[module] && !enabledInApp[dep])
1606-
{
1607-
ok = false;
1608-
++violations;
1609-
1610-
ui::err_line(std::cout, "Enabled module depends on a disabled module");
1611-
ui::kv(std::cout, "module", module, 12);
1612-
ui::kv(std::cout, "depends", dep, 12);
1613-
}
1614-
}
1615-
}
1616-
1617-
std::vector<std::string> cycle;
1618-
1619-
if (check_dependency_cycles(appDeps, cycle))
1620-
{
1621-
ok = false;
1622-
++violations;
1623-
1624-
ui::err_line(std::cout, "Circular module dependency detected");
1625-
ui::kv(std::cout, "cycle", join_cycle(cycle), 12);
1626-
}
16271492
}
16281493

16291494
std::unordered_map<std::string, std::string> routePrefixes;

0 commit comments

Comments
 (0)