Program Listing for File TrackedDeck.hpp

↰ Return to documentation for file (src/utils/TrackedDeck.hpp)

#ifndef TRACKEDDECK_HPP
#define TRACKEDDECK_HPP

#include "../pdfs/PDF.hpp"
#include "ParseUtils.hpp"
#include "TOMLUtils.hpp"
#include <algorithm>
#include <cctype>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <map>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <toml.hpp>
#include <utility>
#include <vector>

namespace utils
{
    struct DeckKeyReport
    {
        // NOLINTBEGIN(readability-identifier-naming) -- trailing underscore matches this project's own member-variable convention (see yields::YieldChannelDescriptor's identical public-struct-fields-with-trailing-underscore precedent)
        std::string path_;
        std::uint32_t line_ = 0;
        std::uint32_t column_ = 0;
        std::string hint_;
        std::string reason_;
        // NOLINTEND(readability-identifier-naming)
    };

    namespace trackeddeck_detail
    {
        inline auto canonicalPath(const std::string_view path) -> std::string
        {
            const toml::path parsed{ path };
            return parsed ? parsed.str() : std::string(path);
        }

        inline auto childPath(const std::string& prefix, const std::string_view key) -> std::string
        {
            const bool bare = !key.empty() && std::ranges::all_of(key, [](const unsigned char c)
                { return (std::isalnum(c) != 0) || c == '_' || c == '-'; });
            std::string text = bare ? std::string(key) : std::string("\"");
            if (!bare)
            {
                for (const char c : key)
                {
                    if (c == '"' || c == '\\') { text += '\\'; }
                    text += c;
                }
                text += '"';
            }
            return prefix.empty() ? text : prefix + "." + text;
        }

        inline auto isUnder(const std::string_view path, const std::string_view prefix) -> bool
        {
            if (!path.starts_with(prefix)) { return false; }
            return path.size() == prefix.size() || path[prefix.size()] == '.' || path[prefix.size()] == '[';
        }

        inline auto lastSegment(const std::string_view path) -> std::string
        {
            const auto dot = path.rfind('.');
            return std::string(dot == std::string_view::npos ? path : path.substr(dot + 1));
        }

        inline auto editDistance(const std::string_view a, const std::string_view b) -> std::size_t
        {
            std::vector<std::size_t> prev(b.size() + 1);
            std::vector<std::size_t> cur(b.size() + 1);
            for (std::size_t j = 0; j <= b.size(); ++j) { prev[j] = j; }
            for (std::size_t i = 1; i <= a.size(); ++i)
            {
                cur[0] = i;
                for (std::size_t j = 1; j <= b.size(); ++j)
                {
                    const std::size_t sub = prev[j - 1] + (a[i - 1] == b[j - 1] ? 0 : 1);
                    cur[j] = std::min({ prev[j] + 1, cur[j - 1] + 1, sub });
                }
                std::swap(prev, cur);
            }
            return prev[b.size()];
        }

        inline auto suggestKey(const std::string& unusedPath, const std::set<std::string>& wanted) -> std::string
        {
            const std::string leaf = lastSegment(unusedPath);
            const auto same = std::ranges::find_if(wanted,
                [&leaf](const std::string& w) { return lastSegment(w) == leaf; });
            if (same != wanted.end()) { return "did you mean '" + *same + "'?"; }

            constexpr std::size_t maxDistance = 2;
            constexpr std::size_t minLength = 4;
            if (leaf.size() < minLength) { return {}; }
            const std::string* best = nullptr;
            std::size_t bestDist = maxDistance + 1;
            for (const std::string& w : wanted)
            {
                const std::size_t d = editDistance(leaf, lastSegment(w));
                if (d < bestDist) { bestDist = d; best = &w; }
            }
            return best == nullptr ? std::string() : "did you mean '" + *best + "'?";
        }
    } // namespace trackeddeck_detail

    class TrackedDeck
    {
    public:
        explicit TrackedDeck(const toml::table& deck) : deck_(std::cref(deck))
        {
            collectLeaves(deck, std::string());
        }

        [[nodiscard]] auto atPath(const std::string_view path) const -> toml::node_view<const toml::node>
        {
            const auto node = deck_.get().at_path(path);
            const std::string key = trackeddeck_detail::canonicalPath(path);
            if (!node)
            {
                wanted_.insert(key);
            }
            else if (!node.is_table())
            {
                markLeafOrAncestor(key);
            }
            return node;
        }

        template<class T>
        [[nodiscard]] auto value(const std::string& path, const bool required = false) const -> std::optional<T>
        {
            static_cast<void>(atPath(path));
            return utils::getTOMLKeyWithError<T>(deck_.get(), path, required);
        }

        [[nodiscard]] auto initPDF(const std::string& path, const std::string& prefix = "") const -> pdfs::PDF
        {
            static_cast<void>(atPath(path));
            return utils::initPDFFromKey(deck_.get(), path, prefix);
        }

        void markUsedAll(const std::string_view prefix) const
        {
            const std::string p = trackeddeck_detail::canonicalPath(prefix);
            for (auto& [path, leaf] : leaves_)
            {
                if (trackeddeck_detail::isUnder(path, p)) { leaf.used_ = true; }
            }
        }

        void markIgnored(const std::string_view prefix, const std::string_view reason) const
        {
            const std::string p = trackeddeck_detail::canonicalPath(prefix);
            for (auto& [path, leaf] : leaves_)
            {
                if (!leaf.used_ && leaf.reason_.empty() && trackeddeck_detail::isUnder(path, p))
                {
                    leaf.reason_ = std::string(reason);
                }
            }
        }

        [[nodiscard]] auto unused() const -> std::vector<DeckKeyReport>
        {
            std::vector<DeckKeyReport> result;
            for (const auto& [path, leaf] : leaves_)
            {
                if (leaf.used_ || !leaf.reason_.empty()) { continue; }
                result.push_back({ path, leaf.line_, leaf.column_,
                    trackeddeck_detail::suggestKey(path, wanted_), std::string() });
            }
            return result;
        }

        [[nodiscard]] auto ignored() const -> std::vector<DeckKeyReport>
        {
            std::vector<DeckKeyReport> result;
            for (const auto& [path, leaf] : leaves_)
            {
                if (leaf.used_ || leaf.reason_.empty()) { continue; }
                result.push_back({ path, leaf.line_, leaf.column_, std::string(), leaf.reason_ });
            }
            return result;
        }

    private:
        struct Leaf
        {
            // NOLINTBEGIN(readability-identifier-naming) -- trailing underscore matches this project's own member-variable convention, as for DeckKeyReport above
            bool used_ = false;
            std::string reason_;
            std::uint32_t line_ = 0;
            std::uint32_t column_ = 0;
            // NOLINTEND(readability-identifier-naming)
        };

        void collectLeaves(const toml::table& table, const std::string& prefix)
        {
            for (const auto& [key, node] : table)
            {
                const std::string path = trackeddeck_detail::childPath(prefix, key.str());
                if (const auto* sub = node.as_table()) { collectLeaves(*sub, path); continue; }
                Leaf leaf;
                leaf.line_ = node.source().begin.line;
                leaf.column_ = node.source().begin.column;
                leaves_.emplace(path, leaf);
            }
        }

        void markLeafOrAncestor(std::string key) const
        {
            while (!key.empty())
            {
                const auto it = leaves_.find(key);
                if (it != leaves_.end()) { it->second.used_ = true; return; }
                const auto cut = key.find_last_of(".[");
                if (cut == std::string::npos) { return; }
                key.resize(cut);
            }
        }

        std::reference_wrapper<const toml::table> deck_;
        mutable std::map<std::string, Leaf> leaves_;
        mutable std::set<std::string> wanted_;
    };

} // namespace utils

#endif // TRACKEDDECK_HPP