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
10 * http://www.apache.org/licenses/LICENSE-2.0
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.
18 * SPDX-License-Identifier: Apache-2.0
19 * ============LICENSE_END=========================================================
21 package org.oran.smo.yangtools.parser.model.resolvers;
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;
29 import java.util.stream.Collectors;
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;
51 * A class that can resolve all usage of "uses" by including the referenced grouping.
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).
56 * @author Mark Hollmann
58 public abstract class UsesResolver {
61 * Resolving means all occurrences of "uses" are replaced by the grouping they are referring to.
63 public static void resolveUsagesOfUses(final ParserExecutionContext context, final Schema schema) {
65 int iterationCount = 10;
66 boolean atLeastOneResolved = true;
68 while (iterationCount > 0 && atLeastOneResolved) {
70 atLeastOneResolved = false;
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.
78 final List<YUses> allUses = findUsesToConsider(schema);
79 for (final YUses uses : allUses) {
81 atLeastOneResolved |= resolveUses(context, schema, uses);
82 } catch (final Exception ex) {
83 /* Swallow and move to next. Best effort here, keep trying other uses. */
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.")));
96 * Done resolving. If some 'uses' are left they could not be resolved because of circular dependencies.
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.")));
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.
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);
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."));
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).
124 private static boolean resolveUses(final ParserExecutionContext context, final Schema schema,
125 final YUses usesStatement) {
127 final String groupingName = usesStatement.getUsesGroupingName();
128 if (groupingName.isEmpty()) {
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.
133 setUsesNotResolvable(usesStatement);
138 * Only now attempt to resolve grouping
140 final YGrouping foundGrouping = Helper.findStatement(context, schema, usesStatement, CY.STMT_GROUPING,
142 if (foundGrouping == null) {
143 setUsesNotResolvable(usesStatement);
144 context.addFinding(new Finding(usesStatement, ParserFindingType.P131_UNRESOLVABLE_GROUPING,
145 "Cannot resolve grouping '" + usesStatement.getUsesGroupingName() + "'."));
150 * Mark the grouping has been used.
152 incGroupingUsageCount(foundGrouping);
155 * Check for nested 'uses' within the 'grouping'. If found, this means that the contents of the
156 * grouping itself must be resolved first.
158 if (usesExistWithinFoundGrouping(context, usesStatement, foundGrouping)) {
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).
168 final YGrouping clonedGrouping = new YGrouping(foundGrouping.getParentStatement(), foundGrouping.getDomElement());
169 clonedGrouping.cloneFrom(foundGrouping);
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);
179 * Handle any status statement under the 'uses' or 'grouping'.
181 handleStatus(usesStatement, clonedGrouping);
184 * Handle 'refine'. These are used to update the contents of the grouping.
186 handleRefines(context, schema, usesStatement, clonedGrouping, foundGrouping);
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.
193 for (final YAugment augment : usesStatement.getAugments()) {
194 handleAugment(context, schema, augment, clonedGrouping, foundGrouping, usesStatement);
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.)
203 for (final YIfFeature origUsesOneIfFeature : usesStatement.getIfFeatures()) {
204 for (final AbstractStatement childOfClonedGrouping : clonedGrouping.getChildStatements()) {
205 final YIfFeature clonedIfFeature = new YIfFeature(childOfClonedGrouping, origUsesOneIfFeature
207 clonedIfFeature.cloneFrom(origUsesOneIfFeature);
208 // No need to explicitly add it - the YIfFeature constructor will add it as child already.
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!
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.
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).
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)
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.
243 final AbstractStatement parentOfUsesStatement = usesStatement.getParentStatement();
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());
256 * Any finally, remove the cloned grouping as it is not needed anymore, and remove the original "uses" statement
258 clonedGrouping.getParentStatement().removeChild(clonedGrouping);
259 usesStatement.getParentStatement().removeChild(usesStatement);
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'.
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
275 private static void handleStatus(final YUses uses, final YGrouping clonedGrouping) {
277 final YStatus statusUnderUses = uses.getStatus();
278 final YStatus statusUnderGrouping = clonedGrouping.getStatus();
280 if (statusUnderUses == null && statusUnderGrouping == null) {
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.
288 YStatus overrideStatus = null;
289 if (statusUnderUses != null && statusUnderGrouping != null) {
291 * We are interested in pushing-down the more severe status, so we need to compare these to
292 * find which one it is.
294 overrideStatus = statusUnderUses.getStatusOrder() > statusUnderGrouping.getStatusOrder() ?
298 } else if (statusUnderUses != null) {
300 overrideStatus = statusUnderUses;
302 } else if (statusUnderGrouping != null) {
304 overrideStatus = statusUnderGrouping;
308 * Now apply the override 'status' to the contents of the 'grouping'.
310 for (final AbstractStatement childOfClonedGrouping : clonedGrouping.getChildStatements()) {
312 if (childOfClonedGrouping.is(CY.STMT_STATUS)) {
316 final YStatus childExplicitStatus = childOfClonedGrouping.getChild(CY.STMT_STATUS);
317 boolean clone = true;
319 if (childExplicitStatus == null) {
321 * There is no 'status' statement under the child, so then we will simply
322 * clone down the parent 'status' in a moment.
324 } else if (childExplicitStatus.getStatusOrder() >= overrideStatus.getStatusOrder()) {
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.
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').
338 * Must clone, so first remove the 'status' statement under the child (if it exists).
340 if (childExplicitStatus != null) {
341 childOfClonedGrouping.removeChild(childExplicitStatus);
344 * Now clone down the parent's (the uses's or grouping's) 'status' into the child.
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.");
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);
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));
361 private static void handleAugment(final ParserExecutionContext context, final Schema schema, final YAugment augment,
362 final YGrouping clonedGrouping, final YGrouping foundGrouping, final YUses usesStatement) {
364 final String augmentTargetNode = augment.getAugmentTargetNode();
365 if (augmentTargetNode.isEmpty() || augmentTargetNode.startsWith("/")) {
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.
374 * First thing check the status of the 'augment'. If it is OBSOLETE nothing gets merged in.
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.");
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."));
393 * Make sure what is being augmented is actually allowed according to the RFC.
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."));
404 * Collect all the statements that will be added to the augment's target node in a moment.
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(
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.
420 if (targetSchemaNodeOfAugment.is(CY.STMT_CHOICE)) {
422 SchemaProcessor.injectCaseForShorthandedStatements(augment);
425 * The direct children of choice may have have changed (case interjected), so we
426 * need to re-fetch these before further processing.
428 statementsToAddAsChildrenOfTargetNode.clear();
429 statementsToAddAsChildrenOfTargetNode.addAll(augment.getChildren(STATEMENTS_UNDER_AUGMENT_TO_HANDLE));
433 * If the augment has a 'when' clause this gets applied to all statements within the 'grouping'.
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.
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();
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.
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);
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.
462 if (augmentStatus.equals(YStatus.DEPRECATED)) {
463 for (final AbstractStatement childToAdd : statementsToAddAsChildrenOfTargetNode) {
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.
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()) {
482 * And now simply move all the statements from under the augment under the target node.
484 targetSchemaNodeOfAugment.addChildren(statementsToAddAsChildrenOfTargetNode);
488 * Check if the found grouping itself contains any "uses", and/or any "uses" that are not resolvable.
490 private static boolean usesExistWithinFoundGrouping(final ParserExecutionContext context, final YUses usesStatement,
491 final YGrouping grouping) {
493 final List<YUses> usesWithinGrouping = new ArrayList<>();
494 Helper.findStatementsInSubtree(grouping, CY.STMT_USES, usesWithinGrouping);
496 boolean groupingContainsNonResolveableUses = false;
497 for (final YUses usesWithin : usesWithinGrouping) {
498 if (isUsesNotResolvable(usesWithin)) {
500 groupingContainsNonResolveableUses = true;
503 * We issue additional findings here to help the user figure out which
504 * nested 'uses' is/are causing the problem.
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() + "."));
513 if (groupingContainsNonResolveableUses) {
515 * If the found grouping has itself a 'uses' that is not resolvable, then this 'uses'
516 * here likewise cannot be resolved.
518 setUsesNotResolvable(usesStatement);
521 return usesWithinGrouping.size() > 0;
524 private static void handleRefines(final ParserExecutionContext context, final Schema schema, final YUses usesStatement,
525 final YGrouping clonedGrouping, final YGrouping foundGrouping) {
527 * We refine the contents of the group, if so required. Note that the 'refine' statement hangs
528 * under the 'uses' statement.
530 for (final YRefine refine : usesStatement.getRefines()) {
532 final String refineTargetNode = refine.getRefineTargetNode();
533 if (refineTargetNode.isEmpty() || refineTargetNode.startsWith("/")) {
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.
541 final AbstractStatement refinedStatement = Helper.findSchemaNode(context, clonedGrouping, refineTargetNode,
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() + "'."));
550 refineYangStatements(context, refinedStatement, usesStatement, refine);
551 refineExtensionStatements(refinedStatement, refine);
555 private static void refineExtensionStatements(final AbstractStatement refinedStatement, final YRefine refine) {
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.
562 * We first collect all extensions that are refined, and keep a note of their "type"
563 * (combination of module name + extension name).
565 final List<AbstractStatement> extensionsUnderRefine = new ArrayList<>();
566 final Set<String> moduleNameAndExtensionNameOfExtensionsUnderRefine = new HashSet<>();
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.");
575 if (extensionsUnderRefine.isEmpty()) {
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'.
583 final List<AbstractStatement> extensionsUnderRefinedStatementToRemove = new ArrayList<>();
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);
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.
598 refinedStatement.removeChildren(extensionsUnderRefinedStatementToRemove);
599 refinedStatement.addChildren(extensionsUnderRefine);
603 * Given an extension, returns a concatenation of the name of the module owning the
604 * extension definition, and the name of the extension.
606 private static String getModuleNameAndExtensionName(final ExtensionStatement extensionStatement) {
608 final String extensionModulePrefix = extensionStatement.getExtensionModulePrefix();
609 final String extensionStatementName = extensionStatement.getExtensionStatementName();
611 final ModuleIdentity owningModuleModuleIdentity = extensionStatement.getPrefixResolver().getModuleForPrefix(
612 extensionModulePrefix);
613 if (owningModuleModuleIdentity == null) {
617 return owningModuleModuleIdentity.getModuleName() + ":::" + extensionStatementName;
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));
632 private static void refineYangStatements(final ParserExecutionContext context, final AbstractStatement refinedStatement,
633 final YUses uses, final YRefine refine) {
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.
639 * The RFC is pretty clear about what statements are "replaced" and "added", see 7.13.2.
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);
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) {
663 if (statementUnderRefine == null) {
667 refineReplaceChildren(context, uses, refine, refinedStatement, Collections.singletonList(statementUnderRefine),
668 allowableElementsAsRefinedStatement);
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) {
675 if (statementsUnderRefine.isEmpty()) {
679 if (allowableElementsAsRefinedStatement != null && !allowableElementsAsRefinedStatement.contains(refinedStatement
680 .getDomElement().getName())) {
682 * We only issue a finding on the first occurrence to prevent spamming of findings.
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() + "'."));
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.
696 if (statementsUnderRefine.get(0).is(CY.STMT_DEFAULT)) {
697 final int nrDefaults = statementsUnderRefine.size();
699 if ((refinedStatement.is(CY.STMT_LEAF) || refinedStatement.is(CY.STMT_CHOICE)) && nrDefaults > 1) {
701 * Note the finding gets issued on the *second* occurrence of 'default' (the first is correct!)
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."));
711 * Replace all existing instances of the statements, and keep a note of it.
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'.");
719 for (final AbstractStatement refineWithStatement : statementsUnderRefine) {
720 Helper.addGeneralInfoAppData(refineWithStatement, "refines previous statement(s).");
723 refinedStatement.replaceChildrenWith(statementsUnderRefine);
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) {
730 if (statementsUnderRefine.isEmpty()) {
734 if (allowableElementsAsRefinedStatement != null && !allowableElementsAsRefinedStatement.contains(refinedStatement
735 .getDomElement().getName())) {
737 * We only issue a finding on the first occurrence to prevent spamming of findings.
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() + "'."));
747 * Simply add the statements.
749 for (final AbstractStatement refineWithStatement : statementsUnderRefine) {
750 Helper.addGeneralInfoAppData(refineWithStatement, "refines previous statement(s).");
753 refinedStatement.addChildren(statementsUnderRefine);
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.
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());
766 // - - - - - - - - - - - - - - - - - - - - - - - - - - - -
768 private static final String USES_RESOLUTION_INFO = "USES_RESOLUTION_INFO";
770 private static void addUsesResolutionAppData(final AbstractStatement statement, final String info) {
771 Helper.addAppDataListInfo(statement, USES_RESOLUTION_INFO, info);
774 // - - - - - - - - - - - - - - - - - - - - - - - - - - - -
776 private static final String GROUPING_USAGE_COUNT = "GROUPING_USAGE_COUNT";
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));
783 grouping.setCustomAppData(GROUPING_USAGE_COUNT, Integer.valueOf(usageCount.intValue() + 1));
787 private static int getGroupingUsageCount(final YGrouping grouping) {
788 final Integer usageCount = grouping.getCustomAppData(GROUPING_USAGE_COUNT);
789 return usageCount == null ? 0 : usageCount.intValue();
792 // - - - - - - - - - - - - - - - - - - - - - - - - - - - -
794 private static final String USES_NOT_RESOLVABLE = "USES_NOT_RESOLVABLE";
796 private static void setUsesNotResolvable(final YUses yUses) {
797 yUses.setCustomAppData(USES_NOT_RESOLVABLE);
800 private static boolean isUsesNotResolvable(final YUses yUses) {
801 return yUses.hasCustomAppData(USES_NOT_RESOLVABLE);
804 // - - - - - - - - - - - - - - - - - - - - - - - - - - - -
806 private static final String GROUPING_REFERENCE = "GROUPING_REFERENCE";
808 private static void addGroupingReference(final AbstractStatement statement, final YGrouping origGrouping) {
809 Helper.addAppDataListInfo(statement, GROUPING_REFERENCE, origGrouping);
812 public static List<YGrouping> getGroupingReference(final AbstractStatement statement) {
813 return Helper.getAppDataListInfo(statement, GROUPING_REFERENCE);