From 416d401fc3b44f3b459ba9546d4f05b21851987b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 2 Feb 2026 10:39:36 +0000 Subject: [PATCH] Remove duplicate metadata and add check to prevent future duplicates Closes gh-49033 --- ...AdditionalSpringConfigurationMetadata.java | 3 +- ...heckManualSpringConfigurationMetadata.java | 3 +- .../ConfigurationPropertiesAnalyzer.java | 54 +++++++++++++++---- .../ConfigurationPropertiesAnalyzerTests.java | 41 ++++++++++++-- ...itional-spring-configuration-metadata.json | 11 ---- 5 files changed, 85 insertions(+), 27 deletions(-) diff --git a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/CheckAdditionalSpringConfigurationMetadata.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/CheckAdditionalSpringConfigurationMetadata.java index 3e8c8d77371..8a0ab6a7a31 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/CheckAdditionalSpringConfigurationMetadata.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/CheckAdditionalSpringConfigurationMetadata.java @@ -58,7 +58,8 @@ public abstract class CheckAdditionalSpringConfigurationMetadata extends SourceT void check() throws IOException { ConfigurationPropertiesAnalyzer analyzer = new ConfigurationPropertiesAnalyzer(getSource().getFiles()); Report report = new Report(this.projectDir); - analyzer.analyzeSort(report); + analyzer.analyzeOrder(report); + analyzer.analyzeDuplicates(report); analyzer.analyzeDeprecationSince(report); File reportFile = getReportLocation().get().getAsFile(); report.write(reportFile); diff --git a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/CheckManualSpringConfigurationMetadata.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/CheckManualSpringConfigurationMetadata.java index 0f61062c3a1..93b32b3baf0 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/CheckManualSpringConfigurationMetadata.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/CheckManualSpringConfigurationMetadata.java @@ -64,7 +64,8 @@ public abstract class CheckManualSpringConfigurationMetadata extends DefaultTask ConfigurationPropertiesAnalyzer analyzer = new ConfigurationPropertiesAnalyzer( List.of(getMetadataLocation().get())); Report report = new Report(this.projectDir); - analyzer.analyzeSort(report); + analyzer.analyzeOrder(report); + analyzer.analyzeDuplicates(report); analyzer.analyzePropertyDescription(report, getExclusions().get()); analyzer.analyzeDeprecationSince(report); File reportFile = getReportLocation().get().getAsFile(); diff --git a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationPropertiesAnalyzer.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationPropertiesAnalyzer.java index 6f073c90f03..04c44b47433 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationPropertiesAnalyzer.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationPropertiesAnalyzer.java @@ -25,9 +25,11 @@ import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Consumer; import com.fasterxml.jackson.core.type.TypeReference; @@ -40,15 +42,18 @@ import org.springframework.util.function.SingletonSupplier; /** * Check configuration metadata for inconsistencies. The available checks are: * * * @author Stephane Nicoll */ class ConfigurationPropertiesAnalyzer { + private static final List ELEMENT_TYPES = List.of("groups", "properties", "hints"); + private final Collection sources; private final SingletonSupplier objectMapperSupplier; @@ -61,23 +66,23 @@ class ConfigurationPropertiesAnalyzer { this.objectMapperSupplier = SingletonSupplier.of(ObjectMapper::new); } - void analyzeSort(Report report) throws IOException { + void analyzeOrder(Report report) throws IOException { for (File source : this.sources) { - report.registerAnalysis(source, analyzeSort(source)); + report.registerAnalysis(source, analyzeOrder(source)); } } - private Analysis analyzeSort(File source) throws IOException { + private Analysis analyzeOrder(File source) throws IOException { Map json = readJsonContent(source); Analysis analysis = new Analysis("Metadata element order:"); - analyzeMetadataElementsSort("groups", json, analysis); - analyzeMetadataElementsSort("properties", json, analysis); - analyzeMetadataElementsSort("hints", json, analysis); + for (String elementType : ELEMENT_TYPES) { + analyzeMetadataElementOrder(elementType, json, analysis); + } return analysis; } @SuppressWarnings("unchecked") - private void analyzeMetadataElementsSort(String key, Map json, Analysis analysis) { + private void analyzeMetadataElementOrder(String key, Map json, Analysis analysis) { List> groups = (List>) json.getOrDefault(key, Collections.emptyList()); List names = groups.stream().map((group) -> (String) group.get("name")).toList(); List sortedNames = names.stream().sorted().toList(); @@ -91,6 +96,35 @@ class ConfigurationPropertiesAnalyzer { } } + void analyzeDuplicates(Report report) throws IOException { + for (File source : this.sources) { + report.registerAnalysis(source, analyzeDuplicates(source)); + } + } + + private Analysis analyzeDuplicates(File source) throws IOException { + Map json = readJsonContent(source); + Analysis analysis = new Analysis("Metadata element duplicates:"); + for (String elementType : ELEMENT_TYPES) { + analyzeMetadataElementDuplicates(elementType, json, analysis); + } + return analysis; + } + + @SuppressWarnings("unchecked") + private void analyzeMetadataElementDuplicates(String key, Map json, Analysis analysis) { + List> elements = (List>) json.getOrDefault(key, + Collections.emptyList()); + List names = elements.stream().map((group) -> (String) group.get("name")).toList(); + Set uniqueNames = new HashSet<>(); + for (int i = 0; i < names.size(); i++) { + String name = names.get(i); + if (!uniqueNames.add(name)) { + analysis.addItem("Duplicate name '" + name + "' at $." + key + "[" + i + "]"); + } + } + } + void analyzePropertyDescription(Report report, List exclusions) throws IOException { for (File source : this.sources) { report.registerAnalysis(source, analyzePropertyDescription(source, exclusions)); diff --git a/buildSrc/src/test/java/org/springframework/boot/build/context/properties/ConfigurationPropertiesAnalyzerTests.java b/buildSrc/src/test/java/org/springframework/boot/build/context/properties/ConfigurationPropertiesAnalyzerTests.java index 959ba4ad6c9..d3adc151b82 100644 --- a/buildSrc/src/test/java/org/springframework/boot/build/context/properties/ConfigurationPropertiesAnalyzerTests.java +++ b/buildSrc/src/test/java/org/springframework/boot/build/context/properties/ConfigurationPropertiesAnalyzerTests.java @@ -46,7 +46,7 @@ class ConfigurationPropertiesAnalyzerTests { } @Test - void analyzeSortWithAlphabeticalOrder(@TempDir File tempDir) throws IOException { + void analyzeOrderWithAlphabeticalOrder(@TempDir File tempDir) throws IOException { File metadata = new File(tempDir, "metadata.json"); Files.writeString(metadata.toPath(), """ { "properties": [ @@ -55,14 +55,14 @@ class ConfigurationPropertiesAnalyzerTests { }"""); Report report = new Report(tempDir); ConfigurationPropertiesAnalyzer analyzer = new ConfigurationPropertiesAnalyzer(List.of(metadata)); - analyzer.analyzeSort(report); + analyzer.analyzeOrder(report); assertThat(report.hasProblems()).isFalse(); assertThat(report.getAnalyses(metadata)).singleElement() .satisfies(((analysis) -> assertThat(analysis.getItems()).isEmpty())); } @Test - void analyzeSortWithViolations(@TempDir File tempDir) throws IOException { + void analyzeOrderWithViolations(@TempDir File tempDir) throws IOException { File metadata = new File(tempDir, "metadata.json"); Files.writeString(metadata.toPath(), """ { "properties": [ @@ -71,7 +71,7 @@ class ConfigurationPropertiesAnalyzerTests { }"""); Report report = new Report(tempDir); ConfigurationPropertiesAnalyzer analyzer = new ConfigurationPropertiesAnalyzer(List.of(metadata)); - analyzer.analyzeSort(report); + analyzer.analyzeOrder(report); assertThat(report.hasProblems()).isTrue(); assertThat(report.getAnalyses(metadata)).singleElement() .satisfies((analysis) -> assertThat(analysis.getItems()).containsExactly( @@ -79,6 +79,39 @@ class ConfigurationPropertiesAnalyzerTests { "Wrong order at $.properties[1].name - expected 'def' but found 'abc'")); } + @Test + void analyzeDuplicatesWithNoDuplicates(@TempDir File tempDir) throws IOException { + File metadata = new File(tempDir, "metadata.json"); + Files.writeString(metadata.toPath(), """ + { "properties": [ + { "name": "abc"}, {"name": "def"}, {"name": "xyz"} + ] + }"""); + Report report = new Report(tempDir); + ConfigurationPropertiesAnalyzer analyzer = new ConfigurationPropertiesAnalyzer(List.of(metadata)); + analyzer.analyzeOrder(report); + assertThat(report.hasProblems()).isFalse(); + assertThat(report.getAnalyses(metadata)).singleElement() + .satisfies(((analysis) -> assertThat(analysis.getItems()).isEmpty())); + } + + @Test + void analyzeDuplicatesWithDuplicate(@TempDir File tempDir) throws IOException { + File metadata = new File(tempDir, "metadata.json"); + Files.writeString(metadata.toPath(), """ + { "properties": [ + { "name": "abc"}, {"name": "abc"}, {"name": "def"} + ] + }"""); + Report report = new Report(tempDir); + ConfigurationPropertiesAnalyzer analyzer = new ConfigurationPropertiesAnalyzer(List.of(metadata)); + analyzer.analyzeDuplicates(report); + assertThat(report.hasProblems()).isTrue(); + assertThat(report.getAnalyses(metadata)).singleElement() + .satisfies((analysis) -> assertThat(analysis.getItems()) + .containsExactly("Duplicate name 'abc' at $.properties[1]")); + } + @Test void analyzePropertyDescription(@TempDir File tempDir) throws IOException { File metadata = new File(tempDir, "metadata.json"); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 5179128a800..ba1912b889f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -3377,17 +3377,6 @@ } ] }, - { - "name": "spring.datasource.xa.data-source-class-name", - "providers": [ - { - "name": "class-reference", - "parameters": { - "target": "javax.sql.XADataSource" - } - } - ] - }, { "name": "spring.graphql.cors.allowed-headers", "values": [