Skip to content

Commit aaa4391

Browse files
authored
Fix duplicate marketplace categories caused by case-sensitive displayName matching (#7334)
Category.merge() and findOrCreateCategory() used equals() to match categories by displayName, causing "AI" and "ai" to be treated as distinct categories. This resulted in duplicate category entries on the marketplace page when different recipe modules used different casing for the same category name (e.g. rewrite-java uses "AI" while rewrite-ai uses "ai"). Switch both methods to equalsIgnoreCase() so categories with the same name but different casing are properly merged. The first-seen casing is preserved.
1 parent d8dfde6 commit aaa4391

2 files changed

Lines changed: 46 additions & 2 deletions

File tree

rewrite-core/src/main/java/org/openrewrite/marketplace/RecipeMarketplace.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public void merge(Category category) {
7979
for (Category subCategory : category.categories) {
8080
Category existingSubCategory = null;
8181
for (Category c : categories) {
82-
if (c.getDisplayName().equals(subCategory.getDisplayName())) {
82+
if (c.getDisplayName().equalsIgnoreCase(subCategory.getDisplayName())) {
8383
existingSubCategory = c;
8484
break;
8585
}
@@ -154,7 +154,7 @@ public void install(RecipeListing recipe, List<CategoryDescriptor> categoryPath)
154154

155155
private Category findOrCreateCategory(CategoryDescriptor categoryDescriptor) {
156156
for (Category category : categories) {
157-
if (category.getDisplayName().equals(categoryDescriptor.getDisplayName())) {
157+
if (category.getDisplayName().equalsIgnoreCase(categoryDescriptor.getDisplayName())) {
158158
return category;
159159
}
160160
}

rewrite-core/src/test/java/org/openrewrite/marketplace/RecipeMarketplaceReaderTest.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,50 @@ void mergePreservesMetadata() {
436436
assertThat(mergedOther.getMetadata().get("embedding")).isEqualTo("xyz789");
437437
}
438438

439+
@Test
440+
void mergeCategoriesCaseInsensitive() {
441+
// Simulates the real-world scenario where rewrite-java defines category "AI"
442+
// and rewrite-ai defines category "ai" -- these should merge into one category
443+
RecipeMarketplace marketplace1 = new RecipeMarketplaceReader().fromCsv("""
444+
name,category1,ecosystem,packageName
445+
org.openrewrite.java.ai.ClassDefinitionLength,AI,maven,org.openrewrite:rewrite-java
446+
org.openrewrite.java.ai.MethodDefinitionLength,AI,maven,org.openrewrite:rewrite-java
447+
""");
448+
449+
RecipeMarketplace marketplace2 = new RecipeMarketplaceReader().fromCsv("""
450+
name,category1,ecosystem,packageName
451+
io.moderne.ai.FindAgentsInUse,ai,maven,io.moderne.recipe:rewrite-ai
452+
io.moderne.ai.FindLibrariesInUse,ai,maven,io.moderne.recipe:rewrite-ai
453+
""");
454+
455+
marketplace1.getRoot().merge(marketplace2.getRoot());
456+
457+
assertThat(marketplace1.getRoot().getCategories())
458+
.as("AI and ai should merge into a single category")
459+
.hasSize(1);
460+
assertThat(marketplace1.getRoot().getCategories().getFirst().getRecipes()).hasSize(4);
461+
// The first-seen casing wins
462+
assertThat(marketplace1.getRoot().getCategories().getFirst().getDisplayName()).isEqualTo("AI");
463+
}
464+
465+
@Test
466+
void installCategoriesCaseInsensitive() {
467+
// When installing recipes via CSV where one uses "AI" and another uses "ai"
468+
// in the same category position, they should land in the same category
469+
RecipeMarketplace marketplace = new RecipeMarketplaceReader().fromCsv("""
470+
name,category1,ecosystem,packageName
471+
org.example.Recipe1,AI,maven,org.example:test1
472+
org.example.Recipe2,ai,maven,org.example:test2
473+
""");
474+
475+
assertThat(marketplace.getRoot().getCategories())
476+
.as("AI and ai should be treated as the same category during install")
477+
.hasSize(1);
478+
assertThat(marketplace.getRoot().getCategories().getFirst().getRecipes()).hasSize(2);
479+
// The first-seen casing wins
480+
assertThat(marketplace.getRoot().getCategories().getFirst().getDisplayName()).isEqualTo("AI");
481+
}
482+
439483
private static RecipeMarketplace.Category findCategory(RecipeMarketplace.Category category, String name) {
440484
return category.getCategories().stream()
441485
.filter(c -> c.getDisplayName().equals(name))

0 commit comments

Comments
 (0)