62c9b673752ca4d284872ed3d0ddb1697ddec945
[yangtools.git] / model / yang-model-util / src / main / java / org / opendaylight / yangtools / yang / model / util / SchemaInferenceStack.java
1 /*
2  * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.yangtools.yang.model.util;
9
10 import static com.google.common.base.Preconditions.checkArgument;
11 import static com.google.common.base.Preconditions.checkState;
12 import static com.google.common.base.Verify.verify;
13 import static com.google.common.base.Verify.verifyNotNull;
14 import static java.util.Objects.requireNonNull;
15
16 import com.google.common.annotations.Beta;
17 import com.google.common.annotations.VisibleForTesting;
18 import com.google.common.base.MoreObjects;
19 import com.google.common.base.VerifyException;
20 import com.google.common.collect.ImmutableList;
21 import com.google.common.collect.Iterators;
22 import java.util.ArrayDeque;
23 import java.util.Deque;
24 import java.util.Iterator;
25 import java.util.List;
26 import java.util.NoSuchElementException;
27 import java.util.Optional;
28 import org.eclipse.jdt.annotation.NonNull;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.opendaylight.yangtools.concepts.Mutable;
31 import org.opendaylight.yangtools.rfc8040.model.api.YangDataEffectiveStatement;
32 import org.opendaylight.yangtools.yang.common.AbstractQName;
33 import org.opendaylight.yangtools.yang.common.QName;
34 import org.opendaylight.yangtools.yang.common.QNameModule;
35 import org.opendaylight.yangtools.yang.common.UnresolvedQName.Unqualified;
36 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
37 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContextProvider;
38 import org.opendaylight.yangtools.yang.model.api.EffectiveStatementInference;
39 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
40 import org.opendaylight.yangtools.yang.model.api.PathExpression;
41 import org.opendaylight.yangtools.yang.model.api.PathExpression.DerefSteps;
42 import org.opendaylight.yangtools.yang.model.api.PathExpression.LocationPathSteps;
43 import org.opendaylight.yangtools.yang.model.api.PathExpression.Steps;
44 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
45 import org.opendaylight.yangtools.yang.model.api.SchemaTreeInference;
46 import org.opendaylight.yangtools.yang.model.api.TypeAware;
47 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
48 import org.opendaylight.yangtools.yang.model.api.TypedDataSchemaNode;
49 import org.opendaylight.yangtools.yang.model.api.meta.EffectiveStatement;
50 import org.opendaylight.yangtools.yang.model.api.stmt.CaseEffectiveStatement;
51 import org.opendaylight.yangtools.yang.model.api.stmt.ChoiceEffectiveStatement;
52 import org.opendaylight.yangtools.yang.model.api.stmt.DataTreeAwareEffectiveStatement;
53 import org.opendaylight.yangtools.yang.model.api.stmt.DataTreeEffectiveStatement;
54 import org.opendaylight.yangtools.yang.model.api.stmt.GroupingEffectiveStatement;
55 import org.opendaylight.yangtools.yang.model.api.stmt.ModuleEffectiveStatement;
56 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier;
57 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute;
58 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaTreeAwareEffectiveStatement;
59 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaTreeEffectiveStatement;
60 import org.opendaylight.yangtools.yang.model.api.stmt.TypedefEffectiveStatement;
61 import org.opendaylight.yangtools.yang.model.api.stmt.TypedefNamespace;
62 import org.opendaylight.yangtools.yang.model.api.type.InstanceIdentifierTypeDefinition;
63 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
64 import org.opendaylight.yangtools.yang.model.spi.AbstractEffectiveStatementInference;
65 import org.opendaylight.yangtools.yang.model.spi.DefaultSchemaTreeInference;
66 import org.opendaylight.yangtools.yang.xpath.api.YangLocationPath;
67 import org.opendaylight.yangtools.yang.xpath.api.YangLocationPath.AxisStep;
68 import org.opendaylight.yangtools.yang.xpath.api.YangLocationPath.QNameStep;
69 import org.opendaylight.yangtools.yang.xpath.api.YangLocationPath.Step;
70 import org.opendaylight.yangtools.yang.xpath.api.YangXPathAxis;
71 import org.slf4j.LoggerFactory;
72
73 /**
74  * A state tracking utility for walking {@link EffectiveModelContext}'s contents along schema/grouping namespaces. This
75  * is conceptually a stack, tracking {@link EffectiveStatement}s encountered along traversal.
76  *
77  * <p>
78  * This is meant to be a replacement concept for the use of {@link SchemaPath} in various places, notably
79  * in {@link SchemaContextUtil} methods.
80  *
81  * <p>
82  * This class is designed for single-threaded uses and does not make any guarantees around concurrent access.
83  */
84 @Beta
85 public final class SchemaInferenceStack implements Mutable, EffectiveModelContextProvider, LeafrefResolver {
86     /**
87      * Semantic binding of {@link EffectiveStatementInference} produced by {@link SchemaInferenceStack}. Sequence of
88      * {@link #statementPath()} is implementation-specific.
89      */
90     @Beta
91     public static final class Inference extends AbstractEffectiveStatementInference<EffectiveStatement<?, ?>> {
92         private final ArrayDeque<EffectiveStatement<?, ?>> deque;
93         private final ModuleEffectiveStatement currentModule;
94         private final int groupingDepth;
95         private final boolean clean;
96
97         Inference(final @NonNull EffectiveModelContext modelContext, final ArrayDeque<EffectiveStatement<?, ?>> deque,
98                 final ModuleEffectiveStatement currentModule, final int groupingDepth, final boolean clean) {
99             super(modelContext);
100             this.deque = requireNonNull(deque);
101             this.currentModule = currentModule;
102             this.groupingDepth = groupingDepth;
103             this.clean = clean;
104         }
105
106         /**
107          * Create a new stack backed by an effective model and set up to point and specified data tree node.
108          *
109          * @param effectiveModel EffectiveModelContext to which this stack is attached
110          * @param qnames Data tree path qnames
111          * @return A new stack
112          * @throws NullPointerException if any argument is null or path contains a null element
113          * @throws IllegalArgumentException if a path element cannot be found
114          */
115         public static @NonNull Inference ofDataTreePath(final EffectiveModelContext effectiveModel,
116                 final QName... qnames) {
117             return SchemaInferenceStack.ofDataTreePath(effectiveModel, qnames).toInference();
118         }
119
120         @Override
121         public List<EffectiveStatement<?, ?>> statementPath() {
122             return ImmutableList.copyOf(deque.descendingIterator());
123         }
124
125         /**
126          * Convert this inference into a {@link SchemaInferenceStack}.
127          *
128          * @return A new stack
129          */
130         public @NonNull SchemaInferenceStack toSchemaInferenceStack() {
131             return new SchemaInferenceStack(getEffectiveModelContext(), deque, currentModule, groupingDepth, clean);
132         }
133     }
134
135     private static final String VERIFY_DEFAULT_SCHEMA_TREE_INFERENCE_PROP =
136         "org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.verifyDefaultSchemaTreeInference";
137     private static final boolean VERIFY_DEFAULT_SCHEMA_TREE_INFERENCE =
138         Boolean.getBoolean(VERIFY_DEFAULT_SCHEMA_TREE_INFERENCE_PROP);
139
140     static {
141         if (VERIFY_DEFAULT_SCHEMA_TREE_INFERENCE) {
142             LoggerFactory.getLogger(SchemaInferenceStack.class)
143                 .info("SchemaTreeStack.ofInference(DefaultSchemaTreeInference) argument is being verified");
144         }
145     }
146
147     private final @NonNull EffectiveModelContext effectiveModel;
148     private final ArrayDeque<EffectiveStatement<?, ?>> deque;
149
150     private @Nullable ModuleEffectiveStatement currentModule;
151     private int groupingDepth;
152
153     // True if there were only steps along grouping and schema tree, hence it is consistent with SchemaNodeIdentifier
154     // False if we have evidence of a data tree lookup succeeding
155     private boolean clean;
156
157     private SchemaInferenceStack(final EffectiveModelContext effectiveModel, final int expectedSize) {
158         deque = new ArrayDeque<>(expectedSize);
159         this.effectiveModel = requireNonNull(effectiveModel);
160         clean = true;
161     }
162
163     private SchemaInferenceStack(final SchemaInferenceStack source) {
164         deque = source.deque.clone();
165         effectiveModel = source.effectiveModel;
166         currentModule = source.currentModule;
167         groupingDepth = source.groupingDepth;
168         clean = source.clean;
169     }
170
171     private SchemaInferenceStack(final EffectiveModelContext effectiveModel,
172             final ArrayDeque<EffectiveStatement<?, ?>> deque, final ModuleEffectiveStatement currentModule,
173             final int groupingDepth, final boolean clean) {
174         this.effectiveModel = requireNonNull(effectiveModel);
175         this.deque = deque.clone();
176         this.currentModule = currentModule;
177         this.groupingDepth = groupingDepth;
178         this.clean = clean;
179     }
180
181     private SchemaInferenceStack(final EffectiveModelContext effectiveModel) {
182         this.effectiveModel = requireNonNull(effectiveModel);
183         deque = new ArrayDeque<>();
184         clean = true;
185     }
186
187     /**
188      * Create a new empty stack backed by an effective model.
189      *
190      * @param effectiveModel EffectiveModelContext to which this stack is attached
191      * @return A new stack
192      * @throws NullPointerException if {@code effectiveModel} is null
193      */
194     public static @NonNull SchemaInferenceStack of(final EffectiveModelContext effectiveModel) {
195         return new SchemaInferenceStack(effectiveModel);
196     }
197
198     /**
199      * Create a new stack backed by an effective model, pointing to specified schema node identified by
200      * {@link Absolute}.
201      *
202      * @param effectiveModel EffectiveModelContext to which this stack is attached
203      * @return A new stack
204      * @throws NullPointerException if {@code effectiveModel} is null
205      * @throws IllegalArgumentException if {@code path} cannot be resolved in the effective model
206      */
207     public static @NonNull SchemaInferenceStack of(final EffectiveModelContext effectiveModel, final Absolute path) {
208         final SchemaInferenceStack ret = new SchemaInferenceStack(effectiveModel);
209         path.getNodeIdentifiers().forEach(ret::enterSchemaTree);
210         return ret;
211     }
212
213     /**
214      * Create a new stack from an {@link EffectiveStatementInference}.
215      *
216      * @param inference Inference to use for initialization
217      * @return A new stack
218      * @throws NullPointerException if {@code inference} is null
219      * @throws IllegalArgumentException if {@code inference} implementation is not supported
220      */
221     public static @NonNull SchemaInferenceStack ofInference(final EffectiveStatementInference inference) {
222         if (inference.statementPath().isEmpty()) {
223             return new SchemaInferenceStack(inference.getEffectiveModelContext());
224         } else if (inference instanceof SchemaTreeInference) {
225             return ofInference((SchemaTreeInference) inference);
226         } else if (inference instanceof Inference) {
227             return ((Inference) inference).toSchemaInferenceStack();
228         } else {
229             throw new IllegalArgumentException("Unsupported Inference " + inference);
230         }
231     }
232
233     /**
234      * Create a new stack from an {@link SchemaTreeInference}.
235      *
236      * @param inference SchemaTreeInference to use for initialization
237      * @return A new stack
238      * @throws NullPointerException if {@code inference} is null
239      * @throws IllegalArgumentException if {@code inference} cannot be resolved to a valid stack
240      */
241     public static @NonNull SchemaInferenceStack ofInference(final SchemaTreeInference inference) {
242         return inference instanceof DefaultSchemaTreeInference ? ofInference((DefaultSchemaTreeInference) inference)
243             : of(inference.getEffectiveModelContext(), inference.toSchemaNodeIdentifier());
244     }
245
246     /**
247      * Create a new stack from an {@link DefaultSchemaTreeInference}. The argument is nominally trusted to be an
248      * accurate representation of the schema tree.
249      *
250      * <p>
251      * Run-time verification of {@code inference} can be enabled by setting the
252      * {@value #VERIFY_DEFAULT_SCHEMA_TREE_INFERENCE_PROP} system property to {@code true}.
253      *
254      * @param inference DefaultSchemaTreeInference to use for initialization
255      * @return A new stack
256      * @throws NullPointerException if {@code inference} is null
257      * @throws IllegalArgumentException if {@code inference} refers to a missing module or when verification is enabled
258      *                                  and it does not match its context's scheam tree
259      */
260     public static @NonNull SchemaInferenceStack ofInference(final DefaultSchemaTreeInference inference) {
261         return VERIFY_DEFAULT_SCHEMA_TREE_INFERENCE ? ofUntrusted(inference) : ofTrusted(inference);
262     }
263
264     private static @NonNull SchemaInferenceStack ofTrusted(final DefaultSchemaTreeInference inference) {
265         final var path = inference.statementPath();
266         final var ret = new SchemaInferenceStack(inference.getEffectiveModelContext(), path.size());
267         ret.currentModule = ret.getModule(path.get(0).argument());
268         path.forEach(ret.deque::push);
269         return ret;
270     }
271
272     @VisibleForTesting
273     static @NonNull SchemaInferenceStack ofUntrusted(final DefaultSchemaTreeInference inference) {
274         final var ret = of(inference.getEffectiveModelContext(), inference.toSchemaNodeIdentifier());
275         if (!Iterators.elementsEqual(ret.deque.descendingIterator(), inference.statementPath().iterator())) {
276             throw new IllegalArgumentException("Provided " + inference + " is not consistent with resolved path "
277                 + ret.toSchemaTreeInference());
278         }
279         return ret;
280     }
281
282     /**
283      * Create a new stack backed by an effective model and set up to point and specified data tree node.
284      *
285      * @param effectiveModel EffectiveModelContext to which this stack is attached
286      * @return A new stack
287      * @throws NullPointerException if any argument is null or path contains a null element
288      * @throws IllegalArgumentException if a path element cannot be found
289      */
290     public static @NonNull SchemaInferenceStack ofDataTreePath(final EffectiveModelContext effectiveModel,
291             final QName... path) {
292         final SchemaInferenceStack ret = new SchemaInferenceStack(effectiveModel);
293         for (QName qname : path) {
294             ret.enterDataTree(qname);
295         }
296         return ret;
297     }
298
299     /**
300      * Create a new stack backed by an effective model, pointing to specified schema node identified by an absolute
301      * {@link SchemaPath} and its {@link SchemaPath#getPathFromRoot()} interpreted as a schema node identifier.
302      *
303      * @param effectiveModel EffectiveModelContext to which this stack is attached
304      * @return A new stack
305      * @throws NullPointerException {@code effectiveModel} is null
306      * @throws IllegalArgumentException if {@code path} cannot be resolved in the effective model or if it is not an
307      *                                  absolute path.
308      */
309     @Deprecated
310     public static @NonNull SchemaInferenceStack ofInstantiatedPath(final EffectiveModelContext effectiveModel,
311             final SchemaPath path) {
312         checkArgument(path.isAbsolute(), "Cannot operate on relative path %s", path);
313         final SchemaInferenceStack ret = new SchemaInferenceStack(effectiveModel);
314         path.getPathFromRoot().forEach(ret::enterSchemaTree);
315         return ret;
316     }
317
318     /**
319      * Create a new stack backed by an effective model, pointing to specified schema node identified by an absolute
320      * {@link SchemaPath} and its {@link SchemaPath#getPathFromRoot()}, interpreted as a series of steps along primarily
321      * schema tree, with grouping namespace being the alternative lookup.
322      *
323      * @param effectiveModel EffectiveModelContext to which this stack is attached
324      * @return A new stack
325      * @throws NullPointerException {@code effectiveModel} is null
326      * @throws IllegalArgumentException if {@code path} cannot be resolved in the effective model or if it is not an
327      *                                  absolute path.
328      */
329     @Deprecated
330     public static @NonNull SchemaInferenceStack ofSchemaPath(final EffectiveModelContext effectiveModel,
331             final SchemaPath path) {
332         checkArgument(path.isAbsolute(), "Cannot operate on relative path %s", path);
333         final SchemaInferenceStack ret = new SchemaInferenceStack(effectiveModel);
334
335         for (QName step : path.getPathFromRoot()) {
336             try {
337                 ret.enterSchemaTree(step);
338             } catch (IllegalArgumentException schemaEx) {
339                 try {
340                     ret.enterGrouping(step);
341                 } catch (IllegalArgumentException ex) {
342                     ex.addSuppressed(schemaEx);
343                     throw ex;
344                 }
345             }
346         }
347
348         return ret;
349     }
350
351     @Override
352     public EffectiveModelContext getEffectiveModelContext() {
353         return effectiveModel;
354     }
355
356     /**
357      * Create a deep copy of this object.
358      *
359      * @return An isolated copy of this object
360      */
361     public @NonNull SchemaInferenceStack copy() {
362         return new SchemaInferenceStack(this);
363     }
364
365     /**
366      * Check if this stack is empty.
367      *
368      * @return True if this stack has not entered any node.
369      */
370     public boolean isEmpty() {
371         return deque.isEmpty();
372     }
373
374     /**
375      * Return the statement at the top of the stack.
376      *
377      * @return Top statement
378      * @throws IllegalStateException if the stack is empty
379      */
380     public @NonNull EffectiveStatement<?, ?> currentStatement() {
381         return checkNonNullState(deque.peekFirst());
382     }
383
384     /**
385      * Return current module the stack has entered.
386      *
387      * @return Current module
388      * @throws IllegalStateException if the stack is empty
389      */
390     public @NonNull ModuleEffectiveStatement currentModule() {
391         return checkNonNullState(currentModule);
392     }
393
394     /**
395      * Check if the stack is in instantiated context. This indicates the stack is non-empty and there are only schema
396      * tree statements in the stack.
397      *
398      * @return False if the stack is empty or contains a statement which is not a {@link SchemaTreeEffectiveStatement},
399      *         true otherwise.
400      */
401     public boolean inInstantiatedContext() {
402         return groupingDepth == 0 && !deque.isEmpty()
403             && deque.stream().allMatch(SchemaTreeEffectiveStatement.class::isInstance);
404     }
405
406     /**
407      * Check if the stack is in a {@code grouping} context.
408      *
409      * @return False if the stack contains a grouping.
410      */
411     public boolean inGrouping() {
412         return groupingDepth != 0;
413     }
414
415     /**
416      * Reset this stack to empty state.
417      */
418     public void clear() {
419         deque.clear();
420         currentModule = null;
421         groupingDepth = 0;
422         clean = true;
423     }
424
425     /**
426      * Lookup a {@code choice} by its node identifier and push it to the stack. This step is very similar to
427      * {@link #enterSchemaTree(QName)}, except it handles the use case where traversal ignores actual {@code case}
428      * intermediate schema tree children.
429      *
430      * @param nodeIdentifier Node identifier of the choice to enter
431      * @return Resolved choice
432      * @throws NullPointerException if {@code nodeIdentifier} is null
433      * @throws IllegalArgumentException if the corresponding choice cannot be found
434      */
435     public @NonNull ChoiceEffectiveStatement enterChoice(final QName nodeIdentifier) {
436         final QName nodeId = requireNonNull(nodeIdentifier);
437         final EffectiveStatement<?, ?> parent = deque.peek();
438         if (parent instanceof ChoiceEffectiveStatement) {
439             return enterChoice((ChoiceEffectiveStatement) parent, nodeId);
440         }
441
442         // Fall back to schema tree lookup. Note if it results in non-choice, we rewind before reporting an error
443         final SchemaTreeEffectiveStatement<?> result = enterSchemaTree(nodeId);
444         if (result instanceof ChoiceEffectiveStatement) {
445             return (ChoiceEffectiveStatement) result;
446         }
447         exit();
448
449         if (parent != null) {
450             throw notPresent(parent, "Choice", nodeId);
451         }
452         throw new IllegalArgumentException("Choice " + nodeId + " not present");
453     }
454
455     // choice -> choice transition, we have to deal with intermediate case nodes
456     private @NonNull ChoiceEffectiveStatement enterChoice(final @NonNull ChoiceEffectiveStatement parent,
457             final QName nodeIdentifier) {
458         for (EffectiveStatement<?, ?> stmt : parent.effectiveSubstatements()) {
459             if (stmt instanceof CaseEffectiveStatement) {
460                 final Optional<ChoiceEffectiveStatement> optMatch = ((CaseEffectiveStatement) stmt)
461                     .findSchemaTreeNode(nodeIdentifier)
462                     .filter(ChoiceEffectiveStatement.class::isInstance)
463                     .map(ChoiceEffectiveStatement.class::cast);
464                 if (optMatch.isPresent()) {
465                     final SchemaTreeEffectiveStatement<?> match = optMatch.orElseThrow();
466                     deque.push(match);
467                     clean = false;
468                     return (ChoiceEffectiveStatement) match;
469                 }
470             }
471         }
472         throw notPresent(parent, "Choice", nodeIdentifier);
473     }
474
475     /**
476      * Lookup a {@code grouping} by its node identifier and push it to the stack.
477      *
478      * @param nodeIdentifier Node identifier of the grouping to enter
479      * @return Resolved grouping
480      * @throws NullPointerException if {@code nodeIdentifier} is null
481      * @throws IllegalArgumentException if the corresponding grouping cannot be found
482      */
483     public @NonNull GroupingEffectiveStatement enterGrouping(final QName nodeIdentifier) {
484         return pushGrouping(requireNonNull(nodeIdentifier));
485     }
486
487     /**
488      * Lookup a {@code schema tree} child by its node identifier and push it to the stack.
489      *
490      * @param nodeIdentifier Node identifier of the schema tree child to enter
491      * @return Resolved schema tree child
492      * @throws NullPointerException if {@code nodeIdentifier} is null
493      * @throws IllegalArgumentException if the corresponding child cannot be found
494      */
495     public @NonNull SchemaTreeEffectiveStatement<?> enterSchemaTree(final QName nodeIdentifier) {
496         return pushSchema(requireNonNull(nodeIdentifier));
497     }
498
499     /**
500      * Lookup a {@code schema tree} node by its schema node identifier and push it to the stack.
501      *
502      * @param nodeIdentifier Schema node identifier of the schema tree node to enter
503      * @return Resolved schema tree node
504      * @throws NullPointerException if {@code nodeIdentifier} is null
505      * @throws IllegalArgumentException if the corresponding node cannot be found
506      */
507     public @NonNull SchemaTreeEffectiveStatement<?> enterSchemaTree(final SchemaNodeIdentifier nodeIdentifier) {
508         if (nodeIdentifier instanceof Absolute) {
509             clear();
510         }
511
512         final Iterator<QName> it = nodeIdentifier.getNodeIdentifiers().iterator();
513         SchemaTreeEffectiveStatement<?> ret;
514         do {
515             ret = enterSchemaTree(it.next());
516         } while (it.hasNext());
517
518         return ret;
519     }
520
521     /**
522      * Lookup a {@code schema tree} child by its node identifier and push it to the stack.
523      *
524      * @param nodeIdentifier Node identifier of the date tree child to enter
525      * @return Resolved date tree child
526      * @throws NullPointerException if {@code nodeIdentifier} is null
527      * @throws IllegalArgumentException if the corresponding child cannot be found
528      */
529     public @NonNull DataTreeEffectiveStatement<?> enterDataTree(final QName nodeIdentifier) {
530         return pushData(requireNonNull(nodeIdentifier));
531     }
532
533     /**
534      * Lookup a {@code typedef} by its node identifier and push it to the stack.
535      *
536      * @param nodeIdentifier Node identifier of the typedef to enter
537      * @return Resolved typedef
538      * @throws NullPointerException if {@code nodeIdentifier} is null
539      * @throws IllegalArgumentException if the corresponding typedef cannot be found
540      */
541     public @NonNull TypedefEffectiveStatement enterTypedef(final QName nodeIdentifier) {
542         return pushTypedef(requireNonNull(nodeIdentifier));
543     }
544
545     /**
546      * Lookup a {@code rc:yang-data} by the module namespace where it is defined and its template name.
547      *
548      * @param namespace Module namespace in which to lookup the template
549      * @param name Template name
550      * @return Resolved yang-data
551      * @throws NullPointerException if any argument is null
552      * @throws IllegalArgumentException if the corresponding yang-data cannot be found
553      * @throws IllegalStateException if this stack is not empty
554      */
555     public @NonNull YangDataEffectiveStatement enterYangData(final QNameModule namespace, final String name) {
556         final EffectiveStatement<?, ?> parent = deque.peekFirst();
557         checkState(parent == null, "Cannot lookup yang-data in a non-empty stack");
558
559         final String templateName = requireNonNull(name);
560         final ModuleEffectiveStatement module = effectiveModel.getModuleStatements().get(requireNonNull(namespace));
561         checkArgument(module != null, "Module for %s not found", namespace);
562
563         final YangDataEffectiveStatement ret = module.streamEffectiveSubstatements(YangDataEffectiveStatement.class)
564             .filter(stmt -> templateName.equals(stmt.argument()))
565             .findFirst()
566             .orElseThrow(
567                 () -> new IllegalArgumentException("yang-data " + templateName + " not present in " + namespace));
568         deque.push(ret);
569         currentModule = module;
570         return ret;
571     }
572
573     /**
574      * Pop the current statement from the stack.
575      *
576      * @return Previous statement
577      * @throws NoSuchElementException if this stack is empty
578      */
579     public @NonNull EffectiveStatement<?, ?> exit() {
580         final EffectiveStatement<?, ?> prev = deque.pop();
581         if (prev instanceof GroupingEffectiveStatement) {
582             --groupingDepth;
583         }
584         if (deque.isEmpty()) {
585             currentModule = null;
586             clean = true;
587         }
588         return prev;
589     }
590
591     /**
592      * Pop the current statement from the stack, asserting it is a {@link DataTreeEffectiveStatement} and that
593      * subsequent {@link #enterDataTree(QName)} will find it again.
594      *
595      * @return Previous statement
596      * @throws NoSuchElementException if this stack is empty
597      * @throws IllegalStateException if current statement is not a DataTreeEffectiveStatement or if its parent is not
598      *                               a {@link DataTreeAwareEffectiveStatement}
599      */
600     public @NonNull DataTreeEffectiveStatement<?> exitToDataTree() {
601         final EffectiveStatement<?, ?> child = exit();
602         checkState(child instanceof DataTreeEffectiveStatement, "Unexpected current %s", child);
603         EffectiveStatement<?, ?> parent = deque.peekFirst();
604         while (parent instanceof ChoiceEffectiveStatement || parent instanceof CaseEffectiveStatement) {
605             deque.pollFirst();
606             parent = deque.peekFirst();
607         }
608
609         checkState(parent == null || parent instanceof DataTreeAwareEffectiveStatement, "Unexpected parent %s", parent);
610         return (DataTreeEffectiveStatement<?>) child;
611     }
612
613     @Override
614     public TypeDefinition<?> resolveLeafref(final LeafrefTypeDefinition type) {
615         final SchemaInferenceStack tmp = copy();
616
617         LeafrefTypeDefinition current = type;
618         while (true) {
619             final EffectiveStatement<?, ?> resolved = tmp.resolvePathExpression(current.getPathStatement());
620             checkState(resolved instanceof TypeAware, "Unexpected result %s resultion of %s", resolved, type);
621             final TypeDefinition<?> result = ((TypedDataSchemaNode) resolved).getType();
622             if (result instanceof LeafrefTypeDefinition) {
623                 checkArgument(result != type, "Resolution of %s loops back onto itself via %s", type, current);
624                 current = (LeafrefTypeDefinition) result;
625             } else {
626                 return result;
627             }
628         }
629     }
630
631     /**
632      * Resolve a {@link PathExpression}.
633      *
634      * <p>
635      * Note if this method throws, this stack may be in an undefined state.
636      *
637      * @param path Requested path
638      * @return Resolved schema tree child
639      * @throws NullPointerException if {@code path} is null
640      * @throws IllegalArgumentException if the target node cannot be found
641      * @throws VerifyException if path expression is invalid
642      */
643     public @NonNull EffectiveStatement<?, ?> resolvePathExpression(final PathExpression path) {
644         final Steps steps = path.getSteps();
645         if (steps instanceof LocationPathSteps) {
646             return resolveLocationPath(((LocationPathSteps) steps).getLocationPath());
647         } else if (steps instanceof DerefSteps) {
648             return resolveDeref((DerefSteps) steps);
649         } else {
650             throw new VerifyException("Unhandled steps " + steps);
651         }
652     }
653
654     private @NonNull EffectiveStatement<?, ?> resolveDeref(final DerefSteps deref) {
655         final EffectiveStatement<?, ?> leafRefSchemaNode = currentStatement();
656         final YangLocationPath.Relative derefArg = deref.getDerefArgument();
657         final EffectiveStatement<?, ?> derefStmt = resolveLocationPath(derefArg);
658         checkArgument(derefStmt != null, "Cannot find deref(%s) target node %s in context of %s",
659                 derefArg, leafRefSchemaNode);
660         checkArgument(derefStmt instanceof TypedDataSchemaNode, "deref(%s) resolved to non-typed %s", derefArg,
661                 derefStmt);
662
663         // We have a deref() target, decide what to do about it
664         final TypeDefinition<?> targetType = ((TypedDataSchemaNode) derefStmt).getType();
665         if (targetType instanceof InstanceIdentifierTypeDefinition) {
666             // Static inference breaks down, we cannot determine where this points to
667             // FIXME: dedicated exception, users can recover from it, derive from IAE
668             throw new UnsupportedOperationException("Cannot infer instance-identifier reference " + targetType);
669         }
670
671         // deref() is defined only for instance-identifier and leafref types, handle the latter
672         checkArgument(targetType instanceof LeafrefTypeDefinition, "Illegal target type %s", targetType);
673
674         final PathExpression dereferencedLeafRefPath = ((LeafrefTypeDefinition) targetType).getPathStatement();
675         EffectiveStatement<?, ?> derefNode = resolvePathExpression(dereferencedLeafRefPath);
676         checkArgument(derefStmt != null, "Can not find target node of dereferenced node %s", derefStmt);
677         checkArgument(derefNode instanceof LeafSchemaNode, "Unexpected %s reference in %s", deref,
678                 dereferencedLeafRefPath);
679         return resolveLocationPath(deref.getRelativePath());
680     }
681
682     private @NonNull EffectiveStatement<?, ?> resolveLocationPath(final YangLocationPath path) {
683         // get the default namespace before we clear and loose our deque
684         final QNameModule defaultNamespace = deque.isEmpty() ? null : ((QName) deque.peek().argument()).getModule();
685         if (path.isAbsolute()) {
686             clear();
687         }
688
689         EffectiveStatement<?, ?> current = null;
690         for (Step step : path.getSteps()) {
691             final YangXPathAxis axis = step.getAxis();
692             switch (axis) {
693                 case PARENT:
694                     verify(step instanceof AxisStep, "Unexpected parent step %s", step);
695                     try {
696                         current = exitToDataTree();
697                     } catch (IllegalStateException | NoSuchElementException e) {
698                         throw new IllegalArgumentException("Illegal parent access in " + path, e);
699                     }
700                     break;
701                 case CHILD:
702                     verify(step instanceof QNameStep, "Unexpected child step %s", step);
703                     current = enterChild((QNameStep) step, defaultNamespace);
704                     break;
705                 default:
706                     throw new VerifyException("Unexpected step " + step);
707             }
708         }
709
710         return verifyNotNull(current);
711     }
712
713     private @NonNull EffectiveStatement<?, ?> enterChild(final QNameStep step, final QNameModule defaultNamespace) {
714         final AbstractQName toResolve = step.getQName();
715         final QName qname;
716         if (toResolve instanceof QName) {
717             qname = (QName) toResolve;
718         } else if (toResolve instanceof Unqualified) {
719             checkArgument(defaultNamespace != null, "Can not find target module of step %s", step);
720             qname = ((Unqualified) toResolve).bindTo(defaultNamespace);
721         } else {
722             throw new VerifyException("Unexpected child step QName " + toResolve);
723         }
724         return enterDataTree(qname);
725     }
726
727     /**
728      * Return an {@link Inference} equivalent of current state.
729      *
730      * @return An {@link Inference}
731      */
732     public @NonNull Inference toInference() {
733         return new Inference(effectiveModel, deque.clone(), currentModule, groupingDepth, clean);
734     }
735
736     /**
737      * Return an {@link SchemaTreeInference} equivalent of current state.
738      *
739      * @return An {@link SchemaTreeInference}
740      * @throws IllegalStateException if current state cannot be converted to a {@link SchemaTreeInference}
741      */
742     public @NonNull SchemaTreeInference toSchemaTreeInference() {
743         checkState(inInstantiatedContext(), "Cannot convert uninstantiated context %s", this);
744         final var cleanDeque = clean ? deque : reconstructSchemaInferenceStack().deque;
745         return DefaultSchemaTreeInference.unsafeOf(getEffectiveModelContext(),
746             ImmutableList.<SchemaTreeEffectiveStatement<?>>builderWithExpectedSize(cleanDeque.size())
747                 .addAll(Iterators.transform(cleanDeque.descendingIterator(),
748                     stmt -> (SchemaTreeEffectiveStatement<?>) stmt))
749                 .build());
750     }
751
752     /**
753      * Convert current state into an absolute schema node identifier.
754      *
755      * @return Absolute schema node identifier representing current state
756      * @throws IllegalStateException if current state is not instantiated
757      */
758     public @NonNull Absolute toSchemaNodeIdentifier() {
759         checkState(inInstantiatedContext(), "Cannot convert uninstantiated context %s", this);
760         return Absolute.of(ImmutableList.<QName>builderWithExpectedSize(deque.size())
761             .addAll(simplePathFromRoot())
762             .build());
763     }
764
765     /**
766      * Convert current state into a SchemaPath.
767      *
768      * @return Absolute SchemaPath representing current state
769      * @throws IllegalStateException if current state is not instantiated
770      * @deprecated This method is meant only for interoperation with SchemaPath-based APIs.
771      */
772     @Deprecated
773     public @NonNull SchemaPath toSchemaPath() {
774         SchemaPath ret = SchemaPath.ROOT;
775         final Iterator<QName> it = simplePathFromRoot();
776         while (it.hasNext()) {
777             ret = ret.createChild(it.next());
778         }
779         return ret;
780     }
781
782     /**
783      * Return an iterator along {@link SchemaPath#getPathFromRoot()}. This method is a faster equivalent of
784      * {@code toSchemaPath().getPathFromRoot().iterator()}.
785      *
786      * @return An unmodifiable iterator
787      */
788     @Deprecated
789     public @NonNull Iterator<QName> schemaPathIterator() {
790         return Iterators.unmodifiableIterator(simplePathFromRoot());
791     }
792
793     @Override
794     public String toString() {
795         return MoreObjects.toStringHelper(this).add("stack", deque).toString();
796     }
797
798     private @NonNull GroupingEffectiveStatement pushGrouping(final @NonNull QName nodeIdentifier) {
799         final EffectiveStatement<?, ?> parent = deque.peekFirst();
800         return parent != null ? pushGrouping(parent, nodeIdentifier) : pushFirstGrouping(nodeIdentifier);
801     }
802
803     private @NonNull GroupingEffectiveStatement pushGrouping(final @NonNull EffectiveStatement<?, ?> parent,
804             final @NonNull QName nodeIdentifier) {
805         final GroupingEffectiveStatement ret = parent.streamEffectiveSubstatements(GroupingEffectiveStatement.class)
806             .filter(stmt -> nodeIdentifier.equals(stmt.argument()))
807             .findFirst()
808             .orElseThrow(() -> notPresent(parent, "Grouping", nodeIdentifier));
809         deque.push(ret);
810         ++groupingDepth;
811         return ret;
812     }
813
814     private @NonNull GroupingEffectiveStatement pushFirstGrouping(final @NonNull QName nodeIdentifier) {
815         final ModuleEffectiveStatement module = getModule(nodeIdentifier);
816         final GroupingEffectiveStatement ret = pushGrouping(module, nodeIdentifier);
817         currentModule = module;
818         return ret;
819     }
820
821     private @NonNull SchemaTreeEffectiveStatement<?> pushSchema(final @NonNull QName nodeIdentifier) {
822         final EffectiveStatement<?, ?> parent = deque.peekFirst();
823         return parent != null ? pushSchema(parent, nodeIdentifier) : pushFirstSchema(nodeIdentifier);
824     }
825
826     private @NonNull SchemaTreeEffectiveStatement<?> pushSchema(final EffectiveStatement<?, ?> parent,
827             final @NonNull QName nodeIdentifier) {
828         checkState(parent instanceof SchemaTreeAwareEffectiveStatement, "Cannot descend schema tree at %s", parent);
829         return pushSchema((SchemaTreeAwareEffectiveStatement<?, ?>) parent, nodeIdentifier);
830     }
831
832     private @NonNull SchemaTreeEffectiveStatement<?> pushSchema(
833             final @NonNull SchemaTreeAwareEffectiveStatement<?, ?> parent, final @NonNull QName nodeIdentifier) {
834         final SchemaTreeEffectiveStatement<?> ret = parent.findSchemaTreeNode(nodeIdentifier)
835             .orElseThrow(() -> notPresent(parent, "Schema tree child ", nodeIdentifier));
836         deque.push(ret);
837         return ret;
838     }
839
840     private @NonNull SchemaTreeEffectiveStatement<?> pushFirstSchema(final @NonNull QName nodeIdentifier) {
841         final ModuleEffectiveStatement module = getModule(nodeIdentifier);
842         final SchemaTreeEffectiveStatement<?> ret = pushSchema(module, nodeIdentifier);
843         currentModule = module;
844         return ret;
845     }
846
847     private @NonNull DataTreeEffectiveStatement<?> pushData(final @NonNull QName nodeIdentifier) {
848         final EffectiveStatement<?, ?> parent = deque.peekFirst();
849         return parent != null ? pushData(parent, nodeIdentifier) : pushFirstData(nodeIdentifier);
850     }
851
852     private @NonNull DataTreeEffectiveStatement<?> pushData(final EffectiveStatement<?, ?> parent,
853             final @NonNull QName nodeIdentifier) {
854         checkState(parent instanceof DataTreeAwareEffectiveStatement, "Cannot descend data tree at %s", parent);
855         return pushData((DataTreeAwareEffectiveStatement<?, ?>) parent, nodeIdentifier);
856     }
857
858     private @NonNull DataTreeEffectiveStatement<?> pushData(final @NonNull DataTreeAwareEffectiveStatement<?, ?> parent,
859             final @NonNull QName nodeIdentifier) {
860         final DataTreeEffectiveStatement<?> ret = parent.findDataTreeNode(nodeIdentifier)
861             .orElseThrow(() -> notPresent(parent, "Data tree child", nodeIdentifier));
862         deque.push(ret);
863         clean = false;
864         return ret;
865     }
866
867     private @NonNull DataTreeEffectiveStatement<?> pushFirstData(final @NonNull QName nodeIdentifier) {
868         final ModuleEffectiveStatement module = getModule(nodeIdentifier);
869         final DataTreeEffectiveStatement<?> ret = pushData(module, nodeIdentifier);
870         currentModule = module;
871         return ret;
872     }
873
874     private @NonNull TypedefEffectiveStatement pushTypedef(final @NonNull QName nodeIdentifier) {
875         final EffectiveStatement<?, ?> parent = deque.peekFirst();
876         return parent != null ? pushTypedef(parent, nodeIdentifier) : pushFirstTypedef(nodeIdentifier);
877     }
878
879     private @NonNull TypedefEffectiveStatement pushTypedef(final @NonNull EffectiveStatement<?, ?> parent,
880             final @NonNull QName nodeIdentifier) {
881         final TypedefEffectiveStatement ret = parent.get(TypedefNamespace.class, nodeIdentifier)
882             .orElseThrow(() -> notPresent(parent, "Typedef", nodeIdentifier));
883         deque.push(ret);
884         return ret;
885     }
886
887     private @NonNull TypedefEffectiveStatement pushFirstTypedef(final @NonNull QName nodeIdentifier) {
888         final ModuleEffectiveStatement module = getModule(nodeIdentifier);
889         final TypedefEffectiveStatement ret = pushTypedef(module, nodeIdentifier);
890         currentModule = module;
891         return ret;
892     }
893
894     private @NonNull ModuleEffectiveStatement getModule(final @NonNull QName nodeIdentifier) {
895         final ModuleEffectiveStatement module = effectiveModel.getModuleStatements().get(nodeIdentifier.getModule());
896         checkArgument(module != null, "Module for %s not found", nodeIdentifier);
897         return module;
898     }
899
900     // Unified access to queue iteration for addressing purposes. Since we keep 'logical' steps as executed by user
901     // at this point, conversion to SchemaNodeIdentifier may be needed. We dispatch based on 'clean'.
902     private Iterator<QName> simplePathFromRoot() {
903         return clean ? iterateQNames() : reconstructQNames();
904     }
905
906     private Iterator<QName> iterateQNames() {
907         return Iterators.transform(deque.descendingIterator(), stmt -> {
908             final Object argument = stmt.argument();
909             verify(argument instanceof QName, "Unexpected statement %s", stmt);
910             return (QName) argument;
911         });
912     }
913
914     // So there are some data tree steps in the stack... we essentially need to convert a data tree item into a series
915     // of schema tree items. This means at least N searches, but after they are done, we get an opportunity to set the
916     // clean flag.
917     private Iterator<QName> reconstructQNames() {
918         return reconstructSchemaInferenceStack().iterateQNames();
919     }
920
921     private SchemaInferenceStack reconstructSchemaInferenceStack() {
922         // Let's walk all statements and decipher them into a temporary stack
923         final SchemaInferenceStack tmp = new SchemaInferenceStack(effectiveModel, deque.size());
924         final Iterator<EffectiveStatement<?, ?>> it = deque.descendingIterator();
925         while (it.hasNext()) {
926             final EffectiveStatement<?, ?> stmt = it.next();
927             // Order of checks is significant
928             if (stmt instanceof DataTreeEffectiveStatement) {
929                 tmp.resolveDataTreeSteps(((DataTreeEffectiveStatement<?>) stmt).argument());
930             } else if (stmt instanceof ChoiceEffectiveStatement) {
931                 tmp.resolveChoiceSteps(((ChoiceEffectiveStatement) stmt).argument());
932             } else if (stmt instanceof SchemaTreeEffectiveStatement) {
933                 tmp.enterSchemaTree(((SchemaTreeEffectiveStatement<?> )stmt).argument());
934             } else if (stmt instanceof GroupingEffectiveStatement) {
935                 tmp.enterGrouping(((GroupingEffectiveStatement) stmt).argument());
936             } else if (stmt instanceof TypedefEffectiveStatement) {
937                 tmp.enterTypedef(((TypedefEffectiveStatement) stmt).argument());
938             } else {
939                 throw new VerifyException("Unexpected statement " + stmt);
940             }
941         }
942
943         // if the sizes match, we did not jump through hoops. let's remember that for future.
944         if (deque.size() == tmp.deque.size()) {
945             clean = true;
946             return this;
947         }
948         return tmp;
949     }
950
951     private void resolveChoiceSteps(final @NonNull QName nodeIdentifier) {
952         final EffectiveStatement<?, ?> parent = deque.peekFirst();
953         if (parent instanceof ChoiceEffectiveStatement) {
954             resolveChoiceSteps((ChoiceEffectiveStatement) parent, nodeIdentifier);
955         } else {
956             enterSchemaTree(nodeIdentifier);
957         }
958     }
959
960     private void resolveChoiceSteps(final @NonNull ChoiceEffectiveStatement parent,
961             final @NonNull QName nodeIdentifier) {
962         for (EffectiveStatement<?, ?> stmt : parent.effectiveSubstatements()) {
963             if (stmt instanceof CaseEffectiveStatement) {
964                 final CaseEffectiveStatement caze = (CaseEffectiveStatement) stmt;
965                 final SchemaTreeEffectiveStatement<?> found = caze.findSchemaTreeNode(nodeIdentifier).orElse(null);
966                 if (found instanceof ChoiceEffectiveStatement) {
967                     deque.push(caze);
968                     deque.push(found);
969                     return;
970                 }
971             }
972         }
973         throw new VerifyException("Failed to resolve " + nodeIdentifier + " in " + parent);
974     }
975
976     private void resolveDataTreeSteps(final @NonNull QName nodeIdentifier) {
977         final EffectiveStatement<?, ?> parent = deque.peekFirst();
978         if (parent != null) {
979             verify(parent instanceof SchemaTreeAwareEffectiveStatement, "Unexpected parent %s", parent);
980             resolveDataTreeSteps((SchemaTreeAwareEffectiveStatement<?, ?>) parent, nodeIdentifier);
981             return;
982         }
983
984         final ModuleEffectiveStatement module = getModule(nodeIdentifier);
985         resolveDataTreeSteps(module, nodeIdentifier);
986         currentModule = module;
987     }
988
989     private void resolveDataTreeSteps(final @NonNull SchemaTreeAwareEffectiveStatement<?, ?> parent,
990             final @NonNull QName nodeIdentifier) {
991         // The algebra of identifiers in 'schema tree versus data tree':
992         // - data tree parents are always schema tree parents
993         // - data tree children are always schema tree children
994
995         // that implies that a data tree parent must satisfy schema tree queries with data tree children,
996         // so a successful lookup of 'data tree parent -> child' and 'schema tree parent -> child' has to be the same
997         // for a direct lookup.
998         final SchemaTreeEffectiveStatement<?> found = parent.findSchemaTreeNode(nodeIdentifier).orElse(null);
999         if (found instanceof DataTreeEffectiveStatement) {
1000             // ... and it did, we are done
1001             deque.push(found);
1002             return;
1003         }
1004
1005         // Alright, so now it's down to filtering choice/case statements. For that we keep some globally-reused state
1006         // and employ a recursive match.
1007         final Deque<EffectiveStatement<QName, ?>> match = new ArrayDeque<>();
1008         for (EffectiveStatement<?, ?> stmt : parent.effectiveSubstatements()) {
1009             if (stmt instanceof ChoiceEffectiveStatement
1010                 && searchChoice(match, (ChoiceEffectiveStatement) stmt, nodeIdentifier)) {
1011                 match.descendingIterator().forEachRemaining(deque::push);
1012                 return;
1013             }
1014         }
1015
1016         throw new VerifyException("Failed to resolve " + nodeIdentifier + " in " + parent);
1017     }
1018
1019     private static boolean searchCase(final @NonNull Deque<EffectiveStatement<QName, ?>> result,
1020             final @NonNull CaseEffectiveStatement parent, final @NonNull QName nodeIdentifier) {
1021         result.push(parent);
1022         for (EffectiveStatement<?, ?> stmt : parent.effectiveSubstatements()) {
1023             if (stmt instanceof DataTreeEffectiveStatement && nodeIdentifier.equals(stmt.argument())) {
1024                 result.push((DataTreeEffectiveStatement<?>) stmt);
1025                 return true;
1026             }
1027             if (stmt instanceof ChoiceEffectiveStatement
1028                 && searchChoice(result, (ChoiceEffectiveStatement) stmt, nodeIdentifier)) {
1029                 return true;
1030             }
1031         }
1032         result.pop();
1033         return false;
1034     }
1035
1036     private static boolean searchChoice(final @NonNull Deque<EffectiveStatement<QName, ?>> result,
1037             final @NonNull ChoiceEffectiveStatement parent, final @NonNull QName nodeIdentifier) {
1038         result.push(parent);
1039         for (EffectiveStatement<?, ?> stmt : parent.effectiveSubstatements()) {
1040             if (stmt instanceof CaseEffectiveStatement
1041                 && searchCase(result, (CaseEffectiveStatement) stmt, nodeIdentifier)) {
1042                 return true;
1043             }
1044         }
1045         result.pop();
1046         return false;
1047     }
1048
1049     private static <T> @NonNull T checkNonNullState(final @Nullable T obj) {
1050         if (obj == null) {
1051             throw new IllegalStateException("Cannot execute on empty stack");
1052         }
1053         return obj;
1054     }
1055
1056     private static @NonNull IllegalArgumentException notPresent(final @NonNull EffectiveStatement<?, ?> parent,
1057             final @NonNull String name, final QName nodeIdentifier) {
1058         return new IllegalArgumentException(name + " " + nodeIdentifier + " not present in " + describeParent(parent));
1059     }
1060
1061     private static @NonNull String describeParent(final @NonNull EffectiveStatement<?, ?> parent) {
1062         // Add just enough information to be useful without being overly-verbose. Note we want to expose namespace
1063         // information, so that we understand what revisions we are dealing with
1064         if (parent instanceof SchemaTreeEffectiveStatement) {
1065             return "schema parent " + parent.argument();
1066         } else if (parent instanceof GroupingEffectiveStatement) {
1067             return "grouping " + parent.argument();
1068         } else if (parent instanceof ModuleEffectiveStatement) {
1069             final var module = (ModuleEffectiveStatement) parent;
1070             return "module " + module.argument().bindTo(module.localQNameModule());
1071         } else {
1072             // Shorthand for QNames, should provide enough context
1073             final Object arg = parent.argument();
1074             return "parent " + (arg instanceof QName ? arg : parent);
1075         }
1076     }
1077 }