diff --git a/tests/yanglint/interactive/sample-skeleton.test b/tests/yanglint/interactive/sample-skeleton.test
new file mode 100644
index 000000000..6968f8a22
--- /dev/null
+++ b/tests/yanglint/interactive/sample-skeleton.test
@@ -0,0 +1,96 @@
+source [expr {[info exists ::env(TESTS_DIR)] ? "$env(TESTS_DIR)/interactive/ly.tcl" : "ly.tcl"}]
+
+test print_yang {} {
+-setup $ly_setup -cleanup $ly_cleanup -body {
+ ly_cmd "load modleaf"
+ ly_cmd "print -f yang modleaf" "leaf lfl"
+}}
+
+test skeleton_generate_xml {Test generating xml skeleton to stdout and to file} {
+-setup $ly_setup -cleanup $ly_cleanup -body {
+ set pattern "(?s)^"
+ append pattern {.*}
+ append pattern {.*}
+ append pattern {.*true}
+ append pattern {.*-100}
+ append pattern {.*9999999999}
+ append pattern {.*hello}
+ append pattern {.*}
+ append pattern {.*10}
+ append pattern {.*20}
+ append pattern {.*}
+ append pattern {.*default-choice-val}
+ append pattern {.*}
+ append pattern {.*}
+ append pattern {.*}
+ append pattern {.*}
+ append pattern {.*}
+ append pattern {.*}
+ append pattern {.*}
+ append pattern {.*}
+ append pattern {.*augmented-value}
+ append pattern {.*1}
+ append pattern {.*2}
+ append pattern {.*}
+ append pattern ".*"
+
+ ly_cmd "load skeleton"
+ ly_cmd "load skeleton-augment"
+ ly_cmd "sample -f xml skeleton" $pattern
+
+ ly_cmd "sample -f xml -o test_output.xml skeleton"
+
+ if {![file exists "test_output.xml"]} {
+ error "Test failed: yanglint did not create 'test_output.xml'!"
+ }
+
+ set fp [open "test_output.xml" r]
+ set file_data [read $fp]
+ close $fp
+
+ if {![regexp $pattern $file_data]} {
+ error "Test failed: The file content did not match!\nFile contained:\n$file_data"
+ }
+
+ file delete "test_output.xml"
+}}
+
+test skeleton_generate_json {} {
+-setup $ly_setup -cleanup $ly_cleanup -body {
+ set pattern "(?s)^"
+ append pattern {.*"skeleton:main-box": \{}
+ append pattern {.*"empty-leaf": \[null\],}
+ append pattern {.*"bool-leaf": true,}
+ append pattern {.*"num32-leaf": -100,}
+ append pattern {.*"num64-leaf": "9999999999",}
+ append pattern {.*"str-leaf": "hello",}
+ append pattern {.*"ll-empty": \[\],}
+ append pattern {.*"ll-numbers": \[}
+ append pattern {.*10,}
+ append pattern {.*20}
+ append pattern {.*],}
+ append pattern {.*"a-leaf": null,}
+ append pattern {.*"c2-leaf": "default-choice-val",}
+ append pattern {.*"server-list": \[}
+ append pattern {.*\{}
+ append pattern {.*"id": null,}
+ append pattern {.*"config": \{}
+ append pattern {.*"ip": null}
+ append pattern {.*\}}
+ append pattern {.*\}}
+ append pattern {.*\],}
+ append pattern {.*"custom-data": \{\},}
+ append pattern {.*"custom-xml": \{\},}
+ append pattern {.*"skeleton-augment:aug-leaf": "augmented-value",}
+ append pattern {.*"skeleton-augment:aug-ll": \[}
+ append pattern {.*1,}
+ append pattern {.*2}
+ append pattern {.*\]}
+ append pattern {.*\}}
+ append pattern ".*"
+
+ ly_cmd "load skeleton"
+ ly_cmd "load skeleton-augment"
+
+ ly_cmd "sample -f json skeleton" $pattern
+}}
diff --git a/tests/yanglint/modules/bad-ietf-prefix-namespace.yang b/tests/yanglint/modules/bad-ietf-prefix-namespace.yang
new file mode 100644
index 000000000..aa3ed8d01
--- /dev/null
+++ b/tests/yanglint/modules/bad-ietf-prefix-namespace.yang
@@ -0,0 +1,26 @@
+module bad-ietf-prefix-namespace {
+ yang-version 1.1;
+ namespace "urn:wrong:namespace";
+ prefix bpn;
+
+ organization "IETF NETMOD Working Group";
+ contact "WG Web: ";
+
+ description
+ "Copyright (c) 2026 IETF Trust and the persons identified as
+ authors of the code. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or
+ without modification, is permitted pursuant to, and subject
+ to the license terms contained in, the Revised BSD License
+ set forth in Section 4.c of the IETF Trust's Legal Provisions
+ Relating to IETF Documents
+ (https://trustee.ietf.org/license-info).
+
+ This version of this YANG module is part of RFC XXXX; see
+ the RFC itself for full legal notices.";
+
+ revision 2026-07-08 {
+ // Triggers 4.8: Missing "reference" and "description" inside the revision block
+ }
+}
diff --git a/tests/yanglint/modules/ietf-bad-boilerplate.yang b/tests/yanglint/modules/ietf-bad-boilerplate.yang
new file mode 100644
index 000000000..582e23426
--- /dev/null
+++ b/tests/yanglint/modules/ietf-bad-boilerplate.yang
@@ -0,0 +1,19 @@
+module ietf-bad-boilerplate {
+ yang-version 1.1;
+ namespace "urn:ietf:params:xml:ns:yang:ietf-bad-boilerplate";
+ prefix ibb;
+
+ organization "IETF NETMOD Working Group";
+ contact "WG Web: ";
+
+ // 1. Missing IETF Trust Copyright (Triggers 3.1)
+ // 2. Missing "This version of this YANG module is part of RFC..." (Triggers Appendix B)
+ // 3. Uses the word "MUST", but lacks the RFC 8174 text block (Triggers BCP 14 check)
+ description
+ "This module MUST intentionally fail the text boilerplate checks.";
+
+ revision 2026-07-08 {
+ description "Initial revision.";
+ reference "RFC XXXX: Test Module";
+ }
+}
diff --git a/tests/yanglint/modules/ietf-correct.yang b/tests/yanglint/modules/ietf-correct.yang
new file mode 100644
index 000000000..f4fb5949a
--- /dev/null
+++ b/tests/yanglint/modules/ietf-correct.yang
@@ -0,0 +1,60 @@
+module ietf-correct {
+ yang-version 1.1;
+ namespace "urn:ietf:params:xml:ns:yang:ietf-correct";
+ prefix isc;
+
+ organization
+ "IETF NETMOD Working Group";
+
+ contact
+ "WG Web: ";
+
+ description
+ "Copyright (c) 2026 IETF Trust and the persons identified as
+ authors of the code. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or
+ without modification, is permitted pursuant to, and subject
+ to the license terms contained in, the Revised BSD License
+ set forth in Section 4.c of the IETF Trust's Legal Provisions
+ Relating to IETF Documents
+ (https://trustee.ietf.org/license-info).
+
+ This version of this YANG module is part of RFC XXXX; see
+ the RFC itself for full legal notices.";
+
+ revision 2026-07-08 {
+ description
+ "Initial revision.";
+ reference
+ "RFC XXXX: Test Module";
+ }
+
+ container system-info {
+ description
+ "A simple top-level container to hold our data. Notice it is
+ not mandatory.";
+
+ leaf system-name {
+ type string;
+ description
+ "The configured name of the system.";
+ }
+
+ leaf status {
+ type enumeration {
+ enum active {
+ description
+ "The system is currently active and processing.";
+ }
+ enum inactive {
+ description
+ "The system is currently inactive.";
+ }
+ }
+ description
+ "The current operational status of the system. Notice there
+ is no explicit 'config true' or 'mandatory false' here.";
+ }
+ }
+}
diff --git a/tests/yanglint/modules/ietf-defaults-and-mandatory.yang b/tests/yanglint/modules/ietf-defaults-and-mandatory.yang
new file mode 100644
index 000000000..00239b6ca
--- /dev/null
+++ b/tests/yanglint/modules/ietf-defaults-and-mandatory.yang
@@ -0,0 +1,66 @@
+module ietf-defaults-and-mandatory {
+ yang-version 1.1;
+ namespace "urn:ietf:params:xml:ns:yang:ietf-defaults-and-mandatory";
+ prefix idm;
+
+ organization "IETF NETMOD Working Group";
+ contact "WG Web: ";
+ description
+ "Copyright (c) 2026 IETF Trust and the persons identified as
+ authors of the code. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or
+ without modification, is permitted pursuant to, and subject
+ to the license terms contained in, the Revised BSD License
+ set forth in Section 4.c of the IETF Trust's Legal Provisions
+ Relating to IETF Documents
+ (https://trustee.ietf.org/license-info).
+
+ This version of this YANG module is part of RFC XXXX; see
+ the RFC itself for full legal notices.";
+
+ revision 2026-07-08 {
+ description "Initial revision.";
+ reference "RFC XXXX: Test Module";
+ }
+
+ extension ext1 {
+ description "Ext description";
+ argument arg1 {
+ // Triggers 4.4: yin-element given with default
+ yin-element false;
+ }
+ }
+
+ // Triggers 4.10: Top-level node cannot be mandatory
+ leaf top-mand-leaf {
+ type string;
+ description "Top level mandatory";
+ mandatory true;
+ }
+
+ leaf bad-defaults-leaf {
+ type string;
+ description "Leaf with explicit defaults";
+ // Triggers 4.4: config true
+ config true;
+ // Triggers 4.4: status current
+ status current;
+ // Triggers 4.4: mandatory false
+ mandatory false;
+ }
+
+ list bad-list {
+ key "k";
+ description "List with explicit defaults";
+ ordered-by system;
+ leaf k {
+ type string;
+ description "Key";
+ }
+ // Triggers 4.4: min-elements 0
+ min-elements 0;
+ // Triggers 4.4: max-elements unbounded
+ max-elements unbounded;
+ }
+}
diff --git a/tests/yanglint/modules/ietf-empty-type.yang b/tests/yanglint/modules/ietf-empty-type.yang
new file mode 100644
index 000000000..feba63ea0
--- /dev/null
+++ b/tests/yanglint/modules/ietf-empty-type.yang
@@ -0,0 +1,62 @@
+module ietf-empty-type {
+ yang-version 1.1;
+ namespace "urn:ietf:params:xml:ns:yang:ietf-empty-type";
+ prefix eett;
+
+ organization
+ "IETF NETMOD (Network Modeling) Working Group";
+
+ contact
+ "WG Web:
+ WG List:
+
+ Author: Test Author
+ ";
+
+ description
+ "Copyright (c) 2026 IETF Trust and the persons identified as
+ authors of the code. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or
+ without modification, is permitted pursuant to, and subject
+ to the license terms contained in, the Revised BSD License
+ set forth in Section 4.c of the IETF Trust's Legal Provisions
+ Relating to IETF Documents
+ (https://trustee.ietf.org/license-info).
+
+ This version of this YANG module is part of RFC XXXX; see
+ the RFC itself for full legal notices.";
+
+ revision 2026-07-10 {
+ description
+ "Initial revision. Testing empty types in keys and leaf-lists.";
+ reference
+ "RFC XXXX: Guidelines for Authors and Reviewers of YANG Data Models";
+ }
+
+ container top-container {
+ description
+ "A top level container to hold our test lists.";
+
+ /* TEST 1: Trigger the 'empty' type in list key warning */
+ list list-with-empty-key {
+ key "id";
+ description
+ "A list to test the empty key warning.";
+
+ /* TEST 2: Trigger the 'empty' type in leaf */
+ leaf id {
+ type empty;
+ description
+ "Key leaf of type empty.";
+ }
+ }
+
+ /* TEST 3: Trigger the 'empty' type in leaf-list warning */
+ leaf-list leaf-list-with-empty-type {
+ type empty;
+ description
+ "A leaf-list to test the empty type warning.";
+ }
+ }
+}
diff --git a/tests/yanglint/modules/ietf-example-this-identifier-is-intentionally-designed-to-exceed-64-chars.yang b/tests/yanglint/modules/ietf-example-this-identifier-is-intentionally-designed-to-exceed-64-chars.yang
new file mode 100644
index 000000000..9a4c5040d
--- /dev/null
+++ b/tests/yanglint/modules/ietf-example-this-identifier-is-intentionally-designed-to-exceed-64-chars.yang
@@ -0,0 +1,90 @@
+module ietf-example-this-identifier-is-intentionally-designed-to-exceed-64-chars {
+ yang-version 1.1;
+ namespace "urn:ietf:params:xml:ns:yang:ietf-example-this-identifier-is-intentionally-designed-to-exceed-64-chars";
+ prefix isc;
+
+ organization
+ "IETF NETMOD Working Group";
+
+ contact
+ "WG Web: ";
+
+ description
+ "Copyright (c) 2026 IETF Trust and the persons identified as
+ authors of the code. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or
+ without modification, is permitted pursuant to, and subject
+ to the license terms contained in, the Revised BSD License
+ set forth in Section 4.c of the IETF Trust's Legal Provisions
+ Relating to IETF Documents
+ (https://trustee.ietf.org/license-info).
+
+ This version of this YANG module is part of RFC XXXX; see
+ the RFC itself for full legal notices.";
+
+ revision 2026-07-08 {
+ description
+ "Initial revision.";
+ reference
+ "RFC XXXX: Test Module";
+ }
+
+ extension this-extension-identifier-is-intentionally-designed-to-exceed-64-chars {
+ description
+ "Testing extension identifier length.";
+ }
+
+ feature this-feature-identifier-is-intentionally-designed-to-exceed-64-chars {
+ description
+ "Testing feature identifier length.";
+ }
+
+ identity this-identity-identifier-is-intentionally-designed-to-exceed-64-chars {
+ description
+ "Testing identity identifier length.";
+ }
+
+ typedef this-typedef-identifier-is-intentionally-designed-to-exceed-64-chars {
+ type string;
+ description
+ "Testing typedef identifier length.";
+ }
+
+ grouping this-grouping-identifier-is-intentionally-designed-to-exceed-64-chars {
+ description
+ "Testing grouping identifier length.";
+
+ leaf dummy-leaf {
+ type string;
+ description "Needed a child to make grouping valid.";
+ }
+ }
+
+ container this-container-identifier-is-intentionally-designed-to-exceed-64-chars {
+ description
+ "Testing data node identifier length.";
+
+ leaf enum-tester {
+ type enumeration {
+ enum this-enum-identifier-is-intentionally-designed-to-exceed-64-chars {
+ description
+ "Testing enum identifier length.";
+ }
+ }
+ description
+ "Leaf to hold the long enum.";
+ }
+
+ leaf bits-tester {
+ type bits {
+ bit this-bit-identifier-is-intentionally-designed-to-exceed-64-chars {
+ description
+ "Testing bit identifier length.";
+ }
+ }
+ description
+ "Leaf to hold the long bit.";
+ }
+ }
+}
diff --git a/tests/yanglint/modules/ietf-imported-module.yang b/tests/yanglint/modules/ietf-imported-module.yang
new file mode 100644
index 000000000..e46327585
--- /dev/null
+++ b/tests/yanglint/modules/ietf-imported-module.yang
@@ -0,0 +1,26 @@
+module ietf-imported-module {
+ yang-version 1.1;
+ namespace "urn:ietf:params:xml:ns:yang:ietf-imported-module";
+ prefix iim;
+
+ organization "IETF NETMOD Working Group";
+ contact "WG Web: ";
+ description
+ "Copyright (c) 2026 IETF Trust and the persons identified as
+ authors of the code. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or
+ without modification, is permitted pursuant to, and subject
+ to the license terms contained in, the Revised BSD License
+ set forth in Section 4.c of the IETF Trust's Legal Provisions
+ Relating to IETF Documents
+ (https://trustee.ietf.org/license-info).
+
+ This version of this YANG module is part of RFC 9907; see
+ the RFC itself for full legal notices.";
+
+ revision 2026-07-10 {
+ description "Initial revision.";
+ reference "RFC 9907";
+ }
+}
diff --git a/tests/yanglint/modules/ietf-list-key.yang b/tests/yanglint/modules/ietf-list-key.yang
new file mode 100644
index 000000000..1341e89ef
--- /dev/null
+++ b/tests/yanglint/modules/ietf-list-key.yang
@@ -0,0 +1,100 @@
+module ietf-list-key {
+ yang-version 1;
+ namespace "urn:ietf:params:xml:ns:yang:ietf-list-key";
+ prefix elkt;
+
+ organization
+ "IETF NETMOD (Network Modeling) Working Group";
+
+ contact
+ "WG Web:
+ WG List:
+
+ Author: Test Author
+ ";
+
+ description
+ "Copyright (c) 2026 IETF Trust and the persons identified as
+ authors of the code. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or
+ without modification, is permitted pursuant to, and subject
+ to the license terms contained in, the Revised BSD License
+ set forth in Section 4.c of the IETF Trust's Legal Provisions
+ Relating to IETF Documents
+ (https://trustee.ietf.org/license-info).
+
+ This version of this YANG module is part of RFC XXXX; see
+ the RFC itself for full legal notices.";
+
+ revision 2026-07-10 {
+ description
+ "Initial revision. Testing list key constraints and state data must statements.";
+ reference
+ "RFC XXXX: Guidelines for Authors and Reviewers of YANG Data Models";
+ }
+
+ feature feature-a {
+ description "A test feature.";
+ }
+
+ feature feature-b {
+ description "Another test feature.";
+ }
+
+ container top-container {
+ description "A top level container to hold our test lists.";
+
+ leaf condition-tester {
+ type boolean;
+ description "A simple leaf to evaluate 'when' statements against.";
+ }
+
+ /* TEST 1: Trigger the "when" statement warning */
+ list list-with-when-on-key {
+ key "id";
+ description "A list to test the 'when' statement warning.";
+
+ leaf id {
+ type string;
+ when "../../condition-tester = 'true'";
+ description "Key leaf with an invalid when statement.";
+ }
+ }
+
+ /* TEST 2: Trigger the if-feature count mismatch error */
+ list list-missing-if-feature {
+ if-feature "feature-a";
+ key "id";
+ description "List with a feature, but the key is missing it.";
+
+ leaf id {
+ type string;
+ description "Key leaf missing the parent's if-feature.";
+ }
+ }
+
+ /* TEST 3: Trigger the if-feature string mismatch error */
+ list list-mismatched-if-feature {
+ if-feature "feature-a";
+ key "id";
+ description "List with feature-a, but the key has feature-b.";
+
+ leaf id {
+ if-feature "feature-b";
+ type string;
+ description "Key leaf with the wrong if-feature string.";
+ }
+ }
+
+ /* TEST 4: Trigger the state data constraint warning */
+ leaf state-data-with-must {
+ type string;
+ config false;
+ must "string-length(.) > 0";
+ description
+ "This leaf tests the RFC 9907 Section 4.5 recommendation to avoid
+ defining 'must' constraints on state data.";
+ }
+ }
+}
diff --git a/tests/yanglint/modules/ietf-miss-desc-data-nodes.yang b/tests/yanglint/modules/ietf-miss-desc-data-nodes.yang
new file mode 100644
index 000000000..d93119045
--- /dev/null
+++ b/tests/yanglint/modules/ietf-miss-desc-data-nodes.yang
@@ -0,0 +1,58 @@
+module ietf-miss-desc-data-nodes {
+ yang-version 1.1;
+ namespace "urn:ietf:params:xml:ns:yang:ietf-miss-desc-data-nodes";
+ prefix desc-data;
+
+ organization "IETF NETMOD Working Group";
+ contact "WG Web: ";
+ description
+ "Copyright (c) 2026 IETF Trust and the persons identified as
+ authors of the code. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or
+ without modification, is permitted pursuant to, and subject
+ to the license terms contained in, the Revised BSD License
+ set forth in Section 4.c of the IETF Trust's Legal Provisions
+ Relating to IETF Documents
+ (https://trustee.ietf.org/license-info).
+
+ This version of this YANG module is part of RFC XXXX; see
+ the RFC itself for full legal notices.";
+
+ revision 2026-07-08 {
+ description "Initial revision.";
+ reference "RFC XXXX: Test Module";
+ }
+
+ container base-cont {
+ }
+
+ leaf top-leaf {
+ type string;
+ }
+
+ list top-list {
+ key "k";
+ leaf k {
+ type string;
+ description "Key leaf description to prevent secondary errors.";
+ }
+ }
+
+ leaf-list top-ll {
+ type string;
+ }
+
+ anyxml top-ax;
+
+ anydata top-ad;
+
+ choice top-choice {
+ case c1 {
+ leaf c-leaf {
+ type string;
+ description "Case leaf description to prevent secondary errors.";
+ }
+ }
+ }
+}
diff --git a/tests/yanglint/modules/ietf-miss-desc-enums-bits.yang b/tests/yanglint/modules/ietf-miss-desc-enums-bits.yang
new file mode 100644
index 000000000..a82295a00
--- /dev/null
+++ b/tests/yanglint/modules/ietf-miss-desc-enums-bits.yang
@@ -0,0 +1,40 @@
+module ietf-miss-desc-enums-bits {
+ yang-version 1.1;
+ namespace "urn:ietf:params:xml:ns:yang:ietf-miss-desc-enums-bits";
+ prefix desc-enum-bits;
+
+ organization "IETF NETMOD Working Group";
+ contact "WG Web: ";
+ description
+ "Copyright (c) 2026 IETF Trust and the persons identified as
+ authors of the code. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or
+ without modification, is permitted pursuant to, and subject
+ to the license terms contained in, the Revised BSD License
+ set forth in Section 4.c of the IETF Trust's Legal Provisions
+ Relating to IETF Documents
+ (https://trustee.ietf.org/license-info).
+
+ This version of this YANG module is part of RFC XXXX; see
+ the RFC itself for full legal notices.";
+
+ revision 2026-07-08 {
+ description "Initial revision.";
+ reference "RFC XXXX: Test Module";
+ }
+
+ leaf enum-leaf {
+ type enumeration {
+ enum a;
+ }
+ description "Leaf description provided to isolate the enum warning.";
+ }
+
+ leaf bits-leaf {
+ type bits {
+ bit b;
+ }
+ description "Leaf description provided to isolate the bit warning.";
+ }
+}
diff --git a/tests/yanglint/modules/ietf-miss-desc-operations.yang b/tests/yanglint/modules/ietf-miss-desc-operations.yang
new file mode 100644
index 000000000..f4867f91f
--- /dev/null
+++ b/tests/yanglint/modules/ietf-miss-desc-operations.yang
@@ -0,0 +1,63 @@
+module ietf-miss-desc-operations {
+ yang-version 1.1;
+ namespace "urn:ietf:params:xml:ns:yang:ietf-miss-desc-operations";
+ prefix desc-ops;
+
+ organization "IETF NETMOD Working Group";
+ contact "WG Web: ";
+ description
+ "Copyright (c) 2026 IETF Trust and the persons identified as
+ authors of the code. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or
+ without modification, is permitted pursuant to, and subject
+ to the license terms contained in, the Revised BSD License
+ set forth in Section 4.c of the IETF Trust's Legal Provisions
+ Relating to IETF Documents
+ (https://trustee.ietf.org/license-info).
+
+ This version of this YANG module is part of RFC XXXX; see
+ the RFC itself for full legal notices.";
+
+ revision 2026-07-08 {
+ description "Initial revision.";
+ reference "RFC XXXX: Test Module";
+ }
+
+ rpc my-rpc {
+ input {
+ typedef in-type {
+ type string;
+ }
+ leaf rpc-in {
+ type string;
+ description "Valid description to isolate the typedef/rpc errors.";
+ }
+ }
+ }
+
+ notification my-notif {
+ leaf n-leaf {
+ type string;
+ description "Valid description to isolate the notification error.";
+ }
+ }
+
+ list action-list {
+ key "k2";
+ description "Valid list description.";
+
+ leaf k2 {
+ type string;
+ description "Valid key description.";
+ }
+
+ action my-act {
+ input {
+ leaf act-in {
+ type string;
+ }
+ }
+ }
+ }
+}
diff --git a/tests/yanglint/modules/ietf-miss-desc-top-level.yang b/tests/yanglint/modules/ietf-miss-desc-top-level.yang
new file mode 100644
index 000000000..24c3094c3
--- /dev/null
+++ b/tests/yanglint/modules/ietf-miss-desc-top-level.yang
@@ -0,0 +1,55 @@
+module ietf-miss-desc-top-level {
+ yang-version 1.1;
+ namespace "urn:ietf:params:xml:ns:yang:ietf-miss-desc-top-level";
+ prefix desc-top;
+
+ organization "IETF NETMOD Working Group";
+ contact "WG Web: ";
+ description
+ "Copyright (c) 2026 IETF Trust and the persons identified as
+ authors of the code. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or
+ without modification, is permitted pursuant to, and subject
+ to the license terms contained in, the Revised BSD License
+ set forth in Section 4.c of the IETF Trust's Legal Provisions
+ Relating to IETF Documents
+ (https://trustee.ietf.org/license-info).
+
+ This version of this YANG module is part of RFC XXXX; see
+ the RFC itself for full legal notices.";
+
+ revision 2026-07-08 {
+ description "Initial revision.";
+ reference "RFC XXXX: Test Module";
+ }
+
+ extension ext1;
+
+ feature feat1;
+
+ identity id1;
+
+ typedef top-type {
+ type string;
+ }
+
+ grouping top-grp {
+ leaf x {
+ type string;
+ description "Silencing internal grouping errors.";
+ }
+ }
+
+ container cont {
+ description "Dummy container for augment target.";
+ }
+
+ augment "/desc-top:cont" {
+ // Top-level augment missing description
+ leaf a-leaf {
+ type string;
+ description "Leaf desc";
+ }
+ }
+}
diff --git a/tests/yanglint/modules/ietf-miss-meta.yang b/tests/yanglint/modules/ietf-miss-meta.yang
new file mode 100644
index 000000000..b5a71b6b0
--- /dev/null
+++ b/tests/yanglint/modules/ietf-miss-meta.yang
@@ -0,0 +1,10 @@
+module ietf-miss-meta {
+ yang-version 1.1;
+ namespace "urn:ietf:params:xml:ns:yang:ietf-miss-meta";
+ prefix imm;
+
+ // Triggers 4.8: missing "organization"
+ // Triggers 4.8: missing "contact"
+ // Triggers 4.8: missing "revision"
+ // Triggers 4.8: missing "description"
+}
diff --git a/tests/yanglint/modules/ietf-newer-sub.yang b/tests/yanglint/modules/ietf-newer-sub.yang
new file mode 100644
index 000000000..928b4c977
--- /dev/null
+++ b/tests/yanglint/modules/ietf-newer-sub.yang
@@ -0,0 +1,27 @@
+submodule ietf-newer-sub {
+ yang-version 1.1;
+ belongs-to ietf-older-main {
+ prefix om;
+ }
+
+ organization "IETF NETMOD Working Group";
+ contact "WG Web: ";
+ description
+ "Copyright (c) 2026 IETF Trust and the persons identified as
+ authors of the code. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or
+ without modification, is permitted pursuant to, and subject
+ to the license terms contained in, the Revised BSD License
+ set forth in Section 4.c of the IETF Trust's Legal Provisions
+ Relating to IETF Documents
+ (https://trustee.ietf.org/license-info).
+
+ This version of this YANG submodule is part of RFC XXXX; see
+ the RFC itself for full legal notices.";
+
+ revision 2026-07-08 {
+ description "Newer revision.";
+ reference "RFC XXXX";
+ }
+}
diff --git a/tests/yanglint/modules/ietf-older-main.yang b/tests/yanglint/modules/ietf-older-main.yang
new file mode 100644
index 000000000..0bd013269
--- /dev/null
+++ b/tests/yanglint/modules/ietf-older-main.yang
@@ -0,0 +1,32 @@
+module ietf-older-main {
+ yang-version 1.1;
+ namespace "urn:ietf:params:xml:ns:yang:ietf-older-main";
+ prefix om;
+
+ include ietf-newer-sub;
+ import ietf-imported-module {
+ prefix iim;
+ }
+
+ organization "IETF NETMOD Working Group";
+ contact "WG Web: ";
+ description
+ "Copyright (c) 2026 IETF Trust and the persons identified as
+ authors of the code. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or
+ without modification, is permitted pursuant to, and subject
+ to the license terms contained in, the Revised BSD License
+ set forth in Section 4.c of the IETF Trust's Legal Provisions
+ Relating to IETF Documents
+ (https://trustee.ietf.org/license-info).
+
+ This version of this YANG module is part of RFC XXXX; see
+ the RFC itself for full legal notices.";
+
+ // Triggers 4.7: Revision date is older than the submodule's date
+ revision 2020-01-01 {
+ description "Initial revision.";
+ reference "RFC XXXX";
+ }
+}
diff --git a/tests/yanglint/modules/ietf-xpath.yang b/tests/yanglint/modules/ietf-xpath.yang
new file mode 100644
index 000000000..213b1cc13
--- /dev/null
+++ b/tests/yanglint/modules/ietf-xpath.yang
@@ -0,0 +1,76 @@
+module ietf-xpath {
+ yang-version 1.1;
+ namespace "urn:ietf:params:xml:ns:yang:ietf-xpath";
+ prefix ixt;
+
+ organization "IETF NETMOD Working Group";
+
+ contact
+ "WG Web:
+ WG List:
+
+ Author: Test Author
+ ";
+
+ description
+ "Copyright (c) 2026 IETF Trust and the persons identified as
+ authors of the code. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or
+ without modification, is permitted pursuant to, and subject
+ to the license terms contained in, the Revised BSD License
+ set forth in Section 4.c of the IETF Trust's Legal Provisions
+ Relating to IETF Documents
+ (https://trustee.ietf.org/license-info).
+
+ This version of this YANG module is part of RFC XXXX; see
+ the RFC itself for full legal notices.";
+
+ revision 2026-07-10 {
+ description
+ "Initial revision. Testing banned XPath functions and axes.";
+ reference
+ "RFC XXXX: Guidelines for Authors and Reviewers of YANG Data Models";
+ }
+
+ container top-container {
+ description "A top level container to hold our test data.";
+
+ /* TEST 1: Banned function 'local-name()' in a 'when' statement */
+ container function-test-1 {
+ when "local-name() = 'function-test-1'";
+ description "Testing banned local-name() function.";
+ }
+
+ /* TEST 2: Banned function 'position()' in a 'must' statement */
+ list function-test-2 {
+ key "id";
+ must "position() < 10";
+ description "Testing banned position() function.";
+
+ leaf id {
+ type string;
+ description "Key leaf.";
+ }
+ }
+
+ /* TEST 3: Banned axis 'preceding-sibling::' in a 'when' statement */
+ leaf axis-test-1 {
+ type string;
+ when "preceding-sibling::function-test-2";
+ description "Testing banned preceding-sibling axis.";
+ }
+
+ /* TEST 4: Banned axis 'following::' in a 'must' statement */
+ leaf-list axis-test-2 {
+ type string;
+ must "following::ixt:axis-test-3 = 'test'";
+ description "Testing banned following axis.";
+ }
+
+ leaf axis-test-3 {
+ type string;
+ description "Dummy node so the 'following' axis actually finds something.";
+ }
+ }
+}
diff --git a/tests/yanglint/modules/skeleton-augment.yang b/tests/yanglint/modules/skeleton-augment.yang
new file mode 100644
index 000000000..0d1f792ef
--- /dev/null
+++ b/tests/yanglint/modules/skeleton-augment.yang
@@ -0,0 +1,24 @@
+module skeleton-augment {
+ yang-version 1.1;
+ namespace "urn:ietf:params:xml:ns:yang:skeleton-augment";
+ prefix sa;
+
+ import skeleton {
+ prefix sc;
+ }
+
+ // Injects these nodes into the parent from a different module
+ augment "/sc:main-box" {
+
+ leaf aug-leaf {
+ type string;
+ default "augmented-value";
+ }
+
+ leaf-list aug-ll {
+ type uint16;
+ default 1;
+ default 2;
+ }
+ }
+}
diff --git a/tests/yanglint/modules/skeleton.yang b/tests/yanglint/modules/skeleton.yang
new file mode 100644
index 000000000..4b7b9002a
--- /dev/null
+++ b/tests/yanglint/modules/skeleton.yang
@@ -0,0 +1,83 @@
+module skeleton {
+ yang-version 1.1;
+ namespace "urn:ietf:params:xml:ns:yang:skeleton-complex";
+ prefix sc;
+
+ rpc do-nothing {
+ input { leaf in { type string; } }
+ }
+ notification alert {
+ leaf msg { type string; }
+ }
+
+ container main-box {
+
+ leaf empty-leaf {
+ type empty;
+ }
+
+ leaf bool-leaf {
+ type boolean;
+ default true;
+ }
+ leaf num32-leaf {
+ type int32;
+ default -100;
+ }
+ leaf num64-leaf {
+ type uint64;
+ default 9999999999;
+ }
+ leaf str-leaf {
+ type string;
+ default "hello";
+ }
+
+ leaf-list ll-empty {
+ type string;
+ }
+ leaf-list ll-numbers {
+ type uint8;
+ default 10;
+ default 20;
+ }
+
+ choice no-def-choice {
+ case a {
+ leaf a-leaf { type string; }
+ }
+ case b {
+ leaf b-leaf { type string; }
+ }
+ }
+
+ choice def-choice {
+ default case-2;
+ case case-1 {
+ leaf c1-leaf { type string; }
+ }
+ case case-2 {
+ leaf c2-leaf {
+ type string;
+ default "default-choice-val";
+ }
+ }
+ }
+
+ list server-list {
+ key "id";
+ leaf id {
+ type string;
+ }
+ action reset {
+ input { leaf delay { type uint32; } }
+ }
+ container config {
+ leaf ip { type string; }
+ }
+ }
+
+ anydata custom-data;
+ anyxml custom-xml;
+ }
+}
diff --git a/tests/yanglint/non-interactive/ietf.test b/tests/yanglint/non-interactive/ietf.test
new file mode 100644
index 000000000..9eac8cf5d
--- /dev/null
+++ b/tests/yanglint/non-interactive/ietf.test
@@ -0,0 +1,156 @@
+source [expr {[info exists ::env(TESTS_DIR)] ? "$env(TESTS_DIR)/non-interactive/ly.tcl" : "ly.tcl"}]
+
+set mdir $::env(YANG_MODULES_DIR)
+
+test ietf_boilerplate_bad_text {} {
+ set pattern "(?s)^"
+ append pattern "(?=.*warning: RFC 9907: 3.1: The IETF Trust Copyright statement seems to be missing or is not correct)"
+ append pattern "(?=.*warning: RFC 9907: Appendix B: The text about which RFC this module is part of seems to be missing or is not correct)"
+ append pattern "(?=.*warning: the module seems to use RFC 2119 keywords, but the required text from RFC 8174 is not found or is not correct)"
+ append pattern ".*"
+
+ ly_cmd_err "--ietf $mdir/ietf-bad-boilerplate.yang" $pattern
+} {}
+
+test ietf_long_indentifier {} {
+ set pattern "(?s)^"
+ append pattern "(?=.*error: RFC 9907: 4.3: identifier this-container-identifier-is-intentionally-designed-to-exceed-64-chars exceeds 64 characters)"
+ append pattern "(?=.*error: RFC 9907: 4.3: identifier this-enum-identifier-is-intentionally-designed-to-exceed-64-chars exceeds 64 characters)"
+ append pattern "(?=.*error: RFC 9907: 4.3: identifier ietf-example-this-identifier-is-intentionally-designed-to-exceed-64-chars exceeds 64 characters)"
+ append pattern "(?=.*error: RFC 9907: 4.3: identifier this-feature-identifier-is-intentionally-designed-to-exceed-64-chars exceeds 64 characters)"
+ append pattern "(?=.*error: RFC 9907: 4.3: identifier this-identity-identifier-is-intentionally-designed-to-exceed-64-chars exceeds 64 characters)"
+ append pattern "(?=.*error: RFC 9907: 4.3: identifier this-typedef-identifier-is-intentionally-designed-to-exceed-64-chars exceeds 64 characters)"
+ append pattern "(?=.*error: RFC 9907: 4.3: identifier this-grouping-identifier-is-intentionally-designed-to-exceed-64-chars exceeds 64 characters)"
+ append pattern ".*"
+
+ ly_cmd_err "--ietf $mdir/ietf-example-this-identifier-is-intentionally-designed-to-exceed-64-chars.yang" $pattern
+} {}
+
+test ietf_defaults_and_mandatory {} {
+ set pattern "(?s)^"
+ append pattern "(?=.*error: RFC 9907: 4.10: top-level node \"top-mand-leaf\" must not be mandatory)"
+ append pattern "(?=.*warning: RFC 9907: 4.4: statement \"config\" is given with its default value \"true\")"
+ append pattern "(?=.*warning: RFC 9907: 4.4: statement \"status\" is given with its default value \"current\")"
+ append pattern "(?=.*warning: RFC 9907: 4.4: statement \"mandatory\" is given with its default value \"false\")"
+ append pattern "(?=.*warning: RFC 9907: 4.4: statement \"min-elements\" is given with its default value \"0\")"
+ append pattern "(?=.*warning: RFC 9907: 4.4: statement \"max-elements\" is given with its default value \"unbounded\")"
+ append pattern "(?=.*warning: RFC 9907: 4.4: statement \"yin-element\" is given with its default value \"false\")"
+ append pattern "(?=.*warning: RFC 9907: 4.4: statement \"ordered-by\" is given with its default value \"system\")"
+ append pattern ".*"
+
+ ly_cmd_err "--ietf $mdir/ietf-defaults-and-mandatory.yang" $pattern
+} {}
+
+test ietf_list_key {} {
+ set pattern "(?s)^"
+ append pattern "(?=.*warning: RFC 9907: 4.5: key leaf \"id\" should not have a \"when\" statement)"
+ append pattern "(?=.*error: RFC 9907: 4.5: key leaf \"id\" must have the exact same \"if-feature\" statements as its parent list \"list-missing-if-feature\")"
+ append pattern "(?=.*error: RFC 9907: 4.5: key leaf \"id\" must have the exact same \"if-feature\" statements as its parent list \"list-mismatched-if-feature\")"
+ append pattern "(?=.*warning: RFC 9907: 4.5: constraints \\('must' statements\\) on state data should be avoided \\(leaf \"state-data-with-must\"\\))"
+ append pattern ".*"
+
+ ly_cmd_err "--ietf $mdir/ietf-list-key.yang" $pattern
+} {}
+
+test ietf_xpath {} {
+ set pattern "(?s)^"
+ append pattern "(?=.*warning: RFC 9907: 4.6.2: XPath function in \"when\" statement on \"function-test-1\" should not be used)"
+ append pattern "(?=.*warning: RFC 9907: 4.6.2: XPath function in \"must\" statement on \"function-test-2\" should not be used)"
+ append pattern "(?=.*warning: RFC 9907: 4.6.3: XPath axis in \"when\" statement on \"axis-test-1\" should not be used)"
+ append pattern "(?=.*warning: RFC 9907: 4.6.3: XPath axis in \"must\" statement on \"axis-test-2\" should not be used)"
+ append pattern ".*"
+
+ ly_cmd_err "--ietf $mdir/ietf-xpath.yang" $pattern
+} {}
+
+test ietf_boilerplate_submodule_rev {} {
+ set pattern "(?s)^"
+ append pattern "(?=.*error: RFC 9907: 4.7: the module's revision 2020-01-01 is older than submodule ietf-newer-sub's revision 2026-07-08)"
+ append pattern "(?=.*warning: RFC 9907: 4.7: statement \"import\" for stable module \"ietf-imported-module\" should have a \"reference\" substatement)"
+ append pattern ".*"
+
+ ly_cmd_err "--ietf $mdir/ietf-older-main.yang" $pattern
+} {}
+
+test ietf_boilerplate_missing_meta {} {
+ set pattern "(?s)^"
+ append pattern "(?=.*error: RFC 9907: 4.8: statement \"module\" must have a \"contact\" substatement)"
+ append pattern "(?=.*error: RFC 9907: 4.8: statement \"module\" must have a \"organization\" substatement)"
+ append pattern "(?=.*error: RFC 9907: 4.8: statement \"module\" must have a \"revision\" substatement)"
+ append pattern "(?=.*error: RFC 9907: 4.8: statement \"module\" must have a \"description\" substatement)"
+ append pattern ".*"
+
+ ly_cmd_err "--ietf $mdir/ietf-miss-meta.yang" $pattern
+} {}
+
+test ietf_boilerplate_naming_ns {} {
+ set pattern "(?s)^"
+ append pattern "(?=.*warning: RFC 9907: 4.1: the module name should start with one of the strings \"ietf-\" or \"iana-\")"
+ append pattern "(?=.*warning: RFC 9907: 4.9: namespace value should be \"urn:ietf:params:xml:ns:yang:bad-ietf-prefix-namespace\")"
+ append pattern "(?=.*error: RFC 9907: 4.8: statement \"revision\" 2026-07-08 must have a \"reference\" substatement)"
+ append pattern "(?=.*warning: RFC 9907: 4.8: statement \"revision\" 2026-07-08 should have a \"description\" substatement summarizing changes)"
+ append pattern ".*"
+
+ ly_cmd_err "--ietf $mdir/bad-ietf-prefix-namespace.yang" $pattern
+} {}
+
+test ietf_description_enum_bits {} {
+ set pattern "(?s)^"
+ append pattern "(?=.*warning: RFC 9907: 4.11.3: statement \"enum\" should have a \"description\" substatement)"
+ append pattern "(?=.*warning: RFC 9907: 4.11.3: statement \"bit\" should have a \"description\" substatement)"
+ append pattern ".*"
+
+ ly_cmd_err "--ietf $mdir/ietf-miss-desc-enums-bits.yang" $pattern
+} {}
+
+test ietf_empty_type {} {
+ set pattern "(?s)^"
+ append pattern "(?=.*warning: RFC 9907: 4.11.5: the type \"empty\" should not be used for a key leaf \"id\")"
+ append pattern "(?=.*warning: RFC 9907: 4.11.5: the type \"empty\" should not be used for a leaf-list \\(\"leaf-list-with-empty-type\"\\))"
+ append pattern "(?=.*warning: RFC 9907: 4.11.5: in \"id\", the \"boolean\" data type should be used instead of the \"empty\" data type)"
+ append pattern ".*"
+
+ ly_cmd_err "--ietf $mdir/ietf-empty-type.yang" $pattern
+} {}
+
+test ietf_description_top {} {
+ set pattern "(?s)^"
+ append pattern "(?=.*error: RFC 9907: 4.14: statement \"extension\" must have a \"description\" substatement)"
+ append pattern "(?=.*error: RFC 9907: 4.14: statement \"grouping\" must have a \"description\" substatement)"
+ append pattern "(?=.*error: RFC 9907: 4.14: statement \"feature\" must have a \"description\" substatement)"
+ append pattern "(?=.*error: RFC 9907: 4.14: statement \"identity\" must have a \"description\" substatement)"
+ append pattern "(?=.*error: RFC 9907: 4.13,4.14: statement \"typedef\" must have a \"description\" substatement)"
+ append pattern "(?=.*error: RFC 9907: 4.14: statement \"augment\" must have a \"description\" substatement)"
+ append pattern ".*"
+
+ ly_cmd_err "--ietf $mdir/ietf-miss-desc-top-level.yang" $pattern
+} {}
+
+test ietf_description_data_nodes {} {
+ set pattern "(?s)^"
+ append pattern "(?=.*error: RFC 9907: 4.14: statement \"container\" must have a \"description\" substatement)"
+ append pattern "(?=.*error: RFC 9907: 4.14: statement \"leaf\" must have a \"description\" substatement)"
+ append pattern "(?=.*error: RFC 9907: 4.14: statement \"list\" must have a \"description\" substatement)"
+ append pattern "(?=.*error: RFC 9907: 4.14: statement \"leaf-list\" must have a \"description\" substatement)"
+ append pattern "(?=.*error: RFC 9907: 4.14: statement \"anyxml\" must have a \"description\" substatement)"
+ append pattern "(?=.*error: RFC 9907: 4.14: statement \"choice\" must have a \"description\" substatement)"
+ append pattern "(?=.*error: RFC 9907: 4.14: statement \"anydata\" must have a \"description\" substatement)"
+ append pattern ".*"
+
+ ly_cmd_err "--ietf $mdir/ietf-miss-desc-data-nodes.yang" $pattern
+} {}
+
+test ietf_description_operations {} {
+ set pattern "(?s)^"
+ append pattern "(?=.*error: RFC 9907: 4.13,4.14: statement \"typedef\" inside input of \"my-rpc\" must have a \"description\" substatement)"
+ append pattern "(?=.*error: RFC 9907: 4.14: statement \"leaf\" must have a \"description\" substatement)"
+ append pattern "(?=.*error: RFC 9907: 4.14: statement \"RPC\" must have a \"description\" substatement)"
+ append pattern "(?=.*error: RFC 9907: 4.14,4.16: statement \"notification\" must have a \"description\" substatement)"
+ append pattern ".*"
+
+ ly_cmd_err "--ietf $mdir/ietf-miss-desc-operations.yang" $pattern
+} {}
+
+test ietf_correct {} {
+ ly_cmd "--ietf $mdir/ietf-correct.yang"
+} {}
diff --git a/tests/yanglint/non-interactive/sample-skeleton.test b/tests/yanglint/non-interactive/sample-skeleton.test
new file mode 100644
index 000000000..a7286b373
--- /dev/null
+++ b/tests/yanglint/non-interactive/sample-skeleton.test
@@ -0,0 +1,69 @@
+source [expr {[info exists ::env(TESTS_DIR)] ? "$env(TESTS_DIR)/non-interactive/ly.tcl" : "ly.tcl"}]
+
+set mdir $::env(YANG_MODULES_DIR)
+
+test skeleton_generate_xml {} {
+ set pattern "(?s)^"
+ append pattern {.*}
+ append pattern {.*}
+ append pattern {.*true}
+ append pattern {.*-100}
+ append pattern {.*9999999999}
+ append pattern {.*hello}
+ append pattern {.*}
+ append pattern {.*10}
+ append pattern {.*20}
+ append pattern {.*}
+ append pattern {.*default-choice-val}
+ append pattern {.*}
+ append pattern {.*}
+ append pattern {.*}
+ append pattern {.*}
+ append pattern {.*}
+ append pattern {.*}
+ append pattern {.*}
+ append pattern {.*}
+ append pattern {.*augmented-value}
+ append pattern {.*1}
+ append pattern {.*2}
+ append pattern {.*}
+ append pattern ".*"
+
+ ly_cmd "-S xml $mdir/skeleton.yang $mdir/skeleton-augment.yang" $pattern
+} {}
+
+test skeleton_generate_json {} {
+ set pattern "(?s)^"
+ append pattern {.*"skeleton:main-box": \{}
+ append pattern {.*"empty-leaf": \[null\],}
+ append pattern {.*"bool-leaf": true,}
+ append pattern {.*"num32-leaf": -100,}
+ append pattern {.*"num64-leaf": "9999999999",}
+ append pattern {.*"str-leaf": "hello",}
+ append pattern {.*"ll-empty": \[\],}
+ append pattern {.*"ll-numbers": \[}
+ append pattern {.*10,}
+ append pattern {.*20}
+ append pattern {.*],}
+ append pattern {.*"a-leaf": null,}
+ append pattern {.*"c2-leaf": "default-choice-val",}
+ append pattern {.*"server-list": \[}
+ append pattern {.*\{}
+ append pattern {.*"id": null,}
+ append pattern {.*"config": \{}
+ append pattern {.*"ip": null}
+ append pattern {.*\}}
+ append pattern {.*\}}
+ append pattern {.*\],}
+ append pattern {.*"custom-data": \{\},}
+ append pattern {.*"custom-xml": \{\},}
+ append pattern {.*"skeleton-augment:aug-leaf": "augmented-value",}
+ append pattern {.*"skeleton-augment:aug-ll": \[}
+ append pattern {.*1,}
+ append pattern {.*2}
+ append pattern {.*\]}
+ append pattern {.*\}}
+ append pattern ".*"
+
+ ly_cmd "-S json $mdir/skeleton.yang $mdir/skeleton-augment.yang" $pattern
+} {}
diff --git a/tools/lint/CMakeLists.txt b/tools/lint/CMakeLists.txt
index 42d43093b..58b99fd57 100644
--- a/tools/lint/CMakeLists.txt
+++ b/tools/lint/CMakeLists.txt
@@ -14,6 +14,8 @@ set(lintsrc
cmd_help.c
cmd_verb.c
cmd_debug.c
+ cmd_sample.c
+ ietf.c
yl_opt.c
yl_schema_features.c
common.c
diff --git a/tools/lint/cmd.c b/tools/lint/cmd.c
index 1f1e791f5..5198470f9 100644
--- a/tools/lint/cmd.c
+++ b/tools/lint/cmd.c
@@ -72,6 +72,10 @@ COMMAND commands[] = {
"data", cmd_data_opt, cmd_data_dep, cmd_data_store, cmd_data_process, cmd_data_help, NULL,
"Load, validate and optionally print instance data", "d:ef:F:hmo:O:R:r:nt:x:k:"
},
+ {
+ "sample", cmd_sample_opt, cmd_sample_dep, cmd_sample_exec, NULL, cmd_sample_help, NULL,
+ "Generate a sample data skeleton for a module", "f:ho:"
+ },
{
"list", cmd_list_opt, cmd_list_dep, cmd_list_exec, NULL, cmd_list_help, NULL,
"List all the loaded modules", "f:h"
diff --git a/tools/lint/cmd.h b/tools/lint/cmd.h
index e9ee5b492..2077dd777 100644
--- a/tools/lint/cmd.h
+++ b/tools/lint/cmd.h
@@ -73,6 +73,7 @@ enum COMMAND_INDEX {
CMD_LOAD,
CMD_PRINT,
CMD_DATA,
+ CMD_SAMPLE,
CMD_LIST,
CMD_FEATURE,
CMD_SEARCHPATH,
@@ -276,6 +277,29 @@ int cmd_print_dep(struct yl_opt *yo, int posc);
int cmd_print_exec(struct ly_ctx **ctx, struct yl_opt *yo, const char *posv);
void cmd_print_help(void);
+/* cmd_sample.c */
+
+/**
+ * @copydoc cmd_add_opt
+ */
+int cmd_sample_opt(struct yl_opt *yo, const char *cmdline, char ***posv, int *posc);
+
+/**
+ * @copydoc cmd_add_dep
+ */
+int cmd_sample_dep(struct yl_opt *yo, int posc);
+void cmd_sample_help(void);
+
+/**
+ * @brief Print a sample skeleton of module in json or xml.
+ *
+ * @param[in,out] ctx context for libyang.
+ * @param[in] yo context for yanglint.
+ * @param[in] posv Name of the module to be printed.
+ * @return 0 on success, 1 on failure.
+ */
+int cmd_sample_exec(struct ly_ctx **ctx, struct yl_opt *yo, const char *posv);
+
/* cmd_searchpath.c */
/**
diff --git a/tools/lint/cmd_add.c b/tools/lint/cmd_add.c
index 9f107110a..00f96a188 100644
--- a/tools/lint/cmd_add.c
+++ b/tools/lint/cmd_add.c
@@ -130,7 +130,7 @@ store_parsed_module(const char *filepath, struct lys_module *mod, struct yl_opt
{
assert(!yo->interactive);
- if (yo->schema_out_format || yo->feature_param_format) {
+ if (yo->schema_out_format || yo->feature_param_format || yo->sample_skeleton || yo->ietf_validation) {
if (ly_set_add(&yo->schema_modules, (void *)mod, 1, NULL)) {
YLMSG_E("Storing parsed schema module (%s) for print failed.", filepath);
return 1;
diff --git a/tools/lint/cmd_sample.c b/tools/lint/cmd_sample.c
new file mode 100644
index 000000000..374719309
--- /dev/null
+++ b/tools/lint/cmd_sample.c
@@ -0,0 +1,566 @@
+/**
+ * @file cmd_sample.c
+ * @author Petr Hanzlik
+ * @brief 'sample-skeleton' command of the libyang's yanglint tool.
+ *
+ * Copyright (c) 2026 CESNET, z.s.p.o.
+ *
+ * This source code is licensed under BSD 3-Clause License (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://opensource.org/licenses/BSD-3-Clause
+ */
+
+#define _GNU_SOURCE
+
+#include "cmd.h"
+
+#include
+#include
+#include
+
+#include "libyang.h"
+
+#include "common.h"
+#include "yl_opt.h"
+
+#define SPACE_COUNT 2
+
+static void print_xml_nodes(struct ly_out *out, const struct lysc_node *node, int space_count);
+static void print_json_nodes(struct ly_out *out, const struct lysc_node *node, const char *name, int space_count, int *need_comma);
+
+void
+cmd_sample_help(void)
+{
+ printf("Usage: sample [-f (xml | json)] [-o OUTFILE] [@revision]\n"
+ " Generate and print a sample data skeleton from a loaded schema module.\n\n"
+ " -f FORMAT, --format=FORMAT\n"
+ " Print the sample skeleton in the specified FORMAT.\n"
+ " Supported formats: xml, json.\n"
+ " -o OUTFILE, --output=OUTFILE\n"
+ " Write the output to OUTFILE instead of stdout.\n"
+ " -h, --help\n"
+ " Display this help message.\n");
+}
+
+int
+cmd_sample_dep(struct yl_opt *yo, int posc)
+{
+ if (yo->interactive && !posc) {
+ YLMSG_E("Missing module name to generate skeleton for.\n");
+ return 1;
+ }
+
+ if (!yo->data_out_format) {
+ YLMSG_E("Missing required format (-f xml | json).\n");
+ return 1;
+ }
+
+ return 0;
+}
+
+int
+cmd_sample_opt(struct yl_opt *yo, const char *cmdline, char ***posv, int *posc)
+{
+ int rc = 0, argc = 0;
+ int opt, opt_index;
+ char **argv = NULL;
+ struct option options[] = {
+ {"format", required_argument, NULL, 'f'},
+ {"output", required_argument, NULL, 'o'},
+ {"help", no_argument, NULL, 'h'},
+ {NULL, 0, NULL, 0}
+ };
+
+ if ((rc = parse_cmdline(cmdline, &argc, &argv))) {
+ return rc;
+ }
+
+ /* reset getopt */
+ optind = 0;
+ while ((opt = getopt_long(argc, argv, "f:o:h", options, &opt_index)) != -1) {
+ switch (opt) {
+ case 'f': /* --format */
+ if (yl_opt_update_data_out_format(optarg, yo)) {
+ rc = 1;
+ goto cleanup;
+ }
+ break;
+
+ case 'o': /* --output */
+ if (yo->out) {
+ ly_out_free(yo->out, NULL, 0);
+ yo->out = NULL;
+ }
+ if (ly_out_new_filepath(optarg, &yo->out)) {
+ YLMSG_E("Unable to open output file %s.\n", optarg);
+ rc = 1;
+ goto cleanup;
+ }
+ break;
+
+ case 'h': /* --help */
+ cmd_sample_help();
+ rc = 1;
+ goto cleanup;
+
+ default:
+ YLMSG_E("Unknown option.\n");
+ rc = 1;
+ goto cleanup;
+ }
+ }
+
+ *posc = argc - optind;
+ *posv = &argv[optind];
+
+ return 0;
+
+cleanup:
+ free_cmdline(argv);
+ return rc;
+}
+
+/**
+ * @brief Print a specific number of spaces for indentation.
+ *
+ * @param[in,out] out Libyang output handler to print to.
+ * @param[in] count Number of spaces to print.
+ */
+static void
+print_space(struct ly_out *out, int count)
+{
+ for (int i = 0; i < count; i++) {
+ ly_print(out, " ");
+ }
+}
+
+/**
+ * @brief Handle and print the default values or closing XML tags for a schema node.
+ *
+ * @param[in,out] out Libyang output handler to print to.
+ * @param[in] node Compiled schema node currently being processed.
+ * @param[in] space_count Current indentation level (number of spaces).
+ */
+static void
+handle_xml_default(struct ly_out *out, const struct lysc_node *node, int space_count)
+{
+ switch (node->nodetype) {
+ case LYS_LEAF: {
+ struct lysc_node_leaf *leaf = (struct lysc_node_leaf *)node;
+
+ if (leaf->dflt.str) {
+ ly_print(out, ">%s%s>\n", leaf->dflt.str, node->name);
+ } else {
+ ly_print(out, "/>\n");
+ }
+ break;
+ }
+ case LYS_LEAFLIST: {
+ struct lysc_node_leaflist *llist = (struct lysc_node_leaflist *)node;
+
+ if (llist->dflts) {
+ LY_ARRAY_COUNT_TYPE i;
+
+ LY_ARRAY_FOR(llist->dflts, i) {
+ print_space(out, space_count);
+ if (node->parent && (node->module != node->parent->module)) {
+ ly_print(out, "<%s xmlns=\"%s\">%s%s>\n", llist->name, node->module->ns, llist->dflts[i].str, llist->name);
+ } else {
+ ly_print(out, "<%s>%s%s>\n", llist->name, llist->dflts[i].str, llist->name);
+ }
+ }
+ } else {
+ print_space(out, space_count);
+ if (node->parent && (node->module != node->parent->module)) {
+ ly_print(out, "<%s xmlns=\"%s\">%s>\n", llist->name, node->module->ns, llist->name);
+ } else {
+ ly_print(out, "<%s>%s>\n", llist->name, llist->name);
+ }
+ }
+ break;
+ }
+ case LYS_CHOICE: {
+ struct lysc_node_choice *choice = (struct lysc_node_choice *)node;
+
+ if (choice->dflt) {
+ print_xml_nodes(out, lysc_node_child((const struct lysc_node *)choice->dflt), space_count);
+ } else {
+ if (lysc_node_child(node)) {
+ print_xml_nodes(out, lysc_node_child(lysc_node_child(node)), space_count);
+ }
+ }
+ break;
+ }
+ case LYS_CONTAINER:
+ case LYS_LIST:
+ case LYS_ANYDATA:
+ case LYS_ANYXML:
+ ly_print(out, "/>\n");
+ break;
+ }
+}
+
+/**
+ * @brief Traverse the compiled schema tree and print XML skeleton tags.
+ *
+ * @param[in,out] out Libyang output handler to print to.
+ * @param[in] node Compiled schema node to begin traversing.
+ * @param[in] space_count Current indentation level.
+ */
+static void
+print_xml_nodes(struct ly_out *out, const struct lysc_node *node, int space_count)
+{
+ const uint32_t PRINT_MASK = LYS_CONTAINER | LYS_LIST | LYS_LEAF | LYS_ANYXML | LYS_ANYDATA;
+ int nodetype;
+
+ while (node) {
+ nodetype = node->nodetype;
+
+ if (nodetype & (LYS_RPC | LYS_ACTION | LYS_NOTIF)) {
+ node = node->next;
+ continue;
+ }
+
+ if (nodetype & PRINT_MASK) {
+ print_space(out, space_count);
+
+ if (!node->parent) {
+ ly_print(out, "<%s xmlns=\"%s\"", node->name, node->module->ns);
+ } else if (node->parent && (node->module != node->parent->module)) {
+ ly_print(out, "<%s xmlns=\"%s\"", node->name, node->module->ns);
+ } else {
+ ly_print(out, "<%s", node->name);
+ }
+ }
+
+ if (lysc_node_child(node)) {
+ if (nodetype & PRINT_MASK) {
+ ly_print(out, ">\n");
+ }
+
+ if (nodetype == LYS_CHOICE) {
+ handle_xml_default(out, node, space_count);
+ node = node->next;
+ continue;
+ } else {
+ print_xml_nodes(out, lysc_node_child(node), space_count + SPACE_COUNT);
+ }
+
+ if (nodetype & PRINT_MASK) {
+ print_space(out, space_count);
+ ly_print(out, "%s>\n", node->name);
+ }
+ } else {
+ handle_xml_default(out, node, space_count);
+ }
+
+ node = node->next;
+ }
+}
+
+/**
+ * @brief Initialize and print the top-level XML skeleton structure.
+ *
+ * @param[in,out] out Libyang output handler to print to.
+ * @param[in] root Root compiled schema node of the module being processed.
+ */
+static void
+print_xml_skeleton(struct ly_out *out, struct lysc_node *root)
+{
+ struct lysc_node *node = root;
+ int spaces = SPACE_COUNT;
+
+ ly_print(out, "\n"
+ "\n");
+ print_xml_nodes(out, node, spaces);
+ ly_print(out, "\n");
+
+}
+
+/**
+ * @brief Determine if a YANG base type requires string quotes in JSON encoding.
+ *
+ * @param[in] basetype Libyang data type to evaluate.
+ * @return 1 if the type requires quotes (string-like), 0 if it must be printed raw (numeric/boolean).
+ */
+static int
+is_json_quote_required(LY_DATA_TYPE basetype)
+{
+ switch (basetype) {
+ case LY_TYPE_INT8:
+ case LY_TYPE_INT16:
+ case LY_TYPE_INT32:
+ case LY_TYPE_UINT8:
+ case LY_TYPE_UINT16:
+ case LY_TYPE_UINT32:
+ case LY_TYPE_BOOL:
+ return 0;
+
+ case LY_TYPE_UNKNOWN:
+ case LY_TYPE_STRING:
+ case LY_TYPE_INT64:
+ case LY_TYPE_UINT64:
+ case LY_TYPE_DEC64:
+ case LY_TYPE_BINARY:
+ case LY_TYPE_BITS:
+ case LY_TYPE_ENUM:
+ case LY_TYPE_IDENT:
+ case LY_TYPE_INST:
+ case LY_TYPE_LEAFREF:
+ case LY_TYPE_UNION:
+ case LY_TYPE_EMPTY:
+ return 1;
+ }
+
+ return 1;
+}
+
+/**
+ * @brief Handle and print the default values or closing JSON tags for a schema node.
+ *
+ * @param[in,out] out Libyang output handler to print to.
+ * @param[in] node Compiled schema node currently being processed.
+ * @param[in] space_count Current indentation level (number of spaces).
+ * @param[in,out] need_comma Pointer to the comma state variable.
+ */
+static void
+handle_json_default(struct ly_out *out, const struct lysc_node *node, int space_count, int *need_comma)
+{
+ switch (node->nodetype) {
+ case LYS_LEAF: {
+ struct lysc_node_leaf *leaf = (struct lysc_node_leaf *)node;
+
+ if (leaf->dflt.str) {
+ if (is_json_quote_required(leaf->type->basetype)) {
+ ly_print(out, " \"%s\"", leaf->dflt.str);
+ } else {
+ ly_print(out, " %s", leaf->dflt.str);
+ }
+ } else {
+ if (leaf->type->basetype == LY_TYPE_EMPTY) {
+ ly_print(out, " [null]");
+ } else {
+ ly_print(out, " null");
+ }
+ }
+ break;
+ }
+ case LYS_LEAFLIST: {
+ struct lysc_node_leaflist *llist = (struct lysc_node_leaflist *)node;
+
+ if (llist->dflts) {
+ LY_ARRAY_COUNT_TYPE i;
+
+ ly_print(out, " [\n");
+
+ LY_ARRAY_FOR(llist->dflts, i) {
+ if (i == 0) {
+ print_space(out, space_count + SPACE_COUNT);
+ } else {
+ ly_print(out, ",\n");
+ print_space(out, space_count + SPACE_COUNT);
+ }
+
+ if (is_json_quote_required(llist->type->basetype)) {
+ ly_print(out, "\"%s\"", llist->dflts[i].str);
+ } else {
+ ly_print(out, "%s", llist->dflts[i].str);
+ }
+ }
+ ly_print(out, "\n");
+ print_space(out, space_count);
+ ly_print(out, "]");
+ } else {
+ ly_print(out, " []");
+ }
+ break;
+ }
+ case LYS_CHOICE: {
+ struct lysc_node_choice *choice = (struct lysc_node_choice *)node;
+
+ if (choice->dflt) {
+ print_json_nodes(out, lysc_node_child((const struct lysc_node *)choice->dflt), NULL, space_count, need_comma);
+ } else {
+ if (lysc_node_child(node)) {
+ print_json_nodes(out, lysc_node_child(lysc_node_child(node)), NULL, space_count, need_comma);
+ }
+ }
+ break;
+ }
+ case LYS_CONTAINER:
+ case LYS_ANYDATA:
+ case LYS_ANYXML:
+ ly_print(out, " {}");
+ break;
+ case LYS_LIST:
+ ly_print(out, " []");
+ break;
+ }
+}
+
+/**
+ * @brief Traverse the compiled schema tree and print JSON skeleton tags.
+ *
+ * @param[in,out] out Libyang output handler to print to.
+ * @param[in] node Compiled schema node to begin traversing.
+ * @param[in] name Name of the module, or NULL if inherited.
+ * @param[in] space_count Current indentation level; dynamically incremented and decremented during traversal.
+ * @param[in,out] need_comma Pointer to the state variable tracking JSON comma formatting. Set to 1 if a trailing comma is required before printing the * next sibling, or 0 if the node is the first element in a block.
+ */
+static void
+print_json_nodes(struct ly_out *out, const struct lysc_node *node, const char *name, int space_count, int *need_comma)
+{
+ int nodetype;
+ const uint32_t PRINT_MASK = LYS_CONTAINER | LYS_LIST | LYS_LEAF | LYS_ANYXML | LYS_ANYDATA | LYS_LEAFLIST;
+
+ while (node) {
+ nodetype = node->nodetype;
+
+ if (nodetype & (LYS_RPC | LYS_ACTION | LYS_NOTIF)) {
+ node = node->next;
+ continue;
+ }
+
+ if (nodetype & PRINT_MASK) {
+ if (*need_comma) {
+ ly_print(out, ",\n");
+ }
+
+ print_space(out, space_count);
+
+ if (name && !node->parent) {
+ ly_print(out, "\"%s:%s\":", node->module->name, node->name);
+ } else if (node->parent && (node->module != node->parent->module)) {
+ ly_print(out, "\"%s:%s\":", node->module->name, node->name);
+ } else {
+ ly_print(out, "\"%s\":", node->name);
+ }
+ }
+
+ if (lysc_node_child(node)) {
+ if (nodetype & PRINT_MASK) {
+ if (nodetype == LYS_LIST) {
+ ly_print(out, " [\n");
+ print_space(out, space_count + SPACE_COUNT);
+ ly_print(out, "{\n");
+ space_count += 2 * SPACE_COUNT;
+ } else {
+ ly_print(out, " {\n");
+ space_count += SPACE_COUNT;
+ }
+ *need_comma = 0;
+ }
+
+ if (nodetype == LYS_CHOICE) {
+ handle_json_default(out, node, space_count, need_comma);
+ node = node->next;
+ continue;
+ } else {
+ print_json_nodes(out, lysc_node_child(node), NULL, space_count, need_comma);
+ }
+
+ if (nodetype & PRINT_MASK) {
+ if (nodetype == LYS_LIST) {
+ space_count -= SPACE_COUNT;
+ ly_print(out, "\n");
+ print_space(out, space_count);
+ ly_print(out, "}\n");
+ space_count -= SPACE_COUNT;
+ print_space(out, space_count);
+ ly_print(out, "]");
+ } else {
+ space_count -= SPACE_COUNT;
+ ly_print(out, "\n");
+ print_space(out, space_count);
+ ly_print(out, "}");
+ }
+ *need_comma = 1;
+ }
+ } else {
+ handle_json_default(out, node, space_count, need_comma);
+
+ if (nodetype & PRINT_MASK) {
+ *need_comma = 1;
+ }
+ }
+
+ node = node->next;
+ }
+}
+
+/**
+ * @brief Initialize and print the top-level JSON skeleton structure.
+ *
+ * @param[in,out] out Libyang output handler to print to.
+ * @param[in] root Root compiled schema node of the module being processed.
+ * @param[in] name Name of the module, used to correctly prefix the top-level JSON objects according to RFC 7951 namespace rules.
+ */
+static void
+print_json_skeleton(struct ly_out *out, struct lysc_node *root, const char *name)
+{
+ struct lysc_node *node = root;
+ int spaces = SPACE_COUNT, need_comma = 0;
+
+ ly_print(out, "{\n");
+ print_json_nodes(out, node, name, spaces, &need_comma);
+ ly_print(out, "\n}\n");
+}
+
+int
+cmd_sample_exec(struct ly_ctx **ctx, struct yl_opt *yo, const char *posv)
+{
+ const struct lys_module *mod;
+ char *revision, *name;
+ uint8_t out_alloc = 0;
+
+ name = strdup(posv);
+ if (!name) {
+ YLMSG_E("Memory allocation failed.");
+ return 1;
+ }
+ revision = strchr(name, '@');
+ if (revision) {
+ revision[0] = '\0';
+ ++revision;
+ }
+
+ mod = revision ?
+ ly_ctx_get_module(*ctx, name, revision) :
+ ly_ctx_get_module_latest(*ctx, name);
+
+ if (!mod || !mod->compiled) {
+ YLMSG_E("Error: Module %s not loaded or has no compiled schema.", name);
+ free(name);
+ return 1;
+ }
+
+ if (!mod->compiled->data) {
+ free(name);
+ return 0;
+ }
+
+ if (!yo->out) {
+ if (ly_out_new_file(stdout, &yo->out)) {
+ YLMSG_E("Unable to allocate output handler.");
+ free(name);
+ return 1;
+ }
+ out_alloc = 1;
+ }
+
+ if (yo->data_out_format == LYD_XML) {
+ print_xml_skeleton(yo->out, mod->compiled->data);
+ } else {
+ print_json_skeleton(yo->out, mod->compiled->data, mod->name);
+ }
+
+ if (out_alloc) {
+ ly_out_free(yo->out, NULL, 0);
+ yo->out = NULL;
+ }
+
+ free(name);
+ return 0;
+}
diff --git a/tools/lint/ietf.c b/tools/lint/ietf.c
new file mode 100644
index 000000000..a44a7928d
--- /dev/null
+++ b/tools/lint/ietf.c
@@ -0,0 +1,1051 @@
+/**
+ * @file ietf.c
+ * @author Petr Hanzlik
+ * @brief 'ietf' command of the libyang's yanglint tool.
+ *
+ * Copyright (c) 2026 CESNET, z.s.p.o.
+ *
+ * This source code is licensed under BSD 3-Clause License (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://opensource.org/licenses/BSD-3-Clause
+ */
+
+#define _GNU_SOURCE
+
+#include "common.h"
+#include "ietf.h"
+#include "yl_opt.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#include "libyang.h"
+
+/* Global variable to track the return code of the IETF validation process. It is set to 1 if any IETF compliance issues are found during the checks. */
+static int rc = 0;
+
+#define IETF_RFC_VERSION "9907"
+
+#define IETF_WARN(file_name, rfc, section, out, ...) \
+ do { \
+ ly_print(out, "%s: warning: RFC %s: %s: ", file_name, rfc, section); \
+ ly_print(out, __VA_ARGS__); \
+ ly_print(out, "\n"); \
+ } while(0)
+
+#define IETF_ERR(file_name, rfc, section, out, ...) \
+ do { \
+ ly_print(out, "%s: error: RFC %s: %s: ", file_name, rfc, section); \
+ ly_print(out, __VA_ARGS__); \
+ ly_print(out, "\n"); \
+ } while(0)
+
+/**
+ * @brief Convert a file path to its base name.
+ *
+ * @param[in] filepath File path to convert.
+ * @return Base name of the file, or NULL if the input is invalid.
+ */
+const char *
+get_file_name(const char *filepath)
+{
+ const char *filename;
+
+ if (!filepath) {
+ return NULL;
+ }
+
+ filename = strrchr(filepath, '/');
+
+ if (filename) {
+ return filename + 1;
+ }
+ return filepath;
+}
+
+/**
+ * @brief Checks if a given text matches a specified regular expression pattern.
+ *
+ * @param[in] text Target string to search within. If NULL, the function returns 0.
+ * @param[in] pattern Pattern to match against.
+ * @return 1 if the pattern is successfully found within the text.
+ * @return 0 if the pattern is not found, if a compilation error occurs, or if invalid NULL arguments are passed.
+ */
+static int
+check_regex(const char *text, const char *pattern)
+{
+ if (!text || !pattern) {
+ return 0;
+ }
+
+ return ly_pattern_match(NULL, pattern, text, 0, NULL) == 0;
+}
+
+/**
+ * @brief Scans a description string for the presence of strict RFC 2119/8174 requirement keywords.
+ *
+ * @param[in] dsc Description text to evaluate.
+ * @return 1 if at least one RFC 2119 keyword is successfully found in the text.
+ * @return 0 if no keywords are found, if a compilation error occurs, or if NULL is passed.
+ */
+static int
+check_words(const char *dsc)
+{
+ const char *pattern;
+
+ /* Find a string that contains at least one of the strict RFC 2119 requirement words as a standalone word. */
+ pattern = ".*(^|[^a-zA-Z])(MUST|MUST NOT|REQUIRED|SHALL|SHALL NOT|SHOULD|SHOULD NOT|RECOMMENDED|NOT RECOMMENDED|MAY|OPTIONAL)([^a-zA-Z]|$).*";
+
+ if (!dsc) {
+ return 0;
+ }
+
+ return ly_pattern_match(NULL, pattern, dsc, 0, NULL) == 0;
+}
+
+/**
+ * @brief Helper to check a single XPath string for banned functions and axes.
+ *
+ * @param[in,out] out Libyang output handler where warnings and errors are printed.
+ * @param[in] xpath XPath string to check.
+ * @param[in] file_name Name of the file.
+ * @param[in] node_name Name of the node.
+ * @param[in] stmt Statement that contains the XPath string.
+ */
+static void
+check_xpath_string(struct ly_out *out, const char *xpath, const char *file_name, const char *node_name, const char *stmt)
+{
+ const char *func_pattern;
+ const char *axes_pattern;
+
+ if (!xpath) {
+ return;
+ }
+
+ /* W3C Regex for banned functions */
+ func_pattern = "((.|\\n)*[^a-zA-Z0-9_-])?(position|last|id|namespace-uri|name|lang|local-name)\\s*\\((.|\\n)*";
+ if (check_regex(xpath, func_pattern)) {
+ IETF_WARN(file_name, IETF_RFC_VERSION, "4.6.2", out, "XPath function in \"%s\" statement on \"%s\" should not be used", stmt, node_name);
+ rc = 1;
+ }
+
+ /* W3C Regex for banned axes */
+ axes_pattern = "((.|\\n)*[^a-zA-Z0-9_-])?(preceding-sibling|following-sibling|preceding|following)\\s*::(.|\\n)*";
+ if (check_regex(xpath, axes_pattern)) {
+ IETF_WARN(file_name, IETF_RFC_VERSION, "4.6.3", out, "XPath axis in \"%s\" statement on \"%s\" should not be used", stmt, node_name);
+ rc = 1;
+ }
+}
+
+/**
+ * @brief Helper to extract and check XPath strings from a node's 'when' and 'must' structures.
+ *
+ * @param[in,out] out Libyang output handler where warnings and errors are printed.
+ * @param[in] when Pointer to the structure containing the 'when' condition.
+ * @param[in] musts Array of structures containing 'must' conditions.
+ * @param[in] file_name Name of the file being checked.
+ * @param[in] node_name Name of the node being checked.
+ */
+static void
+check_node_xpath(struct ly_out *out, const struct lysp_when *when, const struct lysp_restr *musts, const char *file_name, const char *node_name)
+{
+ LY_ARRAY_COUNT_TYPE i;
+
+ /* Check 'when' condition */
+ if (when && when->cond) {
+ check_xpath_string(out, when->cond, file_name, node_name, "when");
+ }
+
+ /* Check all 'must' conditions */
+ if (musts) {
+ LY_ARRAY_FOR(musts, i) {
+ if (musts[i].arg.str) {
+ check_xpath_string(out, musts[i].arg.str, file_name, node_name, "must");
+ }
+ }
+ }
+}
+
+/**
+ * @brief Collapses all consecutive whitespace (newlines, tabs, spaces) into a single space.
+ * @param[in] input Input string.
+ * @return Normalized string.
+ */
+static char *
+normalize_whitespace(const char *input)
+{
+ size_t i, j = 0, len;
+ int in_whitespace = 0;
+
+ if (!input) {
+ return NULL;
+ }
+
+ len = strlen(input);
+ char *normalized = malloc(len + 1);
+
+ if (!normalized) {
+ return NULL;
+ }
+
+ for (i = 0; i < len; i++) {
+ if (isspace((unsigned char)input[i])) {
+ if (!in_whitespace) {
+ normalized[j++] = ' ';
+ in_whitespace = 1;
+ }
+ } else {
+ normalized[j++] = input[i];
+ in_whitespace = 0;
+ }
+ }
+ normalized[j] = '\0';
+ return normalized;
+}
+
+/**
+ * @brief Traverses the compiled schema tree to enforce IETF-specific validation rules.
+ *
+ * @param[in,out] out Libyang output handler where warnings and errors are printed.
+ * @param[in] data First top-level node of the compiled data tree to be inspected.
+ * @param[in] file_name Name of the file being evaluated (used to prefix the error messages).
+ */
+static void
+check_nodes_ietf(struct ly_out *out, const struct lysc_node *data, const char *file_name)
+{
+ const struct lysc_node *root, *elem;
+ char node_path[256];
+ int is_mandatory;
+
+ LY_LIST_FOR(data, root) {
+ is_mandatory = 0;
+
+ if ((root->flags & LYS_MAND_TRUE) && !(root->flags & LYS_CONFIG_R)) {
+ is_mandatory = 1;
+ }
+
+ if (is_mandatory) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.10", out, "top-level node \"%s\" must not be mandatory", root->name);
+ rc = 1;
+ }
+
+ LYSC_TREE_DFS_BEGIN(root, elem) {
+ lysc_path(elem, LYSC_PATH_LOG, node_path, sizeof(node_path));
+
+ if (elem->nodetype == LYS_CONTAINER) {
+ struct lysc_node_container *cont = (struct lysc_node_container *)elem;
+
+ if (cont->actions) {
+ check_nodes_ietf(out, (const struct lysc_node *)cont->actions, file_name);
+ }
+ if (cont->notifs) {
+ check_nodes_ietf(out, (const struct lysc_node *)cont->notifs, file_name);
+ }
+ } else if (elem->nodetype == LYS_LIST) {
+ struct lysc_node_list *list = (struct lysc_node_list *)elem;
+
+ if (list->actions) {
+ check_nodes_ietf(out, (const struct lysc_node *)list->actions, file_name);
+ }
+ if (list->notifs) {
+ check_nodes_ietf(out, (const struct lysc_node *)list->notifs, file_name);
+ }
+ }
+
+ switch (elem->nodetype) {
+ case LYS_CONTAINER:
+ case LYS_LIST:
+ case LYS_LEAF:
+ case LYS_ANYXML:
+ case LYS_ANYDATA:
+ case LYS_LEAFLIST:
+ case LYS_CHOICE:
+ case LYS_RPC:
+ if (!elem->dsc) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.14", out, "statement \"%s\" must have a \"description\" substatement", lys_nodetype2str(elem->nodetype));
+ rc = 1;
+ }
+ break;
+ case LYS_NOTIF:
+ if (!elem->dsc) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.14,4.16", out, "statement \"%s\" must have a \"description\" substatement", lys_nodetype2str(elem->nodetype));
+ rc = 1;
+ }
+ break;
+ default:
+ break;
+ }
+
+ LYSC_TREE_DFS_END(root, elem);
+ }
+ }
+}
+
+/**
+ * @brief Helper function to validate locally scoped typedefs and groupings inside parsed data nodes.
+ *
+ * @param[in,out] out Libyang output handler for printing errors.
+ * @param[in] file_name Name of the parsed file for error prefixes.
+ * @param[in] typedefs Array of parsed typedefs to check.
+ * @param[in] groupings Linked list of parsed groupings to check.
+ * @param[in] node_type_str String literal representing the node type (e.g., "container", "list").
+ * @param[in] node_name String name of the parent node to provide context.
+ * @param[in,out] found_2119 Pointer to an integer flag that tracks if RFC 2119 keywords are used.
+ */
+static void
+check_inner_defs(struct ly_out *out, const char *file_name, struct lysp_tpdf *typedefs, struct lysp_node_grp *groupings, const char *node_type_str, const char *node_name, int *found_2119)
+{
+ LY_ARRAY_COUNT_TYPE i;
+ struct lysp_node_grp *grp;
+
+ LY_ARRAY_FOR(typedefs, i) {
+ if (strlen(typedefs[i].name) > 64) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.3", out, "identifier %s exceeds 64 characters", typedefs[i].name);
+ rc = 1;
+ }
+
+ if (!typedefs[i].dsc) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.13,4.14", out,
+ "statement \"typedef\" inside %s \"%s\" must have a \"description\" substatement",
+ node_type_str, node_name);
+ rc = 1;
+ } else if (check_words(typedefs[i].dsc)) {
+ *found_2119 = 1;
+ }
+ }
+
+ LY_LIST_FOR(groupings, grp) {
+ if (strlen(grp->name) > 64) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.3", out, "identifier %s exceeds 64 characters", grp->name);
+ rc = 1;
+ }
+
+ if (!grp->dsc) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.14", out,
+ "statement \"grouping\" inside %s \"%s\" must have a \"description\" substatement",
+ node_type_str, node_name);
+ rc = 1;
+ } else if (check_words(grp->dsc)) {
+ *found_2119 = 1;
+ }
+ }
+}
+
+/**
+ * @brief Recursively traverses and validates the parsed schema tree for IETF compliance.
+ *
+ * @param[in,out] out Libyang output handler where warnings and errors are printed.
+ * @param[in] node Starting node of the parsed data tree (or sibling list) to inspect.
+ * @param[in] file_name Name of the file being evaluated, used to prefix the log messages.
+ * @param[in,out] found_2119 Pointer to an integer flag used to record whether any node in this tree uses RFC 2119 keywords.
+ */
+static void
+check_parsed_tree_ietf(struct ly_out *out, const struct lysp_node *node, const char *file_name, int *found_2119)
+{
+ const struct lysp_node *elem;
+ LY_ARRAY_COUNT_TYPE i;
+
+ LY_LIST_FOR(node, elem) {
+ if (strlen(elem->name) > 64) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.3", out, "identifier %s exceeds 64 characters", elem->name);
+ rc = 1;
+ }
+
+ if (elem->dsc && check_words(elem->dsc)) {
+ *found_2119 = 1;
+ }
+
+ if (elem->flags & LYS_CONFIG_W) {
+ IETF_WARN(file_name, IETF_RFC_VERSION, "4.4", out, "statement \"config\" is given with its default value \"true\"");
+ rc = 1;
+ }
+
+ if (elem->flags & LYS_STATUS_CURR) {
+ IETF_WARN(file_name, IETF_RFC_VERSION, "4.4", out, "statement \"status\" is given with its default value \"current\"");
+ rc = 1;
+ }
+
+ if (elem->flags & LYS_MAND_FALSE) {
+ IETF_WARN(file_name, IETF_RFC_VERSION, "4.4", out, "statement \"mandatory\" is given with its default value \"false\"");
+ rc = 1;
+ }
+
+ if (elem->flags & LYS_ORDBY_SYSTEM) {
+ IETF_WARN(file_name, IETF_RFC_VERSION, "4.4", out, "statement \"ordered-by\" is given with its default value \"system\"");
+ rc = 1;
+ }
+
+ switch (elem->nodetype) {
+ case LYS_CONTAINER: {
+ const struct lysp_node_container *cont = (struct lysp_node_container *)elem;
+
+ if ((elem->flags & LYS_CONFIG_R) && cont->musts) {
+ IETF_WARN(file_name, IETF_RFC_VERSION, "4.5", out, "constraints ('must' statements) on state data SHOULD be avoided (leaf \"%s\")", cont->name);
+ rc = 1;
+ }
+
+ check_node_xpath(out, cont->when, cont->musts, file_name, elem->name);
+ check_inner_defs(out, file_name, cont->typedefs, cont->groupings, "container", elem->name, found_2119);
+
+ if (cont->child) {
+ check_parsed_tree_ietf(out, cont->child, file_name, found_2119);
+ }
+ if (cont->actions) {
+ check_parsed_tree_ietf(out, (const struct lysp_node *)cont->actions, file_name, found_2119);
+ }
+ if (cont->notifs) {
+ check_parsed_tree_ietf(out, (const struct lysp_node *)cont->notifs, file_name, found_2119);
+ }
+ break;
+ }
+ case LYS_LIST: {
+ const struct lysp_node_list *list = (struct lysp_node_list *)elem;
+
+ check_node_xpath(out, list->when, list->musts, file_name, elem->name);
+ check_inner_defs(out, file_name, list->typedefs, list->groupings, "list", elem->name, found_2119);
+
+ if ((elem->flags & LYS_SET_MIN) && (list->min == 0)) {
+ IETF_WARN(file_name, IETF_RFC_VERSION, "4.4", out, "statement \"min-elements\" is given with its default value \"0\"");
+ rc = 1;
+ }
+
+ if ((elem->flags & LYS_SET_MAX) && (list->max == 0)) {
+ IETF_WARN(file_name, IETF_RFC_VERSION, "4.4", out, "statement \"max-elements\" is given with its default value \"unbounded\"");
+ rc = 1;
+ }
+
+ if ((elem->flags & LYS_CONFIG_R) && list->musts) {
+ IETF_WARN(file_name, IETF_RFC_VERSION, "4.5", out, "constraints ('must' statements) on state data should be avoided (leaf \"%s\")", list->name);
+ rc = 1;
+ }
+
+ if (list->child) {
+ check_parsed_tree_ietf(out, list->child, file_name, found_2119);
+ }
+ if (list->actions) {
+ check_parsed_tree_ietf(out, (const struct lysp_node *)list->actions, file_name, found_2119);
+ }
+ if (list->notifs) {
+ check_parsed_tree_ietf(out, (const struct lysp_node *)list->notifs, file_name, found_2119);
+ }
+ break;
+ }
+ case LYS_LEAFLIST: {
+ const struct lysp_node_leaflist *llist = (struct lysp_node_leaflist *)elem;
+
+ check_node_xpath(out, llist->when, llist->musts, file_name, elem->name);
+
+ if (!strcmp(llist->type.name, "empty")) {
+ IETF_WARN(file_name, IETF_RFC_VERSION, "4.11.5", out, "the type \"empty\" should not be used for a leaf-list (\"%s\")", llist->name);
+ rc = 1;
+ }
+
+ if ((elem->flags & LYS_CONFIG_R) && llist->musts) {
+ IETF_WARN(file_name, IETF_RFC_VERSION, "4.5", out, "constraints ('must' statements) on state data should be avoided (leaf \"%s\")", llist->name);
+ rc = 1;
+ }
+
+ if ((elem->flags & LYS_SET_MIN) && (llist->min == 0)) {
+ IETF_WARN(file_name, IETF_RFC_VERSION, "4.4", out, "statement \"min-elements\" is given with its default value \"0\"");
+ rc = 1;
+ }
+
+ if ((elem->flags & LYS_SET_MAX) && (llist->max == 0)) {
+ IETF_WARN(file_name, IETF_RFC_VERSION, "4.4", out, "statement \"max-elements\" is given with its default value \"unbounded\"");
+ rc = 1;
+ }
+
+ if (llist->type.enums) {
+ LY_ARRAY_FOR(llist->type.enums, i) {
+ if (strlen(llist->type.enums[i].name) > 64) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.3", out, "identifier %s exceeds 64 characters", llist->type.enums[i].name);
+ rc = 1;
+ }
+
+ if (!llist->type.enums[i].dsc) {
+ IETF_WARN(file_name, IETF_RFC_VERSION, "4.11.3", out, "statement \"enum\" should have a \"description\" substatement");
+ rc = 1;
+ }
+ }
+ }
+ if (llist->type.bits) {
+ LY_ARRAY_FOR(llist->type.bits, i) {
+ if (strlen(llist->type.bits[i].name) > 64) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.3", out, "identifier %s exceeds 64 characters", llist->type.bits[i].name);
+ rc = 1;
+ }
+
+ if (!llist->type.bits[i].dsc) {
+ IETF_WARN(file_name, IETF_RFC_VERSION, "4.11.3", out, "statement \"bit\" should have a \"description\" substatement");
+ rc = 1;
+ }
+ }
+ }
+ break;
+ }
+ case LYS_GROUPING: {
+ const struct lysp_node_grp *grp = (struct lysp_node_grp *)elem;
+
+ check_inner_defs(out, file_name, grp->typedefs, grp->groupings, "grouping", elem->name, found_2119);
+ if (grp->child) {
+ check_parsed_tree_ietf(out, grp->child, file_name, found_2119);
+ }
+ break;
+ }
+ case LYS_RPC:
+ case LYS_ACTION: {
+ const struct lysp_node_action *act = (struct lysp_node_action *)elem;
+ const char *type_str = (elem->nodetype == LYS_RPC) ? "rpc" : "action";
+
+ check_inner_defs(out, file_name, act->input.typedefs, act->input.groupings, type_str, elem->name, found_2119);
+
+ if (act->input.typedefs || act->input.groupings) {
+ check_inner_defs(out, file_name, act->input.typedefs, act->input.groupings, "input of", elem->name, found_2119);
+ }
+ if (act->input.child) {
+ check_parsed_tree_ietf(out, act->input.child, file_name, found_2119);
+ }
+
+ if (act->output.typedefs || act->output.groupings) {
+ check_inner_defs(out, file_name, act->output.typedefs, act->output.groupings, "output of", elem->name, found_2119);
+ }
+ if (act->output.child) {
+ check_parsed_tree_ietf(out, act->output.child, file_name, found_2119);
+ }
+ break;
+ }
+ case LYS_NOTIF: {
+ const struct lysp_node_notif *notif = (struct lysp_node_notif *)elem;
+
+ check_inner_defs(out, file_name, notif->typedefs, notif->groupings, "notification", elem->name, found_2119);
+ if (notif->child) {
+ check_parsed_tree_ietf(out, notif->child, file_name, found_2119);
+ }
+ break;
+ }
+ case LYS_CHOICE: {
+ const struct lysp_node_choice *choice = (struct lysp_node_choice *)elem;
+
+ check_node_xpath(out, choice->when, NULL, file_name, elem->name);
+
+ if (choice->child) {
+ check_parsed_tree_ietf(out, choice->child, file_name, found_2119);
+ }
+ break;
+ }
+ case LYS_CASE: {
+ const struct lysp_node_case *cas = (struct lysp_node_case *)elem;
+
+ check_node_xpath(out, cas->when, NULL, file_name, elem->name);
+
+ if (cas->child) {
+ check_parsed_tree_ietf(out, cas->child, file_name, found_2119);
+ }
+ break;
+ }
+ case LYS_LEAF: {
+ const struct lysp_node_leaf *leaf = (struct lysp_node_leaf *)elem;
+ const struct lysp_node_list *parent_list = (struct lysp_node_list *)elem->parent;
+ const struct lysp_node *parent_node = elem->parent;
+ LY_ARRAY_COUNT_TYPE leaf_iff_count;
+ LY_ARRAY_COUNT_TYPE list_iff_count;
+ int is_key = 0;
+
+ check_node_xpath(out, leaf->when, leaf->musts, file_name, elem->name);
+
+ if (elem->parent && (elem->parent->nodetype == LYS_LIST)) {
+ if (parent_list->key) {
+ if (!strcmp(parent_list->key, elem->name)) {
+ is_key = 1;
+ }
+ }
+ }
+
+ if ((elem->flags & LYS_CONFIG_R) && leaf->musts) {
+ IETF_WARN(file_name, IETF_RFC_VERSION, "4.5", out, "constraints ('must' statements) on state data should be avoided (leaf \"%s\")", leaf->name);
+ rc = 1;
+ }
+
+ if (is_key) {
+ if (leaf->when) {
+ IETF_WARN(file_name, IETF_RFC_VERSION, "4.5", out, "key leaf \"%s\" should not have a \"when\" statement", leaf->name);
+ rc = 1;
+ }
+
+ leaf_iff_count = elem->iffeatures ? LY_ARRAY_COUNT(elem->iffeatures) : 0;
+ list_iff_count = parent_node->iffeatures ? LY_ARRAY_COUNT(parent_node->iffeatures) : 0;
+
+ if (leaf_iff_count != list_iff_count) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.5", out, "key leaf \"%s\" must have the exact same \"if-feature\" statements as its parent list \"%s\"", leaf->name, parent_node->name);
+ rc = 1;
+ } else {
+ for (LY_ARRAY_COUNT_TYPE i = 0; i < list_iff_count; i++) {
+ if (strcmp(elem->iffeatures[i].str, parent_node->iffeatures[i].str)) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.5", out, "key leaf \"%s\" must have the exact same \"if-feature\" statements as its parent list \"%s\"", leaf->name, parent_node->name);
+ rc = 1;
+ break;
+ }
+ }
+ }
+
+ if (!strcmp(leaf->type.name, "empty")) {
+ IETF_WARN(file_name, IETF_RFC_VERSION, "4.11.5", out, "the type \"empty\" should not be used for a key leaf \"%s\"", leaf->name);
+ rc = 1;
+ }
+ }
+
+ if (!strcmp(leaf->type.name, "empty")) {
+ IETF_WARN(file_name, IETF_RFC_VERSION, "4.11.5", out, "in \"%s\", the \"boolean\" data type should be used instead of the \"empty\" data type", leaf->name);
+ }
+
+ if (leaf->type.enums) {
+ LY_ARRAY_FOR(leaf->type.enums, i) {
+ if (strlen(leaf->type.enums[i].name) > 64) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.3", out, "identifier %s exceeds 64 characters", leaf->type.enums[i].name);
+ rc = 1;
+ }
+
+ if (!leaf->type.enums[i].dsc) {
+ IETF_WARN(file_name, IETF_RFC_VERSION, "4.11.3", out, "statement \"enum\" should have a \"description\" substatement");
+ rc = 1;
+ }
+ }
+ }
+ if (leaf->type.bits) {
+ LY_ARRAY_FOR(leaf->type.bits, i) {
+ if (strlen(leaf->type.bits[i].name) > 64) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.3", out, "identifier %s exceeds 64 characters", leaf->type.bits[i].name);
+ rc = 1;
+ }
+
+ if (!leaf->type.bits[i].dsc) {
+ IETF_WARN(file_name, IETF_RFC_VERSION, "4.11.3", out, "statement \"bit\" should have a \"description\" substatement");
+ rc = 1;
+ }
+ }
+ }
+ break;
+ }
+ case LYS_USES: {
+ const struct lysp_node_uses *uses = (struct lysp_node_uses *)elem;
+ const struct lysp_node_augment *aug;
+
+ LY_LIST_FOR(uses->augments, aug) {
+ if (!aug->dsc) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.14", out, "statement \"augment\" inside uses \"%s\" must have a \"description\" substatement", uses->name);
+ rc = 1;
+ }
+
+ if (aug->child) {
+ check_parsed_tree_ietf(out, aug->child, file_name, found_2119);
+ }
+ }
+ break;
+ }
+ case LYS_ANYXML:
+ case LYS_ANYDATA: {
+ const struct lysp_node_anydata *anydata = (struct lysp_node_anydata *)elem;
+
+ check_node_xpath(out, anydata->when, anydata->musts, file_name, elem->name);
+ break;
+ }
+
+ default:
+ break;
+ }
+ }
+}
+
+/**
+ * @brief Validates IETF-mandated boilerplate and top-level descriptions for a parsed module or submodule.
+ *
+ * @param[in] name Name of the module or submodule.
+ * @param[in] filepath File path of the module (used to prefix error messages).
+ * @param[in] dsc Top-level `description` text of the module/submodule.
+ * @param[in] contact Top-level `contact` text.
+ * @param[in] org Top-level `organization` text.
+ * @param[in] revs Pointer to the parsed `revision` array.
+ * @param[in] type Integer flag indicating whether the file is a module (0) or submodule (non-zero).
+ * @param[in] exts Array of top-level parsed `extension` statements.
+ * @param[in] feats Array of top-level parsed `feature` statements.
+ * @param[in] idents Array of top-level parsed `identity` statements.
+ * @param[in] augments Linked list of top-level parsed `augment` statements.
+ * @param[in] tpdfs Array of top-level parsed `typedef` statements.
+ * @param[in] grps Linked list of top-level parsed `grouping` statements.
+ * @param[in,out] out Libyang output handler where warnings and errors are printed.
+ * @param[in,out] found_2119 Pointer to a flag tracking if RFC 2119 keywords were found anywhere in this specific file.
+ */
+static void
+check_parsed_boilerplate(const char *name, const char *filepath, const char *dsc, const char *contact, const char *org, struct lysp_revision *revs, int type, struct lysp_ext *exts,
+ struct lysp_feature *feats, struct lysp_ident *idents, struct lysp_node_augment *augments, struct lysp_tpdf *tpdfs, struct lysp_node_grp *grps, struct ly_out *out, int *found_2119)
+{
+ const char *file_name = filepath ? get_file_name(filepath) : name;
+ const char *type_name = type ? "submodule" : "module";
+ const char *rfc8174_target, *tlp, *rfc_start, *tlp_part2;
+ char *norm_dsc, *endptr;
+ struct lysp_node_grp *grp;
+ struct lysp_node_augment *aug;
+ LY_ARRAY_COUNT_TYPE i;
+ int rfc_valid = 0, tlp_valid = 0, is_simplified;
+ long year;
+
+ if (strncmp(name, "ietf-", 5) && strncmp(name, "iana-", 5)) {
+ IETF_WARN(file_name, IETF_RFC_VERSION, "4.1", out, "the module name should start with one of the strings \"ietf-\" or \"iana-\"");
+ rc = 1;
+ }
+
+ if (strlen(name) > 64) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.3", out, "identifier %s exceeds 64 characters", name);
+ rc = 1;
+ }
+
+ if (!contact) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.8", out, "statement \"%s\" must have a \"contact\" substatement", type_name);
+ rc = 1;
+ }
+
+ if (!org) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.8", out, "statement \"%s\" must have a \"organization\" substatement", type_name);
+ rc = 1;
+ }
+
+ if (!revs) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.8", out, "statement \"%s\" must have a \"revision\" substatement", type_name);
+ rc = 1;
+ } else {
+ LY_ARRAY_FOR(revs, i) {
+ if (!revs[i].ref) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.8", out, "statement \"revision\" %s must have a \"reference\" substatement", revs[i].date);
+ rc = 1;
+ }
+ if (!revs[i].dsc) {
+ IETF_WARN(file_name, IETF_RFC_VERSION, "4.8", out, "statement \"revision\" %s should have a \"description\" substatement summarizing changes", revs[i].date);
+ rc = 1;
+ }
+ }
+ }
+
+ if (!dsc) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.8", out, "statement \"%s\" must have a \"description\" substatement", type_name);
+ rc = 1;
+ return;
+ }
+
+ if (check_words(dsc)) {
+ *found_2119 = 1;
+ }
+
+ // Normalize whitespace for strict boilerplate matching
+ norm_dsc = normalize_whitespace(dsc);
+ if (!norm_dsc) {
+ ly_print(out, "%s: error: Memory allocation failed during description normalization.\n", file_name);
+ rc = 1;
+ return;
+ }
+
+ tlp = strstr(norm_dsc, "Copyright (c) ");
+ if (tlp) {
+ tlp += 14; // Skip past "Copyright (c) "
+
+ year = strtol(tlp, &endptr, 10);
+
+ if (tlp != endptr) {
+ tlp = endptr; // Move past the year
+ tlp_part2 =
+ " IETF Trust and the persons identified as authors of the code. All rights reserved. Redistribution and use in source and binary forms, with or without modification, is permitted pursuant to, and subject to the license terms contained in, the ";
+
+ if (strncmp(tlp, tlp_part2, strlen(tlp_part2)) == 0) {
+ tlp += strlen(tlp_part2);
+ is_simplified = 0;
+
+ if (strncmp(tlp, "Revised", 7) == 0) {
+ tlp += 7;
+ } else if (strncmp(tlp, "Simplified", 10) == 0) {
+ is_simplified = 1;
+ tlp += 10;
+ }
+
+ // If we found Revised or Simplified, check the rest of the string
+ if (strncmp(tlp, " BSD License set forth in Section 4.c of the IETF Trust's Legal Provisions Relating to IETF Documents (http", 107) == 0) {
+ tlp += 107;
+ if (*tlp == 's') {
+ tlp++; // allow https or http
+
+ }
+ if (strncmp(tlp, "://trustee.ietf.org/license-info).", 34) == 0) {
+ tlp_valid = 1;
+ // Throw error if they used "Simplified" in 2022 or later
+ if ((year >= 2022) && is_simplified) {
+ IETF_WARN(file_name, IETF_RFC_VERSION, "3.1", out, "The IETF Trust Copyright statement uses 'Simplified' instead of 'Revised' BSD License for a module >= 2022");
+ rc = 1;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (!tlp_valid) {
+ IETF_WARN(file_name, IETF_RFC_VERSION, "3.1", out, "The IETF Trust Copyright statement seems to be missing or is not correct");
+ rc = 1;
+ }
+
+ if (!strncmp(name, "ietf-", 5)) {
+ rfc_start = strstr(norm_dsc, "This version of this YANG module is part of RFC ");
+ if (!rfc_start) {
+ rfc_start = strstr(norm_dsc, "This version of this YANG submodule is part of RFC ");
+ }
+
+ if (rfc_start) {
+ // As long as the ending declaration exists anywhere after the start, it's valid
+ if (strstr(rfc_start, "; see the RFC itself for full legal notices.")) {
+ rfc_valid = 1;
+ }
+ }
+
+ if (!rfc_valid) {
+ IETF_WARN(file_name, IETF_RFC_VERSION, "Appendix B", out, "The text about which RFC this module is part of seems to be missing or is not correct");
+ rc = 1;
+ }
+ }
+
+ LY_ARRAY_FOR(exts, i) {
+ if (exts[i].flags & LYS_YINELEM_FALSE) {
+ IETF_WARN(file_name, IETF_RFC_VERSION, "4.4", out, "statement \"yin-element\" is given with its default value \"false\"");
+ rc = 1;
+ }
+
+ if (!exts[i].dsc) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.14", out, "statement \"%s\" must have a \"description\" substatement", "extension");
+ rc = 1;
+ } else if (check_words(exts[i].dsc)) {
+ *found_2119 = 1;
+ }
+ }
+
+ LY_ARRAY_FOR(feats, i) {
+ if (strlen(feats[i].name) > 64) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.3", out, "identifier %s exceeds 64 characters", feats[i].name);
+ rc = 1;
+ }
+
+ if (!feats[i].dsc) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.14", out, "statement \"%s\" must have a \"description\" substatement", "feature");
+ rc = 1;
+ } else if (check_words(feats[i].dsc)) {
+ *found_2119 = 1;
+ }
+ }
+
+ LY_ARRAY_FOR(idents, i) {
+ if (strlen(idents[i].name) > 64) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.3", out, "identifier %s exceeds 64 characters", idents[i].name);
+ rc = 1;
+ }
+
+ if (!idents[i].dsc) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.14", out, "statement \"%s\" must have a \"description\" substatement", "identity");
+ rc = 1;
+ } else if (check_words(idents[i].dsc)) {
+ *found_2119 = 1;
+ }
+ }
+
+ LY_LIST_FOR(augments, aug) {
+ if (!aug->dsc) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.14", out, "statement \"augment\" must have a \"description\" substatement");
+ rc = 1;
+ } else if (check_words(aug->dsc)) {
+ *found_2119 = 1;
+ }
+
+ if (aug->child) {
+ check_parsed_tree_ietf(out, aug->child, file_name, found_2119);
+ }
+ }
+
+ LY_ARRAY_FOR(tpdfs, i) {
+ if (strlen(tpdfs[i].name) > 64) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.3", out, "identifier %s exceeds 64 characters", tpdfs[i].name);
+ rc = 1;
+ }
+
+ if (!tpdfs[i].dsc) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.13,4.14", out, "statement \"typedef\" must have a \"description\" substatement");
+ rc = 1;
+ } else if (check_words(tpdfs[i].dsc)) {
+ *found_2119 = 1;
+ }
+ }
+
+ LY_LIST_FOR(grps, grp) {
+ if (strlen(grp->name) > 64) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.3", out, "identifier %s exceeds 64 characters", grp->name);
+ rc = 1;
+ }
+
+ if (!grp->dsc) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.14", out, "statement \"grouping\" must have a \"description\" substatement");
+ rc = 1;
+ } else if (check_words(grp->dsc)) {
+ *found_2119 = 1;
+ }
+ }
+
+ if (*found_2119) {
+ rfc8174_target =
+ "The key words 'MUST', 'MUST NOT', 'REQUIRED', 'SHALL', 'SHALL NOT', 'SHOULD', 'SHOULD NOT', 'RECOMMENDED', 'NOT RECOMMENDED', 'MAY', and 'OPTIONAL' in this document are to be interpreted as described in BCP 14 (RFC 2119) (RFC 8174) when, and only when, they appear in all capitals, as shown here.";
+
+ if (!strstr(norm_dsc, rfc8174_target)) {
+ ly_print(out, "%s: warning: %s\n", file_name, "the module seems to use RFC 2119 keywords, but the required text from RFC 8174 is not found or is not correct");
+ rc = 1;
+ }
+ }
+
+ // Free the normalized string to prevent memory leaks
+ free(norm_dsc);
+}
+
+/**
+ * @brief Orchestrator for IETF RFC 9907 compliance validation on a YANG module.
+
+ * @param[in] mod Libyang module structure containing both the parsed schema
+ * @param[in,out] out Libyang output handler where compliance warnings and errors are printed.
+ */
+static void
+check_ietf(const struct lys_module *mod, struct ly_out *out)
+{
+ const char *file_name = mod->filepath ? get_file_name(mod->filepath) : mod->name;
+ const char *sub_file_name;
+ char ns[256];
+ LY_ARRAY_COUNT_TYPE i;
+ int main_found_2119 = 0;
+ struct lysp_submodule *sub;
+
+ if (mod->parsed->data) {
+ check_parsed_tree_ietf(out, mod->parsed->data, file_name, &main_found_2119);
+ }
+ if (mod->parsed->rpcs) {
+ check_parsed_tree_ietf(out, (struct lysp_node *)mod->parsed->rpcs, file_name, &main_found_2119);
+ }
+ if (mod->parsed->notifs) {
+ check_parsed_tree_ietf(out, (struct lysp_node *)mod->parsed->notifs, file_name, &main_found_2119);
+ }
+
+ check_parsed_boilerplate(mod->name, mod->filepath, mod->dsc, mod->contact, mod->org, mod->parsed->revs, mod->parsed->is_submod, mod->parsed->extensions, mod->parsed->features, mod->parsed->identities, mod->parsed->augments, mod->parsed->typedefs,
+ mod->parsed->groupings, out, &main_found_2119);
+
+ snprintf(ns, sizeof(ns), "urn:ietf:params:xml:ns:yang:%s", mod->name);
+ if (strcmp(ns, mod->ns)) {
+ IETF_WARN(file_name, IETF_RFC_VERSION, "4.9", out, "namespace value should be \"%s\"", ns);
+ rc = 1;
+ }
+
+ if (mod->parsed->imports) {
+ LY_ARRAY_FOR(mod->parsed->imports, i) {
+ if (!strncmp(mod->parsed->imports[i].name, "ietf-", 5) || !strncmp(mod->parsed->imports[i].name, "iana-", 5)) {
+ if (!mod->parsed->imports[i].ref) {
+ IETF_WARN(file_name, IETF_RFC_VERSION, "4.7", out, "statement \"import\" for stable module \"%s\" should have a \"reference\" substatement", mod->parsed->imports[i].name);
+ rc = 1;
+ }
+ }
+ }
+ }
+
+ if (mod->parsed->deviations) {
+ if (!strncmp(mod->name, "ietf-", 5) || !strncmp(mod->name, "iana-", 5)) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.20", out, "the YANG \"deviation\" statement is not allowed to appear in IETF YANG modules");
+ rc = 1;
+ }
+ }
+
+ LY_ARRAY_FOR(mod->parsed->includes, i) {
+ sub = mod->parsed->includes[i].submodule;
+ sub_file_name = sub->filepath ? get_file_name(sub->filepath) : sub->name;
+ int sub_found_2119 = 0;
+
+ if (mod->parsed->revs && sub->revs) {
+ if (strcmp(mod->parsed->revs[0].date, sub->revs[0].date) < 0) {
+ IETF_ERR(file_name, IETF_RFC_VERSION, "4.7", out, "the module's revision %s is older than submodule %s's revision %s", mod->parsed->revs[0].date, sub->name, sub->revs[0].date);
+ rc = 1;
+ }
+ }
+
+ if (sub->data) {
+ check_parsed_tree_ietf(out, sub->data, sub_file_name, &sub_found_2119);
+ }
+ if (sub->rpcs) {
+ check_parsed_tree_ietf(out, (struct lysp_node *)sub->rpcs, sub_file_name, &sub_found_2119);
+ }
+ if (sub->notifs) {
+ check_parsed_tree_ietf(out, (struct lysp_node *)sub->notifs, sub_file_name, &sub_found_2119);
+ }
+
+ check_parsed_boilerplate(sub->name, sub->filepath, sub->dsc, sub->contact, sub->org, sub->revs, sub->is_submod, sub->extensions, sub->features, sub->identities, sub->augments, sub->typedefs, sub->groupings, out, &sub_found_2119);
+ }
+
+ if (mod->compiled->data) {
+ check_nodes_ietf(out, mod->compiled->data, file_name);
+ }
+
+ if (mod->compiled->rpcs) {
+ check_nodes_ietf(out, (const struct lysc_node *)mod->compiled->rpcs, file_name);
+ }
+
+ if (mod->compiled->notifs) {
+ check_nodes_ietf(out, (const struct lysc_node *)mod->compiled->notifs, file_name);
+ }
+}
+
+int
+yl_validate_ietf(struct ly_ctx **ctx, struct yl_opt *yo, const char *posv)
+{
+ const struct lys_module *mod;
+ char *revision, *name;
+ uint8_t out_alloc = 0;
+
+ name = strdup(posv);
+ if (!name) {
+ YLMSG_E("Memory allocation failed.");
+ return 1;
+ }
+ revision = strchr(name, '@');
+ if (revision) {
+ revision[0] = '\0';
+ ++revision;
+ }
+
+ mod = revision ?
+ ly_ctx_get_module(*ctx, name, revision) :
+ ly_ctx_get_module_latest(*ctx, name);
+
+ if (!mod || !mod->compiled || !mod->parsed) {
+ YLMSG_E("Error: Module %s not loaded or failed to compile.", name);
+ free(name);
+ return 1;
+ }
+
+ if (!yo->out) {
+ if (ly_out_new_file(stdout, &yo->out)) {
+ YLMSG_E("Unable to allocate output handler.");
+ free(name);
+ return 1;
+ }
+ out_alloc = 1;
+ }
+
+ check_ietf(mod, yo->out);
+
+ if (out_alloc) {
+ ly_out_free(yo->out, NULL, 0);
+ yo->out = NULL;
+ }
+
+ free(name);
+ return rc;
+}
diff --git a/tools/lint/ietf.h b/tools/lint/ietf.h
new file mode 100644
index 000000000..701a3e56c
--- /dev/null
+++ b/tools/lint/ietf.h
@@ -0,0 +1,32 @@
+/**
+ * @file ietf.h
+ * @author Petr Hanzlik
+ * @brief yanglint ietf header
+ *
+ * Copyright (c) 2026 CESNET, z.s.p.o.
+ *
+ * This source code is licensed under BSD 3-Clause License (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://opensource.org/licenses/BSD-3-Clause
+ */
+
+#ifndef IETF_H_
+#define IETF_H_
+
+#include "libyang.h"
+#include "yl_opt.h"
+
+/**
+ * @brief Validate a module against the IETF requirements.
+ *
+ * @param[in,out] ctx A double pointer to the libyang context containing the loaded modules.
+ * @param[in,out] yo A pointer to the yanglint options structure, which manages global
+ * @param[in] posv The positional command-line argument provided by the user, representing the target module name (e.g., "ietf-interfaces" or "ietf-interfaces@2014-05-08").
+ * @return 0 on successful execution of the validation pipeline (note: this returns 0 even if IETF compliance warnings/errors were printed to the output).
+ * @return 1 on fatal execution errors (e.g., module not found, module failed to compile, or memory allocation failure).
+ */
+int yl_validate_ietf(struct ly_ctx **ctx, struct yl_opt *yo, const char *posv);
+
+#endif /* IETF_H_ */
diff --git a/tools/lint/main_ni.c b/tools/lint/main_ni.c
index dc83c9a86..673693456 100644
--- a/tools/lint/main_ni.c
+++ b/tools/lint/main_ni.c
@@ -30,6 +30,7 @@
#include "cmd.h"
#include "common.h"
#include "compat.h"
+#include "ietf.h"
#include "out.h"
#include "tools/config.h"
#include "yl_opt.h"
@@ -52,6 +53,9 @@ help(int shortout)
" yanglint [-f { xml | json }] ... ...\n"
" Validates the YANG modeled data (s) according to the (s) optionally\n"
" printing them in the specified format.\n\n"
+ " yanglint -S { xml | json } ...\n"
+ " Generates and prints a sample data skeleton from the provided YANG (s)\n"
+ " in the specified format (XML or JSON).\n\n"
" yanglint -t (nc-)rpc/notif [-O ] ... \n"
" Validates the YANG/NETCONF RPC/notification according to the (s) using\n"
" with possible references to the operational datastore data.\n"
@@ -78,6 +82,10 @@ help(int shortout)
" yang, yin, tree, info and feature-param for schemas,\n"
" xml, json, and lyb for data.\n\n");
+ printf(" -S FORMAT, --sample-skeleton=FORMAT\n"
+ " Generate a sample data skeleton from the provided schema.\n"
+ " Supported formats: xml, json.\n\n");
+
printf(" -I FORMAT, --in-format=FORMAT\n"
" Load the data in one of the following formats:\n"
" xml, json, lyb\n"
@@ -218,6 +226,9 @@ help(int shortout)
printf(" -J, --json-null\n"
" Allow usage of JSON empty values ('null') within input data\n\n");
+ printf(" -T, --ietf\n"
+ " Enable stricter YANG model validation according to IETF rules.\n\n");
+
printf(" -G GROUPS, --debug=GROUPS\n"
#ifndef NDEBUG
" Enable printing of specific debugging message group\n"
@@ -410,6 +421,14 @@ process_files(int argc, char *argv[], int optind, LYD_FORMAT data_in_format, str
if (cmd_add_exec(&ctx, yo, filepath)) {
return -1;
}
+
+ if (yo->ietf_validation) {
+ struct lys_module *mod = (struct lys_module *)yo->schema_modules.objs[yo->schema_modules.count - 1];
+
+ if (yl_validate_ietf(&ctx, yo, mod->name)) {
+ return -1;
+ }
+ }
} else {
if (cmd_data_store(&ctx, yo, filepath)) {
return -1;
@@ -507,6 +526,8 @@ process_args(int argc, char *argv[], struct yl_opt *yo, struct ly_ctx **ctx)
{"extended-leafref", no_argument, NULL, 'X'},
{"json-null", no_argument, NULL, 'J'},
{"debug", required_argument, NULL, 'G'},
+ {"sample-skeleton", required_argument, NULL, 'S'},
+ {"ietf", no_argument, NULL, 'T'},
{NULL, 0, NULL, 0}
};
uint8_t data_type_set = 0;
@@ -518,7 +539,7 @@ process_args(int argc, char *argv[], struct yl_opt *yo, struct ly_ctx **ctx)
yo->line_length = 0;
opterr = 0;
- while ((opt = getopt_long(argc, argv, "hvVQf:I:p:DF:iP:qs:neE:At:d:lL:o:O:R:myY:XJx:G:", options, &opt_index)) != -1) {
+ while ((opt = getopt_long(argc, argv, "hvVQf:I:p:DF:iP:qs:neE:At:d:lL:o:O:R:myY:XJx:G:S:T", options, &opt_index)) != -1) {
switch (opt) {
case 'h': /* --help */
help(0);
@@ -711,6 +732,20 @@ process_args(int argc, char *argv[], struct yl_opt *yo, struct ly_ctx **ctx)
return -1;
}
break;
+
+ case 'S': /* --sample-skeleton */
+ if (yl_opt_update_data_out_format(optarg, yo)) {
+ YLMSG_E("Unknown out format %s.", optarg);
+ help(1);
+ return -1;
+ }
+ yo->sample_skeleton = 1;
+ break;
+
+ case 'T': /* --ietf */
+ yo->ietf_validation = 1;
+ break;
+
default:
YLMSG_E("Invalid option or missing argument: -%c.", optopt);
return -1;
@@ -818,6 +853,13 @@ main_ni(int argc, char *argv[])
goto cleanup;
}
}
+ } else if (yo.sample_skeleton) {
+ for (u = 0; u < yo.schema_modules.count; ++u) {
+ yo.last_one = (u + 1) == yo.schema_modules.count;
+ if ((ret = cmd_sample_exec(&ctx, &yo, ((struct lys_module *)yo.schema_modules.objs[u])->name))) {
+ goto cleanup;
+ }
+ }
}
/* do the data validation despite the schema was printed */
diff --git a/tools/lint/yl_opt.h b/tools/lint/yl_opt.h
index d66ae4de9..7e01909cb 100644
--- a/tools/lint/yl_opt.h
+++ b/tools/lint/yl_opt.h
@@ -137,6 +137,12 @@ struct yl_opt {
/* storage for --data-xpath */
struct ly_set data_xpath;
+ /* flag for --ietf option */
+ uint8_t ietf_validation;
+
+ /* flag for --sample*/
+ uint8_t sample_skeleton;
+
char **argv;
};