Merge changes Ia9ba2193,I736bf655

* changes:
  Remove empty SECTION
  Fix leafTypeName returning names of primitive types
diff --git a/src/ast_handlers.hpp b/src/ast_handlers.hpp
index ab9958a..33715a4 100644
--- a/src/ast_handlers.hpp
+++ b/src/ast_handlers.hpp
@@ -120,18 +120,6 @@
         return x3::error_handler_result::rethrow;
     }
 };
-struct list_class {
-    template <typename T, typename Iterator, typename Context>
-    void on_success(Iterator const&, Iterator const&, T& ast, Context const& context)
-    {
-        auto& parserContext = x3::get<parser_context_tag>(context);
-        const Schema& schema = parserContext.m_schema;
-
-        if (!schema.isList(parserContext.currentSchemaPath(), {parserContext.m_curModule, ast.m_name})) {
-            _pass(context) = false;
-        }
-    }
-};
 
 struct module_class {
     template <typename T, typename Iterator, typename Context>
@@ -150,15 +138,6 @@
     }
 };
 
-struct dataNodeList_class {
-    template <typename T, typename Iterator, typename Context>
-    void on_success(Iterator const&, Iterator const&, T& ast, Context const& context)
-    {
-        auto& parserContext = x3::get<parser_context_tag>(context);
-        parserContext.pushPathFragment(ast);
-    }
-};
-
 struct absoluteStart_class {
     template <typename T, typename Iterator, typename Context>
     void on_success(Iterator const&, Iterator const&, T&, Context const& context)
@@ -168,10 +147,6 @@
     }
 };
 
-struct dataNodesListEnd_class;
-
-struct dataPathListEnd_class;
-
 struct discard_class;
 
 struct ls_class;
@@ -193,6 +168,12 @@
     {
         auto& parserContext = x3::get<parser_context_tag>(context);
         const auto& schema = parserContext.m_schema;
+        if (ast.m_nodes.empty()) {
+            parserContext.m_errorMsg = "This container is not a presence container.";
+            _pass(context) = false;
+            return;
+        }
+
         try {
             boost::optional<std::string> module;
             if (ast.m_nodes.back().m_prefix)
@@ -216,6 +197,12 @@
     void on_success(Iterator const&, Iterator const&, T& ast, Context const& context)
     {
         auto& parserContext = x3::get<parser_context_tag>(context);
+        if (ast.m_nodes.empty()) {
+            parserContext.m_errorMsg = "This is not a list instance.";
+            _pass(context) = false;
+            return;
+        }
+
         if (ast.m_nodes.back().m_suffix.type() != typeid(listElement_)) {
             parserContext.m_errorMsg = "This is not a list instance.";
             _pass(context) = false;
@@ -255,11 +242,17 @@
     }
 };
 
-struct leaf_path_class {
+struct writable_leaf_path_class {
     template <typename T, typename Iterator, typename Context>
     void on_success(Iterator const&, Iterator const&, T&, Context const& context)
     {
         auto& parserContext = x3::get<parser_context_tag>(context);
+        if (parserContext.currentSchemaPath().m_nodes.empty()) {
+            parserContext.m_errorMsg = "This is not a path to leaf.";
+            _pass(context) = false;
+            return;
+        }
+
         try {
             auto lastNode = parserContext.currentSchemaPath().m_nodes.back();
             auto leaf = boost::get<leaf_>(lastNode.m_suffix);
@@ -341,36 +334,6 @@
 
 struct trailingSlash_class;
 
-struct createPathSuggestions_class {
-    template <typename T, typename Iterator, typename Context>
-    void on_success(Iterator const& begin, Iterator const&, T&, Context const& context)
-    {
-        auto& parserContext = x3::get<parser_context_tag>(context);
-        const auto& schema = parserContext.m_schema;
-
-        parserContext.m_completionIterator = begin;
-        auto suggestions = schema.availableNodes(parserContext.currentSchemaPath(), Recursion::NonRecursive);
-        std::set<Completion> suffixesAdded;
-        std::transform(suggestions.begin(), suggestions.end(),
-                std::inserter(suffixesAdded, suffixesAdded.end()),
-                [&parserContext, &schema](const ModuleNodePair& node) {
-            std::string completion = (node.first ? *node.first + ":" : "") + node.second;
-
-            if (schema.isLeaf(parserContext.currentSchemaPath(), node)) {
-                return Completion{completion + " "};
-            }
-            if (schema.isContainer(parserContext.currentSchemaPath(), node)) {
-                return Completion{completion + "/"};
-            }
-            if (schema.isList(parserContext.currentSchemaPath(), node)) {
-                return Completion{completion, "[", Completion::WhenToAdd::IfFullMatch};
-            }
-            return Completion{completion};
-        });
-        parserContext.m_suggestions = suffixesAdded;
-    }
-};
-
 std::set<Completion> generateMissingKeyCompletionSet(std::set<std::string> keysNeeded, std::map<std::string, leaf_data_> currentSet);
 
 struct createKeySuggestions_class {
diff --git a/src/grammars.hpp b/src/grammars.hpp
index 74a6a17..1b4b49d 100644
--- a/src/grammars.hpp
+++ b/src/grammars.hpp
@@ -47,7 +47,7 @@
 } const ls_options;
 
 auto const ls_def =
-    ls_::name >> *(space_separator >> ls_options) >> -(space_separator >> (dataPathListEnd | anyPath | (module >> "*")));
+    ls_::name >> *(space_separator >> ls_options) >> -(space_separator >> (anyPath | (module >> "*")));
 
 auto const cd_def =
     cd_::name >> space_separator > dataPath;
@@ -62,7 +62,7 @@
     get_::name >> -(space_separator >> ((dataPathListEnd | dataPath) | (module >> "*")));
 
 auto const set_def =
-    set_::name >> space_separator > leafPath > space_separator > leaf_data;
+    set_::name >> space_separator > writableLeafPath > space_separator > leaf_data;
 
 auto const commit_def =
     commit_::name >> x3::attr(commit_());
diff --git a/src/parser_context.hpp b/src/parser_context.hpp
index f837934..87706a5 100644
--- a/src/parser_context.hpp
+++ b/src/parser_context.hpp
@@ -10,6 +10,7 @@
 #include "completion.hpp"
 #include "data_query.hpp"
 #include "schema.hpp"
+
 struct ParserContext {
     ParserContext(const Schema& schema, const std::shared_ptr<const DataQuery> dataQuery, const dataPath_& curDir);
     schemaPath_ currentSchemaPath();
diff --git a/src/path_parser.hpp b/src/path_parser.hpp
index 6360912..15655d7 100644
--- a/src/path_parser.hpp
+++ b/src/path_parser.hpp
@@ -14,48 +14,87 @@
 
 namespace x3 = boost::spirit::x3;
 
-x3::rule<dataNodeList_class, decltype(dataPath_::m_nodes)::value_type> const dataNodeList = "dataNodeList";
-x3::rule<dataNodesListEnd_class, decltype(dataPath_::m_nodes)> const dataNodesListEnd = "dataNodesListEnd";
-x3::rule<dataPathListEnd_class, dataPath_> const dataPathListEnd = "dataPathListEnd";
-x3::rule<leaf_path_class, dataPath_> const leafPath = "leafPath";
+x3::rule<writable_leaf_path_class, dataPath_> const writableLeafPath = "writableLeafPath";
 x3::rule<presenceContainerPath_class, dataPath_> const presenceContainerPath = "presenceContainerPath";
 x3::rule<listInstancePath_class, dataPath_> const listInstancePath = "listInstancePath";
 x3::rule<initializePath_class, x3::unused_type> const initializePath = "initializePath";
-x3::rule<createPathSuggestions_class, x3::unused_type> const createPathSuggestions = "createPathSuggestions";
 x3::rule<trailingSlash_class, TrailingSlash> const trailingSlash = "trailingSlash";
 x3::rule<absoluteStart_class, Scope> const absoluteStart = "absoluteStart";
 x3::rule<keyValue_class, keyValue_> const keyValue = "keyValue";
 x3::rule<key_identifier_class, std::string> const key_identifier = "key_identifier";
 x3::rule<listSuffix_class, std::vector<keyValue_>> const listSuffix = "listSuffix";
-x3::rule<list_class, list_> const list = "list";
 x3::rule<createKeySuggestions_class, x3::unused_type> const createKeySuggestions = "createKeySuggestions";
 x3::rule<createValueSuggestions_class, x3::unused_type> const createValueSuggestions = "createValueSuggestions";
 x3::rule<suggestKeysEnd_class, x3::unused_type> const suggestKeysEnd = "suggestKeysEnd";
 
-template <typename NodeType>
-struct NodeParser : x3::parser<NodeParser<NodeType>> {
-    using attribute_type = NodeType;
+enum class NodeParserMode {
+    CompleteDataNode,
+    IncompleteDataNode,
+    CompletionsOnly,
+    SchemaNode
+};
+
+template <auto>
+struct ModeToAttribute;
+template <>
+struct ModeToAttribute<NodeParserMode::CompleteDataNode> {
+    using type = dataNode_;
+};
+template <>
+struct ModeToAttribute<NodeParserMode::IncompleteDataNode> {
+    using type = dataNode_;
+};
+template <>
+struct ModeToAttribute<NodeParserMode::SchemaNode> {
+    using type = schemaNode_;
+};
+// The CompletionsOnly attribute is dataNode_ only because of convenience:
+// having the same return type means we can get by without a ton of `if constexpr` stanzas.
+// So the code will still "parse data into the target attr" for simplicity.
+template <>
+struct ModeToAttribute<NodeParserMode::CompletionsOnly> {
+    using type = dataNode_;
+};
+
+template <NodeParserMode PARSER_MODE>
+struct NodeParser : x3::parser<NodeParser<PARSER_MODE>> {
+    using attribute_type = typename ModeToAttribute<PARSER_MODE>::type;
+
+    std::function<bool(const Schema&, const std::string& path)> m_filterFunction;
+
+    NodeParser(const std::function<bool(const Schema&, const std::string& path)>& filterFunction)
+        : m_filterFunction(filterFunction)
+    {
+    }
+
+    // GCC complains that `end` isn't used when doing completions only
+    // FIXME: GCC 10.1 doesn't emit a warning here. Remove [[maybe_unused]] when GCC 10 is available
     template <typename It, typename Ctx, typename RCtx, typename Attr>
-    bool parse(It& begin, It end, Ctx const& ctx, RCtx& rctx, Attr& attr) const
+    bool parse(It& begin, [[maybe_unused]] It end, Ctx const& ctx, RCtx& rctx, Attr& attr) const
     {
         std::string tableName;
-        if constexpr (std::is_same<NodeType, schemaNode_>()) {
+        if constexpr (std::is_same<attribute_type, schemaNode_>()) {
             tableName = "schemaNode";
         } else {
             tableName = "dataNode";
         }
-        x3::symbols<NodeType> table(tableName);
+        x3::symbols<attribute_type> table(tableName);
 
         ParserContext& parserContext = x3::get<parser_context_tag>(ctx);
         parserContext.m_suggestions.clear();
         for (const auto& child : parserContext.m_schema.availableNodes(parserContext.currentSchemaPath(), Recursion::NonRecursive)) {
-            NodeType out;
+            attribute_type out;
             std::string parseString;
             if (child.first) {
                 out.m_prefix = module_{*child.first};
                 parseString = *child.first + ":";
             }
             parseString += child.second;
+
+            if (!m_filterFunction(parserContext.m_schema, joinPaths(pathToSchemaString(parserContext.currentSchemaPath(), Prefixes::Always), parseString))) {
+                continue;
+            }
+
             switch (parserContext.m_schema.nodeType(parserContext.currentSchemaPath(), child)) {
                 case yang::NodeTypes::Container:
                 case yang::NodeTypes::PresenceContainer:
@@ -67,7 +106,7 @@
                     parserContext.m_suggestions.emplace(Completion{parseString + " "});
                     break;
                 case yang::NodeTypes::List:
-                    if constexpr (std::is_same<NodeType, schemaNode_>()) {
+                    if constexpr (std::is_same<attribute_type, schemaNode_>()) {
                         out.m_suffix = list_{child.second};
                     } else {
                         out.m_suffix = listElement_{child.second, {}};
@@ -82,7 +121,7 @@
                     continue;
             }
             table.add(parseString, out);
-            table.add("..", NodeType{nodeup_{}});
+            table.add("..", attribute_type{nodeup_{}});
             if (!child.first) {
                 auto topLevelModule = parserContext.currentSchemaPath().m_nodes.begin()->m_prefix;
                 out.m_prefix = topLevelModule;
@@ -90,73 +129,150 @@
             }
         }
         parserContext.m_completionIterator = begin;
-        auto res = table.parse(begin, end, ctx, rctx, attr);
 
-        if (attr.m_prefix) {
-            parserContext.m_curModule = attr.m_prefix->m_name;
-        }
-
-        if (attr.m_suffix.type() == typeid(leaf_)) {
-            parserContext.m_tmpListKeyLeafPath.m_location = parserContext.currentSchemaPath();
-            ModuleNodePair node{attr.m_prefix.flat_map([](const auto& it) {
-                return boost::optional<std::string>{it.m_name};
-            }), boost::get<leaf_>(attr.m_suffix).m_name};
-            parserContext.m_tmpListKeyLeafPath.m_node = node;
-        }
-
-        if constexpr (std::is_same<NodeType, dataNode_>()) {
-            if (attr.m_suffix.type() == typeid(listElement_)) {
-                parserContext.m_tmpListName = boost::get<listElement_>(attr.m_suffix).m_name;
-                res = listSuffix.parse(begin, end, ctx, rctx, boost::get<listElement_>(attr.m_suffix).m_keys);
+        if constexpr (PARSER_MODE == NodeParserMode::CompletionsOnly) {
+            return true;
+        } else {
+            It saveIter;
+            // GCC complains that I assign saveIter because I use it only if NodeType is dataNode_
+            // FIXME: GCC 10.1 doesn't emit a warning here. Make this unconditional when GCC 10 is available.
+            if constexpr (std::is_same<attribute_type, dataNode_>()) {
+                saveIter = begin;
             }
-        }
 
-        if (res) {
-            parserContext.pushPathFragment(attr);
-            parserContext.m_topLevelModulePresent = true;
-        }
+            auto res = table.parse(begin, end, ctx, rctx, attr);
 
-        if (attr.m_prefix) {
-            parserContext.m_curModule = boost::none;
+            if (attr.m_prefix) {
+                parserContext.m_curModule = attr.m_prefix->m_name;
+            }
+
+            if (attr.m_suffix.type() == typeid(leaf_)) {
+                parserContext.m_tmpListKeyLeafPath.m_location = parserContext.currentSchemaPath();
+                ModuleNodePair node{attr.m_prefix.flat_map([](const auto& it) {
+                                        return boost::optional<std::string>{it.m_name};
+                                    }),
+                                    boost::get<leaf_>(attr.m_suffix).m_name};
+                parserContext.m_tmpListKeyLeafPath.m_node = node;
+            }
+
+            if constexpr (std::is_same<attribute_type, dataNode_>()) {
+                if (attr.m_suffix.type() == typeid(listElement_)) {
+                    parserContext.m_tmpListName = boost::get<listElement_>(attr.m_suffix).m_name;
+                    res = listSuffix.parse(begin, end, ctx, rctx, boost::get<listElement_>(attr.m_suffix).m_keys);
+
+                    // FIXME: think of a better way to do this, that is, get rid of manual iterator reverting
+                    if (!res) {
+                        // If listSuffix didn't succeed, we check, if we allow incomplete nodes. If we do, then we replace listElement_ with list_.
+                        // If we don't, we fail the whole symbol table.
+                        if constexpr (PARSER_MODE == NodeParserMode::IncompleteDataNode) {
+                            res = true;
+                            attr.m_suffix = list_{boost::get<listElement_>(attr.m_suffix).m_name};
+                        } else {
+                            begin = saveIter;
+                        }
+                    }
+                }
+            }
+
+            if (res) {
+                parserContext.pushPathFragment(attr);
+                parserContext.m_topLevelModulePresent = true;
+            }
+
+            if (attr.m_prefix) {
+                parserContext.m_curModule = boost::none;
+            }
+            return res;
         }
-        return res;
     }
 };
 
-NodeParser<schemaNode_> schemaNode;
-NodeParser<dataNode_> dataNode;
+using schemaNode = NodeParser<NodeParserMode::SchemaNode>;
+using dataNode = NodeParser<NodeParserMode::CompleteDataNode>;
+using dataNodeAllowList = NodeParser<NodeParserMode::IncompleteDataNode>;
+using pathCompletions = NodeParser<NodeParserMode::CompletionsOnly>;
 
 using AnyPath = boost::variant<schemaPath_, dataPath_>;
 
-template <typename PathType>
-struct PathParser : x3::parser<PathParser<PathType>> {
-    using attribute_type = PathType;
+enum class PathParserMode {
+    AnyPath,
+    DataPath,
+    DataPathListEnd
+};
+
+template <>
+struct ModeToAttribute<PathParserMode::AnyPath> {
+    using type = AnyPath;
+};
+
+template <>
+struct ModeToAttribute<PathParserMode::DataPath> {
+    using type = dataPath_;
+};
+
+template <>
+struct ModeToAttribute<PathParserMode::DataPathListEnd> {
+    using type = dataPath_;
+};
+
+template <PathParserMode PARSER_MODE>
+struct PathParser : x3::parser<PathParser<PARSER_MODE>> {
+    using attribute_type = ModeToAttribute<PARSER_MODE>;
+    std::function<bool(const Schema&, const std::string& path)> m_filterFunction;
+
+    PathParser(const std::function<bool(const Schema&, const std::string& path)>& filterFunction = [] (const auto&, const auto&) {return true;})
+        : m_filterFunction(filterFunction)
+    {
+    }
+
     template <typename It, typename Ctx, typename RCtx, typename Attr>
     bool parse(It& begin, It end, Ctx const& ctx, RCtx& rctx, Attr& attr) const
     {
         initializePath.parse(begin, end, ctx, rctx, x3::unused);
         dataPath_ attrData;
 
+        auto pathEnd = x3::rule<class PathEnd>{"pathEnd"} = &space_separator | x3::eoi;
         // absoluteStart has to be separate from the dataPath parser,
         // otherwise, if the "dataNode % '/'" parser fails, the begin iterator
         // gets reverted to before the starting slash.
-        auto res = -absoluteStart.parse(begin, end, ctx, rctx, attrData.m_scope);
-        auto dataPath = x3::attr(attrData.m_scope) >> dataNode % '/' >> -trailingSlash;
+        auto res = (-absoluteStart).parse(begin, end, ctx, rctx, attrData.m_scope);
+        auto dataPath = x3::attr(attrData.m_scope)
+            >> (dataNode{m_filterFunction} % '/' | pathEnd >> x3::attr(std::vector<dataNode_>{}))
+            >> -trailingSlash;
         res = dataPath.parse(begin, end, ctx, rctx, attrData);
-        attr = attrData;
 
-        if constexpr (std::is_same<Attr, AnyPath>()) {
-            auto pathEnd = x3::rule<class PathEnd>{"pathEnd"} = &space_separator | x3::eoi;
-            // If parsing failed, or if there's more input we try parsing schema nodes
+        // If we allow data paths with a list at the end, we just try to parse that separately.
+        if constexpr (PARSER_MODE == PathParserMode::DataPathListEnd || PARSER_MODE == PathParserMode::AnyPath) {
             if (!res || !pathEnd.parse(begin, end, ctx, rctx, x3::unused)) {
-                // If dataPath parsed some nodes, they will be saved in `attrData`. We have to keep these.
-                schemaPath_ attrSchema = dataPathToSchemaPath(attrData);
-                auto schemaPath = schemaNode % '/';
-                // The schemaPath parser continues where the dataPath parser ended.
-                res = schemaPath.parse(begin, end, ctx, rctx, attrSchema.m_nodes);
-                auto trailing = -trailingSlash >> pathEnd;
-                res = trailing.parse(begin, end, ctx, rctx, attrSchema.m_trailingSlash);
-                attr = attrSchema;
+                dataNode_ attrNodeList;
+                res = dataNodeAllowList{m_filterFunction}.parse(begin, end, ctx, rctx, attrNodeList);
+                if (res) {
+                    attrData.m_nodes.push_back(attrNodeList);
+                    // If the trailing slash matches, no more nodes are parsed.
+                    // That means no more completion. So, I generate them
+                    // manually.
+                    res = (-(trailingSlash >> x3::omit[pathCompletions{m_filterFunction}])).parse(begin, end, ctx, rctx, attrData.m_trailingSlash);
+                }
+            }
+        }
+
+        attr = attrData;
+        if constexpr (PARSER_MODE == PathParserMode::AnyPath) {
+            // If our data path already has some listElement_ fragments, we can't parse rest of the path as a schema path
+            auto hasLists = std::any_of(attrData.m_nodes.begin(), attrData.m_nodes.end(),
+                [] (const auto& node) { return node.m_suffix.type() == typeid(listElement_); });
+            // If parsing failed, or if there's more input we try parsing schema nodes.
+            if (!hasLists) {
+                if (!res || !pathEnd.parse(begin, end, ctx, rctx, x3::unused)) {
+                    // If dataPath parsed some nodes, they will be saved in `attrData`. We have to keep these.
+                    schemaPath_ attrSchema = dataPathToSchemaPath(attrData);
+                    auto schemaPath = schemaNode{m_filterFunction} % '/';
+                    // The schemaPath parser continues where the dataPath parser ended.
+                    res = schemaPath.parse(begin, end, ctx, rctx, attrSchema.m_nodes);
+                    auto trailing = -trailingSlash >> pathEnd;
+                    res = trailing.parse(begin, end, ctx, rctx, attrSchema.m_trailingSlash);
+                    attr = attrSchema;
+                }
             }
         }
         return res;
@@ -170,8 +286,9 @@
 // The PathParser class would get a boost::variant as the attribute, but I
 // don't want to deal with that, so I use these wrappers to ensure the
 // attribute I want (and let Spirit deal with boost::variant).
-auto const anyPath = x3::rule<class anyPath_class, AnyPath>{"anyPath"} = PathParser<AnyPath>{};
-auto const dataPath = x3::rule<class dataPath_class, dataPath_>{"dataPath"} = PathParser<dataPath_>{};
+auto const anyPath = x3::rule<class anyPath_class, AnyPath>{"anyPath"} = PathParser<PathParserMode::AnyPath>{};
+auto const dataPath = x3::rule<class dataPath_class, dataPath_>{"dataPath"} = PathParser<PathParserMode::DataPath>{};
+auto const dataPathListEnd = x3::rule<class dataPath_class, dataPath_>{"dataPath"} = PathParser<PathParserMode::DataPathListEnd>{};
 
 #if __clang__
 #pragma GCC diagnostic push
@@ -214,24 +331,12 @@
 auto const trailingSlash_def =
     x3::omit['/'] >> x3::attr(TrailingSlash::Present);
 
-auto const createPathSuggestions_def =
-    x3::eps;
+auto const filterConfigFalse = [] (const Schema& schema, const std::string& path) {
+    return schema.isConfig(path);
+};
 
-auto const dataNodeList_def =
-    createPathSuggestions >> -(module) >> list;
-
-// This intermediate rule is mandatory, because we need the first alternative
-// to be collapsed to a vector. If we didn't use the intermediate rule,
-// Spirit wouldn't know we want it to collapse.
-// https://github.com/boostorg/spirit/issues/408
-auto const dataNodesListEnd_def =
-    dataNode % '/' >> '/' >> dataNodeList >> -(&char_('/') >> createPathSuggestions) |
-    x3::attr(decltype(dataPath_::m_nodes)()) >> dataNodeList;
-
-auto const dataPathListEnd_def = initializePath >> absoluteStart >> createPathSuggestions >> x3::attr(decltype(dataPath_::m_nodes)()) >> x3::attr(TrailingSlash::NonPresent) >> x3::eoi | initializePath >> -(absoluteStart >> createPathSuggestions) >> dataNodesListEnd >> (-(trailingSlash >> createPathSuggestions) >> -(completing >> rest) >> (&space_separator | x3::eoi));
-
-auto const leafPath_def =
-    dataPath;
+auto const writableLeafPath_def =
+    PathParser<PathParserMode::DataPath>{filterConfigFalse};
 
 auto const presenceContainerPath_def =
     dataPath;
@@ -252,16 +357,11 @@
 BOOST_SPIRIT_DEFINE(keyValue)
 BOOST_SPIRIT_DEFINE(key_identifier)
 BOOST_SPIRIT_DEFINE(listSuffix)
-BOOST_SPIRIT_DEFINE(list)
-BOOST_SPIRIT_DEFINE(dataNodeList)
-BOOST_SPIRIT_DEFINE(dataNodesListEnd)
-BOOST_SPIRIT_DEFINE(leafPath)
+BOOST_SPIRIT_DEFINE(writableLeafPath)
 BOOST_SPIRIT_DEFINE(presenceContainerPath)
 BOOST_SPIRIT_DEFINE(listInstancePath)
-BOOST_SPIRIT_DEFINE(dataPathListEnd)
 BOOST_SPIRIT_DEFINE(initializePath)
 BOOST_SPIRIT_DEFINE(createKeySuggestions)
-BOOST_SPIRIT_DEFINE(createPathSuggestions)
 BOOST_SPIRIT_DEFINE(createValueSuggestions)
 BOOST_SPIRIT_DEFINE(suggestKeysEnd)
 BOOST_SPIRIT_DEFINE(absoluteStart)
diff --git a/src/static_schema.cpp b/src/static_schema.cpp
index aecad5a..738ce49 100644
--- a/src/static_schema.cpp
+++ b/src/static_schema.cpp
@@ -12,10 +12,10 @@
 
 StaticSchema::StaticSchema()
 {
-    m_nodes.emplace("/", std::unordered_map<std::string, NodeType>());
+    m_nodes.emplace("/", std::unordered_map<std::string, NodeInfo>());
 }
 
-const std::unordered_map<std::string, NodeType>& StaticSchema::children(const std::string& name) const
+const std::unordered_map<std::string, NodeInfo>& StaticSchema::children(const std::string& name) const
 {
     return m_nodes.at(name);
 }
@@ -36,11 +36,11 @@
 
 void StaticSchema::addContainer(const std::string& location, const std::string& name, yang::ContainerTraits isPresence)
 {
-    m_nodes.at(location).emplace(name, yang::container{isPresence});
+    m_nodes.at(location).emplace(name, NodeInfo{yang::container{isPresence}, yang::AccessType::Writable});
 
     //create a new set of children for the new node
     std::string key = joinPaths(location, name);
-    m_nodes.emplace(key, std::unordered_map<std::string, NodeType>());
+    m_nodes.emplace(key, std::unordered_map<std::string, NodeInfo>());
 }
 
 bool StaticSchema::listHasKey(const schemaPath_& location, const ModuleNodePair& node, const std::string& key) const
@@ -49,7 +49,7 @@
     assert(isList(location, node));
 
     const auto& child = children(locationString).at(fullNodeName(location, node));
-    const auto& list = boost::get<yang::list>(child);
+    const auto& list = boost::get<yang::list>(child.m_nodeType);
     return list.m_keys.find(key) != list.m_keys.end();
 }
 
@@ -59,16 +59,16 @@
     assert(isList(location, node));
 
     const auto& child = children(locationString).at(fullNodeName(location, node));
-    const auto& list = boost::get<yang::list>(child);
+    const auto& list = boost::get<yang::list>(child.m_nodeType);
     return list.m_keys;
 }
 
 void StaticSchema::addList(const std::string& location, const std::string& name, const std::set<std::string>& keys)
 {
-    m_nodes.at(location).emplace(name, yang::list{keys});
+    m_nodes.at(location).emplace(name, NodeInfo{yang::list{keys}, yang::AccessType::Writable});
 
     std::string key = joinPaths(location, name);
-    m_nodes.emplace(key, std::unordered_map<std::string, NodeType>());
+    m_nodes.emplace(key, std::unordered_map<std::string, NodeInfo>());
 }
 
 std::set<identityRef_> StaticSchema::validIdentities(std::string_view module, std::string_view value)
@@ -79,11 +79,11 @@
     return identities;
 }
 
-void StaticSchema::addLeaf(const std::string& location, const std::string& name, const yang::LeafDataType& type)
+void StaticSchema::addLeaf(const std::string& location, const std::string& name, const yang::LeafDataType& type, const yang::AccessType accessType)
 {
-    m_nodes.at(location).emplace(name, yang::leaf{yang::TypeInfo{type, std::nullopt}});
+    m_nodes.at(location).emplace(name, NodeInfo{yang::leaf{yang::TypeInfo{type, std::nullopt}}, accessType});
     std::string key = joinPaths(location, name);
-    m_nodes.emplace(key, std::unordered_map<std::string, NodeType>());
+    m_nodes.emplace(key, std::unordered_map<std::string, NodeInfo>());
 }
 
 void StaticSchema::addModule(const std::string& name)
@@ -120,14 +120,14 @@
 yang::TypeInfo StaticSchema::leafType(const schemaPath_& location, const ModuleNodePair& node) const
 {
     std::string locationString = pathToSchemaString(location, Prefixes::Always);
-    return boost::get<yang::leaf>(children(locationString).at(fullNodeName(location, node))).m_type;
+    return boost::get<yang::leaf>(children(locationString).at(fullNodeName(location, node)).m_nodeType).m_type;
 }
 
 yang::TypeInfo StaticSchema::leafType(const std::string& path) const
 {
     auto locationString = stripLastNodeFromPath(path);
     auto node = lastNodeOfSchemaPath(path);
-    return boost::get<yang::leaf>(children(locationString).at(node)).m_type;
+    return boost::get<yang::leaf>(children(locationString).at(node).m_nodeType).m_type;
 }
 
 std::set<ModuleNodePair> StaticSchema::availableNodes(const boost::variant<dataPath_, schemaPath_, module_>& path, const Recursion recursion) const
@@ -185,18 +185,18 @@
     try {
         auto targetNode = children(locationString).at(fullName);
 
-        if (targetNode.type() == typeid(yang::container)) {
-            if (boost::get<yang::container>(targetNode).m_presence == yang::ContainerTraits::Presence) {
+        if (targetNode.m_nodeType.type() == typeid(yang::container)) {
+            if (boost::get<yang::container>(targetNode.m_nodeType).m_presence == yang::ContainerTraits::Presence) {
                 return yang::NodeTypes::PresenceContainer;
             }
             return yang::NodeTypes::Container;
         }
 
-        if (targetNode.type() == typeid(yang::list)) {
+        if (targetNode.m_nodeType.type() == typeid(yang::list)) {
             return yang::NodeTypes::List;
         }
 
-        if (targetNode.type() == typeid(yang::leaf)) {
+        if (targetNode.m_nodeType.type() == typeid(yang::leaf)) {
             return yang::NodeTypes::Leaf;
         }
 
@@ -207,6 +207,25 @@
     }
 }
 
+std::string fullNodeName(const std::string& location, const std::string& node)
+{
+    // If the node already contains a module name, just return it.
+    if (node.find_first_of(':') != std::string::npos) {
+        return node;
+    }
+
+    // Otherwise take the module name from the first node of location.
+    return location.substr(location.find_first_not_of('/'), location.find_first_of(':') - 1) + ":" + node;
+}
+
+bool StaticSchema::isConfig(const std::string& leafPath) const
+{
+    auto locationString = stripLastNodeFromPath(leafPath);
+
+    auto node = fullNodeName(locationString, lastNodeOfSchemaPath(leafPath));
+    return children(locationString).at(node).m_configType == yang::AccessType::Writable;
+}
+
 std::optional<std::string> StaticSchema::description([[maybe_unused]] const std::string& path) const
 {
     throw std::runtime_error{"StaticSchema::description not implemented"};
@@ -237,11 +256,6 @@
     throw std::runtime_error{"Internal error: StaticSchema::leafTypeName(std::string) not implemented. The tests should not have called this overload."};
 }
 
-bool StaticSchema::isConfig([[maybe_unused]] const std::string& leafPath) const
-{
-    throw std::runtime_error{"Internal error: StaticSchema::isConfigLeaf(std::string) not implemented. The tests should not have called this overload."};
-}
-
 std::optional<std::string> StaticSchema::defaultValue([[maybe_unused]] const std::string& leafPath) const
 {
     throw std::runtime_error{"Internal error: StaticSchema::defaultValue(std::string) not implemented. The tests should not have called this overload."};
diff --git a/src/static_schema.hpp b/src/static_schema.hpp
index 5184ae6..b94dfa3 100644
--- a/src/static_schema.hpp
+++ b/src/static_schema.hpp
@@ -32,10 +32,20 @@
 
 struct module {
 };
+
+enum class AccessType {
+    Writable,
+    ReadOnly
+};
 }
 
 using NodeType = boost::variant<yang::container, yang::list, yang::leaf, yang::module>;
 
+struct NodeInfo {
+    NodeType m_nodeType;
+    yang::AccessType m_configType;
+};
+
 
 /*! \class StaticSchema
  *     \brief Static schema, used mainly for testing
@@ -66,17 +76,17 @@
      * used in addLeaf for the `type` argument */
     std::set<identityRef_> validIdentities(std::string_view module, std::string_view value);
     void addContainer(const std::string& location, const std::string& name, yang::ContainerTraits isPresence = yang::ContainerTraits::None);
-    void addLeaf(const std::string& location, const std::string& name, const yang::LeafDataType& type);
+    void addLeaf(const std::string& location, const std::string& name, const yang::LeafDataType& type, const yang::AccessType accessType = yang::AccessType::Writable);
     void addList(const std::string& location, const std::string& name, const std::set<std::string>& keys);
     void addModule(const std::string& name);
     void addIdentity(const std::optional<identityRef_>& base, const identityRef_& name);
 
 private:
-    const std::unordered_map<std::string, NodeType>& children(const std::string& name) const;
+    const std::unordered_map<std::string, NodeInfo>& children(const std::string& name) const;
     void getIdentSet(const identityRef_& ident, std::set<identityRef_>& res) const;
     bool nodeExists(const std::string& location, const std::string& node) const;
 
-    std::unordered_map<std::string, std::unordered_map<std::string, NodeType>> m_nodes;
+    std::unordered_map<std::string, std::unordered_map<std::string, NodeInfo>> m_nodes;
     std::set<std::string> m_modules;
 
     std::map<identityRef_, std::set<identityRef_>> m_identities;
diff --git a/tests/leaf_editing.cpp b/tests/leaf_editing.cpp
index 75433e0..6da7868 100644
--- a/tests/leaf_editing.cpp
+++ b/tests/leaf_editing.cpp
@@ -73,6 +73,7 @@
         yang::TypeInfo{yang::Empty{}},
     }});
     schema->addLeaf("/", "mod:dummy", yang::Empty{});
+    schema->addLeaf("/", "mod:readonly", yang::Int32{}, yang::AccessType::ReadOnly);
 
     Parser parser(schema);
     std::string input;
@@ -601,6 +602,16 @@
             input = "set mod:dummy";
         }
 
+        SECTION("empty path")
+        {
+            input = "set ";
+        }
+
+        SECTION("setting readonly data")
+        {
+            input = "set mod:readonly 123";
+        }
+
         REQUIRE_THROWS_AS(parser.parseCommand(input, errorStream), InvalidCommandException);
         REQUIRE(errorStream.str().find(expectedError) != std::string::npos);
     }
diff --git a/tests/ls.cpp b/tests/ls.cpp
index 93ec75b..6d70414 100644
--- a/tests/ls.cpp
+++ b/tests/ls.cpp
@@ -9,6 +9,7 @@
 #include "trompeloeil_doctest.hpp"
 #include "ast_commands.hpp"
 #include "parser.hpp"
+#include "pretty_printers.hpp"
 #include "static_schema.hpp"
 
 TEST_CASE("ls")
diff --git a/tests/path_completion.cpp b/tests/path_completion.cpp
index bfc83d2..71fc917 100644
--- a/tests/path_completion.cpp
+++ b/tests/path_completion.cpp
@@ -37,6 +37,7 @@
     schema->addLeaf("/example:twoKeyList", "example:name", yang::String{});
     schema->addLeaf("/example:twoKeyList", "example:number", yang::Int32{});
     schema->addLeaf("/", "example:leafInt", yang::Int32{});
+    schema->addLeaf("/", "example:readonly", yang::Int32{}, yang::AccessType::ReadOnly);
     auto mockDatastore = std::make_shared<MockDatastoreAccess>();
 
     // The parser will use DataQuery for key value completion, but I'm not testing that here, so I don't return anything.
@@ -66,14 +67,14 @@
         SECTION("ls ")
         {
             input = "ls ";
-            expectedCompletions = {"example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list", "example:ovoce", "example:ovocezelenina", "example:twoKeyList", "second:amelie/"};
+            expectedCompletions = {"example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list", "example:ovoce", "example:readonly ", "example:ovocezelenina", "example:twoKeyList", "second:amelie/"};
             expectedContextLength = 0;
         }
 
         SECTION("ls e")
         {
             input = "ls e";
-            expectedCompletions = {"example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list", "example:ovoce", "example:ovocezelenina", "example:twoKeyList"};
+            expectedCompletions = {"example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list", "example:ovoce", "example:readonly ", "example:ovocezelenina", "example:twoKeyList"};
             expectedContextLength = 1;
         }
 
@@ -101,14 +102,14 @@
         SECTION("ls /")
         {
             input = "ls /";
-            expectedCompletions = {"example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list", "example:ovoce", "example:ovocezelenina", "example:twoKeyList", "second:amelie/"};
+            expectedCompletions = {"example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list", "example:ovoce", "example:readonly ", "example:ovocezelenina", "example:twoKeyList", "second:amelie/"};
             expectedContextLength = 0;
         }
 
         SECTION("ls /e")
         {
             input = "ls /e";
-            expectedCompletions = {"example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list", "example:ovoce", "example:ovocezelenina", "example:twoKeyList"};
+            expectedCompletions = {"example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list", "example:ovoce", "example:readonly ", "example:ovocezelenina", "example:twoKeyList"};
             expectedContextLength = 1;
         }
 
@@ -312,6 +313,12 @@
         }
     }
 
+    SECTION("no readonly data leafs in set")
+    {
+        input = "set example:read";
+        expectedContextLength = 12;
+    }
+
     SECTION("clear completions when no longer inputting path")
     {
         input = "set example:leafInt ";
diff --git a/tests/presence_containers.cpp b/tests/presence_containers.cpp
index 7c6397d..f248597 100644
--- a/tests/presence_containers.cpp
+++ b/tests/presence_containers.cpp
@@ -107,6 +107,11 @@
             input = "list[quote='lol']";
         }
 
+        SECTION("no path")
+        {
+            input = " ";
+        }
+
         REQUIRE_THROWS_AS(parser.parseCommand("create " + input, errorStream), InvalidCommandException);
         REQUIRE_THROWS_AS(parser.parseCommand("delete " + input, errorStream), InvalidCommandException);
     }
diff --git a/tests/pretty_printers.hpp b/tests/pretty_printers.hpp
index fb8aebf..7f59adc 100644
--- a/tests/pretty_printers.hpp
+++ b/tests/pretty_printers.hpp
@@ -110,3 +110,21 @@
     }
     return s;
 }
+
+std::ostream& operator<<(std::ostream& s, const boost::optional<boost::variant<dataPath_, schemaPath_, module_>>& path)
+{
+    if (path) {
+        s << *path;
+    } else {
+        s << "boost::none";
+    }
+
+    return s;
+}
+
+
+std::ostream& operator<<(std::ostream& s, const ls_& ls)
+{
+    s << "\nls_ {\n    " << ls.m_path << "}\n";
+    return s;
+}