659c6dde3357b79e7631ff37669951050a873b50
[smo/teiv.git] /
1 /*
2  *  ============LICENSE_START=======================================================
3  *  Copyright (C) 2024 Ericsson
4  *  Modifications Copyright (C) 2024 OpenInfra Foundation Europe
5  *  ================================================================================
6  *  Licensed under the Apache License, Version 2.0 (the "License");
7  *  you may not use this file except in compliance with the License.
8  *  You may obtain a copy of the License at
9  *
10  *        http://www.apache.org/licenses/LICENSE-2.0
11  *
12  *  Unless required by applicable law or agreed to in writing, software
13  *  distributed under the License is distributed on an "AS IS" BASIS,
14  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  *  See the License for the specific language governing permissions and
16  *  limitations under the License.
17  *
18  *  SPDX-License-Identifier: Apache-2.0
19  *  ============LICENSE_END=========================================================
20  */
21 package org.oran.smo.yangtools.parser.model.resolvers;
22
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.Collections;
26 import java.util.HashSet;
27 import java.util.List;
28 import java.util.Set;
29 import java.util.stream.Collectors;
30
31 import org.oran.smo.yangtools.parser.ParserExecutionContext;
32 import org.oran.smo.yangtools.parser.findings.Finding;
33 import org.oran.smo.yangtools.parser.findings.ParserFindingType;
34 import org.oran.smo.yangtools.parser.model.ModuleIdentity;
35 import org.oran.smo.yangtools.parser.model.schema.Schema;
36 import org.oran.smo.yangtools.parser.model.schema.SchemaProcessor;
37 import org.oran.smo.yangtools.parser.model.statements.AbstractStatement;
38 import org.oran.smo.yangtools.parser.model.statements.ExtensionStatement;
39 import org.oran.smo.yangtools.parser.model.statements.StatementModuleAndName;
40 import org.oran.smo.yangtools.parser.model.statements.yang.CY;
41 import org.oran.smo.yangtools.parser.model.statements.yang.YAugment;
42 import org.oran.smo.yangtools.parser.model.statements.yang.YGrouping;
43 import org.oran.smo.yangtools.parser.model.statements.yang.YIfFeature;
44 import org.oran.smo.yangtools.parser.model.statements.yang.YRefine;
45 import org.oran.smo.yangtools.parser.model.statements.yang.YStatus;
46 import org.oran.smo.yangtools.parser.model.statements.yang.YUses;
47 import org.oran.smo.yangtools.parser.model.statements.yang.YWhen;
48 import org.oran.smo.yangtools.parser.model.util.StringHelper;
49
50 /**
51  * A class that can resolve all usage of "uses" by including the referenced grouping.
52  *
53  * This class will correctly handle nested grouping resolution - that means, can handle a
54  * grouping referring to another grouping (as it has a uses).
55  *
56  * @author Mark Hollmann
57  */
58 public abstract class UsesResolver {
59
60     /**
61      * Resolving means all occurrences of "uses" are replaced by the grouping they are referring to.
62      */
63     public static void resolveUsagesOfUses(final ParserExecutionContext context, final Schema schema) {
64
65         int iterationCount = 10;
66         boolean atLeastOneResolved = true;
67
68         while (iterationCount > 0 && atLeastOneResolved) {
69
70             atLeastOneResolved = false;
71             iterationCount--;
72
73             /*
74              * It is correct that the list of "uses" statements is fetched every time here, and not once outside
75              * the while-loop. The reason is that otherwise we would simply keep doing the same merge/replace 10 times,
76              * and also replaced "uses" statements are detached from the tree, so no need to do these again.
77              */
78             final List<YUses> allUses = findUsesToConsider(schema);
79             for (final YUses uses : allUses) {
80                 try {
81                     atLeastOneResolved |= resolveUses(context, schema, uses);
82                 } catch (final Exception ex) {
83                     /* Swallow and move to next. Best effort here, keep trying other uses. */
84                 }
85             }
86
87             if (iterationCount == 7) {
88                 final List<YUses> usesWithExcessiveGroupingDepth = findUsesToConsider(schema);
89                 usesWithExcessiveGroupingDepth.forEach(yUses -> context.addFinding(new Finding(yUses,
90                         ParserFindingType.P122_EXCESSIVE_USES_DEPTH,
91                         "'uses' statement refers to 'grouping' with nesting depth > 3.")));
92             }
93         }
94
95         /*
96          * Done resolving. If some 'uses' are left they could not be resolved because of circular dependencies.
97          */
98         final List<YUses> allUses = findUsesToConsider(schema);
99         allUses.forEach(yUses -> context.addFinding(new Finding(yUses, ParserFindingType.P121_CIRCULAR_USES_REFERENCES,
100                 "Likely circular references between 'uses' and 'grouping'. Use the quoted file and line number as starting point for investigation.")));
101
102         /*
103          * Finished with replacing all usages of grouping. Perform a check to see
104          * which groupings have only be used once, or not used at all.
105          */
106         @SuppressWarnings("unchecked") final List<YGrouping> allGroupings = (List<YGrouping>) Helper.findStatementsInSchema(
107                 CY.STMT_GROUPING, schema);
108         for (final YGrouping oneGrouping : allGroupings) {
109             final int used = getGroupingUsageCount(oneGrouping);
110             if (used == 0) {
111                 context.addFinding(new Finding(oneGrouping, ParserFindingType.P132_GROUPING_NOT_USED,
112                         "grouping statement '" + oneGrouping.getGroupingName() + "' not used."));
113             } else if (used == 1) {
114                 context.addFinding(new Finding(oneGrouping, ParserFindingType.P133_GROUPING_USED_ONCE_ONLY,
115                         "grouping statement '" + oneGrouping.getGroupingName() + "' used only once; consider inlining."));
116             }
117         }
118     }
119
120     /**
121      * Does what it says on the tin. Note that the 'uses' statement will be removed from the tree once it has been
122      * resolved (and likewise it will remain in the statement tree if it cannot be resolved).
123      */
124     private static boolean resolveUses(final ParserExecutionContext context, final Schema schema,
125             final YUses usesStatement) {
126
127         final String groupingName = usesStatement.getUsesGroupingName();
128         if (groupingName.isEmpty()) {
129             /*
130              * Pointless trying to resolve the grouping. No point issuing a finding either, a
131              * P015_INVALID_SYNTAX_IN_DOCUMENT would have been issued already.
132              */
133             setUsesNotResolvable(usesStatement);
134             return false;
135         }
136
137         /*
138          * Only now attempt to resolve grouping
139          */
140         final YGrouping foundGrouping = Helper.findStatement(context, schema, usesStatement, CY.STMT_GROUPING,
141                 groupingName);
142         if (foundGrouping == null) {
143             setUsesNotResolvable(usesStatement);
144             context.addFinding(new Finding(usesStatement, ParserFindingType.P131_UNRESOLVABLE_GROUPING,
145                     "Cannot resolve grouping '" + usesStatement.getUsesGroupingName() + "'."));
146             return false;
147         }
148
149         /*
150          * Mark the grouping has been used.
151          */
152         incGroupingUsageCount(foundGrouping);
153
154         /*
155          * Check for nested 'uses' within the 'grouping'. If found, this means that the contents of the
156          * grouping itself must be resolved first.
157          */
158         if (usesExistWithinFoundGrouping(context, usesStatement, foundGrouping)) {
159             return false;
160         }
161
162         /*
163          * We first create a 1:1 clone of the grouping. Note that the prefix resolver stays the same, i.e. is the
164          * prefix resolver from the module containing the found grouping statement. The cloned grouping statement
165          * is a sibling of the found grouping so that we can apply refine/augments to it (the cloned grouping will
166          * be removed again later on).
167          */
168         final YGrouping clonedGrouping = new YGrouping(foundGrouping.getParentStatement(), foundGrouping.getDomElement());
169         clonedGrouping.cloneFrom(foundGrouping);
170
171         for (final AbstractStatement oneChildOfClonedGrouping : clonedGrouping.getChildStatements()) {
172             Helper.addGeneralInfoAppData(oneChildOfClonedGrouping, "statement placed here by 'uses' in " + StringHelper
173                     .getModuleLineString(usesStatement) + " of grouping '" + usesStatement
174                             .getUsesGroupingName() + "' from " + StringHelper.getModuleLineString(foundGrouping));
175             addGroupingReference(oneChildOfClonedGrouping, foundGrouping);
176         }
177
178         /*
179          * Handle any status statement under the 'uses' or 'grouping'.
180          */
181         handleStatus(usesStatement, clonedGrouping);
182
183         /*
184          * Handle 'refine'. These are used to update the contents of the grouping.
185          */
186         handleRefines(context, schema, usesStatement, clonedGrouping, foundGrouping);
187
188         /*
189          * We apply any "augments". The augments hangs under the "uses" statement; we simply re-parent
190          * all statements that hang under augments into the correct location in the group, based on the
191          * target node of the augments.
192          */
193         for (final YAugment augment : usesStatement.getAugments()) {
194             handleAugment(context, schema, augment, clonedGrouping, foundGrouping, usesStatement);
195         }
196
197         /*
198          * If there is an "if-feature" underneath the 'uses', then this if-feature will be
199          * applied to each of the direct children of the cloned grouping statement. Note that
200          * any if-feature will be *added*, not set (i.e. it's a merge operation of the
201          * if-feature statements, not a replace.)
202          */
203         for (final YIfFeature origUsesOneIfFeature : usesStatement.getIfFeatures()) {
204             for (final AbstractStatement childOfClonedGrouping : clonedGrouping.getChildStatements()) {
205                 final YIfFeature clonedIfFeature = new YIfFeature(childOfClonedGrouping, origUsesOneIfFeature
206                         .getDomElement());
207                 clonedIfFeature.cloneFrom(origUsesOneIfFeature);
208                 // No need to explicitly add it - the YIfFeature constructor will add it as child already.
209             }
210         }
211
212         /*
213          * If there is a 'when' statement underneath the 'uses', then this 'when' will be applied to each of the
214          * direct children of the cloned grouping statement. Note this is different from a 'when' statement that
215          * is part of the augments for a uses. The original 'when' clause relates to the 'uses' statement, whose
216          * data node is the parent of the uses. Hence the cloned when statements apply to the parent of the
217          * respective data node, not the data node in the grouping itself!
218          */
219         final YWhen origUsesWhen = usesStatement.getWhen();
220         if (origUsesWhen != null) {
221             for (final AbstractStatement statementToApplyWhenTo : clonedGrouping.getChildStatements()) {
222                 final YWhen clonedWhen = new YWhen(statementToApplyWhenTo, origUsesWhen.getDomElement());
223                 clonedWhen.cloneFrom(origUsesWhen);
224                 clonedWhen.setAppliesToParentSchemaNode();
225                 // No need to explicitly add it - the YWhen constructor will add it as child already.
226             }
227         }
228
229         /*
230          * The cloned grouping is now complete. We now hang the contents of the cloned grouping underneath the parent
231          * statement of the 'uses' (i.e., in effect replace the 'uses' statement with the contents of the cloned grouping).
232          *
233          * When we do this, the following child elements of the cloned grouping can be ignored:
234          * - if-feature (not a valid child underneath grouping)
235          * - grouping (nested, would have resolved any uses of the grouping beforehand)
236          * - typedef (would have been resolved beforehand)
237          * - uses (nested, would have been resolved beforehand)
238          *
239          * Note that we have to re-parent the cloned groupings child statements, of course. The prefix resolver is ok,
240          * as it would have inherited down from the cloned grouping, and the cloned grouping got the prefix resolver
241          * of the original 'grouping' statement.
242          */
243         final AbstractStatement parentOfUsesStatement = usesStatement.getParentStatement();
244
245         parentOfUsesStatement.addChildren(clonedGrouping.getActions());
246         parentOfUsesStatement.addChildren(clonedGrouping.getAnyxmls());
247         parentOfUsesStatement.addChildren(clonedGrouping.getAnydata());
248         parentOfUsesStatement.addChildren(clonedGrouping.getChoices());
249         parentOfUsesStatement.addChildren(clonedGrouping.getContainers());
250         parentOfUsesStatement.addChildren(clonedGrouping.getLeafs());
251         parentOfUsesStatement.addChildren(clonedGrouping.getLeafLists());
252         parentOfUsesStatement.addChildren(clonedGrouping.getLists());
253         parentOfUsesStatement.addChildren(clonedGrouping.getNotifications());
254
255         /*
256          * Any finally, remove the cloned grouping as it is not needed anymore, and remove the original "uses" statement
257          */
258         clonedGrouping.getParentStatement().removeChild(clonedGrouping);
259         usesStatement.getParentStatement().removeChild(usesStatement);
260
261         return true;
262     }
263
264     /**
265      * Handle a possible 'status' statement under the 'uses' or 'grouping'. We must do this here during
266      * the merge of the 'grouping' content, as the 'grouping' and the 'uses' and their possible child
267      * 'status' will disappear from the schema tree, hence the 'status' will be lost. To retain the
268      * information, we must clone the 'status' statement into the contents of the 'grouping'.
269      *
270      * Note there is some special handling - if the status is more restrictive under the used
271      * statement then this would not be replaced. For example, if the status is DEPRECATED under the
272      * 'uses', but it is explicitly OBSOLETE under a container being a child of the 'grouping', this would
273      * not be updated.
274      */
275     private static void handleStatus(final YUses uses, final YGrouping clonedGrouping) {
276
277         final YStatus statusUnderUses = uses.getStatus();
278         final YStatus statusUnderGrouping = clonedGrouping.getStatus();
279
280         if (statusUnderUses == null && statusUnderGrouping == null) {
281             return;
282         }
283
284         /*
285          * So this gets a bit tricky. There can be a 'status' statement either under the 'uses',
286          * or under the 'grouping', or possibly both.
287          */
288         YStatus overrideStatus = null;
289         if (statusUnderUses != null && statusUnderGrouping != null) {
290             /*
291              * We are interested in pushing-down the more severe status, so we need to compare these to
292              * find which one it is.
293              */
294             overrideStatus = statusUnderUses.getStatusOrder() > statusUnderGrouping.getStatusOrder() ?
295                     statusUnderUses :
296                     statusUnderGrouping;
297
298         } else if (statusUnderUses != null) {
299
300             overrideStatus = statusUnderUses;
301
302         } else if (statusUnderGrouping != null) {
303
304             overrideStatus = statusUnderGrouping;
305         }
306
307         /*
308          * Now apply the override 'status' to the contents of the 'grouping'.
309          */
310         for (final AbstractStatement childOfClonedGrouping : clonedGrouping.getChildStatements()) {
311
312             if (childOfClonedGrouping.is(CY.STMT_STATUS)) {
313                 continue;
314             }
315
316             final YStatus childExplicitStatus = childOfClonedGrouping.getChild(CY.STMT_STATUS);
317             boolean clone = true;
318
319             if (childExplicitStatus == null) {
320                 /*
321                  * There is no 'status' statement under the child, so then we will simply
322                  * clone down the parent 'status' in a moment.
323                  */
324             } else if (childExplicitStatus.getStatusOrder() >= overrideStatus.getStatusOrder()) {
325                 /*
326                  * There is an explicit 'status' statement under the child. If the child 'status'
327                  * is more restrictive, or the same, as the 'status' of the parent we don't have
328                  * to do anything, i.e. don't clone.
329                  *
330                  * For example, child is DEPRECATED, parent is CURRENT - hence child is more
331                  * restrictive, so don't overwrite the 'status' (don't clone the parent 'status').
332                  */
333                 clone = false;
334             }
335
336             if (clone) {
337                 /*
338                  * Must clone, so first remove the 'status' statement under the child (if it exists).
339                  */
340                 if (childExplicitStatus != null) {
341                     childOfClonedGrouping.removeChild(childExplicitStatus);
342                 }
343                 /*
344                  * Now clone down the parent's (the uses's or grouping's) 'status' into the child.
345                  */
346                 final YStatus clonedStatus = new YStatus(childOfClonedGrouping, overrideStatus.getDomElement());
347                 clonedStatus.cloneFrom(overrideStatus);
348                 addUsesResolutionAppData(clonedStatus,
349                         "This 'status' statement has been inherited from the 'uses'/'grouping' statement.");
350             }
351         }
352     }
353
354     private static final List<StatementModuleAndName> TARGETS_ALLOWED_FOR_AUGMENTATION = Arrays.asList(CY.STMT_CONTAINER,
355             CY.STMT_LIST, CY.STMT_CHOICE, CY.STMT_CASE, CY.STMT_INPUT, CY.STMT_OUTPUT, CY.STMT_NOTIFICATION);
356
357     private static final Set<StatementModuleAndName> STATEMENTS_UNDER_AUGMENT_TO_HANDLE = new HashSet<>(Arrays.asList(
358             CY.STMT_ACTION, CY.STMT_ANYDATA, CY.STMT_ANYXML, CY.STMT_CASE, CY.STMT_CHOICE, CY.STMT_CONTAINER,
359             CY.STMT_LEAF_LIST, CY.STMT_LEAF, CY.STMT_LIST, CY.STMT_NOTIFICATION));
360
361     private static void handleAugment(final ParserExecutionContext context, final Schema schema, final YAugment augment,
362             final YGrouping clonedGrouping, final YGrouping foundGrouping, final YUses usesStatement) {
363
364         final String augmentTargetNode = augment.getAugmentTargetNode();
365         if (augmentTargetNode.isEmpty() || augmentTargetNode.startsWith("/")) {
366             /*
367              * Pointless trying to resolve the path. No point issuing a finding either, a
368              * P015_INVALID_SYNTAX_IN_DOCUMENT would have been issued already.
369              */
370             return;
371         }
372
373         /*
374          * First thing check the status of the 'augment'. If it is OBSOLETE nothing gets merged in.
375          */
376         final String augmentStatus = augment.getStatus() != null ? augment.getStatus().getValue() : YStatus.CURRENT;
377         if (augmentStatus.equals(YStatus.OBSOLETE)) {
378             Helper.addGeneralInfoAppData(augment,
379                     "'augment' not applied to grouping as the augment is marked as OBSOLETE.");
380             return;
381         }
382
383         final AbstractStatement targetSchemaNodeOfAugment = Helper.findSchemaNode(context, clonedGrouping,
384                 augmentTargetNode, schema);
385         if (targetSchemaNodeOfAugment == null) {
386             context.addFinding(new Finding(augment.getDomElement(), ParserFindingType.P054_UNRESOLVABLE_PATH.toString(),
387                     "Cannot find schema node with path '" + augment
388                             .getAugmentTargetNode() + "' relative to the 'uses' statement."));
389             return;
390         }
391
392         /*
393          * Make sure what is being augmented is actually allowed according to the RFC.
394          */
395         if (!TARGETS_ALLOWED_FOR_AUGMENTATION.contains(targetSchemaNodeOfAugment.getStatementModuleAndName())) {
396             context.addFinding(new Finding(augment.getDomElement(), ParserFindingType.P123_INVALID_USES_AUGMENT_TARGET_NODE
397                     .toString(), "Statement '" + targetSchemaNodeOfAugment
398                             .getStatementName() + "' pointed to by '" + augment
399                                     .getAugmentTargetNode() + "' cannot be augmented."));
400             return;
401         }
402
403         /*
404          * Collect all the statements that will be added to the augment's target node in a moment.
405          */
406         final List<AbstractStatement> statementsToAddAsChildrenOfTargetNode = augment.getChildren(
407                 STATEMENTS_UNDER_AUGMENT_TO_HANDLE);
408         for (final AbstractStatement statementToAddAsChildOfTargetNode : statementsToAddAsChildrenOfTargetNode) {
409             Helper.addGeneralInfoAppData(statementToAddAsChildOfTargetNode,
410                     "augmented-in into used grouping '" + foundGrouping
411                             .getGroupingName() + "' by 'uses' statement in " + StringHelper.getModuleLineString(
412                                     usesStatement));
413         }
414
415         /*
416          * This is where things get interesting. The target node could be a CHOICE, and the children nodes could be data
417          * definition statements (short-hand notation). In this scenario we want to interject an artificial CASE statement
418          * to clean up the schema tree. Otherwise, other augments (or deviations) may not work subsequently.
419          */
420         if (targetSchemaNodeOfAugment.is(CY.STMT_CHOICE)) {
421
422             SchemaProcessor.injectCaseForShorthandedStatements(augment);
423
424             /*
425              * The direct children of choice may have have changed (case interjected), so we
426              * need to re-fetch these before further processing.
427              */
428             statementsToAddAsChildrenOfTargetNode.clear();
429             statementsToAddAsChildrenOfTargetNode.addAll(augment.getChildren(STATEMENTS_UNDER_AUGMENT_TO_HANDLE));
430         }
431
432         /*
433          * If the augment has a 'when' clause this gets applied to all statements within the 'grouping'.
434          *
435          * Note that the "when" statement is *added*, not *replaced*. That's a bit of a hack, as YANG only allows for a single
436          * "when" for statements. However, it could conceivably be the case that each of the statements amended thus has
437          * itself already a "when" clause - so using a *replace* would be wrong.
438          */
439         if (augment.getWhen() != null) {
440             for (final AbstractStatement childToAdd : statementsToAddAsChildrenOfTargetNode) {
441                 final YWhen clonedWhen = new YWhen(childToAdd, augment.getWhen().getDomElement());
442                 clonedWhen.cloneFrom(augment.getWhen());
443                 clonedWhen.setAppliesToParentSchemaNode();
444             }
445         }
446
447         /*
448          * If the augment has one or multiple "if-feature" statements then these will be applied to each
449          * of the augment's statements individually.
450          */
451         for (final YIfFeature ifFeature : augment.getIfFeatures()) {
452             for (final AbstractStatement childToAdd : statementsToAddAsChildrenOfTargetNode) {
453                 final YIfFeature clonedIfFeature = new YIfFeature(childToAdd, ifFeature.getDomElement());
454                 clonedIfFeature.cloneFrom(ifFeature);
455             }
456         }
457
458         /*
459          * If the 'augment' has a status then this status must likewise be applied to the
460          * children. Really, it can only conceivable have CURRENT or DEPRECATED.
461          */
462         if (augmentStatus.equals(YStatus.DEPRECATED)) {
463             for (final AbstractStatement childToAdd : statementsToAddAsChildrenOfTargetNode) {
464                 /*
465                  * Rules:
466                  *
467                  * 1. Child does not have a status -> then it gets one (same as status on the augment).
468                  * 2. Child has status and it is the same as that on augment -> do nothing.
469                  * 3. Child has status and it is OBSOLETE -> do nothing.
470                  */
471                 final YStatus childStatus = childToAdd.getChild(CY.STMT_STATUS);
472                 if (childStatus == null) {
473                     final YStatus clonedStatus = new YStatus(childToAdd, augment.getStatus().getDomElement());
474                     clonedStatus.cloneFrom(augment.getStatus());
475                 } else if (childStatus.getValue().equals(augmentStatus) || childStatus.isObsolete()) {
476                     // do nothing.
477                 }
478             }
479         }
480
481         /*
482          * And now simply move all the statements from under the augment under the target node.
483          */
484         targetSchemaNodeOfAugment.addChildren(statementsToAddAsChildrenOfTargetNode);
485     }
486
487     /**
488      * Check if the found grouping itself contains any "uses", and/or any "uses" that are not resolvable.
489      */
490     private static boolean usesExistWithinFoundGrouping(final ParserExecutionContext context, final YUses usesStatement,
491             final YGrouping grouping) {
492
493         final List<YUses> usesWithinGrouping = new ArrayList<>();
494         Helper.findStatementsInSubtree(grouping, CY.STMT_USES, usesWithinGrouping);
495
496         boolean groupingContainsNonResolveableUses = false;
497         for (final YUses usesWithin : usesWithinGrouping) {
498             if (isUsesNotResolvable(usesWithin)) {
499
500                 groupingContainsNonResolveableUses = true;
501
502                 /*
503                  * We issue additional findings here to help the user figure out which
504                  * nested 'uses' is/are causing the problem.
505                  */
506                 context.addFinding(new Finding(usesStatement, ParserFindingType.P134_NESTED_USES_NOT_RESOLVABLE,
507                         "Referenced grouping '" + usesStatement
508                                 .getUsesGroupingName() + "' has nested unresolvable 'uses' statement " + usesWithin
509                                         .getDomElement().getNameValue() + "."));
510             }
511         }
512
513         if (groupingContainsNonResolveableUses) {
514             /*
515              * If the found grouping has itself a 'uses' that is not resolvable, then this 'uses'
516              * here likewise cannot be resolved.
517              */
518             setUsesNotResolvable(usesStatement);
519         }
520
521         return usesWithinGrouping.size() > 0;
522     }
523
524     private static void handleRefines(final ParserExecutionContext context, final Schema schema, final YUses usesStatement,
525             final YGrouping clonedGrouping, final YGrouping foundGrouping) {
526         /*
527          * We refine the contents of the group, if so required. Note that the 'refine' statement hangs
528          * under the 'uses' statement.
529          */
530         for (final YRefine refine : usesStatement.getRefines()) {
531
532             final String refineTargetNode = refine.getRefineTargetNode();
533             if (refineTargetNode.isEmpty() || refineTargetNode.startsWith("/")) {
534                 /*
535                  * Pointless trying to resolve the path. No point issuing a finding either, a
536                  * P015_INVALID_SYNTAX_IN_DOCUMENT would have been issued already.
537                  */
538                 continue;
539             }
540
541             final AbstractStatement refinedStatement = Helper.findSchemaNode(context, clonedGrouping, refineTargetNode,
542                     schema);
543             if (refinedStatement == null) {
544                 context.addFinding(new Finding(refine, ParserFindingType.P054_UNRESOLVABLE_PATH,
545                         "Cannot find schema node with path '" + refineTargetNode + "' for refine of grouping '" + foundGrouping
546                                 .getGroupingName() + "'."));
547                 continue;
548             }
549
550             refineYangStatements(context, refinedStatement, usesStatement, refine);
551             refineExtensionStatements(refinedStatement, refine);
552         }
553     }
554
555     private static void refineExtensionStatements(final AbstractStatement refinedStatement, final YRefine refine) {
556         /*
557          * Extensions have to be handled. The RFC does not stipulate how these are to be handled. The
558          * working assumption here is that 'replace' semantics shall apply to all of these. In other
559          * words, extensions of a given type (identified through the extension name and its owning module
560          * name) replace any instance of the same type.
561          *
562          * We first collect all extensions that are refined, and keep a note of their "type"
563          * (combination of module name + extension name).
564          */
565         final List<AbstractStatement> extensionsUnderRefine = new ArrayList<>();
566         final Set<String> moduleNameAndExtensionNameOfExtensionsUnderRefine = new HashSet<>();
567
568         refine.getExtensionChildStatements().forEach(extensionStatement -> {
569             extensionsUnderRefine.add(extensionStatement);
570             final String moduleNameAndExtensionName = getModuleNameAndExtensionName(extensionStatement);
571             moduleNameAndExtensionNameOfExtensionsUnderRefine.add(moduleNameAndExtensionName);
572             Helper.addGeneralInfoAppData(extensionStatement, "refines previous extension statement(s) of the same type.");
573         });
574
575         if (extensionsUnderRefine.isEmpty()) {
576             return;
577         }
578
579         /*
580          * Now collect all extensions instances that sit under the refined statement, and that are of
581          * the same type as any of those sitting under 'refine'.
582          */
583         final List<AbstractStatement> extensionsUnderRefinedStatementToRemove = new ArrayList<>();
584
585         refinedStatement.getExtensionChildStatements().forEach(extensionStatement -> {
586             final String moduleNameAndExtensionName = getModuleNameAndExtensionName(extensionStatement);
587             if (moduleNameAndExtensionNameOfExtensionsUnderRefine.contains(moduleNameAndExtensionName)) {
588                 Helper.addGeneralInfoAppData(refinedStatement, "previous extension statement " + extensionStatement
589                         .getDomElement().getNameValue() + " removed as it has been refined by 'uses'.");
590                 extensionsUnderRefinedStatementToRemove.add(extensionStatement);
591             }
592         });
593
594         /*
595          * And now simply remove from the refined statement the replaced extensions, and add all
596          * the extension statements that sit under the refine statement.
597          */
598         refinedStatement.removeChildren(extensionsUnderRefinedStatementToRemove);
599         refinedStatement.addChildren(extensionsUnderRefine);
600     }
601
602     /**
603      * Given an extension, returns a concatenation of the name of the module owning the
604      * extension definition, and the name of the extension.
605      */
606     private static String getModuleNameAndExtensionName(final ExtensionStatement extensionStatement) {
607
608         final String extensionModulePrefix = extensionStatement.getExtensionModulePrefix();
609         final String extensionStatementName = extensionStatement.getExtensionStatementName();
610
611         final ModuleIdentity owningModuleModuleIdentity = extensionStatement.getPrefixResolver().getModuleForPrefix(
612                 extensionModulePrefix);
613         if (owningModuleModuleIdentity == null) {
614             return null;
615         }
616
617         return owningModuleModuleIdentity.getModuleName() + ":::" + extensionStatementName;
618     }
619
620     private static final Set<String> ALLOWABLE_ELEMENTS_FOR_REFINE_MANDATORY = new HashSet<>(Arrays.asList(CY.LEAF,
621             CY.ANYDATA, CY.ANYXML, CY.CHOICE));
622     private static final Set<String> ALLOWABLE_ELEMENTS_FOR_REFINE_DEFAULT = new HashSet<>(Arrays.asList(CY.LEAF,
623             CY.LEAF_LIST, CY.CHOICE));
624     private static final Set<String> ALLOWABLE_ELEMENTS_FOR_REFINE_PRESENCE = new HashSet<>(Arrays.asList(CY.CONTAINER));
625     private static final Set<String> ALLOWABLE_ELEMENTS_FOR_REFINE_MUST = new HashSet<>(Arrays.asList(CY.LEAF, CY.LEAF_LIST,
626             CY.LIST, CY.CONTAINER, CY.ANYDATA, CY.ANYXML));
627     private static final Set<String> ALLOWABLE_ELEMENTS_FOR_REFINE_MIN_MAX_ELEMENTS = new HashSet<>(Arrays.asList(
628             CY.LEAF_LIST, CY.LIST));
629     private static final Set<String> ALLOWABLE_ELEMENTS_FOR_REFINE_IF_FEATURE = new HashSet<>(Arrays.asList(CY.LEAF,
630             CY.LEAF_LIST, CY.LIST, CY.CONTAINER, CY.CHOICE, CY.CASE, CY.ANYDATA, CY.ANYXML));
631
632     private static void refineYangStatements(final ParserExecutionContext context, final AbstractStatement refinedStatement,
633             final YUses uses, final YRefine refine) {
634         /*
635          * We refine the contents of the group, if so required. For this, we simply grab the 'refine' statement, and
636          * apply its content to whatever schema node it should be applied to. Note that the 'refine' statement hangs
637          * under the 'uses' statement.
638          *
639          * The RFC is pretty clear about what statements are "replaced" and "added", see 7.13.2.
640          */
641         refineReplaceChild(context, uses, refine, refinedStatement, refine.getDescription(), null);
642         refineReplaceChild(context, uses, refine, refinedStatement, refine.getReference(), null);
643         refineReplaceChild(context, uses, refine, refinedStatement, refine.getConfig(), null);
644         refineReplaceChildren(context, uses, refine, refinedStatement, refine.getDefaults(),
645                 ALLOWABLE_ELEMENTS_FOR_REFINE_DEFAULT);
646         refineReplaceChild(context, uses, refine, refinedStatement, refine.getMandatory(),
647                 ALLOWABLE_ELEMENTS_FOR_REFINE_MANDATORY);
648         refineReplaceChild(context, uses, refine, refinedStatement, refine.getPresence(),
649                 ALLOWABLE_ELEMENTS_FOR_REFINE_PRESENCE);
650         refineAddChildren(context, uses, refine, refinedStatement, refine.getMusts(), ALLOWABLE_ELEMENTS_FOR_REFINE_MUST);
651         refineReplaceChild(context, uses, refine, refinedStatement, refine.getMinElements(),
652                 ALLOWABLE_ELEMENTS_FOR_REFINE_MIN_MAX_ELEMENTS);
653         refineReplaceChild(context, uses, refine, refinedStatement, refine.getMaxElements(),
654                 ALLOWABLE_ELEMENTS_FOR_REFINE_MIN_MAX_ELEMENTS);
655         refineAddChildren(context, uses, refine, refinedStatement, refine.getIfFeatures(),
656                 ALLOWABLE_ELEMENTS_FOR_REFINE_IF_FEATURE);
657     }
658
659     private static void refineReplaceChild(final ParserExecutionContext context, final YUses uses, final YRefine refine,
660             final AbstractStatement refinedStatement, final AbstractStatement statementUnderRefine,
661             final Set<String> allowableElementsAsRefinedStatement) {
662
663         if (statementUnderRefine == null) {
664             return;
665         }
666
667         refineReplaceChildren(context, uses, refine, refinedStatement, Collections.singletonList(statementUnderRefine),
668                 allowableElementsAsRefinedStatement);
669     }
670
671     private static <T extends AbstractStatement> void refineReplaceChildren(final ParserExecutionContext context,
672             final YUses uses, final YRefine refine, final AbstractStatement refinedStatement,
673             final List<T> statementsUnderRefine, final Set<String> allowableElementsAsRefinedStatement) {
674
675         if (statementsUnderRefine.isEmpty()) {
676             return;
677         }
678
679         if (allowableElementsAsRefinedStatement != null && !allowableElementsAsRefinedStatement.contains(refinedStatement
680                 .getDomElement().getName())) {
681             /*
682              * We only issue a finding on the first occurrence to prevent spamming of findings.
683              */
684             context.addFinding(new Finding(uses.getParentStatement(), ParserFindingType.P124_INVALID_REFINE_TARGET_NODE,
685                     "Statement '" + statementsUnderRefine.get(0)
686                             .getStatementName() + "' cannot be used to refine a '" + refinedStatement
687                                     .getStatementName() + "'."));
688             return;
689         }
690
691         /*
692          * Special case: The 'refine' statement allows for multiple instances of 'default' underneath, but that would
693          * only be allowed if the refined schema node is a leaf-list. Otherwise there can only be a single instance
694          * of default (leaf, choice). Same with the reverse, of course.
695          */
696         if (statementsUnderRefine.get(0).is(CY.STMT_DEFAULT)) {
697             final int nrDefaults = statementsUnderRefine.size();
698
699             if ((refinedStatement.is(CY.STMT_LEAF) || refinedStatement.is(CY.STMT_CHOICE)) && nrDefaults > 1) {
700                 /*
701                  * Note the finding gets issued on the *second* occurrence of 'default' (the first is correct!)
702                  */
703                 context.addFinding(new Finding(uses.getParentStatement(), ParserFindingType.P015_INVALID_SYNTAX_IN_DOCUMENT,
704                         "There can only be a single instance of 'default' under " + refine.getDomElement()
705                                 .getNameValue() + " as the refine's target node is a leaf or choice."));
706                 return;
707             }
708         }
709
710         /*
711          * Replace all existing instances of the statements, and keep a note of it.
712          */
713         for (final AbstractStatement childOfRefinedStatement : refinedStatement.getChildren(statementsUnderRefine.get(0)
714                 .getStatementModuleAndName())) {
715             Helper.addGeneralInfoAppData(refinedStatement, "previous statement " + childOfRefinedStatement.getDomElement()
716                     .getNameValue() + " removed as it has been refined by 'uses'.");
717         }
718
719         for (final AbstractStatement refineWithStatement : statementsUnderRefine) {
720             Helper.addGeneralInfoAppData(refineWithStatement, "refines previous statement(s).");
721         }
722
723         refinedStatement.replaceChildrenWith(statementsUnderRefine);
724     }
725
726     private static <T extends AbstractStatement> void refineAddChildren(final ParserExecutionContext context,
727             final YUses uses, final YRefine refine, final AbstractStatement refinedStatement,
728             final List<T> statementsUnderRefine, final Set<String> allowableElementsAsRefinedStatement) {
729
730         if (statementsUnderRefine.isEmpty()) {
731             return;
732         }
733
734         if (allowableElementsAsRefinedStatement != null && !allowableElementsAsRefinedStatement.contains(refinedStatement
735                 .getDomElement().getName())) {
736             /*
737              * We only issue a finding on the first occurrence to prevent spamming of findings.
738              */
739             context.addFinding(new Finding(uses.getParentStatement(), ParserFindingType.P124_INVALID_REFINE_TARGET_NODE,
740                     "Statement '" + statementsUnderRefine.get(0)
741                             .getStatementName() + "' cannot be used to refine a '" + refinedStatement
742                                     .getStatementName() + "'."));
743             return;
744         }
745
746         /*
747          * Simply add the statements.
748          */
749         for (final AbstractStatement refineWithStatement : statementsUnderRefine) {
750             Helper.addGeneralInfoAppData(refineWithStatement, "refines previous statement(s).");
751         }
752
753         refinedStatement.addChildren(statementsUnderRefine);
754     }
755
756     /**
757      * Returns all 'uses' statements that should be considered. In effect, all 'uses'
758      * statements that (still) sit in the tree and which have not been ruled out to be unresolvable.
759      */
760     @SuppressWarnings("unchecked")
761     private static List<YUses> findUsesToConsider(final Schema schema) {
762         final List<YUses> allUses = (List<YUses>) Helper.findStatementsInSchema(CY.STMT_USES, schema);
763         return allUses.stream().filter(yUses -> !isUsesNotResolvable(yUses)).collect(Collectors.toList());
764     }
765
766     // - - - - - - - - - - - - - - - - - - - - - - - - - - - -
767
768     private static final String USES_RESOLUTION_INFO = "USES_RESOLUTION_INFO";
769
770     private static void addUsesResolutionAppData(final AbstractStatement statement, final String info) {
771         Helper.addAppDataListInfo(statement, USES_RESOLUTION_INFO, info);
772     }
773
774     // - - - - - - - - - - - - - - - - - - - - - - - - - - - -
775
776     private static final String GROUPING_USAGE_COUNT = "GROUPING_USAGE_COUNT";
777
778     private static void incGroupingUsageCount(final YGrouping grouping) {
779         final Integer usageCount = grouping.getCustomAppData(GROUPING_USAGE_COUNT);
780         if (usageCount == null) {
781             grouping.setCustomAppData(GROUPING_USAGE_COUNT, Integer.valueOf(1));
782         } else {
783             grouping.setCustomAppData(GROUPING_USAGE_COUNT, Integer.valueOf(usageCount.intValue() + 1));
784         }
785     }
786
787     private static int getGroupingUsageCount(final YGrouping grouping) {
788         final Integer usageCount = grouping.getCustomAppData(GROUPING_USAGE_COUNT);
789         return usageCount == null ? 0 : usageCount.intValue();
790     }
791
792     // - - - - - - - - - - - - - - - - - - - - - - - - - - - -
793
794     private static final String USES_NOT_RESOLVABLE = "USES_NOT_RESOLVABLE";
795
796     private static void setUsesNotResolvable(final YUses yUses) {
797         yUses.setCustomAppData(USES_NOT_RESOLVABLE);
798     }
799
800     private static boolean isUsesNotResolvable(final YUses yUses) {
801         return yUses.hasCustomAppData(USES_NOT_RESOLVABLE);
802     }
803
804     // - - - - - - - - - - - - - - - - - - - - - - - - - - - -
805
806     private static final String GROUPING_REFERENCE = "GROUPING_REFERENCE";
807
808     private static void addGroupingReference(final AbstractStatement statement, final YGrouping origGrouping) {
809         Helper.addAppDataListInfo(statement, GROUPING_REFERENCE, origGrouping);
810     }
811
812     public static List<YGrouping> getGroupingReference(final AbstractStatement statement) {
813         return Helper.getAppDataListInfo(statement, GROUPING_REFERENCE);
814     }
815 }