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.schema;
23 import java.util.HashSet;
24 import java.util.List;
26 import java.util.stream.Collectors;
28 import org.oran.smo.yangtools.parser.ParserExecutionContext;
29 import org.oran.smo.yangtools.parser.findings.Finding;
30 import org.oran.smo.yangtools.parser.findings.ParserFindingType;
31 import org.oran.smo.yangtools.parser.model.ConformanceType;
32 import org.oran.smo.yangtools.parser.model.YangModel;
33 import org.oran.smo.yangtools.parser.model.statements.yang.YBelongsTo;
34 import org.oran.smo.yangtools.parser.model.statements.yang.YImport;
35 import org.oran.smo.yangtools.parser.model.statements.yang.YInclude;
36 import org.oran.smo.yangtools.parser.model.statements.yang.YPrefix;
37 import org.oran.smo.yangtools.parser.model.statements.yang.YRevision;
38 import org.oran.smo.yangtools.parser.model.util.StringHelper;
41 * Checks for prefixes, imports, revisions, includes and belongs-to in order to catch
42 * basic issues before further processing is done.
44 * @author Mark Hollmann
46 public abstract class SchemaCheckModuleRelationships {
48 public static void performChecks(final ParserExecutionContext context, final ModuleRegistry moduleRegistry) {
50 checkPrefixesUnique(context, moduleRegistry);
51 checkImportsUnique(context, moduleRegistry);
52 checkImportsSatisfied(context, moduleRegistry);
53 checkIncludesSatisfied(context, moduleRegistry);
54 checkBelongsTosSatisfied(context, moduleRegistry);
55 checkForDuplicateModules(context, moduleRegistry);
56 checkForDuplicateRevisions(context, moduleRegistry);
57 checkImplementsAndImports(context, moduleRegistry);
60 // ==============================================================================================================================================
63 * For each YAM check that all prefixes declared within the YAM are unique. If this is not the case
64 * then there will be ambiguity later on when prefixes are resolved.
66 private static void checkPrefixesUnique(final ParserExecutionContext context, final ModuleRegistry moduleRegistry) {
68 for (final YangModel yangModel : moduleRegistry.getAllYangModels()) {
70 final Set<String> prefixesInYam = new HashSet<>();
72 final YPrefix yPrefix = yangModel.getYangModelRoot().getPrefix();
73 if (yPrefix != null) {
74 prefixesInYam.add(yPrefix.getPrefix());
77 for (final YImport yImport : yangModel.getYangModelRoot().getImports()) {
78 if (yImport.getPrefix() == null) {
79 /* No need for a finding, a invalid syntax finding would have previously issued. */
83 final String importPrefix = yImport.getPrefix().getValue();
84 if (prefixesInYam.contains(importPrefix)) {
85 context.addFinding(new Finding(yImport.getPrefix(), ParserFindingType.P031_PREFIX_NOT_UNIQUE,
86 "Prefix '" + importPrefix + "' not unique in document."));
88 prefixesInYam.add(importPrefix);
93 // ==============================================================================================================================================
96 * For each YAM check that each import is unique. Note that YANG 1.1 allows the same module to be
97 * imported more than once, but then the revision has to be explicitly specified.
99 private static void checkImportsUnique(final ParserExecutionContext context, final ModuleRegistry moduleRegistry) {
101 for (final YangModel yangModel : moduleRegistry.getAllYangModels()) {
103 final Set<String> importedModulesUsingRevision = new HashSet<>();
104 final Set<String> importedModulesWithoutRevision = new HashSet<>();
105 final Set<String> importedModulesNameAndRevision = new HashSet<>();
107 for (final YImport yImport : yangModel.getYangModelRoot().getImports()) {
109 final String importedModuleName = yImport.getImportedModuleName();
110 if (importedModuleName == null) {
111 /* No need for a finding, a invalid syntax finding would have previously issued. */
115 final String importedModuleRevision = yImport.getRevisionDate() == null ?
117 yImport.getRevisionDate().getValue();
119 if (importedModuleRevision == null) {
120 checkOneImportWithoutRevisionIsUnique(context, yImport, importedModuleName,
121 importedModulesUsingRevision, importedModulesWithoutRevision);
123 checkOneImportWithRevisionIsUnique(context, yImport, importedModuleName, importedModuleRevision,
124 importedModulesUsingRevision, importedModulesWithoutRevision, importedModulesNameAndRevision);
130 private static void checkOneImportWithoutRevisionIsUnique(final ParserExecutionContext context, final YImport yImport,
131 final String importedModuleName, final Set<String> importedModulesUsingRevision,
132 final Set<String> importedModulesWithoutRevision) {
135 * Imported without revision, i.e. use "any" version of the module. Check that there is not already
136 * an import with an explicit revision.
138 if (importedModulesUsingRevision.contains(importedModuleName)) {
139 context.addFinding(new Finding(yImport, ParserFindingType.P036_MODULE_IMPORTED_MULTIPLE_TIMES,
140 "Module '" + importedModuleName + "' imported more than once - once without revision, once with explicit revision."));
142 if (importedModulesWithoutRevision.contains(importedModuleName)) {
143 context.addFinding(new Finding(yImport, ParserFindingType.P036_MODULE_IMPORTED_MULTIPLE_TIMES,
144 "Module '" + importedModuleName + "' imported multiple times without revision."));
147 importedModulesWithoutRevision.add(importedModuleName);
150 private static void checkOneImportWithRevisionIsUnique(final ParserExecutionContext context, final YImport yImport,
151 final String importedModuleName, final String importedModuleRevision,
152 final Set<String> importedModulesUsingRevision, final Set<String> importedModulesWithoutRevision,
153 final Set<String> importedModulesNameAndRevision) {
155 * Import with exact revision. Check not already imported without revision, and not doubly-imported.
157 if (importedModulesWithoutRevision.contains(importedModuleName)) {
158 context.addFinding(new Finding(yImport, ParserFindingType.P036_MODULE_IMPORTED_MULTIPLE_TIMES,
159 "Module '" + importedModuleName + "' imported more than once - once without revision, once with explicit revision."));
162 final String key = importedModuleName + "/" + importedModuleRevision;
164 if (importedModulesNameAndRevision.contains(key)) {
165 context.addFinding(new Finding(yImport, ParserFindingType.P036_MODULE_IMPORTED_MULTIPLE_TIMES,
166 "Module '" + key + "' imported more than once."));
169 importedModulesUsingRevision.add(importedModuleName);
170 importedModulesNameAndRevision.add(key);
173 // ==============================================================================================================================================
176 * For each YAM check that all "import" statements can be satisfied,
177 * i.e. the imported YAM is also available.
179 private static void checkImportsSatisfied(final ParserExecutionContext context, final ModuleRegistry moduleRegistry) {
181 for (final YangModel yangModel : moduleRegistry.getAllYangModels()) {
182 for (final YImport yImport : yangModel.getYangModelRoot().getImports()) {
184 final String importedModuleName = yImport.getImportedModuleName();
185 final String importedModuleRevision = yImport.getRevisionDate() == null ?
187 yImport.getRevisionDate().getValue();
189 if (importedModuleName == null) {
190 //No need for a finding, a invalid syntax finding would have previously issued.
194 if (yangModel.getModuleIdentity().getModuleName().equals(importedModuleName)) {
195 context.addFinding(new Finding(yImport, ParserFindingType.P034_UNRESOLVABLE_IMPORT,
196 "Module '" + importedModuleName + "' imports itself."));
198 if (importedModuleRevision == null) {
199 checkOneImportWithoutRevision(context, moduleRegistry, yImport, importedModuleName);
201 checkOneImportWithExplicitRevision(context, moduleRegistry, yImport, importedModuleName,
202 importedModuleRevision);
209 private static void checkOneImportWithoutRevision(final ParserExecutionContext context,
210 final ModuleRegistry moduleRegistry, final YImport yImport, final String importedModuleName) {
212 * The import does not have a revisions. So try to find exactly one module of the given name in the input.
214 final List<YangModel> foundModulesAnyRevision = moduleRegistry.byModuleName(importedModuleName);
215 switch (foundModulesAnyRevision.size()) {
217 context.addFinding(new Finding(yImport, ParserFindingType.P034_UNRESOLVABLE_IMPORT,
218 "Module '" + importedModuleName + "' not found in input."));
222 * OK, the import uses "any" revision and exactly one revision of the module was found in the registry.
227 * This is ambiguous, so issue a finding.
229 context.addFinding(new Finding(yImport, ParserFindingType.P035_AMBIGUOUS_IMPORT,
230 "Module '" + importedModuleName + "' has multiple revisions in the input, but desired exact revision not specified in the 'import' statement."));
234 private static void checkOneImportWithExplicitRevision(final ParserExecutionContext context,
235 final ModuleRegistry moduleRegistry, final YImport yImport, final String importedModuleName,
236 final String importedModuleRevision) {
238 * The import has a revisions. So try to find exactly one module of the given name and given revision in the input.
240 final YangModel foundModulesExactRevision = moduleRegistry.exactMatch(importedModuleName, importedModuleRevision);
241 if (foundModulesExactRevision == null) {
243 * That revision does not exist. Maybe a different revision is in the input?
245 final List<YangModel> foundModulesAnyRevision = moduleRegistry.byModuleName(importedModuleName);
246 if (foundModulesAnyRevision.isEmpty()) {
247 context.addFinding(new Finding(yImport, ParserFindingType.P034_UNRESOLVABLE_IMPORT,
248 "Module '" + importedModuleName + "' with revision '" + importedModuleRevision + "' not found in input."));
250 context.addFinding(new Finding(yImport, ParserFindingType.P034_UNRESOLVABLE_IMPORT,
251 "Module '" + importedModuleName + "' with revision '" + importedModuleRevision + "' not found in input, but a module with that name and revision '" + foundModulesAnyRevision
252 .get(0).getModuleIdentity().getRevision() + "' has been found."));
257 // ==============================================================================================================================================
260 * For each submodule check that the "belongs-to" statement can be satisfied.
262 private static void checkBelongsTosSatisfied(final ParserExecutionContext context,
263 final ModuleRegistry moduleRegistry) {
265 final List<YangModel> subModules = moduleRegistry.getAllYangModels().stream().filter(yangModel -> yangModel
266 .getYangModelRoot().isSubmodule()).collect(Collectors.toList());
268 for (final YangModel subModuleYangModel : subModules) {
270 final YBelongsTo belongsTo = subModuleYangModel.getYangModelRoot().getSubmodule().getBelongsTo();
271 if (belongsTo == null) {
272 /* No need for a finding, would have been previously issued as missing mandatory child statement. */
276 final String belongsToModuleName = belongsTo.getBelongsToModuleName();
277 if (belongsToModuleName == null) {
278 /* No need for a finding, a invalid syntax finding would have previously issued. */
282 final List<YangModel> modules = moduleRegistry.byModuleName(belongsToModuleName);
283 if (modules.isEmpty()) {
284 context.addFinding(new Finding(belongsTo, ParserFindingType.P039_UNRESOLVABLE_BELONGS_TO,
285 "Owning module '" + belongsToModuleName + "' not found in input."));
287 checkBelongsToAgainstModule(context, belongsTo, subModuleYangModel, modules.get(0));
292 private static void checkBelongsToAgainstModule(final ParserExecutionContext context, final YBelongsTo belongsTo,
293 final YangModel subModuleModelInput, final YangModel owningModuleModelInput) {
296 * Check the found module is actually a module, and that it actually references the submodule!
298 if (!owningModuleModelInput.getYangModelRoot().isModule()) {
299 context.addFinding(new Finding(belongsTo, ParserFindingType.P046_NOT_A_MODULE, "'" + belongsTo
300 .getBelongsToModuleName() + "' is not a module."));
302 final String subModuleName = subModuleModelInput.getYangModelRoot().getModuleOrSubModuleName();
303 boolean owningModuleIncludesTheSubmodule = false;
304 for (final YInclude include : owningModuleModelInput.getYangModelRoot().getModule().getIncludes()) {
305 if (subModuleName.equals(include.getIncludedSubModuleName())) {
306 owningModuleIncludesTheSubmodule = true;
310 if (!owningModuleIncludesTheSubmodule) {
311 context.addFinding(new Finding(subModuleModelInput, ParserFindingType.P048_ORPHAN_SUBMODULE,
312 "Owning module '" + owningModuleModelInput.getYangModelRoot()
313 .getModuleOrSubModuleName() + "' does not 'include' this submodule."));
318 // ==============================================================================================================================================
321 * Check up the usage of the Conformance Types.
323 private static void checkImplementsAndImports(final ParserExecutionContext context,
324 final ModuleRegistry moduleRegistry) {
326 int nrYangFilesThatImplement = 0;
328 for (final YangModel yangModel : moduleRegistry.getAllYangModels()) {
330 if (yangModel.getConformanceType() == ConformanceType.IMPLEMENT) {
331 nrYangFilesThatImplement++;
334 if (yangModel.getYangModelRoot().isSubmodule()) {
335 final YBelongsTo belongsTo = yangModel.getYangModelRoot().getSubmodule().getBelongsTo();
336 if (belongsTo == null) {
337 /* No need for a finding, a missing mandatory child statement would have issued already. */
341 final String belongsToModuleName = belongsTo.getBelongsToModuleName();
342 if (belongsToModuleName == null) {
343 /* No need for a finding, an invalid syntax finding would have issued already. */
347 final List<YangModel> modules = moduleRegistry.byModuleName(belongsToModuleName);
348 if (!modules.isEmpty() && yangModel.getConformanceType() != modules.get(0).getConformanceType()) {
349 context.addFinding(new Finding(belongsTo, ParserFindingType.P006_IMPLEMENT_IMPORT_MISMATCH,
350 "Submodule is " + yangModel.getConformanceType().toString() + " but owning module is " + modules
351 .get(0).getConformanceType()));
356 if (nrYangFilesThatImplement == 0) {
357 context.addFinding(new Finding(ParserFindingType.P005_NO_IMPLEMENTS,
358 "Need at least a single module that IMPLEMENTs."));
362 // ==============================================================================================================================================
365 * A module with the exact same name should not be more than once in the input as IMPLEMENT.
367 private static void checkForDuplicateModules(final ParserExecutionContext context,
368 final ModuleRegistry moduleRegistry) {
370 final Set<String> implementingModules = new HashSet<>();
371 final Set<String> importedModules = new HashSet<>();
373 for (final YangModel yangModel : moduleRegistry.getAllYangModels()) {
375 final String moduleName = yangModel.getModuleIdentity().getModuleName();
377 if (yangModel.getConformanceType() == ConformanceType.IMPLEMENT) {
379 if (implementingModules.contains(moduleName)) {
380 context.addFinding(new Finding(yangModel, ParserFindingType.P043_SAME_MODULE_IMPLEMENTS_MORE_THAN_ONCE,
381 "Module '" + moduleName + "' multiple times in the input with conformance type IMPLEMENT."));
383 if (importedModules.contains(moduleName)) {
384 context.addFinding(new Finding(yangModel, ParserFindingType.P044_SAME_MODULE_IMPLEMENTS_AND_IMPORTS,
385 "Module '" + moduleName + "' multiple times in the input, with both conformance types IMPLEMENT and IMPORT."));
387 implementingModules.add(moduleName);
389 } else { // ConformanceType.IMPORT
391 if (implementingModules.contains(moduleName)) {
392 context.addFinding(new Finding(yangModel, ParserFindingType.P044_SAME_MODULE_IMPLEMENTS_AND_IMPORTS,
393 "Module '" + moduleName + "' multiple times in the input, with both conformance types IMPLEMENT and IMPORT."));
395 importedModules.add(moduleName);
400 // ==============================================================================================================================================
403 * A module should not have duplicate revisions.
405 private static void checkForDuplicateRevisions(final ParserExecutionContext context,
406 final ModuleRegistry moduleRegistry) {
408 for (final YangModel yangModel : moduleRegistry.getAllYangModels()) {
410 final Set<String> uniqueRevisions = new HashSet<>();
412 for (final YRevision yRevision : yangModel.getYangModelRoot().getRevisions()) {
413 final String revision = yRevision.getValue();
414 if (uniqueRevisions.contains(revision)) {
415 if (revision.equals(yangModel.getModuleIdentity().getRevision())) {
416 context.addFinding(new Finding(yangModel, ParserFindingType.P050_DUPLICATE_LATEST_REVISION,
417 "Latest revision '" + revision + "' exists more than once in the (sub-)module."));
419 context.addFinding(new Finding(yangModel, ParserFindingType.P049_DUPLICATE_REVISION,
420 "Prior revision '" + revision + "' exists more than once in the (sub-)module."));
423 uniqueRevisions.add(revision);
428 // ==============================================================================================================================================
431 * For each module check that all "include" statements can be satisfied, i.e. are in the input. Also that the submodule
432 * belongs to the module.
434 private static void checkIncludesSatisfied(final ParserExecutionContext context, final ModuleRegistry moduleRegistry) {
436 final List<YangModel> modulesOnly = moduleRegistry.getAllYangModels().stream().filter(yangModel -> yangModel
437 .getYangModelRoot().isModule()).collect(Collectors.toList());
439 for (final YangModel yangModel : modulesOnly) {
441 for (final YInclude yInclude : yangModel.getYangModelRoot().getModule().getIncludes()) {
443 final String includedSubmoduleName = yInclude.getIncludedSubModuleName();
444 if (includedSubmoduleName == null) {
445 // No need for a finding, an invalid syntax finding would have issued already.
449 final String includedSubmoduleRevision = yInclude.getRevisionDate() == null ?
451 yInclude.getRevisionDate().getValue();
453 if (includedSubmoduleRevision == null) {
454 checkOneIncludeWithoutRevision(context, moduleRegistry, yangModel, yInclude, includedSubmoduleName);
456 checkOneIncludeWithExplicitRevision(context, moduleRegistry, yangModel, yInclude, includedSubmoduleName,
457 includedSubmoduleRevision);
463 private static void checkOneIncludeWithoutRevision(final ParserExecutionContext context,
464 final ModuleRegistry moduleRegistry, final YangModel yangModel, final YInclude yInclude,
465 final String includedSubmoduleName) {
467 * The include was done without revision, so try to find "any" revision.
469 final List<YangModel> subModulesFoundAnyRevision = moduleRegistry.byModuleName(includedSubmoduleName);
471 switch (subModulesFoundAnyRevision.size()) {
473 context.addFinding(new Finding(yInclude, ParserFindingType.P037_UNRESOLVABLE_INCLUDE,
474 "Submodule " + includedSubmoduleName + " not found in input."));
477 checkIncludedSubmoduleAgainstModule(context, yInclude, subModulesFoundAnyRevision.get(0), yangModel);
480 context.addFinding(new Finding(yInclude, ParserFindingType.P038_AMBIGUOUS_INCLUDE,
481 "Multiple revisions of submodule " + includedSubmoduleName + " found in input."));
486 private static void checkOneIncludeWithExplicitRevision(final ParserExecutionContext context,
487 final ModuleRegistry moduleRegistry, final YangModel yangModel, final YInclude yInclude,
488 final String includedSubmoduleName, final String includedSubmoduleRevision) {
490 * The include was done with revision, so try to find the exact revision.
492 final String moduleNameAndRevision = StringHelper.getModuleNameAndRevision(includedSubmoduleName,
493 includedSubmoduleRevision);
495 final YangModel subModuleFoundWithExactRevision = moduleRegistry.exactMatch(includedSubmoduleName,
496 includedSubmoduleRevision);
497 if (subModuleFoundWithExactRevision == null) {
498 final List<YangModel> subModulesFoundAnyRevision = moduleRegistry.byModuleName(includedSubmoduleName);
499 if (subModulesFoundAnyRevision.isEmpty()) {
500 context.addFinding(new Finding(yInclude, ParserFindingType.P037_UNRESOLVABLE_INCLUDE,
501 "Submodule " + moduleNameAndRevision + " not found in input."));
504 * Different revision found.
506 context.addFinding(new Finding(yInclude, ParserFindingType.P037_UNRESOLVABLE_INCLUDE,
507 "Submodule " + moduleNameAndRevision + " not found in the input, but a submodule with that name and with revision '" + subModulesFoundAnyRevision
508 .get(0).getModuleIdentity().getRevision() + "' is in the input."));
511 checkIncludedSubmoduleAgainstModule(context, yInclude, subModuleFoundWithExactRevision, yangModel);
515 private static void checkIncludedSubmoduleAgainstModule(final ParserExecutionContext context,
516 final YInclude includeStatement, final YangModel subModuleModelInput, final YangModel moduleModelInput) {
518 final String yangVersionOfSubmodule = subModuleModelInput.getYangModelRoot().getYangVersion();
519 final String yangVersionOfModule = moduleModelInput.getYangModelRoot().getYangVersion();
522 * Make sure the submodule is actually a submodule.
524 if (!subModuleModelInput.getYangModelRoot().isSubmodule()) {
525 context.addFinding(new Finding(includeStatement, ParserFindingType.P045_NOT_A_SUBMODULE,
526 "'" + subModuleModelInput.getYangModelRoot()
527 .getModuleOrSubModuleName() + "' is not a submodule and can therefore not be included."));
531 * YANG versions must match up between module and all its submodules.
533 if (!yangVersionOfModule.equals(yangVersionOfSubmodule)) {
534 context.addFinding(new Finding(includeStatement,
535 ParserFindingType.P041_DIFFERENT_YANG_VERSIONS_BETWEEN_MODULE_AND_SUBMODULES,
536 "The yang versions differ between module and submodule(s)."));
539 * Make sure the submodule actually belongs to the module.
541 final YBelongsTo belongsTo = subModuleModelInput.getYangModelRoot().getSubmodule().getBelongsTo();
542 if (belongsTo == null) {
543 // No need for a finding, a missing mandatory child statement finding would have issued already.
547 final String belongsToModuleName = belongsTo.getBelongsToModuleName();
548 if (belongsToModuleName == null) {
549 // No need for a finding, an invalid syntax finding would have issued already.
553 if (!belongsToModuleName.equals(moduleModelInput.getYangModelRoot().getModuleOrSubModuleName())) {
554 context.addFinding(new Finding(includeStatement, ParserFindingType.P047_SUBMODULE_OWNERSHIP_MISMATCH,
555 "The referenced submodule belongs to '" + belongsToModuleName + "', not this module here."));