/* * Copyright 2003-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jetbrains.mps.project.validation; import jetbrains.mps.extapi.model.TransientSModel; import jetbrains.mps.extapi.module.TransientSModule; import jetbrains.mps.generator.impl.RuleUtil; import jetbrains.mps.generator.impl.plan.ModelScanner; import jetbrains.mps.persistence.PersistenceVersionAware; import jetbrains.mps.progress.EmptyProgressMonitor; import jetbrains.mps.project.AbstractModule; import jetbrains.mps.project.DevKit; import jetbrains.mps.project.Solution; import jetbrains.mps.project.structure.ProjectStructureModule; import jetbrains.mps.project.structure.modules.Dependency; import jetbrains.mps.project.structure.modules.ModuleDescriptor; import jetbrains.mps.project.structure.modules.mappingpriorities.MappingConfig_AbstractRef; import jetbrains.mps.project.structure.modules.mappingpriorities.MappingPriorityRule; import jetbrains.mps.project.validation.ValidationProblem.Severity; import jetbrains.mps.smodel.FastNodeFinder; import jetbrains.mps.smodel.FastNodeFinderManager; import jetbrains.mps.smodel.Generator; import jetbrains.mps.smodel.InvalidSModel; import jetbrains.mps.smodel.Language; import jetbrains.mps.smodel.LanguageAspect; import jetbrains.mps.smodel.ModelDependencyScanner; import jetbrains.mps.smodel.ModuleRepositoryFacade; import jetbrains.mps.smodel.SModelInternal; import jetbrains.mps.smodel.SModelOperations; import jetbrains.mps.smodel.SModelStereotype; import jetbrains.mps.smodel.adapter.MetaAdapterByDeclaration; import jetbrains.mps.smodel.adapter.structure.language.SLanguageAdapter; import jetbrains.mps.smodel.language.LanguageRegistry; import jetbrains.mps.smodel.language.LanguageRuntime; import jetbrains.mps.smodel.persistence.def.ModelPersistence; import jetbrains.mps.util.CollectionUtil; import jetbrains.mps.util.IterableUtil; import jetbrains.mps.util.Pair; import jetbrains.mps.vfs.IFile; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.openapi.language.SConcept; import org.jetbrains.mps.openapi.language.SContainmentLink; import org.jetbrains.mps.openapi.language.SLanguage; import org.jetbrains.mps.openapi.language.SProperty; import org.jetbrains.mps.openapi.language.SReferenceLink; import org.jetbrains.mps.openapi.model.SModel; import org.jetbrains.mps.openapi.model.SModelReference; import org.jetbrains.mps.openapi.model.SNode; import org.jetbrains.mps.openapi.model.SNodeUtil; import org.jetbrains.mps.openapi.model.SReference; import org.jetbrains.mps.openapi.module.SDependency; import org.jetbrains.mps.openapi.module.SDependencyScope; import org.jetbrains.mps.openapi.module.SModule; import org.jetbrains.mps.openapi.module.SModuleReference; import org.jetbrains.mps.openapi.module.SRepository; import org.jetbrains.mps.openapi.module.SearchScope; import org.jetbrains.mps.openapi.persistence.ModelFactory; import org.jetbrains.mps.openapi.persistence.PersistenceFacade; import org.jetbrains.mps.openapi.util.Processor; import java.io.File; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; public class ValidationUtil { //this processes all nodes and shows the most "common" problem for each node. E.g. if the language of the node is missing, //this won't show "concept missing" error public static void validateModelContent(Iterable<SNode> roots, @NotNull Processor<ValidationProblem> processor) { for (SNode root : roots) { for (SNode node : SNodeUtil.getDescendants(root)) { if (!validateSingleNode(node, processor)) { return; } } } } public static boolean validateSingleNode(SNode node, @NotNull Processor<ValidationProblem> processor) { SLanguage lang = node.getConcept().getLanguage(); if (!lang.isValid()) { LanguageMissingError error = new LanguageMissingError(node, lang, lang.getSourceModule() == null); return processor.process(error); } SConcept concept = node.getConcept(); if (!concept.isValid()) { return processor.process(new ConceptMissingError(node, concept)); } // in case of props, refs, links, list should be better than set List<SProperty> props = IterableUtil.asList(concept.getProperties()); for (SProperty p : node.getProperties()) { if (props.contains(p)) { continue; } if (!processor.process(new ConceptFeatureMissingError(node, p, String.format("Missing property: %s", p.getName())))) { return false; } } List<SContainmentLink> links = IterableUtil.asList(concept.getContainmentLinks()); for (SNode n : node.getChildren()) { SContainmentLink l = n.getContainmentLink(); if (links.contains(l)) { continue; } if (!processor.process(new ConceptFeatureMissingError(node, l, String.format("Missing link: %s", l.getName())))) { return false; } } List<SReferenceLink> refs = IterableUtil.asList(concept.getReferenceLinks()); for (SReference r : node.getReferences()) { if (r.getTargetNodeReference().resolve(node.getModel().getRepository()) == null) { if (!processor.process(new BrokenReferenceError(r))) { return false; } } SReferenceLink l = r.getLink(); if (refs.contains(l)) { continue; } if (!processor.process(new ConceptFeatureMissingError(node, l, String.format("Missing reference: %s", l.getName())))) { return false; } } for (SContainmentLink link : concept.getContainmentLinks()) { Collection<? extends SNode> children = IterableUtil.asCollection(node.getChildren(link)); if (!link.isOptional() && children.isEmpty()) { // TODO this is a hack for constructor declarations if (jetbrains.mps.smodel.SNodeUtil.link_ConstructorDeclaration_returnType.equals(link)) { continue; } // todo this is a hack introduced because we haven't yet done cardinalities checking on generators // todo state behavior on a meeting and remove this hack if (SModelStereotype.isGeneratorModel(node.getModel())) { continue; } if (!processor.process(new ConceptFeatureCardinalityError(node, link, String.format("No child in obligatory role %s", link.getName())))) { return false; } } if (!link.isMultiple() && children.size() > 1) { // todo this is a hack introduced because we haven't yet done cardinalities checking on generators // todo state behavior on a meeting and remove this hack if (SModelStereotype.isGeneratorModel(node.getModel())) { continue; } if (!processor.process(new ConceptFeatureCardinalityError(node, link, String.format("Only one child is allowed in role %s", link.getName())))) { return false; } } } for (SReferenceLink ref : concept.getReferenceLinks()) { if (!ref.isOptional()) { if (node.getReference(ref) == null) { // todo this is a hack introduced because we haven't yet done cardinalities checking on generators // todo state behavior on a meeting and remove this hack if (SModelStereotype.isGeneratorModel(node.getModel())) { continue; } if (!processor.process(new ConceptFeatureCardinalityError(node, ref, String.format("No reference in obligatory role %s", ref.getName())))) { return false; } } } } return true; } public static void validateModel(@NotNull final SModel model, @NotNull Processor<ValidationProblem> processor) { final SRepository repository = model.getRepository(); if (repository != null) { repository.getModelAccess().checkReadAccess(); } if (model instanceof TransientSModel) { return; } if (model.getProblems().iterator().hasNext()) { for (SModel.Problem m : model.getProblems()) { if (!m.isError()) { continue; } if (!processor.process(new ValidationProblem(Severity.ERROR, m.getText()))) { return; } } return; } if (jetbrains.mps.util.SNodeOperations.isModelDisposed(model)) { processor.process(new ValidationProblem(Severity.ERROR, "Model is disposed, validation aborted")); return; // force return } if (model.getModule() == null) { processor.process(new ValidationProblem(Severity.ERROR, "Model is not part of a module, validation aborted")); return; // force return } if (!model.isReadOnly() && model instanceof PersistenceVersionAware) { PersistenceVersionAware pvaModel = (PersistenceVersionAware) model; ModelFactory pvaModelFactory = pvaModel.getModelFactory(); ModelFactory xmlModelFactory = PersistenceFacade.getInstance().getDefaultModelFactory(); if (pvaModelFactory != null && (xmlModelFactory == pvaModelFactory || xmlModelFactory.getFileExtension().equals(pvaModelFactory.getFileExtension()))) { // ModelPersistence.LAST_VERSION doesn't make sense for anything but default xml persistence int persistenceVersion = pvaModel.getPersistenceVersion(); if (persistenceVersion < ModelPersistence.LAST_VERSION) { String msg; if (persistenceVersion == -1) { msg = "Undefined model persistence version, please check model persistence"; } else { msg = String.format("Outdated model persistence is used: %d. Please upgrade model persistence.", persistenceVersion); } if (!processor.process(new ValidationProblem(Severity.ERROR, msg))) { return; } } } } if (repository == null) { processor.process(new ValidationProblem(Severity.WARNING, "Model is detached from a repository, could not process further")); return; // force return } if (model.getReference().resolve(repository) == null) { processor.process(new ValidationProblem(Severity.ERROR, "Model's repository could not resolve the model by reference")); return; // force return } SModule module = model.getModule(); final SearchScope moduleScope = (module instanceof AbstractModule) ? ((AbstractModule) module).getScope() : null; final SModelReference modelToValidateRef = model.getReference(); for (final SModelReference reference : SModelOperations.getImportedModelUIDs(model)) { if (reference.resolve(repository) == null) { final SModuleReference depModule = reference.getModuleReference(); final String msg; if (depModule != null && depModule.resolve(repository) == null) { msg = String.format("Can't find imported model %s due to missing module %s", reference.getName(), depModule.getModuleName()); } else { msg = String.format("Can't find imported model: %s", reference.getName()); } if (!processor.process(new MissingModelError(model, msg, reference))) { return; } } else { if (moduleScope != null && moduleScope.resolve(reference) == null) { String msg = String.format("Imported model %s is not visible in module's scope", reference.getName()); // FIXME could have dedicated problem kind with quick fix to add module import if (!processor.process(new ValidationProblem(Severity.ERROR, msg))) { return; } } } if (reference.equals(modelToValidateRef)) { if (!processor.process(new ImportSelfWarning(model, reference))) { return; } } } LanguageRegistry languageRegistry = LanguageRegistry.getInstance(repository); for (SLanguage lang : ((SModelInternal) model).importedLanguageIds()) { final LanguageRuntime lr = languageRegistry.getLanguage(lang); if (lr == null) { if (!processor.process(new MissingImportedLanguageError(model, lang))) { return; } } else if (!lang.getQualifiedName().equals(lr.getNamespace())) { final String msg = String.format("Stale language import '%s', actual name is '%s'", lang.getQualifiedName(), lr.getNamespace()); if (!processor.process(new ValidationProblem(Severity.WARNING, msg))) { return; } } } for (SLanguage lang : ((SModelInternal) model).getLanguagesEngagedOnGeneration()) { final LanguageRuntime lr = languageRegistry.getLanguage(lang); if (lr == null) { if (!processor.process(new MissingImportedLanguageError(model, lang))) { return; } } else if (!lang.getQualifiedName().equals(lr.getNamespace())) { final String msg = String.format("Stale language import '%s', actual name is '%s'", lang.getQualifiedName(), lr.getNamespace()); if (!processor.process(new ValidationProblem(Severity.WARNING, msg))) { return; } } } Pair<DevKit, SModelReference> devkitAssociatedPlan = null; for (SModuleReference devKit : ((SModelInternal) model).importedDevkits()) { final SModule devkitModule = devKit.resolve(repository); if (devkitModule == null) { if (!processor.process(new ValidationProblem(Severity.ERROR, "Can't find devkit: " + devKit.getModuleName()))) { return; } } else if (devkitModule instanceof DevKit) { final SModelReference plan = ((DevKit) devkitModule).getModuleDescriptor().getAssociatedGenPlan(); if (plan != null) { if (devkitAssociatedPlan == null) { devkitAssociatedPlan = new Pair<DevKit, SModelReference>((DevKit) devkitModule, plan); } else { String m = String.format("Both devkit %s and %s supply generation plan, ", devkitModule.getModuleName(), devkitAssociatedPlan.o1.getModuleName()); processor.process(new ValidationProblem(Severity.ERROR, m)); } } } } if (LanguageAspect.STRUCTURE.is(model)) { new StructureAspectCheck(model, processor::process).check(new EmptyProgressMonitor()); } if (SModelStereotype.isGeneratorModel(model)) { checkGeneratorModel(model, null, processor); } } public static void validateModule(final SModule m, Processor<ValidationProblem> processor) { if (m instanceof TransientSModule || m instanceof ProjectStructureModule) { return; } if (m instanceof DevKit) { validateDevkit((DevKit) m, processor); } else if (m instanceof Language) { new LanguageValidator((Language) m, m.getRepository(), processor).validate(); } else if (m instanceof Generator) { validateGenerator((Generator) m, processor); } else if (m instanceof Solution) { validateAbstractModule((Solution) m, processor); } else { throw new IllegalArgumentException("Unknown module for validation: " + m.getClass()); } } private static void validateDevkit(final DevKit dk, Processor<ValidationProblem> processor) { Throwable loadException = dk.getModuleDescriptor().getLoadException(); if (loadException != null) { if (!processor.process(new ValidationProblem(Severity.ERROR, "Couldn't load devkit: " + loadException.getMessage()))) { return; } return; } for (SModuleReference extDevkit : dk.getModuleDescriptor().getExtendedDevkits()) { if (ModuleRepositoryFacade.getInstance().getModule(extDevkit) != null) { continue; } if (!processor.process(new ValidationProblem(Severity.ERROR, "Can't find extended devkit: " + extDevkit.getModuleName()))) { return; } } for (SModuleReference expLang : dk.getModuleDescriptor().getExportedLanguages()) { if (ModuleRepositoryFacade.getInstance().getModule(expLang) != null) { continue; } if (!processor.process(new ValidationProblem(Severity.ERROR, "Can't find exported language: " + expLang.getModuleName()))) { return; } } for (SModuleReference expSol : dk.getModuleDescriptor().getExportedSolutions()) { if (ModuleRepositoryFacade.getInstance().getModule(expSol) != null) { continue; } if (!processor.process(new ValidationProblem(Severity.ERROR, "Can't find exported language: " + expSol.getModuleName()))) { return; } } } private static void validateGenerator(final Generator generator, Processor<ValidationProblem> processor) { if (!validateAbstractModule(generator, processor)) { return; } final SRepository repository = generator.getRepository(); for (SModuleReference gen : generator.getModuleDescriptor().getDepGenerators()) { if (gen.resolve(repository) != null) { continue; } if (!processor.process(new ValidationProblem(Severity.ERROR, "Can't find generator dependency: " + gen.getModuleName()))) { return; } } if (!checkPriorityRules(generator, processor)) { return; } Set<SLanguage> usedLanguages = new HashSet<SLanguage>(); ModelDependencyScanner depScan = new ModelDependencyScanner(); depScan.crossModelReferences(true).usedLanguages(false); // dependencies check is meaningless if we didn't collect cross-generator references. // XXX not sure decision to ignore utility models is right, though. boolean anyGeneratorModelNotLoaded = false; for (SModel model : generator.getModels()) { // Note: the following method invocation traverses the whole model. // For performance reasons, we perform these checks only for loaded models. // FIXME this brings incorrect results for explicit Check Module actions (where one would expect thorough check) // shall consider different execution models for the validator (in addition to description object instead of String) if (!model.isLoaded()) { anyGeneratorModelNotLoaded |= SModelStereotype.isGeneratorModel(model); continue; } if (SModelStereotype.isGeneratorModel(model) && !checkGeneratorModel(model, usedLanguages, processor)) { return; } depScan.walk(model); } if (!warnMissingTargetLangRuntime(generator, usedLanguages, processor)) { return; } if (!anyGeneratorModelNotLoaded) { if (!warnStrictGeneratorDependencies(generator, depScan, processor)) { return; } if (generator.getOwnTemplateModels().isEmpty()) { // quickFix possible, remove module processor.process(new ValidationProblem(Severity.WARNING, "No template models in the generator, generator is no-op")); } } } private static boolean checkPriorityRules(Generator generator, Processor<ValidationProblem> processor) { boolean goOn = true; for (MappingPriorityRule mpr : generator.getModuleDescriptor().getPriorityRules()) { if (!goOn) { return false; } MappingConfig_AbstractRef left = mpr.getLeft(); MappingConfig_AbstractRef right = mpr.getRight(); if (left == null || right == null) { final String s = mpr.asString(generator.getRepository()); goOn = processor.process(new ValidationProblem(Severity.ERROR, String.format("Broken priority rule: %s", s))); continue; } if (left.isIncomplete()) { final String s = mpr.asString(generator.getRepository()); goOn = processor.process(new ValidationProblem(Severity.ERROR, String.format("Left-hand side of rule %s is incomplete", s))); } if (right.isIncomplete()) { final String s = mpr.asString(generator.getRepository()); goOn &= processor.process(new ValidationProblem(Severity.ERROR, String.format("Right-hand side of rule %s is incomplete", s))); } } return true; } //returns true to continue analysing, false to stop private static boolean warnStrictGeneratorDependencies(Generator generator, ModelDependencyScanner dependencies, Processor<ValidationProblem> processor) { HashSet<SModule> seen = new HashSet<SModule>(); for (SDependency dep : generator.getDeclaredDependencies()) { SModule depTarget = dep.getTarget(); if (depTarget == null || seen.contains(depTarget) || (dep.getScope() != SDependencyScope.EXTENDS && dep.getScope() != SDependencyScope.DEFAULT)) { continue; } if (!(depTarget instanceof Generator)) { continue; } HashSet<SModelReference> otherGeneratorModels = new HashSet<SModelReference>(); for (SModel m : depTarget.getModels()) { otherGeneratorModels.add(m.getReference()); } final Language otherGenLanguage = ((Generator) depTarget).getSourceLanguage(); for (SModel m : (otherGenLanguage == null ? Collections.<SModel>emptySet() : otherGenLanguage.getModels())) { otherGeneratorModels.add(m.getReference()); } seen.add(depTarget); if (CollectionUtil.intersects(dependencies.getCrossModelReferences(), otherGeneratorModels)) { continue; } // models of the dep.target are not referenced, likely superfluous dependency. String msg = "Superfluous dependency to generator " + depTarget.getModuleName() + ", no generator template nor its source language's node is in use"; if (!processor.process(new ValidationProblem(Severity.WARNING, msg))) { return false; } } return true; } //returns true to continue analysing, false to stop private static boolean warnMissingTargetLangRuntime(Generator generator, Set<SLanguage> usedLanguages, Processor<ValidationProblem> processor) { Language sourceLanguage = generator.getSourceLanguage(); SLanguage sourceLanguageDeployed = MetaAdapterByDeclaration.getLanguage(sourceLanguage); usedLanguages.remove(sourceLanguageDeployed); if (usedLanguages.isEmpty()) { return true; } final HashSet<SModuleReference> compileTimeDeps = new HashSet<SModuleReference>(); /* * Shall not use GMDM(module).getModules(COMPILE), as it gives a set of classpath dependencies required to build given module, NOT cp dependencies to build * modules using this language! E.g. see https://youtrack.jetbrains.com/issue/MPS-22857 * Here we'd like to figure out if there's a model M written in sourceLanguage L, whether it's generated code would receive all runtime modules * of languages L's generator would produce. */ compileTimeDeps.addAll(IterableUtil.asCollection(sourceLanguageDeployed.getLanguageRuntimes())); for (SLanguage lang : usedLanguages) { Collection<SModuleReference> langRuntimes = IterableUtil.asCollection(lang.getLanguageRuntimes()); if (langRuntimes.isEmpty()) { continue; } // language we generate into (target) has runtime, check we've got appropriate dependency if (compileTimeDeps.containsAll(langRuntimes)) { continue; } String m = String.format("%s shall specify language %s as generation target to include its runtime modules into compilation", sourceLanguage, lang); if (!processor.process(new ValidationProblem(Severity.WARNING, m))) { return false; } } return true; } //returns true to continue analysing, false to stop // package-local until extracted into AbstractModuleValidator superclass to get subclassed by validators of particular module kind /*package*/ static boolean validateAbstractModule(final AbstractModule module, Processor<ValidationProblem> processor) { Throwable loadException = module.getModuleDescriptor().getLoadException(); if (loadException != null) { return processor.process(new ValidationProblem(Severity.ERROR, "Couldn't load module: " + loadException.getMessage())); } SRepository repository = module.getRepository(); LanguageRegistry languageRegistry = LanguageRegistry.getInstance(repository); for (SLanguage lang : module.getUsedLanguages()) { if (languageRegistry.getLanguage(lang) != null) { continue; } if (!processor.process(new ValidationProblem(Severity.ERROR, String.format("Used language %s is not deployed", lang.getQualifiedName())))) { return false; } } ModuleDescriptor descriptor = module.getModuleDescriptor(); for (Dependency dep : descriptor.getDependencies()) { SModuleReference moduleRef = dep.getModuleRef(); if (moduleRef.resolve(repository) != null) { continue; } if (!processor.process(new ValidationProblem(Severity.ERROR, "Can't find dependency: " + moduleRef.getModuleName()))) { return false; } } for (SModuleReference reference : descriptor.getUsedDevkits()) { if (reference.resolve(repository) != null) { continue; } if (!processor.process(new ValidationProblem(Severity.ERROR, "Can't find used devkit: " + reference.getModuleName()))) { return false; } } if (descriptor.getSourcePaths() != null && !module.isPackaged()) { for (String sourcePath : descriptor.getSourcePaths()) { IFile file = module.getFileSystem().getFile(sourcePath); if (!file.exists()) { if (!processor.process(new ValidationProblem(Severity.ERROR, "Can't find source path: " + sourcePath))) { return false; } } } } if (descriptor.getAdditionalJavaStubPaths() != null) { for (String path : descriptor.getAdditionalJavaStubPaths()) { IFile file = module.getFileSystem().getFile(path); if (!file.exists()) { String msg = (new File(path).exists() ? "Idea VFS is not up-to-date. " : "") + "Can't find library: " + path; if (!processor.process(new ValidationProblem(Severity.ERROR, msg))) { return false; } } } } return true; } // pre: SModelStereotype.isGeneratorModel(model) == true private static boolean checkGeneratorModel(SModel model, @Nullable Collection<SLanguage> usedLanguages, Processor<ValidationProblem> processor) { ModelScanner ms = new ModelScanner().scan(model); if (usedLanguages != null) { usedLanguages.addAll(ms.getTargetLanguages()); } if (ms.getTargetLanguages().isEmpty() && ms.getQueryLanguages().isEmpty()) { FastNodeFinder fnf = FastNodeFinderManager.get(model); boolean noModifyRules = fnf.getNodes(RuleUtil.concept_AbandonInput_RuleConsequence, false).isEmpty(); noModifyRules = noModifyRules && fnf.getNodes(RuleUtil.concept_DropRootRule, false).isEmpty(); noModifyRules = noModifyRules && fnf.getNodes(RuleUtil.concept_DropAttributeRule, false).isEmpty(); if (noModifyRules) { String m = String.format("Generator Model %s got no target nor query language. No rules to modify an input. Is it empty?", model.getModelName()); // TODO quickFix possible, remove model return processor.process(new ValidationProblem(Severity.WARNING, m)); } } return true; } }