Patrick Kelley 8fd444092b initial
2025-05-07 15:35:15 -04:00

278 lines
8.8 KiB
C++

// Copyright (c) 2020-now by the Zeek Project. See LICENSE for details.
#pragma once
#include <algorithm>
#include <initializer_list>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include <hilti/base/util.h>
namespace hilti::detail {
using normalizer_func = std::optional<std::string> (*)(std::string_view);
inline std::optional<std::string> identityNormalizer(std::string_view s) { return std::nullopt; }
/**
* Base class for representing scoped language IDs. It provides a number of
* standard accessors and manipulators to support operations on/with
* namespaces. This class assumes that namespaces are separated with `::`.
*
* @tparam Derived name of the class deriving from this one (CRTP).
* @tparam N a function that may preprocess/normalize all ID components before storing them
*
*/
template<class Derived, normalizer_func N = identityNormalizer>
class IDBase {
public:
/** Tag for creating IDs from an already normalized string. */
struct AlreadyNormalized {};
/** Creates an empty ID. */
IDBase() { _init("", false); }
/** Creates an ID from an (not normalized) string. */
IDBase(const char* s) { _init(s, false); }
explicit IDBase(std::string_view s) { _init(s, false); }
/**
* Creates an ID from a string that's already normalized. The assumption is
* that the input string is the output of a prior `str()` call on an
* existing ID object.
*/
IDBase(std::string_view s, AlreadyNormalized) { _init(s, true); }
/** Concatenates multiple strings into a single ID, separating them with `::`. */
template<typename... T, typename enable = std::enable_if_t<(... && std::is_convertible_v<T, std::string_view>)>>
explicit IDBase(const T&... s) {
_init((util::join<std::string>({s...}, "::")), false);
}
/** Concatenates multiple strings into a single ID, separating them with `::`. */
IDBase(std::initializer_list<std::string_view> x) { _init(util::join(x, "::"), false); }
IDBase(const IDBase& other) : _id(std::move(other._id)) {}
IDBase(IDBase&& other) noexcept : _id(std::move(other._id)) {}
~IDBase() = default;
IDBase& operator=(const IDBase& other) {
if ( &other == this )
return *this;
_id = other._id;
_views.reset();
return *this;
}
IDBase& operator=(IDBase&& other) noexcept {
if ( &other == this )
return *this;
_id = std::move(other._id);
_views.reset();
return *this;
}
/** Returns the ID's full name as a string. */
const std::string& str() const { return _id; }
/** Returns the ID local part, which is the most-rhs element of the ID path. */
Derived local() const { return Derived(_cachedViews()->local, AlreadyNormalized()); }
/** Returns the ID's namespace. That's everything except the local part. */
Derived namespace_() const { return Derived(_cachedViews()->namespace_, AlreadyNormalized()); }
/** Returns true if the ID's value has length zero. */
bool empty() const { return _id.empty(); }
/** Returns the number of namespace components. */
size_t length() const { return _cachedViews()->path.size(); }
/** Returns true if the ID is absolute, i.e., starts with `::`. */
bool isAbsolute() const { return ! _id.empty() && _id[0] == ':'; }
/**
* Returns a new ID containing just single component of the path's of the
* ID. Indices are zero-based and, if negative, counted from the end
* Python-style.
*
* @param i index of path component to return
*/
Derived sub(int i) const {
const auto& path = _cachedViews()->path;
if ( i < 0 )
i = static_cast<int>(path.size()) + i;
if ( i >= 0 && static_cast<size_t>(i) < path.size() )
return Derived(path[i], AlreadyNormalized());
else
return Derived();
}
/**
* Returns a new ID containing a subpath of the ID. Indices are
* zero-based and, if negative, counted from the end Python-style.
*
* @param from 1st index to include
* @param to one beyond last index to include
*/
Derived sub(int from, int to) const {
return Derived(util::join(util::slice(_cachedViews()->path, from, to), "::"), AlreadyNormalized());
}
/**
* "Rebases" the ID relative to another one.
*
* If the ID already starts with `root`, the remaining part is returned.
* If not, the returned value is `root` plus the ID.
*/
Derived relativeTo(const Derived& root) const {
if ( _id == root._id )
return Derived();
if ( ! util::startsWith(_id, root._id + "::") )
return Derived(root + _id, AlreadyNormalized());
return Derived(_id.substr(root._id.size() + 2), AlreadyNormalized());
}
/**
* Turns the ID into absolute one, i.e., prepends `::` if not already
* present. Aftwerwards, `isAbsolute()` will return true.
*/
Derived makeAbsolute() const {
if ( isAbsolute() )
return Derived(*this);
return Derived("::" + _id, AlreadyNormalized());
}
/** Appends an ID, separating it with `::`. */
Derived& operator+=(const Derived& other) {
if ( ! other.empty() ) {
if ( empty() )
*this = other;
else
*this = Derived(_id + "::" + other._id, AlreadyNormalized());
}
return static_cast<Derived&>(*this);
}
/** Appends an ID, separating it with `::`. */
Derived& operator+=(std::string_view other) {
auto other_ = Derived(other);
return *this += other_;
}
/** Concatenates two IDs, separating them with `::`. */
Derived operator+(std::string_view other) const {
Derived n = Derived(*this);
n += other;
return n;
}
/** Concatenates two IDs, separating them with `::`. */
Derived operator+(const Derived& other) const {
Derived n = Derived(*this);
n += other;
return n;
}
bool operator==(const Derived& other) const { return _id == other._id; };
bool operator!=(const Derived& other) const { return ! (*this == other); }
bool operator<(const Derived& other) const { return _id < other._id; };
/** Returns true if the ID is not empty. */
explicit operator bool() const { return ! empty(); }
/** Returns the ID as a string, with all components normalized. */
operator std::string() const { return _id; }
/** Returns the ID as a string, with all components normalized. */
operator std::string_view() const { return _id; }
private:
// Caches views into the ID string.
struct Views {
std::vector<std::string_view> path; // views into _id; empty for empty ID
std::string_view local; // view into _id
std::string_view namespace_; // view into _id
};
void _init(std::string_view s, bool already_normalized) {
if ( s.empty() )
return;
if ( already_normalized )
_id = s;
else {
_id.reserve(s.size()); // we'll need at least this much
for ( size_t i = 0; i < s.size(); /* empty */ ) {
if ( auto p = s.find("::", i); p != std::string::npos ) {
_normalizeAndAdd(s.substr(i, p - i));
_id += "::";
i = p + 2;
}
else {
_normalizeAndAdd(s.substr(i));
break;
}
}
}
}
void _normalizeAndAdd(std::string_view x) {
assert(x.find("::") == std::string::npos);
if ( auto nx = N(x) )
_id += *nx;
else
_id += x;
}
Views* _cachedViews() const noexcept {
if ( _views )
return _views.get();
_views = std::make_unique<Views>();
size_t ns_end = std::string::npos;
_views->path.clear();
for ( size_t i = 0; i < _id.size(); /* empty */ ) {
if ( auto p = _id.find("::", i); p != std::string::npos ) {
_views->path.emplace_back(std::string_view(_id).substr(i, p - i));
i = p + 2;
ns_end = p;
}
else {
_views->path.emplace_back(std::string_view(_id).substr(i));
break;
}
}
if ( ns_end != std::string::npos ) {
_views->namespace_ = std::string_view(_id).substr(0, ns_end);
_views->local = std::string_view(_id).substr(ns_end + 2);
}
else {
_views->namespace_ = {};
_views->local = std::string_view(_id);
}
return _views.get();
}
std::string _id; // normalized full-path ID
mutable std::unique_ptr<Views> _views; // allocated on demand first time view information is needed
};
} // namespace hilti::detail