diff --git a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightVisitorImpl.java b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightVisitorImpl.java index 47096aa11132..448c427c445c 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightVisitorImpl.java +++ b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightVisitorImpl.java @@ -65,6 +65,7 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh private JavaSdkVersion myJavaSdkVersion; @SuppressWarnings("StatefulEp") private PsiFile myFile; + @SuppressWarnings("StatefulEp") private PsiJavaModule myJavaModule; // map codeBlock->List of PsiReferenceExpression of uninitialized final variables private final Map> myUninitializedVarProblems = new THashMap<>(); @@ -155,13 +156,9 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh @Override public boolean analyze(@NotNull PsiFile file, boolean updateWholeFile, @NotNull HighlightInfoHolder holder, @NotNull Runnable highlight) { - myFile = file; - myHolder = Holder.CHECK_ELEMENT_LEVEL ? new CheckLevelHighlightInfoHolder(file, holder) : holder; - boolean success = true; try { - myLanguageLevel = PsiUtil.getLanguageLevel(file); - myJavaSdkVersion = notNull(JavaVersionService.getInstance().getJavaSdkVersion(file), JavaSdkVersion.fromLanguageLevel(myLanguageLevel)); + prepare(Holder.CHECK_ELEMENT_LEVEL ? new CheckLevelHighlightInfoHolder(file, holder) : holder, file); if (updateWholeFile) { ProgressIndicator progress = ProgressManager.getInstance().getProgressIndicator(); if (progress == null) throw new IllegalStateException("Must be run under progress"); @@ -193,6 +190,7 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh myReassignedParameters.clear(); myRefCountHolder = null; + myJavaModule = null; myFile = null; myHolder = null; myDuplicateMethods.clear(); @@ -202,6 +200,18 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh return success; } + protected void prepareToRunAsInspection(@NotNull HighlightInfoHolder holder) { + prepare(holder, holder.getContextFile()); + } + + private void prepare(HighlightInfoHolder holder, PsiFile file) { + myHolder = holder; + myFile = file; + myLanguageLevel = PsiUtil.getLanguageLevel(file); + myJavaSdkVersion = notNull(JavaVersionService.getInstance().getJavaSdkVersion(file), JavaSdkVersion.fromLanguageLevel(myLanguageLevel)); + myJavaModule = myLanguageLevel.isAtLeast(LanguageLevel.JDK_1_9) ? ModuleHighlightUtil.getModuleDescriptor(file) : null; + } + @Override public void visitElement(PsiElement element) { if (myRefCountHolder != null && myFile instanceof ServerPageFile) { @@ -1030,8 +1040,14 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh @Override public void visitReferenceElement(PsiJavaCodeReferenceElement ref) { - JavaResolveResult resolveResult = doVisitReferenceElement(ref); - if (resolveResult != null && !myHolder.hasErrorResults()) myHolder.add(GenericsHighlightUtil.checkRawOnParameterizedType(ref, resolveResult.getElement())); + JavaResolveResult result = doVisitReferenceElement(ref); + if (result != null) { + PsiElement resolved = result.getElement(); + if (!myHolder.hasErrorResults()) myHolder.add(GenericsHighlightUtil.checkRawOnParameterizedType(ref, resolved)); + if (!myHolder.hasErrorResults() && resolved != null && myJavaModule != null) { + myHolder.add(ModuleHighlightUtil.checkPackageAccessibility(ref, resolved, myJavaModule)); + } + } } private JavaResolveResult doVisitReferenceElement(@NotNull PsiJavaCodeReferenceElement ref) { @@ -1688,14 +1704,4 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh private HighlightInfo checkFeature(@NotNull PsiElement element, @NotNull Feature feature) { return HighlightUtil.checkFeature(element, feature, myLanguageLevel, myFile); } - - protected void prepareToRunAsInspection(@NotNull HighlightInfoHolder holder) { - PsiFile file = holder.getContextFile(); - JavaSdkVersion sdkVersion = JavaVersionService.getInstance().getJavaSdkVersion(file); - - myHolder = holder; - myFile = file; - myLanguageLevel = PsiUtil.getLanguageLevel(file); - myJavaSdkVersion = sdkVersion != null ? sdkVersion : JavaSdkVersion.fromLanguageLevel(myLanguageLevel); - } } \ No newline at end of file diff --git a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/JavaModuleGraphUtil.java b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/JavaModuleGraphUtil.java index 8c77bb0e7540..48400b4eef99 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/JavaModuleGraphUtil.java +++ b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/JavaModuleGraphUtil.java @@ -24,10 +24,12 @@ import com.intellij.psi.*; import com.intellij.psi.search.FilenameIndex; import com.intellij.psi.util.CachedValueProvider.Result; import com.intellij.psi.util.CachedValuesManager; +import com.intellij.psi.util.PsiUtil; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.MultiMap; import com.intellij.util.graph.DFSTBuilder; import com.intellij.util.graph.Graph; +import com.intellij.util.graph.GraphGenerator; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -49,6 +51,13 @@ public class JavaModuleGraphUtil { return ContainerUtil.find(cycles, set -> set.contains(module)); } + public static boolean reads(@NotNull PsiJavaModule source, @NotNull PsiJavaModule destination) { + Project project = source.getProject(); + RequiresGraph graph = CachedValuesManager.getManager(project).getCachedValue(project, () -> + Result.create(ReadAction.compute(() -> buildRequiresGraph(project)), OUT_OF_CODE_BLOCK_MODIFICATION_COUNT)); + return graph.reads(source, destination); + } + // Looks for cycles between Java modules in the project sources. // Library/JDK modules are excluded - in assumption there can't be any lib -> src dependencies. // Module references are resolved "globally" (i.e., without taking project dependencies into account). @@ -65,15 +74,15 @@ public class JavaModuleGraphUtil { if (!projectModules.isEmpty()) { MultiMap relations = MultiMap.create(); - for (PsiJavaModule moduleDeclaration : projectModules) { - for (PsiRequiresStatement statement : psiTraverser().children(moduleDeclaration).filter(PsiRequiresStatement.class)) { + for (PsiJavaModule module : projectModules) { + for (PsiRequiresStatement statement : psiTraverser().children(module).filter(PsiRequiresStatement.class)) { Optional.ofNullable(statement.getReferenceElement()) .map(PsiJavaModuleReferenceElement::getReference) .map(ref -> ref.multiResolve(true)) .map(a -> a.length == 1 ? a[0].getElement() : null) .map(e -> e instanceof PsiJavaModule ? (PsiJavaModule)e : null) .filter(projectModules::contains) - .ifPresent(dependency -> relations.putValue(moduleDeclaration, dependency)); + .ifPresent(dependency -> relations.putValue(module, dependency)); } } @@ -90,6 +99,74 @@ public class JavaModuleGraphUtil { return Collections.emptyList(); } + // Starting from source modules, collects all module dependencies in the project. + // The resulting graph is used for tracing readability. + private static RequiresGraph buildRequiresGraph(Project project) { + MultiMap relations = MultiMap.create(); + Set publicEdges = ContainerUtil.newTroveSet(); + for (Module module : ModuleManager.getInstance(project).getModules()) { + Collection files = FilenameIndex.getVirtualFilesByName(project, MODULE_INFO_FILE, module.getModuleScope(false)); + Optional.ofNullable(ContainerUtil.getFirstItem(files)) + .map(PsiManager.getInstance(project)::findFile) + .map(f -> f instanceof PsiJavaFile ? ((PsiJavaFile)f).getModuleDeclaration() : null) + .ifPresent(m -> visit(m, relations, publicEdges)); + } + + GraphGenerator graph = GraphGenerator.create(new RequiresSemiGraph(relations)); + return new RequiresGraph(graph, publicEdges); + } + + private static void visit(PsiJavaModule module, MultiMap relations, Set publicEdges) { + if (!relations.containsKey(module)) { + relations.putValues(module, Collections.emptyList()); + for (PsiRequiresStatement statement : psiTraverser().children(module).filter(PsiRequiresStatement.class)) { + Optional.ofNullable(statement.getReferenceElement()) + .map(PsiJavaModuleReferenceElement::getReference) + .map(PsiReference::resolve) + .map(e -> e instanceof PsiJavaModule ? (PsiJavaModule)e : null) + .ifPresent(dependency -> { + relations.putValue(module, dependency); + if (isPublic(statement)) publicEdges.add(RequiresGraph.key(dependency, module)); + visit(dependency, relations, publicEdges); + }); + } + } + } + + private static boolean isPublic(PsiRequiresStatement statement) { + for (PsiElement child = statement.getFirstChild(); child != null; child = child.getNextSibling()) { + if (PsiUtil.isJavaToken(child, JavaTokenType.PUBLIC_KEYWORD)) return true; + if (child instanceof PsiJavaModuleReferenceElement) break; + } + return false; + } + + private static class RequiresGraph { + private final Graph myGraph; + private final Set myPublicEdges; + + public RequiresGraph(Graph graph, Set publicEdges) { + myGraph = graph; + myPublicEdges = publicEdges; + } + + public boolean reads(PsiJavaModule source, PsiJavaModule destination) { + Iterator directReaders = myGraph.getOut(destination); + while (directReaders.hasNext()) { + PsiJavaModule next = directReaders.next(); + if (source.equals(next) || myPublicEdges.contains(key(destination, next)) && reads(source, next)) { + return true; + } + } + return false; + } + + public static String key(PsiJavaModule module, PsiJavaModule exporter) { + return module.getModuleName() + '/' + exporter.getModuleName(); + } + } + + // private static class SourceSemiGraph implements Graph { private final MultiMap myMap; @@ -112,4 +189,23 @@ public class JavaModuleGraphUtil { return myMap.get(n).iterator(); } } + + private static class RequiresSemiGraph implements GraphGenerator.SemiGraph { + private final MultiMap myMap; + + public RequiresSemiGraph(MultiMap map) { + myMap = map; + } + + @Override + public Collection getNodes() { + return myMap.keySet(); + } + + @Override + public Iterator getIn(PsiJavaModule n) { + return myMap.get(n).iterator(); + } + } + // } \ No newline at end of file diff --git a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/ModuleHighlightUtil.java b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/ModuleHighlightUtil.java index 75b3e8a0b15f..480c5b0af920 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/ModuleHighlightUtil.java +++ b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/ModuleHighlightUtil.java @@ -54,6 +54,34 @@ import static com.intellij.psi.PsiJavaModule.MODULE_INFO_FILE; import static com.intellij.psi.SyntaxTraverser.psiTraverser; public class ModuleHighlightUtil { + @Nullable + static PsiJavaModule getModuleDescriptor(@NotNull PsiElement element) { + VirtualFile file = Optional.of(element) + .map(e -> e instanceof PsiFileSystemItem ? (PsiFileSystemItem)e : e.getContainingFile()) + .map(PsiFileSystemItem::getVirtualFile) + .orElse(null); + if (file == null) return null; + + Project project = element.getProject(); + ProjectFileIndex index = ProjectFileIndex.SERVICE.getInstance(project); + if (element instanceof PsiCompiledElement) { + return Optional.ofNullable(index.getClassRootForFile(file)) + .map(r -> r.findChild(PsiJavaModule.MODULE_INFO_CLS_FILE)) + .map(PsiManager.getInstance(project)::findFile) + .map(f -> f instanceof PsiJavaFile ? ((PsiJavaFile)f).getModuleDeclaration() : null) + .orElse(null); + } + else { + Module module = index.getModuleForFile(file); + return Optional.ofNullable(module) + .map(m -> FilenameIndex.getVirtualFilesByName(project, MODULE_INFO_FILE, m.getModuleScope(false))) + .map(c -> c.size () == 1 ? c.iterator().next() : null) + .map(PsiManager.getInstance(project)::findFile) + .map(f -> f instanceof PsiJavaFile ? ((PsiJavaFile)f).getModuleDeclaration() : null) + .orElse(null); + } + } + @Nullable static HighlightInfo checkFileName(@NotNull PsiJavaModule element, @NotNull PsiFile file) { if (!MODULE_INFO_FILE.equals(file.getName())) { @@ -307,6 +335,70 @@ public class ModuleHighlightUtil { return null; } + @Nullable + static HighlightInfo checkPackageAccessibility(@NotNull PsiJavaCodeReferenceElement ref, + @NotNull PsiElement target, + @NotNull PsiJavaModule refModule) { + Module module = ModuleUtilCore.findModuleForPsiElement(ref); + if (module != null) { + if (target instanceof PsiClass && !(target instanceof PsiCompiledElement) && module != ModuleUtilCore.findModuleForPsiElement(target)) { + PsiElement targetFile = target.getParent(); + if (targetFile instanceof PsiClassOwner) { + PsiJavaModule targetModule = getModuleDescriptor(target); + String packageName = ((PsiClassOwner)targetFile).getPackageName(); + return checkPackageAccessibility(ref, refModule, targetModule, packageName); + } + } + else if (target instanceof PsiPackage) { + PsiElement refImport = ref.getParent(); + if (refImport instanceof PsiImportStatementBase && ((PsiImportStatementBase)refImport).isOnDemand()) { + PsiDirectory[] dirs = ((PsiPackage)target).getDirectories(module.getModuleWithDependenciesAndLibrariesScope(false)); + if (dirs.length == 1 && ModuleUtilCore.findModuleForPsiElement(dirs[0]) != module) { + PsiJavaModule targetModule = getModuleDescriptor(dirs[0]); + String packageName = ((PsiPackage)target).getQualifiedName(); + return checkPackageAccessibility(ref, refModule, targetModule, packageName); + } + } + } + } + + return null; + } + + private static HighlightInfo checkPackageAccessibility(PsiJavaCodeReferenceElement ref, + PsiJavaModule refModule, + PsiJavaModule targetModule, + String packageName) { + if (targetModule == null) { + String message = JavaErrorMessages.message("module.package.on.classpath"); + return HighlightInfo.newHighlightInfo(HighlightInfoType.WRONG_REF).range(ref).description(message).create(); + } + + String refModuleName = refModule.getModuleName(); + String requiredName = targetModule.getModuleName(); + if (!(targetModule instanceof PsiCompiledElement) && !isExported(targetModule, packageName)) { + String message = JavaErrorMessages.message("module.package.not.exported", requiredName, packageName, refModuleName); + return HighlightInfo.newHighlightInfo(HighlightInfoType.WRONG_REF).range(ref).description(message).create(); + } + + if (!(PsiJavaModule.JAVA_BASE.equals(requiredName) || JavaModuleGraphUtil.reads(refModule, targetModule))) { + String message = JavaErrorMessages.message("module.not.in.requirements", refModuleName, requiredName); + return HighlightInfo.newHighlightInfo(HighlightInfoType.WRONG_REF).range(ref).description(message).create(); + } + + return null; + } + + private static boolean isExported(PsiJavaModule module, String packageName) { + for (PsiExportsStatement statement : psiTraverser().children(module).filter(PsiExportsStatement.class)) { + String exportedName = Optional.ofNullable(statement.getPackageReference()).map(ModuleHighlightUtil::refText).orElse(""); + if (packageName.equals(exportedName)) { + return true; + } + } + return false; + } + private static HighlightInfo moduleResolveError(PsiJavaModuleReferenceElement refElement, PsiPolyVariantReference ref) { boolean missing = ref.multiResolve(true).length == 0; String message = JavaErrorMessages.message(missing ? "module.not.found" : "module.not.on.path", refElement.getReferenceText()); diff --git a/java/java-psi-api/src/com/intellij/psi/PsiJavaModule.java b/java/java-psi-api/src/com/intellij/psi/PsiJavaModule.java index 11f3f622b205..7063b26b5e74 100644 --- a/java/java-psi-api/src/com/intellij/psi/PsiJavaModule.java +++ b/java/java-psi-api/src/com/intellij/psi/PsiJavaModule.java @@ -26,6 +26,7 @@ public interface PsiJavaModule extends NavigatablePsiElement, PsiNamedElement, P String MODULE_INFO_CLASS = "module-info"; String MODULE_INFO_FILE = MODULE_INFO_CLASS + ".java"; String MODULE_INFO_CLS_FILE = MODULE_INFO_CLASS + ".class"; + String JAVA_BASE = "java.base"; @NotNull PsiJavaModuleReferenceElement getNameElement(); @NotNull String getModuleName(); diff --git a/java/java-psi-impl/src/messages/JavaErrorMessages.properties b/java/java-psi-impl/src/messages/JavaErrorMessages.properties index 8fc2d0f4d052..cae0654cb54f 100644 --- a/java/java-psi-impl/src/messages/JavaErrorMessages.properties +++ b/java/java-psi-impl/src/messages/JavaErrorMessages.properties @@ -411,6 +411,9 @@ module.service.abstract=The service implementation is an abstract class: {0} module.service.no.ctor=The service implementation does not have a default constructor: {0} module.service.hidden.ctor=The default constructor of the service implementation is not public: {0} module.service.unused=Service interface provided but not exported or used +module.package.not.exported=The module ''{0}'' does not export the package ''{1}'' to the module ''{2}'' +module.package.on.classpath=A named module cannot access packages of an unnamed one +module.not.in.requirements=The module ''{0}'' does not have the module ''{1}'' in requirements feature.generics=Generics feature.annotations=Annotations diff --git a/java/java-tests/testSrc/com/intellij/codeInsight/daemon/ModuleHighlightingTest.kt b/java/java-tests/testSrc/com/intellij/codeInsight/daemon/ModuleHighlightingTest.kt index c1db1138e1ec..b183184a5efe 100644 --- a/java/java-tests/testSrc/com/intellij/codeInsight/daemon/ModuleHighlightingTest.kt +++ b/java/java-tests/testSrc/com/intellij/codeInsight/daemon/ModuleHighlightingTest.kt @@ -17,8 +17,7 @@ package com.intellij.codeInsight.daemon import com.intellij.psi.PsiJavaModule import com.intellij.testFramework.fixtures.LightJava9ModulesCodeInsightFixtureTestCase -import com.intellij.testFramework.fixtures.MultiModuleJava9ProjectDescriptor.ModuleDescriptor.M2 -import com.intellij.testFramework.fixtures.MultiModuleJava9ProjectDescriptor.ModuleDescriptor.M3 +import com.intellij.testFramework.fixtures.MultiModuleJava9ProjectDescriptor.ModuleDescriptor.* import com.intellij.testFramework.fixtures.impl.CodeInsightTestFixtureImpl import org.assertj.core.api.Assertions.assertThat @@ -136,6 +135,33 @@ class ModuleHighlightingTest : LightJava9ModulesCodeInsightFixtureTestCase() { fixes("module M { uses pkg.m3.C3; }", "AddModuleDependencyFix") } + fun testPackageAccessibility() { + addFile("module-info.java", "module M { requires M2; requires M6; }") + addFile("module-info.java", "module M2 { exports pkg.m2; }", M2) + addFile("pkg/m2/C2.java", "package pkg.m2;\npublic class C2 { }", M2) + addFile("pkg/m2/impl/C2Impl.java", "package pkg.m2.impl;\nimport pkg.m2.C2;\npublic class C2Impl { public static C2 make() {} }", M2) + addFile("pkg/m4/C4.java", "package pkg.m4;\npublic class C4 { }", M4) + addFile("module-info.java", "module M5 { exports pkg.m5; }", M5) + addFile("pkg/m5/C5.java", "package pkg.m5;\npublic class C5 { }", M5) + addFile("module-info.java", "module M6 { requires public M7; }", M6) + addFile("module-info.java", "module M7 { exports pkg.m7; }", M7) + addFile("pkg/m7/C7.java", "package pkg.m7;\npublic class C7 { }", M7) + + highlight("test.java", """ + import pkg.m2.C2; + import pkg.m2.*; + import pkg.m2.impl.C2Impl; + import pkg.m2.impl.*; + import pkg.m4.C4; + import pkg.m5.C5; + import pkg.m7.C7; + + import static pkg.m2.impl.C2Impl.make; + + class C { } + """.trimIndent(), true) + } + // private fun highlight(text: String, filter: Boolean = false) = highlight("module-info.java", text, filter) diff --git a/java/java-tests/testSrc/com/intellij/testFramework/fixtures/MultiModuleJava9ProjectDescriptor.kt b/java/java-tests/testSrc/com/intellij/testFramework/fixtures/MultiModuleJava9ProjectDescriptor.kt index 2021dcd603a3..cdc211e11e3c 100644 --- a/java/java-tests/testSrc/com/intellij/testFramework/fixtures/MultiModuleJava9ProjectDescriptor.kt +++ b/java/java-tests/testSrc/com/intellij/testFramework/fixtures/MultiModuleJava9ProjectDescriptor.kt @@ -20,10 +20,7 @@ import com.intellij.openapi.module.Module import com.intellij.openapi.module.ModuleManager import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk -import com.intellij.openapi.roots.ContentEntry -import com.intellij.openapi.roots.LanguageLevelModuleExtension -import com.intellij.openapi.roots.ModifiableRootModel -import com.intellij.openapi.roots.ModuleRootModificationUtil +import com.intellij.openapi.roots.* import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.ex.temp.TempFileSystem @@ -32,13 +29,17 @@ import com.intellij.testFramework.IdeaTestUtil import com.intellij.testFramework.LightPlatformTestCase /** - * Dependencies: 'main' -> 'm2', 'm3' + * Dependencies: 'main' -> 'm2', 'main' -> 'm4', 'main' -> 'm5', 'main' -> 'm6' => 'm7' */ object MultiModuleJava9ProjectDescriptor : DefaultLightProjectDescriptor() { enum class ModuleDescriptor(internal val moduleName: String, internal val rootName: String) { MAIN(TEST_MODULE_NAME, "/not_used/"), M2("${TEST_MODULE_NAME}_m2", "src_m2"), - M3("${TEST_MODULE_NAME}_m3", "src_m3"); + M3("${TEST_MODULE_NAME}_m3", "src_m3"), + M4("${TEST_MODULE_NAME}_m4", "src_m4"), + M5("${TEST_MODULE_NAME}_m5", "src_m5"), + M6("${TEST_MODULE_NAME}_m6", "src_m6"), + M7("${TEST_MODULE_NAME}_m7", "src_m7"); fun root(): VirtualFile = if (this == MAIN) LightPlatformTestCase.getSourceRoot() else TempFileSystem.getInstance().findFileByPath("/$rootName")!! @@ -50,9 +51,23 @@ object MultiModuleJava9ProjectDescriptor : DefaultLightProjectDescriptor() { super.setUpProject(project, handler) runWriteAction { val main = ModuleManager.getInstance(project).findModuleByName(TEST_MODULE_NAME)!! + val m2 = makeModule(project, ModuleDescriptor.M2) ModuleRootModificationUtil.addDependency(main, m2) + makeModule(project, ModuleDescriptor.M3) + + val m4 = makeModule(project, ModuleDescriptor.M4) + ModuleRootModificationUtil.addDependency(main, m4) + + val m5 = makeModule(project, ModuleDescriptor.M5) + ModuleRootModificationUtil.addDependency(main, m5) + + val m6 = makeModule(project, ModuleDescriptor.M6) + ModuleRootModificationUtil.addDependency(main, m6) + + val m7 = makeModule(project, ModuleDescriptor.M7) + ModuleRootModificationUtil.addDependency(m6, m7, DependencyScope.COMPILE, true) } }