From cbbc5aa447ef59b841e57e309117c3961432ab3a Mon Sep 17 00:00:00 2001 From: Tom Tourwe <86676109+tomtourwe@users.noreply.github.com> Date: Fri, 13 Nov 2026 18:12:26 +0100 Subject: [PATCH] InfoByFqn.removeAllSources: rebuild the multi-source-set lists instead of removeIf A name defined in several source sets is registered with a list built by List.of or Stream.toList(); those lists cannot be changed, so removeIf threw UnsupportedOperationException. JavaInspectorImpl.invalidateAllSources() stopped at its first line and left the javac file managers and shared jar archives open. Seen on the refactor server when dropping hibernate-orm (60 source sets). Test: TestRemoveAllSourcesWithOneNameInTwoSourceSets. --- ...eAllSourcesWithOneNameInTwoSourceSets.java | 94 +++++++++++++++++++ .../maddi/inspection/resource/InfoByFqn.java | 3 +- 2 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 maddi-inspection-openjdk/src/test/java/io/codelaser/maddi/inspection/openjdk/TestRemoveAllSourcesWithOneNameInTwoSourceSets.java diff --git a/maddi-inspection-openjdk/src/test/java/io/codelaser/maddi/inspection/openjdk/TestRemoveAllSourcesWithOneNameInTwoSourceSets.java b/maddi-inspection-openjdk/src/test/java/io/codelaser/maddi/inspection/openjdk/TestRemoveAllSourcesWithOneNameInTwoSourceSets.java new file mode 100644 index 000000000..7e71edcec --- /dev/null +++ b/maddi-inspection-openjdk/src/test/java/io/codelaser/maddi/inspection/openjdk/TestRemoveAllSourcesWithOneNameInTwoSourceSets.java @@ -0,0 +1,94 @@ +/* + * maddi: a modification analyzer for duplication detection and immutability. + * Copyright 2020-2026, CodeLaser BV, https://github.com/CodeLaser/maddi + * + * 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 io.codelaser.maddi.inspection.openjdk; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.net.URI; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import io.codelaser.maddi.cst.api.element.CompilationUnit; +import io.codelaser.maddi.cst.api.element.SourceSet; +import io.codelaser.maddi.cst.api.info.TypeInfo; +import io.codelaser.maddi.cst.api.runtime.Runtime; +import io.codelaser.maddi.cst.impl.runtime.RuntimeImpl; +import io.codelaser.maddi.inspection.resource.InfoByFqn; +import io.codelaser.maddi.inspection.resource.SourceSetImpl; + +/** + * {@link InfoByFqn#removeAllSources()} forgets every source type, also a name that two source sets both define. + * + *

Where it was found

+ * refactor.codelaser.io, dropping the project hibernate-orm--main (2026-11-13): + *
+ * WARN ProjectServiceImpl - Releasing the old model's inspector of hibernate-orm--main failed
+ * java.lang.UnsupportedOperationException
+ *     at java.util.ImmutableCollections$AbstractImmutableCollection.removeIf
+ *     at io.codelaser.maddi.inspection.resource.InfoByFqn.lambda$removeAllSources$0(InfoByFqn.java:83)
+ *     at io.codelaser.maddi.inspection.openjdk.JavaInspectorImpl.invalidateAllSources(JavaInspectorImpl.java:221)
+ * 
+ * A name defined in several source sets is registered with a list built by {@code List.of} or + * {@code Stream.toList()}, and those lists cannot be changed: {@code removeIf} on them throws. hibernate-orm has 60 + * source sets and many class names defined in more than one. The exception stops + * {@code JavaInspectorImpl.invalidateAllSources()} at its first line, so the javac file managers and the shared jar + * archives it should close stay open. + */ +public class TestRemoveAllSourcesWithOneNameInTwoSourceSets { + + @DisplayName("a name defined in two source sets is forgotten; a library type with another name stays") + @Test + public void bothSourceCopiesGoTheLibraryTypeStays() { + Runtime runtime = new RuntimeImpl(); + SourceSet main = sourceSet("core/main"); + SourceSet test = sourceSet("core/test"); + SourceSet jar = new SourceSetImpl.Builder().setName("lib-1.0.jar").setUri(URI.create("file:/lib-1.0.jar")) + .setLibrary(true).setExternalLibrary(true).build(); + InfoByFqn registry = new InfoByFqn(); + + TypeInfo inMain = type(runtime, main, "a", "Helper"); + registry.put(inMain.fullyQualifiedName(), inMain, main); + TypeInfo inTest = type(runtime, test, "a", "Helper"); + registry.put(inTest.fullyQualifiedName(), inTest, test); + TypeInfo library = type(runtime, jar, "lib", "Thing"); + registry.put(library.fullyQualifiedName(), library, main); + assertEquals(List.of(inMain, inTest), registry.allTypes("a.Helper"), + "set-up: both source sets define a.Helper, so the registry holds two copies of that name"); + + assertDoesNotThrow(registry::removeAllSources); + + assertEquals(List.of(), registry.allTypes("a.Helper"), + "a.Helper is defined only in the source sets core/main and core/test; after removeAllSources the" + + " registry must hold no copy of it"); + assertEquals(List.of(library), registry.allTypes("lib.Thing"), + "lib.Thing comes from lib-1.0.jar, which is not a source set; removeAllSources must keep it"); + } + + private static SourceSet sourceSet(String name) { + return new SourceSetImpl.Builder().setName(name).setUri(URI.create("file:/" + name)).build(); + } + + private static TypeInfo type(Runtime runtime, SourceSet sourceSet, String packageName, String simpleName) { + CompilationUnit cu = runtime.newCompilationUnitBuilder().setPackageName(packageName).setSourceSet(sourceSet) + .setURI(URI.create("file:/" + sourceSet.name() + "/" + simpleName + ".java")).build(); + return runtime.newTypeInfo(cu, simpleName); + } +} diff --git a/maddi-inspection-resource/src/main/java/io/codelaser/maddi/inspection/resource/InfoByFqn.java b/maddi-inspection-resource/src/main/java/io/codelaser/maddi/inspection/resource/InfoByFqn.java index 6677fb959..b9fa78913 100644 --- a/maddi-inspection-resource/src/main/java/io/codelaser/maddi/inspection/resource/InfoByFqn.java +++ b/maddi-inspection-resource/src/main/java/io/codelaser/maddi/inspection/resource/InfoByFqn.java @@ -80,7 +80,8 @@ public synchronized boolean markClassScannerSetupDone(TypeInfo typeInfo) { public synchronized void removeAllSources() { singleTypeByFqn.values().removeIf(InfoByFqn::isSourceType); - multiTypeByFqn.values().forEach(list -> list.removeIf(InfoByFqn::isSourceType)); + // rebuild: the lists in this map are immutable (List.of/Stream.toList), so removeIf would throw + multiTypeByFqn.replaceAll((fqn, list) -> list.stream().filter(ti -> !isSourceType(ti)).toList()); multiTypeByFqn.values().removeIf(List::isEmpty); classScannerSetupDone.removeIf(InfoByFqn::isSourceType); }