From 2d36069fbd65371afe88e7194986512841211a70 Mon Sep 17 00:00:00 2001 From: mmews Date: Wed, 17 Jul 2024 11:36:54 +0200 Subject: [PATCH] migrate --- .../validators/N4JSVariableValidator.java | 121 ++++++++ .../validators/N4JSVariableValidator.xtend | 109 ------- .../RuntimeDependencyValidator.java | 277 ++++++++++++++++++ .../RuntimeDependencyValidator.xtend | 243 --------------- 4 files changed, 398 insertions(+), 352 deletions(-) create mode 100644 plugins/org.eclipse.n4js/src/org/eclipse/n4js/validation/validators/N4JSVariableValidator.java delete mode 100644 plugins/org.eclipse.n4js/src/org/eclipse/n4js/validation/validators/N4JSVariableValidator.xtend create mode 100644 plugins/org.eclipse.n4js/src/org/eclipse/n4js/validation/validators/RuntimeDependencyValidator.java delete mode 100644 plugins/org.eclipse.n4js/src/org/eclipse/n4js/validation/validators/RuntimeDependencyValidator.xtend diff --git a/plugins/org.eclipse.n4js/src/org/eclipse/n4js/validation/validators/N4JSVariableValidator.java b/plugins/org.eclipse.n4js/src/org/eclipse/n4js/validation/validators/N4JSVariableValidator.java new file mode 100644 index 0000000000..59438b3c76 --- /dev/null +++ b/plugins/org.eclipse.n4js/src/org/eclipse/n4js/validation/validators/N4JSVariableValidator.java @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2016 NumberFour AG. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * NumberFour AG - Initial API and implementation + */ +package org.eclipse.n4js.validation.validators; + +import static org.eclipse.n4js.validation.IssueCodes.AST_VAR_DECL_RECURSIVE; +import static org.eclipse.n4js.validation.IssueCodes.CFG_LOCAL_VAR_UNUSED; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.n4js.n4JS.FunctionExpression; +import org.eclipse.n4js.n4JS.IdentifierRef; +import org.eclipse.n4js.n4JS.N4ClassExpression; +import org.eclipse.n4js.n4JS.NewExpression; +import org.eclipse.n4js.n4JS.ParameterizedCallExpression; +import org.eclipse.n4js.n4JS.ParenExpression; +import org.eclipse.n4js.n4JS.VariableDeclaration; +import org.eclipse.n4js.postprocessing.ASTMetaInfoUtils; +import org.eclipse.n4js.ts.types.TVariable; +import org.eclipse.n4js.validation.AbstractN4JSDeclarativeValidator; +import org.eclipse.xtext.validation.Check; +import org.eclipse.xtext.validation.EValidatorRegistrar; + +/** + * Validations for variable declarations and variables. + */ +@SuppressWarnings("javadoc") +public class N4JSVariableValidator extends AbstractN4JSDeclarativeValidator { + + /** + * NEEDED + * + * when removed check methods will be called twice once by N4JSValidator, and once by + * AbstractDeclarativeN4JSValidator + */ + @Override + public void register(EValidatorRegistrar registrar) { + // nop + } + + @Check + public void checkVariableDeclaration(VariableDeclaration varDecl) { + if (varDecl.getExpression() != null) { + // TODO: GH-331, remove cases where also the 'UsedBeforeDeclared' issue is raised + List refs = collectIdentifierRefsTo(varDecl.getExpression(), varDecl, new ArrayList<>()); + for (IdentifierRef currRef : refs) { + addIssue(currRef, null, AST_VAR_DECL_RECURSIVE.toIssueItem(varDecl.getName())); + } + } + } + + @Check + public void checkUnusedVariables(VariableDeclaration varDecl) { + if (varDecl.isDirectlyExported()) { + return; + } + + TVariable tVariable = varDecl.getDefinedVariable(); + if (tVariable != null && ASTMetaInfoUtils.getLocalVariableReferences(tVariable).isEmpty()) { + // deactivated during tests + addIssue(varDecl, findNameFeature(varDecl).getValue(), CFG_LOCAL_VAR_UNUSED.toIssueItem(varDecl.getName())); + } + } + + private static List collectIdentifierRefsTo(EObject astNode, VariableDeclaration varDecl, + List result) { + // exception cases: + // do not inspect class expressions and function expressions because contained code will be executed later + // and therefore self-reference is ok UNLESS the class is immediately instantiated / the function immediately + // called + if (astNode instanceof N4ClassExpression && !isInstantiatedOrCalled(astNode)) + return result; // add nothing + if (astNode instanceof FunctionExpression && !isInstantiatedOrCalled(astNode)) + return result; // add nothing + + // standard cases: + TVariable targetForReferencesToVarDecl = varDecl.getDefinedVariable(); + if (astNode instanceof IdentifierRef) { + IdentifierRef idRef = (IdentifierRef) astNode; + if (idRef.getId() == targetForReferencesToVarDecl) { + result.add(idRef); + } + } + for (EObject child : astNode.eContents()) { + collectIdentifierRefsTo(child, varDecl, result); + } + return result; + } + + public static boolean containsIdentifierRefsTo(EObject astNode, VariableDeclaration varDecl) { + return !collectIdentifierRefsTo(astNode, varDecl, new ArrayList<>()).isEmpty(); + } + + /** + * Tells if given astNode is an expression serving as target to a new or call expression. This provides only a + * heuristic; might produce false negatives (but no false positives). + */ + private static boolean isInstantiatedOrCalled(EObject astNode) { + EObject curr = astNode; + while (curr.eContainer() instanceof ParenExpression) { + curr = curr.eContainer(); + } + EObject parent = curr.eContainer(); + if (parent instanceof NewExpression) { + return ((NewExpression) parent).getCallee() == curr; + } + if (parent instanceof ParameterizedCallExpression) { + return ((ParameterizedCallExpression) parent).getTarget() == curr; + } + return false; + } +} diff --git a/plugins/org.eclipse.n4js/src/org/eclipse/n4js/validation/validators/N4JSVariableValidator.xtend b/plugins/org.eclipse.n4js/src/org/eclipse/n4js/validation/validators/N4JSVariableValidator.xtend deleted file mode 100644 index 15134e5cac..0000000000 --- a/plugins/org.eclipse.n4js/src/org/eclipse/n4js/validation/validators/N4JSVariableValidator.xtend +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Copyright (c) 2016 NumberFour AG. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - * - * Contributors: - * NumberFour AG - Initial API and implementation - */ -package org.eclipse.n4js.validation.validators - -import java.util.List -import org.eclipse.emf.ecore.EObject -import org.eclipse.n4js.n4JS.FunctionExpression -import org.eclipse.n4js.n4JS.IdentifierRef -import org.eclipse.n4js.n4JS.N4ClassExpression -import org.eclipse.n4js.n4JS.NewExpression -import org.eclipse.n4js.n4JS.ParameterizedCallExpression -import org.eclipse.n4js.n4JS.ParenExpression -import org.eclipse.n4js.n4JS.VariableDeclaration -import org.eclipse.n4js.postprocessing.ASTMetaInfoUtils -import org.eclipse.n4js.validation.AbstractN4JSDeclarativeValidator -import org.eclipse.xtext.validation.Check -import org.eclipse.xtext.validation.EValidatorRegistrar - -import static org.eclipse.n4js.validation.IssueCodes.* - -/** - * Validations for variable declarations and variables. - */ -class N4JSVariableValidator extends AbstractN4JSDeclarativeValidator { - - /** - * NEEDED - * - * when removed check methods will be called twice once by N4JSValidator, and once by - * AbstractDeclarativeN4JSValidator - */ - override register(EValidatorRegistrar registrar) { - // nop - } - - - @Check - def void checkVariableDeclaration(VariableDeclaration varDecl) { - if(varDecl.expression!==null) { - // TODO: GH-331, remove cases where also the 'UsedBeforeDeclared' issue is raised - val refs = varDecl.expression.collectIdentifierRefsTo(varDecl,newArrayList); - for(IdentifierRef currRef : refs) { - addIssue(currRef, null, AST_VAR_DECL_RECURSIVE.toIssueItem(varDecl.name)); - } - } - } - - @Check - def void checkUnusedVariables(VariableDeclaration varDecl) { - if (varDecl.directlyExported) { - return; - } - - val tVariable = varDecl.definedVariable; - if (tVariable !== null && ASTMetaInfoUtils.getLocalVariableReferences(tVariable).empty) { - addIssue(varDecl, findNameFeature(varDecl).value, CFG_LOCAL_VAR_UNUSED.toIssueItem(varDecl.name)); // deactivated during tests - } - } - - - def private static List collectIdentifierRefsTo(EObject astNode, VariableDeclaration varDecl, List result) { - // exception cases: - // do not inspect class expressions and function expressions because contained code will be executed later - // and therefore self-reference is ok UNLESS the class is immediately instantiated / the function immediately called - if(astNode instanceof N4ClassExpression && !astNode.isInstantiatedOrCalled) - return result; // add nothing - if(astNode instanceof FunctionExpression && !astNode.isInstantiatedOrCalled) - return result; // add nothing - - // standard cases: - val targetForReferencesToVarDecl = varDecl.definedVariable; - if(astNode instanceof IdentifierRef) { - if(astNode.id===targetForReferencesToVarDecl) { - result.add(astNode); - } - } - for(child : astNode.eContents) { - child.collectIdentifierRefsTo(varDecl,result); - } - return result; - } - def public static boolean containsIdentifierRefsTo(EObject astNode, VariableDeclaration varDecl) { - return !astNode.collectIdentifierRefsTo(varDecl,newArrayList).empty - } - /** - * Tells if given astNode is an expression serving as target to a new or call expression. This provides only a - * heuristic; might produce false negatives (but no false positives). - */ - def private static boolean isInstantiatedOrCalled(EObject astNode) { - var curr = astNode; - while(curr.eContainer instanceof ParenExpression) { - curr = curr.eContainer; - } - val parent = curr.eContainer; - return switch(parent) { - NewExpression: parent.callee===curr - ParameterizedCallExpression: parent.target===curr - default: false - } - } -} diff --git a/plugins/org.eclipse.n4js/src/org/eclipse/n4js/validation/validators/RuntimeDependencyValidator.java b/plugins/org.eclipse.n4js/src/org/eclipse/n4js/validation/validators/RuntimeDependencyValidator.java new file mode 100644 index 0000000000..5aa70990ee --- /dev/null +++ b/plugins/org.eclipse.n4js/src/org/eclipse/n4js/validation/validators/RuntimeDependencyValidator.java @@ -0,0 +1,277 @@ +/** + * Copyright (c) 2020 NumberFour AG. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * NumberFour AG - Initial API and implementation + */ +package org.eclipse.n4js.validation.validators; + +import static org.eclipse.n4js.utils.N4JSLanguageUtils.hasRuntimeRepresentation; +import static org.eclipse.n4js.validation.IssueCodes.LTD_ILLEGAL_LOADTIME_REFERENCE; +import static org.eclipse.n4js.validation.IssueCodes.LTD_LOADTIME_DEPENDENCY_CONFLICT; +import static org.eclipse.n4js.validation.IssueCodes.LTD_LOADTIME_DEPENDENCY_CYCLE; +import static org.eclipse.n4js.validation.IssueCodes.LTD_REFERENCE_TO_LOADTIME_DEPENDENCY_TARGET; +import static org.eclipse.xtext.xbase.lib.IterableExtensions.filter; +import static org.eclipse.xtext.xbase.lib.IterableExtensions.map; +import static org.eclipse.xtext.xbase.lib.IterableExtensions.sortWith; +import static org.eclipse.xtext.xbase.lib.IterableExtensions.toList; + +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.eclipse.emf.common.util.EList; +import org.eclipse.n4js.n4JS.IdentifierRef; +import org.eclipse.n4js.n4JS.ModuleRef; +import org.eclipse.n4js.n4JS.N4JSASTUtils; +import org.eclipse.n4js.n4JS.N4JSPackage; +import org.eclipse.n4js.n4JS.Script; +import org.eclipse.n4js.postprocessing.RuntimeDependencyProcessor; +import org.eclipse.n4js.resource.N4JSResource; +import org.eclipse.n4js.ts.types.IdentifiableElement; +import org.eclipse.n4js.ts.types.TModule; +import org.eclipse.n4js.utils.Strings; +import org.eclipse.n4js.validation.AbstractN4JSDeclarativeValidator; +import org.eclipse.xtext.EcoreUtil2; +import org.eclipse.xtext.validation.Check; +import org.eclipse.xtext.validation.EValidatorRegistrar; + +import com.google.common.collect.Iterables; + +/** + * Validations related to runtime dependencies (in particular, illegal load-time dependency cycles). This class relies + * on the TModule-information computed in {@link RuntimeDependencyProcessor}. + */ +public class RuntimeDependencyValidator extends AbstractN4JSDeclarativeValidator { + + private static final String INDENT = " "; + + /** + * NEEDED + * + * when removed check methods will be called twice once by N4JSValidator, and once by + * AbstractDeclarativeN4JSValidator + */ + @Override + public void register(EValidatorRegistrar registrar) { + // nop + } + + /** + * Req. GH-1678, Constraint 4 + */ + @Check + public void checkIllegalLoadtimeReference(IdentifierRef idRef) { + if (!N4JSASTUtils.isTopLevelCode(idRef)) { + return; // only interested in top-level == load-time references here! + } + if (idRef.eContainingFeature() == N4JSPackage.Literals.NAMED_EXPORT_SPECIFIER__EXPORTED_ELEMENT) { + return; // re-exports are not harmful + } + TModule targetModule = getTargetModule(idRef); + if (targetModule == null) { + return; + } + TModule containingModule = ((N4JSResource) idRef.eResource()).getModule(); + boolean hasCycle = (targetModule == containingModule && !containingModule.getCyclicModulesRuntime().isEmpty()) + || (targetModule != containingModule + && containingModule.getCyclicModulesRuntime().contains(targetModule)); + if (hasCycle) { + String cycleStr = "\n" + dependencyCycleToString(containingModule, false, INDENT); + addIssue(idRef, LTD_ILLEGAL_LOADTIME_REFERENCE.toIssueItem(cycleStr)); + } + } + + /***/ + @Check + public void checkIllegalModuleRefToLTDTarget(Script script) { + TModule containingModule = script.getModule(); + List refsToOtherModules = toList( + filter(filter(script.getScriptElements(), ModuleRef.class), mref -> mref.isReferringToOtherModule())); + + Set modulesInHealedCycles = new HashSet<>(); + for (ModuleRef moduleRef : refsToOtherModules) { + if (holdsNotAnIllegalModuleRefWithinLoadtimeCycle(containingModule, moduleRef) + && holdsNotAnIllegalModuleRefToLTDTarget(containingModule, moduleRef, modulesInHealedCycles)) { + + if (moduleRef.isRetainedAtRuntime()) { + // we have a valid reference to another module that will be retained at runtime + // --> it may contribute to healing later moduleRefs: + TModule targetModule = moduleRef.getModule(); + if (!targetModule.getCyclicModulesRuntime().isEmpty()) { + modulesInHealedCycles.add(targetModule); + modulesInHealedCycles.addAll(targetModule.getCyclicModulesRuntime()); + } + } + } + } + } + + /** + * Req. GH-1678, Constraint 1 + *

+ * This method will show an error on all module references (i.e. imports/exports with 'from "..."') that constitute + * the cycle. + */ + private boolean holdsNotAnIllegalModuleRefWithinLoadtimeCycle(TModule containingModule, ModuleRef moduleRef) { + TModule targetModule = moduleRef.getModule(); + if (containingModule.getCyclicModulesLoadtimeForInheritance().contains(targetModule)) { + String cycleStr = "\n" + dependencyCycleToString(targetModule, true, INDENT); + addIssue(moduleRef, N4JSPackage.eINSTANCE.getModuleRef_Module(), + LTD_LOADTIME_DEPENDENCY_CYCLE.toIssueItem(cycleStr)); + return false; + } + return true; + } + + /** + * Req. GH-1678, Constraints 2 and 3. + */ + private boolean holdsNotAnIllegalModuleRefToLTDTarget(TModule containingModule, ModuleRef moduleRef, + Set modulesInHealedCycles) { + if (!moduleRef.isRetainedAtRuntime()) { + return true; // only interested in imports/exports that are retained at runtime + } + + TModule targetModule = moduleRef.getModule(); + if (!isLTDTarget(targetModule)) { + return true; // only interested in references to LTD targets + } + if (!targetModule.getCyclicModulesLoadtimeForInheritance().isEmpty()) { + // target is part of a load-time dependency cycle, so other errors are shown already there + return true; // avoid unnecessary follow-up errors + } + + EList ltdSources = targetModule.getRuntimeCyclicLoadtimeDependents(); // never includes the containing + // module itself! + boolean isSingletonLTDTarget = ltdSources.size() == 1 + && !containingModule.equals(Iterables.getFirst(ltdSources, null)); // excludes the one valid reference + // to an LTD target + boolean isMultiLTDTarget = ltdSources.size() > 1; + + if (isSingletonLTDTarget || isMultiLTDTarget) { + if (!modulesInHealedCycles.contains(targetModule)) { + // illegal reference to an LTD target + boolean withinSameDependencyCycleCluster = targetModule.getCyclicModulesRuntime() + .contains(containingModule); + if (withinSameDependencyCycleCluster) { + // ERROR: referring to a multi-LTD-target from within the dependency cycle cluster (Req. GH-1678, + // Constraint 2) + // --> load-time dependency conflict + String otherLTDSources = otherLTDSourcesToString(containingModule, targetModule); + String cycleStr = "\nContaining runtime dependency cycle cluster:\n" + + dependencyCycleToString(targetModule, false, INDENT); + addIssue(moduleRef, N4JSPackage.eINSTANCE.getModuleRef_Module(), LTD_LOADTIME_DEPENDENCY_CONFLICT + .toIssueItem(targetModule.getSimpleName(), otherLTDSources, cycleStr)); + return false; + } else { + // ERROR: referring to an LTD target from outside the dependency cycle cluster (Req. GH-1678, + // Constraint 3) + String healingModulesStr = healingModulesToString(targetModule); + String cycleStr = "\nContaining runtime dependency cycle cluster:\n" + + dependencyCycleToString(targetModule, false, INDENT); + addIssue(moduleRef, N4JSPackage.eINSTANCE.getModuleRef_Module(), + LTD_REFERENCE_TO_LOADTIME_DEPENDENCY_TARGET.toIssueItem(targetModule.getSimpleName(), + healingModulesStr, cycleStr)); + return true; // because we assume a healing import will be added by transpiler, this module + // reference can be treated as healing in calling method + } + } + } + + return true; + } + + private TModule getTargetModule(IdentifierRef idRef) { + IdentifiableElement target = idRef.getTargetElement(); + if (target == null || target.eIsProxy()) { + return null; + } + + if (!hasRuntimeRepresentation(target)) { + return null; + } + + TModule targetModule = EcoreUtil2.getContainerOfType(target, TModule.class); + if (targetModule != null) { + return targetModule; + } + + // references to local variables within the same module don't point to the TModule but to + // the variable declaration in the AST, so we need the following additional check: + Script targetScript = EcoreUtil2.getContainerOfType(target, Script.class); + if (targetScript != null) { + return targetScript.getModule(); + } + + return null; + } + + private String otherLTDSourcesToString(TModule module, TModule ltdTarget) { + List otherSources = sortModules( + filter(ltdTarget.getRuntimeCyclicLoadtimeDependents(), it -> it != module)); + String prefix = (otherSources.size() > 1) ? "modules " : "module "; + return prefix + Strings.join(", ", map(otherSources, os -> os.getSimpleName())); + } + + private String healingModulesToString(TModule module) { + List healingModules = sortModules(filter(module.getCyclicModulesRuntime(), it -> !isLTDTarget(it))); + String prefix = (healingModules.size() > 1) ? "one of the modules " : "module "; + return prefix + Strings.join(", ", map(healingModules, os -> os.getSimpleName())); + } + + private String dependencyCycleToString(TModule module, boolean onlyLoadtimeForInheritance, CharSequence indent) { + EList cyclicModulesToUse = (onlyLoadtimeForInheritance) + ? module.getCyclicModulesLoadtimeForInheritance() + : module.getCyclicModulesRuntime(); + Set cyclicModules = new LinkedHashSet<>(cyclicModulesToUse); + if (cyclicModules.isEmpty()) { + return null; + } + cyclicModules.add(module); + + StringBuilder sb = new StringBuilder(); + for (TModule cyclicModule : sortModules(cyclicModules)) { + if (sb.length() > 0) { + sb.append('\n'); + } + sb.append(indent); + if (!onlyLoadtimeForInheritance && isLTDTarget(cyclicModule)) { + sb.append('*'); + } + sb.append(getFileName(cyclicModule)); + sb.append(" --> "); + int listStart = sb.length(); + Iterable targetsOfRuntimeDeps = map(cyclicModule.getDependenciesRuntime(), it -> it.getTarget()); + for (TModule requiredModule : sortModules(targetsOfRuntimeDeps)) { + if (cyclicModules.contains(requiredModule)) { + if (sb.length() > listStart) { + sb.append(", "); + } + sb.append(getFileName(requiredModule)); + } + } + } + return sb.toString(); + } + + private List sortModules(Iterable modules) { + return sortWith(modules, (m1, m2) -> CharSequence.compare(m1.getSimpleName(), m2.getSimpleName())); + } + + private boolean isLTDTarget(TModule module) { + return !module.getRuntimeCyclicLoadtimeDependents().isEmpty(); + } + + private String getFileName(TModule module) { + if (module == null || module.eResource() == null || module.eResource().getURI() == null) { + return null; + } + return module.eResource().getURI().lastSegment(); + } +} diff --git a/plugins/org.eclipse.n4js/src/org/eclipse/n4js/validation/validators/RuntimeDependencyValidator.xtend b/plugins/org.eclipse.n4js/src/org/eclipse/n4js/validation/validators/RuntimeDependencyValidator.xtend deleted file mode 100644 index ec0ad75891..0000000000 --- a/plugins/org.eclipse.n4js/src/org/eclipse/n4js/validation/validators/RuntimeDependencyValidator.xtend +++ /dev/null @@ -1,243 +0,0 @@ -/** - * Copyright (c) 2020 NumberFour AG. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - * - * Contributors: - * NumberFour AG - Initial API and implementation - */ -package org.eclipse.n4js.validation.validators - -import com.google.common.collect.Iterables -import java.util.HashSet -import java.util.LinkedHashSet -import java.util.List -import java.util.Set -import org.eclipse.n4js.n4JS.IdentifierRef -import org.eclipse.n4js.n4JS.ModuleRef -import org.eclipse.n4js.n4JS.N4JSASTUtils -import org.eclipse.n4js.n4JS.N4JSPackage -import org.eclipse.n4js.n4JS.Script -import org.eclipse.n4js.postprocessing.RuntimeDependencyProcessor -import org.eclipse.n4js.resource.N4JSResource -import org.eclipse.n4js.ts.types.TModule -import org.eclipse.n4js.validation.AbstractN4JSDeclarativeValidator -import org.eclipse.xtext.EcoreUtil2 -import org.eclipse.xtext.validation.Check -import org.eclipse.xtext.validation.EValidatorRegistrar - -import static org.eclipse.n4js.utils.N4JSLanguageUtils.* -import static org.eclipse.n4js.validation.IssueCodes.* - -/** - * Validations related to runtime dependencies (in particular, illegal load-time dependency cycles). - * This class relies on the TModule-information computed in {@link RuntimeDependencyProcessor}. - */ -class RuntimeDependencyValidator extends AbstractN4JSDeclarativeValidator { - - private static final String INDENT = " "; - - /** - * NEEDED - * - * when removed check methods will be called twice once by N4JSValidator, and once by - * AbstractDeclarativeN4JSValidator - */ - override register(EValidatorRegistrar registrar) { - // nop - } - - /** - * Req. GH-1678, Constraint 4 - */ - @Check - def void checkIllegalLoadtimeReference(IdentifierRef idRef) { - if (!N4JSASTUtils.isTopLevelCode(idRef)) { - return; // only interested in top-level == load-time references here! - } - if (idRef.eContainingFeature === N4JSPackage.Literals.NAMED_EXPORT_SPECIFIER__EXPORTED_ELEMENT) { - return; // re-exports are not harmful - } - val targetModule = getTargetModule(idRef); - if (targetModule === null) { - return; - } - val containingModule = (idRef.eResource as N4JSResource).module; - val hasCycle = (targetModule === containingModule && !containingModule.cyclicModulesRuntime.empty) - || (targetModule !== containingModule && containingModule.cyclicModulesRuntime.contains(targetModule)); - if (hasCycle) { - val cycleStr = '\n' + dependencyCycleToString(containingModule, false, INDENT); - addIssue(idRef, LTD_ILLEGAL_LOADTIME_REFERENCE.toIssueItem(cycleStr)); - } - } - - @Check - def void checkIllegalModuleRefToLTDTarget(Script script) { - val containingModule = script.module; - val refsToOtherModules = script.scriptElements.filter(ModuleRef).filter[referringToOtherModule].toList; - - val modulesInHealedCycles = new HashSet(); - for (moduleRef : refsToOtherModules) { - if (holdsNotAnIllegalModuleRefWithinLoadtimeCycle(containingModule, moduleRef) - && holdsNotAnIllegalModuleRefToLTDTarget(containingModule, moduleRef, modulesInHealedCycles)) { - - if (moduleRef.retainedAtRuntime) { - // we have a valid reference to another module that will be retained at runtime - // --> it may contribute to healing later moduleRefs: - val targetModule = moduleRef.module; - if (!targetModule.cyclicModulesRuntime.empty) { - modulesInHealedCycles += targetModule; - modulesInHealedCycles += targetModule.cyclicModulesRuntime; - } - } - } - } - } - - /** - * Req. GH-1678, Constraint 1 - *

- * This method will show an error on all module references (i.e. imports/exports with 'from "..."') that constitute the cycle. - */ - def private boolean holdsNotAnIllegalModuleRefWithinLoadtimeCycle(TModule containingModule, ModuleRef moduleRef) { - val targetModule = moduleRef.module; - if (containingModule.cyclicModulesLoadtimeForInheritance.contains(targetModule)) { - val cycleStr = "\n" + dependencyCycleToString(targetModule, true, INDENT); - addIssue(moduleRef, N4JSPackage.eINSTANCE.moduleRef_Module, LTD_LOADTIME_DEPENDENCY_CYCLE.toIssueItem(cycleStr)); - return false; - } - return true; - } - - /** - * Req. GH-1678, Constraints 2 and 3. - */ - def private boolean holdsNotAnIllegalModuleRefToLTDTarget(TModule containingModule, ModuleRef moduleRef, Set modulesInHealedCycles) { - if (!moduleRef.isRetainedAtRuntime()) { - return true; // only interested in imports/exports that are retained at runtime - } - - val targetModule = moduleRef.module; - if (!targetModule.isLTDTarget) { - return true; // only interested in references to LTD targets - } - if (!targetModule.cyclicModulesLoadtimeForInheritance.empty) { - // target is part of a load-time dependency cycle, so other errors are shown already there - return true; // avoid unnecessary follow-up errors - } - - val ltdSources = targetModule.runtimeCyclicLoadtimeDependents; // never includes the containing module itself! - val isSingletonLTDTarget = ltdSources.size() == 1 - && !containingModule.equals(Iterables.getFirst(ltdSources, null)); // excludes the one valid reference to an LTD target - val isMultiLTDTarget = ltdSources.size() > 1; - - if (isSingletonLTDTarget || isMultiLTDTarget) { - if (!modulesInHealedCycles.contains(targetModule)) { - // illegal reference to an LTD target - val withinSameDependencyCycleCluster = targetModule.cyclicModulesRuntime.contains(containingModule); - if (withinSameDependencyCycleCluster) { - // ERROR: referring to a multi-LTD-target from within the dependency cycle cluster (Req. GH-1678, Constraint 2) - // --> load-time dependency conflict - val otherLTDSources = otherLTDSourcesToString(containingModule, targetModule); - val cycleStr = "\nContaining runtime dependency cycle cluster:\n" + dependencyCycleToString(targetModule, false, INDENT); - addIssue(moduleRef, N4JSPackage.eINSTANCE.moduleRef_Module, LTD_LOADTIME_DEPENDENCY_CONFLICT.toIssueItem(targetModule.simpleName, otherLTDSources, cycleStr)); - return false; - } else { - // ERROR: referring to an LTD target from outside the dependency cycle cluster (Req. GH-1678, Constraint 3) - val healingModulesStr = healingModulesToString(targetModule); - val cycleStr = "\nContaining runtime dependency cycle cluster:\n" + dependencyCycleToString(targetModule, false, INDENT); - addIssue(moduleRef, N4JSPackage.eINSTANCE.moduleRef_Module, LTD_REFERENCE_TO_LOADTIME_DEPENDENCY_TARGET.toIssueItem(targetModule.simpleName, healingModulesStr, cycleStr)); - return true; // because we assume a healing import will be added by transpiler, this module reference can be treated as healing in calling method - } - } - } - - return true; - } - - def private TModule getTargetModule(IdentifierRef idRef) { - val target = idRef.targetElement; - if (target === null || target.eIsProxy()) { - return null; - } - - if (!hasRuntimeRepresentation(target)) { - return null; - } - - val targetModule = EcoreUtil2.getContainerOfType(target, TModule); - if (targetModule !== null) { - return targetModule; - } - - // references to local variables within the same module don't point to the TModule but to - // the variable declaration in the AST, so we need the following additional check: - val targetScript = EcoreUtil2.getContainerOfType(target, Script); - if (targetScript !== null) { - return targetScript.getModule(); - } - - return null; - } - - def private String otherLTDSourcesToString(TModule module, TModule ltdTarget) { - val otherSources = ltdTarget.runtimeCyclicLoadtimeDependents.filter[it !== module].sortModules; - val prefix = if (otherSources.size > 1) "modules " else "module "; - return prefix + otherSources.map[simpleName].join(", "); - } - - def private String healingModulesToString(TModule module) { - val healingModules = module.cyclicModulesRuntime.filter[!it.isLTDTarget].sortModules; - val prefix = if (healingModules.size > 1) "one of the modules " else "module "; - return prefix + healingModules.map[simpleName].join(", "); - } - - def private String dependencyCycleToString(TModule module, boolean onlyLoadtimeForInheritance, CharSequence indent) { - val cyclicModulesToUse = if (onlyLoadtimeForInheritance) module.cyclicModulesLoadtimeForInheritance else module.cyclicModulesRuntime; - val cyclicModules = new LinkedHashSet(cyclicModulesToUse); - if (cyclicModules.empty) { - return null; - } - cyclicModules += module; - - val sb = new StringBuilder(); - for (cyclicModule : sortModules(cyclicModules)) { - if (sb.length > 0) { - sb.append('\n'); - } - sb.append(indent); - if (!onlyLoadtimeForInheritance && cyclicModule.isLTDTarget) { - sb.append('*'); - } - sb.append(cyclicModule.fileName) - sb.append(" --> "); - val listStart = sb.length; - val targetsOfRuntimeDeps = cyclicModule.dependenciesRuntime.map[target]; - for (requiredModule : sortModules(targetsOfRuntimeDeps)) { - if (cyclicModules.contains(requiredModule)) { - if (sb.length > listStart) { - sb.append(", "); - } - sb.append(requiredModule.fileName); - } - } - } - return sb.toString(); - } - - def private List sortModules(Iterable modules) { - return modules.sortWith([m1, m2 | - return CharSequence.compare(m1.simpleName, m2.simpleName); - ]); - } - - def private boolean isLTDTarget(TModule module) { - return !module.runtimeCyclicLoadtimeDependents.empty; - } - - def private String getFileName(TModule module) { - return module?.eResource?.URI?.lastSegment; - } -}