Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
e924c39
Increase namespace/type-name shortening threshold from 64 to 128
ramsessanchez Jul 7, 2026
944fb5b
Merge branch 'main' into ramsessanchez/increase-name-shortening-thres…
ramsessanchez Jul 7, 2026
c4ddac3
Add Python refiner tests for namespace/type-name shortening
ramsessanchez Jul 7, 2026
12378c9
Merge branch 'main' into ramsessanchez/increase-name-shortening-thres…
gavinbarron Jul 8, 2026
7d829ee
Merge branch 'main' into ramsessanchez/increase-name-shortening-thres…
ramsessanchez Jul 8, 2026
1cd8376
Raise namespace/type-name shortening threshold to 255
ramsessanchez Jul 8, 2026
77e795e
Merge remote-tracking branch 'origin/ramsessanchez/increase-name-shor…
ramsessanchez Jul 8, 2026
c7636ad
Lower name-shortening threshold to 250 for file-extension buffer
ramsessanchez Jul 8, 2026
386bb24
Use lower shortening threshold (240) for Python only
ramsessanchez Jul 8, 2026
44b8622
Merge branch 'main' into ramsessanchez/increase-name-shortening-thres…
ramsessanchez Jul 8, 2026
27fc6e5
Remove CHANGELOG.md changes from this branch
ramsessanchez Jul 8, 2026
a638cda
Add CHANGELOG entry for name-shortening threshold change
ramsessanchez Jul 8, 2026
6756248
Lower Python name-shortening threshold to 225
ramsessanchez Jul 8, 2026
e97ee97
Restore UTF-8 BOM on refiner test files
ramsessanchez Jul 8, 2026
aef9992
Lower name-shortening threshold to 200 across all languages
ramsessanchez Jul 8, 2026
2da186a
Fix surface-diff crash: pass workspace-relative patch path to contain…
ramsessanchez Jul 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/surface-area-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions src/Kiota.Builder/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
/// <summary>
/// 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.
/// </summary>
/// <param name="name">The name to potentially shorten</param>
/// <param name="maxLength">The maximum allowed length. Default is 64.</param>
/// <param name="maxLength">The maximum allowed length. Default is 200.</param>
/// <returns>The original name if within limits, or a truncated name with hash suffix for uniqueness</returns>
public static string ShortenNameSegment(this string name, int maxLength = DefaultMaxNameSegmentLength)
{
Expand Down
2 changes: 1 addition & 1 deletion src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </param>
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))
{
Expand Down
22 changes: 12 additions & 10 deletions tests/Kiota.Builder.Tests/Extensions/StringExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Kiota.Builder.Extensions;
using System.Linq;

using Kiota.Builder.Extensions;

using Xunit;

Expand Down Expand Up @@ -140,32 +142,32 @@ 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);
}
[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);
Expand Down
18 changes: 9 additions & 9 deletions tests/Kiota.Builder.Tests/Refiners/JavaLanguageRefinerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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);
Expand All @@ -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}");
Expand All @@ -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}");
Expand Down Expand Up @@ -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
{
Expand All @@ -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
}
10 changes: 5 additions & 5 deletions tests/Kiota.Builder.Tests/Refiners/PhpLanguageRefinerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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
Expand Down Expand Up @@ -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");
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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
Expand Down
Loading