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); + } }