d38e1466da
canon add module <path> creates <stem>/<path>.hpp, .cpp and .test.cpp (--no-test skips the test), records the module in .canon.toml, adds the source to the project's target and re-renders the managed block of CMakeLists.txt. canon add unit-test adds the test later. Both work from any directory inside the project. - toml: a reader for the TOML subset canon writes, with line numbers in errors, so canon stays dependency-free - manifest: strict parsing (unknown keys, wrong types, bad references) - project: finds .canon.toml by walking up from the current directory - plan/executor: update_file refuses to write if the file changed after it was read, and the report skips directories that already exist - tests: shared scenarios drive both the golden and end-to-end tests; goldens now live in tests/golden/<scenario>/ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
105 lines
3.8 KiB
C++
105 lines
3.8 KiB
C++
#include <canon/name.hpp>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <format>
|
|
|
|
namespace canon {
|
|
|
|
namespace {
|
|
|
|
constexpr auto cxx_keywords = std::to_array<std::string_view>({
|
|
"alignas", "alignof", "and", "and_eq", "asm", "auto",
|
|
"bitand", "bitor", "bool", "break", "case", "catch",
|
|
"char", "char8_t", "char16_t", "char32_t", "class", "compl",
|
|
"concept", "const", "const_cast", "constexpr", "constinit", "continue",
|
|
"co_await", "co_return", "co_yield", "decltype", "default", "delete",
|
|
"do", "double", "dynamic_cast", "else", "enum", "explicit",
|
|
"export", "extern", "false", "float", "for", "friend",
|
|
"goto", "if", "inline", "int", "long", "mutable",
|
|
"namespace", "new", "noexcept", "not", "not_eq", "nullptr",
|
|
"operator", "or", "or_eq", "private", "protected", "public",
|
|
"register", "reinterpret_cast", "requires", "return", "short", "signed",
|
|
"sizeof", "static", "static_assert", "static_cast", "struct", "switch",
|
|
"template", "this", "thread_local", "throw", "true", "try",
|
|
"typedef", "typeid", "typename", "union", "unsigned", "using",
|
|
"virtual", "void", "volatile", "wchar_t", "while", "xor",
|
|
"xor_eq",
|
|
});
|
|
|
|
bool is_separator(char c) {
|
|
return c == '-' || c == '_';
|
|
}
|
|
|
|
} // namespace
|
|
|
|
std::string_view to_string(project_kind kind) {
|
|
return kind == project_kind::library ? "lib" : "exe";
|
|
}
|
|
|
|
bool is_cxx_keyword(std::string_view word) {
|
|
return std::ranges::contains(cxx_keywords, word);
|
|
}
|
|
|
|
std::string name_problem(std::string_view s) {
|
|
if (s.empty()) {
|
|
return "the name is empty";
|
|
}
|
|
if (s.front() < 'a' || s.front() > 'z') {
|
|
return "it must start with a lowercase letter";
|
|
}
|
|
char previous = '\0';
|
|
for (char c : s) {
|
|
const bool alnum = (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9');
|
|
if (!alnum && !is_separator(c)) {
|
|
return std::format("'{}' is not allowed (use a-z, 0-9, '-' or '_')", c);
|
|
}
|
|
if (is_separator(c) && is_separator(previous)) {
|
|
return "'-' and '_' cannot appear next to each other";
|
|
}
|
|
previous = c;
|
|
}
|
|
if (is_separator(previous)) {
|
|
return "it cannot end with '-' or '_'";
|
|
}
|
|
return {};
|
|
}
|
|
|
|
std::expected<project_name, std::string> parse_project_name(project_kind kind,
|
|
std::string_view name) {
|
|
constexpr std::string_view prefix = "lib";
|
|
|
|
std::string_view stem = name;
|
|
if (kind == project_kind::library) {
|
|
if (!name.starts_with(prefix)) {
|
|
return std::unexpected(std::format(
|
|
"library name '{}' must start with 'lib' (P1204R0), e.g. 'lib{}'", name, name));
|
|
}
|
|
stem.remove_prefix(prefix.size());
|
|
} else if (name.starts_with(prefix)) {
|
|
return std::unexpected(std::format(
|
|
"executable name '{}' must not start with 'lib' (P1204R0 reserves it for libraries)",
|
|
name));
|
|
}
|
|
|
|
if (const std::string problem = name_problem(stem); !problem.empty()) {
|
|
return std::unexpected(std::format("invalid name '{}': {}", name, problem));
|
|
}
|
|
|
|
std::string ident(stem);
|
|
std::ranges::replace(ident, '-', '_');
|
|
if (is_cxx_keyword(ident)) {
|
|
return std::unexpected(std::format(
|
|
"invalid name '{}': '{}' is a C++ keyword and cannot be a namespace", name, ident));
|
|
}
|
|
|
|
return project_name{
|
|
.kind = kind,
|
|
.name = std::string(name),
|
|
.stem = std::string(stem),
|
|
.ident = std::move(ident),
|
|
};
|
|
}
|
|
|
|
} // namespace canon
|