/*
* Copyright 2003-2017 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.generator.GenerationFacade;
import jetbrains.mps.project.Solution;
import jetbrains.mps.project.dependency.VisibilityUtil;
import jetbrains.mps.project.validation.ValidationProblem.Severity;
import jetbrains.mps.smodel.BootstrapLanguages;
import jetbrains.mps.smodel.ConceptDeclarationScanner;
import jetbrains.mps.smodel.Language;
import jetbrains.mps.smodel.LanguageAspect;
import jetbrains.mps.smodel.SModelOperations;
import jetbrains.mps.smodel.adapter.structure.MetaAdapterFactory;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.language.SLanguage;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelReference;
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.util.Processor;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.stream.Collectors;
/**
* Checks for Language (source) module.
* Code extracted from ValidationUtil full of statics (which in turn melded distinct validation classes)
* and is revival of LanguageValidator that used to be there till March 18, 2015.
* @author Artem Tikhomirov
* @since 3.4
*/
public class LanguageValidator {
private final Language myLanguage;
private final SRepository myRepository;
private final Processor<ValidationProblem> myProcessor;
// XXX Decide whether to have per-language instance of validator or to pass language as validate() argument.
// Is there anything we can reuse in former case? If we would pass progress monitor, however, there might be too many arguments in the latter.
public LanguageValidator(@NotNull Language language, @NotNull SRepository repository, @NotNull Processor<ValidationProblem> processor) {
myLanguage = language;
myRepository = repository;
myProcessor = processor;
}
public void validate() {
if (!ValidationUtil.validateAbstractModule(myLanguage, myProcessor)) {
// FIXME validation of abstract module has to be in superclass.
return;
}
ArrayDeque<Language> extendedLanguages = new ArrayDeque<>();
Collection<SModuleReference> extendedLanguagesFromStructure = getActuallyExtendedLanguagesFromStructure();
for (SModuleReference el : myLanguage.getExtendedLanguageRefs()) {
if (!extendedLanguagesFromStructure.contains(el) && !BootstrapLanguages.coreLanguageRef().equals(el)) {
// Language.getExtendedLanguageRefs() adds implicitly extended lang.core, we don't need to warn about it.
// Perhaps, lang.core has not be part of getExtendedLanguageRefs(), but added at RT only? Do we need to manifest it at source level? To reference
// core stuff without direct import?
if (!myProcessor.process(new ValidationProblem(Severity.WARNING, String.format("Superficial extended module %s, not referenced from structure aspect", el.getModuleName())))) {
return;
}
}
final SModule resolved = el.resolve(myRepository);
if (resolved instanceof Language) {
extendedLanguages.add((Language) resolved);
continue;
}
if (!myProcessor.process(new ValidationProblem(Severity.ERROR, String.format(resolved == null ? "Can't find extended language: %s" : "Module %s is not a language, can't extend it", el.getModuleName())))) {
return;
}
}
// XXX why it's essential to have behavior aspects in extended languages?
HashSet<Language> visited = new HashSet<>();
if (LanguageAspect.BEHAVIOR.get(myLanguage) == null) {
if (!myProcessor.process(new ValidationProblem(Severity.ERROR, "Behavior aspect is absent"))) {
return;
}
}
visited.add(myLanguage);
while (!extendedLanguages.isEmpty()) {
Language l = extendedLanguages.removeFirst();
if (l == myLanguage) {
if (!myProcessor.process(new ValidationProblem(Severity.WARNING, "Cycle in extended language hierarchy"))) {
return;
}
}
if (!visited.add(l)) {
continue;
}
for (SModuleReference el : l.getExtendedLanguageRefs()) {
final SModule resolved = el.resolve(myRepository);
if (resolved instanceof Language) {
extendedLanguages.add((Language) resolved);
}
}
SModel descriptor = LanguageAspect.BEHAVIOR.get(l);
if (descriptor != null) {
continue;
}
if (!myProcessor.process(new ValidationProblem(Severity.ERROR, "Cannot extend language without behavior aspect: " + l.getModuleName()))) {
return;
}
}
for (SModuleReference mr : myLanguage.getRuntimeModulesReferences()) {
SModule runtimeModule = mr.resolve(myRepository);
if (runtimeModule == null) {
if (!myProcessor.process(new ValidationProblem(Severity.WARNING, String.format("Missing runtime module %s", mr.getModuleName())))) {
return;
}
continue;
}
if (runtimeModule instanceof Solution) {
continue;
}
if (!myProcessor.process(new ValidationProblem(Severity.ERROR, String.format("Runtime module %s is not a solution", runtimeModule)))) {
return;
}
}
for (SModelReference accessory : myLanguage.getModuleDescriptor().getAccessoryModels()) {
//this check is wrong in common as we don't know what the user wants to do with the acc model in build.
//but I'll not delete it until accessories removal just to have some warning on project consistency
SModel accModel = accessory.resolve(myRepository);
if (accModel == null) {
if (!myProcessor.process(new ValidationProblem(Severity.WARNING, String.format("Missing accessory model %s", accessory.getModelName())))) {
return;
}
continue;
}
if (!VisibilityUtil.isVisible(myLanguage, accModel)) {
if (!myProcessor.process(new ValidationProblem(Severity.ERROR, String.format("Accessory model %s is not visible in the module", accessory.getModelName())))) {
return;
}
}
if (accModel.getModule() == myLanguage) {
// Towards clear scenario to use accessory models: we intend them to be design-time auxiliary elements, with no generated code generally.
// Generally, accessory models are written in a language they are part of. However, we can't generate them as there's no generator the moment language is built
// (although it's a bit superficial limitation, due to build dependency chunk being of module size). Generally, nobody could expect to build reasonable code
// from language's accessory model which uses the language, but it doesn't hurt to report an error so that LD get a chance to understand what's going on.
if (GenerationFacade.canGenerate(accModel)) {
SLanguage checkedLanguage = MetaAdapterFactory.getLanguage(myLanguage.getModuleReference());
if (SModelOperations.getAllLanguageImports(accModel).contains(checkedLanguage)) {
// accessory model written in a language it's part of. We could not possibly generate this model.
if (!myProcessor.process(new ValidationProblem(Severity.ERROR, String.format("Accessory model %s uses language it's part of. Mark the model as 'do not generate' to avoid unnecessary bootstrap dependency", accModel.getName())))) {
return;
}
return;
} else {
// just a model to generate some code, not the best way to utilize accessory models
if (!myProcessor.process(new ValidationProblem(Severity.WARNING, String.format("Accessory models are deemed design-time facility and generally shall be marked as 'do not generate': %s", accModel.getName())))) {
return;
}
}
}
}
}
}
/**
* TODO For the time being, we just look at structure model dependencies from other structure models.
* However, we shall look into actual references to tell e.g. aggregation of foreign concept from extension
*/
private Collection<SModuleReference> getActuallyExtendedLanguagesFromStructure() {
SModel structureModel = LanguageAspect.STRUCTURE.get(myLanguage);
if (structureModel == null) {
return Collections.emptyList();
}
// find structure models, their modules is what we truly need to extend
// except for lang.core, which is extended by default.
ConceptDeclarationScanner cds = new ConceptDeclarationScanner().omitLangCore();
return cds.scan(structureModel).getDependencyModules().stream().map(SModule::getModuleReference).collect(Collectors.toList());
}
}