yanglint FEATURE schema mount support (#1901)

* modify existing context if provided

In ly_ctx_new_yldata(), check if *ctx is NULL.  If so, proceed as before
with allocating a new context.  Otherwise, load modules into the existing
context.

Signed-off-by: Eric Kinzie <ekinzie@labn.net>

* add parent-reference xpath helper to schema-mount plugin

This new function produces a list of schema nodes from the expanded
parent-reference xpath expressions.

Signed-off-by: Eric Kinzie <ekinzie@labn.net>

* add context lookup to schema-mount plugin

This function allocates a new context for a particular instance of the
yangmnt:mount-point extension.

Signed-off-by: Eric Kinzie <ekinzie@labn.net>

* yanglint: schema-mount extension callback

Add a commandline argument "-x" that accepts a file containing extension
data, used to create a context for the schema-mount extension.  This file
has the same format as what is provided to the "-Y" option.  A callback
function for the schema-mount extenion is registered if the new option
is specified.

This allows validating instance data for models that include a schema
mount point.

Signed-off-by: Eric Kinzie <ekinzie@labn.net>

* yanglint: print mounted schema trees

Display "mp" flag for mount point and print mounted tree.  Also display
the / and @ opts in node names.

Signed-off-by: Eric Kinzie <ekinzie@labn.net>

* add unit test for tree mount-point flag

Test is courtesy of aPiecek <piecek@cesnet.cz>.

Signed-off-by: Eric Kinzie <ekinzie@labn.net>

* add example data files for validation with schema-mount

From tools/lint/examples directory:

% ../../../build/yanglint \
    -f json -t config -p ../../../models -p . \
    -Y ./sm-context-main.xml -x ./sm-context-extension.xml sm-data.xml

% ../../../build/yanglint \
    -f tree -p ../../../models -p . \
    -Y ./sm-context-main.xml -x ./sm-context-extension.xml sm-main.yang

Signed-off-by: Eric Kinzie <ekinzie@labn.net>

Signed-off-by: Eric Kinzie <ekinzie@labn.net>
Co-authored-by: Eric Kinzie <ekinzie@labn.net>
diff --git a/tools/lint/examples/sm-context-extension.xml b/tools/lint/examples/sm-context-extension.xml
new file mode 100644
index 0000000..747c60f
--- /dev/null
+++ b/tools/lint/examples/sm-context-extension.xml
@@ -0,0 +1,64 @@
+<yang-library xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-library"
+     xmlns:ds="urn:ietf:params:xml:ns:yang:ietf-datastores">
+   <module-set>
+     <name>test-set</name>
+     <module>
+       <name>ietf-datastores</name>
+       <revision>2018-02-14</revision>
+       <namespace>urn:ietf:params:xml:ns:yang:ietf-datastores</namespace>
+     </module>
+     <module>
+       <name>ietf-yang-library</name>
+       <revision>2019-01-04</revision>
+       <namespace>urn:ietf:params:xml:ns:yang:ietf-yang-library</namespace>
+     </module>
+     <module>
+       <name>sm-extension</name>
+       <namespace>urn:sm-ext</namespace>
+     </module>
+     <module>
+       <name>iana-if-type</name>
+       <namespace>urn:ietf:params:xml:ns:yang:iana-if-type</namespace>
+     </module>
+     <import-only-module>
+       <name>ietf-yang-types</name>
+       <revision>2013-07-15</revision>
+       <namespace>urn:ietf:params:xml:ns:yang:ietf-yang-types</namespace>
+     </import-only-module>
+     <import-only-module>
+       <name>sm-mod</name>
+       <revision>2017-01-26</revision>
+       <namespace>urn:yanglint:sm-mod</namespace>
+     </import-only-module>
+   </module-set>
+   <schema>
+     <name>test-schema</name>
+     <module-set>test-set</module-set>
+   </schema>
+   <datastore>
+     <name>ds:running</name>
+     <schema>test-schema</schema>
+   </datastore>
+   <datastore>
+     <name>ds:operational</name>
+     <schema>test-schema</schema>
+   </datastore>
+   <content-id>1</content-id>
+ </yang-library>
+ <modules-state xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-library">
+   <module-set-id>1</module-set-id>
+ </modules-state>
+ <schema-mounts xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-schema-mount">
+   <namespace>
+     <prefix>if</prefix>
+     <uri>urn:ietf:params:xml:ns:yang:ietf-interfaces</uri>
+   </namespace>
+   <mount-point>
+     <module>sm-main</module>
+     <label>mnt-root</label>
+     <shared-schema>
+       <parent-reference>/if:interfaces/if:interface/if:name</parent-reference>
+       <parent-reference>/if:interfaces/if:interface/if:type</parent-reference>
+     </shared-schema>
+   </mount-point>
+  </schema-mounts>
diff --git a/tools/lint/examples/sm-context-main.xml b/tools/lint/examples/sm-context-main.xml
new file mode 100644
index 0000000..43558c3
--- /dev/null
+++ b/tools/lint/examples/sm-context-main.xml
@@ -0,0 +1,54 @@
+<yang-library xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-library"
+     xmlns:ds="urn:ietf:params:xml:ns:yang:ietf-datastores">
+   <module-set>
+     <name>main-set</name>
+     <module>
+       <name>ietf-datastores</name>
+       <revision>2018-02-14</revision>
+       <namespace>urn:ietf:params:xml:ns:yang:ietf-datastores</namespace>
+     </module>
+     <module>
+       <name>ietf-yang-library</name>
+       <revision>2019-01-04</revision>
+       <namespace>urn:ietf:params:xml:ns:yang:ietf-yang-library</namespace>
+     </module>
+     <module>
+       <name>ietf-yang-schema-mount</name>
+       <revision>2019-01-14</revision>
+       <namespace>urn:ietf:params:xml:ns:yang:ietf-yang-schema-mount</namespace>
+     </module>
+     <module>
+       <name>sm-main</name>
+       <namespace>urn:sm-main</namespace>
+     </module>
+     <module>
+       <name>iana-if-type</name>
+       <namespace>urn:ietf:params:xml:ns:yang:iana-if-type</namespace>
+     </module>
+     <module>
+       <name>ietf-interfaces</name>
+       <namespace>urn:ietf:params:xml:ns:yang:ietf-interfaces</namespace>
+     </module>
+     <import-only-module>
+       <name>ietf-yang-types</name>
+       <revision>2013-07-15</revision>
+       <namespace>urn:ietf:params:xml:ns:yang:ietf-yang-types</namespace>
+     </import-only-module>
+   </module-set>
+   <schema>
+     <name>main-schema</name>
+     <module-set>main-set</module-set>
+   </schema>
+   <datastore>
+     <name>ds:running</name>
+     <schema>main-schema</schema>
+   </datastore>
+   <datastore>
+     <name>ds:operational</name>
+     <schema>main-schema</schema>
+   </datastore>
+   <content-id>1</content-id>
+ </yang-library>
+ <modules-state xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-library">
+   <module-set-id>2</module-set-id>
+ </modules-state>
diff --git a/tools/lint/examples/sm-data.xml b/tools/lint/examples/sm-data.xml
new file mode 100644
index 0000000..478d324
--- /dev/null
+++ b/tools/lint/examples/sm-data.xml
@@ -0,0 +1,19 @@
+<interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces">
+  <interface>
+    <name>eth0</name>
+    <type xmlns:ift="urn:ietf:params:xml:ns:yang:iana-if-type">ift:ethernetCsmacd</type>
+  </interface>
+  <interface>
+    <name>eth1</name>
+    <type xmlns:ift="urn:ietf:params:xml:ns:yang:iana-if-type">ift:ethernetCsmacd</type>
+  </interface>
+</interfaces>
+<root3 xmlns="urn:sm-main">
+  <my-list>
+    <name>list item 1</name>
+    <things xmlns="urn:sm-ext">
+      <name>eth0</name>
+      <attribute>1</attribute>
+    </things>
+  </my-list>
+</root3>
diff --git a/tools/lint/examples/sm-extension.yang b/tools/lint/examples/sm-extension.yang
new file mode 100644
index 0000000..2214cf6
--- /dev/null
+++ b/tools/lint/examples/sm-extension.yang
@@ -0,0 +1,39 @@
+module sm-extension {
+    yang-version 1.1;
+    namespace "urn:sm-ext";
+    prefix "sm-ext";
+
+    import ietf-interfaces {
+        prefix if;
+    }
+    import sm-mod {
+        prefix sm-mod;
+    }
+
+    revision 2022-09-15 {
+      description
+        "initial";
+      reference
+        "";
+    }
+
+    list things {
+        key "name";
+        leaf name {
+            type leafref {
+              path "/if:interfaces/if:interface/if:name";
+            }
+        }
+        leaf attribute {
+            type uint32;
+        }
+    }
+
+    augment "/if:interfaces/if:interface" {
+        leaf thing-attribute {
+            type leafref {
+              path "/things/attribute";
+            }
+        }
+    }
+}
diff --git a/tools/lint/examples/sm-main.yang b/tools/lint/examples/sm-main.yang
new file mode 100644
index 0000000..53df6b6
--- /dev/null
+++ b/tools/lint/examples/sm-main.yang
@@ -0,0 +1,32 @@
+module sm-main {
+    yang-version 1.1;
+    namespace "urn:sm-main";
+    prefix "sm-main";
+
+    import ietf-yang-schema-mount {
+        prefix yangmnt;
+    }
+    import ietf-interfaces {
+        prefix if;
+    }
+
+    list root {
+        key "node";
+        leaf node {
+            type string;
+        }
+        yangmnt:mount-point "root";
+    }
+    container root2 {
+        yangmnt:mount-point "root";
+    }
+    container root3 {
+        list my-list {
+            key name;
+            leaf name {
+                type string;
+            }
+            yangmnt:mount-point "mnt-root";
+        }
+    }
+}
diff --git a/tools/lint/examples/sm-mod.yang b/tools/lint/examples/sm-mod.yang
new file mode 100644
index 0000000..79d1a50
--- /dev/null
+++ b/tools/lint/examples/sm-mod.yang
@@ -0,0 +1,21 @@
+module sm-mod {
+    yang-version 1.1;
+    namespace "urn:yanglint:sm-mod";
+    prefix "sm-mod";
+
+    revision 2017-01-26 {
+      description
+        "initial";
+      reference
+        "";
+    }
+
+    container not-compiled {
+        leaf first {
+            type string;
+        }
+        leaf second {
+            type string;
+        }
+    }
+}
diff --git a/tools/lint/main_ni.c b/tools/lint/main_ni.c
index 845d5f1..175daee 100644
--- a/tools/lint/main_ni.c
+++ b/tools/lint/main_ni.c
@@ -67,6 +67,12 @@
     const struct lysc_node *schema_node;
     const char *submodule;
 
+    /* name of file containing explicit context passed to callback
+     * for schema-mount extension.  This also causes a callback to
+     * be registered.
+     */
+    char *schema_context_filename;
+
     /* value of --format in case of schema format */
     LYS_OUTFORMAT schema_out_format;
     ly_bool feature_param_format;
@@ -112,6 +118,10 @@
 
     ly_out_free(c->out, NULL,  0);
     ly_ctx_destroy(c->ctx);
+
+    if (c->schema_context_filename) {
+        free(c->schema_context_filename);
+    }
 }
 
 static void
@@ -191,6 +201,11 @@
     printf("  -s SUBMODULE, --submodule=SUBMODULE\n"
             "                Print the specific submodule instead of the main module.\n\n");
 
+    printf("  -x FILE, --ext-data=FILE\n"
+            "                File containing the specific data required by an extension. Required by\n"
+            "                the schema-mount extension, for example, when the mounted data are\n"
+            "                expected in the file. File format is guessed.\n\n");
+
     printf("  -n, --not-strict\n"
             "                Do not require strict data parsing (silently skip unknown data),\n"
             "                has no effect for schemas.\n\n");
@@ -317,6 +332,22 @@
     return NULL;
 }
 
+static LY_ERR
+ext_data_clb(const struct lysc_ext_instance *ext, void *user_data, void **ext_data, ly_bool *ext_data_free)
+{
+    struct ly_ctx *ctx;
+    struct lyd_node *data = NULL;
+
+    ctx = ext->module->ctx;
+    if (user_data) {
+        lyd_parse_data_path(ctx, user_data, LYD_XML, LYD_PARSE_STRICT, LYD_VALIDATE_PRESENT, &data);
+    }
+
+    *ext_data = data;
+    *ext_data_free = 1;
+    return LY_SUCCESS;
+}
+
 static int
 fill_context_inputs(int argc, char *argv[], struct context *c)
 {
@@ -334,8 +365,13 @@
         ly_set_erase(&c->schema_features, free_features);
 
         /* create context from the yang-library file */
+        if (ly_ctx_new(searchdir, c->ctx_options, &c->ctx)) {
+            YLMSG_E("Unable to create libyang context\n");
+            return -1;
+        }
+        ly_ctx_set_ext_data_clb(c->ctx, ext_data_clb, c->schema_context_filename);
         if (ly_ctx_new_ylpath(searchdir, c->yang_lib_file, LYD_UNKNOWN, c->ctx_options, &c->ctx)) {
-            YLMSG_E("Unable to create libyang context from yang-library data.\n");
+            YLMSG_E("Unable to modify libyang context with yang-library data.\n");
             return -1;
         }
     } else {
@@ -350,6 +386,13 @@
             return -1;
         }
 
+        if (c->schema_context_filename) {
+            if (ly_ctx_set_ext_data_clb(c->ctx, ext_data_clb, c->schema_context_filename)) {
+                YLMSG_E("Unable to set extension callback data.\n");
+                return -1;
+            }
+        }
+
         /* set the rest of searchdirs */
         for (uint32_t i = 1; i < c->searchpaths.count; ++i) {
             ly_ctx_set_searchdir(c->ctx, c->searchpaths.objs[i]);
@@ -495,6 +538,7 @@
         {"schema-node",       required_argument, NULL, 'P'},
         {"single-node",       no_argument,       NULL, 'q'},
         {"submodule",         required_argument, NULL, 's'},
+        {"ext-data",          required_argument, NULL, 'x'},
         {"not-strict",        no_argument,       NULL, 'n'},
         {"present",           no_argument,       NULL, 'e'},
         {"type",              required_argument, NULL, 't'},
@@ -520,9 +564,9 @@
 
     opterr = 0;
 #ifndef NDEBUG
-    while ((opt = getopt_long(argc, argv, "hvVQf:p:DF:iP:qs:net:d:lL:o:O:R:myY:G:", options, &opt_index)) != -1)
+    while ((opt = getopt_long(argc, argv, "hvVQf:p:DF:iP:qs:net:d:lL:o:O:R:myY:x:G:", options, &opt_index)) != -1)
 #else
-    while ((opt = getopt_long(argc, argv, "hvVQf:p:DF:iP:qs:net:d:lL:o:O:R:myY:", options, &opt_index)) != -1)
+    while ((opt = getopt_long(argc, argv, "hvVQf:p:DF:iP:qs:net:d:lL:o:O:R:myY:x:", options, &opt_index)) != -1)
 #endif
     {
         switch (opt) {
@@ -645,6 +689,10 @@
             c->submodule = optarg;
             break;
 
+        case 'x': /* --ext-data */
+            c->schema_context_filename = strdup(optarg);
+            break;
+
         case 'n': /* --not-strict */
             c->data_parse_options &= ~LYD_PARSE_STRICT;
             break;