diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/reconcile/ModulithTypeReferenceViolation.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/reconcile/ModulithTypeReferenceViolation.java index f117607fb..5009778ef 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/reconcile/ModulithTypeReferenceViolation.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/reconcile/ModulithTypeReferenceViolation.java @@ -10,6 +10,7 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.rewrite.reconcile; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; @@ -17,6 +18,7 @@ import org.openrewrite.ExecutionContext; import org.openrewrite.Tree; import org.openrewrite.java.JavaIsoVisitor; import org.openrewrite.java.JavaVisitor; +import org.openrewrite.java.marker.JavaSourceSet; import org.openrewrite.java.tree.J; import org.openrewrite.java.tree.J.CompilationUnit; import org.openrewrite.java.tree.J.FieldAccess; @@ -33,6 +35,7 @@ import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemTy import org.springframework.ide.vscode.commons.rewrite.config.MarkerVisitorContext; import org.springframework.ide.vscode.commons.rewrite.config.RecipeCodeActionDescriptor; import org.springframework.ide.vscode.commons.rewrite.java.FixAssistMarker; +import org.springframework.ide.vscode.commons.rewrite.java.ProjectParser; public class ModulithTypeReferenceViolation implements RecipeCodeActionDescriptor { @@ -59,6 +62,10 @@ public class ModulithTypeReferenceViolation implements RecipeCodeActionDescripto if (appModules == null) { return cu; } else { + JavaSourceSet sourceSet = cu.getMarkers().findFirst(JavaSourceSet.class).orElse(null); + if (sourceSet != null && ProjectParser.TEST.equals(sourceSet.getName())) { + return cu; + } String pkgName = cu.getPackageDeclaration() == null ? "" : cu.getPackageDeclaration().getPackageName(); p.putMessage(MSG_PKG_NAME, pkgName); return super.visitCompilationUnit(cu, p); @@ -76,19 +83,26 @@ public class ModulithTypeReferenceViolation implements RecipeCodeActionDescripto @Override public Identifier visitIdentifier(Identifier identifier, ExecutionContext p) { Identifier i = super.visitIdentifier(identifier, p); - if (!(getCursor().getParent().firstEnclosingOrThrow(J.class) instanceof J.FieldAccess)) { - return process(i, p.getMessage(MSG_PKG_NAME), TypeUtils.asFullyQualified(identifier.getType())); + if (getCursor().getParent().firstEnclosingOrThrow(J.class) instanceof J.FieldAccess) { + return i; } + // check if identifier is a simple name of the type + FullyQualified type = TypeUtils.asFullyQualified(identifier.getType()); + if (type != null && identifier.getSimpleName().equals(type.getClassName())) { + return process(i, p.getMessage(MSG_PKG_NAME), type); + } + return i; } private T process(T node, String packageName, FullyQualified type) { if (type != null) { - if (!appModules.isReferenceAllowed(packageName, type.getFullyQualifiedName())) { + Optional opt = appModules.getModuleNotExposingType(packageName, type.getFullyQualifiedName()).map(module -> { FixAssistMarker fixMarker = new FixAssistMarker(Tree.randomId(), getId()) - .withLabel("Type is not allowed to be used in this package. Consider changing you 'Modulith' structure." ); - node = node.withMarkers(node.getMarkers().add(fixMarker)); - } + .withLabel("Cannot use type in this package. Type is not exposed in module '" + module.name() + "'." ); + return node.withMarkers(node.getMarkers().add(fixMarker)); + }); + return opt.orElse(node); } return node; } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ServerUtils.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ServerUtils.java index f9688c529..b07c3cc5d 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ServerUtils.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ServerUtils.java @@ -31,8 +31,7 @@ public class ServerUtils { private static final List CLASS_FILES_TO_WATCH_GLOB = List.of("**/*.class"); public static void listenToClassFileChanges(FileObserver fileObserver, JavaProjectFinder projectFinder, Consumer callback) { - fileObserver.onFilesChanged(CLASS_FILES_TO_WATCH_GLOB, files -> handleFiles(projectFinder, files, callback)); - fileObserver.onFilesCreated(CLASS_FILES_TO_WATCH_GLOB, files -> handleFiles(projectFinder, files, callback)); + fileObserver.onAnyChange(CLASS_FILES_TO_WATCH_GLOB, files -> handleFiles(projectFinder, files, callback)); } private static void handleFiles(JavaProjectFinder projectFinder, String[] files, Consumer callback) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/modulith/AppModule.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/modulith/AppModule.java index 93319b898..2b442abf5 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/modulith/AppModule.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/modulith/AppModule.java @@ -12,7 +12,7 @@ package org.springframework.ide.vscode.boot.modulith; import java.util.Collection; -record AppModule( +public record AppModule( String name, String basePackage, Collection namedInterfaces diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/modulith/AppModules.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/modulith/AppModules.java index de19b76c6..94e301d7b 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/modulith/AppModules.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/modulith/AppModules.java @@ -10,7 +10,9 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.modulith; +import java.util.ArrayList; import java.util.List; +import java.util.Optional; public final class AppModules { @@ -20,14 +22,42 @@ public final class AppModules { this.modules = modules; } - public boolean isReferenceAllowed(String targetPackage, String referenceFqName) { - String referencePackage = ModulithService.getPackageNameFromTypeFQName(referenceFqName); - return modules - .stream() - .filter(m -> m.basePackage().equals(referencePackage)) - .findFirst() - .map(m -> m.namedInterfaces().contains(referenceFqName)) - .orElse(true); + public Optional getModuleNotExposingType(String targetPackage, String referenceFqName) { + return getModuleForType(referenceFqName).flatMap(refModule -> { + if (refModule.namedInterfaces().contains(referenceFqName)) { + return Optional.empty(); + } else { + if (getModuleForPackage(targetPackage).map(targetModule -> targetModule == refModule).orElse(false)) { + // same module for target package and reference type + return Optional.empty(); + } + return Optional.of(refModule); + } + }); + } + + private Optional getModuleForPackage(String pkgName) { + return generatePackageHierarchy(pkgName) + .stream() + .map(p -> modules.stream().filter(m -> m.basePackage().equals(p)).findFirst()) + .filter(o -> o.isPresent()) + .map(o -> o.get()) + .findFirst(); + } + + private Optional getModuleForType(String typeFqName) { + return getModuleForPackage(ModulithService.getPackageNameFromTypeFQName(typeFqName)); + } + + private List generatePackageHierarchy(String pkgName) { + List packageHierarchy = new ArrayList<>(); + packageHierarchy.add(pkgName); + for (int i = pkgName.length() - 1; i >= 0; i--) { + if (pkgName.charAt(i) == '.') { + packageHierarchy.add(pkgName.substring(0, i)); + } + } + return packageHierarchy; } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/modulith/ModulithService.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/modulith/ModulithService.java index 2e3713f25..5d18adef3 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/modulith/ModulithService.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/modulith/ModulithService.java @@ -13,6 +13,8 @@ package org.springframework.ide.vscode.boot.modulith; import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.URI; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; @@ -29,6 +31,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; import org.springframework.ide.vscode.boot.java.Annotations; +import org.springframework.ide.vscode.commons.java.IClasspathUtil; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.java.SpringProjectUtil; import org.springframework.ide.vscode.commons.java.Version; @@ -85,7 +88,16 @@ public class ModulithService { for (String f : files) { URI uri = URI.create(f); TextDocumentIdentifier docId = new TextDocumentIdentifier(uri.toASCIIString()); - projectFinder.find(docId).ifPresent(this::invalidate); + projectFinder.find(docId).ifPresent(project -> { + synchronized (project) { + if (cache.containsKey(project)) { + Path filePath = Paths.get(uri); + if (IClasspathUtil.getProjectJavaSourceFoldersWithoutTests(project.getClasspath()).map(folder -> folder.toPath()).anyMatch(folderPath -> filePath.startsWith(folderPath))) { + cache.remove(project); + } + } + } + }); } }