Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions .clang-tidy
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,22 @@ Checks: >
clang-diagnostic-*,
clang-analyzer-*,
bugprone-*,
cppcoreguidelines-*,
misc-*,
modernize-*,
performance-*,
portability-*,
readability-*,
cert-*,
concurrency-*,
-modernize-use-trailing-return-type,
-readability-magic-numbers,
-readability-named-parameter,
-bugprone-easily-swappable-parameters,
-readability-convert-member-functions-to-static,
-bugprone-empty-catch
-cppcoreguidelines-avoid-magic-numbers,
-misc-non-private-member-variables-in-classes,
-portability-avoid-pragma-once,
-misc-include-cleaner

WarningsAsErrors: ""
WarningsAsErrors: "*"

HeaderFilterRegex: "include/morph/.*"

Expand All @@ -37,4 +42,4 @@ CheckOptions:
- key: readability-identifier-length.IgnoredVariableNames
value: "^[ijk]$|^[xy]$|^lk$|^cb$|^op$|^fn$"
- key: readability-identifier-length.IgnoredLoopCounterNames
value: "^[ijk]$"
value: "^[ijk]$"
8 changes: 8 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ if(MORPH_BUILD_FORMS_QML AND (NOT MORPH_BUILD_EXAMPLES OR EMSCRIPTEN))
endif()
option(MORPH_BUILD_QT "Build Qt6 WebSocket backend and tests" OFF)
option(MORPH_BUILD_DOCUMENTATION "Build doxygen docs" OFF)
option(MORPH_BUILD_CLANG_TIDY "Enable clang-tidy checks (warnings-as-errors)" OFF)

# Both Qt consumers (the WebSocket backend and the forms QML renderer) need
# Qt6 auto-detected on Windows before their find_package(Qt6 ...) calls run.
Expand All @@ -31,6 +32,13 @@ endif()

include(cmake/compiler_options.cmake)

# ── clang-tidy integration ─────────────────────────────────────────────────
if(MORPH_BUILD_CLANG_TIDY)
find_program(CLANG_TIDY_BIN clang-tidy REQUIRED)
set(CMAKE_CXX_CLANG_TIDY "${CLANG_TIDY_BIN};--warnings-as-errors=*;--header-filter=include/morph/.*")
message(STATUS "clang-tidy: ${CLANG_TIDY_BIN} (warnings as errors)")
endif()

# Registered before any subdirectory so demo-level add_test calls (examples/
# forms) land in the test set alongside the Catch2 suite.
if(MORPH_BUILD_TESTS)
Expand Down
10 changes: 6 additions & 4 deletions include/morph/action_log.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ inline LogEntry fromJson(std::string_view json) {
/// items once retried successfully). Implementations range from in-memory
/// (`InMemoryActionLog`) to file, SQL, or network-backed stores supplied by the
/// host application.
// NOLINTBEGIN(cppcoreguidelines-special-member-functions)
struct IActionLog {
virtual ~IActionLog() = default;

Expand All @@ -120,6 +121,7 @@ struct IActionLog {
/// @return Matching entries, in append order.
[[nodiscard]] virtual std::vector<LogEntry> entries(std::string_view entityKey = {}) const = 0;
};
// NOLINTEND(cppcoreguidelines-special-member-functions)

/// @brief Thread-safe in-memory implementation of `IActionLog`.
///
Expand All @@ -130,7 +132,7 @@ class InMemoryActionLog : public IActionLog {
/// @brief Appends @p entry, assigning a monotonically increasing `seq`. Thread-safe.
/// @param entry Entry to append; `seq` is overwritten regardless of the input value.
void append(LogEntry entry) override {
std::scoped_lock lock{_mtx};
std::scoped_lock const lock{_mtx};
entry.seq = ++_nextSeq;
_entries.push_back(std::move(entry));
}
Expand All @@ -142,7 +144,7 @@ class InMemoryActionLog : public IActionLog {
/// @param entityKey If non-empty, restricts the result to that entity's entries.
/// @return Matching entries, in append order.
[[nodiscard]] std::vector<LogEntry> entries(std::string_view entityKey = {}) const override {
std::scoped_lock lock{_mtx};
std::scoped_lock const lock{_mtx};
if (entityKey.empty()) {
return _entries;
}
Expand Down Expand Up @@ -190,15 +192,15 @@ inline std::pair<std::mutex, std::shared_ptr<IActionLog>>& defaultActionLogState
/// (existing instances keep whatever they already have).
inline void setActionLog(std::shared_ptr<IActionLog> log) {
auto& [mtx, slot] = detail::defaultActionLogState();
std::scoped_lock lock{mtx};
std::scoped_lock const lock{mtx};
slot = std::move(log);
}

/// @brief Returns the currently installed default action log, or `nullptr`
/// if none has been set. Thread-safe.
[[nodiscard]] inline std::shared_ptr<IActionLog> defaultActionLog() {
auto& [mtx, slot] = detail::defaultActionLogState();
std::scoped_lock lock{mtx};
std::scoped_lock const lock{mtx};
return slot;
}

Expand Down
18 changes: 10 additions & 8 deletions include/morph/backend.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ struct ActionCall {
/// A backend owns model instances and dispatches actions against them.
/// `Bridge` holds one active backend at a time and can swap it atomically
/// via `Bridge::switchBackend()`.
// NOLINTBEGIN(cppcoreguidelines-special-member-functions)
struct IBackend {
virtual ~IBackend() = default;

Expand Down Expand Up @@ -112,8 +113,9 @@ struct IBackend {
/// `LocalBackend`) never invoke it.
/// @param handler Callable invoked on the backend's transport thread after a
/// successful reconnect. Pass `nullptr` to clear.
virtual void setReconnectHandler(std::function<void()> handler) { (void)handler; }
virtual void setReconnectHandler(const std::function<void()>& handler) { (void)handler; }
};
// NOLINTEND(cppcoreguidelines-special-member-functions)

} // namespace detail

Expand Down Expand Up @@ -161,21 +163,21 @@ class LocalBackend : public detail::IBackend {
const std::string& /*typeId*/,
std::function<std::unique_ptr<::morph::model::detail::IModelHolder>()> factory) override {
::morph::exec::detail::ModelId mid{_nextId.fetch_add(1) + 1};
std::scoped_lock lock{_regMtx};
std::scoped_lock const lock{_regMtx};
_models[mid] = factory();
return mid;
}

/// @brief Removes the model with @p mid. Thread-safe.
/// @param mid Id returned by a prior `registerModel()` call.
void deregisterModel(::morph::exec::detail::ModelId mid) override {
std::scoped_lock lock{_regMtx};
std::scoped_lock const lock{_regMtx};
_models.erase(mid);
}

/// @brief Notifies every live model that implements `IBackendChangedSink`. Thread-safe.
void notifyBackendChanged() override {
std::scoped_lock lock{_regMtx};
std::scoped_lock const lock{_regMtx};
for (auto& [modelId, holder] : _models) {
if (auto* sink = dynamic_cast<::morph::model::detail::IBackendChangedSink*>(holder.get())) {
sink->onBackendChanged();
Expand All @@ -200,7 +202,7 @@ class LocalBackend : public detail::IBackend {

std::shared_ptr<::morph::model::detail::IModelHolder> holder;
{
std::scoped_lock lock{_regMtx};
std::scoped_lock const lock{_regMtx};
auto iter = _models.find(mid);
if (iter != _models.end()) {
holder = iter->second;
Expand All @@ -217,7 +219,7 @@ class LocalBackend : public detail::IBackend {
_strand.post(mid, [localOp = std::move(localOp), holder = std::move(holder), compState,
session = std::move(session)]() mutable {
try {
::morph::session::detail::ScopedContext scoped{session};
::morph::session::detail::ScopedContext const scoped{session};
compState->setValue(localOp(*holder));
} catch (...) {
compState->setException(std::current_exception());
Expand All @@ -231,7 +233,7 @@ class LocalBackend : public detail::IBackend {
void cancelPending(const std::exception_ptr& exc) override {
std::vector<std::weak_ptr<::morph::async::detail::CompletionState<std::shared_ptr<void>>>> snapshot;
{
std::scoped_lock lock{_pendingMtx};
std::scoped_lock const lock{_pendingMtx};
snapshot.swap(_pending);
}
for (auto& weak : snapshot) {
Expand All @@ -243,7 +245,7 @@ class LocalBackend : public detail::IBackend {

private:
void trackPending(const std::shared_ptr<::morph::async::detail::CompletionState<std::shared_ptr<void>>>& state) {
std::scoped_lock lock{_pendingMtx};
std::scoped_lock const lock{_pendingMtx};
std::erase_if(_pending, [](const auto& weak) { return weak.expired(); });
_pending.emplace_back(state);
}
Expand Down
44 changes: 21 additions & 23 deletions include/morph/bridge.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,7 @@ class ActionExecuteRegistry {

private:
using Key = std::pair<std::string, std::string>;
struct KeyHash {
std::size_t operator()(const Key& key) const noexcept {
std::size_t seed = std::hash<std::string>{}(key.first);
seed ^= std::hash<std::string>{}(key.second) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
return seed;
}
};
std::unordered_map<Key, Executor, KeyHash> _executors;
std::unordered_map<Key, Executor, ::morph::model::detail::PairKeyHash> _executors;
};

inline ActionExecuteRegistry& ActionExecuteRegistry::instance() {
Expand Down Expand Up @@ -206,7 +199,7 @@ class Bridge {
auto binding = std::make_shared<detail::HandlerBinding>();
binding->typeId = std::string{::morph::model::ModelTraits<Model>::typeId()};
binding->modelFactory = [] { return ::morph::model::detail::ModelFactory::create<Model>(); };
std::scoped_lock lock{_mtx};
std::scoped_lock const lock{_mtx};
binding->currentId.store(
loadBackend()->registerModelWithContext(binding->typeId, binding->modelFactory, binding->contextKey).v);
_handlers.push_back(binding);
Expand All @@ -220,7 +213,7 @@ class Bridge {
/// `binding->contextKey` needs to be set before registration (see `HandlerBinding::contextKey`).
/// @param binding Pre-constructed binding. Its `typeId` and `modelFactory` must be set.
void registerHandler(const std::shared_ptr<detail::HandlerBinding>& binding) {
std::scoped_lock lock{_mtx};
std::scoped_lock const lock{_mtx};
binding->currentId.store(
loadBackend()->registerModelWithContext(binding->typeId, binding->modelFactory, binding->contextKey).v);
_handlers.push_back(binding);
Expand All @@ -235,14 +228,14 @@ class Bridge {
///
/// @param session The new default. Pass `{}` to clear.
void setDefaultSession(::morph::session::Context session) {
std::scoped_lock lock{_sessionMtx};
std::scoped_lock const lock{_sessionMtx};
_defaultSession = std::move(session);
}

/// @brief Returns a copy of the currently installed default session. Thread-safe.
/// @return Snapshot of the default `Context`.
[[nodiscard]] ::morph::session::Context defaultSession() const {
std::scoped_lock lock{_sessionMtx};
std::scoped_lock const lock{_sessionMtx};
return _defaultSession;
}

Expand All @@ -263,7 +256,7 @@ class Bridge {
auto newShared = std::shared_ptr<::morph::backend::detail::IBackend>(std::move(newBackend));
std::shared_ptr<::morph::backend::detail::IBackend> previous;
{
std::scoped_lock lock{_mtx};
std::scoped_lock const lock{_mtx};

std::vector<std::weak_ptr<detail::HandlerBinding>> live;
for (auto& weak : _handlers) {
Expand Down Expand Up @@ -298,8 +291,8 @@ class Bridge {
/// complete with an error.
/// @param binding Binding to remove. Must have been returned by `registerHandler()`.
void deregisterHandler(const std::shared_ptr<detail::HandlerBinding>& binding) {
std::scoped_lock lock{_mtx};
uint64_t raw = binding->currentId.load();
std::scoped_lock const lock{_mtx};
uint64_t const raw = binding->currentId.load();
if (raw != 0U) {
loadBackend()->deregisterModel(::morph::exec::detail::ModelId{raw});
}
Expand Down Expand Up @@ -332,7 +325,7 @@ class Bridge {
using R = ::morph::model::ActionTraits<Action>::Result;

auto backend = loadBackend();
uint64_t raw = binding->currentId.load();
uint64_t const raw = binding->currentId.load();

auto typedState = std::make_shared<::morph::async::detail::CompletionState<R>>();
::morph::async::Completion<R> typed{typedState, cbExec};
Expand Down Expand Up @@ -372,7 +365,7 @@ class Bridge {
return result;
};
{
std::scoped_lock lock{_sessionMtx};
std::scoped_lock const lock{_sessionMtx};
call.session = _defaultSession;
}
auto anyCompletion = backend->execute(::morph::exec::detail::ModelId{raw}, std::move(call), cbExec);
Expand All @@ -386,13 +379,13 @@ class Bridge {

private:
std::shared_ptr<::morph::backend::detail::IBackend> loadBackend() const {
std::scoped_lock lock{_backendMtx};
std::scoped_lock const lock{_backendMtx};
return _backend;
}

std::shared_ptr<::morph::backend::detail::IBackend> exchangeBackend(
std::shared_ptr<::morph::backend::detail::IBackend> next) {
std::scoped_lock lock{_backendMtx};
std::scoped_lock const lock{_backendMtx};
auto previous = std::move(_backend);
_backend = std::move(next);
return previous;
Expand All @@ -405,10 +398,10 @@ class Bridge {
// The handler is invoked on the backend's transport thread. We keep a
// weak_ptr to the same shared backend so a stale callback fired after a
// switchBackend is a no-op.
std::weak_ptr<::morph::backend::detail::IBackend> weakBackend{backend};
std::weak_ptr<::morph::backend::detail::IBackend> const weakBackend{backend};
backend->setReconnectHandler([this, weakBackend] {
auto pinned = weakBackend.lock();
std::scoped_lock lock{_mtx};
std::scoped_lock const lock{_mtx};
if (!pinned || pinned != loadBackend()) {
return; // We've moved on to a different backend; ignore.
}
Expand Down Expand Up @@ -448,6 +441,7 @@ class Bridge {
///
/// @tparam Model Concrete model type.
template <typename Model>
// NOLINTNEXTLINE(cppcoreguidelines-special-member-functions)
class BridgeHandler {
public:
/// @brief Constructs and registers the handler using the default model factory.
Expand Down Expand Up @@ -607,6 +601,9 @@ class BridgeHandler {
Bridge* bridge{nullptr};
std::shared_ptr<detail::HandlerBinding> binding;
::morph::exec::IExecutor* guiExec{nullptr};
/// @note Keys are `std::string_view` pointing to string literals from
/// `ActionTraits<A>::typeId()` — all call sites pass compile-time strings
/// with static storage duration, so the map's keys never dangle.
std::unordered_map<std::string_view, SubscriberEntry> entries;
};

Expand Down Expand Up @@ -710,14 +707,15 @@ class BridgeHandler {
/// Placed here after BridgeHandler is fully defined so we can safely cast and call its methods.
template <typename Model, typename Action>
inline void ActionExecuteRegistry::registerAction(std::string_view modelId, std::string_view actionId) {
Key key{std::string{modelId}, std::string{actionId}};
Key const key{std::string{modelId}, std::string{actionId}};
_executors[key] = [](void* handlerVoid, std::string_view bodyJson) -> ::morph::async::Completion<std::string> {
auto* handler = static_cast<BridgeHandler<Model>*>(handlerVoid);
auto resultState = std::make_shared<::morph::async::detail::CompletionState<std::string>>();
try {
Action action = ::morph::model::ActionTraits<Action>::fromJson(bodyJson);
handler->template execute<Action>(std::move(action))
.then([resultState](typename ::morph::model::ActionTraits<Action>::Result result) {
// NOLINTNEXTLINE(performance-unnecessary-value-param) — lambda captures the result by value
.then([resultState](auto result) {
resultState->setValue(::morph::model::ActionTraits<Action>::resultToJson(result));
})
.onError([resultState](const std::exception_ptr& err) { resultState->setException(err); });
Expand Down
3 changes: 2 additions & 1 deletion include/morph/choice.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,10 @@ struct FixedString {

/// @brief Captures a string literal.
/// @param literal The literal to copy, e.g. `"ListSamples"`.
// NOLINTNEXTLINE(modernize-avoid-c-arrays) — string literals ARE C arrays.
// NOLINTNEXTLINE(modernize-avoid-c-arrays, cppcoreguidelines-avoid-c-arrays, cppcoreguidelines-pro-bounds-constant-array-index, cppcoreguidelines-pro-bounds-avoid-unchecked-container-access) — string literals ARE C arrays.
consteval FixedString(const char (&literal)[N]) noexcept {
for (std::size_t index = 0; index < N; ++index) {
// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index, cppcoreguidelines-pro-bounds-avoid-unchecked-container-access)
data[index] = literal[index];
}
}
Expand Down
Loading
Loading