diff --git a/CHANGELOG.md b/CHANGELOG.md index c9486f2e..12811368 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html) ## [Unreleased] +### Fixed + +- **A GCV tested by an `` condition but defined nowhere was a `gcv-undefined` error.** The + engine reads an undefined GCV there as having no value (`not-available` is true, `equal` is false) and + nothing fails; packages test GCVs another package may define that way. An `available` / `not-available` + test is no longer reported, and any other op is the new info `gcv-undefined-condition`. A GCV that is + also substituted (`token-global-variable`, `~name~`) is still `gcv-undefined`. + ## [0.64.0] - 2026-11-02 ### Added diff --git a/docs/validation.md b/docs/validation.md index 5ff2436c..0e40e6ec 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -139,6 +139,7 @@ run time, so an undefined one fails the rule on its first event. | `script-required-attribute` | W | a DirXML-Script element lacks an attribute its grammar requires (Designer's DTD 4.7.5: `do-send-email-from-template` without `template-dn`, `token-map` without `src`/`dest`, `do-status` without `level`, …). The engine may still load the policy; Designer's importer drops it. Named by rule description | | `gcv-undefined` | E | a driver policy reads a GCV nothing in the driver's scope defines (names built from a local variable, `drv.x.$name$`, are skipped) | | `gcv-undefined-library` | W | a Library policy reads a GCV no driver in the set defines (it may be linked only by drivers that do) | +| `gcv-undefined-condition` | I | an `` condition compares a GCV nothing defines; the engine reads it as having no value, so the test always comes out the same (`available` / `not-available` tests are never reported) | ### `mapping-tables` — MappingTableCheck diff --git a/src/main/java/com/pointblue/dirxml/dev/validate/GcvCheck.java b/src/main/java/com/pointblue/dirxml/dev/validate/GcvCheck.java index 1c7db18b..ed442d63 100644 --- a/src/main/java/com/pointblue/dirxml/dev/validate/GcvCheck.java +++ b/src/main/java/com/pointblue/dirxml/dev/validate/GcvCheck.java @@ -25,7 +25,15 @@ * ({@code gcv-undefined-library}), since the policy may be linked only by drivers * that do define it. * - *

Codes: {@code gcv-undefined} (E), {@code gcv-undefined-library} (W). + *

An {@code } condition is different: the engine reads an + * undefined GCV there as having no value ({@code not-available} is true, {@code equal} + * is false) and nothing fails. Packages test GCVs another package may define that way. + * A condition with {@code op="available"} or {@code "not-available"} asks exactly that + * and is never reported; any other op on an undefined GCV is an {@code INFO} + * ({@code gcv-undefined-condition}), since its outcome is then fixed. + * + *

Codes: {@code gcv-undefined} (E), {@code gcv-undefined-library} (W), + * {@code gcv-undefined-condition} (I). */ public final class GcvCheck implements Check { @@ -47,6 +55,7 @@ public void run(DriverSet ds, Report r) { r.add(Finding.error("gcv-undefined", p.path(), "references undefined GCV(s): " + String.join(", ", missing))); } + conditionFinding(r, p, defined); } } for (Policy p : ds.library.policies) { @@ -55,13 +64,36 @@ public void run(DriverSet ds, Report r) { r.add(Finding.warning("gcv-undefined-library", p.path(), "references GCV(s) no driver in this set defines: " + String.join(", ", missing))); } + conditionFinding(r, p, union); + } + } + + private static void conditionFinding(Report r, Policy p, Set defined) { + if (p.content == null) { + return; + } + Set names = new LinkedHashSet<>(); + conditions(p.content, names); + names.removeAll(GcvReferences.referenced(p.content)); + List missing = undefined(names, defined); + if (!missing.isEmpty()) { + r.add(Finding.info("gcv-undefined-condition", p.path(), + "tests GCV(s) nothing defines: " + String.join(", ", missing) + + " — the engine reads them as having no value, so those tests always come out the same")); } } - /** {@code } conditions read a GCV too; they have no token form, so the reference scan misses them. */ + /** + * {@code } conditions that compare a GCV's value; they have + * no token form, so the reference scan misses them. {@code available} and + * {@code not-available} only ask whether it is defined, so they're left out. + */ private static void conditions(org.w3c.dom.Element e, Set names) { if ("if-global-variable".equals(e.getNodeName()) && !e.getAttribute("name").isEmpty()) { - names.add(e.getAttribute("name")); + String op = e.getAttribute("op"); + if (!"available".equals(op) && !"not-available".equals(op)) { + names.add(e.getAttribute("name")); + } } for (org.w3c.dom.Node c = e.getFirstChild(); c != null; c = c.getNextSibling()) { if (c instanceof org.w3c.dom.Element) { @@ -71,12 +103,14 @@ private static void conditions(org.w3c.dom.Element e, Set names) { } private static List missing(Policy p, Set defined) { - List out = new ArrayList<>(); if (p.content == null) { - return out; + return new ArrayList<>(); } - Set names = new java.util.LinkedHashSet<>(GcvReferences.referenced(p.content)); - conditions(p.content, names); + return undefined(GcvReferences.referenced(p.content), defined); + } + + private static List undefined(java.util.Collection names, Set defined) { + List out = new ArrayList<>(); for (String name : names) { // A name built from a local variable (drv.x.$entName$) is resolved at run // time; nothing static to check. diff --git a/src/test/java/com/pointblue/dirxml/dev/validate/ValidatorTest.java b/src/test/java/com/pointblue/dirxml/dev/validate/ValidatorTest.java index 3662ec9b..6cfb2802 100644 --- a/src/test/java/com/pointblue/dirxml/dev/validate/ValidatorTest.java +++ b/src/test/java/com/pointblue/dirxml/dev/validate/ValidatorTest.java @@ -284,6 +284,36 @@ public void gcvUndefinedToken() { assertTrue(f.get(0).message, f.get(0).message.endsWith("drv.missing")); } + /** The engine reads an undefined GCV in a condition as having no value; nothing fails. */ + @Test + public void gcvUndefinedInAConditionIsInfo() { + DriverSet ds = clean(); + addSub(ds, "gcv", "r" + + "" + + "false" + + "" + + "true" + + ""); + Report r = owned().validate(ds); + assertTrue(r.text(), r.ok()); + List f = r.withCode("gcv-undefined-condition"); + assertEquals(r.text(), 1, f.size()); + assertTrue(f.get(0).message, f.get(0).message.startsWith("tests GCV(s) nothing defines: drv.optional ")); + } + + /** A GCV both tested and substituted is still an error for the substitution. */ + @Test + public void gcvUndefinedTokenAndConditionIsOneError() { + DriverSet ds = clean(); + addSub(ds, "gcv", "r" + + "true" + + "" + + ""); + Report r = owned().validate(ds); + assertEquals(r.text(), 1, r.withCode("gcv-undefined").size()); + assertTrue(r.text(), r.withCode("gcv-undefined-condition").isEmpty()); + } + /** #134: a driver that runs on another server of the set has its GCVs in that server's never-sync copy. */ @Test public void gcvDefinedOnlyOnAnotherServerIsDefined() {