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..bf09a2ce63 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; }); + NormalizeAcronymCasing(generatedCode); RemoveClassNamePrefixFromNestedClasses(generatedCode); InsertOverrideMethodForRequestExecutorsAndBuildersAndConstructors(generatedCode); ReplaceIndexersByMethodsWithParameter(generatedCode, @@ -552,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); + } } 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() {