From d59aa881a21a889744dae657b68f46c87a63790b Mon Sep 17 00:00:00 2001
From: ramsessanchez <63934382+ramsessanchez@users.noreply.github.com>
Date: Tue, 28 Apr 2026 10:56:23 -0700
Subject: [PATCH 1/2] fix(Java): normalize PascalCase acronyms in class/enum
names to prevent casing mismatch
When OpenAPI metadata changes acronym casing (e.g. Dlp to DLP), git with
core.ignorecase=true preserves the old file name on disk while the class
declaration uses the new casing, causing Java compilation failures.
Normalize consecutive uppercase acronyms in model class and enum names
during Java refinement so the generated output is deterministic regardless
of upstream acronym casing changes.
Fixes #7654
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../Extensions/StringExtensions.cs | 24 +++++++++++++++++
src/Kiota.Builder/Refiners/JavaRefiner.cs | 1 +
.../Extensions/StringExtensionsTests.cs | 19 +++++++++++++
.../Refiners/JavaLanguageRefinerTests.cs | 27 +++++++++++++++++++
4 files changed, 71 insertions(+)
diff --git a/src/Kiota.Builder/Extensions/StringExtensions.cs b/src/Kiota.Builder/Extensions/StringExtensions.cs
index 3d3a295114..45f5e561a8 100644
--- a/src/Kiota.Builder/Extensions/StringExtensions.cs
+++ b/src/Kiota.Builder/Extensions/StringExtensions.cs
@@ -20,6 +20,30 @@ public static string ToFirstCharacterLowerCase(this string? input)
public static string ToFirstCharacterUpperCase(this string? input)
=> string.IsNullOrEmpty(input) ? string.Empty : char.ToUpperInvariant(input[0]) + input[1..];
+ ///
+ /// Normalizes PascalCase names by lowering consecutive uppercase acronyms so that only
+ /// the first letter of each acronym remains uppercase (e.g., "ComplianceDLPApplications" becomes
+ /// "ComplianceDlpApplications"). When the last uppercase letter in a run is followed by a
+ /// lowercase letter, it is kept uppercase because it starts a new word.
+ ///
+ public static string NormalizePascalCaseAcronyms(this string? input)
+ {
+ if (string.IsNullOrEmpty(input) || input.Length < 2) return input ?? string.Empty;
+
+ var result = input.ToCharArray();
+ for (var i = 1; i < input.Length; i++)
+ {
+ if (char.IsUpper(input[i]) && char.IsUpper(input[i - 1]))
+ {
+ // Keep uppercase if this is the last uppercase letter before a lowercase letter (new word start)
+ if (i + 1 < input.Length && char.IsLower(input[i + 1]))
+ continue;
+ result[i] = char.ToLowerInvariant(input[i]);
+ }
+ }
+ return new string(result);
+ }
+
private static readonly char[] defaultSeparators = ['-'];
///
/// Converts a string delimited by a symbol to camel case, conserving the casing for the first character
diff --git a/src/Kiota.Builder/Refiners/JavaRefiner.cs b/src/Kiota.Builder/Refiners/JavaRefiner.cs
index 123a2e8fc6..c57ee36a96 100644
--- a/src/Kiota.Builder/Refiners/JavaRefiner.cs
+++ b/src/Kiota.Builder/Refiners/JavaRefiner.cs
@@ -60,6 +60,7 @@ public override Task RefineAsync(CodeNamespace generatedCode, CancellationToken
else
return s;
});
+ CorrectNames(generatedCode, s => s.NormalizePascalCaseAcronyms());
RemoveClassNamePrefixFromNestedClasses(generatedCode);
InsertOverrideMethodForRequestExecutorsAndBuildersAndConstructors(generatedCode);
ReplaceIndexersByMethodsWithParameter(generatedCode,
diff --git a/tests/Kiota.Builder.Tests/Extensions/StringExtensionsTests.cs b/tests/Kiota.Builder.Tests/Extensions/StringExtensionsTests.cs
index 9a1197998d..3a47d89471 100644
--- a/tests/Kiota.Builder.Tests/Extensions/StringExtensionsTests.cs
+++ b/tests/Kiota.Builder.Tests/Extensions/StringExtensionsTests.cs
@@ -47,6 +47,25 @@ public void ToPascalCase()
Assert.Equal("Toto", "toto".ToPascalCase());
Assert.Equal("TotoPascalCase", "toto-pascal-case".ToPascalCase());
}
+ [Theory]
+ [InlineData(null, "")]
+ [InlineData("", "")]
+ [InlineData("A", "A")]
+ [InlineData("Ab", "Ab")]
+ [InlineData("ComplianceDLPApplicationsAuditRecord", "ComplianceDlpApplicationsAuditRecord")]
+ [InlineData("PowerBIAuditRecord", "PowerBiAuditRecord")]
+ [InlineData("PowerBIDlpAuditRecord", "PowerBiDlpAuditRecord")]
+ [InlineData("OnPremisesSharePointScannerDLPAuditRecord", "OnPremisesSharePointScannerDlpAuditRecord")]
+ [InlineData("ComplianceDLPExchangeClassificationCdpRecord", "ComplianceDlpExchangeClassificationCdpRecord")]
+ [InlineData("XMLHTTPRequest", "XmlhttpRequest")]
+ [InlineData("AuditRecord", "AuditRecord")]
+ [InlineData("somemodel", "somemodel")]
+ [InlineData("ABC", "Abc")]
+ [InlineData("ABCDef", "AbcDef")]
+ public void NormalizePascalCaseAcronyms(string input, string expected)
+ {
+ Assert.Equal(expected, input.NormalizePascalCaseAcronyms());
+ }
[Fact]
public void ToPascalCaseCustomSeparator()
{
diff --git a/tests/Kiota.Builder.Tests/Refiners/JavaLanguageRefinerTests.cs b/tests/Kiota.Builder.Tests/Refiners/JavaLanguageRefinerTests.cs
index 938adf9783..3571186d1a 100644
--- a/tests/Kiota.Builder.Tests/Refiners/JavaLanguageRefinerTests.cs
+++ b/tests/Kiota.Builder.Tests/Refiners/JavaLanguageRefinerTests.cs
@@ -616,6 +616,33 @@ public async Task ProduceCorrectNamesAsync()
await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Java }, root, cancellationToken: TestContext.Current.CancellationToken);
Assert.True(string.IsNullOrEmpty(model.Properties.First(static x => "custom".Equals(x.Name))!.NamePrefix));
}
+ [Theory]
+ [InlineData("ComplianceDLPApplicationsAuditRecord", "ComplianceDlpApplicationsAuditRecord")]
+ [InlineData("PowerBIAuditRecord", "PowerBiAuditRecord")]
+ [InlineData("OnPremisesSharePointScannerDLPAuditRecord", "OnPremisesSharePointScannerDlpAuditRecord")]
+ [InlineData("SimpleModel", "SimpleModel")]
+ public async Task NormalizesModelClassAcronymCasingAsync(string originalName, string expectedName)
+ {
+ var model = root.AddClass(new CodeClass
+ {
+ Name = originalName,
+ Kind = CodeClassKind.Model
+ }).First();
+ await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Java }, root, cancellationToken: TestContext.Current.CancellationToken);
+ Assert.Equal(expectedName, model.Name);
+ }
+ [Theory]
+ [InlineData("AuditLogRecordDLP", "AuditLogRecordDlp")]
+ [InlineData("SimpleEnum", "SimpleEnum")]
+ public async Task NormalizesEnumAcronymCasingAsync(string originalName, string expectedName)
+ {
+ var model = root.AddEnum(new CodeEnum
+ {
+ Name = originalName,
+ }).First();
+ await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Java }, root, cancellationToken: TestContext.Current.CancellationToken);
+ Assert.Equal(expectedName, model.Name);
+ }
[Fact]
public async Task AddsMethodsOverloadsAsync()
{
From ecfa20d038302f2b9235c647469298ef889c185d Mon Sep 17 00:00:00 2001
From: ramsessanchez <63934382+ramsessanchez@users.noreply.github.com>
Date: Wed, 29 Apr 2026 13:23:24 -0700
Subject: [PATCH 2/2] fix: use dedicated NormalizeAcronymCasing to bypass
case-insensitive duplicate check
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
CorrectNames uses FindChildByName which relies on InnerChildElements
(OrdinalIgnoreCase dictionary). For case-only renames like DLP→Dlp,
FindChildByName finds the existing element and blocks the rename.
NormalizeAcronymCasing skips the duplicate check since case-only renames
are safe with the OrdinalIgnoreCase dictionary - RenameChildElement
removes by old key and re-adds with new casing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
src/Kiota.Builder/Refiners/JavaRefiner.cs | 25 ++++++++++++++++++++++-
1 file changed, 24 insertions(+), 1 deletion(-)
diff --git a/src/Kiota.Builder/Refiners/JavaRefiner.cs b/src/Kiota.Builder/Refiners/JavaRefiner.cs
index c57ee36a96..bf09a2ce63 100644
--- a/src/Kiota.Builder/Refiners/JavaRefiner.cs
+++ b/src/Kiota.Builder/Refiners/JavaRefiner.cs
@@ -60,7 +60,7 @@ public override Task RefineAsync(CodeNamespace generatedCode, CancellationToken
else
return s;
});
- CorrectNames(generatedCode, s => s.NormalizePascalCaseAcronyms());
+ NormalizeAcronymCasing(generatedCode);
RemoveClassNamePrefixFromNestedClasses(generatedCode);
InsertOverrideMethodForRequestExecutorsAndBuildersAndConstructors(generatedCode);
ReplaceIndexersByMethodsWithParameter(generatedCode,
@@ -553,4 +553,27 @@ private void AddQueryParameterExtractorMethod(CodeElement currentElement, string
}
CrawlTree(currentElement, x => AddQueryParameterExtractorMethod(x, methodName));
}
+ ///
+ /// Normalizes acronym casing in class and enum names to prevent file name mismatches
+ /// when upstream metadata changes acronym casing (e.g., powerBi → powerBI).
+ /// Unlike CorrectNames, this allows case-only renames since InnerChildElements uses OrdinalIgnoreCase.
+ ///
+ private static void NormalizeAcronymCasing(CodeElement current)
+ {
+ if (current is CodeClass currentClass &&
+ currentClass.Name.NormalizePascalCaseAcronyms() is string refinedClassName &&
+ !currentClass.Name.Equals(refinedClassName, StringComparison.Ordinal) &&
+ currentClass.Parent is IBlock classParentBlock)
+ {
+ classParentBlock.RenameChildElement(currentClass.Name, refinedClassName);
+ }
+ else if (current is CodeEnum currentEnum &&
+ currentEnum.Name.NormalizePascalCaseAcronyms() is string refinedEnumName &&
+ !currentEnum.Name.Equals(refinedEnumName, StringComparison.Ordinal) &&
+ currentEnum.Parent is IBlock enumParentBlock)
+ {
+ enumParentBlock.RenameChildElement(currentEnum.Name, refinedEnumName);
+ }
+ CrawlTree(current, NormalizeAcronymCasing);
+ }
}