Files
canon/canon/name.cpp
T
paulhorn d38e1466da Add canon add module and canon add unit-test
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>
2026-09-17 18:24:53 +02:00

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