diff --git a/.github/workflows/surface-area-tests.yml b/.github/workflows/surface-area-tests.yml index e2185e3250..86a836464e 100644 --- a/.github/workflows/surface-area-tests.yml +++ b/.github/workflows/surface-area-tests.yml @@ -68,14 +68,18 @@ jobs: shell: pwsh timeout-minutes: 30 run: | - $patchPath = Join-Path -Path (Get-Location) -ChildPath "${{ matrix.language }}-dom-export.patch" + $patchFileName = "${{ matrix.language }}-dom-export.patch" + $patchPath = Join-Path -Path (Get-Location) -ChildPath $patchFileName ./it/compare-dom-export.ps1 ` -descriptionUrl "${{ matrix.description }}" ` -language "${{ matrix.language }}" ` -patchPath $patchPath ` -baselineVersion "${{ github.event.inputs.baselineVersion }}" $hasPatch = (Test-Path $patchPath) -and ((Get-Item $patchPath).Length -gt 0) - Write-Output "patchFilePath=$patchPath" >> $Env:GITHUB_OUTPUT + # The diff tool runs as a Docker container action with the repository mounted at + # /github/workspace, so it must receive a workspace-relative path. Passing the + # absolute host path fails inside the container with a DirectoryNotFoundException. + Write-Output "patchFilePath=$patchFileName" >> $Env:GITHUB_OUTPUT Write-Output "hasPatch=$($hasPatch.ToString().ToLowerInvariant())" >> $Env:GITHUB_OUTPUT - name: Analyze DOM export diff for breaking changes diff --git a/CHANGELOG.md b/CHANGELOG.md index fa0370342d..128bb9ecc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Reduced the aggressiveness of namespace segment and type name shortening by raising the length threshold to 200 characters, so only names longer than 200 characters are truncated (to a 191-character prefix plus an underscore and 8-character hash). The threshold stays well below the 255 file-system per-component limit to leave room for extensions and language-specific suffixes appended by path segmenters and compilers (e.g. Java nested-class `.class` files and the extra underscores Python adds when converting names to snake_case). + ## [1.34.0] - 2026-07-08 ### Added diff --git a/src/Kiota.Builder/Extensions/StringExtensions.cs b/src/Kiota.Builder/Extensions/StringExtensions.cs index bec39ad53e..3f6fc87d05 100644 --- a/src/Kiota.Builder/Extensions/StringExtensions.cs +++ b/src/Kiota.Builder/Extensions/StringExtensions.cs @@ -75,14 +75,18 @@ public static string ShortenFileName(this string name, int length = 251) => (!string.IsNullOrEmpty(name) && name.Length > length) ? HashString(name).ToLowerInvariant() : name; #pragma warning restore CA1308 - private const int DefaultMaxNameSegmentLength = 64; + // 200 keeps the shortened name well below the 255 file-system per-component limit (NAME_MAX), + // leaving room for extensions and language-specific suffixes that path segmenters and compilers + // append (e.g. ".java" plus nested-class suffixes like "$GetQueryParameters.class", or the extra + // underscores Python introduces when converting names to snake_case file names). + private const int DefaultMaxNameSegmentLength = 200; private const int HashSuffixLength = 8; /// /// Shortens a name segment to the specified maximum length by truncating and appending a short hash suffix. /// Preserves human readability by keeping the first part of the original name. /// /// The name to potentially shorten - /// The maximum allowed length. Default is 64. + /// The maximum allowed length. Default is 200. /// The original name if within limits, or a truncated name with hash suffix for uniqueness public static string ShortenNameSegment(this string name, int maxLength = DefaultMaxNameSegmentLength) { diff --git a/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs b/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs index f8e8015320..6d5e270c4d 100644 --- a/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs +++ b/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs @@ -1635,7 +1635,7 @@ protected static void DeduplicateErrorMappings(CodeElement codeElement) /// that map only namespaces to directories and group types into barrel files, where renaming types would /// break downstream name-based generation without any file-system benefit. /// - protected static void ShortenOversizedNamespaceSegments(CodeElement currentElement, int maxSegmentLength = 64, bool shortenTypeNames = true) + protected static void ShortenOversizedNamespaceSegments(CodeElement currentElement, int maxSegmentLength = 200, bool shortenTypeNames = true) { if (currentElement is CodeNamespace codeNamespace && !string.IsNullOrEmpty(codeNamespace.Name)) { diff --git a/tests/Kiota.Builder.Tests/Extensions/StringExtensionsTests.cs b/tests/Kiota.Builder.Tests/Extensions/StringExtensionsTests.cs index 7c3dab379e..62921af62e 100644 --- a/tests/Kiota.Builder.Tests/Extensions/StringExtensionsTests.cs +++ b/tests/Kiota.Builder.Tests/Extensions/StringExtensionsTests.cs @@ -1,4 +1,6 @@ -using Kiota.Builder.Extensions; +using System.Linq; + +using Kiota.Builder.Extensions; using Xunit; @@ -140,23 +142,23 @@ public void ShortenNameSegmentReturnsOriginalWhenUnderLimit() [Fact] public void ShortenNameSegmentReturnsOriginalAtExactLimit() { - var name = new string('a', 64); + var name = new string('a', 200); Assert.Equal(name, name.ShortenNameSegment()); } [Fact] public void ShortenNameSegmentTruncatesAndAppendsHashWhenOverLimit() { - var longName = "MicrosoftGraphNetworkaccessDeviceReportWithStartDateTimeWithEndDateTimeWithDiscoveredApplicationSegmentId"; + var longName = string.Concat(Enumerable.Repeat("MicrosoftGraphNetworkaccessDeviceReportSegment", 8)); // 368 chars > 200 var result = longName.ShortenNameSegment(); - Assert.Equal(64, result.Length); - // First 55 chars are preserved (64 - 8 hash - 1 underscore) - Assert.StartsWith("MicrosoftGraphNetworkaccessDeviceReportWithStartDateTim", result); - Assert.Contains("_", result); + Assert.Equal(200, result.Length); + // First 191 chars are preserved (200 - 8 hash - 1 underscore) + Assert.StartsWith(longName[..191], result); + Assert.Equal('_', result[191]); } [Fact] public void ShortenNameSegmentIsDeterministic() { - var longName = "MicrosoftGraphNetworkaccessDeviceReportWithStartDateTimeWithEndDateTimeWithDiscoveredApplicationSegmentId"; + var longName = string.Concat(Enumerable.Repeat("MicrosoftGraphNetworkaccessDeviceReportSegment", 8)); // 368 chars > 255 var result1 = longName.ShortenNameSegment(); var result2 = longName.ShortenNameSegment(); Assert.Equal(result1, result2); @@ -164,8 +166,8 @@ public void ShortenNameSegmentIsDeterministic() [Fact] public void ShortenNameSegmentProducesDifferentResultsForDifferentInputs() { - var name1 = "MicrosoftGraphNetworkaccessDeviceReportWithStartDateTimeWithEndDateTimeWithDiscoveredApplicationSegmentId"; - var name2 = "MicrosoftGraphNetworkaccessDeviceReportWithStartDateTimeWithEndDateTimeWithApplicationId"; + var name1 = string.Concat(Enumerable.Repeat("MicrosoftGraphNetworkaccessDeviceReportSegment", 8)) + "One"; // > 255 + var name2 = string.Concat(Enumerable.Repeat("MicrosoftGraphNetworkaccessDeviceReportSegment", 8)) + "Two"; // > 255 var result1 = name1.ShortenNameSegment(); var result2 = name2.ShortenNameSegment(); Assert.NotEqual(result1, result2); diff --git a/tests/Kiota.Builder.Tests/Refiners/JavaLanguageRefinerTests.cs b/tests/Kiota.Builder.Tests/Refiners/JavaLanguageRefinerTests.cs index 4edf0c13c2..c433c1ba83 100644 --- a/tests/Kiota.Builder.Tests/Refiners/JavaLanguageRefinerTests.cs +++ b/tests/Kiota.Builder.Tests/Refiners/JavaLanguageRefinerTests.cs @@ -774,7 +774,7 @@ public async Task AddsUsingForUntypedNodeInMethodParameterAsync() public async Task ShortensOversizedNamespaceSegmentsAsync() { // Simulate the problematic long namespace segment from OData functions with many parameters - var longSegment = "microsoftgraphnetworkaccessdevicereportwithstartdatetimewithenddatetimediscoveredapplicationsegmentiddiscoveredapplicationsegmentidapplicationidapplicationidaiagentidaiagentidaiagentnameaiagentnamecloudapplicationnamecloudapplicationname"; + var longSegment = string.Concat(Enumerable.Repeat("microsoftgraphnetworkaccessdevicereportsegment", 6)); // 276 chars > 255 var childNs = root.AddNamespace($"com.microsoft.graph.beta.networkaccess.reports.{longSegment}"); var requestBuilderClass = childNs.AddClass(new CodeClass { @@ -791,11 +791,11 @@ public async Task ShortensOversizedNamespaceSegmentsAsync() var nameSegments = childNs.Name.Split('.'); foreach (var segment in nameSegments) { - Assert.True(segment.Length <= 64, $"Segment '{segment}' exceeds 64 chars (length: {segment.Length})"); + Assert.True(segment.Length <= 200, $"Segment '{segment}' exceeds 200 chars (length: {segment.Length})"); } // Class name should be shortened - Assert.True(requestBuilderClass.Name.Length <= 64, $"Class name '{requestBuilderClass.Name}' exceeds 64 chars (length: {requestBuilderClass.Name.Length})"); + Assert.True(requestBuilderClass.Name.Length <= 200, $"Class name '{requestBuilderClass.Name}' exceeds 200 chars (length: {requestBuilderClass.Name.Length})"); // Doc comment should contain original name for disambiguation Assert.Contains("Original name:", requestBuilderClass.Documentation.DescriptionTemplate); @@ -816,7 +816,7 @@ public async Task DoesNotShortenNormalLengthNamespacesAsync() [Fact] public async Task ShorteningIsDeterministicAsync() { - var longSegment = "microsoftgraphnetworkaccessdevicereportwithstartdatetimewithenddatetimediscoveredapplicationsegmentiddiscoveredapplicationsegmentid"; + var longSegment = string.Concat(Enumerable.Repeat("microsoftgraphnetworkaccessdevicereportsegment", 6)); // 276 chars > 255 var root2 = CodeNamespace.InitRootNamespace(); var childNs1 = root.AddNamespace($"com.microsoft.graph.beta.{longSegment}"); var childNs2 = root2.AddNamespace($"com.microsoft.graph.beta.{longSegment}"); @@ -830,7 +830,7 @@ public async Task ShorteningIsDeterministicAsync() public async Task ShortenedClassNameIsReflectedInMethodReturnTypeAsync() { // Simulate: parent request builder has a method returning a child request builder with a long name - var longClassName = "MicrosoftGraphNetworkaccessDeviceReportWithStartDateTimeWithEndDateTimeRequestBuilder"; + var longClassName = string.Concat(Enumerable.Repeat("MicrosoftGraphNetworkAccessDeviceReportSegment", 6)) + "RequestBuilder"; // > 255 var parentNs = root.AddNamespace("com.microsoft.graph.beta.networkaccess.reports"); var longSegment = "microsoftgraphnetworkaccessdevicereportwithstartdatetimewithenddatetime"; var childNs = root.AddNamespace($"com.microsoft.graph.beta.networkaccess.reports.{longSegment}"); @@ -861,16 +861,16 @@ public async Task ShortenedClassNameIsReflectedInMethodReturnTypeAsync() await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Java }, root, cancellationToken: TestContext.Current.CancellationToken); // The class should be shortened - Assert.True(targetClass.Name.Length <= 64, $"Class name '{targetClass.Name}' exceeds 64 chars"); + Assert.True(targetClass.Name.Length <= 200, $"Class name '{targetClass.Name}' exceeds 200 chars"); // The method's return type should automatically reflect the shortened name via TypeDefinition Assert.Equal(targetClass.Name, returnType.Name); - Assert.True(returnType.Name.Length <= 64, $"Return type name '{returnType.Name}' exceeds 64 chars"); + Assert.True(returnType.Name.Length <= 200, $"Return type name '{returnType.Name}' exceeds 200 chars"); } [Fact] public async Task ShorteningWorksWhenDocumentationIsNullAsync() { - var longSegment = "microsoftgraphnetworkaccessdevicereportwithstartdatetimewithenddatetimediscoveredapplicationsegmentiddiscoveredapplicationsegmentid"; + var longSegment = string.Concat(Enumerable.Repeat("microsoftgraphnetworkaccessdevicereportsegment", 6)); // 276 chars > 255 var childNs = root.AddNamespace($"com.microsoft.graph.beta.{longSegment}"); var requestBuilderClass = childNs.AddClass(new CodeClass { @@ -883,7 +883,7 @@ public async Task ShorteningWorksWhenDocumentationIsNullAsync() await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Java }, root, cancellationToken: TestContext.Current.CancellationToken); // Should shorten without throwing - Assert.True(requestBuilderClass.Name.Length <= 64); + Assert.True(requestBuilderClass.Name.Length <= 200); } #endregion } diff --git a/tests/Kiota.Builder.Tests/Refiners/PhpLanguageRefinerTests.cs b/tests/Kiota.Builder.Tests/Refiners/PhpLanguageRefinerTests.cs index dcf5d44160..66067b1f7a 100644 --- a/tests/Kiota.Builder.Tests/Refiners/PhpLanguageRefinerTests.cs +++ b/tests/Kiota.Builder.Tests/Refiners/PhpLanguageRefinerTests.cs @@ -303,7 +303,7 @@ public async Task DoesNotCreateDuplicateComposedTypeWrapperIfOneAlreadyExistsAsy [Fact] public async Task ShortensOversizedNamespaceSegmentsAsync() { - var longSegment = "MicrosoftGraphNetworkaccessDeviceReportWithStartDateTimeWithEndDateTimeDiscoveredApplicationSegmentIdDiscoveredApplicationSegmentIdApplicationIdApplicationId"; + var longSegment = string.Concat(Enumerable.Repeat("MicrosoftGraphNetworkAccessDeviceReportSegment", 6)); // 276 chars > 255 var childNs = root.AddNamespace($"ApiSdk.NetworkAccess.Reports.{longSegment}"); var requestBuilderClass = childNs.AddClass(new CodeClass { @@ -319,18 +319,18 @@ public async Task ShortensOversizedNamespaceSegmentsAsync() // Namespace segment should be shortened foreach (var segment in childNs.Name.Split('.')) { - Assert.True(segment.Length <= 64, $"Segment '{segment}' exceeds 64 chars (length: {segment.Length})"); + Assert.True(segment.Length <= 200, $"Segment '{segment}' exceeds 200 chars (length: {segment.Length})"); } // Class name should be shortened - Assert.True(requestBuilderClass.Name.Length <= 64, $"Class name '{requestBuilderClass.Name}' exceeds 64 chars"); + Assert.True(requestBuilderClass.Name.Length <= 200, $"Class name '{requestBuilderClass.Name}' exceeds 200 chars"); } [Fact] public async Task ShortenedClassImportsAreConsistentAsync() { // Parent request builder references a child with a long name via method return type var parentNs = root.AddNamespace("ApiSdk.NetworkAccess.Reports"); - var longSegment = "MicrosoftGraphNetworkaccessDeviceReportWithStartDateTimeWithEndDateTimeDiscoveredApplicationSegmentId"; + var longSegment = string.Concat(Enumerable.Repeat("MicrosoftGraphNetworkAccessDeviceReportSegment", 6)); // 276 chars > 255 var childNs = root.AddNamespace($"ApiSdk.NetworkAccess.Reports.{longSegment}"); var targetClass = childNs.AddClass(new CodeClass @@ -363,7 +363,7 @@ public async Task ShortenedClassImportsAreConsistentAsync() var nsSegments = u.Name.Split('.'); foreach (var seg in nsSegments) { - Assert.True(seg.Length <= 64, $"Using namespace segment '{seg}' exceeds 64 chars — stale pre-shortened name"); + Assert.True(seg.Length <= 200, $"Using namespace segment '{seg}' exceeds 200 chars — stale pre-shortened name"); } } } diff --git a/tests/Kiota.Builder.Tests/Refiners/PythonLanguageRefinerTests.cs b/tests/Kiota.Builder.Tests/Refiners/PythonLanguageRefinerTests.cs index 8a24fc95ab..4f4e158147 100644 --- a/tests/Kiota.Builder.Tests/Refiners/PythonLanguageRefinerTests.cs +++ b/tests/Kiota.Builder.Tests/Refiners/PythonLanguageRefinerTests.cs @@ -652,4 +652,85 @@ public async Task ReplacesUntypedNodeInMethodParameterAndReturnTypeAsync() Assert.Equal("bytes", method.ReturnType.Name);// return type is renamed to use the stream type } #endregion + #region NameShortening + [Fact] + public async Task ShortensOversizedNamespaceSegmentsAsync() + { + // Simulate the problematic long namespace segment from OData functions with many parameters + var longSegment = string.Concat(Enumerable.Repeat("microsoftgraphnetworkaccessdevicereportsegment", 6)); // 276 chars > 255 + var childNs = root.AddNamespace($"graph.networkaccess.reports.{longSegment}"); + var requestBuilderClass = childNs.AddClass(new CodeClass + { + Name = $"{longSegment}RequestBuilder", + Kind = CodeClassKind.RequestBuilder, + Documentation = new() + { + DescriptionTemplate = "Builds and executes requests for operations under /networkAccess/reports/...", + }, + }).First(); + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Python }, root, cancellationToken: TestContext.Current.CancellationToken); + + // Namespace segment should be shortened + foreach (var segment in childNs.Name.Split('.')) + { + Assert.True(segment.Length <= 200, $"Segment '{segment}' exceeds 200 chars (length: {segment.Length})"); + } + + // Class name should be shortened + Assert.True(requestBuilderClass.Name.Length <= 200, $"Class name '{requestBuilderClass.Name}' exceeds 200 chars (length: {requestBuilderClass.Name.Length})"); + + // Doc comment should contain original name for disambiguation + Assert.Contains("Original name:", requestBuilderClass.Documentation.DescriptionTemplate); + } + [Fact] + public async Task DoesNotShortenNormalLengthNamespacesAsync() + { + var childNs = root.AddNamespace("graph.networkaccess.users"); + childNs.AddClass(new CodeClass + { + Name = "UsersRequestBuilder", + Kind = CodeClassKind.RequestBuilder, + }); + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Python }, root, cancellationToken: TestContext.Current.CancellationToken); + + Assert.Equal("graph.networkaccess.users", childNs.Name); + } + [Fact] + public async Task DoesNotShortenSegmentsWithinThresholdAsync() + { + // 150 characters: above the previous 64/128 thresholds but within the current 200 threshold, + // so it must be preserved verbatim (both namespace segment and type name). + var mediumSegment = new string('a', 150); + var childNs = root.AddNamespace($"graph.networkaccess.{mediumSegment}"); + var requestBuilderClass = childNs.AddClass(new CodeClass + { + Name = mediumSegment, + Kind = CodeClassKind.RequestBuilder, + }).First(); + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Python }, root, cancellationToken: TestContext.Current.CancellationToken); + + Assert.EndsWith($".{mediumSegment}", childNs.Name); + // The type name is left intact (case normalization aside) — not hash-shortened. + Assert.Equal(mediumSegment.Length, requestBuilderClass.Name.Length); + Assert.Equal(mediumSegment, requestBuilderClass.Name, ignoreCase: true); + } + [Fact] + public async Task ShorteningWorksWhenDocumentationIsNullAsync() + { + var longSegment = string.Concat(Enumerable.Repeat("microsoftgraphnetworkaccessdevicereportsegment", 6)); // 276 chars > 255 + var childNs = root.AddNamespace($"graph.networkaccess.{longSegment}"); + var requestBuilderClass = childNs.AddClass(new CodeClass + { + Name = $"{longSegment}RequestBuilder", + Kind = CodeClassKind.RequestBuilder, + }).First(); + // Explicitly set Documentation to null to test the defensive null guard + requestBuilderClass.Documentation = null!; + + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Python }, root, cancellationToken: TestContext.Current.CancellationToken); + + // Should shorten without throwing + Assert.True(requestBuilderClass.Name.Length <= 200); + } + #endregion } diff --git a/tests/Kiota.Builder.Tests/Refiners/TypeScriptLanguageRefinerTests.cs b/tests/Kiota.Builder.Tests/Refiners/TypeScriptLanguageRefinerTests.cs index ea49d3957a..301f73ddd8 100644 --- a/tests/Kiota.Builder.Tests/Refiners/TypeScriptLanguageRefinerTests.cs +++ b/tests/Kiota.Builder.Tests/Refiners/TypeScriptLanguageRefinerTests.cs @@ -93,7 +93,7 @@ public async Task AddStaticMethodsUsingsForDeserializerAsync() [Fact] public async Task ShortensOversizedNamespaceSegmentsAsync() { - var longSegment = "MicrosoftGraphNetworkaccessDeviceReportWithStartDateTimeWithEndDateTimeDiscoveredApplicationSegmentIdDiscoveredApplicationSegmentIdApplicationIdApplicationId"; + var longSegment = string.Concat(Enumerable.Repeat("MicrosoftGraphNetworkAccessDeviceReportSegment", 6)); // 276 chars > 255 var childNs = root.AddNamespace($"ApiSdk.NetworkAccess.Reports.{longSegment}"); var requestBuilderClass = childNs.AddClass(new CodeClass { @@ -120,7 +120,7 @@ public async Task ShortensOversizedNamespaceSegmentsAsync() // TypeScript maps namespaces (not type names) to directories, so only segments are shortened. foreach (var segment in childNs.Name.Split('.')) { - Assert.True(segment.Length <= 64, $"Segment '{segment}' exceeds 64 chars (length: {segment.Length})"); + Assert.True(segment.Length <= 200, $"Segment '{segment}' exceeds 200 chars (length: {segment.Length})"); } // Type names must be left intact: TypeScript groups types into index.ts barrels and relies on