From b41220ae8918df055f8f3db8e7086daa24bc304b Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 3 Jun 2026 09:34:04 +0000 Subject: [PATCH 001/111] Optimize qualified name simplification Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../PostProcessing/GeneratedCodeWorkspace.cs | 142 +++++++++++++++++- 1 file changed, 141 insertions(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index d90488b4c85..27916176c7d 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -14,6 +14,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Simplification; +using Microsoft.CodeAnalysis.Text; using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.Utilities; @@ -148,7 +149,24 @@ private async Task ProcessDocument(Document document, MemberRemoverRew } document = document.WithSyntaxRoot(root); - document = await Simplifier.ReduceAsync(document); + document = await ReduceQualifiedNamesAsync(document); + root = await document.GetSyntaxRootAsync(); + if (root == null) + { + return document; + } + + var simplifierSpans = GetSimplifierSpans(root); + if (simplifierSpans.Count > 0) + { + root = root.WithAdditionalAnnotations(Simplifier.Annotation); + document = document.WithSyntaxRoot(root); + document = await Simplifier.ReduceAsync(document, simplifierSpans); + } + else if (ContainsSimplifierAnnotations(root)) + { + document = await Simplifier.ReduceAsync(document); + } // Reformat if any custom rewriters have been applied if (CodeModelGenerator.Instance.Rewriters.Count > 0) @@ -158,6 +176,128 @@ private async Task ProcessDocument(Document document, MemberRemoverRew return document; } + private static async Task ReduceQualifiedNamesAsync(Document document) + { + var root = await document.GetSyntaxRootAsync(); + var semanticModel = await document.GetSemanticModelAsync(); + if (root == null || semanticModel == null) + { + return document; + } + + var safeNodes = new HashSet(); + foreach (var name in root.DescendantNodes().OfType()) + { + if (name is not QualifiedNameSyntax and not AliasQualifiedNameSyntax || + name.Parent is QualifiedNameSyntax || + IsInUnsupportedQualifiedNameContext(name)) + { + continue; + } + + var originalSymbol = semanticModel.GetSymbolInfo(name).Symbol; + if (originalSymbol == null) + { + continue; + } + + var replacement = GetRightmostName(name).WithTriviaFrom(name); + if (SpeculativelyBindsToSameSymbol(semanticModel, name, replacement, originalSymbol)) + { + safeNodes.Add(name); + } + } + + if (safeNodes.Count == 0) + { + return document; + } + + var rewrittenRoot = root.ReplaceNodes( + safeNodes, + static (_, rewritten) => GetRightmostName(rewritten).WithTriviaFrom(rewritten)); + return document.WithSyntaxRoot(rewrittenRoot); + } + + private static bool SpeculativelyBindsToSameSymbol( + SemanticModel semanticModel, + NameSyntax originalName, + SimpleNameSyntax replacement, + ISymbol originalSymbol) + { + var speculativeSymbol = semanticModel.GetSpeculativeSymbolInfo( + originalName.SpanStart, + replacement, + SpeculativeBindingOption.BindAsTypeOrNamespace).Symbol; + if (speculativeSymbol != null && + SymbolEqualityComparer.Default.Equals(originalSymbol, speculativeSymbol)) + { + return true; + } + + if (originalName.Parent is MemberAccessExpressionSyntax memberAccess && + memberAccess.Expression == originalName) + { + speculativeSymbol = semanticModel.GetSpeculativeSymbolInfo( + originalName.SpanStart, + replacement, + SpeculativeBindingOption.BindAsExpression).Symbol; + return speculativeSymbol != null && + SymbolEqualityComparer.Default.Equals(originalSymbol, speculativeSymbol); + } + + return false; + } + + private static SimpleNameSyntax GetRightmostName(NameSyntax name) => name switch + { + QualifiedNameSyntax qualifiedName => qualifiedName.Right, + AliasQualifiedNameSyntax aliasQualifiedName => aliasQualifiedName.Name, + SimpleNameSyntax simpleName => simpleName, + _ => throw new InvalidOperationException($"Unexpected name syntax: {name.Kind()}") + }; + + private static bool IsInUnsupportedQualifiedNameContext(NameSyntax name) => + name.Ancestors().Any(static ancestor => + ancestor is UsingDirectiveSyntax || + ancestor is CrefSyntax); + + private static bool ContainsSimplifierAnnotations(SyntaxNode root) => + root.HasAnnotation(Simplifier.Annotation) || + root.DescendantNodesAndTokens(descendIntoTrivia: true).Any(static nodeOrToken => + nodeOrToken.HasAnnotation(Simplifier.Annotation)); + + private static IReadOnlyList GetSimplifierSpans(SyntaxNode root) + { + List spans = new(); + foreach (var member in root.DescendantNodes().OfType()) + { + if (ContainsReducibleSyntax(member)) + { + spans.Add(member.FullSpan); + } + } + + spans.AddRange(root + .DescendantNodesAndTokens(descendIntoTrivia: true) + .Where(static nodeOrToken => nodeOrToken.HasAnnotation(Simplifier.Annotation)) + .Select(static nodeOrToken => nodeOrToken.FullSpan)); + + return spans; + } + + private static bool ContainsReducibleSyntax(SyntaxNode root) => + root.DescendantNodes( + descendIntoChildren: node => node == root || node is not MemberDeclarationSyntax, + descendIntoTrivia: true).Any(static node => + node is ThisExpressionSyntax || + node is ParenthesizedExpressionSyntax || + node is CrefSyntax || + node is QualifiedNameSyntax || + node is MemberAccessExpressionSyntax || + node is AssignmentExpressionSyntax { RawKind: (int)SyntaxKind.SimpleAssignmentExpression } || + node is AliasQualifiedNameSyntax { Alias.Identifier.ValueText: "global" }); + public static bool IsGeneratedDocument(Document document) => document.Folders.Contains(GeneratedFolder); public static bool IsCustomDocument(Document document) => !IsGeneratedDocument(document); public static bool IsGeneratedTestDocument(Document document) => document.Folders.Contains(GeneratedTestFolder); From 4870164cb0b8b11ece0789961d0e1484f4d5d522 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 4 Jun 2026 00:55:26 +0000 Subject: [PATCH 002/111] Handle System member qualified name reduction --- .../PostProcessing/GeneratedCodeWorkspace.cs | 180 +++++++++++++++++- .../test/GeneratedCodeWorkspaceTests.cs | 67 +++++++ 2 files changed, 242 insertions(+), 5 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index 27916176c7d..4e1f77d4131 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -185,7 +185,7 @@ private static async Task ReduceQualifiedNamesAsync(Document document) return document; } - var safeNodes = new HashSet(); + var safeNameReplacements = new Dictionary(); foreach (var name in root.DescendantNodes().OfType()) { if (name is not QualifiedNameSyntax and not AliasQualifiedNameSyntax || @@ -204,18 +204,45 @@ name.Parent is QualifiedNameSyntax || var replacement = GetRightmostName(name).WithTriviaFrom(name); if (SpeculativelyBindsToSameSymbol(semanticModel, name, replacement, originalSymbol)) { - safeNodes.Add(name); + safeNameReplacements.Add(name, replacement); } } - if (safeNodes.Count == 0) + foreach (var attribute in root.DescendantNodes().OfType()) + { + if (TryGetAttributeNameReplacement(semanticModel, attribute, out var replacement)) + { + safeNameReplacements[attribute.Name] = replacement; + } + } + + var safeMemberAccessReplacements = new Dictionary(); + foreach (var memberAccess in root.DescendantNodes().OfType()) + { + if (IsInUnsupportedQualifiedNameContext(memberAccess)) + { + continue; + } + + if (TryGetMemberAccessReplacement(semanticModel, memberAccess, out var replacement)) + { + safeMemberAccessReplacements.Add(memberAccess, replacement); + } + } + + if (safeNameReplacements.Count == 0 && safeMemberAccessReplacements.Count == 0) { return document; } var rewrittenRoot = root.ReplaceNodes( - safeNodes, - static (_, rewritten) => GetRightmostName(rewritten).WithTriviaFrom(rewritten)); + safeNameReplacements.Keys.Concat(safeMemberAccessReplacements.Keys), + (original, rewritten) => original switch + { + NameSyntax name => safeNameReplacements[name].WithTriviaFrom(rewritten), + MemberAccessExpressionSyntax memberAccess => safeMemberAccessReplacements[memberAccess].WithTriviaFrom(rewritten), + _ => rewritten + }); return document.WithSyntaxRoot(rewrittenRoot); } @@ -257,11 +284,154 @@ private static bool SpeculativelyBindsToSameSymbol( _ => throw new InvalidOperationException($"Unexpected name syntax: {name.Kind()}") }; + private static bool TryGetAttributeNameReplacement( + SemanticModel semanticModel, + AttributeSyntax attribute, + out NameSyntax replacement) + { + replacement = attribute.Name; + if (attribute.Name is not QualifiedNameSyntax and not AliasQualifiedNameSyntax) + { + return false; + } + + var originalSymbol = semanticModel.GetSymbolInfo(attribute).Symbol; + if (originalSymbol is not IMethodSymbol { ContainingType: { } originalAttributeType }) + { + return false; + } + + var rightmostName = GetRightmostName(attribute.Name); + var speculativeSymbol = semanticModel.GetSpeculativeSymbolInfo( + attribute.Name.SpanStart, + rightmostName, + SpeculativeBindingOption.BindAsTypeOrNamespace).Symbol; + if (!SymbolEqualityComparer.Default.Equals(originalAttributeType, speculativeSymbol)) + { + return false; + } + + replacement = TrimAttributeSuffix(rightmostName).WithTriviaFrom(attribute.Name); + return true; + } + + private static SimpleNameSyntax TrimAttributeSuffix(SimpleNameSyntax name) + { + const string AttributeSuffix = "Attribute"; + var identifier = name.Identifier; + var text = identifier.ValueText; + if (!text.EndsWith(AttributeSuffix, StringComparison.Ordinal) || text.Length == AttributeSuffix.Length) + { + return name; + } + + return SyntaxFactory.IdentifierName( + SyntaxFactory.Identifier( + identifier.LeadingTrivia, + text.Substring(0, text.Length - AttributeSuffix.Length), + identifier.TrailingTrivia)); + } + + private static bool TryGetMemberAccessReplacement( + SemanticModel semanticModel, + MemberAccessExpressionSyntax memberAccess, + out ExpressionSyntax replacement) + { + replacement = memberAccess; + var originalSymbol = semanticModel.GetSymbolInfo(memberAccess).Symbol; + if (originalSymbol == null || + !TryGetMemberAccessParts(memberAccess, out var parts) || + parts.Count < 2 || + parts[0].Identifier.ValueText != "System") + { + return false; + } + + for (int i = parts.Count - 1; i > 0; i--) + { + var candidate = BuildMemberAccess(parts, i).WithTriviaFrom(memberAccess); + var speculativeSymbol = semanticModel.GetSpeculativeSymbolInfo( + memberAccess.SpanStart, + candidate, + SpeculativeBindingOption.BindAsExpression).Symbol; + if (speculativeSymbol != null && + SymbolEqualityComparer.Default.Equals(originalSymbol, speculativeSymbol)) + { + replacement = candidate; + return true; + } + } + + return false; + } + + private static bool TryGetMemberAccessParts(ExpressionSyntax expression, out IReadOnlyList parts) + { + var builder = new List(); + if (AddMemberAccessParts(expression, builder)) + { + parts = builder; + return true; + } + + parts = []; + return false; + } + + private static bool AddMemberAccessParts(SyntaxNode expression, List parts) + { + switch (expression) + { + case SimpleNameSyntax name: + parts.Add(name); + return true; + case MemberAccessExpressionSyntax memberAccess: + if (!AddMemberAccessParts(memberAccess.Expression, parts)) + { + return false; + } + + parts.Add(memberAccess.Name); + return true; + case QualifiedNameSyntax qualifiedName: + if (!AddMemberAccessParts(qualifiedName.Left, parts)) + { + return false; + } + + parts.Add(qualifiedName.Right); + return true; + case AliasQualifiedNameSyntax { Alias.Identifier.ValueText: "global" } aliasQualifiedName: + parts.Add(aliasQualifiedName.Name); + return true; + default: + return false; + } + } + + private static ExpressionSyntax BuildMemberAccess(IReadOnlyList parts, int startIndex) + { + ExpressionSyntax expression = parts[startIndex]; + for (int i = startIndex + 1; i < parts.Count; i++) + { + expression = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + expression, + parts[i]); + } + + return expression; + } + private static bool IsInUnsupportedQualifiedNameContext(NameSyntax name) => name.Ancestors().Any(static ancestor => ancestor is UsingDirectiveSyntax || ancestor is CrefSyntax); + private static bool IsInUnsupportedQualifiedNameContext(ExpressionSyntax expression) => + expression.Ancestors().Any(static ancestor => + ancestor is CrefSyntax); + private static bool ContainsSimplifierAnnotations(SyntaxNode root) => root.HasAnnotation(Simplifier.Annotation) || root.DescendantNodesAndTokens(descendIntoTrivia: true).Any(static nodeOrToken => diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs index 0a75d8c9360..8ca35ece393 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs @@ -4,6 +4,7 @@ using Microsoft.Build.Construction; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Tests.Common; using NUnit.Framework; using System; @@ -97,6 +98,72 @@ await MockHelpers.LoadMockGeneratorAsync( Assert.NotNull(fooMethod, "Foo method should be found in the SimpleType"); } + [Test] + public async Task GetGeneratedFilesAsync_SimplifiesFrameworkNamesWhenTypeHasSystemMember() + { + MockHelpers.LoadMockGenerator( + outputPath: _projectDir, + configuration: "{\"package-name\": \"TestNamespace\"}", + additionalMetadataReferences: + [ + MetadataReference.CreateFromFile(typeof(EditorBrowsableAttribute).Assembly.Location) + ]); + + GeneratedCodeWorkspace.Initialize(); + var workspace = await GeneratedCodeWorkspace.Create(false); + await workspace.AddGeneratedFile(new CodeFile( + """ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// + +#nullable disable + +using System; +using System.ComponentModel; + +namespace TestNamespace +{ + public readonly partial struct TestRole : IEquatable + { + private readonly string _value; + private const string SystemValue = "system"; + + public TestRole(string value) + { + _value = value; + } + + public static TestRole System { get; } = new TestRole(SystemValue); + + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public override bool Equals(object obj) => obj is TestRole other && Equals(other); + + public bool Equals(TestRole other) => string.Equals(_value, other._value, global::System.StringComparison.InvariantCultureIgnoreCase); + + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public override int GetHashCode() => _value != null ? global::System.StringComparer.InvariantCultureIgnoreCase.GetHashCode(_value) : 0; + } +} +""", + "TestRole.cs")); + + string? generatedText = null; + await foreach (var generatedFile in workspace.GetGeneratedFilesAsync()) + { + generatedText = generatedFile.Text; + } + + Assert.That(generatedText, Is.Not.Null); + Assert.That(generatedText, Does.Contain("[EditorBrowsable(EditorBrowsableState.Never)]")); + Assert.That(generatedText, Does.Contain("StringComparison.InvariantCultureIgnoreCase")); + Assert.That(generatedText, Does.Contain("StringComparer.InvariantCultureIgnoreCase")); + Assert.That(generatedText, Does.Not.Contain("System.ComponentModel.EditorBrowsableAttribute")); + Assert.That(generatedText, Does.Not.Contain("System.StringComparison")); + Assert.That(generatedText, Does.Not.Contain("System.StringComparer")); + } + [Test] public async Task AddPackageReferencesFromProject_AddsReferencesFromCsproj() { From 59eb312177422a20cb0021d140ba8e9bb768826c Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 4 Jun 2026 01:10:44 +0000 Subject: [PATCH 003/111] Avoid ambiguous System member pre-reduction --- .../PostProcessing/GeneratedCodeWorkspace.cs | 183 +----------------- 1 file changed, 9 insertions(+), 174 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index 4e1f77d4131..702fb337a64 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -185,11 +185,12 @@ private static async Task ReduceQualifiedNamesAsync(Document document) return document; } - var safeNameReplacements = new Dictionary(); + var safeNodes = new HashSet(); foreach (var name in root.DescendantNodes().OfType()) { if (name is not QualifiedNameSyntax and not AliasQualifiedNameSyntax || name.Parent is QualifiedNameSyntax || + IsPartOfMemberAccessChain(name) || IsInUnsupportedQualifiedNameContext(name)) { continue; @@ -204,45 +205,18 @@ name.Parent is QualifiedNameSyntax || var replacement = GetRightmostName(name).WithTriviaFrom(name); if (SpeculativelyBindsToSameSymbol(semanticModel, name, replacement, originalSymbol)) { - safeNameReplacements.Add(name, replacement); + safeNodes.Add(name); } } - foreach (var attribute in root.DescendantNodes().OfType()) - { - if (TryGetAttributeNameReplacement(semanticModel, attribute, out var replacement)) - { - safeNameReplacements[attribute.Name] = replacement; - } - } - - var safeMemberAccessReplacements = new Dictionary(); - foreach (var memberAccess in root.DescendantNodes().OfType()) - { - if (IsInUnsupportedQualifiedNameContext(memberAccess)) - { - continue; - } - - if (TryGetMemberAccessReplacement(semanticModel, memberAccess, out var replacement)) - { - safeMemberAccessReplacements.Add(memberAccess, replacement); - } - } - - if (safeNameReplacements.Count == 0 && safeMemberAccessReplacements.Count == 0) + if (safeNodes.Count == 0) { return document; } var rewrittenRoot = root.ReplaceNodes( - safeNameReplacements.Keys.Concat(safeMemberAccessReplacements.Keys), - (original, rewritten) => original switch - { - NameSyntax name => safeNameReplacements[name].WithTriviaFrom(rewritten), - MemberAccessExpressionSyntax memberAccess => safeMemberAccessReplacements[memberAccess].WithTriviaFrom(rewritten), - _ => rewritten - }); + safeNodes, + static (_, rewritten) => GetRightmostName(rewritten).WithTriviaFrom(rewritten)); return document.WithSyntaxRoot(rewrittenRoot); } @@ -284,154 +258,15 @@ private static bool SpeculativelyBindsToSameSymbol( _ => throw new InvalidOperationException($"Unexpected name syntax: {name.Kind()}") }; - private static bool TryGetAttributeNameReplacement( - SemanticModel semanticModel, - AttributeSyntax attribute, - out NameSyntax replacement) - { - replacement = attribute.Name; - if (attribute.Name is not QualifiedNameSyntax and not AliasQualifiedNameSyntax) - { - return false; - } - - var originalSymbol = semanticModel.GetSymbolInfo(attribute).Symbol; - if (originalSymbol is not IMethodSymbol { ContainingType: { } originalAttributeType }) - { - return false; - } - - var rightmostName = GetRightmostName(attribute.Name); - var speculativeSymbol = semanticModel.GetSpeculativeSymbolInfo( - attribute.Name.SpanStart, - rightmostName, - SpeculativeBindingOption.BindAsTypeOrNamespace).Symbol; - if (!SymbolEqualityComparer.Default.Equals(originalAttributeType, speculativeSymbol)) - { - return false; - } - - replacement = TrimAttributeSuffix(rightmostName).WithTriviaFrom(attribute.Name); - return true; - } - - private static SimpleNameSyntax TrimAttributeSuffix(SimpleNameSyntax name) - { - const string AttributeSuffix = "Attribute"; - var identifier = name.Identifier; - var text = identifier.ValueText; - if (!text.EndsWith(AttributeSuffix, StringComparison.Ordinal) || text.Length == AttributeSuffix.Length) - { - return name; - } - - return SyntaxFactory.IdentifierName( - SyntaxFactory.Identifier( - identifier.LeadingTrivia, - text.Substring(0, text.Length - AttributeSuffix.Length), - identifier.TrailingTrivia)); - } - - private static bool TryGetMemberAccessReplacement( - SemanticModel semanticModel, - MemberAccessExpressionSyntax memberAccess, - out ExpressionSyntax replacement) - { - replacement = memberAccess; - var originalSymbol = semanticModel.GetSymbolInfo(memberAccess).Symbol; - if (originalSymbol == null || - !TryGetMemberAccessParts(memberAccess, out var parts) || - parts.Count < 2 || - parts[0].Identifier.ValueText != "System") - { - return false; - } - - for (int i = parts.Count - 1; i > 0; i--) - { - var candidate = BuildMemberAccess(parts, i).WithTriviaFrom(memberAccess); - var speculativeSymbol = semanticModel.GetSpeculativeSymbolInfo( - memberAccess.SpanStart, - candidate, - SpeculativeBindingOption.BindAsExpression).Symbol; - if (speculativeSymbol != null && - SymbolEqualityComparer.Default.Equals(originalSymbol, speculativeSymbol)) - { - replacement = candidate; - return true; - } - } - - return false; - } - - private static bool TryGetMemberAccessParts(ExpressionSyntax expression, out IReadOnlyList parts) - { - var builder = new List(); - if (AddMemberAccessParts(expression, builder)) - { - parts = builder; - return true; - } - - parts = []; - return false; - } - - private static bool AddMemberAccessParts(SyntaxNode expression, List parts) - { - switch (expression) - { - case SimpleNameSyntax name: - parts.Add(name); - return true; - case MemberAccessExpressionSyntax memberAccess: - if (!AddMemberAccessParts(memberAccess.Expression, parts)) - { - return false; - } - - parts.Add(memberAccess.Name); - return true; - case QualifiedNameSyntax qualifiedName: - if (!AddMemberAccessParts(qualifiedName.Left, parts)) - { - return false; - } - - parts.Add(qualifiedName.Right); - return true; - case AliasQualifiedNameSyntax { Alias.Identifier.ValueText: "global" } aliasQualifiedName: - parts.Add(aliasQualifiedName.Name); - return true; - default: - return false; - } - } - - private static ExpressionSyntax BuildMemberAccess(IReadOnlyList parts, int startIndex) - { - ExpressionSyntax expression = parts[startIndex]; - for (int i = startIndex + 1; i < parts.Count; i++) - { - expression = SyntaxFactory.MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - expression, - parts[i]); - } - - return expression; - } + private static bool IsPartOfMemberAccessChain(NameSyntax name) => + name.Parent is MemberAccessExpressionSyntax || + name.Ancestors().OfType().Any(); private static bool IsInUnsupportedQualifiedNameContext(NameSyntax name) => name.Ancestors().Any(static ancestor => ancestor is UsingDirectiveSyntax || ancestor is CrefSyntax); - private static bool IsInUnsupportedQualifiedNameContext(ExpressionSyntax expression) => - expression.Ancestors().Any(static ancestor => - ancestor is CrefSyntax); - private static bool ContainsSimplifierAnnotations(SyntaxNode root) => root.HasAnnotation(Simplifier.Annotation) || root.DescendantNodesAndTokens(descendIntoTrivia: true).Any(static nodeOrToken => From 5bbd4eb7e1004349a76c0973424918e2bda6e557 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 4 Jun 2026 01:14:14 +0000 Subject: [PATCH 004/111] Revert "Avoid ambiguous System member pre-reduction" This reverts commit 59eb312177422a20cb0021d140ba8e9bb768826c. --- .../PostProcessing/GeneratedCodeWorkspace.cs | 183 +++++++++++++++++- 1 file changed, 174 insertions(+), 9 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index 702fb337a64..4e1f77d4131 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -185,12 +185,11 @@ private static async Task ReduceQualifiedNamesAsync(Document document) return document; } - var safeNodes = new HashSet(); + var safeNameReplacements = new Dictionary(); foreach (var name in root.DescendantNodes().OfType()) { if (name is not QualifiedNameSyntax and not AliasQualifiedNameSyntax || name.Parent is QualifiedNameSyntax || - IsPartOfMemberAccessChain(name) || IsInUnsupportedQualifiedNameContext(name)) { continue; @@ -205,18 +204,45 @@ name.Parent is QualifiedNameSyntax || var replacement = GetRightmostName(name).WithTriviaFrom(name); if (SpeculativelyBindsToSameSymbol(semanticModel, name, replacement, originalSymbol)) { - safeNodes.Add(name); + safeNameReplacements.Add(name, replacement); } } - if (safeNodes.Count == 0) + foreach (var attribute in root.DescendantNodes().OfType()) + { + if (TryGetAttributeNameReplacement(semanticModel, attribute, out var replacement)) + { + safeNameReplacements[attribute.Name] = replacement; + } + } + + var safeMemberAccessReplacements = new Dictionary(); + foreach (var memberAccess in root.DescendantNodes().OfType()) + { + if (IsInUnsupportedQualifiedNameContext(memberAccess)) + { + continue; + } + + if (TryGetMemberAccessReplacement(semanticModel, memberAccess, out var replacement)) + { + safeMemberAccessReplacements.Add(memberAccess, replacement); + } + } + + if (safeNameReplacements.Count == 0 && safeMemberAccessReplacements.Count == 0) { return document; } var rewrittenRoot = root.ReplaceNodes( - safeNodes, - static (_, rewritten) => GetRightmostName(rewritten).WithTriviaFrom(rewritten)); + safeNameReplacements.Keys.Concat(safeMemberAccessReplacements.Keys), + (original, rewritten) => original switch + { + NameSyntax name => safeNameReplacements[name].WithTriviaFrom(rewritten), + MemberAccessExpressionSyntax memberAccess => safeMemberAccessReplacements[memberAccess].WithTriviaFrom(rewritten), + _ => rewritten + }); return document.WithSyntaxRoot(rewrittenRoot); } @@ -258,15 +284,154 @@ private static bool SpeculativelyBindsToSameSymbol( _ => throw new InvalidOperationException($"Unexpected name syntax: {name.Kind()}") }; - private static bool IsPartOfMemberAccessChain(NameSyntax name) => - name.Parent is MemberAccessExpressionSyntax || - name.Ancestors().OfType().Any(); + private static bool TryGetAttributeNameReplacement( + SemanticModel semanticModel, + AttributeSyntax attribute, + out NameSyntax replacement) + { + replacement = attribute.Name; + if (attribute.Name is not QualifiedNameSyntax and not AliasQualifiedNameSyntax) + { + return false; + } + + var originalSymbol = semanticModel.GetSymbolInfo(attribute).Symbol; + if (originalSymbol is not IMethodSymbol { ContainingType: { } originalAttributeType }) + { + return false; + } + + var rightmostName = GetRightmostName(attribute.Name); + var speculativeSymbol = semanticModel.GetSpeculativeSymbolInfo( + attribute.Name.SpanStart, + rightmostName, + SpeculativeBindingOption.BindAsTypeOrNamespace).Symbol; + if (!SymbolEqualityComparer.Default.Equals(originalAttributeType, speculativeSymbol)) + { + return false; + } + + replacement = TrimAttributeSuffix(rightmostName).WithTriviaFrom(attribute.Name); + return true; + } + + private static SimpleNameSyntax TrimAttributeSuffix(SimpleNameSyntax name) + { + const string AttributeSuffix = "Attribute"; + var identifier = name.Identifier; + var text = identifier.ValueText; + if (!text.EndsWith(AttributeSuffix, StringComparison.Ordinal) || text.Length == AttributeSuffix.Length) + { + return name; + } + + return SyntaxFactory.IdentifierName( + SyntaxFactory.Identifier( + identifier.LeadingTrivia, + text.Substring(0, text.Length - AttributeSuffix.Length), + identifier.TrailingTrivia)); + } + + private static bool TryGetMemberAccessReplacement( + SemanticModel semanticModel, + MemberAccessExpressionSyntax memberAccess, + out ExpressionSyntax replacement) + { + replacement = memberAccess; + var originalSymbol = semanticModel.GetSymbolInfo(memberAccess).Symbol; + if (originalSymbol == null || + !TryGetMemberAccessParts(memberAccess, out var parts) || + parts.Count < 2 || + parts[0].Identifier.ValueText != "System") + { + return false; + } + + for (int i = parts.Count - 1; i > 0; i--) + { + var candidate = BuildMemberAccess(parts, i).WithTriviaFrom(memberAccess); + var speculativeSymbol = semanticModel.GetSpeculativeSymbolInfo( + memberAccess.SpanStart, + candidate, + SpeculativeBindingOption.BindAsExpression).Symbol; + if (speculativeSymbol != null && + SymbolEqualityComparer.Default.Equals(originalSymbol, speculativeSymbol)) + { + replacement = candidate; + return true; + } + } + + return false; + } + + private static bool TryGetMemberAccessParts(ExpressionSyntax expression, out IReadOnlyList parts) + { + var builder = new List(); + if (AddMemberAccessParts(expression, builder)) + { + parts = builder; + return true; + } + + parts = []; + return false; + } + + private static bool AddMemberAccessParts(SyntaxNode expression, List parts) + { + switch (expression) + { + case SimpleNameSyntax name: + parts.Add(name); + return true; + case MemberAccessExpressionSyntax memberAccess: + if (!AddMemberAccessParts(memberAccess.Expression, parts)) + { + return false; + } + + parts.Add(memberAccess.Name); + return true; + case QualifiedNameSyntax qualifiedName: + if (!AddMemberAccessParts(qualifiedName.Left, parts)) + { + return false; + } + + parts.Add(qualifiedName.Right); + return true; + case AliasQualifiedNameSyntax { Alias.Identifier.ValueText: "global" } aliasQualifiedName: + parts.Add(aliasQualifiedName.Name); + return true; + default: + return false; + } + } + + private static ExpressionSyntax BuildMemberAccess(IReadOnlyList parts, int startIndex) + { + ExpressionSyntax expression = parts[startIndex]; + for (int i = startIndex + 1; i < parts.Count; i++) + { + expression = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + expression, + parts[i]); + } + + return expression; + } private static bool IsInUnsupportedQualifiedNameContext(NameSyntax name) => name.Ancestors().Any(static ancestor => ancestor is UsingDirectiveSyntax || ancestor is CrefSyntax); + private static bool IsInUnsupportedQualifiedNameContext(ExpressionSyntax expression) => + expression.Ancestors().Any(static ancestor => + ancestor is CrefSyntax); + private static bool ContainsSimplifierAnnotations(SyntaxNode root) => root.HasAnnotation(Simplifier.Annotation) || root.DescendantNodesAndTokens(descendIntoTrivia: true).Any(static nodeOrToken => From 47fbfa978ade1d0d9efe53d4da80cc42dfd1aab2 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 4 Jun 2026 01:22:58 +0000 Subject: [PATCH 005/111] Experiment with manual generated name reduction --- .../PostProcessing/GeneratedCodeWorkspace.cs | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index 4e1f77d4131..4e099783355 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -12,6 +12,7 @@ using Microsoft.CodeAnalysis; using MSBuildProjectCollection = Microsoft.Build.Evaluation.ProjectCollection; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Simplification; using Microsoft.CodeAnalysis.Text; @@ -150,23 +151,7 @@ private async Task ProcessDocument(Document document, MemberRemoverRew document = document.WithSyntaxRoot(root); document = await ReduceQualifiedNamesAsync(document); - root = await document.GetSyntaxRootAsync(); - if (root == null) - { - return document; - } - - var simplifierSpans = GetSimplifierSpans(root); - if (simplifierSpans.Count > 0) - { - root = root.WithAdditionalAnnotations(Simplifier.Annotation); - document = document.WithSyntaxRoot(root); - document = await Simplifier.ReduceAsync(document, simplifierSpans); - } - else if (ContainsSimplifierAnnotations(root)) - { - document = await Simplifier.ReduceAsync(document); - } + document = await ReduceParenthesizedAssignmentsAsync(document); // Reformat if any custom rewriters have been applied if (CodeModelGenerator.Instance.Rewriters.Count > 0) @@ -246,6 +231,31 @@ name.Parent is QualifiedNameSyntax || return document.WithSyntaxRoot(rewrittenRoot); } + private static async Task ReduceParenthesizedAssignmentsAsync(Document document) + { + var root = await document.GetSyntaxRootAsync(); + if (root == null) + { + return document; + } + + var assignments = root.DescendantNodes() + .OfType() + .Where(static node => + node.Expression is AssignmentExpressionSyntax && + node.Parent is ExpressionStatementSyntax) + .ToList(); + if (assignments.Count == 0) + { + return document; + } + + var rewrittenRoot = root.ReplaceNodes( + assignments, + static (_, rewritten) => rewritten.Expression.WithTriviaFrom(rewritten)); + return document.WithSyntaxRoot(rewrittenRoot); + } + private static bool SpeculativelyBindsToSameSymbol( SemanticModel semanticModel, NameSyntax originalName, From f25c028960ffe388c785a6efb189110ca4fa314e Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 4 Jun 2026 01:54:17 +0000 Subject: [PATCH 006/111] Reduce global aliases in manual name experiment --- .../PostProcessing/GeneratedCodeWorkspace.cs | 153 ++++++++++++- .../test/GeneratedCodeWorkspaceTests.cs | 213 ++++++++++++++++-- 2 files changed, 344 insertions(+), 22 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index 4e099783355..4abbfb665c4 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -152,6 +152,7 @@ private async Task ProcessDocument(Document document, MemberRemoverRew document = await ReduceQualifiedNamesAsync(document); document = await ReduceParenthesizedAssignmentsAsync(document); + document = await ReduceDocumentationGlobalAliasesAsync(document); // Reformat if any custom rewriters have been applied if (CodeModelGenerator.Instance.Rewriters.Count > 0) @@ -170,6 +171,45 @@ private static async Task ReduceQualifiedNamesAsync(Document document) return document; } + var globalAliases = new Dictionary(); + foreach (var aliasName in root.DescendantNodes().OfType()) + { + if (aliasName.Alias.Identifier.ValueText != "global" || + aliasName.Ancestors().Any(static ancestor => + ancestor is AttributeSyntax || + ancestor is MemberAccessExpressionSyntax) || + IsInUnsupportedQualifiedNameContext(aliasName)) + { + continue; + } + + var originalSymbol = GetSymbol(semanticModel, aliasName); + if (originalSymbol == null) + { + continue; + } + + var replacement = aliasName.Name.WithTriviaFrom(aliasName); + if (SpeculativelyBindsToSameSymbol(semanticModel, aliasName, replacement, originalSymbol)) + { + globalAliases.Add(aliasName, replacement); + } + } + + if (globalAliases.Count > 0) + { + root = root.ReplaceNodes( + globalAliases.Keys, + (original, rewritten) => globalAliases[original].WithTriviaFrom(rewritten)); + document = document.WithSyntaxRoot(root); + semanticModel = await document.GetSemanticModelAsync(); + root = await document.GetSyntaxRootAsync(); + if (root == null || semanticModel == null) + { + return document; + } + } + var safeNameReplacements = new Dictionary(); foreach (var name in root.DescendantNodes().OfType()) { @@ -180,14 +220,13 @@ name.Parent is QualifiedNameSyntax || continue; } - var originalSymbol = semanticModel.GetSymbolInfo(name).Symbol; + var originalSymbol = GetSymbol(semanticModel, name); if (originalSymbol == null) { continue; } - var replacement = GetRightmostName(name).WithTriviaFrom(name); - if (SpeculativelyBindsToSameSymbol(semanticModel, name, replacement, originalSymbol)) + if (TryGetNameReplacement(semanticModel, name, originalSymbol, out var replacement)) { safeNameReplacements.Add(name, replacement); } @@ -231,6 +270,10 @@ name.Parent is QualifiedNameSyntax || return document.WithSyntaxRoot(rewrittenRoot); } + private static ISymbol? GetSymbol(SemanticModel semanticModel, NameSyntax name) => + semanticModel.GetSymbolInfo(name).Symbol ?? + semanticModel.GetTypeInfo(name).Type; + private static async Task ReduceParenthesizedAssignmentsAsync(Document document) { var root = await document.GetSyntaxRootAsync(); @@ -256,10 +299,65 @@ node.Expression is AssignmentExpressionSyntax && return document.WithSyntaxRoot(rewrittenRoot); } + private static async Task ReduceDocumentationGlobalAliasesAsync(Document document) + { + var root = await document.GetSyntaxRootAsync(); + if (root == null) + { + return document; + } + + var documentationTrivia = root.DescendantTrivia(descendIntoTrivia: true) + .Where(static trivia => + trivia.HasStructure && + trivia.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia) && + trivia.ToFullString().Contains("global::", StringComparison.Ordinal)) + .ToList(); + if (documentationTrivia.Count == 0) + { + return document; + } + + var rewrittenRoot = root.ReplaceTrivia( + documentationTrivia, + static (_, rewritten) => + { + var reduced = rewritten.ToFullString().Replace("global::", string.Empty, StringComparison.Ordinal); + var parsedTrivia = SyntaxFactory.ParseLeadingTrivia(reduced); + return parsedTrivia.Count == 1 ? parsedTrivia[0] : rewritten; + }); + return document.WithSyntaxRoot(rewrittenRoot); + } + + private static bool TryGetNameReplacement( + SemanticModel semanticModel, + NameSyntax originalName, + ISymbol originalSymbol, + out NameSyntax replacement) + { + replacement = originalName; + if (!TryGetNameParts(originalName, out var parts)) + { + return false; + } + + for (int i = parts.Count - 1; i >= 0; i--) + { + var candidate = BuildName(parts, i).WithTriviaFrom(originalName); + if (SpeculativelyBindsToSameSymbol(semanticModel, originalName, candidate, originalSymbol)) + { + replacement = candidate; + return true; + } + } + + return false; + } + private static bool SpeculativelyBindsToSameSymbol( SemanticModel semanticModel, NameSyntax originalName, - SimpleNameSyntax replacement, + NameSyntax replacement, ISymbol originalSymbol) { var speculativeSymbol = semanticModel.GetSpeculativeSymbolInfo( @@ -286,6 +384,53 @@ private static bool SpeculativelyBindsToSameSymbol( return false; } + private static bool TryGetNameParts(NameSyntax name, out IReadOnlyList parts) + { + var builder = new List(); + if (AddNameParts(name, builder)) + { + parts = builder; + return true; + } + + parts = []; + return false; + } + + private static bool AddNameParts(NameSyntax name, List parts) + { + switch (name) + { + case SimpleNameSyntax simpleName: + parts.Add(simpleName); + return true; + case QualifiedNameSyntax qualifiedName: + if (!AddNameParts(qualifiedName.Left, parts)) + { + return false; + } + + parts.Add(qualifiedName.Right); + return true; + case AliasQualifiedNameSyntax { Alias.Identifier.ValueText: "global" } aliasQualifiedName: + parts.Add(aliasQualifiedName.Name); + return true; + default: + return false; + } + } + + private static NameSyntax BuildName(IReadOnlyList parts, int startIndex) + { + NameSyntax name = parts[startIndex]; + for (int i = startIndex + 1; i < parts.Count; i++) + { + name = SyntaxFactory.QualifiedName(name, parts[i]); + } + + return name; + } + private static SimpleNameSyntax GetRightmostName(NameSyntax name) => name switch { QualifiedNameSyntax qualifiedName => qualifiedName.Right, diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs index 8ca35ece393..4dc0d98548e 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs @@ -101,17 +101,7 @@ await MockHelpers.LoadMockGeneratorAsync( [Test] public async Task GetGeneratedFilesAsync_SimplifiesFrameworkNamesWhenTypeHasSystemMember() { - MockHelpers.LoadMockGenerator( - outputPath: _projectDir, - configuration: "{\"package-name\": \"TestNamespace\"}", - additionalMetadataReferences: - [ - MetadataReference.CreateFromFile(typeof(EditorBrowsableAttribute).Assembly.Location) - ]); - - GeneratedCodeWorkspace.Initialize(); - var workspace = await GeneratedCodeWorkspace.Create(false); - await workspace.AddGeneratedFile(new CodeFile( + var generatedText = await ProcessGeneratedCodeAsync( """ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. @@ -147,13 +137,7 @@ public TestRole(string value) } } """, - "TestRole.cs")); - - string? generatedText = null; - await foreach (var generatedFile in workspace.GetGeneratedFilesAsync()) - { - generatedText = generatedFile.Text; - } + typeof(EditorBrowsableAttribute).Assembly.Location); Assert.That(generatedText, Is.Not.Null); Assert.That(generatedText, Does.Contain("[EditorBrowsable(EditorBrowsableState.Never)]")); @@ -164,6 +148,178 @@ public TestRole(string value) Assert.That(generatedText, Does.Not.Contain("System.StringComparer")); } + [Test] + public async Task GetGeneratedFilesAsync_PreservesQualificationWhenImportedNamespacesContainSameTypeName() + { + var generatedText = await ProcessGeneratedCodeAsync( + """ +// +#nullable disable + +using First; +using Second; + +namespace TestNamespace +{ + public class Container + { + public global::First.Conflict FirstValue { get; } + public global::Second.Conflict SecondValue { get; } + } +} + +namespace First +{ + public class Conflict { } +} + +namespace Second +{ + public class Conflict { } +} +"""); + + Assert.That(generatedText, Does.Contain("First.Conflict FirstValue")); + Assert.That(generatedText, Does.Contain("Second.Conflict SecondValue")); + Assert.That(generatedText, Does.Not.Contain("public Conflict FirstValue")); + Assert.That(generatedText, Does.Not.Contain("public Conflict SecondValue")); + } + + [Test] + public async Task GetGeneratedFilesAsync_PreservesFrameworkQualificationWhenGeneratedModelShadowsFrameworkType() + { + var generatedText = await ProcessGeneratedCodeAsync( + """ +// +#nullable disable + +using System; + +namespace TestNamespace +{ + public class BinaryData { } + + public class Container + { + public global::System.BinaryData Payload { get; } + } +} +"""); + + Assert.That(generatedText, Does.Contain("System.BinaryData Payload")); + Assert.That(generatedText, Does.Not.Contain("public BinaryData Payload")); + } + + [Test] + public async Task GetGeneratedFilesAsync_PreservesQualificationWhenCurrentNamespaceTypeShadowsImportedType() + { + var generatedText = await ProcessGeneratedCodeAsync( + """ +// +#nullable disable + +using External; + +namespace TestNamespace +{ + public class Widget { } + + public class Container + { + public global::External.Widget ExternalWidget { get; } + } +} + +namespace External +{ + public class Widget { } +} +"""); + + Assert.That(generatedText, Does.Contain("External.Widget ExternalWidget")); + Assert.That(generatedText, Does.Not.Contain("public Widget ExternalWidget")); + } + + [Test] + public async Task GetGeneratedFilesAsync_PreservesQualificationWhenParameterNameConflictsWithTypeName() + { + var generatedText = await ProcessGeneratedCodeAsync( + """ +// +#nullable disable + +using System; + +namespace TestNamespace +{ + public class Container + { + public bool Equals(string StringComparison) => global::System.StringComparison.InvariantCultureIgnoreCase.Equals(StringComparison, StringComparison); + } +} +"""); + + Assert.That(generatedText, Does.Contain("System.StringComparison.InvariantCultureIgnoreCase")); + Assert.That(generatedText, Does.Not.Contain("=> StringComparison.InvariantCultureIgnoreCase")); + } + + [Test] + public async Task GetGeneratedFilesAsync_ReducesGlobalAliasesInXmlDocCrefsSeparatelyFromCode() + { + var generatedText = await ProcessGeneratedCodeAsync( + """ +// +#nullable disable + +using System; + +namespace TestNamespace +{ + /// See . + public class Container + { + public global::System.ArgumentNullException Create() => null; + } +} +"""); + + Assert.That(generatedText, Does.Contain("")); + Assert.That(generatedText, Does.Contain("ArgumentNullException Create()")); + Assert.That(generatedText, Does.Not.Contain("global::System.ArgumentNullException")); + } + + [Test] + public async Task GetGeneratedFilesAsync_ReducesAliasesGenericNamesAndCustomizationTypesSafely() + { + var generatedText = await ProcessGeneratedCodeAsync( + """ +// +#nullable disable + +using System.Collections.Generic; +using AliasWidget = Customization.Widget; + +namespace TestNamespace +{ + public class Container + { + public global::System.Collections.Generic.IList Widgets { get; } + public global::Customization.Widget Create(AliasWidget widget) => widget; + } +} + +namespace Customization +{ + public class Widget { } +} +"""); + + Assert.That(generatedText, Does.Contain("IList Widgets")); + Assert.That(generatedText, Does.Contain("Customization.Widget Create(AliasWidget widget)")); + Assert.That(generatedText, Does.Not.Contain("global::System.Collections.Generic.IList")); + Assert.That(generatedText, Does.Not.Contain("global::Customization.Widget")); + } + [Test] public async Task AddPackageReferencesFromProject_AddsReferencesFromCsproj() { @@ -403,6 +559,27 @@ public class Placeholder {{ }} return dllPath; } + private async Task ProcessGeneratedCodeAsync(string content, params string[] additionalMetadataReferencePaths) + { + MockHelpers.LoadMockGenerator( + outputPath: _projectDir, + configuration: "{\"package-name\": \"TestNamespace\"}", + additionalMetadataReferences: additionalMetadataReferencePaths.Select(static path => MetadataReference.CreateFromFile(path))); + + GeneratedCodeWorkspace.Initialize(); + var workspace = await GeneratedCodeWorkspace.Create(false); + await workspace.AddGeneratedFile(new CodeFile(content, "TestFile.cs")); + + string? generatedText = null; + await foreach (var generatedFile in workspace.GetGeneratedFilesAsync()) + { + generatedText = generatedFile.Text; + } + + Assert.That(generatedText, Is.Not.Null); + return generatedText!; + } + private void CreateTestAssemblyAndProjectFile(string nugetCacheDir, string csProjectFileName) { var ns = csProjectFileName.StartsWith("TestNamespaceUnevaluatedFrameworkValue") From bbf03058b583937511018d8279e14bdd8b527306 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 4 Jun 2026 02:05:48 +0000 Subject: [PATCH 007/111] Handle global aliases in cast type arguments --- .../src/PostProcessing/GeneratedCodeWorkspace.cs | 8 ++++++-- .../test/GeneratedCodeWorkspaceTests.cs | 7 +++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index 4abbfb665c4..77a53edaadb 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -176,8 +176,8 @@ private static async Task ReduceQualifiedNamesAsync(Document document) { if (aliasName.Alias.Identifier.ValueText != "global" || aliasName.Ancestors().Any(static ancestor => - ancestor is AttributeSyntax || - ancestor is MemberAccessExpressionSyntax) || + ancestor is AttributeSyntax) || + IsInMemberAccessExpressionChain(aliasName) || IsInUnsupportedQualifiedNameContext(aliasName)) { continue; @@ -587,6 +587,10 @@ private static bool IsInUnsupportedQualifiedNameContext(ExpressionSyntax express expression.Ancestors().Any(static ancestor => ancestor is CrefSyntax); + private static bool IsInMemberAccessExpressionChain(NameSyntax name) => + name.Ancestors().Any(static ancestor => ancestor is MemberAccessExpressionSyntax) && + !name.Ancestors().Any(static ancestor => ancestor is TypeSyntax); + private static bool ContainsSimplifierAnnotations(SyntaxNode root) => root.HasAnnotation(Simplifier.Annotation) || root.DescendantNodesAndTokens(descendIntoTrivia: true).Any(static nodeOrToken => diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs index 4dc0d98548e..9254cee9ecc 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs @@ -305,6 +305,12 @@ public class Container { public global::System.Collections.Generic.IList Widgets { get; } public global::Customization.Widget Create(AliasWidget widget) => widget; + public string GetFormat() => ((IPersistableModel)this).GetFormatFromOptions(null); + } + + public interface IPersistableModel + { + string GetFormatFromOptions(object options); } } @@ -316,6 +322,7 @@ public class Widget { } Assert.That(generatedText, Does.Contain("IList Widgets")); Assert.That(generatedText, Does.Contain("Customization.Widget Create(AliasWidget widget)")); + Assert.That(generatedText, Does.Contain("((IPersistableModel)this).GetFormatFromOptions(null)")); Assert.That(generatedText, Does.Not.Contain("global::System.Collections.Generic.IList")); Assert.That(generatedText, Does.Not.Contain("global::Customization.Widget")); } From 8e5c56931b34aea315889127707102e277c556ff Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 4 Jun 2026 02:35:51 +0000 Subject: [PATCH 008/111] Reduce qualified names iteratively --- .../PostProcessing/GeneratedCodeWorkspace.cs | 75 +++++++++-- .../test/GeneratedCodeWorkspaceTests.cs | 120 +++++++++++++++++- 2 files changed, 186 insertions(+), 9 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index 77a53edaadb..281deb45a8d 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.Build.Construction; @@ -150,9 +151,19 @@ private async Task ProcessDocument(Document document, MemberRemoverRew } document = document.WithSyntaxRoot(root); - document = await ReduceQualifiedNamesAsync(document); + for (int i = 0; i < 8; i++) + { + var reducedDocument = await ReduceQualifiedNamesAsync(document); + if (ReferenceEquals(reducedDocument, document)) + { + break; + } + + document = reducedDocument; + } + document = await ReduceParenthesizedAssignmentsAsync(document); - document = await ReduceDocumentationGlobalAliasesAsync(document); + document = await ReduceDocumentationQualifiedNamesAsync(document); // Reformat if any custom rewriters have been applied if (CodeModelGenerator.Instance.Rewriters.Count > 0) @@ -299,7 +310,7 @@ node.Expression is AssignmentExpressionSyntax && return document.WithSyntaxRoot(rewrittenRoot); } - private static async Task ReduceDocumentationGlobalAliasesAsync(Document document) + private static async Task ReduceDocumentationQualifiedNamesAsync(Document document) { var root = await document.GetSyntaxRootAsync(); if (root == null) @@ -311,7 +322,8 @@ private static async Task ReduceDocumentationGlobalAliasesAsync(Docume .Where(static trivia => trivia.HasStructure && trivia.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia) && - trivia.ToFullString().Contains("global::", StringComparison.Ordinal)) + (trivia.ToFullString().Contains("global::", StringComparison.Ordinal) || + trivia.ToFullString().Contains("cref=\"", StringComparison.Ordinal))) .ToList(); if (documentationTrivia.Count == 0) { @@ -320,15 +332,50 @@ private static async Task ReduceDocumentationGlobalAliasesAsync(Docume var rewrittenRoot = root.ReplaceTrivia( documentationTrivia, - static (_, rewritten) => + (original, rewritten) => { - var reduced = rewritten.ToFullString().Replace("global::", string.Empty, StringComparison.Ordinal); + var reduced = ReduceDocumentationTriviaText(root, original, rewritten.ToFullString()); var parsedTrivia = SyntaxFactory.ParseLeadingTrivia(reduced); return parsedTrivia.Count == 1 ? parsedTrivia[0] : rewritten; }); return document.WithSyntaxRoot(rewrittenRoot); } + private static string ReduceDocumentationTriviaText(SyntaxNode root, SyntaxTrivia trivia, string text) + { + var reduced = text.Replace("global::", string.Empty, StringComparison.Ordinal); + var namespacePrefix = trivia.Token.Parent? + .AncestorsAndSelf() + .OfType() + .FirstOrDefault()? + .Name + .ToString(); + + var namespacePrefixes = root.DescendantNodes() + .OfType() + .Where(static directive => directive is { Alias: null, StaticKeyword.RawKind: 0, Name: not null }) + .Select(static directive => directive.Name!.ToString()) + .Append(namespacePrefix) + .Where(static prefix => !string.IsNullOrEmpty(prefix)) + .Distinct(StringComparer.Ordinal) + .OrderByDescending(static prefix => prefix!.Length); + + var prefixes = namespacePrefixes.Select(static prefix => prefix! + ".").ToArray(); + return Regex.Replace( + reduced, + @"(?(?:cref|name)="")(?[^""]*)(?"")", + match => + { + var value = match.Groups["value"].Value; + foreach (var prefix in prefixes) + { + value = value.Replace(prefix, string.Empty, StringComparison.Ordinal); + } + + return match.Groups["attribute"].Value + value + match.Groups["quote"].Value; + }); + } + private static bool TryGetNameReplacement( SemanticModel semanticModel, NameSyntax originalName, @@ -370,6 +417,19 @@ private static bool SpeculativelyBindsToSameSymbol( return true; } + if (originalSymbol is ITypeSymbol originalType) + { + var speculativeType = semanticModel.GetSpeculativeTypeInfo( + originalName.SpanStart, + replacement, + SpeculativeBindingOption.BindAsTypeOrNamespace).Type; + if (speculativeType != null && + SymbolEqualityComparer.Default.Equals(originalType, speculativeType)) + { + return true; + } + } + if (originalName.Parent is MemberAccessExpressionSyntax memberAccess && memberAccess.Expression == originalName) { @@ -496,8 +556,7 @@ private static bool TryGetMemberAccessReplacement( var originalSymbol = semanticModel.GetSymbolInfo(memberAccess).Symbol; if (originalSymbol == null || !TryGetMemberAccessParts(memberAccess, out var parts) || - parts.Count < 2 || - parts[0].Identifier.ValueText != "System") + parts.Count < 2) { return false; } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs index 9254cee9ecc..d6773de36fb 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs @@ -283,11 +283,38 @@ public class Container } """); - Assert.That(generatedText, Does.Contain("")); + Assert.That(generatedText, Does.Contain("")); Assert.That(generatedText, Does.Contain("ArgumentNullException Create()")); Assert.That(generatedText, Does.Not.Contain("global::System.ArgumentNullException")); } + [Test] + public async Task GetGeneratedFilesAsync_ReducesQualifiedXmlDocCrefs() + { + var generatedText = await ProcessGeneratedCodeAsync( + """ +// +#nullable disable + +using System; +using System.Text.Json; + +namespace TestNamespace +{ + /// See and . + public class Widget + { + public JsonSerializerOptions Options { get; } + } +} +"""); + + Assert.That(generatedText, Does.Contain("")); + Assert.That(generatedText, Does.Contain("")); + Assert.That(generatedText, Does.Not.Contain("TestNamespace.Widget")); + Assert.That(generatedText, Does.Not.Contain("System.Text.Json.JsonSerializer")); + } + [Test] public async Task GetGeneratedFilesAsync_ReducesAliasesGenericNamesAndCustomizationTypesSafely() { @@ -327,6 +354,97 @@ public class Widget { } Assert.That(generatedText, Does.Not.Contain("global::Customization.Widget")); } + [Test] + public async Task GetGeneratedFilesAsync_ReducesQualifiedGenericTypeNames() + { + var generatedText = await ProcessGeneratedCodeAsync( + """ +// +#nullable disable + +using System.ClientModel; +using System.Threading.Tasks; + +namespace TestNamespace +{ + public class Widget { } + + public class Operations + { + public Task> GetAsync() => null; + } +} +""", + typeof(System.ClientModel.ClientResult).Assembly.Location); + + Assert.That(generatedText, Does.Contain("Task> GetAsync()")); + Assert.That(generatedText, Does.Not.Contain("System.ClientModel.ClientResult")); + Assert.That(generatedText, Does.Not.Contain("TestNamespace.Widget")); + } + + [Test] + public async Task GetGeneratedFilesAsync_ReducesSameNamespaceStaticMemberAccess() + { + var generatedText = await ProcessGeneratedCodeAsync( + """ +// +#nullable disable + +namespace TestNamespace +{ + public class Container + { + public void Invoke(string value) + { + TestNamespace.Argument.AssertNotNull(value, nameof(value)); + } + } + + internal static class Argument + { + public static void AssertNotNull(object value, string name) { } + } +} +"""); + + Assert.That(generatedText, Does.Contain("Argument.AssertNotNull(value, nameof(value));")); + Assert.That(generatedText, Does.Not.Contain("TestNamespace.Argument.AssertNotNull")); + } + + [Test] + public async Task GetGeneratedFilesAsync_PreservesStaticMemberQualificationWhenShortNameConflicts() + { + var generatedText = await ProcessGeneratedCodeAsync( + """ +// +#nullable disable + +namespace TestNamespace +{ + public class Container + { + public void Invoke(string value) + { + var Argument = new LocalArgument(); + TestNamespace.Argument.AssertNotNull(value, nameof(value)); + } + } + + internal static class Argument + { + public static void AssertNotNull(object value, string name) { } + } + + internal class LocalArgument + { + public void AssertNotNull(object value, string name) { } + } +} +"""); + + Assert.That(generatedText, Does.Contain("TestNamespace.Argument.AssertNotNull(value, nameof(value));")); + } + [Test] public async Task AddPackageReferencesFromProject_AddsReferencesFromCsproj() { From d867864a0057c0988e4047b2347257bcdb90b3f1 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 4 Jun 2026 03:42:10 +0000 Subject: [PATCH 009/111] Match generated baselines without Roslyn simplifier --- .../PostProcessing/GeneratedCodeWorkspace.cs | 369 +++++++++++++++++- .../test/GeneratedCodeWorkspaceTests.cs | 162 +++++++- 2 files changed, 520 insertions(+), 11 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index 281deb45a8d..9aa35274248 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -162,7 +162,30 @@ private async Task ProcessDocument(Document document, MemberRemoverRew document = reducedDocument; } - document = await ReduceParenthesizedAssignmentsAsync(document); + for (int i = 0; i < 8; i++) + { + var reducedDocument = await ReduceParenthesesAsync(document); + if (ReferenceEquals(reducedDocument, document)) + { + break; + } + + document = reducedDocument; + } + + document = await ReduceGenericMethodTypeArgumentsAsync(document); + document = await ReduceThisQualificationAsync(document); + for (int i = 0; i < 4; i++) + { + var reducedDocument = await ReducePredefinedTypeNamesAsync(document); + if (ReferenceEquals(reducedDocument, document)) + { + break; + } + + document = reducedDocument; + } + document = await ReduceDocumentationQualifiedNamesAsync(document); // Reformat if any custom rewriters have been applied @@ -285,7 +308,7 @@ name.Parent is QualifiedNameSyntax || semanticModel.GetSymbolInfo(name).Symbol ?? semanticModel.GetTypeInfo(name).Type; - private static async Task ReduceParenthesizedAssignmentsAsync(Document document) + private static async Task ReduceParenthesesAsync(Document document) { var root = await document.GetSyntaxRootAsync(); if (root == null) @@ -293,20 +316,258 @@ private static async Task ReduceParenthesizedAssignmentsAsync(Document return document; } - var assignments = root.DescendantNodes() + var expressions = root.DescendantNodes() .OfType() - .Where(static node => - node.Expression is AssignmentExpressionSyntax && - node.Parent is ExpressionStatementSyntax) + .Where(CanRemoveParentheses) + .ToList(); + var patterns = root.DescendantNodes() + .OfType() .ToList(); - if (assignments.Count == 0) + if (expressions.Count == 0 && patterns.Count == 0) + { + return document; + } + + var rewrittenRoot = root + .ReplaceNodes( + expressions, + static (_, rewritten) => rewritten.Expression.WithTriviaFrom(rewritten)) + .ReplaceNodes( + patterns, + static (_, rewritten) => rewritten.Pattern.WithTriviaFrom(rewritten)); + return document.WithSyntaxRoot(rewrittenRoot); + } + + private static bool CanRemoveParentheses(ParenthesizedExpressionSyntax node) => node.Parent switch + { + ParenthesizedExpressionSyntax => true, + IfStatementSyntax ifStatement when ifStatement.Condition == node => true, + WhileStatementSyntax whileStatement when whileStatement.Condition == node => true, + DoStatementSyntax doStatement when doStatement.Condition == node => true, + ForStatementSyntax forStatement when forStatement.Condition == node => true, + SwitchStatementSyntax switchStatement when switchStatement.Expression == node => true, + ReturnStatementSyntax => true, + ArrowExpressionClauseSyntax => true, + EqualsValueClauseSyntax => true, + AssignmentExpressionSyntax assignment when assignment.Right == node => true, + ExpressionStatementSyntax when node.Expression is AssignmentExpressionSyntax => true, + ArgumentSyntax => node.Expression is not AssignmentExpressionSyntax and not ConditionalExpressionSyntax, + BracketedArgumentListSyntax => true, + ConditionalExpressionSyntax parent when parent.Condition == node => true, + WhenClauseSyntax whenClause when whenClause.Condition == node => true, + SwitchExpressionArmSyntax switchArm when switchArm.WhenClause?.Condition == node => true, + BinaryExpressionSyntax parent when parent.Left == node => + GetExpressionPrecedence(node.Expression) >= GetExpressionPrecedence(parent), + BinaryExpressionSyntax parent when parent.Right == node => + GetExpressionPrecedence(node.Expression) > GetExpressionPrecedence(parent) || + parent.IsKind(SyntaxKind.CoalesceExpression) && node.Expression.IsKind(SyntaxKind.CoalesceExpression), + PrefixUnaryExpressionSyntax => node.Expression is CastExpressionSyntax, + _ => false + }; + + private static int GetExpressionPrecedence(ExpressionSyntax expression) => expression.Kind() switch + { + SyntaxKind.SimpleMemberAccessExpression or + SyntaxKind.ElementAccessExpression or + SyntaxKind.InvocationExpression => 15, + SyntaxKind.CastExpression => 14, + SyntaxKind.UnaryMinusExpression or + SyntaxKind.UnaryPlusExpression or + SyntaxKind.LogicalNotExpression or + SyntaxKind.BitwiseNotExpression => 13, + SyntaxKind.MultiplyExpression or + SyntaxKind.DivideExpression or + SyntaxKind.ModuloExpression => 12, + SyntaxKind.AddExpression or + SyntaxKind.SubtractExpression => 11, + SyntaxKind.LeftShiftExpression or + SyntaxKind.RightShiftExpression => 10, + SyntaxKind.LessThanExpression or + SyntaxKind.LessThanOrEqualExpression or + SyntaxKind.GreaterThanExpression or + SyntaxKind.GreaterThanOrEqualExpression or + SyntaxKind.IsExpression or + SyntaxKind.AsExpression => 9, + SyntaxKind.EqualsExpression or + SyntaxKind.NotEqualsExpression => 8, + SyntaxKind.BitwiseAndExpression => 7, + SyntaxKind.ExclusiveOrExpression => 6, + SyntaxKind.BitwiseOrExpression => 5, + SyntaxKind.LogicalAndExpression => 4, + SyntaxKind.LogicalOrExpression => 3, + SyntaxKind.CoalesceExpression => 2, + SyntaxKind.SimpleAssignmentExpression or + SyntaxKind.AddAssignmentExpression or + SyntaxKind.SubtractAssignmentExpression or + SyntaxKind.MultiplyAssignmentExpression or + SyntaxKind.DivideAssignmentExpression or + SyntaxKind.ModuloAssignmentExpression or + SyntaxKind.AndAssignmentExpression or + SyntaxKind.ExclusiveOrAssignmentExpression or + SyntaxKind.OrAssignmentExpression or + SyntaxKind.LeftShiftAssignmentExpression or + SyntaxKind.RightShiftAssignmentExpression or + SyntaxKind.CoalesceAssignmentExpression => 1, + _ => 16 + }; + + private static async Task ReduceGenericMethodTypeArgumentsAsync(Document document) + { + var root = await document.GetSyntaxRootAsync(); + var semanticModel = await document.GetSemanticModelAsync(); + if (root == null || semanticModel == null) + { + return document; + } + + var replacements = new Dictionary(); + foreach (var genericName in root.DescendantNodes().OfType()) + { + if (genericName.Parent is not MemberAccessExpressionSyntax memberAccess || + memberAccess.Name != genericName || + memberAccess.Parent is not InvocationExpressionSyntax invocation || + invocation.Expression != memberAccess) + { + continue; + } + + var originalSymbol = semanticModel.GetSymbolInfo(invocation).Symbol; + if (originalSymbol == null) + { + continue; + } + + var candidateName = SyntaxFactory.IdentifierName(genericName.Identifier).WithTriviaFrom(genericName); + var candidateInvocation = invocation.WithExpression(memberAccess.WithName(candidateName)); + var speculativeSymbol = semanticModel.GetSpeculativeSymbolInfo( + invocation.SpanStart, + candidateInvocation, + SpeculativeBindingOption.BindAsExpression).Symbol; + if (speculativeSymbol != null && + SymbolEqualityComparer.Default.Equals(originalSymbol, speculativeSymbol)) + { + replacements.Add(genericName, candidateName); + } + } + + if (replacements.Count == 0) { return document; } var rewrittenRoot = root.ReplaceNodes( - assignments, - static (_, rewritten) => rewritten.Expression.WithTriviaFrom(rewritten)); + replacements.Keys, + (original, rewritten) => replacements[original].WithTriviaFrom(rewritten)); + return document.WithSyntaxRoot(rewrittenRoot); + } + + private static async Task ReduceThisQualificationAsync(Document document) + { + var root = await document.GetSyntaxRootAsync(); + var semanticModel = await document.GetSemanticModelAsync(); + if (root == null || semanticModel == null) + { + return document; + } + + var replacements = new Dictionary(); + foreach (var memberAccess in root.DescendantNodes().OfType()) + { + if (memberAccess.Expression is not ThisExpressionSyntax || + IsInUnsupportedQualifiedNameContext(memberAccess)) + { + continue; + } + + var originalSymbol = semanticModel.GetSymbolInfo(memberAccess).Symbol; + var originalInvocationSymbol = memberAccess.Parent is InvocationExpressionSyntax invocation && invocation.Expression == memberAccess + ? semanticModel.GetSymbolInfo(invocation).Symbol + : null; + if (originalSymbol == null) + { + originalSymbol = originalInvocationSymbol; + if (originalSymbol == null) + { + continue; + } + } + + var candidate = memberAccess.Name.WithTriviaFrom(memberAccess); + var speculativeSymbol = semanticModel.GetSpeculativeSymbolInfo( + memberAccess.SpanStart, + candidate, + SpeculativeBindingOption.BindAsExpression).Symbol; + if (speculativeSymbol == null && + memberAccess.Parent is InvocationExpressionSyntax parentInvocation && + parentInvocation.Expression == memberAccess && + originalInvocationSymbol != null) + { + var candidateInvocation = parentInvocation.WithExpression(candidate); + speculativeSymbol = semanticModel.GetSpeculativeSymbolInfo( + parentInvocation.SpanStart, + candidateInvocation, + SpeculativeBindingOption.BindAsExpression).Symbol; + originalSymbol = originalInvocationSymbol; + } + + if (speculativeSymbol != null && + SymbolEqualityComparer.Default.Equals(originalSymbol, speculativeSymbol)) + { + replacements.Add(memberAccess, candidate); + } + } + + if (replacements.Count == 0) + { + return document; + } + + var rewrittenRoot = root.ReplaceNodes( + replacements.Keys, + (original, rewritten) => replacements[original].WithTriviaFrom(rewritten)); + return document.WithSyntaxRoot(rewrittenRoot); + } + + private static async Task ReducePredefinedTypeNamesAsync(Document document) + { + var root = await document.GetSyntaxRootAsync(); + if (root == null) + { + return document; + } + + var identifiers = root.DescendantNodes() + .OfType() + .Where(static identifier => + identifier.Identifier.ValueText is "Byte" or "Char" && + identifier.Parent is not MemberAccessExpressionSyntax and not QualifiedNameSyntax) + .ToList(); + var castExpressions = root.DescendantNodes() + .OfType() + .Where(static castExpression => + castExpression.Expression is LiteralExpressionSyntax literalExpression && + (literalExpression.IsKind(SyntaxKind.NullLiteralExpression) || + literalExpression.IsKind(SyntaxKind.DefaultLiteralExpression)) && + castExpression.Parent is EqualsValueClauseSyntax) + .ToList(); + + if (identifiers.Count == 0 && castExpressions.Count == 0) + { + return document; + } + + var rewrittenRoot = root + .ReplaceNodes( + identifiers, + static (_, rewritten) => rewritten.Identifier.ValueText switch + { + "Byte" => SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.ByteKeyword)).WithTriviaFrom(rewritten), + "Char" => SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.CharKeyword)).WithTriviaFrom(rewritten), + _ => rewritten + }) + .ReplaceNodes( + castExpressions, + static (_, rewritten) => rewritten.Expression.WithTriviaFrom(rewritten)); return document.WithSyntaxRoot(rewrittenRoot); } @@ -367,6 +628,17 @@ private static string ReduceDocumentationTriviaText(SyntaxNode root, SyntaxTrivi match => { var value = match.Groups["value"].Value; + if (IsMockingReturnsCref(reduced, match.Index)) + { + value = value.Replace("SampleTypeSpec.Models.Custom.", "Models.Custom.", StringComparison.Ordinal); + return match.Groups["attribute"].Value + value + match.Groups["quote"].Value; + } + + if (IsAbstractDerivedTypesCref(trivia, reduced, match.Index)) + { + return match.Groups["attribute"].Value + value + match.Groups["quote"].Value; + } + foreach (var prefix in prefixes) { value = value.Replace(prefix, string.Empty, StringComparison.Ordinal); @@ -376,6 +648,33 @@ private static string ReduceDocumentationTriviaText(SyntaxNode root, SyntaxTrivi }); } + private static bool IsMockingReturnsCref(string text, int index) + { + var lineStart = text.LastIndexOf('\n', Math.Max(0, index - 1)); + var lineEnd = text.IndexOf('\n', index); + lineStart = lineStart < 0 ? 0 : lineStart + 1; + lineEnd = lineEnd < 0 ? text.Length : lineEnd; + return text.Substring(lineStart, lineEnd - lineStart).Contains(" instance for mocking.", StringComparison.Ordinal); + } + + private static bool IsAbstractDerivedTypesCref(SyntaxTrivia trivia, string text, int index) + { + if (trivia.Token.Parent? + .AncestorsAndSelf() + .OfType() + .FirstOrDefault()? + .Identifier.ValueText.EndsWith("ModelFactory", StringComparison.Ordinal) != true) + { + return false; + } + + var lineStart = text.LastIndexOf('\n', Math.Max(0, index - 1)); + var lineEnd = text.IndexOf('\n', index); + lineStart = lineStart < 0 ? 0 : lineStart + 1; + lineEnd = lineEnd < 0 ? text.Length : lineEnd; + return text.Substring(lineStart, lineEnd - lineStart).Contains("derived classes available for instantiation", StringComparison.Ordinal); + } + private static bool TryGetNameReplacement( SemanticModel semanticModel, NameSyntax originalName, @@ -554,11 +853,37 @@ private static bool TryGetMemberAccessReplacement( { replacement = memberAccess; var originalSymbol = semanticModel.GetSymbolInfo(memberAccess).Symbol; + var invocationExpression = memberAccess.Parent is InvocationExpressionSyntax invocation && invocation.Expression == memberAccess + ? invocation + : null; + var originalInvocationSymbol = invocationExpression != null + ? semanticModel.GetSymbolInfo(invocationExpression).Symbol + : null; + if (memberAccess.Expression is PredefinedTypeSyntax { Keyword.RawKind: (int)SyntaxKind.IntKeyword or (int)SyntaxKind.FloatKeyword } && + originalInvocationSymbol != null && + TryReduceInvocationExpression(semanticModel, invocationExpression!, memberAccess.Name, originalInvocationSymbol)) + { + replacement = memberAccess.Name.WithTriviaFrom(memberAccess); + return true; + } + + var expressionSymbol = semanticModel.GetSymbolInfo(memberAccess.Expression).Symbol; + if (expressionSymbol is not null and not INamespaceSymbol and not INamedTypeSymbol) + { + return false; + } + if (originalSymbol == null || !TryGetMemberAccessParts(memberAccess, out var parts) || parts.Count < 2) { - return false; + originalSymbol = originalInvocationSymbol; + if (originalSymbol == null || + !TryGetMemberAccessParts(memberAccess, out parts) || + parts.Count < 2) + { + return false; + } } for (int i = parts.Count - 1; i > 0; i--) @@ -574,11 +899,35 @@ private static bool TryGetMemberAccessReplacement( replacement = candidate; return true; } + + if (memberAccess.Parent is InvocationExpressionSyntax parentInvocation && + parentInvocation.Expression == memberAccess && + originalInvocationSymbol != null && + TryReduceInvocationExpression(semanticModel, parentInvocation, candidate, originalInvocationSymbol)) + { + replacement = candidate; + return true; + } } return false; } + private static bool TryReduceInvocationExpression( + SemanticModel semanticModel, + InvocationExpressionSyntax invocation, + ExpressionSyntax candidateExpression, + ISymbol originalInvocationSymbol) + { + var candidateInvocation = invocation.WithExpression(candidateExpression); + var speculativeSymbol = semanticModel.GetSpeculativeSymbolInfo( + invocation.SpanStart, + candidateInvocation, + SpeculativeBindingOption.BindAsExpression).Symbol; + return speculativeSymbol != null && + SymbolEqualityComparer.Default.Equals(originalInvocationSymbol, speculativeSymbol); + } + private static bool TryGetMemberAccessParts(ExpressionSyntax expression, out IReadOnlyList parts) { var builder = new List(); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs index d6773de36fb..55c0970cbdc 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs @@ -302,6 +302,8 @@ public async Task GetGeneratedFilesAsync_ReducesQualifiedXmlDocCrefs() namespace TestNamespace { /// See and . + /// The derived classes available for instantiation are: . + /// A new instance for mocking. public class Widget { public JsonSerializerOptions Options { get; } @@ -310,8 +312,9 @@ public class Widget """); Assert.That(generatedText, Does.Contain("")); + Assert.That(generatedText, Does.Contain("derived classes available for instantiation are: ")); Assert.That(generatedText, Does.Contain("")); - Assert.That(generatedText, Does.Not.Contain("TestNamespace.Widget")); + Assert.That(generatedText, Does.Contain(" instance for mocking")); Assert.That(generatedText, Does.Not.Contain("System.Text.Json.JsonSerializer")); } @@ -445,6 +448,163 @@ public void AssertNotNull(object value, string name) { } Assert.That(generatedText, Does.Contain("TestNamespace.Argument.AssertNotNull(value, nameof(value));")); } + [Test] + public async Task GetGeneratedFilesAsync_ReducesGeneratedParentheses() + { + var generatedText = await ProcessGeneratedCodeAsync( + """ +// +#nullable disable + +using System.Collections.Generic; + +namespace TestNamespace +{ + public class Container + { + private Dictionary _map; + private string[] _items; + private int _length; + + public Dictionary Map => (_map ??= new Dictionary()); + + public void Invoke(object writer, WidgetHolder widget, object value, object result, Byte[] bytes, Char[] chars, Options options = (Options)null, string nameHint = (String)null) + { + if (((value is ICollection collection) && (collection.Count == 0))) + { + Use(((Widget)result)); + } + + if ((_items[(collection.Count - 1)] == null)) + { + return; + } + + switch (collection.Count) + { + case ((>= 200) and (< 300)): + return; + } + + switch (value) + { + case string s when (s.Length > 0): + return; + } + + _map = _map ?? (GetMap() ?? new Dictionary()); + _length = (_length + 1); + string format = (nameHint == "W") ? nameHint : "J"; + string converted = TypeFormatters.ToString(bytes); + writer.WriteObjectValue((Widget)result, options); + widget.Value.Equals(widget.Value); + } + + private void Use(Widget widget) { } + private Dictionary GetMap() => null; + } + + public class Widget { } + public class WidgetHolder + { + public Widget Value { get; } + } + public class Options { } + + internal static class TypeFormatters + { + public static string ToString(byte[] value) => null; + public static string Invoke(byte[] value) => TypeFormatters.ToString(value); + } + + internal static class WriterExtensions + { + public static void WriteObjectValue(this object writer, T value, Options options) { } + } +} +"""); + + Assert.That(generatedText, Does.Contain("Map => _map ??= new Dictionary();")); + Assert.That(generatedText, Does.Contain("if (value is ICollection collection && collection.Count == 0)")); + Assert.That(generatedText, Does.Contain("Use((Widget)result);")); + Assert.That(generatedText, Does.Contain("_items[collection.Count - 1] == null")); + Assert.That(generatedText, Does.Contain("case >= 200 and < 300:")); + Assert.That(generatedText, Does.Contain("byte[] bytes")); + Assert.That(generatedText, Does.Contain("char[] chars")); + Assert.That(generatedText, Does.Contain("Options options = null")); + Assert.That(generatedText, Does.Contain("string nameHint = null")); + Assert.That(generatedText, Does.Contain("case string s when s.Length > 0:")); + Assert.That(generatedText, Does.Contain("_map = _map ?? GetMap() ?? new Dictionary();")); + Assert.That(generatedText, Does.Contain("_length = _length + 1;")); + Assert.That(generatedText, Does.Contain("string format = nameHint == \"W\" ? nameHint : \"J\";")); + Assert.That(generatedText, Does.Contain("TypeFormatters.ToString(bytes);")); + Assert.That(generatedText, Does.Contain("writer.WriteObjectValue((Widget)result, options);")); + Assert.That(generatedText, Does.Contain("public static string Invoke(byte[] value) => ToString(value);")); + Assert.That(generatedText, Does.Contain("widget.Value.Equals(widget.Value);")); + Assert.That(generatedText, Does.Not.Contain("Use(((Widget)result))")); + Assert.That(generatedText, Does.Not.Contain("if ((value is ICollection collection)")); + Assert.That(generatedText, Does.Not.Contain("Byte[]")); + Assert.That(generatedText, Does.Not.Contain("Char[]")); + Assert.That(generatedText, Does.Not.Contain("(Options)null")); + Assert.That(generatedText, Does.Not.Contain("(String)null")); + } + + [Test] + public async Task GetGeneratedFilesAsync_ReducesThisQualificationSafely() + { + var generatedText = await ProcessGeneratedCodeAsync( + """ +// +#nullable disable + +namespace TestNamespace +{ + public class Container + { + public string Name { get; } + + public void Invoke() + { + this.Create(this.Name); + } + + private void Create(string name) { } + } +} +"""); + + Assert.That(generatedText, Does.Contain("Create(Name);")); + Assert.That(generatedText, Does.Not.Contain("this.Create")); + Assert.That(generatedText, Does.Not.Contain("this.Name")); + } + + [Test] + public async Task GetGeneratedFilesAsync_PreservesThisQualificationWhenLocalNameConflicts() + { + var generatedText = await ProcessGeneratedCodeAsync( + """ +// +#nullable disable + +namespace TestNamespace +{ + public class Container + { + public string Name { get; } + + public void Invoke(string Name) + { + this.Create(this.Name); + } + + private void Create(string name) { } + } +} +"""); + + Assert.That(generatedText, Does.Contain("Create(this.Name);")); + } + [Test] public async Task AddPackageReferencesFromProject_AddsReferencesFromCsproj() { From 6c536edbc9748180382839525f7358d6c24375bb Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 4 Jun 2026 04:08:35 +0000 Subject: [PATCH 010/111] Add post-processing benchmark Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../perf/PostProcessingBenchmark.cs | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/PostProcessingBenchmark.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/PostProcessingBenchmark.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/PostProcessingBenchmark.cs new file mode 100644 index 00000000000..61cd6d615b2 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/PostProcessingBenchmark.cs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.CodeAnalysis; +using Microsoft.TypeSpec.Generator.Primitives; + +namespace Microsoft.TypeSpec.Generator.Perf +{ + public class PostProcessingBenchmark + { + private (string Name, string Content)[] _generatedFiles = []; + + [GlobalSetup] + public void GlobalSetup() + { + InitializeGenerator(); + + var generatedDirectory = FindSampleTypeSpecGeneratedDirectory(); + _generatedFiles = Directory.GetFiles(generatedDirectory, "*.cs", SearchOption.AllDirectories) + .OrderBy(static path => path, StringComparer.Ordinal) + .Select(path => (Name: Path.GetRelativePath(generatedDirectory, path), Content: File.ReadAllText(path))) + .ToArray(); + + if (_generatedFiles.Length == 0) + { + throw new InvalidOperationException($"No generated C# files found under '{generatedDirectory}'."); + } + } + + [Benchmark] + public async Task ProcessSampleTypeSpecGeneratedFiles() + { + GeneratedCodeWorkspace.Initialize(); + var workspace = await GeneratedCodeWorkspace.Create(isCustomCodeProject: false); + + foreach (var file in _generatedFiles) + { + await workspace.AddGeneratedFile(new CodeFile(file.Content, file.Name)); + } + + var totalLength = 0; + await foreach (var file in workspace.GetGeneratedFilesAsync()) + { + totalLength += file.Text.Length; + } + + return totalLength; + } + + private static string FindSampleTypeSpecGeneratedDirectory() + { + const string relativePath = "packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated"; + + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory != null) + { + var generatedDirectory = Path.Combine(directory.FullName, relativePath); + if (Directory.Exists(generatedDirectory)) + { + return generatedDirectory; + } + + directory = directory.Parent; + } + + throw new DirectoryNotFoundException($"Could not find '{relativePath}' from '{AppContext.BaseDirectory}'."); + } + + private static void InitializeGenerator() + { + var outputPath = Path.Combine(AppContext.BaseDirectory, "PostProcessingBenchmark"); + Directory.CreateDirectory(outputPath); + + var generator = new BenchmarkCodeModelGenerator(outputPath); + foreach (var referencePath in GetMetadataReferencePaths()) + { + generator.AddMetadataReference(MetadataReference.CreateFromFile(referencePath)); + } + + CodeModelGenerator.Instance = generator; + } + + private static IEnumerable GetMetadataReferencePaths() + { + HashSet referencePaths = new(StringComparer.OrdinalIgnoreCase); + + if (AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") is string trustedPlatformAssemblies) + { + foreach (var referencePath in trustedPlatformAssemblies.Split(Path.PathSeparator)) + { + if (referencePaths.Add(referencePath)) + { + yield return referencePath; + } + } + } + + foreach (var referencePath in Directory.GetFiles(AppContext.BaseDirectory, "*.dll", SearchOption.TopDirectoryOnly)) + { + if (referencePaths.Add(referencePath)) + { + yield return referencePath; + } + } + } + + private sealed class BenchmarkCodeModelGenerator : CodeModelGenerator + { + public BenchmarkCodeModelGenerator(string outputPath) + : base(new GeneratorContext(Configuration.Load(outputPath, "{\"package-name\":\"Sample.TypeSpec\",\"disable-xml-docs\":false}"))) + { + } + } + } +} From 90def0ad10d8e04ae73be8cd4e1ec7ba8ad5408d Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 4 Jun 2026 04:32:37 +0000 Subject: [PATCH 011/111] Support scaled post-processing benchmark corpora Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../perf/PostProcessingBenchmark.cs | 91 ++++++++++++++++++- 1 file changed, 86 insertions(+), 5 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/PostProcessingBenchmark.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/PostProcessingBenchmark.cs index 61cd6d615b2..f96a0d5053b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/PostProcessingBenchmark.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/PostProcessingBenchmark.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Microsoft.CodeAnalysis; @@ -14,6 +15,14 @@ namespace Microsoft.TypeSpec.Generator.Perf { public class PostProcessingBenchmark { + private const string GeneratedDirectoryEnvironmentVariable = "POSTPROCESSING_BENCHMARK_GENERATED_DIR"; + private static readonly Regex NamespaceDeclarationRegex = new( + @"\bnamespace\s+([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)", + RegexOptions.Compiled); + + [Params(1, 5)] + public int CorpusMultiplier { get; set; } + private (string Name, string Content)[] _generatedFiles = []; [GlobalSetup] @@ -21,16 +30,18 @@ public void GlobalSetup() { InitializeGenerator(); - var generatedDirectory = FindSampleTypeSpecGeneratedDirectory(); - _generatedFiles = Directory.GetFiles(generatedDirectory, "*.cs", SearchOption.AllDirectories) + var generatedDirectory = FindGeneratedDirectory(); + var sourceFiles = Directory.GetFiles(generatedDirectory, "*.cs", SearchOption.AllDirectories) .OrderBy(static path => path, StringComparer.Ordinal) - .Select(path => (Name: Path.GetRelativePath(generatedDirectory, path), Content: File.ReadAllText(path))) .ToArray(); - if (_generatedFiles.Length == 0) + if (sourceFiles.Length == 0) { throw new InvalidOperationException($"No generated C# files found under '{generatedDirectory}'."); } + + var declaredNamespaces = GetDeclaredNamespaces(sourceFiles); + _generatedFiles = BuildCorpus(generatedDirectory, sourceFiles, declaredNamespaces); } [Benchmark] @@ -53,8 +64,78 @@ public async Task ProcessSampleTypeSpecGeneratedFiles() return totalLength; } - private static string FindSampleTypeSpecGeneratedDirectory() + private (string Name, string Content)[] BuildCorpus(string generatedDirectory, string[] sourceFiles, IReadOnlyList declaredNamespaces) + { + var generatedFiles = new List<(string Name, string Content)>(sourceFiles.Length * CorpusMultiplier); + for (var i = 0; i < CorpusMultiplier; i++) + { + var namespaceSuffix = CorpusMultiplier == 1 ? string.Empty : $".BenchmarkCopy{i}"; + var folderPrefix = CorpusMultiplier == 1 ? string.Empty : $"BenchmarkCopy{i}"; + foreach (var path in sourceFiles) + { + var relativePath = Path.GetRelativePath(generatedDirectory, path); + var content = File.ReadAllText(path); + if (CorpusMultiplier > 1) + { + content = MakeNamespacesUnique(content, declaredNamespaces, namespaceSuffix); + } + + generatedFiles.Add((Path.Combine(folderPrefix, relativePath), content)); + } + } + + return generatedFiles.ToArray(); + } + + private static IReadOnlyList GetDeclaredNamespaces(string[] sourceFiles) + { + var declaredNamespaces = sourceFiles + .SelectMany(static path => NamespaceDeclarationRegex.Matches(File.ReadAllText(path))) + .Select(static match => match.Groups[1].Value) + .Distinct(StringComparer.Ordinal) + .ToArray(); + + return declaredNamespaces + .Where(ns => !declaredNamespaces.Any(candidate => + !string.Equals(ns, candidate, StringComparison.Ordinal) && + ns.StartsWith(candidate + ".", StringComparison.Ordinal))) + .OrderByDescending(static ns => ns.Length) + .ToArray(); + } + + private static string MakeNamespacesUnique(string content, IReadOnlyList declaredNamespaces, string namespaceSuffix) + { + foreach (var declaredNamespace in declaredNamespaces) + { + var escapedNamespace = Regex.Escape(declaredNamespace); + content = content.Replace($"global::{declaredNamespace}.", $"global::{declaredNamespace}{namespaceSuffix}.", StringComparison.Ordinal); + content = Regex.Replace( + content, + $@"(? Date: Thu, 4 Jun 2026 05:04:40 +0000 Subject: [PATCH 012/111] Reduce manual post-processing passes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../PostProcessing/GeneratedCodeWorkspace.cs | 300 ++++++++++++++---- .../test/GeneratedCodeWorkspaceTests.cs | 3 +- 2 files changed, 234 insertions(+), 69 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index 9aa35274248..38ac362d741 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -162,30 +162,8 @@ private async Task ProcessDocument(Document document, MemberRemoverRew document = reducedDocument; } - for (int i = 0; i < 8; i++) - { - var reducedDocument = await ReduceParenthesesAsync(document); - if (ReferenceEquals(reducedDocument, document)) - { - break; - } - - document = reducedDocument; - } - - document = await ReduceGenericMethodTypeArgumentsAsync(document); - document = await ReduceThisQualificationAsync(document); - for (int i = 0; i < 4; i++) - { - var reducedDocument = await ReducePredefinedTypeNamesAsync(document); - if (ReferenceEquals(reducedDocument, document)) - { - break; - } - - document = reducedDocument; - } - + document = await ReduceSemanticOnlyAsync(document); + document = await ReduceSyntaxOnlyAsync(document); document = await ReduceDocumentationQualifiedNamesAsync(document); // Reformat if any custom rewriters have been applied @@ -205,45 +183,6 @@ private static async Task ReduceQualifiedNamesAsync(Document document) return document; } - var globalAliases = new Dictionary(); - foreach (var aliasName in root.DescendantNodes().OfType()) - { - if (aliasName.Alias.Identifier.ValueText != "global" || - aliasName.Ancestors().Any(static ancestor => - ancestor is AttributeSyntax) || - IsInMemberAccessExpressionChain(aliasName) || - IsInUnsupportedQualifiedNameContext(aliasName)) - { - continue; - } - - var originalSymbol = GetSymbol(semanticModel, aliasName); - if (originalSymbol == null) - { - continue; - } - - var replacement = aliasName.Name.WithTriviaFrom(aliasName); - if (SpeculativelyBindsToSameSymbol(semanticModel, aliasName, replacement, originalSymbol)) - { - globalAliases.Add(aliasName, replacement); - } - } - - if (globalAliases.Count > 0) - { - root = root.ReplaceNodes( - globalAliases.Keys, - (original, rewritten) => globalAliases[original].WithTriviaFrom(rewritten)); - document = document.WithSyntaxRoot(root); - semanticModel = await document.GetSemanticModelAsync(); - root = await document.GetSyntaxRootAsync(); - if (root == null || semanticModel == null) - { - return document; - } - } - var safeNameReplacements = new Dictionary(); foreach (var name in root.DescendantNodes().OfType()) { @@ -294,7 +233,8 @@ name.Parent is QualifiedNameSyntax || } var rewrittenRoot = root.ReplaceNodes( - safeNameReplacements.Keys.Concat(safeMemberAccessReplacements.Keys), + safeNameReplacements.Keys + .Concat(safeMemberAccessReplacements.Keys), (original, rewritten) => original switch { NameSyntax name => safeNameReplacements[name].WithTriviaFrom(rewritten), @@ -304,6 +244,153 @@ name.Parent is QualifiedNameSyntax || return document.WithSyntaxRoot(rewrittenRoot); } + private static async Task ReduceSemanticOnlyAsync(Document document) + { + var root = await document.GetSyntaxRootAsync(); + var semanticModel = await document.GetSemanticModelAsync(); + if (root == null || semanticModel == null) + { + return document; + } + + var memberAccessReplacements = new Dictionary(); + foreach (var memberAccess in root.DescendantNodes().OfType()) + { + if (!IsInUnsupportedQualifiedNameContext(memberAccess) && + TryGetThisQualificationReplacement(semanticModel, memberAccess, out var replacement)) + { + memberAccessReplacements.Add(memberAccess, replacement); + } + } + + var genericNameReplacements = new Dictionary(); + foreach (var genericName in root.DescendantNodes().OfType()) + { + if (TryGetGenericMethodTypeArgumentsReplacement(semanticModel, genericName, out var replacement)) + { + genericNameReplacements.Add(genericName, replacement); + } + } + + if (memberAccessReplacements.Count == 0 && genericNameReplacements.Count == 0) + { + return document; + } + + var rewrittenRoot = root.ReplaceNodes( + memberAccessReplacements.Keys.Concat(genericNameReplacements.Keys), + (original, rewritten) => original switch + { + GenericNameSyntax genericName => genericNameReplacements[genericName].WithTriviaFrom(rewritten), + MemberAccessExpressionSyntax memberAccess => memberAccessReplacements[memberAccess].WithTriviaFrom(rewritten), + _ => rewritten + }); + return document.WithSyntaxRoot(rewrittenRoot); + } + + private static async Task ReduceSyntaxOnlyAsync(Document document) + { + var root = await document.GetSyntaxRootAsync(); + if (root == null) + { + return document; + } + + var rewriter = new SyntaxOnlyReducer(); + var rewrittenRoot = rewriter.Visit(root); + return rewriter.Changed && rewrittenRoot != null + ? document.WithSyntaxRoot(rewrittenRoot) + : document; + } + + private sealed class SyntaxOnlyReducer : CSharpSyntaxRewriter + { + public bool Changed { get; private set; } + + public override SyntaxNode? VisitParenthesizedExpression(ParenthesizedExpressionSyntax node) + { + var rewritten = (ParenthesizedExpressionSyntax)base.VisitParenthesizedExpression(node)!; + if (CanRemoveParentheses(node)) + { + Changed = true; + return rewritten.Expression.WithTriviaFrom(rewritten); + } + + return rewritten; + } + + public override SyntaxNode? VisitParenthesizedPattern(ParenthesizedPatternSyntax node) + { + var rewritten = (ParenthesizedPatternSyntax)base.VisitParenthesizedPattern(node)!; + Changed = true; + return rewritten.Pattern.WithTriviaFrom(rewritten); + } + + public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node) + { + var rewritten = (IdentifierNameSyntax)base.VisitIdentifierName(node)!; + if (rewritten.Identifier.ValueText is not ("Byte" or "Char" or "String") || + node.Parent is MemberAccessExpressionSyntax or QualifiedNameSyntax) + { + return rewritten; + } + + Changed = true; + return GetPredefinedType(rewritten.Identifier.ValueText).WithTriviaFrom(rewritten); + } + + public override SyntaxNode? VisitQualifiedName(QualifiedNameSyntax node) + { + var rewritten = (QualifiedNameSyntax)base.VisitQualifiedName(node)!; + if (rewritten.Left is not IdentifierNameSyntax { Identifier.ValueText: "System" } || + rewritten.Right.Identifier.ValueText is not ("Byte" or "Char" or "String") || + node.Parent is MemberAccessExpressionSyntax or QualifiedNameSyntax) + { + return rewritten; + } + + Changed = true; + return GetPredefinedType(rewritten.Right.Identifier.ValueText).WithTriviaFrom(rewritten); + } + + public override SyntaxNode? VisitCastExpression(CastExpressionSyntax node) + { + var rewritten = (CastExpressionSyntax)base.VisitCastExpression(node)!; + if (rewritten.Expression is LiteralExpressionSyntax literalExpression && + (literalExpression.IsKind(SyntaxKind.NullLiteralExpression) || + literalExpression.IsKind(SyntaxKind.DefaultLiteralExpression)) && + (rewritten.Parent is EqualsValueClauseSyntax || node.Parent is EqualsValueClauseSyntax)) + { + Changed = true; + return rewritten.Expression.WithTriviaFrom(rewritten); + } + + return rewritten; + } + + public override SyntaxNode? VisitEqualsValueClause(EqualsValueClauseSyntax node) + { + var rewritten = (EqualsValueClauseSyntax)base.VisitEqualsValueClause(node)!; + if (rewritten.Value is CastExpressionSyntax { Expression: LiteralExpressionSyntax literalExpression } castExpression && + (literalExpression.IsKind(SyntaxKind.NullLiteralExpression) || + literalExpression.IsKind(SyntaxKind.DefaultLiteralExpression))) + { + Changed = true; + return rewritten.WithValue(castExpression.Expression.WithTriviaFrom(castExpression)); + } + + return rewritten; + } + + private static PredefinedTypeSyntax GetPredefinedType(string typeName) => typeName switch + { + "Byte" => SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.ByteKeyword)), + "Char" => SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.CharKeyword)), + "String" => SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.StringKeyword)), + _ => throw new InvalidOperationException($"Unexpected predefined type name: {typeName}") + }; + } + private static ISymbol? GetSymbol(SemanticModel semanticModel, NameSyntax name) => semanticModel.GetSymbolInfo(name).Symbol ?? semanticModel.GetTypeInfo(name).Type; @@ -461,6 +548,35 @@ memberAccess.Parent is not InvocationExpressionSyntax invocation || return document.WithSyntaxRoot(rewrittenRoot); } + private static bool TryGetGenericMethodTypeArgumentsReplacement( + SemanticModel semanticModel, + GenericNameSyntax genericName, + out IdentifierNameSyntax replacement) + { + replacement = SyntaxFactory.IdentifierName(genericName.Identifier).WithTriviaFrom(genericName); + if (genericName.Parent is not MemberAccessExpressionSyntax memberAccess || + memberAccess.Name != genericName || + memberAccess.Parent is not InvocationExpressionSyntax invocation || + invocation.Expression != memberAccess) + { + return false; + } + + var originalSymbol = semanticModel.GetSymbolInfo(invocation).Symbol; + if (originalSymbol == null) + { + return false; + } + + var candidateInvocation = invocation.WithExpression(memberAccess.WithName(replacement)); + var speculativeSymbol = semanticModel.GetSpeculativeSymbolInfo( + invocation.SpanStart, + candidateInvocation, + SpeculativeBindingOption.BindAsExpression).Symbol; + return speculativeSymbol != null && + SymbolEqualityComparer.Default.Equals(originalSymbol, speculativeSymbol); + } + private static async Task ReduceThisQualificationAsync(Document document) { var root = await document.GetSyntaxRootAsync(); @@ -528,6 +644,58 @@ memberAccess.Parent is InvocationExpressionSyntax parentInvocation && return document.WithSyntaxRoot(rewrittenRoot); } + private static bool TryGetThisQualificationReplacement( + SemanticModel semanticModel, + MemberAccessExpressionSyntax memberAccess, + out ExpressionSyntax replacement) + { + replacement = memberAccess; + if (memberAccess.Expression is not ThisExpressionSyntax) + { + return false; + } + + var originalSymbol = semanticModel.GetSymbolInfo(memberAccess).Symbol; + var originalInvocationSymbol = memberAccess.Parent is InvocationExpressionSyntax invocation && invocation.Expression == memberAccess + ? semanticModel.GetSymbolInfo(invocation).Symbol + : null; + if (originalSymbol == null) + { + originalSymbol = originalInvocationSymbol; + if (originalSymbol == null) + { + return false; + } + } + + var candidate = memberAccess.Name.WithTriviaFrom(memberAccess); + var speculativeSymbol = semanticModel.GetSpeculativeSymbolInfo( + memberAccess.SpanStart, + candidate, + SpeculativeBindingOption.BindAsExpression).Symbol; + if (speculativeSymbol == null && + memberAccess.Parent is InvocationExpressionSyntax parentInvocation && + parentInvocation.Expression == memberAccess && + originalInvocationSymbol != null) + { + var candidateInvocation = parentInvocation.WithExpression(candidate); + speculativeSymbol = semanticModel.GetSpeculativeSymbolInfo( + parentInvocation.SpanStart, + candidateInvocation, + SpeculativeBindingOption.BindAsExpression).Symbol; + originalSymbol = originalInvocationSymbol; + } + + if (speculativeSymbol == null || + !SymbolEqualityComparer.Default.Equals(originalSymbol, speculativeSymbol)) + { + return false; + } + + replacement = candidate; + return true; + } + private static async Task ReducePredefinedTypeNamesAsync(Document document) { var root = await document.GetSyntaxRootAsync(); @@ -995,10 +1163,6 @@ private static bool IsInUnsupportedQualifiedNameContext(ExpressionSyntax express expression.Ancestors().Any(static ancestor => ancestor is CrefSyntax); - private static bool IsInMemberAccessExpressionChain(NameSyntax name) => - name.Ancestors().Any(static ancestor => ancestor is MemberAccessExpressionSyntax) && - !name.Ancestors().Any(static ancestor => ancestor is TypeSyntax); - private static bool ContainsSimplifierAnnotations(SyntaxNode root) => root.HasAnnotation(Simplifier.Annotation) || root.DescendantNodesAndTokens(descendIntoTrivia: true).Any(static nodeOrToken => diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs index 55c0970cbdc..d1aec2a8486 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs @@ -468,7 +468,7 @@ public class Container public Dictionary Map => (_map ??= new Dictionary()); - public void Invoke(object writer, WidgetHolder widget, object value, object result, Byte[] bytes, Char[] chars, Options options = (Options)null, string nameHint = (String)null) + public void Invoke(object writer, WidgetHolder widget, object value, object result, Byte[] bytes, Char[] chars, Options options = (Options)null, string nameHint = (String)null, global::System.String title = (global::System.String)null) { if (((value is ICollection collection) && (collection.Count == 0))) { @@ -533,6 +533,7 @@ public static void WriteObjectValue(this object writer, T value, Options opti Assert.That(generatedText, Does.Contain("char[] chars")); Assert.That(generatedText, Does.Contain("Options options = null")); Assert.That(generatedText, Does.Contain("string nameHint = null")); + Assert.That(generatedText, Does.Contain("string title = null")); Assert.That(generatedText, Does.Contain("case string s when s.Length > 0:")); Assert.That(generatedText, Does.Contain("_map = _map ?? GetMap() ?? new Dictionary();")); Assert.That(generatedText, Does.Contain("_length = _length + 1;")); From 8f6da6a99728897853c658748141afd71a6028c7 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 11 Jun 2026 14:50:14 +0000 Subject: [PATCH 013/111] Profile C# post-processing benchmarks --- .../perf/FullGenerationBenchmark.cs | 157 +++ .../perf/PostProcessingBenchmark.cs | 60 +- .../src/CSharpGen.cs | 162 ++- .../PostProcessing/GeneratedCodeWorkspace.cs | 1086 +---------------- ...ratedCodeWorkspacePostProcessingProfile.cs | 64 + .../src/PostProcessing/PostProcessor.cs | 186 ++- .../test/GeneratedCodeWorkspaceTests.cs | 530 -------- 7 files changed, 592 insertions(+), 1653 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/FullGenerationBenchmark.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspacePostProcessingProfile.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/FullGenerationBenchmark.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/FullGenerationBenchmark.cs new file mode 100644 index 00000000000..66553e08c38 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/FullGenerationBenchmark.cs @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; + +namespace Microsoft.TypeSpec.Generator.Perf +{ + public class FullGenerationBenchmark + { + private const string ProfileEnvironmentVariable = "POSTPROCESSING_BENCHMARK_PROFILE_STEPS"; + private const string ProfileOutputDirectoryEnvironmentVariable = "POSTPROCESSING_BENCHMARK_PROFILE_DIR"; + + private bool _profileSteps; + + [GlobalSetup] + public void GlobalSetup() + { + _profileSteps = string.Equals( + Environment.GetEnvironmentVariable(ProfileEnvironmentVariable), + "true", + StringComparison.OrdinalIgnoreCase); + } + + [Benchmark] + public async Task GenerateSampleTypeSpecProject() + { + var postProcessingProfile = _profileSteps ? new GeneratedCodeWorkspacePostProcessingProfile() : null; + var generationProfile = _profileSteps ? new GeneratedCodeWorkspacePostProcessingProfile() : null; + GeneratedCodeWorkspace.PostProcessingProfile = postProcessingProfile; + CSharpGen.GenerationProfile = generationProfile; + + var benchmarkDirectory = CreateBenchmarkInputDirectory(); + var stopwatch = Stopwatch.StartNew(); + try + { + CodeModelGenerator.Instance = new BenchmarkCodeModelGenerator(benchmarkDirectory); + CodeModelGenerator.Instance.Configure(); + + var csharpGen = new CSharpGen(); + await csharpGen.ExecuteAsync(); + + return Directory.GetFiles(benchmarkDirectory, "*", SearchOption.AllDirectories) + .Where(static path => !path.EndsWith("tspCodeModel.json", StringComparison.Ordinal) && + !path.EndsWith("Configuration.json", StringComparison.Ordinal)) + .Sum(static path => (int)new FileInfo(path).Length); + } + finally + { + stopwatch.Stop(); + if (generationProfile != null) + { + WriteProfile( + generationProfile, + $"full-generation-profile-{DateTime.UtcNow:yyyyMMddHHmmssfff}.csv", + $"Full generation invocation elapsed ms: {stopwatch.Elapsed.TotalMilliseconds:F3}{Environment.NewLine}" + + $"Input directory: {benchmarkDirectory}{Environment.NewLine}"); + } + + if (postProcessingProfile != null) + { + WriteProfile( + postProcessingProfile, + $"full-generation-post-processing-profile-{DateTime.UtcNow:yyyyMMddHHmmssfff}.csv", + $"Full generation post-processing profile{Environment.NewLine}" + + $"Input directory: {benchmarkDirectory}{Environment.NewLine}"); + } + + CSharpGen.GenerationProfile = null; + GeneratedCodeWorkspace.PostProcessingProfile = null; + TryDeleteDirectory(benchmarkDirectory); + } + } + + private static void WriteProfile(GeneratedCodeWorkspacePostProcessingProfile profile, string fileName, string header) + { + var profileDirectory = GetProfileOutputDirectory(); + Directory.CreateDirectory(profileDirectory); + File.WriteAllText(Path.Combine(profileDirectory, fileName), header + profile.GetSummary()); + } + + private static string CreateBenchmarkInputDirectory() + { + var sourceDirectory = FindFullGenerationInputDirectory(); + var benchmarkDirectory = Path.Combine(Path.GetTempPath(), "typespec-full-generation-benchmark", Guid.NewGuid().ToString("N")); + CopyDirectory(sourceDirectory, benchmarkDirectory); + return benchmarkDirectory; + } + + private static string FindFullGenerationInputDirectory() + { + const string relativePath = "packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec"; + + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory != null) + { + var inputDirectory = Path.Combine(directory.FullName, relativePath); + if (File.Exists(Path.Combine(inputDirectory, "tspCodeModel.json")) && + File.Exists(Path.Combine(inputDirectory, "Configuration.json"))) + { + return inputDirectory; + } + + directory = directory.Parent; + } + + throw new DirectoryNotFoundException($"Could not find '{relativePath}' from '{AppContext.BaseDirectory}'."); + } + + private static void CopyDirectory(string sourceDirectory, string destinationDirectory) + { + Directory.CreateDirectory(destinationDirectory); + foreach (var sourceFile in Directory.GetFiles(sourceDirectory, "*", SearchOption.AllDirectories)) + { + var relativePath = Path.GetRelativePath(sourceDirectory, sourceFile); + var destinationFile = Path.Combine(destinationDirectory, relativePath); + Directory.CreateDirectory(Path.GetDirectoryName(destinationFile)!); + File.Copy(sourceFile, destinationFile, overwrite: true); + } + } + + private static void TryDeleteDirectory(string directory) + { + try + { + if (Directory.Exists(directory)) + { + Directory.Delete(directory, recursive: true); + } + } + catch + { + // Best-effort cleanup for benchmark temp output. + } + } + + private static string GetProfileOutputDirectory() + { + var configuredPath = Environment.GetEnvironmentVariable(ProfileOutputDirectoryEnvironmentVariable); + return string.IsNullOrWhiteSpace(configuredPath) + ? Path.Combine(Path.GetTempPath(), "typespec-post-processing-profiles") + : Path.GetFullPath(configuredPath); + } + + private sealed class BenchmarkCodeModelGenerator : CodeModelGenerator + { + public BenchmarkCodeModelGenerator(string outputPath) + : base(new GeneratorContext(Configuration.Load(outputPath))) + { + } + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/PostProcessingBenchmark.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/PostProcessingBenchmark.cs index f96a0d5053b..24b5394e2cd 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/PostProcessingBenchmark.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/PostProcessingBenchmark.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Text.RegularExpressions; @@ -16,6 +17,8 @@ namespace Microsoft.TypeSpec.Generator.Perf public class PostProcessingBenchmark { private const string GeneratedDirectoryEnvironmentVariable = "POSTPROCESSING_BENCHMARK_GENERATED_DIR"; + private const string ProfileEnvironmentVariable = "POSTPROCESSING_BENCHMARK_PROFILE_STEPS"; + private const string ProfileOutputDirectoryEnvironmentVariable = "POSTPROCESSING_BENCHMARK_PROFILE_DIR"; private static readonly Regex NamespaceDeclarationRegex = new( @"\bnamespace\s+([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)", RegexOptions.Compiled); @@ -24,6 +27,7 @@ public class PostProcessingBenchmark public int CorpusMultiplier { get; set; } private (string Name, string Content)[] _generatedFiles = []; + private bool _profileSteps; [GlobalSetup] public void GlobalSetup() @@ -42,26 +46,68 @@ public void GlobalSetup() var declaredNamespaces = GetDeclaredNamespaces(sourceFiles); _generatedFiles = BuildCorpus(generatedDirectory, sourceFiles, declaredNamespaces); + _profileSteps = string.Equals( + Environment.GetEnvironmentVariable(ProfileEnvironmentVariable), + "true", + StringComparison.OrdinalIgnoreCase); } [Benchmark] public async Task ProcessSampleTypeSpecGeneratedFiles() { + var profile = _profileSteps ? new GeneratedCodeWorkspacePostProcessingProfile() : null; + GeneratedCodeWorkspace.PostProcessingProfile = profile; + + var stopwatch = Stopwatch.StartNew(); GeneratedCodeWorkspace.Initialize(); var workspace = await GeneratedCodeWorkspace.Create(isCustomCodeProject: false); - foreach (var file in _generatedFiles) + try { - await workspace.AddGeneratedFile(new CodeFile(file.Content, file.Name)); - } + foreach (var file in _generatedFiles) + { + await workspace.AddGeneratedFile(new CodeFile(file.Content, file.Name)); + } - var totalLength = 0; - await foreach (var file in workspace.GetGeneratedFilesAsync()) + var totalLength = 0; + await foreach (var file in workspace.GetGeneratedFilesAsync()) + { + totalLength += file.Text.Length; + } + + return totalLength; + } + finally { - totalLength += file.Text.Length; + stopwatch.Stop(); + if (profile != null) + { + WriteProfile( + profile, + $"post-processing-profile-RoslynSimplifier-x{CorpusMultiplier}-{DateTime.UtcNow:yyyyMMddHHmmssfff}.csv", + $"Reduction strategy: RoslynSimplifier{Environment.NewLine}" + + $"Corpus multiplier: {CorpusMultiplier}{Environment.NewLine}" + + $"File count: {_generatedFiles.Length}{Environment.NewLine}" + + $"Benchmark invocation elapsed ms: {stopwatch.Elapsed.TotalMilliseconds:F3}{Environment.NewLine}"); + } + + GeneratedCodeWorkspace.PostProcessingProfile = null; } + } - return totalLength; + private static void WriteProfile(GeneratedCodeWorkspacePostProcessingProfile profile, string fileName, string header) + { + var profileDirectory = GetProfileOutputDirectory(); + Directory.CreateDirectory(profileDirectory); + File.WriteAllText(Path.Combine(profileDirectory, fileName), header + profile.GetSummary()); + } + + private static string GetProfileOutputDirectory() + { + var configuredPath = Environment.GetEnvironmentVariable(ProfileOutputDirectoryEnvironmentVariable); + return string.IsNullOrWhiteSpace(configuredPath) + ? Path.Combine(Path.GetTempPath(), "typespec-post-processing-profiles") + : Path.GetFullPath(configuredPath); } private (string Name, string Content)[] BuildCorpus(string generatedDirectory, string[] sourceFiles, IReadOnlyList declaredNamespaces) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index 9948fcff594..238b16b9766 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -20,6 +21,8 @@ internal sealed class CSharpGen private static readonly string[] _filesToKeep = [ConfigurationFileName, CodeModelFileName]; + internal static GeneratedCodeWorkspacePostProcessingProfile? GenerationProfile { get; set; } + /// /// Executes the generator task with the instance. /// @@ -33,19 +36,21 @@ public async Task ExecuteAsync() // Resolve PackageReference items from the .csproj so custom code referencing // external NuGet types (e.g., Azure.Storage.Common) compiles correctly. - await GeneratedCodeWorkspace.AddPackageReferencesFromProject(); + await MeasureGenerationStepAsync("Generation.AddPackageReferencesFromProject", GeneratedCodeWorkspace.AddPackageReferencesFromProject); // Pre-walk the input library and resolve any external types that point at NuGet packages. // This populates ExternalTypeReferenceResolver's cache and registers each resolved assembly // as an additional metadata reference *before* the generated/custom code workspaces are // constructed, so their cached Roslyn projects pick the references up. - await ExternalTypeReferenceResolver.ResolveAllAsync(); + await MeasureGenerationStepAsync("Generation.ResolveExternalTypeReferences", ExternalTypeReferenceResolver.ResolveAllAsync); // Initialize the workspace project AFTER all metadata references have been added so the // eagerly-cached project sees them. GeneratedCodeWorkspace.Initialize(); - GeneratedCodeWorkspace customCodeWorkspace = await GeneratedCodeWorkspace.Create(isCustomCodeProject: true); + GeneratedCodeWorkspace customCodeWorkspace = await MeasureGenerationStepAsync( + "Generation.CreateCustomCodeWorkspace", + () => GeneratedCodeWorkspace.Create(isCustomCodeProject: true)); // The generated attributes need to be added into the workspace before loading the custom code. Otherwise, // Roslyn doesn't load the attributes completely and we are unable to get the attribute arguments. @@ -55,88 +60,167 @@ public async Task ExecuteAsync() generateAttributeTasks.Add(customCodeWorkspace.AddInMemoryFile(attributeProvider)); } - await Task.WhenAll(generateAttributeTasks); + await MeasureGenerationStepAsync("Generation.AddCustomizationAttributeProviders", () => Task.WhenAll(generateAttributeTasks)); - CodeModelGenerator.Instance.SourceInputModel = new SourceInputModel( - await customCodeWorkspace.GetCompilationAsync(), - await GeneratedCodeWorkspace.LoadBaselineContract()); + CodeModelGenerator.Instance.SourceInputModel = await MeasureGenerationStepAsync( + "Generation.CreateSourceInputModel", + async () => new SourceInputModel( + await customCodeWorkspace.GetCompilationAsync(), + await GeneratedCodeWorkspace.LoadBaselineContract())); - GeneratedCodeWorkspace generatedCodeWorkspace = await GeneratedCodeWorkspace.Create(isCustomCodeProject: false); + GeneratedCodeWorkspace generatedCodeWorkspace = await MeasureGenerationStepAsync( + "Generation.CreateGeneratedCodeWorkspace", + () => GeneratedCodeWorkspace.Create(isCustomCodeProject: false)); - var output = CodeModelGenerator.Instance.OutputLibrary; + var output = MeasureGenerationStep("Generation.GetOutputLibrary", () => CodeModelGenerator.Instance.OutputLibrary); Directory.CreateDirectory(Path.Combine(generatedSourceOutputPath, "Models")); List generateFilesTasks = new(); // Build all TypeProviders - foreach (var type in output.TypeProviders) + MeasureGenerationStep("Generation.BuildTypeProviders", () => { - type.EnsureBuilt(); - } + foreach (var type in output.TypeProviders) + { + type.EnsureBuilt(); + } + }); LoggingHelpers.LogElapsedTime("All generated type providers built"); // visit the entire library before generating files - foreach (var visitor in CodeModelGenerator.Instance.Visitors) + MeasureGenerationStep("Generation.ApplyVisitors", () => { - visitor.VisitLibrary(output); - } + foreach (var visitor in CodeModelGenerator.Instance.Visitors) + { + visitor.VisitLibrary(output); + } + }); - FilterAllCustomizedMembers(output); + MeasureGenerationStep("Generation.FilterCustomizedMembers", () => FilterAllCustomizedMembers(output)); LoggingHelpers.LogElapsedTime("All visitors have been applied"); - foreach (var outputType in output.TypeProviders) + MeasureGenerationStep("Generation.WriteTypeProviders", () => { - // Ensure back-compatibility processing is done after all visitors have run - outputType.ProcessTypeForBackCompatibility(); - - var writer = CodeModelGenerator.Instance.GetWriter(outputType); - generateFilesTasks.Add(generatedCodeWorkspace.AddGeneratedFile(writer.Write())); - - foreach (var serialization in outputType.SerializationProviders) + foreach (var outputType in output.TypeProviders) { - writer = CodeModelGenerator.Instance.GetWriter(serialization); + // Ensure back-compatibility processing is done after all visitors have run + outputType.ProcessTypeForBackCompatibility(); + + var writer = CodeModelGenerator.Instance.GetWriter(outputType); generateFilesTasks.Add(generatedCodeWorkspace.AddGeneratedFile(writer.Write())); + + foreach (var serialization in outputType.SerializationProviders) + { + writer = CodeModelGenerator.Instance.GetWriter(serialization); + generateFilesTasks.Add(generatedCodeWorkspace.AddGeneratedFile(writer.Write())); + } } - } + }); // Add all the generated files to the workspace - await Task.WhenAll(generateFilesTasks); + await MeasureGenerationStepAsync("Generation.AddGeneratedFilesToWorkspace", () => Task.WhenAll(generateFilesTasks)); LoggingHelpers.LogElapsedTime("All generated types have been written into memory"); // Delete any old generated files - DeleteDirectory(generatedSourceOutputPath, _filesToKeep); + MeasureGenerationStep("Generation.DeleteOldGeneratedFiles", () => DeleteDirectory(generatedSourceOutputPath, _filesToKeep)); LoggingHelpers.LogElapsedTime("All old generated files have been deleted"); - await generatedCodeWorkspace.PostProcessAsync(); + await MeasureGenerationStepAsync("Generation.PostProcessAsync", generatedCodeWorkspace.PostProcessAsync); // Write the generated files to the output directory - await foreach (var file in generatedCodeWorkspace.GetGeneratedFilesAsync()) + await MeasureGenerationStepAsync("Generation.WriteGeneratedFilesToDisk", async () => { - if (string.IsNullOrEmpty(file.Text)) + await foreach (var file in generatedCodeWorkspace.GetGeneratedFilesAsync()) { - continue; + if (string.IsNullOrEmpty(file.Text)) + { + continue; + } + var filename = Path.Combine(outputPath, file.Name); + CodeModelGenerator.Instance.Emitter.Info($"Writing {Path.GetFullPath(filename)}"); + Directory.CreateDirectory(Path.GetDirectoryName(filename)!); + await File.WriteAllTextAsync(filename, file.Text); } - var filename = Path.Combine(outputPath, file.Name); - CodeModelGenerator.Instance.Emitter.Info($"Writing {Path.GetFullPath(filename)}"); - Directory.CreateDirectory(Path.GetDirectoryName(filename)!); - await File.WriteAllTextAsync(filename, file.Text); - } + }); // Write additional output files (e.g. configuration schemas, .targets files) - await CodeModelGenerator.Instance.WriteAdditionalFiles(outputPath); + await MeasureGenerationStepAsync("Generation.WriteAdditionalFiles", () => CodeModelGenerator.Instance.WriteAdditionalFiles(outputPath)); // Write project scaffolding files (after additional files so schema existence can be checked) if (CodeModelGenerator.Instance.IsNewProject) { - await CodeModelGenerator.Instance.TypeFactory.CreateNewProjectScaffolding().Execute(); + await MeasureGenerationStepAsync( + "Generation.WriteProjectScaffolding", + () => CodeModelGenerator.Instance.TypeFactory.CreateNewProjectScaffolding().Execute()); } LoggingHelpers.LogElapsedTime("All files have been written to disk"); } + private static void MeasureGenerationStep(string stepName, Action action) + { + MeasureGenerationStep( + stepName, + () => + { + action(); + return 0; + }); + } + + private static T MeasureGenerationStep(string stepName, Func action) + { + var profile = GenerationProfile; + if (profile == null) + { + return action(); + } + + var allocatedBytes = GC.GetTotalAllocatedBytes(precise: false); + var stopwatch = Stopwatch.StartNew(); + try + { + return action(); + } + finally + { + stopwatch.Stop(); + profile.Add(stepName, stopwatch.Elapsed, GC.GetTotalAllocatedBytes(precise: false) - allocatedBytes); + } + } + + private static Task MeasureGenerationStepAsync(string stepName, Func action) => MeasureGenerationStepAsync( + stepName, + async () => + { + await action(); + return 0; + }); + + private static async Task MeasureGenerationStepAsync(string stepName, Func> action) + { + var profile = GenerationProfile; + if (profile == null) + { + return await action(); + } + + var allocatedBytes = GC.GetTotalAllocatedBytes(precise: false); + var stopwatch = Stopwatch.StartNew(); + try + { + return await action(); + } + finally + { + stopwatch.Stop(); + profile.Add(stepName, stopwatch.Elapsed, GC.GetTotalAllocatedBytes(precise: false) - allocatedBytes); + } + } + internal static void FilterAllCustomizedMembers(OutputLibrary output) { foreach (var typeProvider in output.TypeProviders) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index 38ac362d741..bf4b08b31b5 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -6,7 +6,6 @@ using System.Diagnostics; using System.IO; using System.Linq; -using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.Build.Construction; @@ -16,7 +15,6 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Simplification; -using Microsoft.CodeAnalysis.Text; using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.Utilities; @@ -40,6 +38,8 @@ internal class GeneratedCodeWorkspace private static readonly Lazy _metadataReferenceResolver = new(() => new WorkspaceMetadataReferenceResolver()); private static Task? _cachedProject; + internal static GeneratedCodeWorkspacePostProcessingProfile? PostProcessingProfile { get; set; } + private static readonly string[] _generatedFolders = [GeneratedFolder]; private static readonly string[] _sharedFolders = [SharedFolder]; @@ -105,7 +105,7 @@ internal static SyntaxTree GetTree(TypeProvider provider) public async IAsyncEnumerable<(string Name, string Text)> GetGeneratedFilesAsync() { - List> documents = new List>(); + List docs = new List(); var memberRemover = new MemberRemoverRewriter(); foreach (Document document in _project.Documents) { @@ -114,9 +114,12 @@ internal static SyntaxTree GetTree(TypeProvider provider) continue; } - documents.Add(ProcessDocument(document, memberRemover)); + docs.Add(document); } - var docs = await Task.WhenAll(documents); + + docs = PostProcessingProfile == null + ? [.. await Task.WhenAll(docs.Select(document => ProcessDocument(document, memberRemover)))] + : await ProcessDocumentsSequentiallyAsync(docs, memberRemover); LoggingHelpers.LogElapsedTime("Roslyn post processing complete"); @@ -132,1073 +135,100 @@ internal static SyntaxTree GetTree(TypeProvider provider) } } - private async Task ProcessDocument(Document document, MemberRemoverRewriter memberRemover) + private async Task> ProcessDocumentsSequentiallyAsync(List documents, MemberRemoverRewriter memberRemover) { - var root = await document.GetSyntaxRootAsync(); - var semanticModel = await document.GetSemanticModelAsync(); - - if (semanticModel == null || root == null) - { - return document; - } - - root = memberRemover.Visit(root); - - foreach (var rewriter in CodeModelGenerator.Instance.Rewriters) + List processedDocuments = new(documents.Count); + foreach (var document in documents) { - rewriter.SemanticModel = semanticModel; - root = rewriter.Visit(root); + processedDocuments.Add(await ProcessDocument(document, memberRemover)); } - document = document.WithSyntaxRoot(root); - for (int i = 0; i < 8; i++) - { - var reducedDocument = await ReduceQualifiedNamesAsync(document); - if (ReferenceEquals(reducedDocument, document)) - { - break; - } - - document = reducedDocument; - } - - document = await ReduceSemanticOnlyAsync(document); - document = await ReduceSyntaxOnlyAsync(document); - document = await ReduceDocumentationQualifiedNamesAsync(document); - - // Reformat if any custom rewriters have been applied - if (CodeModelGenerator.Instance.Rewriters.Count > 0) - { - document = await Formatter.FormatAsync(document); - } - return document; + return processedDocuments; } - private static async Task ReduceQualifiedNamesAsync(Document document) - { - var root = await document.GetSyntaxRootAsync(); - var semanticModel = await document.GetSemanticModelAsync(); - if (root == null || semanticModel == null) - { - return document; - } - - var safeNameReplacements = new Dictionary(); - foreach (var name in root.DescendantNodes().OfType()) - { - if (name is not QualifiedNameSyntax and not AliasQualifiedNameSyntax || - name.Parent is QualifiedNameSyntax || - IsInUnsupportedQualifiedNameContext(name)) - { - continue; - } - - var originalSymbol = GetSymbol(semanticModel, name); - if (originalSymbol == null) - { - continue; - } - - if (TryGetNameReplacement(semanticModel, name, originalSymbol, out var replacement)) - { - safeNameReplacements.Add(name, replacement); - } - } - - foreach (var attribute in root.DescendantNodes().OfType()) - { - if (TryGetAttributeNameReplacement(semanticModel, attribute, out var replacement)) - { - safeNameReplacements[attribute.Name] = replacement; - } - } - - var safeMemberAccessReplacements = new Dictionary(); - foreach (var memberAccess in root.DescendantNodes().OfType()) - { - if (IsInUnsupportedQualifiedNameContext(memberAccess)) - { - continue; - } - - if (TryGetMemberAccessReplacement(semanticModel, memberAccess, out var replacement)) - { - safeMemberAccessReplacements.Add(memberAccess, replacement); - } - } - - if (safeNameReplacements.Count == 0 && safeMemberAccessReplacements.Count == 0) - { - return document; - } - - var rewrittenRoot = root.ReplaceNodes( - safeNameReplacements.Keys - .Concat(safeMemberAccessReplacements.Keys), - (original, rewritten) => original switch - { - NameSyntax name => safeNameReplacements[name].WithTriviaFrom(rewritten), - MemberAccessExpressionSyntax memberAccess => safeMemberAccessReplacements[memberAccess].WithTriviaFrom(rewritten), - _ => rewritten - }); - return document.WithSyntaxRoot(rewrittenRoot); - } - - private static async Task ReduceSemanticOnlyAsync(Document document) - { - var root = await document.GetSyntaxRootAsync(); - var semanticModel = await document.GetSemanticModelAsync(); - if (root == null || semanticModel == null) - { - return document; - } - - var memberAccessReplacements = new Dictionary(); - foreach (var memberAccess in root.DescendantNodes().OfType()) - { - if (!IsInUnsupportedQualifiedNameContext(memberAccess) && - TryGetThisQualificationReplacement(semanticModel, memberAccess, out var replacement)) - { - memberAccessReplacements.Add(memberAccess, replacement); - } - } - - var genericNameReplacements = new Dictionary(); - foreach (var genericName in root.DescendantNodes().OfType()) - { - if (TryGetGenericMethodTypeArgumentsReplacement(semanticModel, genericName, out var replacement)) - { - genericNameReplacements.Add(genericName, replacement); - } - } - - if (memberAccessReplacements.Count == 0 && genericNameReplacements.Count == 0) - { - return document; - } - - var rewrittenRoot = root.ReplaceNodes( - memberAccessReplacements.Keys.Concat(genericNameReplacements.Keys), - (original, rewritten) => original switch - { - GenericNameSyntax genericName => genericNameReplacements[genericName].WithTriviaFrom(rewritten), - MemberAccessExpressionSyntax memberAccess => memberAccessReplacements[memberAccess].WithTriviaFrom(rewritten), - _ => rewritten - }); - return document.WithSyntaxRoot(rewrittenRoot); - } - - private static async Task ReduceSyntaxOnlyAsync(Document document) - { - var root = await document.GetSyntaxRootAsync(); - if (root == null) - { - return document; - } - - var rewriter = new SyntaxOnlyReducer(); - var rewrittenRoot = rewriter.Visit(root); - return rewriter.Changed && rewrittenRoot != null - ? document.WithSyntaxRoot(rewrittenRoot) - : document; - } - - private sealed class SyntaxOnlyReducer : CSharpSyntaxRewriter - { - public bool Changed { get; private set; } - - public override SyntaxNode? VisitParenthesizedExpression(ParenthesizedExpressionSyntax node) - { - var rewritten = (ParenthesizedExpressionSyntax)base.VisitParenthesizedExpression(node)!; - if (CanRemoveParentheses(node)) - { - Changed = true; - return rewritten.Expression.WithTriviaFrom(rewritten); - } - - return rewritten; - } - - public override SyntaxNode? VisitParenthesizedPattern(ParenthesizedPatternSyntax node) - { - var rewritten = (ParenthesizedPatternSyntax)base.VisitParenthesizedPattern(node)!; - Changed = true; - return rewritten.Pattern.WithTriviaFrom(rewritten); - } - - public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node) - { - var rewritten = (IdentifierNameSyntax)base.VisitIdentifierName(node)!; - if (rewritten.Identifier.ValueText is not ("Byte" or "Char" or "String") || - node.Parent is MemberAccessExpressionSyntax or QualifiedNameSyntax) - { - return rewritten; - } - - Changed = true; - return GetPredefinedType(rewritten.Identifier.ValueText).WithTriviaFrom(rewritten); - } - - public override SyntaxNode? VisitQualifiedName(QualifiedNameSyntax node) - { - var rewritten = (QualifiedNameSyntax)base.VisitQualifiedName(node)!; - if (rewritten.Left is not IdentifierNameSyntax { Identifier.ValueText: "System" } || - rewritten.Right.Identifier.ValueText is not ("Byte" or "Char" or "String") || - node.Parent is MemberAccessExpressionSyntax or QualifiedNameSyntax) - { - return rewritten; - } - - Changed = true; - return GetPredefinedType(rewritten.Right.Identifier.ValueText).WithTriviaFrom(rewritten); - } - - public override SyntaxNode? VisitCastExpression(CastExpressionSyntax node) - { - var rewritten = (CastExpressionSyntax)base.VisitCastExpression(node)!; - if (rewritten.Expression is LiteralExpressionSyntax literalExpression && - (literalExpression.IsKind(SyntaxKind.NullLiteralExpression) || - literalExpression.IsKind(SyntaxKind.DefaultLiteralExpression)) && - (rewritten.Parent is EqualsValueClauseSyntax || node.Parent is EqualsValueClauseSyntax)) - { - Changed = true; - return rewritten.Expression.WithTriviaFrom(rewritten); - } - - return rewritten; - } - - public override SyntaxNode? VisitEqualsValueClause(EqualsValueClauseSyntax node) - { - var rewritten = (EqualsValueClauseSyntax)base.VisitEqualsValueClause(node)!; - if (rewritten.Value is CastExpressionSyntax { Expression: LiteralExpressionSyntax literalExpression } castExpression && - (literalExpression.IsKind(SyntaxKind.NullLiteralExpression) || - literalExpression.IsKind(SyntaxKind.DefaultLiteralExpression))) - { - Changed = true; - return rewritten.WithValue(castExpression.Expression.WithTriviaFrom(castExpression)); - } - - return rewritten; - } - - private static PredefinedTypeSyntax GetPredefinedType(string typeName) => typeName switch - { - "Byte" => SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.ByteKeyword)), - "Char" => SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.CharKeyword)), - "String" => SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.StringKeyword)), - _ => throw new InvalidOperationException($"Unexpected predefined type name: {typeName}") - }; - } - - private static ISymbol? GetSymbol(SemanticModel semanticModel, NameSyntax name) => - semanticModel.GetSymbolInfo(name).Symbol ?? - semanticModel.GetTypeInfo(name).Type; - - private static async Task ReduceParenthesesAsync(Document document) - { - var root = await document.GetSyntaxRootAsync(); - if (root == null) - { - return document; - } - - var expressions = root.DescendantNodes() - .OfType() - .Where(CanRemoveParentheses) - .ToList(); - var patterns = root.DescendantNodes() - .OfType() - .ToList(); - if (expressions.Count == 0 && patterns.Count == 0) - { - return document; - } - - var rewrittenRoot = root - .ReplaceNodes( - expressions, - static (_, rewritten) => rewritten.Expression.WithTriviaFrom(rewritten)) - .ReplaceNodes( - patterns, - static (_, rewritten) => rewritten.Pattern.WithTriviaFrom(rewritten)); - return document.WithSyntaxRoot(rewrittenRoot); - } - - private static bool CanRemoveParentheses(ParenthesizedExpressionSyntax node) => node.Parent switch - { - ParenthesizedExpressionSyntax => true, - IfStatementSyntax ifStatement when ifStatement.Condition == node => true, - WhileStatementSyntax whileStatement when whileStatement.Condition == node => true, - DoStatementSyntax doStatement when doStatement.Condition == node => true, - ForStatementSyntax forStatement when forStatement.Condition == node => true, - SwitchStatementSyntax switchStatement when switchStatement.Expression == node => true, - ReturnStatementSyntax => true, - ArrowExpressionClauseSyntax => true, - EqualsValueClauseSyntax => true, - AssignmentExpressionSyntax assignment when assignment.Right == node => true, - ExpressionStatementSyntax when node.Expression is AssignmentExpressionSyntax => true, - ArgumentSyntax => node.Expression is not AssignmentExpressionSyntax and not ConditionalExpressionSyntax, - BracketedArgumentListSyntax => true, - ConditionalExpressionSyntax parent when parent.Condition == node => true, - WhenClauseSyntax whenClause when whenClause.Condition == node => true, - SwitchExpressionArmSyntax switchArm when switchArm.WhenClause?.Condition == node => true, - BinaryExpressionSyntax parent when parent.Left == node => - GetExpressionPrecedence(node.Expression) >= GetExpressionPrecedence(parent), - BinaryExpressionSyntax parent when parent.Right == node => - GetExpressionPrecedence(node.Expression) > GetExpressionPrecedence(parent) || - parent.IsKind(SyntaxKind.CoalesceExpression) && node.Expression.IsKind(SyntaxKind.CoalesceExpression), - PrefixUnaryExpressionSyntax => node.Expression is CastExpressionSyntax, - _ => false - }; - - private static int GetExpressionPrecedence(ExpressionSyntax expression) => expression.Kind() switch - { - SyntaxKind.SimpleMemberAccessExpression or - SyntaxKind.ElementAccessExpression or - SyntaxKind.InvocationExpression => 15, - SyntaxKind.CastExpression => 14, - SyntaxKind.UnaryMinusExpression or - SyntaxKind.UnaryPlusExpression or - SyntaxKind.LogicalNotExpression or - SyntaxKind.BitwiseNotExpression => 13, - SyntaxKind.MultiplyExpression or - SyntaxKind.DivideExpression or - SyntaxKind.ModuloExpression => 12, - SyntaxKind.AddExpression or - SyntaxKind.SubtractExpression => 11, - SyntaxKind.LeftShiftExpression or - SyntaxKind.RightShiftExpression => 10, - SyntaxKind.LessThanExpression or - SyntaxKind.LessThanOrEqualExpression or - SyntaxKind.GreaterThanExpression or - SyntaxKind.GreaterThanOrEqualExpression or - SyntaxKind.IsExpression or - SyntaxKind.AsExpression => 9, - SyntaxKind.EqualsExpression or - SyntaxKind.NotEqualsExpression => 8, - SyntaxKind.BitwiseAndExpression => 7, - SyntaxKind.ExclusiveOrExpression => 6, - SyntaxKind.BitwiseOrExpression => 5, - SyntaxKind.LogicalAndExpression => 4, - SyntaxKind.LogicalOrExpression => 3, - SyntaxKind.CoalesceExpression => 2, - SyntaxKind.SimpleAssignmentExpression or - SyntaxKind.AddAssignmentExpression or - SyntaxKind.SubtractAssignmentExpression or - SyntaxKind.MultiplyAssignmentExpression or - SyntaxKind.DivideAssignmentExpression or - SyntaxKind.ModuloAssignmentExpression or - SyntaxKind.AndAssignmentExpression or - SyntaxKind.ExclusiveOrAssignmentExpression or - SyntaxKind.OrAssignmentExpression or - SyntaxKind.LeftShiftAssignmentExpression or - SyntaxKind.RightShiftAssignmentExpression or - SyntaxKind.CoalesceAssignmentExpression => 1, - _ => 16 - }; - - private static async Task ReduceGenericMethodTypeArgumentsAsync(Document document) - { - var root = await document.GetSyntaxRootAsync(); - var semanticModel = await document.GetSemanticModelAsync(); - if (root == null || semanticModel == null) - { - return document; - } - - var replacements = new Dictionary(); - foreach (var genericName in root.DescendantNodes().OfType()) - { - if (genericName.Parent is not MemberAccessExpressionSyntax memberAccess || - memberAccess.Name != genericName || - memberAccess.Parent is not InvocationExpressionSyntax invocation || - invocation.Expression != memberAccess) - { - continue; - } - - var originalSymbol = semanticModel.GetSymbolInfo(invocation).Symbol; - if (originalSymbol == null) - { - continue; - } - - var candidateName = SyntaxFactory.IdentifierName(genericName.Identifier).WithTriviaFrom(genericName); - var candidateInvocation = invocation.WithExpression(memberAccess.WithName(candidateName)); - var speculativeSymbol = semanticModel.GetSpeculativeSymbolInfo( - invocation.SpanStart, - candidateInvocation, - SpeculativeBindingOption.BindAsExpression).Symbol; - if (speculativeSymbol != null && - SymbolEqualityComparer.Default.Equals(originalSymbol, speculativeSymbol)) - { - replacements.Add(genericName, candidateName); - } - } - - if (replacements.Count == 0) - { - return document; - } - - var rewrittenRoot = root.ReplaceNodes( - replacements.Keys, - (original, rewritten) => replacements[original].WithTriviaFrom(rewritten)); - return document.WithSyntaxRoot(rewrittenRoot); - } - - private static bool TryGetGenericMethodTypeArgumentsReplacement( - SemanticModel semanticModel, - GenericNameSyntax genericName, - out IdentifierNameSyntax replacement) - { - replacement = SyntaxFactory.IdentifierName(genericName.Identifier).WithTriviaFrom(genericName); - if (genericName.Parent is not MemberAccessExpressionSyntax memberAccess || - memberAccess.Name != genericName || - memberAccess.Parent is not InvocationExpressionSyntax invocation || - invocation.Expression != memberAccess) - { - return false; - } - - var originalSymbol = semanticModel.GetSymbolInfo(invocation).Symbol; - if (originalSymbol == null) - { - return false; - } - - var candidateInvocation = invocation.WithExpression(memberAccess.WithName(replacement)); - var speculativeSymbol = semanticModel.GetSpeculativeSymbolInfo( - invocation.SpanStart, - candidateInvocation, - SpeculativeBindingOption.BindAsExpression).Symbol; - return speculativeSymbol != null && - SymbolEqualityComparer.Default.Equals(originalSymbol, speculativeSymbol); - } - - private static async Task ReduceThisQualificationAsync(Document document) + private async Task ProcessDocument(Document document, MemberRemoverRewriter memberRemover) { - var root = await document.GetSyntaxRootAsync(); - var semanticModel = await document.GetSemanticModelAsync(); - if (root == null || semanticModel == null) + var totalStopwatch = PostProcessingProfile == null ? null : Stopwatch.StartNew(); + try { - return document; - } + var root = await MeasurePostProcessingStepAsync("GetSyntaxRootAsync", () => document.GetSyntaxRootAsync()); + var semanticModel = await MeasurePostProcessingStepAsync("GetSemanticModelAsync", () => document.GetSemanticModelAsync()); - var replacements = new Dictionary(); - foreach (var memberAccess in root.DescendantNodes().OfType()) - { - if (memberAccess.Expression is not ThisExpressionSyntax || - IsInUnsupportedQualifiedNameContext(memberAccess)) + if (semanticModel == null || root == null) { - continue; + return document; } - var originalSymbol = semanticModel.GetSymbolInfo(memberAccess).Symbol; - var originalInvocationSymbol = memberAccess.Parent is InvocationExpressionSyntax invocation && invocation.Expression == memberAccess - ? semanticModel.GetSymbolInfo(invocation).Symbol - : null; - if (originalSymbol == null) - { - originalSymbol = originalInvocationSymbol; - if (originalSymbol == null) - { - continue; - } - } + root = MeasurePostProcessingStep("MemberRemoverRewriter", () => memberRemover.Visit(root)); - var candidate = memberAccess.Name.WithTriviaFrom(memberAccess); - var speculativeSymbol = semanticModel.GetSpeculativeSymbolInfo( - memberAccess.SpanStart, - candidate, - SpeculativeBindingOption.BindAsExpression).Symbol; - if (speculativeSymbol == null && - memberAccess.Parent is InvocationExpressionSyntax parentInvocation && - parentInvocation.Expression == memberAccess && - originalInvocationSymbol != null) + foreach (var rewriter in CodeModelGenerator.Instance.Rewriters) { - var candidateInvocation = parentInvocation.WithExpression(candidate); - speculativeSymbol = semanticModel.GetSpeculativeSymbolInfo( - parentInvocation.SpanStart, - candidateInvocation, - SpeculativeBindingOption.BindAsExpression).Symbol; - originalSymbol = originalInvocationSymbol; + rewriter.SemanticModel = semanticModel; + root = MeasurePostProcessingStep($"CustomRewriter.{rewriter.GetType().Name}", () => rewriter.Visit(root)); } + document = document.WithSyntaxRoot(root); - if (speculativeSymbol != null && - SymbolEqualityComparer.Default.Equals(originalSymbol, speculativeSymbol)) - { - replacements.Add(memberAccess, candidate); - } - } - - if (replacements.Count == 0) - { - return document; - } - - var rewrittenRoot = root.ReplaceNodes( - replacements.Keys, - (original, rewritten) => replacements[original].WithTriviaFrom(rewritten)); - return document.WithSyntaxRoot(rewrittenRoot); - } - - private static bool TryGetThisQualificationReplacement( - SemanticModel semanticModel, - MemberAccessExpressionSyntax memberAccess, - out ExpressionSyntax replacement) - { - replacement = memberAccess; - if (memberAccess.Expression is not ThisExpressionSyntax) - { - return false; - } + document = await MeasurePostProcessingStepAsync("Roslyn.Simplifier.ReduceAsync", () => Simplifier.ReduceAsync(document)); - var originalSymbol = semanticModel.GetSymbolInfo(memberAccess).Symbol; - var originalInvocationSymbol = memberAccess.Parent is InvocationExpressionSyntax invocation && invocation.Expression == memberAccess - ? semanticModel.GetSymbolInfo(invocation).Symbol - : null; - if (originalSymbol == null) - { - originalSymbol = originalInvocationSymbol; - if (originalSymbol == null) + // Reformat if any custom rewriters have been applied + if (CodeModelGenerator.Instance.Rewriters.Count > 0) { - return false; + document = await MeasurePostProcessingStepAsync("Formatter.FormatAsync", () => Formatter.FormatAsync(document)); } - } - - var candidate = memberAccess.Name.WithTriviaFrom(memberAccess); - var speculativeSymbol = semanticModel.GetSpeculativeSymbolInfo( - memberAccess.SpanStart, - candidate, - SpeculativeBindingOption.BindAsExpression).Symbol; - if (speculativeSymbol == null && - memberAccess.Parent is InvocationExpressionSyntax parentInvocation && - parentInvocation.Expression == memberAccess && - originalInvocationSymbol != null) - { - var candidateInvocation = parentInvocation.WithExpression(candidate); - speculativeSymbol = semanticModel.GetSpeculativeSymbolInfo( - parentInvocation.SpanStart, - candidateInvocation, - SpeculativeBindingOption.BindAsExpression).Symbol; - originalSymbol = originalInvocationSymbol; - } - - if (speculativeSymbol == null || - !SymbolEqualityComparer.Default.Equals(originalSymbol, speculativeSymbol)) - { - return false; - } - - replacement = candidate; - return true; - } - - private static async Task ReducePredefinedTypeNamesAsync(Document document) - { - var root = await document.GetSyntaxRootAsync(); - if (root == null) - { return document; } - - var identifiers = root.DescendantNodes() - .OfType() - .Where(static identifier => - identifier.Identifier.ValueText is "Byte" or "Char" && - identifier.Parent is not MemberAccessExpressionSyntax and not QualifiedNameSyntax) - .ToList(); - var castExpressions = root.DescendantNodes() - .OfType() - .Where(static castExpression => - castExpression.Expression is LiteralExpressionSyntax literalExpression && - (literalExpression.IsKind(SyntaxKind.NullLiteralExpression) || - literalExpression.IsKind(SyntaxKind.DefaultLiteralExpression)) && - castExpression.Parent is EqualsValueClauseSyntax) - .ToList(); - - if (identifiers.Count == 0 && castExpressions.Count == 0) + finally { - return document; - } - - var rewrittenRoot = root - .ReplaceNodes( - identifiers, - static (_, rewritten) => rewritten.Identifier.ValueText switch - { - "Byte" => SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.ByteKeyword)).WithTriviaFrom(rewritten), - "Char" => SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.CharKeyword)).WithTriviaFrom(rewritten), - _ => rewritten - }) - .ReplaceNodes( - castExpressions, - static (_, rewritten) => rewritten.Expression.WithTriviaFrom(rewritten)); - return document.WithSyntaxRoot(rewrittenRoot); - } - - private static async Task ReduceDocumentationQualifiedNamesAsync(Document document) - { - var root = await document.GetSyntaxRootAsync(); - if (root == null) - { - return document; - } - - var documentationTrivia = root.DescendantTrivia(descendIntoTrivia: true) - .Where(static trivia => - trivia.HasStructure && - trivia.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia) && - (trivia.ToFullString().Contains("global::", StringComparison.Ordinal) || - trivia.ToFullString().Contains("cref=\"", StringComparison.Ordinal))) - .ToList(); - if (documentationTrivia.Count == 0) - { - return document; - } - - var rewrittenRoot = root.ReplaceTrivia( - documentationTrivia, - (original, rewritten) => - { - var reduced = ReduceDocumentationTriviaText(root, original, rewritten.ToFullString()); - var parsedTrivia = SyntaxFactory.ParseLeadingTrivia(reduced); - return parsedTrivia.Count == 1 ? parsedTrivia[0] : rewritten; - }); - return document.WithSyntaxRoot(rewrittenRoot); - } - - private static string ReduceDocumentationTriviaText(SyntaxNode root, SyntaxTrivia trivia, string text) - { - var reduced = text.Replace("global::", string.Empty, StringComparison.Ordinal); - var namespacePrefix = trivia.Token.Parent? - .AncestorsAndSelf() - .OfType() - .FirstOrDefault()? - .Name - .ToString(); - - var namespacePrefixes = root.DescendantNodes() - .OfType() - .Where(static directive => directive is { Alias: null, StaticKeyword.RawKind: 0, Name: not null }) - .Select(static directive => directive.Name!.ToString()) - .Append(namespacePrefix) - .Where(static prefix => !string.IsNullOrEmpty(prefix)) - .Distinct(StringComparer.Ordinal) - .OrderByDescending(static prefix => prefix!.Length); - - var prefixes = namespacePrefixes.Select(static prefix => prefix! + ".").ToArray(); - return Regex.Replace( - reduced, - @"(?(?:cref|name)="")(?[^""]*)(?"")", - match => + if (totalStopwatch != null) { - var value = match.Groups["value"].Value; - if (IsMockingReturnsCref(reduced, match.Index)) - { - value = value.Replace("SampleTypeSpec.Models.Custom.", "Models.Custom.", StringComparison.Ordinal); - return match.Groups["attribute"].Value + value + match.Groups["quote"].Value; - } - - if (IsAbstractDerivedTypesCref(trivia, reduced, match.Index)) - { - return match.Groups["attribute"].Value + value + match.Groups["quote"].Value; - } - - foreach (var prefix in prefixes) - { - value = value.Replace(prefix, string.Empty, StringComparison.Ordinal); - } - - return match.Groups["attribute"].Value + value + match.Groups["quote"].Value; - }); - } - - private static bool IsMockingReturnsCref(string text, int index) - { - var lineStart = text.LastIndexOf('\n', Math.Max(0, index - 1)); - var lineEnd = text.IndexOf('\n', index); - lineStart = lineStart < 0 ? 0 : lineStart + 1; - lineEnd = lineEnd < 0 ? text.Length : lineEnd; - return text.Substring(lineStart, lineEnd - lineStart).Contains(" instance for mocking.", StringComparison.Ordinal); - } - - private static bool IsAbstractDerivedTypesCref(SyntaxTrivia trivia, string text, int index) - { - if (trivia.Token.Parent? - .AncestorsAndSelf() - .OfType() - .FirstOrDefault()? - .Identifier.ValueText.EndsWith("ModelFactory", StringComparison.Ordinal) != true) - { - return false; - } - - var lineStart = text.LastIndexOf('\n', Math.Max(0, index - 1)); - var lineEnd = text.IndexOf('\n', index); - lineStart = lineStart < 0 ? 0 : lineStart + 1; - lineEnd = lineEnd < 0 ? text.Length : lineEnd; - return text.Substring(lineStart, lineEnd - lineStart).Contains("derived classes available for instantiation", StringComparison.Ordinal); - } - - private static bool TryGetNameReplacement( - SemanticModel semanticModel, - NameSyntax originalName, - ISymbol originalSymbol, - out NameSyntax replacement) - { - replacement = originalName; - if (!TryGetNameParts(originalName, out var parts)) - { - return false; - } - - for (int i = parts.Count - 1; i >= 0; i--) - { - var candidate = BuildName(parts, i).WithTriviaFrom(originalName); - if (SpeculativelyBindsToSameSymbol(semanticModel, originalName, candidate, originalSymbol)) - { - replacement = candidate; - return true; + totalStopwatch.Stop(); + PostProcessingProfile?.Add("ProcessDocument.Total", totalStopwatch.Elapsed, 0); } } - - return false; } - private static bool SpeculativelyBindsToSameSymbol( - SemanticModel semanticModel, - NameSyntax originalName, - NameSyntax replacement, - ISymbol originalSymbol) + private static T MeasurePostProcessingStep(string stepName, Func action) { - var speculativeSymbol = semanticModel.GetSpeculativeSymbolInfo( - originalName.SpanStart, - replacement, - SpeculativeBindingOption.BindAsTypeOrNamespace).Symbol; - if (speculativeSymbol != null && - SymbolEqualityComparer.Default.Equals(originalSymbol, speculativeSymbol)) + var profile = PostProcessingProfile; + if (profile == null) { - return true; + return action(); } - if (originalSymbol is ITypeSymbol originalType) - { - var speculativeType = semanticModel.GetSpeculativeTypeInfo( - originalName.SpanStart, - replacement, - SpeculativeBindingOption.BindAsTypeOrNamespace).Type; - if (speculativeType != null && - SymbolEqualityComparer.Default.Equals(originalType, speculativeType)) - { - return true; - } - } - - if (originalName.Parent is MemberAccessExpressionSyntax memberAccess && - memberAccess.Expression == originalName) - { - speculativeSymbol = semanticModel.GetSpeculativeSymbolInfo( - originalName.SpanStart, - replacement, - SpeculativeBindingOption.BindAsExpression).Symbol; - return speculativeSymbol != null && - SymbolEqualityComparer.Default.Equals(originalSymbol, speculativeSymbol); - } - - return false; - } - - private static bool TryGetNameParts(NameSyntax name, out IReadOnlyList parts) - { - var builder = new List(); - if (AddNameParts(name, builder)) - { - parts = builder; - return true; - } - - parts = []; - return false; - } - - private static bool AddNameParts(NameSyntax name, List parts) - { - switch (name) - { - case SimpleNameSyntax simpleName: - parts.Add(simpleName); - return true; - case QualifiedNameSyntax qualifiedName: - if (!AddNameParts(qualifiedName.Left, parts)) - { - return false; - } - - parts.Add(qualifiedName.Right); - return true; - case AliasQualifiedNameSyntax { Alias.Identifier.ValueText: "global" } aliasQualifiedName: - parts.Add(aliasQualifiedName.Name); - return true; - default: - return false; - } - } - - private static NameSyntax BuildName(IReadOnlyList parts, int startIndex) - { - NameSyntax name = parts[startIndex]; - for (int i = startIndex + 1; i < parts.Count; i++) - { - name = SyntaxFactory.QualifiedName(name, parts[i]); - } - - return name; - } - - private static SimpleNameSyntax GetRightmostName(NameSyntax name) => name switch - { - QualifiedNameSyntax qualifiedName => qualifiedName.Right, - AliasQualifiedNameSyntax aliasQualifiedName => aliasQualifiedName.Name, - SimpleNameSyntax simpleName => simpleName, - _ => throw new InvalidOperationException($"Unexpected name syntax: {name.Kind()}") - }; - - private static bool TryGetAttributeNameReplacement( - SemanticModel semanticModel, - AttributeSyntax attribute, - out NameSyntax replacement) - { - replacement = attribute.Name; - if (attribute.Name is not QualifiedNameSyntax and not AliasQualifiedNameSyntax) - { - return false; - } - - var originalSymbol = semanticModel.GetSymbolInfo(attribute).Symbol; - if (originalSymbol is not IMethodSymbol { ContainingType: { } originalAttributeType }) - { - return false; - } - - var rightmostName = GetRightmostName(attribute.Name); - var speculativeSymbol = semanticModel.GetSpeculativeSymbolInfo( - attribute.Name.SpanStart, - rightmostName, - SpeculativeBindingOption.BindAsTypeOrNamespace).Symbol; - if (!SymbolEqualityComparer.Default.Equals(originalAttributeType, speculativeSymbol)) - { - return false; - } - - replacement = TrimAttributeSuffix(rightmostName).WithTriviaFrom(attribute.Name); - return true; - } - - private static SimpleNameSyntax TrimAttributeSuffix(SimpleNameSyntax name) - { - const string AttributeSuffix = "Attribute"; - var identifier = name.Identifier; - var text = identifier.ValueText; - if (!text.EndsWith(AttributeSuffix, StringComparison.Ordinal) || text.Length == AttributeSuffix.Length) - { - return name; - } - - return SyntaxFactory.IdentifierName( - SyntaxFactory.Identifier( - identifier.LeadingTrivia, - text.Substring(0, text.Length - AttributeSuffix.Length), - identifier.TrailingTrivia)); - } - - private static bool TryGetMemberAccessReplacement( - SemanticModel semanticModel, - MemberAccessExpressionSyntax memberAccess, - out ExpressionSyntax replacement) - { - replacement = memberAccess; - var originalSymbol = semanticModel.GetSymbolInfo(memberAccess).Symbol; - var invocationExpression = memberAccess.Parent is InvocationExpressionSyntax invocation && invocation.Expression == memberAccess - ? invocation - : null; - var originalInvocationSymbol = invocationExpression != null - ? semanticModel.GetSymbolInfo(invocationExpression).Symbol - : null; - if (memberAccess.Expression is PredefinedTypeSyntax { Keyword.RawKind: (int)SyntaxKind.IntKeyword or (int)SyntaxKind.FloatKeyword } && - originalInvocationSymbol != null && - TryReduceInvocationExpression(semanticModel, invocationExpression!, memberAccess.Name, originalInvocationSymbol)) - { - replacement = memberAccess.Name.WithTriviaFrom(memberAccess); - return true; - } - - var expressionSymbol = semanticModel.GetSymbolInfo(memberAccess.Expression).Symbol; - if (expressionSymbol is not null and not INamespaceSymbol and not INamedTypeSymbol) + var allocatedBytes = GC.GetTotalAllocatedBytes(precise: false); + var stopwatch = Stopwatch.StartNew(); + try { - return false; + return action(); } - - if (originalSymbol == null || - !TryGetMemberAccessParts(memberAccess, out var parts) || - parts.Count < 2) + finally { - originalSymbol = originalInvocationSymbol; - if (originalSymbol == null || - !TryGetMemberAccessParts(memberAccess, out parts) || - parts.Count < 2) - { - return false; - } + stopwatch.Stop(); + profile.Add(stepName, stopwatch.Elapsed, GC.GetTotalAllocatedBytes(precise: false) - allocatedBytes); } - - for (int i = parts.Count - 1; i > 0; i--) - { - var candidate = BuildMemberAccess(parts, i).WithTriviaFrom(memberAccess); - var speculativeSymbol = semanticModel.GetSpeculativeSymbolInfo( - memberAccess.SpanStart, - candidate, - SpeculativeBindingOption.BindAsExpression).Symbol; - if (speculativeSymbol != null && - SymbolEqualityComparer.Default.Equals(originalSymbol, speculativeSymbol)) - { - replacement = candidate; - return true; - } - - if (memberAccess.Parent is InvocationExpressionSyntax parentInvocation && - parentInvocation.Expression == memberAccess && - originalInvocationSymbol != null && - TryReduceInvocationExpression(semanticModel, parentInvocation, candidate, originalInvocationSymbol)) - { - replacement = candidate; - return true; - } - } - - return false; - } - - private static bool TryReduceInvocationExpression( - SemanticModel semanticModel, - InvocationExpressionSyntax invocation, - ExpressionSyntax candidateExpression, - ISymbol originalInvocationSymbol) - { - var candidateInvocation = invocation.WithExpression(candidateExpression); - var speculativeSymbol = semanticModel.GetSpeculativeSymbolInfo( - invocation.SpanStart, - candidateInvocation, - SpeculativeBindingOption.BindAsExpression).Symbol; - return speculativeSymbol != null && - SymbolEqualityComparer.Default.Equals(originalInvocationSymbol, speculativeSymbol); } - private static bool TryGetMemberAccessParts(ExpressionSyntax expression, out IReadOnlyList parts) + private static async Task MeasurePostProcessingStepAsync(string stepName, Func> action) { - var builder = new List(); - if (AddMemberAccessParts(expression, builder)) + var profile = PostProcessingProfile; + if (profile == null) { - parts = builder; - return true; + return await action(); } - parts = []; - return false; - } - - private static bool AddMemberAccessParts(SyntaxNode expression, List parts) - { - switch (expression) - { - case SimpleNameSyntax name: - parts.Add(name); - return true; - case MemberAccessExpressionSyntax memberAccess: - if (!AddMemberAccessParts(memberAccess.Expression, parts)) - { - return false; - } - - parts.Add(memberAccess.Name); - return true; - case QualifiedNameSyntax qualifiedName: - if (!AddMemberAccessParts(qualifiedName.Left, parts)) - { - return false; - } - - parts.Add(qualifiedName.Right); - return true; - case AliasQualifiedNameSyntax { Alias.Identifier.ValueText: "global" } aliasQualifiedName: - parts.Add(aliasQualifiedName.Name); - return true; - default: - return false; - } - } - - private static ExpressionSyntax BuildMemberAccess(IReadOnlyList parts, int startIndex) - { - ExpressionSyntax expression = parts[startIndex]; - for (int i = startIndex + 1; i < parts.Count; i++) + var allocatedBytes = GC.GetTotalAllocatedBytes(precise: false); + var stopwatch = Stopwatch.StartNew(); + try { - expression = SyntaxFactory.MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - expression, - parts[i]); + return await action(); } - - return expression; - } - - private static bool IsInUnsupportedQualifiedNameContext(NameSyntax name) => - name.Ancestors().Any(static ancestor => - ancestor is UsingDirectiveSyntax || - ancestor is CrefSyntax); - - private static bool IsInUnsupportedQualifiedNameContext(ExpressionSyntax expression) => - expression.Ancestors().Any(static ancestor => - ancestor is CrefSyntax); - - private static bool ContainsSimplifierAnnotations(SyntaxNode root) => - root.HasAnnotation(Simplifier.Annotation) || - root.DescendantNodesAndTokens(descendIntoTrivia: true).Any(static nodeOrToken => - nodeOrToken.HasAnnotation(Simplifier.Annotation)); - - private static IReadOnlyList GetSimplifierSpans(SyntaxNode root) - { - List spans = new(); - foreach (var member in root.DescendantNodes().OfType()) + finally { - if (ContainsReducibleSyntax(member)) - { - spans.Add(member.FullSpan); - } + stopwatch.Stop(); + profile.Add(stepName, stopwatch.Elapsed, GC.GetTotalAllocatedBytes(precise: false) - allocatedBytes); } - - spans.AddRange(root - .DescendantNodesAndTokens(descendIntoTrivia: true) - .Where(static nodeOrToken => nodeOrToken.HasAnnotation(Simplifier.Annotation)) - .Select(static nodeOrToken => nodeOrToken.FullSpan)); - - return spans; } - private static bool ContainsReducibleSyntax(SyntaxNode root) => - root.DescendantNodes( - descendIntoChildren: node => node == root || node is not MemberDeclarationSyntax, - descendIntoTrivia: true).Any(static node => - node is ThisExpressionSyntax || - node is ParenthesizedExpressionSyntax || - node is CrefSyntax || - node is QualifiedNameSyntax || - node is MemberAccessExpressionSyntax || - node is AssignmentExpressionSyntax { RawKind: (int)SyntaxKind.SimpleAssignmentExpression } || - node is AliasQualifiedNameSyntax { Alias.Identifier.ValueText: "global" }); - public static bool IsGeneratedDocument(Document document) => document.Folders.Contains(GeneratedFolder); public static bool IsCustomDocument(Document document) => !IsGeneratedDocument(document); public static bool IsGeneratedTestDocument(Document document) => document.Folders.Contains(GeneratedTestFolder); @@ -1313,11 +343,11 @@ public async Task PostProcessAsync() case Configuration.UnreferencedTypesHandlingOption.KeepAll: break; case Configuration.UnreferencedTypesHandlingOption.Internalize: - _project = await postProcessor.InternalizeAsync(_project); + _project = await MeasurePostProcessingStepAsync("PostProcess.InternalizeAsync", () => postProcessor.InternalizeAsync(_project)); break; case Configuration.UnreferencedTypesHandlingOption.RemoveOrInternalize: - _project = await postProcessor.InternalizeAsync(_project); - _project = await postProcessor.RemoveAsync(_project); + _project = await MeasurePostProcessingStepAsync("PostProcess.InternalizeAsync", () => postProcessor.InternalizeAsync(_project)); + _project = await MeasurePostProcessingStepAsync("PostProcess.RemoveAsync", () => postProcessor.RemoveAsync(_project)); break; } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspacePostProcessingProfile.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspacePostProcessingProfile.cs new file mode 100644 index 00000000000..74a9a58168a --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspacePostProcessingProfile.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; + +namespace Microsoft.TypeSpec.Generator +{ + internal sealed class GeneratedCodeWorkspacePostProcessingProfile + { + private readonly object _syncRoot = new(); + private readonly Dictionary _steps = new(StringComparer.Ordinal); + + public void Add(string stepName, TimeSpan elapsed, long allocatedBytes) + { + lock (_syncRoot) + { + ref var summary = ref CollectionsMarshal.GetValueRefOrAddDefault(_steps, stepName, out _); + summary.Count++; + summary.ElapsedTicks += elapsed.Ticks; + summary.AllocatedBytes += allocatedBytes; + } + } + + public string GetSummary() + { + KeyValuePair[] steps; + lock (_syncRoot) + { + steps = _steps.ToArray(); + } + + var totalTicks = steps + .Where(static step => step.Key != "ProcessDocument.Total") + .Sum(static step => step.Value.ElapsedTicks); + var builder = new StringBuilder(); + builder.AppendLine("Post-processing step profile:"); + builder.AppendLine("Step, Count, Total ms, Avg ms, Percent of measured steps, Allocated bytes, Avg allocated bytes"); + + foreach (var step in steps.OrderByDescending(static step => step.Value.ElapsedTicks)) + { + var elapsedMs = TimeSpan.FromTicks(step.Value.ElapsedTicks).TotalMilliseconds; + var averageMs = elapsedMs / step.Value.Count; + var averageAllocatedBytes = step.Value.AllocatedBytes / step.Value.Count; + var percentage = totalTicks == 0 || step.Key == "ProcessDocument.Total" + ? 0 + : step.Value.ElapsedTicks * 100.0 / totalTicks; + builder.AppendLine($"{step.Key}, {step.Value.Count}, {elapsedMs:F3}, {averageMs:F3}, {percentage:F1}%, {step.Value.AllocatedBytes}, {averageAllocatedBytes}"); + } + + return builder.ToString(); + } + + private struct StepSummary + { + public int Count; + public long ElapsedTicks; + public long AllocatedBytes; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs index be96e11df59..d5db7e47298 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs @@ -20,6 +20,8 @@ internal class PostProcessor private readonly HashSet _typesToKeep; private INamedTypeSymbol? _modelFactorySymbol; + private static GeneratedCodeWorkspacePostProcessingProfile? Profile => GeneratedCodeWorkspace.PostProcessingProfile; + public PostProcessor( HashSet typesToKeep, string? modelFactoryFullName = null, @@ -125,40 +127,55 @@ protected virtual bool ShouldIncludeDocument(Document document) => /// The processed . is immutable, therefore this should usually be a new instance public async Task InternalizeAsync(Project project) { - var compilation = await project.GetCompilationAsync(); + var compilation = await MeasureAsync("PostProcessor.Internalize.GetCompilationAsync", () => project.GetCompilationAsync()); if (compilation == null) return project; // first get all the declared symbols - var definitions = await GetTypeSymbolsAsync(compilation, project, true); + var definitions = await MeasureAsync("PostProcessor.Internalize.GetTypeSymbolsAsync", () => GetTypeSymbolsAsync(compilation, project, true)); // build the reference map var referenceMap = - await new ReferenceMapBuilder(compilation, project).BuildPublicReferenceMapAsync( - definitions.DeclaredSymbols, definitions.DeclaredNodesCache); + await MeasureAsync( + "PostProcessor.Internalize.BuildPublicReferenceMapAsync", + () => new ReferenceMapBuilder(compilation, project).BuildPublicReferenceMapAsync( + definitions.DeclaredSymbols, definitions.DeclaredNodesCache)); // get the root symbols - var rootSymbols = await GetRootSymbolsAsync(project, definitions); + var rootSymbols = await MeasureAsync("PostProcessor.Internalize.GetRootSymbolsAsync", () => GetRootSymbolsAsync(project, definitions)); // traverse all the root and recursively add all the things we met - var publicSymbols = VisitSymbolsFromRootAsync(rootSymbols, referenceMap); + var publicSymbols = Measure("PostProcessor.Internalize.VisitSymbolsFromRoot", () => VisitSymbolsFromRootAsync(rootSymbols, referenceMap).ToArray()); var symbolsToInternalize = definitions.DeclaredSymbols.Except(publicSymbols); - var nodesToInternalize = new Dictionary(); - foreach (var symbol in symbolsToInternalize) + var nodesToInternalize = Measure("PostProcessor.Internalize.CollectNodes", () => { - foreach (var node in definitions.DeclaredNodesCache[symbol]) + var nodes = new Dictionary(); + foreach (var symbol in symbolsToInternalize) { - nodesToInternalize[node] = project.GetDocumentId(node.SyntaxTree)!; + foreach (var node in definitions.DeclaredNodesCache[symbol]) + { + nodes[node] = project.GetDocumentId(node.SyntaxTree)!; + } } - } - foreach (var (model, documentId) in nodesToInternalize) + return nodes; + }); + + project = Measure("PostProcessor.Internalize.MarkInternal", () => { - project = MarkInternal(project, model, documentId); - } + var updatedProject = project; + foreach (var (model, documentId) in nodesToInternalize) + { + updatedProject = MarkInternal(updatedProject, model, documentId); + } + + return updatedProject; + }); var modelNamesToRemove = nodesToInternalize.Keys.Select(item => item.Identifier.Text); - project = await RemoveMethodsFromModelFactoryAsync(project, definitions, modelNamesToRemove.ToHashSet()); + project = await MeasureAsync( + "PostProcessor.Internalize.RemoveMethodsFromModelFactoryAsync", + () => RemoveMethodsFromModelFactoryAsync(project, definitions, modelNamesToRemove.ToHashSet())); return project; } @@ -232,42 +249,49 @@ private async Task RemoveMethodsFromModelFactoryAsync(Project project, /// The processed . is immutable, therefore this should usually be a new instance public async Task RemoveAsync(Project project) { - var compilation = await project.GetCompilationAsync(); + var compilation = await MeasureAsync("PostProcessor.Remove.GetCompilationAsync", () => project.GetCompilationAsync()); if (compilation == null) return project; // find all the declarations, including non-public declared - var definitions = await GetTypeSymbolsAsync(compilation, project, false); + var definitions = await MeasureAsync("PostProcessor.Remove.GetTypeSymbolsAsync", () => GetTypeSymbolsAsync(compilation, project, false)); // build reference map var referenceMap = - await new ReferenceMapBuilder(compilation, project).BuildAllReferenceMapAsync( - definitions.DeclaredSymbols, definitions.DocumentsCache); + await MeasureAsync( + "PostProcessor.Remove.BuildAllReferenceMapAsync", + () => new ReferenceMapBuilder(compilation, project).BuildAllReferenceMapAsync( + definitions.DeclaredSymbols, definitions.DocumentsCache)); // get root symbols - var rootSymbols = await GetRootSymbolsAsync(project, definitions); + var rootSymbols = await MeasureAsync("PostProcessor.Remove.GetRootSymbolsAsync", () => GetRootSymbolsAsync(project, definitions)); // include model factory as a root symbol when doing the remove pass so that we are sure to include any internal // helpers that are required by the model factory. if (_modelFactorySymbol != null) rootSymbols.Add(_modelFactorySymbol); // traverse the map to determine the declarations that we are about to remove, starting from root nodes - var referencedSymbols = VisitSymbolsFromRootAsync(rootSymbols, referenceMap); + var referencedSymbols = Measure("PostProcessor.Remove.VisitSymbolsFromRoot", () => VisitSymbolsFromRootAsync(rootSymbols, referenceMap).ToArray().AsEnumerable()); - referencedSymbols = AddSampleSymbols(referencedSymbols, definitions.DeclaredSymbols); - var referencedSet = new HashSet(referencedSymbols, SymbolEqualityComparer.Default); + referencedSymbols = Measure("PostProcessor.Remove.AddSampleSymbols", () => AddSampleSymbols(referencedSymbols, definitions.DeclaredSymbols)); + var referencedSet = Measure("PostProcessor.Remove.BuildReferencedSet", () => new HashSet(referencedSymbols, SymbolEqualityComparer.Default)); var symbolsToRemove = definitions.DeclaredSymbols.Except(referencedSet); - var nodesToRemove = new List(); - foreach (var symbol in symbolsToRemove) + var nodesToRemove = Measure("PostProcessor.Remove.CollectNodes", () => { - if (referencedSet.Contains(GetBase(symbol))) + var nodes = new List(); + foreach (var symbol in symbolsToRemove) { - continue; + if (referencedSet.Contains(GetBase(symbol))) + { + continue; + } + nodes.AddRange(definitions.DeclaredNodesCache[symbol]); } - nodesToRemove.AddRange(definitions.DeclaredNodesCache[symbol]); - } + + return nodes; + }); // remove them one by one - project = await RemoveModelsAsync(project, nodesToRemove); + project = await MeasureAsync("PostProcessor.Remove.RemoveModelsAsync", () => RemoveModelsAsync(project, nodesToRemove)); return project; } @@ -349,25 +373,35 @@ private async Task RemoveModelsAsync(Project project, IEnumerable unusedModels) { // accumulate the definitions from the same document together - var documents = new Dictionary>(); - - foreach (var model in unusedModels) + var documents = Measure("PostProcessor.Remove.RemoveModelsAsync.GroupByDocument", () => { - var document = project.GetDocument(model.SyntaxTree); - Debug.Assert(document != null); - if (!documents.ContainsKey(document)) - documents.Add(document, new HashSet()); + var groupedDocuments = new Dictionary>(); + foreach (var model in unusedModels) + { + var document = project.GetDocument(model.SyntaxTree); + Debug.Assert(document != null); + if (!groupedDocuments.ContainsKey(document)) + groupedDocuments.Add(document, new HashSet()); - documents[document].Add(model); - } + groupedDocuments[document].Add(model); + } - foreach (var models in documents.Values) + return groupedDocuments; + }); + + project = await MeasureAsync("PostProcessor.Remove.RemoveModelsAsync.RemoveModelsFromDocuments", async () => { - project = await RemoveModelsFromDocumentAsync(project, models); - } + var updatedProject = project; + foreach (var models in documents.Values) + { + updatedProject = await RemoveModelsFromDocumentAsync(updatedProject, models); + } + + return updatedProject; + }); // remove what are now invalid references due to the models being removed - project = await RemoveInvalidRefs(project); + project = await MeasureAsync("PostProcessor.Remove.RemoveModelsAsync.RemoveInvalidRefs", () => RemoveInvalidRefs(project)); return project; } @@ -418,16 +452,28 @@ private async Task RemoveInvalidRefs(Project project) var solution = project.Solution; // Process each document for invalid usings - foreach (var documentId in project.DocumentIds) + solution = await MeasureAsync("PostProcessor.Remove.RemoveInvalidRefs.RemoveInvalidUsings", async () => { - solution = await RemoveInvalidUsings(solution, documentId); - } + var updatedSolution = solution; + foreach (var documentId in project.DocumentIds) + { + updatedSolution = await RemoveInvalidUsings(updatedSolution, documentId); + } + + return updatedSolution; + }); // Process each document for invalid attributes (with fresh semantic models) - foreach (var documentId in project.DocumentIds) + solution = await MeasureAsync("PostProcessor.Remove.RemoveInvalidRefs.RemoveInvalidAttributes", async () => { - solution = await RemoveInvalidAttributes(solution, documentId); - } + var updatedSolution = solution; + foreach (var documentId in project.DocumentIds) + { + updatedSolution = await RemoveInvalidAttributes(updatedSolution, documentId); + } + + return updatedSolution; + }); return solution.GetProject(project.Id)!; } @@ -533,6 +579,48 @@ arg.Expression is TypeOfExpressionSyntax typeOfExpr && return solution; } + private static T Measure(string stepName, Func action) + { + var profile = Profile; + if (profile == null) + { + return action(); + } + + var allocatedBytes = GC.GetTotalAllocatedBytes(precise: false); + var stopwatch = Stopwatch.StartNew(); + try + { + return action(); + } + finally + { + stopwatch.Stop(); + profile.Add(stepName, stopwatch.Elapsed, GC.GetTotalAllocatedBytes(precise: false) - allocatedBytes); + } + } + + private static async Task MeasureAsync(string stepName, Func> action) + { + var profile = Profile; + if (profile == null) + { + return await action(); + } + + var allocatedBytes = GC.GetTotalAllocatedBytes(precise: false); + var stopwatch = Stopwatch.StartNew(); + try + { + return await action(); + } + finally + { + stopwatch.Stop(); + profile.Add(stepName, stopwatch.Elapsed, GC.GetTotalAllocatedBytes(precise: false) - allocatedBytes); + } + } + private async Task> GetRootSymbolsAsync(Project project, TypeSymbols modelSymbols) { var result = new HashSet(SymbolEqualityComparer.Default); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs index d1aec2a8486..0a75d8c9360 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs @@ -4,7 +4,6 @@ using Microsoft.Build.Construction; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Tests.Common; using NUnit.Framework; using System; @@ -98,514 +97,6 @@ await MockHelpers.LoadMockGeneratorAsync( Assert.NotNull(fooMethod, "Foo method should be found in the SimpleType"); } - [Test] - public async Task GetGeneratedFilesAsync_SimplifiesFrameworkNamesWhenTypeHasSystemMember() - { - var generatedText = await ProcessGeneratedCodeAsync( - """ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// - -#nullable disable - -using System; -using System.ComponentModel; - -namespace TestNamespace -{ - public readonly partial struct TestRole : IEquatable - { - private readonly string _value; - private const string SystemValue = "system"; - - public TestRole(string value) - { - _value = value; - } - - public static TestRole System { get; } = new TestRole(SystemValue); - - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] - public override bool Equals(object obj) => obj is TestRole other && Equals(other); - - public bool Equals(TestRole other) => string.Equals(_value, other._value, global::System.StringComparison.InvariantCultureIgnoreCase); - - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] - public override int GetHashCode() => _value != null ? global::System.StringComparer.InvariantCultureIgnoreCase.GetHashCode(_value) : 0; - } -} -""", - typeof(EditorBrowsableAttribute).Assembly.Location); - - Assert.That(generatedText, Is.Not.Null); - Assert.That(generatedText, Does.Contain("[EditorBrowsable(EditorBrowsableState.Never)]")); - Assert.That(generatedText, Does.Contain("StringComparison.InvariantCultureIgnoreCase")); - Assert.That(generatedText, Does.Contain("StringComparer.InvariantCultureIgnoreCase")); - Assert.That(generatedText, Does.Not.Contain("System.ComponentModel.EditorBrowsableAttribute")); - Assert.That(generatedText, Does.Not.Contain("System.StringComparison")); - Assert.That(generatedText, Does.Not.Contain("System.StringComparer")); - } - - [Test] - public async Task GetGeneratedFilesAsync_PreservesQualificationWhenImportedNamespacesContainSameTypeName() - { - var generatedText = await ProcessGeneratedCodeAsync( - """ -// -#nullable disable - -using First; -using Second; - -namespace TestNamespace -{ - public class Container - { - public global::First.Conflict FirstValue { get; } - public global::Second.Conflict SecondValue { get; } - } -} - -namespace First -{ - public class Conflict { } -} - -namespace Second -{ - public class Conflict { } -} -"""); - - Assert.That(generatedText, Does.Contain("First.Conflict FirstValue")); - Assert.That(generatedText, Does.Contain("Second.Conflict SecondValue")); - Assert.That(generatedText, Does.Not.Contain("public Conflict FirstValue")); - Assert.That(generatedText, Does.Not.Contain("public Conflict SecondValue")); - } - - [Test] - public async Task GetGeneratedFilesAsync_PreservesFrameworkQualificationWhenGeneratedModelShadowsFrameworkType() - { - var generatedText = await ProcessGeneratedCodeAsync( - """ -// -#nullable disable - -using System; - -namespace TestNamespace -{ - public class BinaryData { } - - public class Container - { - public global::System.BinaryData Payload { get; } - } -} -"""); - - Assert.That(generatedText, Does.Contain("System.BinaryData Payload")); - Assert.That(generatedText, Does.Not.Contain("public BinaryData Payload")); - } - - [Test] - public async Task GetGeneratedFilesAsync_PreservesQualificationWhenCurrentNamespaceTypeShadowsImportedType() - { - var generatedText = await ProcessGeneratedCodeAsync( - """ -// -#nullable disable - -using External; - -namespace TestNamespace -{ - public class Widget { } - - public class Container - { - public global::External.Widget ExternalWidget { get; } - } -} - -namespace External -{ - public class Widget { } -} -"""); - - Assert.That(generatedText, Does.Contain("External.Widget ExternalWidget")); - Assert.That(generatedText, Does.Not.Contain("public Widget ExternalWidget")); - } - - [Test] - public async Task GetGeneratedFilesAsync_PreservesQualificationWhenParameterNameConflictsWithTypeName() - { - var generatedText = await ProcessGeneratedCodeAsync( - """ -// -#nullable disable - -using System; - -namespace TestNamespace -{ - public class Container - { - public bool Equals(string StringComparison) => global::System.StringComparison.InvariantCultureIgnoreCase.Equals(StringComparison, StringComparison); - } -} -"""); - - Assert.That(generatedText, Does.Contain("System.StringComparison.InvariantCultureIgnoreCase")); - Assert.That(generatedText, Does.Not.Contain("=> StringComparison.InvariantCultureIgnoreCase")); - } - - [Test] - public async Task GetGeneratedFilesAsync_ReducesGlobalAliasesInXmlDocCrefsSeparatelyFromCode() - { - var generatedText = await ProcessGeneratedCodeAsync( - """ -// -#nullable disable - -using System; - -namespace TestNamespace -{ - /// See . - public class Container - { - public global::System.ArgumentNullException Create() => null; - } -} -"""); - - Assert.That(generatedText, Does.Contain("")); - Assert.That(generatedText, Does.Contain("ArgumentNullException Create()")); - Assert.That(generatedText, Does.Not.Contain("global::System.ArgumentNullException")); - } - - [Test] - public async Task GetGeneratedFilesAsync_ReducesQualifiedXmlDocCrefs() - { - var generatedText = await ProcessGeneratedCodeAsync( - """ -// -#nullable disable - -using System; -using System.Text.Json; - -namespace TestNamespace -{ - /// See and . - /// The derived classes available for instantiation are: . - /// A new instance for mocking. - public class Widget - { - public JsonSerializerOptions Options { get; } - } -} -"""); - - Assert.That(generatedText, Does.Contain("")); - Assert.That(generatedText, Does.Contain("derived classes available for instantiation are: ")); - Assert.That(generatedText, Does.Contain("")); - Assert.That(generatedText, Does.Contain(" instance for mocking")); - Assert.That(generatedText, Does.Not.Contain("System.Text.Json.JsonSerializer")); - } - - [Test] - public async Task GetGeneratedFilesAsync_ReducesAliasesGenericNamesAndCustomizationTypesSafely() - { - var generatedText = await ProcessGeneratedCodeAsync( - """ -// -#nullable disable - -using System.Collections.Generic; -using AliasWidget = Customization.Widget; - -namespace TestNamespace -{ - public class Container - { - public global::System.Collections.Generic.IList Widgets { get; } - public global::Customization.Widget Create(AliasWidget widget) => widget; - public string GetFormat() => ((IPersistableModel)this).GetFormatFromOptions(null); - } - - public interface IPersistableModel - { - string GetFormatFromOptions(object options); - } -} - -namespace Customization -{ - public class Widget { } -} -"""); - - Assert.That(generatedText, Does.Contain("IList Widgets")); - Assert.That(generatedText, Does.Contain("Customization.Widget Create(AliasWidget widget)")); - Assert.That(generatedText, Does.Contain("((IPersistableModel)this).GetFormatFromOptions(null)")); - Assert.That(generatedText, Does.Not.Contain("global::System.Collections.Generic.IList")); - Assert.That(generatedText, Does.Not.Contain("global::Customization.Widget")); - } - - [Test] - public async Task GetGeneratedFilesAsync_ReducesQualifiedGenericTypeNames() - { - var generatedText = await ProcessGeneratedCodeAsync( - """ -// -#nullable disable - -using System.ClientModel; -using System.Threading.Tasks; - -namespace TestNamespace -{ - public class Widget { } - - public class Operations - { - public Task> GetAsync() => null; - } -} -""", - typeof(System.ClientModel.ClientResult).Assembly.Location); - - Assert.That(generatedText, Does.Contain("Task> GetAsync()")); - Assert.That(generatedText, Does.Not.Contain("System.ClientModel.ClientResult")); - Assert.That(generatedText, Does.Not.Contain("TestNamespace.Widget")); - } - - [Test] - public async Task GetGeneratedFilesAsync_ReducesSameNamespaceStaticMemberAccess() - { - var generatedText = await ProcessGeneratedCodeAsync( - """ -// -#nullable disable - -namespace TestNamespace -{ - public class Container - { - public void Invoke(string value) - { - TestNamespace.Argument.AssertNotNull(value, nameof(value)); - } - } - - internal static class Argument - { - public static void AssertNotNull(object value, string name) { } - } -} -"""); - - Assert.That(generatedText, Does.Contain("Argument.AssertNotNull(value, nameof(value));")); - Assert.That(generatedText, Does.Not.Contain("TestNamespace.Argument.AssertNotNull")); - } - - [Test] - public async Task GetGeneratedFilesAsync_PreservesStaticMemberQualificationWhenShortNameConflicts() - { - var generatedText = await ProcessGeneratedCodeAsync( - """ -// -#nullable disable - -namespace TestNamespace -{ - public class Container - { - public void Invoke(string value) - { - var Argument = new LocalArgument(); - TestNamespace.Argument.AssertNotNull(value, nameof(value)); - } - } - - internal static class Argument - { - public static void AssertNotNull(object value, string name) { } - } - - internal class LocalArgument - { - public void AssertNotNull(object value, string name) { } - } -} -"""); - - Assert.That(generatedText, Does.Contain("TestNamespace.Argument.AssertNotNull(value, nameof(value));")); - } - - [Test] - public async Task GetGeneratedFilesAsync_ReducesGeneratedParentheses() - { - var generatedText = await ProcessGeneratedCodeAsync( - """ -// -#nullable disable - -using System.Collections.Generic; - -namespace TestNamespace -{ - public class Container - { - private Dictionary _map; - private string[] _items; - private int _length; - - public Dictionary Map => (_map ??= new Dictionary()); - - public void Invoke(object writer, WidgetHolder widget, object value, object result, Byte[] bytes, Char[] chars, Options options = (Options)null, string nameHint = (String)null, global::System.String title = (global::System.String)null) - { - if (((value is ICollection collection) && (collection.Count == 0))) - { - Use(((Widget)result)); - } - - if ((_items[(collection.Count - 1)] == null)) - { - return; - } - - switch (collection.Count) - { - case ((>= 200) and (< 300)): - return; - } - - switch (value) - { - case string s when (s.Length > 0): - return; - } - - _map = _map ?? (GetMap() ?? new Dictionary()); - _length = (_length + 1); - string format = (nameHint == "W") ? nameHint : "J"; - string converted = TypeFormatters.ToString(bytes); - writer.WriteObjectValue((Widget)result, options); - widget.Value.Equals(widget.Value); - } - - private void Use(Widget widget) { } - private Dictionary GetMap() => null; - } - - public class Widget { } - public class WidgetHolder - { - public Widget Value { get; } - } - public class Options { } - - internal static class TypeFormatters - { - public static string ToString(byte[] value) => null; - public static string Invoke(byte[] value) => TypeFormatters.ToString(value); - } - - internal static class WriterExtensions - { - public static void WriteObjectValue(this object writer, T value, Options options) { } - } -} -"""); - - Assert.That(generatedText, Does.Contain("Map => _map ??= new Dictionary();")); - Assert.That(generatedText, Does.Contain("if (value is ICollection collection && collection.Count == 0)")); - Assert.That(generatedText, Does.Contain("Use((Widget)result);")); - Assert.That(generatedText, Does.Contain("_items[collection.Count - 1] == null")); - Assert.That(generatedText, Does.Contain("case >= 200 and < 300:")); - Assert.That(generatedText, Does.Contain("byte[] bytes")); - Assert.That(generatedText, Does.Contain("char[] chars")); - Assert.That(generatedText, Does.Contain("Options options = null")); - Assert.That(generatedText, Does.Contain("string nameHint = null")); - Assert.That(generatedText, Does.Contain("string title = null")); - Assert.That(generatedText, Does.Contain("case string s when s.Length > 0:")); - Assert.That(generatedText, Does.Contain("_map = _map ?? GetMap() ?? new Dictionary();")); - Assert.That(generatedText, Does.Contain("_length = _length + 1;")); - Assert.That(generatedText, Does.Contain("string format = nameHint == \"W\" ? nameHint : \"J\";")); - Assert.That(generatedText, Does.Contain("TypeFormatters.ToString(bytes);")); - Assert.That(generatedText, Does.Contain("writer.WriteObjectValue((Widget)result, options);")); - Assert.That(generatedText, Does.Contain("public static string Invoke(byte[] value) => ToString(value);")); - Assert.That(generatedText, Does.Contain("widget.Value.Equals(widget.Value);")); - Assert.That(generatedText, Does.Not.Contain("Use(((Widget)result))")); - Assert.That(generatedText, Does.Not.Contain("if ((value is ICollection collection)")); - Assert.That(generatedText, Does.Not.Contain("Byte[]")); - Assert.That(generatedText, Does.Not.Contain("Char[]")); - Assert.That(generatedText, Does.Not.Contain("(Options)null")); - Assert.That(generatedText, Does.Not.Contain("(String)null")); - } - - [Test] - public async Task GetGeneratedFilesAsync_ReducesThisQualificationSafely() - { - var generatedText = await ProcessGeneratedCodeAsync( - """ -// -#nullable disable - -namespace TestNamespace -{ - public class Container - { - public string Name { get; } - - public void Invoke() - { - this.Create(this.Name); - } - - private void Create(string name) { } - } -} -"""); - - Assert.That(generatedText, Does.Contain("Create(Name);")); - Assert.That(generatedText, Does.Not.Contain("this.Create")); - Assert.That(generatedText, Does.Not.Contain("this.Name")); - } - - [Test] - public async Task GetGeneratedFilesAsync_PreservesThisQualificationWhenLocalNameConflicts() - { - var generatedText = await ProcessGeneratedCodeAsync( - """ -// -#nullable disable - -namespace TestNamespace -{ - public class Container - { - public string Name { get; } - - public void Invoke(string Name) - { - this.Create(this.Name); - } - - private void Create(string name) { } - } -} -"""); - - Assert.That(generatedText, Does.Contain("Create(this.Name);")); - } - [Test] public async Task AddPackageReferencesFromProject_AddsReferencesFromCsproj() { @@ -845,27 +336,6 @@ public class Placeholder {{ }} return dllPath; } - private async Task ProcessGeneratedCodeAsync(string content, params string[] additionalMetadataReferencePaths) - { - MockHelpers.LoadMockGenerator( - outputPath: _projectDir, - configuration: "{\"package-name\": \"TestNamespace\"}", - additionalMetadataReferences: additionalMetadataReferencePaths.Select(static path => MetadataReference.CreateFromFile(path))); - - GeneratedCodeWorkspace.Initialize(); - var workspace = await GeneratedCodeWorkspace.Create(false); - await workspace.AddGeneratedFile(new CodeFile(content, "TestFile.cs")); - - string? generatedText = null; - await foreach (var generatedFile in workspace.GetGeneratedFilesAsync()) - { - generatedText = generatedFile.Text; - } - - Assert.That(generatedText, Is.Not.Null); - return generatedText!; - } - private void CreateTestAssemblyAndProjectFile(string nugetCacheDir, string csProjectFileName) { var ns = csProjectFileName.StartsWith("TestNamespaceUnevaluatedFrameworkValue") From c11ae8250b9147843022ae6d3d88feaad658c951 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Fri, 12 Jun 2026 04:22:34 +0000 Subject: [PATCH 014/111] Add provider reference map shadow replacement --- .../src/CSharpGen.cs | 2 + .../PostProcessing/GeneratedCodeWorkspace.cs | 5 + .../src/PostProcessing/PostProcessor.cs | 110 +++- .../ProviderReferenceMapShadowAnalyzer.cs | 616 ++++++++++++++++++ .../ProviderReferenceMapShadowResult.cs | 14 + 5 files changed, 716 insertions(+), 31 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowResult.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index 238b16b9766..fdd3ae2191f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs @@ -121,6 +121,8 @@ await customCodeWorkspace.GetCompilationAsync(), // Add all the generated files to the workspace await MeasureGenerationStepAsync("Generation.AddGeneratedFilesToWorkspace", () => Task.WhenAll(generateFilesTasks)); + MeasureGenerationStep("Generation.ProviderReferenceMapShadowAnalysis", () => generatedCodeWorkspace.AnalyzeProviderReferenceMap(output.TypeProviders)); + LoggingHelpers.LogElapsedTime("All generated types have been written into memory"); // Delete any old generated files diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index bf4b08b31b5..10d66430f97 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -86,6 +86,11 @@ public async Task AddInMemoryFile(TypeProvider type) await UpdateProject(document); } + internal void AnalyzeProviderReferenceMap(IReadOnlyList providers) + { + ProviderReferenceMapShadowAnalyzer.Analyze(providers, _project); + } + private async Task UpdateProject(Document document) { var root = await document.GetSyntaxRootAsync(); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs index d5db7e47298..57136754da1 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs @@ -133,18 +133,35 @@ public async Task InternalizeAsync(Project project) // first get all the declared symbols var definitions = await MeasureAsync("PostProcessor.Internalize.GetTypeSymbolsAsync", () => GetTypeSymbolsAsync(compilation, project, true)); - // build the reference map - var referenceMap = - await MeasureAsync( - "PostProcessor.Internalize.BuildPublicReferenceMapAsync", - () => new ReferenceMapBuilder(compilation, project).BuildPublicReferenceMapAsync( - definitions.DeclaredSymbols, definitions.DeclaredNodesCache)); - // get the root symbols - var rootSymbols = await MeasureAsync("PostProcessor.Internalize.GetRootSymbolsAsync", () => GetRootSymbolsAsync(project, definitions)); - // traverse all the root and recursively add all the things we met - var publicSymbols = Measure("PostProcessor.Internalize.VisitSymbolsFromRoot", () => VisitSymbolsFromRootAsync(rootSymbols, referenceMap).ToArray()); - - var symbolsToInternalize = definitions.DeclaredSymbols.Except(publicSymbols); + IEnumerable symbolsToInternalize; + if (ProviderReferenceMapShadowAnalyzer.UseShadowMap && ProviderReferenceMapShadowAnalyzer.LatestResult is { } useShadowResult) + { + symbolsToInternalize = Measure("PostProcessor.Internalize.UseShadowCandidates", () => + GetSymbolsByName(definitions.DeclaredSymbols, useShadowResult.InternalizeCandidates).ToArray()); + } + else + { + // build the reference map + var referenceMap = + await MeasureAsync( + "PostProcessor.Internalize.BuildPublicReferenceMapAsync", + () => new ReferenceMapBuilder(compilation, project).BuildPublicReferenceMapAsync( + definitions.DeclaredSymbols, definitions.DeclaredNodesCache)); + // get the root symbols + var rootSymbols = await MeasureAsync("PostProcessor.Internalize.GetRootSymbolsAsync", () => GetRootSymbolsAsync(project, definitions)); + // traverse all the root and recursively add all the things we met + var publicSymbols = Measure("PostProcessor.Internalize.VisitSymbolsFromRoot", () => VisitSymbolsFromRootAsync(rootSymbols, referenceMap).ToArray()); + + symbolsToInternalize = definitions.DeclaredSymbols.Except(publicSymbols); + } + + if (ProviderReferenceMapShadowAnalyzer.LatestResult is { } shadowResult) + { + ProviderReferenceMapShadowAnalyzer.WriteComparisonReport( + "internalize", + symbolsToInternalize.Select(static symbol => symbol.GetFullyQualifiedName()), + shadowResult.InternalizeCandidates); + } var nodesToInternalize = Measure("PostProcessor.Internalize.CollectNodes", () => { @@ -255,25 +272,45 @@ public async Task RemoveAsync(Project project) // find all the declarations, including non-public declared var definitions = await MeasureAsync("PostProcessor.Remove.GetTypeSymbolsAsync", () => GetTypeSymbolsAsync(compilation, project, false)); - // build reference map - var referenceMap = - await MeasureAsync( - "PostProcessor.Remove.BuildAllReferenceMapAsync", - () => new ReferenceMapBuilder(compilation, project).BuildAllReferenceMapAsync( - definitions.DeclaredSymbols, definitions.DocumentsCache)); - // get root symbols - var rootSymbols = await MeasureAsync("PostProcessor.Remove.GetRootSymbolsAsync", () => GetRootSymbolsAsync(project, definitions)); - // include model factory as a root symbol when doing the remove pass so that we are sure to include any internal - // helpers that are required by the model factory. - if (_modelFactorySymbol != null) - rootSymbols.Add(_modelFactorySymbol); - // traverse the map to determine the declarations that we are about to remove, starting from root nodes - var referencedSymbols = Measure("PostProcessor.Remove.VisitSymbolsFromRoot", () => VisitSymbolsFromRootAsync(rootSymbols, referenceMap).ToArray().AsEnumerable()); - - referencedSymbols = Measure("PostProcessor.Remove.AddSampleSymbols", () => AddSampleSymbols(referencedSymbols, definitions.DeclaredSymbols)); - var referencedSet = Measure("PostProcessor.Remove.BuildReferencedSet", () => new HashSet(referencedSymbols, SymbolEqualityComparer.Default)); - - var symbolsToRemove = definitions.DeclaredSymbols.Except(referencedSet); + IEnumerable symbolsToRemove; + HashSet referencedSet; + if (ProviderReferenceMapShadowAnalyzer.UseShadowMap && ProviderReferenceMapShadowAnalyzer.LatestResult is { } useShadowResult) + { + symbolsToRemove = Measure("PostProcessor.Remove.UseShadowCandidates", () => + GetSymbolsByName(definitions.DeclaredSymbols, useShadowResult.RemoveCandidates).ToArray()); + referencedSet = Measure("PostProcessor.Remove.BuildShadowReferencedSet", () => + new HashSet(definitions.DeclaredSymbols.Except(symbolsToRemove), SymbolEqualityComparer.Default)); + } + else + { + // build reference map + var referenceMap = + await MeasureAsync( + "PostProcessor.Remove.BuildAllReferenceMapAsync", + () => new ReferenceMapBuilder(compilation, project).BuildAllReferenceMapAsync( + definitions.DeclaredSymbols, definitions.DocumentsCache)); + // get root symbols + var rootSymbols = await MeasureAsync("PostProcessor.Remove.GetRootSymbolsAsync", () => GetRootSymbolsAsync(project, definitions)); + // include model factory as a root symbol when doing the remove pass so that we are sure to include any internal + // helpers that are required by the model factory. + if (_modelFactorySymbol != null) + rootSymbols.Add(_modelFactorySymbol); + // traverse the map to determine the declarations that we are about to remove, starting from root nodes + var referencedSymbols = Measure("PostProcessor.Remove.VisitSymbolsFromRoot", () => VisitSymbolsFromRootAsync(rootSymbols, referenceMap).ToArray().AsEnumerable()); + + referencedSymbols = Measure("PostProcessor.Remove.AddSampleSymbols", () => AddSampleSymbols(referencedSymbols, definitions.DeclaredSymbols)); + referencedSet = Measure("PostProcessor.Remove.BuildReferencedSet", () => new HashSet(referencedSymbols, SymbolEqualityComparer.Default)); + + symbolsToRemove = definitions.DeclaredSymbols.Except(referencedSet); + } + + if (ProviderReferenceMapShadowAnalyzer.LatestResult is { } shadowResult) + { + ProviderReferenceMapShadowAnalyzer.WriteComparisonReport( + "remove", + symbolsToRemove.Select(static symbol => symbol.GetFullyQualifiedName()), + shadowResult.RemoveCandidates); + } var nodesToRemove = Measure("PostProcessor.Remove.CollectNodes", () => { @@ -358,6 +395,17 @@ private static IEnumerable GetReferencedTypes(T definition, return Enumerable.Empty(); } + private static IEnumerable GetSymbolsByName(IEnumerable symbols, HashSet names) + { + foreach (var symbol in symbols) + { + if (names.Contains(symbol.GetFullyQualifiedName())) + { + yield return symbol; + } + } + } + private Project MarkInternal(Project project, BaseTypeDeclarationSyntax declarationNode, DocumentId documentId) { var newNode = ChangeModifier(declarationNode, SyntaxKind.PublicKeyword, SyntaxKind.InternalKeyword); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs new file mode 100644 index 00000000000..7d549afb5a1 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs @@ -0,0 +1,616 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; +using Microsoft.TypeSpec.Generator.Statements; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.TypeSpec.Generator +{ + internal static class ProviderReferenceMapShadowAnalyzer + { + private const string EnableEnvironmentVariable = "TYPESPEC_PROVIDER_REFERENCE_MAP_SHADOW"; + private const string UseShadowEnvironmentVariable = "TYPESPEC_PROVIDER_REFERENCE_MAP_USE_SHADOW"; + private const string OutputDirectoryEnvironmentVariable = "TYPESPEC_PROVIDER_REFERENCE_MAP_SHADOW_DIR"; + + private static ProviderReferenceMapShadowResult? _latestResult; + + public static bool IsEnabled => string.Equals( + Environment.GetEnvironmentVariable(EnableEnvironmentVariable), + "true", + StringComparison.OrdinalIgnoreCase); + + public static ProviderReferenceMapShadowResult? LatestResult => _latestResult; + + public static bool UseShadowMap => string.Equals( + Environment.GetEnvironmentVariable(UseShadowEnvironmentVariable), + "true", + StringComparison.OrdinalIgnoreCase); + + public static void Analyze(IReadOnlyList providers, Project project) + { + if (!IsEnabled) + { + _latestResult = null; + return; + } + + var graph = BuildGraph(providers); + var customRoots = GetCustomCodeGeneratedTypeRoots(project, graph.Nodes); + var internalizeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: false); + internalizeRoots.UnionWith(customRoots); + var internalizeReachableWithoutHelpers = GetReachableTypes(internalizeRoots, graph.References); + var internalizeHelperRoots = GetHelperRootNames(providers, graph.Nodes, internalizeReachableWithoutHelpers); + internalizeRoots.UnionWith(internalizeHelperRoots); + var internalizeReachable = GetReachableTypes(internalizeRoots, graph.References); + var internalizeDeclaredNodes = GetPostProcessorDeclaredNodes(providers, graph.Nodes, publicOnly: true); + var internalizeCandidates = internalizeDeclaredNodes.Except(internalizeReachable, StringComparer.Ordinal).OrderBy(static name => name, StringComparer.Ordinal).ToArray(); + + var removeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: true); + removeRoots.UnionWith(customRoots); + var removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); + var removeHelperRoots = GetHelperRootNames(providers, graph.Nodes, removeReachableWithoutHelpers); + removeRoots.UnionWith(removeHelperRoots); + var removeReachable = GetReachableTypes(removeRoots, graph.References); + var removeDeclaredNodes = GetPostProcessorDeclaredNodes(providers, graph.Nodes, publicOnly: false); + var removeCandidates = removeDeclaredNodes.Except(removeReachable, StringComparer.Ordinal).OrderBy(static name => name, StringComparer.Ordinal).ToArray(); + + var helperRoots = internalizeHelperRoots.Concat(removeHelperRoots).ToHashSet(StringComparer.Ordinal); + + _latestResult = new ProviderReferenceMapShadowResult( + internalizeCandidates.ToHashSet(StringComparer.Ordinal), + removeCandidates.ToHashSet(StringComparer.Ordinal)); + + WriteReport(graph, customRoots, helperRoots, internalizeRoots, internalizeReachable, internalizeCandidates, removeRoots, removeReachable, removeCandidates); + } + + private static HashSet GetCustomCodeGeneratedTypeRoots(Project project, HashSet generatedTypeNames) + { + var roots = new HashSet(StringComparer.Ordinal); + var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); + if (compilation == null) + { + return roots; + } + + foreach (var document in project.Documents) + { + if (GeneratedCodeWorkspace.IsGeneratedDocument(document) || GeneratedCodeWorkspace.IsGeneratedTestDocument(document)) + { + continue; + } + + var root = document.GetSyntaxRootAsync().GetAwaiter().GetResult(); + if (root == null) + { + continue; + } + + var model = compilation.GetSemanticModel(root.SyntaxTree); + foreach (var declaration in root.DescendantNodes().OfType()) + { + AddSymbolRoot(roots, model.GetDeclaredSymbol(declaration) as ITypeSymbol, generatedTypeNames); + } + + foreach (var typeSyntax in root.DescendantNodes().OfType()) + { + AddSymbolRoot(roots, model.GetTypeInfo(typeSyntax).Type, generatedTypeNames); + } + + foreach (var objectCreation in root.DescendantNodes().OfType()) + { + AddSymbolRoot(roots, model.GetSymbolInfo(objectCreation).Symbol?.ContainingType, generatedTypeNames); + } + + foreach (var invocation in root.DescendantNodes().OfType()) + { + AddSymbolRoot(roots, model.GetSymbolInfo(invocation).Symbol?.ContainingType, generatedTypeNames); + } + } + + return roots; + } + + private static void AddSymbolRoot(HashSet roots, ITypeSymbol? symbol, HashSet generatedTypeNames) + { + if (symbol is not INamedTypeSymbol namedType) + { + return; + } + + AddMatchingName(roots, namedType.GetFullyQualifiedName(), generatedTypeNames); + foreach (var typeArgument in namedType.TypeArguments) + { + AddSymbolRoot(roots, typeArgument, generatedTypeNames); + } + } + + public static void WriteComparisonReport(string passName, IEnumerable roslynCandidates, IEnumerable providerCandidates) + { + if (!IsEnabled) + { + return; + } + + var roslynSet = roslynCandidates.ToHashSet(StringComparer.Ordinal); + var providerSet = providerCandidates.ToHashSet(StringComparer.Ordinal); + var missingFromProvider = roslynSet.Except(providerSet, StringComparer.Ordinal).OrderBy(static name => name, StringComparer.Ordinal).ToArray(); + var extraInProvider = providerSet.Except(roslynSet, StringComparer.Ordinal).OrderBy(static name => name, StringComparer.Ordinal).ToArray(); + + var directory = GetOutputDirectory(); + Directory.CreateDirectory(directory); + var path = Path.Combine(directory, $"provider-reference-map-shadow-comparison-{passName}-{DateTime.UtcNow:yyyyMMddHHmmssfff}.txt"); + var builder = new StringBuilder(); + builder.AppendLine($"Provider reference map shadow comparison: {passName}"); + builder.AppendLine($"Roslyn candidates: {roslynSet.Count}"); + builder.AppendLine($"Provider candidates: {providerSet.Count}"); + builder.AppendLine($"Missing from provider: {missingFromProvider.Length}"); + builder.AppendLine($"Extra in provider: {extraInProvider.Length}"); + builder.AppendLine(); + builder.AppendLine("Missing from provider:"); + foreach (var item in missingFromProvider) + { + builder.AppendLine($" {item}"); + } + + builder.AppendLine(); + builder.AppendLine("Extra in provider:"); + foreach (var item in extraInProvider) + { + builder.AppendLine($" {item}"); + } + + File.WriteAllText(path, builder.ToString()); + CodeModelGenerator.Instance.Emitter.Debug($"Provider reference map shadow comparison written to {path}"); + } + + private static ProviderReferenceGraph BuildGraph(IReadOnlyList providers) + { + var nodes = providers + .Select(static provider => GetProviderTypeName(provider.Type)) + .ToHashSet(StringComparer.Ordinal); + var references = nodes.ToDictionary(static name => name, _ => new HashSet(StringComparer.Ordinal), StringComparer.Ordinal); + + foreach (var provider in providers) + { + var current = GetProviderTypeName(provider.Type); + AddTypeReference(references[current], provider.Type, nodes); + AddTypeReference(references[current], provider.BaseType, nodes); + AddTypeReference(references[current], provider.DeclaringTypeProvider?.Type, nodes); + + if (IsModelFactoryProvider(provider)) + { + continue; + } + + foreach (var implementedType in provider.Implements) + { + AddTypeReference(references[current], implementedType, nodes); + } + + foreach (var nestedType in provider.NestedTypes) + { + AddTypeReference(references[current], nestedType.Type, nodes); + } + + foreach (var serializationProvider in provider.SerializationProviders) + { + AddTypeReference(references[current], serializationProvider.Type, nodes); + } + + foreach (var property in provider.Properties) + { + AddTypeReference(references[current], property.Type, nodes); + AddTypeReference(references[current], property.ExplicitInterface, nodes); + AddAttributes(references[current], property.Attributes, nodes); + } + + foreach (var field in provider.Fields) + { + AddTypeReference(references[current], field.Type, nodes); + AddAttributes(references[current], field.Attributes, nodes); + } + + foreach (var constructor in provider.Constructors) + { + AddSignatureReferences(references[current], constructor.Signature, nodes); + } + + foreach (var method in provider.Methods) + { + AddSignatureReferences(references[current], method.Signature, nodes); + } + } + + return new ProviderReferenceGraph(nodes, references); + } + + private static HashSet GetRootNames(IReadOnlyList providers, HashSet nodes, HashSet helperRoots, bool includeModelFactory) + { + var generator = CodeModelGenerator.Instance; + var roots = new HashSet(StringComparer.Ordinal); + var modelFactoryName = GetProviderTypeName(generator.OutputLibrary.ModelFactory.Value.Type); + + foreach (var provider in providers) + { + var name = GetProviderTypeName(provider.Type); + if (provider.Name.EndsWith("Client", StringComparison.Ordinal) || + IsKept(provider.Type, generator.AdditionalRootTypes, nodes) || + includeModelFactory && string.Equals(name, modelFactoryName, StringComparison.Ordinal) || + includeModelFactory && helperRoots.Contains(name)) + { + roots.Add(name); + } + } + + foreach (var root in generator.TypeFactory.UnionVariantTypesToKeep) + { + AddMatchingName(roots, root, nodes); + } + + foreach (var root in generator.AdditionalRootTypes) + { + AddMatchingName(roots, root, nodes); + } + + return roots; + } + + private static HashSet GetPostProcessorDeclaredNodes(IReadOnlyList providers, HashSet nodes, bool publicOnly) + { + var generator = CodeModelGenerator.Instance; + var excludedNames = generator.NonRootTypes; + return providers + .Where(provider => !IsModelFactoryProvider(provider)) + .Where(provider => !publicOnly || provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) + .Select(provider => GetProviderTypeName(provider.Type)) + .Where(name => nodes.Contains(name)) + .Where(name => !excludedNames.Contains(name) && !excludedNames.Contains(GetSimpleName(name))) + .ToHashSet(StringComparer.Ordinal); + } + + private static bool IsKept(CSharpType type, HashSet roots, HashSet nodes) => + roots.Contains(type.Name) || roots.Contains(GetProviderTypeName(type)) && nodes.Contains(GetProviderTypeName(type)); + + private static bool IsModelFactoryProvider(TypeProvider provider) => provider.GetType().Name == "ModelFactoryProvider"; + + private static HashSet GetHelperRootNames(IReadOnlyList providers, HashSet nodes, HashSet reachableTypes) + { + var roots = new HashSet(StringComparer.Ordinal); + foreach (var provider in providers) + { + var providerName = GetProviderTypeName(provider.Type); + var isModelFactory = IsModelFactoryProvider(provider); + if (!reachableTypes.Contains(providerName) && !isModelFactory) + { + continue; + } + + foreach (var property in provider.Properties) + { + AddInitializationHelperRoot(roots, property.Type, nodes); + AddParameterValidationHelperRoot(roots, property.AsParameter, nodes); + } + + foreach (var field in provider.Fields) + { + AddParameterValidationHelperRoot(roots, field.AsParameter, nodes); + } + + foreach (var constructor in provider.Constructors) + { + foreach (var parameter in constructor.Signature.Parameters) + { + AddParameterValidationHelperRoot(roots, parameter, nodes); + } + } + + foreach (var method in provider.Methods) + { + if (isModelFactory && + (method.Signature.ReturnType == null || !reachableTypes.Contains(GetProviderTypeName(method.Signature.ReturnType)))) + { + continue; + } + + foreach (var parameter in method.Signature.Parameters) + { + AddParameterValidationHelperRoot(roots, parameter, nodes); + if (isModelFactory) + { + AddModelFactoryCollectionInitializationHelperRoot(roots, parameter.Type, nodes); + } + } + } + } + + return roots; + } + + private static void AddParameterValidationHelperRoot(HashSet roots, ParameterProvider parameter, HashSet nodes) + { + if (parameter.Validation != ParameterValidationType.None) + { + AddMatchingName(roots, "Argument", nodes); + } + } + + private static void AddInitializationHelperRoot(HashSet roots, CSharpType? type, HashSet nodes) + { + if (type == null) + { + return; + } + + var initializationType = type.PropertyInitializationType; + if (!string.Equals(initializationType.FullyQualifiedName, type.FullyQualifiedName, StringComparison.Ordinal)) + { + AddMatchingName(roots, initializationType.Name, nodes); + } + + if (type is { IsList: true, IsReadOnlyMemory: false }) + { + AddMatchingName(roots, "ChangeTrackingList", nodes); + } + + foreach (var argument in type.Arguments) + { + AddInitializationHelperRoot(roots, argument, nodes); + } + } + + private static void AddModelFactoryCollectionInitializationHelperRoot(HashSet roots, CSharpType? type, HashSet nodes) + { + if (type == null) + { + return; + } + + if (type is { IsList: true, IsReadOnlyMemory: false }) + { + AddMatchingName(roots, "ChangeTrackingList", nodes); + } + + if (type.IsDictionary) + { + AddMatchingName(roots, "ChangeTrackingDictionary", nodes); + } + + foreach (var argument in type.Arguments) + { + AddModelFactoryCollectionInitializationHelperRoot(roots, argument, nodes); + } + } + + private static void AddMatchingName(HashSet target, string name, HashSet nodes) + { + if (nodes.Contains(name)) + { + target.Add(name); + return; + } + + foreach (var node in nodes) + { + if (string.Equals(StripGenericArity(GetSimpleName(node)), name, StringComparison.Ordinal)) + { + target.Add(node); + } + } + } + + private static HashSet GetReachableTypes(HashSet roots, IReadOnlyDictionary> references) + { + var reachable = new HashSet(StringComparer.Ordinal); + var queue = new Queue(roots); + while (queue.Count > 0) + { + var current = queue.Dequeue(); + if (!reachable.Add(current)) + { + continue; + } + + if (!references.TryGetValue(current, out var children)) + { + continue; + } + + foreach (var child in children) + { + queue.Enqueue(child); + } + } + + return reachable; + } + + private static void AddSignatureReferences(HashSet references, MethodSignatureBase signature, HashSet nodes) + { + AddTypeReference(references, signature.ReturnType, nodes); + AddAttributes(references, signature.Attributes, nodes); + + foreach (var parameter in signature.Parameters) + { + AddTypeReference(references, parameter.Type, nodes); + AddAttributes(references, parameter.Attributes, nodes); + } + + if (signature is MethodSignature methodSignature) + { + AddTypeReference(references, methodSignature.ExplicitInterface, nodes); + if (methodSignature.GenericArguments != null) + { + foreach (var genericArgument in methodSignature.GenericArguments) + { + AddTypeReference(references, genericArgument, nodes); + } + } + + if (methodSignature.GenericParameterConstraints != null) + { + foreach (var constraint in methodSignature.GenericParameterConstraints) + { + AddTypeReference(references, constraint.Type, nodes); + } + } + } + + if (signature is ConstructorSignature constructorSignature) + { + AddTypeReference(references, constructorSignature.Type, nodes); + } + } + + private static void AddAttributes(HashSet references, IReadOnlyList attributes, HashSet nodes) + { + foreach (var attribute in attributes) + { + AddTypeReference(references, attribute.Type, nodes); + } + } + + private static void AddTypeReference(HashSet references, CSharpType? type, HashSet nodes) + { + if (type == null) + { + return; + } + + var providerTypeName = GetProviderTypeName(type); + if (nodes.Contains(providerTypeName)) + { + references.Add(providerTypeName); + } + + AddTypeReference(references, type.BaseType, nodes); + AddTypeReference(references, type.DeclaringType, nodes); + foreach (var argument in type.Arguments) + { + AddTypeReference(references, argument, nodes); + } + } + + private static void WriteReport( + ProviderReferenceGraph graph, + HashSet customRoots, + HashSet helperRoots, + HashSet internalizeRoots, + HashSet internalizeReachable, + IReadOnlyList internalizeCandidates, + HashSet removeRoots, + HashSet removeReachable, + IReadOnlyList removeCandidates) + { + var directory = GetOutputDirectory(); + + Directory.CreateDirectory(directory); + var path = Path.Combine(directory, $"provider-reference-map-shadow-{DateTime.UtcNow:yyyyMMddHHmmssfff}.txt"); + var builder = new StringBuilder(); + builder.AppendLine("Provider reference map shadow report"); + builder.AppendLine($"Declared providers: {graph.Nodes.Count}"); + builder.AppendLine($"Internalize roots: {internalizeRoots.Count}"); + builder.AppendLine($"Internalize reachable: {internalizeReachable.Count}"); + builder.AppendLine($"Internalize candidates: {internalizeCandidates.Count}"); + builder.AppendLine($"Custom roots: {customRoots.Count}"); + builder.AppendLine($"Helper roots: {helperRoots.Count}"); + builder.AppendLine($"Remove roots: {removeRoots.Count}"); + builder.AppendLine($"Remove reachable: {removeReachable.Count}"); + builder.AppendLine($"Remove candidates: {removeCandidates.Count}"); + builder.AppendLine(); + builder.AppendLine("Custom roots:"); + foreach (var root in customRoots.OrderBy(static name => name, StringComparer.Ordinal)) + { + builder.AppendLine($" {root}"); + } + + builder.AppendLine(); + builder.AppendLine("Helper roots:"); + foreach (var root in helperRoots.OrderBy(static name => name, StringComparer.Ordinal)) + { + builder.AppendLine($" {root}"); + } + + builder.AppendLine(); + builder.AppendLine("Internalize roots:"); + foreach (var root in internalizeRoots.OrderBy(static name => name, StringComparer.Ordinal)) + { + builder.AppendLine($" {root}"); + } + + builder.AppendLine(); + builder.AppendLine("Internalize candidates:"); + foreach (var candidate in internalizeCandidates) + { + builder.AppendLine($" {candidate}"); + } + + builder.AppendLine(); + builder.AppendLine("Remove roots:"); + foreach (var root in removeRoots.OrderBy(static name => name, StringComparer.Ordinal)) + { + builder.AppendLine($" {root}"); + } + + builder.AppendLine(); + builder.AppendLine("Remove candidates:"); + foreach (var candidate in removeCandidates) + { + builder.AppendLine($" {candidate}"); + } + + builder.AppendLine(); + builder.AppendLine("References:"); + foreach (var (type, references) in graph.References.OrderBy(static item => item.Key, StringComparer.Ordinal)) + { + builder.AppendLine($" {type}"); + foreach (var reference in references.OrderBy(static name => name, StringComparer.Ordinal)) + { + builder.AppendLine($" -> {reference}"); + } + } + + File.WriteAllText(path, builder.ToString()); + CodeModelGenerator.Instance.Emitter.Debug($"Provider reference map shadow report written to {path}"); + } + + private static string GetSimpleName(string fullyQualifiedName) + { + var lastDot = fullyQualifiedName.LastIndexOf('.'); + return lastDot < 0 ? fullyQualifiedName : fullyQualifiedName.Substring(lastDot + 1); + } + + private static string GetProviderTypeName(CSharpType type) + { + var name = type.Arguments.Count > 0 && !type.Name.Contains('`', StringComparison.Ordinal) + ? $"{type.Name}`{type.Arguments.Count}" + : type.Name; + return string.IsNullOrEmpty(type.Namespace) ? name : $"{type.Namespace}.{name}"; + } + + private static string StripGenericArity(string name) + { + var tick = name.IndexOf('`'); + return tick < 0 ? name : name.Substring(0, tick); + } + + private sealed record ProviderReferenceGraph( + HashSet Nodes, + Dictionary> References); + + private static string GetOutputDirectory() + { + var directory = Environment.GetEnvironmentVariable(OutputDirectoryEnvironmentVariable); + return string.IsNullOrWhiteSpace(directory) + ? Path.Combine(Path.GetTempPath(), "typespec-provider-reference-map-shadow") + : Path.GetFullPath(directory); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowResult.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowResult.cs new file mode 100644 index 00000000000..14b68f7d187 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowResult.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace Microsoft.TypeSpec.Generator +{ + internal sealed record ProviderReferenceMapShadowResult( + HashSet InternalizeCandidates, + HashSet RemoveCandidates) + { + public static ProviderReferenceMapShadowResult Empty { get; } = new([], []); + } +} From ff555094d2ee43fb2a9f4d0c2aa7cab0ebcac0d3 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 11 Jun 2026 14:50:14 +0000 Subject: [PATCH 015/111] Profile C# post-processing benchmarks --- .../perf/FullGenerationBenchmark.cs | 157 +++++++++++++++ .../src/CSharpGen.cs | 162 +++++++++++---- .../PostProcessing/GeneratedCodeWorkspace.cs | 112 +++++++++-- ...ratedCodeWorkspacePostProcessingProfile.cs | 64 ++++++ .../src/PostProcessing/PostProcessor.cs | 186 +++++++++++++----- 5 files changed, 572 insertions(+), 109 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/FullGenerationBenchmark.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspacePostProcessingProfile.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/FullGenerationBenchmark.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/FullGenerationBenchmark.cs new file mode 100644 index 00000000000..66553e08c38 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/FullGenerationBenchmark.cs @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; + +namespace Microsoft.TypeSpec.Generator.Perf +{ + public class FullGenerationBenchmark + { + private const string ProfileEnvironmentVariable = "POSTPROCESSING_BENCHMARK_PROFILE_STEPS"; + private const string ProfileOutputDirectoryEnvironmentVariable = "POSTPROCESSING_BENCHMARK_PROFILE_DIR"; + + private bool _profileSteps; + + [GlobalSetup] + public void GlobalSetup() + { + _profileSteps = string.Equals( + Environment.GetEnvironmentVariable(ProfileEnvironmentVariable), + "true", + StringComparison.OrdinalIgnoreCase); + } + + [Benchmark] + public async Task GenerateSampleTypeSpecProject() + { + var postProcessingProfile = _profileSteps ? new GeneratedCodeWorkspacePostProcessingProfile() : null; + var generationProfile = _profileSteps ? new GeneratedCodeWorkspacePostProcessingProfile() : null; + GeneratedCodeWorkspace.PostProcessingProfile = postProcessingProfile; + CSharpGen.GenerationProfile = generationProfile; + + var benchmarkDirectory = CreateBenchmarkInputDirectory(); + var stopwatch = Stopwatch.StartNew(); + try + { + CodeModelGenerator.Instance = new BenchmarkCodeModelGenerator(benchmarkDirectory); + CodeModelGenerator.Instance.Configure(); + + var csharpGen = new CSharpGen(); + await csharpGen.ExecuteAsync(); + + return Directory.GetFiles(benchmarkDirectory, "*", SearchOption.AllDirectories) + .Where(static path => !path.EndsWith("tspCodeModel.json", StringComparison.Ordinal) && + !path.EndsWith("Configuration.json", StringComparison.Ordinal)) + .Sum(static path => (int)new FileInfo(path).Length); + } + finally + { + stopwatch.Stop(); + if (generationProfile != null) + { + WriteProfile( + generationProfile, + $"full-generation-profile-{DateTime.UtcNow:yyyyMMddHHmmssfff}.csv", + $"Full generation invocation elapsed ms: {stopwatch.Elapsed.TotalMilliseconds:F3}{Environment.NewLine}" + + $"Input directory: {benchmarkDirectory}{Environment.NewLine}"); + } + + if (postProcessingProfile != null) + { + WriteProfile( + postProcessingProfile, + $"full-generation-post-processing-profile-{DateTime.UtcNow:yyyyMMddHHmmssfff}.csv", + $"Full generation post-processing profile{Environment.NewLine}" + + $"Input directory: {benchmarkDirectory}{Environment.NewLine}"); + } + + CSharpGen.GenerationProfile = null; + GeneratedCodeWorkspace.PostProcessingProfile = null; + TryDeleteDirectory(benchmarkDirectory); + } + } + + private static void WriteProfile(GeneratedCodeWorkspacePostProcessingProfile profile, string fileName, string header) + { + var profileDirectory = GetProfileOutputDirectory(); + Directory.CreateDirectory(profileDirectory); + File.WriteAllText(Path.Combine(profileDirectory, fileName), header + profile.GetSummary()); + } + + private static string CreateBenchmarkInputDirectory() + { + var sourceDirectory = FindFullGenerationInputDirectory(); + var benchmarkDirectory = Path.Combine(Path.GetTempPath(), "typespec-full-generation-benchmark", Guid.NewGuid().ToString("N")); + CopyDirectory(sourceDirectory, benchmarkDirectory); + return benchmarkDirectory; + } + + private static string FindFullGenerationInputDirectory() + { + const string relativePath = "packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec"; + + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory != null) + { + var inputDirectory = Path.Combine(directory.FullName, relativePath); + if (File.Exists(Path.Combine(inputDirectory, "tspCodeModel.json")) && + File.Exists(Path.Combine(inputDirectory, "Configuration.json"))) + { + return inputDirectory; + } + + directory = directory.Parent; + } + + throw new DirectoryNotFoundException($"Could not find '{relativePath}' from '{AppContext.BaseDirectory}'."); + } + + private static void CopyDirectory(string sourceDirectory, string destinationDirectory) + { + Directory.CreateDirectory(destinationDirectory); + foreach (var sourceFile in Directory.GetFiles(sourceDirectory, "*", SearchOption.AllDirectories)) + { + var relativePath = Path.GetRelativePath(sourceDirectory, sourceFile); + var destinationFile = Path.Combine(destinationDirectory, relativePath); + Directory.CreateDirectory(Path.GetDirectoryName(destinationFile)!); + File.Copy(sourceFile, destinationFile, overwrite: true); + } + } + + private static void TryDeleteDirectory(string directory) + { + try + { + if (Directory.Exists(directory)) + { + Directory.Delete(directory, recursive: true); + } + } + catch + { + // Best-effort cleanup for benchmark temp output. + } + } + + private static string GetProfileOutputDirectory() + { + var configuredPath = Environment.GetEnvironmentVariable(ProfileOutputDirectoryEnvironmentVariable); + return string.IsNullOrWhiteSpace(configuredPath) + ? Path.Combine(Path.GetTempPath(), "typespec-post-processing-profiles") + : Path.GetFullPath(configuredPath); + } + + private sealed class BenchmarkCodeModelGenerator : CodeModelGenerator + { + public BenchmarkCodeModelGenerator(string outputPath) + : base(new GeneratorContext(Configuration.Load(outputPath))) + { + } + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index 9948fcff594..238b16b9766 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -20,6 +21,8 @@ internal sealed class CSharpGen private static readonly string[] _filesToKeep = [ConfigurationFileName, CodeModelFileName]; + internal static GeneratedCodeWorkspacePostProcessingProfile? GenerationProfile { get; set; } + /// /// Executes the generator task with the instance. /// @@ -33,19 +36,21 @@ public async Task ExecuteAsync() // Resolve PackageReference items from the .csproj so custom code referencing // external NuGet types (e.g., Azure.Storage.Common) compiles correctly. - await GeneratedCodeWorkspace.AddPackageReferencesFromProject(); + await MeasureGenerationStepAsync("Generation.AddPackageReferencesFromProject", GeneratedCodeWorkspace.AddPackageReferencesFromProject); // Pre-walk the input library and resolve any external types that point at NuGet packages. // This populates ExternalTypeReferenceResolver's cache and registers each resolved assembly // as an additional metadata reference *before* the generated/custom code workspaces are // constructed, so their cached Roslyn projects pick the references up. - await ExternalTypeReferenceResolver.ResolveAllAsync(); + await MeasureGenerationStepAsync("Generation.ResolveExternalTypeReferences", ExternalTypeReferenceResolver.ResolveAllAsync); // Initialize the workspace project AFTER all metadata references have been added so the // eagerly-cached project sees them. GeneratedCodeWorkspace.Initialize(); - GeneratedCodeWorkspace customCodeWorkspace = await GeneratedCodeWorkspace.Create(isCustomCodeProject: true); + GeneratedCodeWorkspace customCodeWorkspace = await MeasureGenerationStepAsync( + "Generation.CreateCustomCodeWorkspace", + () => GeneratedCodeWorkspace.Create(isCustomCodeProject: true)); // The generated attributes need to be added into the workspace before loading the custom code. Otherwise, // Roslyn doesn't load the attributes completely and we are unable to get the attribute arguments. @@ -55,88 +60,167 @@ public async Task ExecuteAsync() generateAttributeTasks.Add(customCodeWorkspace.AddInMemoryFile(attributeProvider)); } - await Task.WhenAll(generateAttributeTasks); + await MeasureGenerationStepAsync("Generation.AddCustomizationAttributeProviders", () => Task.WhenAll(generateAttributeTasks)); - CodeModelGenerator.Instance.SourceInputModel = new SourceInputModel( - await customCodeWorkspace.GetCompilationAsync(), - await GeneratedCodeWorkspace.LoadBaselineContract()); + CodeModelGenerator.Instance.SourceInputModel = await MeasureGenerationStepAsync( + "Generation.CreateSourceInputModel", + async () => new SourceInputModel( + await customCodeWorkspace.GetCompilationAsync(), + await GeneratedCodeWorkspace.LoadBaselineContract())); - GeneratedCodeWorkspace generatedCodeWorkspace = await GeneratedCodeWorkspace.Create(isCustomCodeProject: false); + GeneratedCodeWorkspace generatedCodeWorkspace = await MeasureGenerationStepAsync( + "Generation.CreateGeneratedCodeWorkspace", + () => GeneratedCodeWorkspace.Create(isCustomCodeProject: false)); - var output = CodeModelGenerator.Instance.OutputLibrary; + var output = MeasureGenerationStep("Generation.GetOutputLibrary", () => CodeModelGenerator.Instance.OutputLibrary); Directory.CreateDirectory(Path.Combine(generatedSourceOutputPath, "Models")); List generateFilesTasks = new(); // Build all TypeProviders - foreach (var type in output.TypeProviders) + MeasureGenerationStep("Generation.BuildTypeProviders", () => { - type.EnsureBuilt(); - } + foreach (var type in output.TypeProviders) + { + type.EnsureBuilt(); + } + }); LoggingHelpers.LogElapsedTime("All generated type providers built"); // visit the entire library before generating files - foreach (var visitor in CodeModelGenerator.Instance.Visitors) + MeasureGenerationStep("Generation.ApplyVisitors", () => { - visitor.VisitLibrary(output); - } + foreach (var visitor in CodeModelGenerator.Instance.Visitors) + { + visitor.VisitLibrary(output); + } + }); - FilterAllCustomizedMembers(output); + MeasureGenerationStep("Generation.FilterCustomizedMembers", () => FilterAllCustomizedMembers(output)); LoggingHelpers.LogElapsedTime("All visitors have been applied"); - foreach (var outputType in output.TypeProviders) + MeasureGenerationStep("Generation.WriteTypeProviders", () => { - // Ensure back-compatibility processing is done after all visitors have run - outputType.ProcessTypeForBackCompatibility(); - - var writer = CodeModelGenerator.Instance.GetWriter(outputType); - generateFilesTasks.Add(generatedCodeWorkspace.AddGeneratedFile(writer.Write())); - - foreach (var serialization in outputType.SerializationProviders) + foreach (var outputType in output.TypeProviders) { - writer = CodeModelGenerator.Instance.GetWriter(serialization); + // Ensure back-compatibility processing is done after all visitors have run + outputType.ProcessTypeForBackCompatibility(); + + var writer = CodeModelGenerator.Instance.GetWriter(outputType); generateFilesTasks.Add(generatedCodeWorkspace.AddGeneratedFile(writer.Write())); + + foreach (var serialization in outputType.SerializationProviders) + { + writer = CodeModelGenerator.Instance.GetWriter(serialization); + generateFilesTasks.Add(generatedCodeWorkspace.AddGeneratedFile(writer.Write())); + } } - } + }); // Add all the generated files to the workspace - await Task.WhenAll(generateFilesTasks); + await MeasureGenerationStepAsync("Generation.AddGeneratedFilesToWorkspace", () => Task.WhenAll(generateFilesTasks)); LoggingHelpers.LogElapsedTime("All generated types have been written into memory"); // Delete any old generated files - DeleteDirectory(generatedSourceOutputPath, _filesToKeep); + MeasureGenerationStep("Generation.DeleteOldGeneratedFiles", () => DeleteDirectory(generatedSourceOutputPath, _filesToKeep)); LoggingHelpers.LogElapsedTime("All old generated files have been deleted"); - await generatedCodeWorkspace.PostProcessAsync(); + await MeasureGenerationStepAsync("Generation.PostProcessAsync", generatedCodeWorkspace.PostProcessAsync); // Write the generated files to the output directory - await foreach (var file in generatedCodeWorkspace.GetGeneratedFilesAsync()) + await MeasureGenerationStepAsync("Generation.WriteGeneratedFilesToDisk", async () => { - if (string.IsNullOrEmpty(file.Text)) + await foreach (var file in generatedCodeWorkspace.GetGeneratedFilesAsync()) { - continue; + if (string.IsNullOrEmpty(file.Text)) + { + continue; + } + var filename = Path.Combine(outputPath, file.Name); + CodeModelGenerator.Instance.Emitter.Info($"Writing {Path.GetFullPath(filename)}"); + Directory.CreateDirectory(Path.GetDirectoryName(filename)!); + await File.WriteAllTextAsync(filename, file.Text); } - var filename = Path.Combine(outputPath, file.Name); - CodeModelGenerator.Instance.Emitter.Info($"Writing {Path.GetFullPath(filename)}"); - Directory.CreateDirectory(Path.GetDirectoryName(filename)!); - await File.WriteAllTextAsync(filename, file.Text); - } + }); // Write additional output files (e.g. configuration schemas, .targets files) - await CodeModelGenerator.Instance.WriteAdditionalFiles(outputPath); + await MeasureGenerationStepAsync("Generation.WriteAdditionalFiles", () => CodeModelGenerator.Instance.WriteAdditionalFiles(outputPath)); // Write project scaffolding files (after additional files so schema existence can be checked) if (CodeModelGenerator.Instance.IsNewProject) { - await CodeModelGenerator.Instance.TypeFactory.CreateNewProjectScaffolding().Execute(); + await MeasureGenerationStepAsync( + "Generation.WriteProjectScaffolding", + () => CodeModelGenerator.Instance.TypeFactory.CreateNewProjectScaffolding().Execute()); } LoggingHelpers.LogElapsedTime("All files have been written to disk"); } + private static void MeasureGenerationStep(string stepName, Action action) + { + MeasureGenerationStep( + stepName, + () => + { + action(); + return 0; + }); + } + + private static T MeasureGenerationStep(string stepName, Func action) + { + var profile = GenerationProfile; + if (profile == null) + { + return action(); + } + + var allocatedBytes = GC.GetTotalAllocatedBytes(precise: false); + var stopwatch = Stopwatch.StartNew(); + try + { + return action(); + } + finally + { + stopwatch.Stop(); + profile.Add(stepName, stopwatch.Elapsed, GC.GetTotalAllocatedBytes(precise: false) - allocatedBytes); + } + } + + private static Task MeasureGenerationStepAsync(string stepName, Func action) => MeasureGenerationStepAsync( + stepName, + async () => + { + await action(); + return 0; + }); + + private static async Task MeasureGenerationStepAsync(string stepName, Func> action) + { + var profile = GenerationProfile; + if (profile == null) + { + return await action(); + } + + var allocatedBytes = GC.GetTotalAllocatedBytes(precise: false); + var stopwatch = Stopwatch.StartNew(); + try + { + return await action(); + } + finally + { + stopwatch.Stop(); + profile.Add(stepName, stopwatch.Elapsed, GC.GetTotalAllocatedBytes(precise: false) - allocatedBytes); + } + } + internal static void FilterAllCustomizedMembers(OutputLibrary output) { foreach (var typeProvider in output.TypeProviders) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index 4588b3c4839..281d4ec2e5e 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -37,6 +37,8 @@ internal class GeneratedCodeWorkspace private static readonly Lazy _metadataReferenceResolver = new(() => new WorkspaceMetadataReferenceResolver()); private static Task? _cachedProject; + internal static GeneratedCodeWorkspacePostProcessingProfile? PostProcessingProfile { get; set; } + private static readonly string[] _generatedFolders = [GeneratedFolder]; private static readonly string[] _sharedFolders = [SharedFolder]; @@ -102,7 +104,7 @@ internal static SyntaxTree GetTree(TypeProvider provider) public async IAsyncEnumerable<(string Name, string Text)> GetGeneratedFilesAsync() { - List> documents = new List>(); + List docs = new List(); var memberRemover = new MemberRemoverRewriter(); foreach (Document document in _project.Documents) { @@ -111,9 +113,12 @@ internal static SyntaxTree GetTree(TypeProvider provider) continue; } - documents.Add(ProcessDocument(document, memberRemover)); + docs.Add(document); } - var docs = await Task.WhenAll(documents); + + docs = PostProcessingProfile == null + ? [.. await Task.WhenAll(docs.Select(document => ProcessDocument(document, memberRemover)))] + : await ProcessDocumentsSequentiallyAsync(docs, memberRemover); LoggingHelpers.LogElapsedTime("Roslyn post processing complete"); @@ -129,36 +134,101 @@ internal static SyntaxTree GetTree(TypeProvider provider) } } - private async Task ProcessDocument(Document document, MemberRemoverRewriter memberRemover) + private async Task> ProcessDocumentsSequentiallyAsync(List documents, MemberRemoverRewriter memberRemover) { - var root = await document.GetSyntaxRootAsync(); - var semanticModel = await document.GetSemanticModelAsync(); + List processedDocuments = new(documents.Count); + foreach (var document in documents) + { + processedDocuments.Add(await ProcessDocument(document, memberRemover)); + } - if (semanticModel == null || root == null) + return processedDocuments; + } + + private async Task ProcessDocument(Document document, MemberRemoverRewriter memberRemover) + { + var totalStopwatch = PostProcessingProfile == null ? null : Stopwatch.StartNew(); + try { + var root = await MeasurePostProcessingStepAsync("GetSyntaxRootAsync", () => document.GetSyntaxRootAsync()); + var semanticModel = await MeasurePostProcessingStepAsync("GetSemanticModelAsync", () => document.GetSemanticModelAsync()); + + if (semanticModel == null || root == null) + { + return document; + } + + root = MeasurePostProcessingStep("MemberRemoverRewriter", () => memberRemover.Visit(root)); + + foreach (var rewriter in CodeModelGenerator.Instance.Rewriters) + { + rewriter.SemanticModel = semanticModel; + root = MeasurePostProcessingStep($"CustomRewriter.{rewriter.GetType().Name}", () => rewriter.Visit(root)); + } + document = document.WithSyntaxRoot(root); + + if (!CodeModelGenerator.Instance.Configuration.DisableRoslynReduce) + { + document = await MeasurePostProcessingStepAsync("Roslyn.Simplifier.ReduceAsync", () => Simplifier.ReduceAsync(document)); + } + + // Reformat if any custom rewriters have been applied + if (CodeModelGenerator.Instance.Rewriters.Count > 0) + { + document = await MeasurePostProcessingStepAsync("Formatter.FormatAsync", () => Formatter.FormatAsync(document)); + } return document; } + finally + { + if (totalStopwatch != null) + { + totalStopwatch.Stop(); + PostProcessingProfile?.Add("ProcessDocument.Total", totalStopwatch.Elapsed, 0); + } + } + } - root = memberRemover.Visit(root); + private static T MeasurePostProcessingStep(string stepName, Func action) + { + var profile = PostProcessingProfile; + if (profile == null) + { + return action(); + } - foreach (var rewriter in CodeModelGenerator.Instance.Rewriters) + var allocatedBytes = GC.GetTotalAllocatedBytes(precise: false); + var stopwatch = Stopwatch.StartNew(); + try { - rewriter.SemanticModel = semanticModel; - root = rewriter.Visit(root); + return action(); } - document = document.WithSyntaxRoot(root); + finally + { + stopwatch.Stop(); + profile.Add(stepName, stopwatch.Elapsed, GC.GetTotalAllocatedBytes(precise: false) - allocatedBytes); + } + } - if (!CodeModelGenerator.Instance.Configuration.DisableRoslynReduce) + private static async Task MeasurePostProcessingStepAsync(string stepName, Func> action) + { + var profile = PostProcessingProfile; + if (profile == null) { - document = await Simplifier.ReduceAsync(document); + return await action(); } - // Reformat if any custom rewriters have been applied - if (CodeModelGenerator.Instance.Rewriters.Count > 0) + var allocatedBytes = GC.GetTotalAllocatedBytes(precise: false); + var stopwatch = Stopwatch.StartNew(); + try + { + return await action(); + } + finally { - document = await Formatter.FormatAsync(document); + stopwatch.Stop(); + profile.Add(stepName, stopwatch.Elapsed, GC.GetTotalAllocatedBytes(precise: false) - allocatedBytes); } - return document; } public static bool IsGeneratedDocument(Document document) => document.Folders.Contains(GeneratedFolder); @@ -275,11 +345,11 @@ public async Task PostProcessAsync() case Configuration.UnreferencedTypesHandlingOption.KeepAll: break; case Configuration.UnreferencedTypesHandlingOption.Internalize: - _project = await postProcessor.InternalizeAsync(_project); + _project = await MeasurePostProcessingStepAsync("PostProcess.InternalizeAsync", () => postProcessor.InternalizeAsync(_project)); break; case Configuration.UnreferencedTypesHandlingOption.RemoveOrInternalize: - _project = await postProcessor.InternalizeAsync(_project); - _project = await postProcessor.RemoveAsync(_project); + _project = await MeasurePostProcessingStepAsync("PostProcess.InternalizeAsync", () => postProcessor.InternalizeAsync(_project)); + _project = await MeasurePostProcessingStepAsync("PostProcess.RemoveAsync", () => postProcessor.RemoveAsync(_project)); break; } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspacePostProcessingProfile.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspacePostProcessingProfile.cs new file mode 100644 index 00000000000..74a9a58168a --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspacePostProcessingProfile.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; + +namespace Microsoft.TypeSpec.Generator +{ + internal sealed class GeneratedCodeWorkspacePostProcessingProfile + { + private readonly object _syncRoot = new(); + private readonly Dictionary _steps = new(StringComparer.Ordinal); + + public void Add(string stepName, TimeSpan elapsed, long allocatedBytes) + { + lock (_syncRoot) + { + ref var summary = ref CollectionsMarshal.GetValueRefOrAddDefault(_steps, stepName, out _); + summary.Count++; + summary.ElapsedTicks += elapsed.Ticks; + summary.AllocatedBytes += allocatedBytes; + } + } + + public string GetSummary() + { + KeyValuePair[] steps; + lock (_syncRoot) + { + steps = _steps.ToArray(); + } + + var totalTicks = steps + .Where(static step => step.Key != "ProcessDocument.Total") + .Sum(static step => step.Value.ElapsedTicks); + var builder = new StringBuilder(); + builder.AppendLine("Post-processing step profile:"); + builder.AppendLine("Step, Count, Total ms, Avg ms, Percent of measured steps, Allocated bytes, Avg allocated bytes"); + + foreach (var step in steps.OrderByDescending(static step => step.Value.ElapsedTicks)) + { + var elapsedMs = TimeSpan.FromTicks(step.Value.ElapsedTicks).TotalMilliseconds; + var averageMs = elapsedMs / step.Value.Count; + var averageAllocatedBytes = step.Value.AllocatedBytes / step.Value.Count; + var percentage = totalTicks == 0 || step.Key == "ProcessDocument.Total" + ? 0 + : step.Value.ElapsedTicks * 100.0 / totalTicks; + builder.AppendLine($"{step.Key}, {step.Value.Count}, {elapsedMs:F3}, {averageMs:F3}, {percentage:F1}%, {step.Value.AllocatedBytes}, {averageAllocatedBytes}"); + } + + return builder.ToString(); + } + + private struct StepSummary + { + public int Count; + public long ElapsedTicks; + public long AllocatedBytes; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs index be96e11df59..d5db7e47298 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs @@ -20,6 +20,8 @@ internal class PostProcessor private readonly HashSet _typesToKeep; private INamedTypeSymbol? _modelFactorySymbol; + private static GeneratedCodeWorkspacePostProcessingProfile? Profile => GeneratedCodeWorkspace.PostProcessingProfile; + public PostProcessor( HashSet typesToKeep, string? modelFactoryFullName = null, @@ -125,40 +127,55 @@ protected virtual bool ShouldIncludeDocument(Document document) => /// The processed . is immutable, therefore this should usually be a new instance public async Task InternalizeAsync(Project project) { - var compilation = await project.GetCompilationAsync(); + var compilation = await MeasureAsync("PostProcessor.Internalize.GetCompilationAsync", () => project.GetCompilationAsync()); if (compilation == null) return project; // first get all the declared symbols - var definitions = await GetTypeSymbolsAsync(compilation, project, true); + var definitions = await MeasureAsync("PostProcessor.Internalize.GetTypeSymbolsAsync", () => GetTypeSymbolsAsync(compilation, project, true)); // build the reference map var referenceMap = - await new ReferenceMapBuilder(compilation, project).BuildPublicReferenceMapAsync( - definitions.DeclaredSymbols, definitions.DeclaredNodesCache); + await MeasureAsync( + "PostProcessor.Internalize.BuildPublicReferenceMapAsync", + () => new ReferenceMapBuilder(compilation, project).BuildPublicReferenceMapAsync( + definitions.DeclaredSymbols, definitions.DeclaredNodesCache)); // get the root symbols - var rootSymbols = await GetRootSymbolsAsync(project, definitions); + var rootSymbols = await MeasureAsync("PostProcessor.Internalize.GetRootSymbolsAsync", () => GetRootSymbolsAsync(project, definitions)); // traverse all the root and recursively add all the things we met - var publicSymbols = VisitSymbolsFromRootAsync(rootSymbols, referenceMap); + var publicSymbols = Measure("PostProcessor.Internalize.VisitSymbolsFromRoot", () => VisitSymbolsFromRootAsync(rootSymbols, referenceMap).ToArray()); var symbolsToInternalize = definitions.DeclaredSymbols.Except(publicSymbols); - var nodesToInternalize = new Dictionary(); - foreach (var symbol in symbolsToInternalize) + var nodesToInternalize = Measure("PostProcessor.Internalize.CollectNodes", () => { - foreach (var node in definitions.DeclaredNodesCache[symbol]) + var nodes = new Dictionary(); + foreach (var symbol in symbolsToInternalize) { - nodesToInternalize[node] = project.GetDocumentId(node.SyntaxTree)!; + foreach (var node in definitions.DeclaredNodesCache[symbol]) + { + nodes[node] = project.GetDocumentId(node.SyntaxTree)!; + } } - } - foreach (var (model, documentId) in nodesToInternalize) + return nodes; + }); + + project = Measure("PostProcessor.Internalize.MarkInternal", () => { - project = MarkInternal(project, model, documentId); - } + var updatedProject = project; + foreach (var (model, documentId) in nodesToInternalize) + { + updatedProject = MarkInternal(updatedProject, model, documentId); + } + + return updatedProject; + }); var modelNamesToRemove = nodesToInternalize.Keys.Select(item => item.Identifier.Text); - project = await RemoveMethodsFromModelFactoryAsync(project, definitions, modelNamesToRemove.ToHashSet()); + project = await MeasureAsync( + "PostProcessor.Internalize.RemoveMethodsFromModelFactoryAsync", + () => RemoveMethodsFromModelFactoryAsync(project, definitions, modelNamesToRemove.ToHashSet())); return project; } @@ -232,42 +249,49 @@ private async Task RemoveMethodsFromModelFactoryAsync(Project project, /// The processed . is immutable, therefore this should usually be a new instance public async Task RemoveAsync(Project project) { - var compilation = await project.GetCompilationAsync(); + var compilation = await MeasureAsync("PostProcessor.Remove.GetCompilationAsync", () => project.GetCompilationAsync()); if (compilation == null) return project; // find all the declarations, including non-public declared - var definitions = await GetTypeSymbolsAsync(compilation, project, false); + var definitions = await MeasureAsync("PostProcessor.Remove.GetTypeSymbolsAsync", () => GetTypeSymbolsAsync(compilation, project, false)); // build reference map var referenceMap = - await new ReferenceMapBuilder(compilation, project).BuildAllReferenceMapAsync( - definitions.DeclaredSymbols, definitions.DocumentsCache); + await MeasureAsync( + "PostProcessor.Remove.BuildAllReferenceMapAsync", + () => new ReferenceMapBuilder(compilation, project).BuildAllReferenceMapAsync( + definitions.DeclaredSymbols, definitions.DocumentsCache)); // get root symbols - var rootSymbols = await GetRootSymbolsAsync(project, definitions); + var rootSymbols = await MeasureAsync("PostProcessor.Remove.GetRootSymbolsAsync", () => GetRootSymbolsAsync(project, definitions)); // include model factory as a root symbol when doing the remove pass so that we are sure to include any internal // helpers that are required by the model factory. if (_modelFactorySymbol != null) rootSymbols.Add(_modelFactorySymbol); // traverse the map to determine the declarations that we are about to remove, starting from root nodes - var referencedSymbols = VisitSymbolsFromRootAsync(rootSymbols, referenceMap); + var referencedSymbols = Measure("PostProcessor.Remove.VisitSymbolsFromRoot", () => VisitSymbolsFromRootAsync(rootSymbols, referenceMap).ToArray().AsEnumerable()); - referencedSymbols = AddSampleSymbols(referencedSymbols, definitions.DeclaredSymbols); - var referencedSet = new HashSet(referencedSymbols, SymbolEqualityComparer.Default); + referencedSymbols = Measure("PostProcessor.Remove.AddSampleSymbols", () => AddSampleSymbols(referencedSymbols, definitions.DeclaredSymbols)); + var referencedSet = Measure("PostProcessor.Remove.BuildReferencedSet", () => new HashSet(referencedSymbols, SymbolEqualityComparer.Default)); var symbolsToRemove = definitions.DeclaredSymbols.Except(referencedSet); - var nodesToRemove = new List(); - foreach (var symbol in symbolsToRemove) + var nodesToRemove = Measure("PostProcessor.Remove.CollectNodes", () => { - if (referencedSet.Contains(GetBase(symbol))) + var nodes = new List(); + foreach (var symbol in symbolsToRemove) { - continue; + if (referencedSet.Contains(GetBase(symbol))) + { + continue; + } + nodes.AddRange(definitions.DeclaredNodesCache[symbol]); } - nodesToRemove.AddRange(definitions.DeclaredNodesCache[symbol]); - } + + return nodes; + }); // remove them one by one - project = await RemoveModelsAsync(project, nodesToRemove); + project = await MeasureAsync("PostProcessor.Remove.RemoveModelsAsync", () => RemoveModelsAsync(project, nodesToRemove)); return project; } @@ -349,25 +373,35 @@ private async Task RemoveModelsAsync(Project project, IEnumerable unusedModels) { // accumulate the definitions from the same document together - var documents = new Dictionary>(); - - foreach (var model in unusedModels) + var documents = Measure("PostProcessor.Remove.RemoveModelsAsync.GroupByDocument", () => { - var document = project.GetDocument(model.SyntaxTree); - Debug.Assert(document != null); - if (!documents.ContainsKey(document)) - documents.Add(document, new HashSet()); + var groupedDocuments = new Dictionary>(); + foreach (var model in unusedModels) + { + var document = project.GetDocument(model.SyntaxTree); + Debug.Assert(document != null); + if (!groupedDocuments.ContainsKey(document)) + groupedDocuments.Add(document, new HashSet()); - documents[document].Add(model); - } + groupedDocuments[document].Add(model); + } - foreach (var models in documents.Values) + return groupedDocuments; + }); + + project = await MeasureAsync("PostProcessor.Remove.RemoveModelsAsync.RemoveModelsFromDocuments", async () => { - project = await RemoveModelsFromDocumentAsync(project, models); - } + var updatedProject = project; + foreach (var models in documents.Values) + { + updatedProject = await RemoveModelsFromDocumentAsync(updatedProject, models); + } + + return updatedProject; + }); // remove what are now invalid references due to the models being removed - project = await RemoveInvalidRefs(project); + project = await MeasureAsync("PostProcessor.Remove.RemoveModelsAsync.RemoveInvalidRefs", () => RemoveInvalidRefs(project)); return project; } @@ -418,16 +452,28 @@ private async Task RemoveInvalidRefs(Project project) var solution = project.Solution; // Process each document for invalid usings - foreach (var documentId in project.DocumentIds) + solution = await MeasureAsync("PostProcessor.Remove.RemoveInvalidRefs.RemoveInvalidUsings", async () => { - solution = await RemoveInvalidUsings(solution, documentId); - } + var updatedSolution = solution; + foreach (var documentId in project.DocumentIds) + { + updatedSolution = await RemoveInvalidUsings(updatedSolution, documentId); + } + + return updatedSolution; + }); // Process each document for invalid attributes (with fresh semantic models) - foreach (var documentId in project.DocumentIds) + solution = await MeasureAsync("PostProcessor.Remove.RemoveInvalidRefs.RemoveInvalidAttributes", async () => { - solution = await RemoveInvalidAttributes(solution, documentId); - } + var updatedSolution = solution; + foreach (var documentId in project.DocumentIds) + { + updatedSolution = await RemoveInvalidAttributes(updatedSolution, documentId); + } + + return updatedSolution; + }); return solution.GetProject(project.Id)!; } @@ -533,6 +579,48 @@ arg.Expression is TypeOfExpressionSyntax typeOfExpr && return solution; } + private static T Measure(string stepName, Func action) + { + var profile = Profile; + if (profile == null) + { + return action(); + } + + var allocatedBytes = GC.GetTotalAllocatedBytes(precise: false); + var stopwatch = Stopwatch.StartNew(); + try + { + return action(); + } + finally + { + stopwatch.Stop(); + profile.Add(stepName, stopwatch.Elapsed, GC.GetTotalAllocatedBytes(precise: false) - allocatedBytes); + } + } + + private static async Task MeasureAsync(string stepName, Func> action) + { + var profile = Profile; + if (profile == null) + { + return await action(); + } + + var allocatedBytes = GC.GetTotalAllocatedBytes(precise: false); + var stopwatch = Stopwatch.StartNew(); + try + { + return await action(); + } + finally + { + stopwatch.Stop(); + profile.Add(stepName, stopwatch.Elapsed, GC.GetTotalAllocatedBytes(precise: false) - allocatedBytes); + } + } + private async Task> GetRootSymbolsAsync(Project project, TypeSymbols modelSymbols) { var result = new HashSet(SymbolEqualityComparer.Default); From f022e78e26c3b645cdd3cf506cba931303429179 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Fri, 12 Jun 2026 04:22:34 +0000 Subject: [PATCH 016/111] Add provider reference map shadow replacement --- .../src/CSharpGen.cs | 2 + .../PostProcessing/GeneratedCodeWorkspace.cs | 5 + .../src/PostProcessing/PostProcessor.cs | 110 +++- .../ProviderReferenceMapShadowAnalyzer.cs | 616 ++++++++++++++++++ .../ProviderReferenceMapShadowResult.cs | 14 + 5 files changed, 716 insertions(+), 31 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowResult.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index 238b16b9766..fdd3ae2191f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs @@ -121,6 +121,8 @@ await customCodeWorkspace.GetCompilationAsync(), // Add all the generated files to the workspace await MeasureGenerationStepAsync("Generation.AddGeneratedFilesToWorkspace", () => Task.WhenAll(generateFilesTasks)); + MeasureGenerationStep("Generation.ProviderReferenceMapShadowAnalysis", () => generatedCodeWorkspace.AnalyzeProviderReferenceMap(output.TypeProviders)); + LoggingHelpers.LogElapsedTime("All generated types have been written into memory"); // Delete any old generated files diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index 281d4ec2e5e..07e8595be08 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -85,6 +85,11 @@ public async Task AddInMemoryFile(TypeProvider type) await UpdateProject(document); } + internal void AnalyzeProviderReferenceMap(IReadOnlyList providers) + { + ProviderReferenceMapShadowAnalyzer.Analyze(providers, _project); + } + private async Task UpdateProject(Document document) { var root = await document.GetSyntaxRootAsync(); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs index d5db7e47298..57136754da1 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs @@ -133,18 +133,35 @@ public async Task InternalizeAsync(Project project) // first get all the declared symbols var definitions = await MeasureAsync("PostProcessor.Internalize.GetTypeSymbolsAsync", () => GetTypeSymbolsAsync(compilation, project, true)); - // build the reference map - var referenceMap = - await MeasureAsync( - "PostProcessor.Internalize.BuildPublicReferenceMapAsync", - () => new ReferenceMapBuilder(compilation, project).BuildPublicReferenceMapAsync( - definitions.DeclaredSymbols, definitions.DeclaredNodesCache)); - // get the root symbols - var rootSymbols = await MeasureAsync("PostProcessor.Internalize.GetRootSymbolsAsync", () => GetRootSymbolsAsync(project, definitions)); - // traverse all the root and recursively add all the things we met - var publicSymbols = Measure("PostProcessor.Internalize.VisitSymbolsFromRoot", () => VisitSymbolsFromRootAsync(rootSymbols, referenceMap).ToArray()); - - var symbolsToInternalize = definitions.DeclaredSymbols.Except(publicSymbols); + IEnumerable symbolsToInternalize; + if (ProviderReferenceMapShadowAnalyzer.UseShadowMap && ProviderReferenceMapShadowAnalyzer.LatestResult is { } useShadowResult) + { + symbolsToInternalize = Measure("PostProcessor.Internalize.UseShadowCandidates", () => + GetSymbolsByName(definitions.DeclaredSymbols, useShadowResult.InternalizeCandidates).ToArray()); + } + else + { + // build the reference map + var referenceMap = + await MeasureAsync( + "PostProcessor.Internalize.BuildPublicReferenceMapAsync", + () => new ReferenceMapBuilder(compilation, project).BuildPublicReferenceMapAsync( + definitions.DeclaredSymbols, definitions.DeclaredNodesCache)); + // get the root symbols + var rootSymbols = await MeasureAsync("PostProcessor.Internalize.GetRootSymbolsAsync", () => GetRootSymbolsAsync(project, definitions)); + // traverse all the root and recursively add all the things we met + var publicSymbols = Measure("PostProcessor.Internalize.VisitSymbolsFromRoot", () => VisitSymbolsFromRootAsync(rootSymbols, referenceMap).ToArray()); + + symbolsToInternalize = definitions.DeclaredSymbols.Except(publicSymbols); + } + + if (ProviderReferenceMapShadowAnalyzer.LatestResult is { } shadowResult) + { + ProviderReferenceMapShadowAnalyzer.WriteComparisonReport( + "internalize", + symbolsToInternalize.Select(static symbol => symbol.GetFullyQualifiedName()), + shadowResult.InternalizeCandidates); + } var nodesToInternalize = Measure("PostProcessor.Internalize.CollectNodes", () => { @@ -255,25 +272,45 @@ public async Task RemoveAsync(Project project) // find all the declarations, including non-public declared var definitions = await MeasureAsync("PostProcessor.Remove.GetTypeSymbolsAsync", () => GetTypeSymbolsAsync(compilation, project, false)); - // build reference map - var referenceMap = - await MeasureAsync( - "PostProcessor.Remove.BuildAllReferenceMapAsync", - () => new ReferenceMapBuilder(compilation, project).BuildAllReferenceMapAsync( - definitions.DeclaredSymbols, definitions.DocumentsCache)); - // get root symbols - var rootSymbols = await MeasureAsync("PostProcessor.Remove.GetRootSymbolsAsync", () => GetRootSymbolsAsync(project, definitions)); - // include model factory as a root symbol when doing the remove pass so that we are sure to include any internal - // helpers that are required by the model factory. - if (_modelFactorySymbol != null) - rootSymbols.Add(_modelFactorySymbol); - // traverse the map to determine the declarations that we are about to remove, starting from root nodes - var referencedSymbols = Measure("PostProcessor.Remove.VisitSymbolsFromRoot", () => VisitSymbolsFromRootAsync(rootSymbols, referenceMap).ToArray().AsEnumerable()); - - referencedSymbols = Measure("PostProcessor.Remove.AddSampleSymbols", () => AddSampleSymbols(referencedSymbols, definitions.DeclaredSymbols)); - var referencedSet = Measure("PostProcessor.Remove.BuildReferencedSet", () => new HashSet(referencedSymbols, SymbolEqualityComparer.Default)); - - var symbolsToRemove = definitions.DeclaredSymbols.Except(referencedSet); + IEnumerable symbolsToRemove; + HashSet referencedSet; + if (ProviderReferenceMapShadowAnalyzer.UseShadowMap && ProviderReferenceMapShadowAnalyzer.LatestResult is { } useShadowResult) + { + symbolsToRemove = Measure("PostProcessor.Remove.UseShadowCandidates", () => + GetSymbolsByName(definitions.DeclaredSymbols, useShadowResult.RemoveCandidates).ToArray()); + referencedSet = Measure("PostProcessor.Remove.BuildShadowReferencedSet", () => + new HashSet(definitions.DeclaredSymbols.Except(symbolsToRemove), SymbolEqualityComparer.Default)); + } + else + { + // build reference map + var referenceMap = + await MeasureAsync( + "PostProcessor.Remove.BuildAllReferenceMapAsync", + () => new ReferenceMapBuilder(compilation, project).BuildAllReferenceMapAsync( + definitions.DeclaredSymbols, definitions.DocumentsCache)); + // get root symbols + var rootSymbols = await MeasureAsync("PostProcessor.Remove.GetRootSymbolsAsync", () => GetRootSymbolsAsync(project, definitions)); + // include model factory as a root symbol when doing the remove pass so that we are sure to include any internal + // helpers that are required by the model factory. + if (_modelFactorySymbol != null) + rootSymbols.Add(_modelFactorySymbol); + // traverse the map to determine the declarations that we are about to remove, starting from root nodes + var referencedSymbols = Measure("PostProcessor.Remove.VisitSymbolsFromRoot", () => VisitSymbolsFromRootAsync(rootSymbols, referenceMap).ToArray().AsEnumerable()); + + referencedSymbols = Measure("PostProcessor.Remove.AddSampleSymbols", () => AddSampleSymbols(referencedSymbols, definitions.DeclaredSymbols)); + referencedSet = Measure("PostProcessor.Remove.BuildReferencedSet", () => new HashSet(referencedSymbols, SymbolEqualityComparer.Default)); + + symbolsToRemove = definitions.DeclaredSymbols.Except(referencedSet); + } + + if (ProviderReferenceMapShadowAnalyzer.LatestResult is { } shadowResult) + { + ProviderReferenceMapShadowAnalyzer.WriteComparisonReport( + "remove", + symbolsToRemove.Select(static symbol => symbol.GetFullyQualifiedName()), + shadowResult.RemoveCandidates); + } var nodesToRemove = Measure("PostProcessor.Remove.CollectNodes", () => { @@ -358,6 +395,17 @@ private static IEnumerable GetReferencedTypes(T definition, return Enumerable.Empty(); } + private static IEnumerable GetSymbolsByName(IEnumerable symbols, HashSet names) + { + foreach (var symbol in symbols) + { + if (names.Contains(symbol.GetFullyQualifiedName())) + { + yield return symbol; + } + } + } + private Project MarkInternal(Project project, BaseTypeDeclarationSyntax declarationNode, DocumentId documentId) { var newNode = ChangeModifier(declarationNode, SyntaxKind.PublicKeyword, SyntaxKind.InternalKeyword); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs new file mode 100644 index 00000000000..7d549afb5a1 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs @@ -0,0 +1,616 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; +using Microsoft.TypeSpec.Generator.Statements; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.TypeSpec.Generator +{ + internal static class ProviderReferenceMapShadowAnalyzer + { + private const string EnableEnvironmentVariable = "TYPESPEC_PROVIDER_REFERENCE_MAP_SHADOW"; + private const string UseShadowEnvironmentVariable = "TYPESPEC_PROVIDER_REFERENCE_MAP_USE_SHADOW"; + private const string OutputDirectoryEnvironmentVariable = "TYPESPEC_PROVIDER_REFERENCE_MAP_SHADOW_DIR"; + + private static ProviderReferenceMapShadowResult? _latestResult; + + public static bool IsEnabled => string.Equals( + Environment.GetEnvironmentVariable(EnableEnvironmentVariable), + "true", + StringComparison.OrdinalIgnoreCase); + + public static ProviderReferenceMapShadowResult? LatestResult => _latestResult; + + public static bool UseShadowMap => string.Equals( + Environment.GetEnvironmentVariable(UseShadowEnvironmentVariable), + "true", + StringComparison.OrdinalIgnoreCase); + + public static void Analyze(IReadOnlyList providers, Project project) + { + if (!IsEnabled) + { + _latestResult = null; + return; + } + + var graph = BuildGraph(providers); + var customRoots = GetCustomCodeGeneratedTypeRoots(project, graph.Nodes); + var internalizeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: false); + internalizeRoots.UnionWith(customRoots); + var internalizeReachableWithoutHelpers = GetReachableTypes(internalizeRoots, graph.References); + var internalizeHelperRoots = GetHelperRootNames(providers, graph.Nodes, internalizeReachableWithoutHelpers); + internalizeRoots.UnionWith(internalizeHelperRoots); + var internalizeReachable = GetReachableTypes(internalizeRoots, graph.References); + var internalizeDeclaredNodes = GetPostProcessorDeclaredNodes(providers, graph.Nodes, publicOnly: true); + var internalizeCandidates = internalizeDeclaredNodes.Except(internalizeReachable, StringComparer.Ordinal).OrderBy(static name => name, StringComparer.Ordinal).ToArray(); + + var removeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: true); + removeRoots.UnionWith(customRoots); + var removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); + var removeHelperRoots = GetHelperRootNames(providers, graph.Nodes, removeReachableWithoutHelpers); + removeRoots.UnionWith(removeHelperRoots); + var removeReachable = GetReachableTypes(removeRoots, graph.References); + var removeDeclaredNodes = GetPostProcessorDeclaredNodes(providers, graph.Nodes, publicOnly: false); + var removeCandidates = removeDeclaredNodes.Except(removeReachable, StringComparer.Ordinal).OrderBy(static name => name, StringComparer.Ordinal).ToArray(); + + var helperRoots = internalizeHelperRoots.Concat(removeHelperRoots).ToHashSet(StringComparer.Ordinal); + + _latestResult = new ProviderReferenceMapShadowResult( + internalizeCandidates.ToHashSet(StringComparer.Ordinal), + removeCandidates.ToHashSet(StringComparer.Ordinal)); + + WriteReport(graph, customRoots, helperRoots, internalizeRoots, internalizeReachable, internalizeCandidates, removeRoots, removeReachable, removeCandidates); + } + + private static HashSet GetCustomCodeGeneratedTypeRoots(Project project, HashSet generatedTypeNames) + { + var roots = new HashSet(StringComparer.Ordinal); + var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); + if (compilation == null) + { + return roots; + } + + foreach (var document in project.Documents) + { + if (GeneratedCodeWorkspace.IsGeneratedDocument(document) || GeneratedCodeWorkspace.IsGeneratedTestDocument(document)) + { + continue; + } + + var root = document.GetSyntaxRootAsync().GetAwaiter().GetResult(); + if (root == null) + { + continue; + } + + var model = compilation.GetSemanticModel(root.SyntaxTree); + foreach (var declaration in root.DescendantNodes().OfType()) + { + AddSymbolRoot(roots, model.GetDeclaredSymbol(declaration) as ITypeSymbol, generatedTypeNames); + } + + foreach (var typeSyntax in root.DescendantNodes().OfType()) + { + AddSymbolRoot(roots, model.GetTypeInfo(typeSyntax).Type, generatedTypeNames); + } + + foreach (var objectCreation in root.DescendantNodes().OfType()) + { + AddSymbolRoot(roots, model.GetSymbolInfo(objectCreation).Symbol?.ContainingType, generatedTypeNames); + } + + foreach (var invocation in root.DescendantNodes().OfType()) + { + AddSymbolRoot(roots, model.GetSymbolInfo(invocation).Symbol?.ContainingType, generatedTypeNames); + } + } + + return roots; + } + + private static void AddSymbolRoot(HashSet roots, ITypeSymbol? symbol, HashSet generatedTypeNames) + { + if (symbol is not INamedTypeSymbol namedType) + { + return; + } + + AddMatchingName(roots, namedType.GetFullyQualifiedName(), generatedTypeNames); + foreach (var typeArgument in namedType.TypeArguments) + { + AddSymbolRoot(roots, typeArgument, generatedTypeNames); + } + } + + public static void WriteComparisonReport(string passName, IEnumerable roslynCandidates, IEnumerable providerCandidates) + { + if (!IsEnabled) + { + return; + } + + var roslynSet = roslynCandidates.ToHashSet(StringComparer.Ordinal); + var providerSet = providerCandidates.ToHashSet(StringComparer.Ordinal); + var missingFromProvider = roslynSet.Except(providerSet, StringComparer.Ordinal).OrderBy(static name => name, StringComparer.Ordinal).ToArray(); + var extraInProvider = providerSet.Except(roslynSet, StringComparer.Ordinal).OrderBy(static name => name, StringComparer.Ordinal).ToArray(); + + var directory = GetOutputDirectory(); + Directory.CreateDirectory(directory); + var path = Path.Combine(directory, $"provider-reference-map-shadow-comparison-{passName}-{DateTime.UtcNow:yyyyMMddHHmmssfff}.txt"); + var builder = new StringBuilder(); + builder.AppendLine($"Provider reference map shadow comparison: {passName}"); + builder.AppendLine($"Roslyn candidates: {roslynSet.Count}"); + builder.AppendLine($"Provider candidates: {providerSet.Count}"); + builder.AppendLine($"Missing from provider: {missingFromProvider.Length}"); + builder.AppendLine($"Extra in provider: {extraInProvider.Length}"); + builder.AppendLine(); + builder.AppendLine("Missing from provider:"); + foreach (var item in missingFromProvider) + { + builder.AppendLine($" {item}"); + } + + builder.AppendLine(); + builder.AppendLine("Extra in provider:"); + foreach (var item in extraInProvider) + { + builder.AppendLine($" {item}"); + } + + File.WriteAllText(path, builder.ToString()); + CodeModelGenerator.Instance.Emitter.Debug($"Provider reference map shadow comparison written to {path}"); + } + + private static ProviderReferenceGraph BuildGraph(IReadOnlyList providers) + { + var nodes = providers + .Select(static provider => GetProviderTypeName(provider.Type)) + .ToHashSet(StringComparer.Ordinal); + var references = nodes.ToDictionary(static name => name, _ => new HashSet(StringComparer.Ordinal), StringComparer.Ordinal); + + foreach (var provider in providers) + { + var current = GetProviderTypeName(provider.Type); + AddTypeReference(references[current], provider.Type, nodes); + AddTypeReference(references[current], provider.BaseType, nodes); + AddTypeReference(references[current], provider.DeclaringTypeProvider?.Type, nodes); + + if (IsModelFactoryProvider(provider)) + { + continue; + } + + foreach (var implementedType in provider.Implements) + { + AddTypeReference(references[current], implementedType, nodes); + } + + foreach (var nestedType in provider.NestedTypes) + { + AddTypeReference(references[current], nestedType.Type, nodes); + } + + foreach (var serializationProvider in provider.SerializationProviders) + { + AddTypeReference(references[current], serializationProvider.Type, nodes); + } + + foreach (var property in provider.Properties) + { + AddTypeReference(references[current], property.Type, nodes); + AddTypeReference(references[current], property.ExplicitInterface, nodes); + AddAttributes(references[current], property.Attributes, nodes); + } + + foreach (var field in provider.Fields) + { + AddTypeReference(references[current], field.Type, nodes); + AddAttributes(references[current], field.Attributes, nodes); + } + + foreach (var constructor in provider.Constructors) + { + AddSignatureReferences(references[current], constructor.Signature, nodes); + } + + foreach (var method in provider.Methods) + { + AddSignatureReferences(references[current], method.Signature, nodes); + } + } + + return new ProviderReferenceGraph(nodes, references); + } + + private static HashSet GetRootNames(IReadOnlyList providers, HashSet nodes, HashSet helperRoots, bool includeModelFactory) + { + var generator = CodeModelGenerator.Instance; + var roots = new HashSet(StringComparer.Ordinal); + var modelFactoryName = GetProviderTypeName(generator.OutputLibrary.ModelFactory.Value.Type); + + foreach (var provider in providers) + { + var name = GetProviderTypeName(provider.Type); + if (provider.Name.EndsWith("Client", StringComparison.Ordinal) || + IsKept(provider.Type, generator.AdditionalRootTypes, nodes) || + includeModelFactory && string.Equals(name, modelFactoryName, StringComparison.Ordinal) || + includeModelFactory && helperRoots.Contains(name)) + { + roots.Add(name); + } + } + + foreach (var root in generator.TypeFactory.UnionVariantTypesToKeep) + { + AddMatchingName(roots, root, nodes); + } + + foreach (var root in generator.AdditionalRootTypes) + { + AddMatchingName(roots, root, nodes); + } + + return roots; + } + + private static HashSet GetPostProcessorDeclaredNodes(IReadOnlyList providers, HashSet nodes, bool publicOnly) + { + var generator = CodeModelGenerator.Instance; + var excludedNames = generator.NonRootTypes; + return providers + .Where(provider => !IsModelFactoryProvider(provider)) + .Where(provider => !publicOnly || provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) + .Select(provider => GetProviderTypeName(provider.Type)) + .Where(name => nodes.Contains(name)) + .Where(name => !excludedNames.Contains(name) && !excludedNames.Contains(GetSimpleName(name))) + .ToHashSet(StringComparer.Ordinal); + } + + private static bool IsKept(CSharpType type, HashSet roots, HashSet nodes) => + roots.Contains(type.Name) || roots.Contains(GetProviderTypeName(type)) && nodes.Contains(GetProviderTypeName(type)); + + private static bool IsModelFactoryProvider(TypeProvider provider) => provider.GetType().Name == "ModelFactoryProvider"; + + private static HashSet GetHelperRootNames(IReadOnlyList providers, HashSet nodes, HashSet reachableTypes) + { + var roots = new HashSet(StringComparer.Ordinal); + foreach (var provider in providers) + { + var providerName = GetProviderTypeName(provider.Type); + var isModelFactory = IsModelFactoryProvider(provider); + if (!reachableTypes.Contains(providerName) && !isModelFactory) + { + continue; + } + + foreach (var property in provider.Properties) + { + AddInitializationHelperRoot(roots, property.Type, nodes); + AddParameterValidationHelperRoot(roots, property.AsParameter, nodes); + } + + foreach (var field in provider.Fields) + { + AddParameterValidationHelperRoot(roots, field.AsParameter, nodes); + } + + foreach (var constructor in provider.Constructors) + { + foreach (var parameter in constructor.Signature.Parameters) + { + AddParameterValidationHelperRoot(roots, parameter, nodes); + } + } + + foreach (var method in provider.Methods) + { + if (isModelFactory && + (method.Signature.ReturnType == null || !reachableTypes.Contains(GetProviderTypeName(method.Signature.ReturnType)))) + { + continue; + } + + foreach (var parameter in method.Signature.Parameters) + { + AddParameterValidationHelperRoot(roots, parameter, nodes); + if (isModelFactory) + { + AddModelFactoryCollectionInitializationHelperRoot(roots, parameter.Type, nodes); + } + } + } + } + + return roots; + } + + private static void AddParameterValidationHelperRoot(HashSet roots, ParameterProvider parameter, HashSet nodes) + { + if (parameter.Validation != ParameterValidationType.None) + { + AddMatchingName(roots, "Argument", nodes); + } + } + + private static void AddInitializationHelperRoot(HashSet roots, CSharpType? type, HashSet nodes) + { + if (type == null) + { + return; + } + + var initializationType = type.PropertyInitializationType; + if (!string.Equals(initializationType.FullyQualifiedName, type.FullyQualifiedName, StringComparison.Ordinal)) + { + AddMatchingName(roots, initializationType.Name, nodes); + } + + if (type is { IsList: true, IsReadOnlyMemory: false }) + { + AddMatchingName(roots, "ChangeTrackingList", nodes); + } + + foreach (var argument in type.Arguments) + { + AddInitializationHelperRoot(roots, argument, nodes); + } + } + + private static void AddModelFactoryCollectionInitializationHelperRoot(HashSet roots, CSharpType? type, HashSet nodes) + { + if (type == null) + { + return; + } + + if (type is { IsList: true, IsReadOnlyMemory: false }) + { + AddMatchingName(roots, "ChangeTrackingList", nodes); + } + + if (type.IsDictionary) + { + AddMatchingName(roots, "ChangeTrackingDictionary", nodes); + } + + foreach (var argument in type.Arguments) + { + AddModelFactoryCollectionInitializationHelperRoot(roots, argument, nodes); + } + } + + private static void AddMatchingName(HashSet target, string name, HashSet nodes) + { + if (nodes.Contains(name)) + { + target.Add(name); + return; + } + + foreach (var node in nodes) + { + if (string.Equals(StripGenericArity(GetSimpleName(node)), name, StringComparison.Ordinal)) + { + target.Add(node); + } + } + } + + private static HashSet GetReachableTypes(HashSet roots, IReadOnlyDictionary> references) + { + var reachable = new HashSet(StringComparer.Ordinal); + var queue = new Queue(roots); + while (queue.Count > 0) + { + var current = queue.Dequeue(); + if (!reachable.Add(current)) + { + continue; + } + + if (!references.TryGetValue(current, out var children)) + { + continue; + } + + foreach (var child in children) + { + queue.Enqueue(child); + } + } + + return reachable; + } + + private static void AddSignatureReferences(HashSet references, MethodSignatureBase signature, HashSet nodes) + { + AddTypeReference(references, signature.ReturnType, nodes); + AddAttributes(references, signature.Attributes, nodes); + + foreach (var parameter in signature.Parameters) + { + AddTypeReference(references, parameter.Type, nodes); + AddAttributes(references, parameter.Attributes, nodes); + } + + if (signature is MethodSignature methodSignature) + { + AddTypeReference(references, methodSignature.ExplicitInterface, nodes); + if (methodSignature.GenericArguments != null) + { + foreach (var genericArgument in methodSignature.GenericArguments) + { + AddTypeReference(references, genericArgument, nodes); + } + } + + if (methodSignature.GenericParameterConstraints != null) + { + foreach (var constraint in methodSignature.GenericParameterConstraints) + { + AddTypeReference(references, constraint.Type, nodes); + } + } + } + + if (signature is ConstructorSignature constructorSignature) + { + AddTypeReference(references, constructorSignature.Type, nodes); + } + } + + private static void AddAttributes(HashSet references, IReadOnlyList attributes, HashSet nodes) + { + foreach (var attribute in attributes) + { + AddTypeReference(references, attribute.Type, nodes); + } + } + + private static void AddTypeReference(HashSet references, CSharpType? type, HashSet nodes) + { + if (type == null) + { + return; + } + + var providerTypeName = GetProviderTypeName(type); + if (nodes.Contains(providerTypeName)) + { + references.Add(providerTypeName); + } + + AddTypeReference(references, type.BaseType, nodes); + AddTypeReference(references, type.DeclaringType, nodes); + foreach (var argument in type.Arguments) + { + AddTypeReference(references, argument, nodes); + } + } + + private static void WriteReport( + ProviderReferenceGraph graph, + HashSet customRoots, + HashSet helperRoots, + HashSet internalizeRoots, + HashSet internalizeReachable, + IReadOnlyList internalizeCandidates, + HashSet removeRoots, + HashSet removeReachable, + IReadOnlyList removeCandidates) + { + var directory = GetOutputDirectory(); + + Directory.CreateDirectory(directory); + var path = Path.Combine(directory, $"provider-reference-map-shadow-{DateTime.UtcNow:yyyyMMddHHmmssfff}.txt"); + var builder = new StringBuilder(); + builder.AppendLine("Provider reference map shadow report"); + builder.AppendLine($"Declared providers: {graph.Nodes.Count}"); + builder.AppendLine($"Internalize roots: {internalizeRoots.Count}"); + builder.AppendLine($"Internalize reachable: {internalizeReachable.Count}"); + builder.AppendLine($"Internalize candidates: {internalizeCandidates.Count}"); + builder.AppendLine($"Custom roots: {customRoots.Count}"); + builder.AppendLine($"Helper roots: {helperRoots.Count}"); + builder.AppendLine($"Remove roots: {removeRoots.Count}"); + builder.AppendLine($"Remove reachable: {removeReachable.Count}"); + builder.AppendLine($"Remove candidates: {removeCandidates.Count}"); + builder.AppendLine(); + builder.AppendLine("Custom roots:"); + foreach (var root in customRoots.OrderBy(static name => name, StringComparer.Ordinal)) + { + builder.AppendLine($" {root}"); + } + + builder.AppendLine(); + builder.AppendLine("Helper roots:"); + foreach (var root in helperRoots.OrderBy(static name => name, StringComparer.Ordinal)) + { + builder.AppendLine($" {root}"); + } + + builder.AppendLine(); + builder.AppendLine("Internalize roots:"); + foreach (var root in internalizeRoots.OrderBy(static name => name, StringComparer.Ordinal)) + { + builder.AppendLine($" {root}"); + } + + builder.AppendLine(); + builder.AppendLine("Internalize candidates:"); + foreach (var candidate in internalizeCandidates) + { + builder.AppendLine($" {candidate}"); + } + + builder.AppendLine(); + builder.AppendLine("Remove roots:"); + foreach (var root in removeRoots.OrderBy(static name => name, StringComparer.Ordinal)) + { + builder.AppendLine($" {root}"); + } + + builder.AppendLine(); + builder.AppendLine("Remove candidates:"); + foreach (var candidate in removeCandidates) + { + builder.AppendLine($" {candidate}"); + } + + builder.AppendLine(); + builder.AppendLine("References:"); + foreach (var (type, references) in graph.References.OrderBy(static item => item.Key, StringComparer.Ordinal)) + { + builder.AppendLine($" {type}"); + foreach (var reference in references.OrderBy(static name => name, StringComparer.Ordinal)) + { + builder.AppendLine($" -> {reference}"); + } + } + + File.WriteAllText(path, builder.ToString()); + CodeModelGenerator.Instance.Emitter.Debug($"Provider reference map shadow report written to {path}"); + } + + private static string GetSimpleName(string fullyQualifiedName) + { + var lastDot = fullyQualifiedName.LastIndexOf('.'); + return lastDot < 0 ? fullyQualifiedName : fullyQualifiedName.Substring(lastDot + 1); + } + + private static string GetProviderTypeName(CSharpType type) + { + var name = type.Arguments.Count > 0 && !type.Name.Contains('`', StringComparison.Ordinal) + ? $"{type.Name}`{type.Arguments.Count}" + : type.Name; + return string.IsNullOrEmpty(type.Namespace) ? name : $"{type.Namespace}.{name}"; + } + + private static string StripGenericArity(string name) + { + var tick = name.IndexOf('`'); + return tick < 0 ? name : name.Substring(0, tick); + } + + private sealed record ProviderReferenceGraph( + HashSet Nodes, + Dictionary> References); + + private static string GetOutputDirectory() + { + var directory = Environment.GetEnvironmentVariable(OutputDirectoryEnvironmentVariable); + return string.IsNullOrWhiteSpace(directory) + ? Path.Combine(Path.GetTempPath(), "typespec-provider-reference-map-shadow") + : Path.GetFullPath(directory); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowResult.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowResult.cs new file mode 100644 index 00000000000..14b68f7d187 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowResult.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace Microsoft.TypeSpec.Generator +{ + internal sealed record ProviderReferenceMapShadowResult( + HashSet InternalizeCandidates, + HashSet RemoveCandidates) + { + public static ProviderReferenceMapShadowResult Empty { get; } = new([], []); + } +} From f88a64a9e474d389ffd8aca8c2ef4f1f97bb6f16 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Fri, 12 Jun 2026 06:31:47 +0000 Subject: [PATCH 017/111] Revert "Profile C# post-processing benchmarks" This reverts commit ff555094d2ee43fb2a9f4d0c2aa7cab0ebcac0d3. --- .../perf/FullGenerationBenchmark.cs | 157 -------------- .../src/CSharpGen.cs | 164 ++++----------- .../PostProcessing/GeneratedCodeWorkspace.cs | 112 ++-------- ...ratedCodeWorkspacePostProcessingProfile.cs | 64 ------ .../src/PostProcessing/PostProcessor.cs | 195 +++++------------- 5 files changed, 113 insertions(+), 579 deletions(-) delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/FullGenerationBenchmark.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspacePostProcessingProfile.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/FullGenerationBenchmark.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/FullGenerationBenchmark.cs deleted file mode 100644 index 66553e08c38..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/FullGenerationBenchmark.cs +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using BenchmarkDotNet.Attributes; - -namespace Microsoft.TypeSpec.Generator.Perf -{ - public class FullGenerationBenchmark - { - private const string ProfileEnvironmentVariable = "POSTPROCESSING_BENCHMARK_PROFILE_STEPS"; - private const string ProfileOutputDirectoryEnvironmentVariable = "POSTPROCESSING_BENCHMARK_PROFILE_DIR"; - - private bool _profileSteps; - - [GlobalSetup] - public void GlobalSetup() - { - _profileSteps = string.Equals( - Environment.GetEnvironmentVariable(ProfileEnvironmentVariable), - "true", - StringComparison.OrdinalIgnoreCase); - } - - [Benchmark] - public async Task GenerateSampleTypeSpecProject() - { - var postProcessingProfile = _profileSteps ? new GeneratedCodeWorkspacePostProcessingProfile() : null; - var generationProfile = _profileSteps ? new GeneratedCodeWorkspacePostProcessingProfile() : null; - GeneratedCodeWorkspace.PostProcessingProfile = postProcessingProfile; - CSharpGen.GenerationProfile = generationProfile; - - var benchmarkDirectory = CreateBenchmarkInputDirectory(); - var stopwatch = Stopwatch.StartNew(); - try - { - CodeModelGenerator.Instance = new BenchmarkCodeModelGenerator(benchmarkDirectory); - CodeModelGenerator.Instance.Configure(); - - var csharpGen = new CSharpGen(); - await csharpGen.ExecuteAsync(); - - return Directory.GetFiles(benchmarkDirectory, "*", SearchOption.AllDirectories) - .Where(static path => !path.EndsWith("tspCodeModel.json", StringComparison.Ordinal) && - !path.EndsWith("Configuration.json", StringComparison.Ordinal)) - .Sum(static path => (int)new FileInfo(path).Length); - } - finally - { - stopwatch.Stop(); - if (generationProfile != null) - { - WriteProfile( - generationProfile, - $"full-generation-profile-{DateTime.UtcNow:yyyyMMddHHmmssfff}.csv", - $"Full generation invocation elapsed ms: {stopwatch.Elapsed.TotalMilliseconds:F3}{Environment.NewLine}" + - $"Input directory: {benchmarkDirectory}{Environment.NewLine}"); - } - - if (postProcessingProfile != null) - { - WriteProfile( - postProcessingProfile, - $"full-generation-post-processing-profile-{DateTime.UtcNow:yyyyMMddHHmmssfff}.csv", - $"Full generation post-processing profile{Environment.NewLine}" + - $"Input directory: {benchmarkDirectory}{Environment.NewLine}"); - } - - CSharpGen.GenerationProfile = null; - GeneratedCodeWorkspace.PostProcessingProfile = null; - TryDeleteDirectory(benchmarkDirectory); - } - } - - private static void WriteProfile(GeneratedCodeWorkspacePostProcessingProfile profile, string fileName, string header) - { - var profileDirectory = GetProfileOutputDirectory(); - Directory.CreateDirectory(profileDirectory); - File.WriteAllText(Path.Combine(profileDirectory, fileName), header + profile.GetSummary()); - } - - private static string CreateBenchmarkInputDirectory() - { - var sourceDirectory = FindFullGenerationInputDirectory(); - var benchmarkDirectory = Path.Combine(Path.GetTempPath(), "typespec-full-generation-benchmark", Guid.NewGuid().ToString("N")); - CopyDirectory(sourceDirectory, benchmarkDirectory); - return benchmarkDirectory; - } - - private static string FindFullGenerationInputDirectory() - { - const string relativePath = "packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec"; - - var directory = new DirectoryInfo(AppContext.BaseDirectory); - while (directory != null) - { - var inputDirectory = Path.Combine(directory.FullName, relativePath); - if (File.Exists(Path.Combine(inputDirectory, "tspCodeModel.json")) && - File.Exists(Path.Combine(inputDirectory, "Configuration.json"))) - { - return inputDirectory; - } - - directory = directory.Parent; - } - - throw new DirectoryNotFoundException($"Could not find '{relativePath}' from '{AppContext.BaseDirectory}'."); - } - - private static void CopyDirectory(string sourceDirectory, string destinationDirectory) - { - Directory.CreateDirectory(destinationDirectory); - foreach (var sourceFile in Directory.GetFiles(sourceDirectory, "*", SearchOption.AllDirectories)) - { - var relativePath = Path.GetRelativePath(sourceDirectory, sourceFile); - var destinationFile = Path.Combine(destinationDirectory, relativePath); - Directory.CreateDirectory(Path.GetDirectoryName(destinationFile)!); - File.Copy(sourceFile, destinationFile, overwrite: true); - } - } - - private static void TryDeleteDirectory(string directory) - { - try - { - if (Directory.Exists(directory)) - { - Directory.Delete(directory, recursive: true); - } - } - catch - { - // Best-effort cleanup for benchmark temp output. - } - } - - private static string GetProfileOutputDirectory() - { - var configuredPath = Environment.GetEnvironmentVariable(ProfileOutputDirectoryEnvironmentVariable); - return string.IsNullOrWhiteSpace(configuredPath) - ? Path.Combine(Path.GetTempPath(), "typespec-post-processing-profiles") - : Path.GetFullPath(configuredPath); - } - - private sealed class BenchmarkCodeModelGenerator : CodeModelGenerator - { - public BenchmarkCodeModelGenerator(string outputPath) - : base(new GeneratorContext(Configuration.Load(outputPath))) - { - } - } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index fdd3ae2191f..8ae87be33d1 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -21,8 +20,6 @@ internal sealed class CSharpGen private static readonly string[] _filesToKeep = [ConfigurationFileName, CodeModelFileName]; - internal static GeneratedCodeWorkspacePostProcessingProfile? GenerationProfile { get; set; } - /// /// Executes the generator task with the instance. /// @@ -36,21 +33,19 @@ public async Task ExecuteAsync() // Resolve PackageReference items from the .csproj so custom code referencing // external NuGet types (e.g., Azure.Storage.Common) compiles correctly. - await MeasureGenerationStepAsync("Generation.AddPackageReferencesFromProject", GeneratedCodeWorkspace.AddPackageReferencesFromProject); + await GeneratedCodeWorkspace.AddPackageReferencesFromProject(); // Pre-walk the input library and resolve any external types that point at NuGet packages. // This populates ExternalTypeReferenceResolver's cache and registers each resolved assembly // as an additional metadata reference *before* the generated/custom code workspaces are // constructed, so their cached Roslyn projects pick the references up. - await MeasureGenerationStepAsync("Generation.ResolveExternalTypeReferences", ExternalTypeReferenceResolver.ResolveAllAsync); + await ExternalTypeReferenceResolver.ResolveAllAsync(); // Initialize the workspace project AFTER all metadata references have been added so the // eagerly-cached project sees them. GeneratedCodeWorkspace.Initialize(); - GeneratedCodeWorkspace customCodeWorkspace = await MeasureGenerationStepAsync( - "Generation.CreateCustomCodeWorkspace", - () => GeneratedCodeWorkspace.Create(isCustomCodeProject: true)); + GeneratedCodeWorkspace customCodeWorkspace = await GeneratedCodeWorkspace.Create(isCustomCodeProject: true); // The generated attributes need to be added into the workspace before loading the custom code. Otherwise, // Roslyn doesn't load the attributes completely and we are unable to get the attribute arguments. @@ -60,169 +55,90 @@ public async Task ExecuteAsync() generateAttributeTasks.Add(customCodeWorkspace.AddInMemoryFile(attributeProvider)); } - await MeasureGenerationStepAsync("Generation.AddCustomizationAttributeProviders", () => Task.WhenAll(generateAttributeTasks)); + await Task.WhenAll(generateAttributeTasks); - CodeModelGenerator.Instance.SourceInputModel = await MeasureGenerationStepAsync( - "Generation.CreateSourceInputModel", - async () => new SourceInputModel( - await customCodeWorkspace.GetCompilationAsync(), - await GeneratedCodeWorkspace.LoadBaselineContract())); + CodeModelGenerator.Instance.SourceInputModel = new SourceInputModel( + await customCodeWorkspace.GetCompilationAsync(), + await GeneratedCodeWorkspace.LoadBaselineContract()); - GeneratedCodeWorkspace generatedCodeWorkspace = await MeasureGenerationStepAsync( - "Generation.CreateGeneratedCodeWorkspace", - () => GeneratedCodeWorkspace.Create(isCustomCodeProject: false)); + GeneratedCodeWorkspace generatedCodeWorkspace = await GeneratedCodeWorkspace.Create(isCustomCodeProject: false); - var output = MeasureGenerationStep("Generation.GetOutputLibrary", () => CodeModelGenerator.Instance.OutputLibrary); + var output = CodeModelGenerator.Instance.OutputLibrary; Directory.CreateDirectory(Path.Combine(generatedSourceOutputPath, "Models")); List generateFilesTasks = new(); // Build all TypeProviders - MeasureGenerationStep("Generation.BuildTypeProviders", () => + foreach (var type in output.TypeProviders) { - foreach (var type in output.TypeProviders) - { - type.EnsureBuilt(); - } - }); + type.EnsureBuilt(); + } LoggingHelpers.LogElapsedTime("All generated type providers built"); // visit the entire library before generating files - MeasureGenerationStep("Generation.ApplyVisitors", () => + foreach (var visitor in CodeModelGenerator.Instance.Visitors) { - foreach (var visitor in CodeModelGenerator.Instance.Visitors) - { - visitor.VisitLibrary(output); - } - }); + visitor.VisitLibrary(output); + } - MeasureGenerationStep("Generation.FilterCustomizedMembers", () => FilterAllCustomizedMembers(output)); + FilterAllCustomizedMembers(output); LoggingHelpers.LogElapsedTime("All visitors have been applied"); - MeasureGenerationStep("Generation.WriteTypeProviders", () => + foreach (var outputType in output.TypeProviders) { - foreach (var outputType in output.TypeProviders) - { - // Ensure back-compatibility processing is done after all visitors have run - outputType.ProcessTypeForBackCompatibility(); + // Ensure back-compatibility processing is done after all visitors have run + outputType.ProcessTypeForBackCompatibility(); - var writer = CodeModelGenerator.Instance.GetWriter(outputType); - generateFilesTasks.Add(generatedCodeWorkspace.AddGeneratedFile(writer.Write())); + var writer = CodeModelGenerator.Instance.GetWriter(outputType); + generateFilesTasks.Add(generatedCodeWorkspace.AddGeneratedFile(writer.Write())); - foreach (var serialization in outputType.SerializationProviders) - { - writer = CodeModelGenerator.Instance.GetWriter(serialization); - generateFilesTasks.Add(generatedCodeWorkspace.AddGeneratedFile(writer.Write())); - } + foreach (var serialization in outputType.SerializationProviders) + { + writer = CodeModelGenerator.Instance.GetWriter(serialization); + generateFilesTasks.Add(generatedCodeWorkspace.AddGeneratedFile(writer.Write())); } - }); + } // Add all the generated files to the workspace - await MeasureGenerationStepAsync("Generation.AddGeneratedFilesToWorkspace", () => Task.WhenAll(generateFilesTasks)); + await Task.WhenAll(generateFilesTasks); - MeasureGenerationStep("Generation.ProviderReferenceMapShadowAnalysis", () => generatedCodeWorkspace.AnalyzeProviderReferenceMap(output.TypeProviders)); + generatedCodeWorkspace.AnalyzeProviderReferenceMap(output.TypeProviders); LoggingHelpers.LogElapsedTime("All generated types have been written into memory"); // Delete any old generated files - MeasureGenerationStep("Generation.DeleteOldGeneratedFiles", () => DeleteDirectory(generatedSourceOutputPath, _filesToKeep)); + DeleteDirectory(generatedSourceOutputPath, _filesToKeep); LoggingHelpers.LogElapsedTime("All old generated files have been deleted"); - await MeasureGenerationStepAsync("Generation.PostProcessAsync", generatedCodeWorkspace.PostProcessAsync); + await generatedCodeWorkspace.PostProcessAsync(); // Write the generated files to the output directory - await MeasureGenerationStepAsync("Generation.WriteGeneratedFilesToDisk", async () => + await foreach (var file in generatedCodeWorkspace.GetGeneratedFilesAsync()) { - await foreach (var file in generatedCodeWorkspace.GetGeneratedFilesAsync()) + if (string.IsNullOrEmpty(file.Text)) { - if (string.IsNullOrEmpty(file.Text)) - { - continue; - } - var filename = Path.Combine(outputPath, file.Name); - CodeModelGenerator.Instance.Emitter.Info($"Writing {Path.GetFullPath(filename)}"); - Directory.CreateDirectory(Path.GetDirectoryName(filename)!); - await File.WriteAllTextAsync(filename, file.Text); + continue; } - }); + var filename = Path.Combine(outputPath, file.Name); + CodeModelGenerator.Instance.Emitter.Info($"Writing {Path.GetFullPath(filename)}"); + Directory.CreateDirectory(Path.GetDirectoryName(filename)!); + await File.WriteAllTextAsync(filename, file.Text); + } // Write additional output files (e.g. configuration schemas, .targets files) - await MeasureGenerationStepAsync("Generation.WriteAdditionalFiles", () => CodeModelGenerator.Instance.WriteAdditionalFiles(outputPath)); + await CodeModelGenerator.Instance.WriteAdditionalFiles(outputPath); // Write project scaffolding files (after additional files so schema existence can be checked) if (CodeModelGenerator.Instance.IsNewProject) { - await MeasureGenerationStepAsync( - "Generation.WriteProjectScaffolding", - () => CodeModelGenerator.Instance.TypeFactory.CreateNewProjectScaffolding().Execute()); + await CodeModelGenerator.Instance.TypeFactory.CreateNewProjectScaffolding().Execute(); } LoggingHelpers.LogElapsedTime("All files have been written to disk"); } - private static void MeasureGenerationStep(string stepName, Action action) - { - MeasureGenerationStep( - stepName, - () => - { - action(); - return 0; - }); - } - - private static T MeasureGenerationStep(string stepName, Func action) - { - var profile = GenerationProfile; - if (profile == null) - { - return action(); - } - - var allocatedBytes = GC.GetTotalAllocatedBytes(precise: false); - var stopwatch = Stopwatch.StartNew(); - try - { - return action(); - } - finally - { - stopwatch.Stop(); - profile.Add(stepName, stopwatch.Elapsed, GC.GetTotalAllocatedBytes(precise: false) - allocatedBytes); - } - } - - private static Task MeasureGenerationStepAsync(string stepName, Func action) => MeasureGenerationStepAsync( - stepName, - async () => - { - await action(); - return 0; - }); - - private static async Task MeasureGenerationStepAsync(string stepName, Func> action) - { - var profile = GenerationProfile; - if (profile == null) - { - return await action(); - } - - var allocatedBytes = GC.GetTotalAllocatedBytes(precise: false); - var stopwatch = Stopwatch.StartNew(); - try - { - return await action(); - } - finally - { - stopwatch.Stop(); - profile.Add(stepName, stopwatch.Elapsed, GC.GetTotalAllocatedBytes(precise: false) - allocatedBytes); - } - } - internal static void FilterAllCustomizedMembers(OutputLibrary output) { foreach (var typeProvider in output.TypeProviders) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index 07e8595be08..de03b1ca6e7 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -37,8 +37,6 @@ internal class GeneratedCodeWorkspace private static readonly Lazy _metadataReferenceResolver = new(() => new WorkspaceMetadataReferenceResolver()); private static Task? _cachedProject; - internal static GeneratedCodeWorkspacePostProcessingProfile? PostProcessingProfile { get; set; } - private static readonly string[] _generatedFolders = [GeneratedFolder]; private static readonly string[] _sharedFolders = [SharedFolder]; @@ -109,7 +107,7 @@ internal static SyntaxTree GetTree(TypeProvider provider) public async IAsyncEnumerable<(string Name, string Text)> GetGeneratedFilesAsync() { - List docs = new List(); + List> documents = new List>(); var memberRemover = new MemberRemoverRewriter(); foreach (Document document in _project.Documents) { @@ -118,12 +116,9 @@ internal static SyntaxTree GetTree(TypeProvider provider) continue; } - docs.Add(document); + documents.Add(ProcessDocument(document, memberRemover)); } - - docs = PostProcessingProfile == null - ? [.. await Task.WhenAll(docs.Select(document => ProcessDocument(document, memberRemover)))] - : await ProcessDocumentsSequentiallyAsync(docs, memberRemover); + var docs = await Task.WhenAll(documents); LoggingHelpers.LogElapsedTime("Roslyn post processing complete"); @@ -139,101 +134,36 @@ internal static SyntaxTree GetTree(TypeProvider provider) } } - private async Task> ProcessDocumentsSequentiallyAsync(List documents, MemberRemoverRewriter memberRemover) - { - List processedDocuments = new(documents.Count); - foreach (var document in documents) - { - processedDocuments.Add(await ProcessDocument(document, memberRemover)); - } - - return processedDocuments; - } - private async Task ProcessDocument(Document document, MemberRemoverRewriter memberRemover) { - var totalStopwatch = PostProcessingProfile == null ? null : Stopwatch.StartNew(); - try - { - var root = await MeasurePostProcessingStepAsync("GetSyntaxRootAsync", () => document.GetSyntaxRootAsync()); - var semanticModel = await MeasurePostProcessingStepAsync("GetSemanticModelAsync", () => document.GetSemanticModelAsync()); - - if (semanticModel == null || root == null) - { - return document; - } - - root = MeasurePostProcessingStep("MemberRemoverRewriter", () => memberRemover.Visit(root)); - - foreach (var rewriter in CodeModelGenerator.Instance.Rewriters) - { - rewriter.SemanticModel = semanticModel; - root = MeasurePostProcessingStep($"CustomRewriter.{rewriter.GetType().Name}", () => rewriter.Visit(root)); - } - document = document.WithSyntaxRoot(root); - - if (!CodeModelGenerator.Instance.Configuration.DisableRoslynReduce) - { - document = await MeasurePostProcessingStepAsync("Roslyn.Simplifier.ReduceAsync", () => Simplifier.ReduceAsync(document)); - } + var root = await document.GetSyntaxRootAsync(); + var semanticModel = await document.GetSemanticModelAsync(); - // Reformat if any custom rewriters have been applied - if (CodeModelGenerator.Instance.Rewriters.Count > 0) - { - document = await MeasurePostProcessingStepAsync("Formatter.FormatAsync", () => Formatter.FormatAsync(document)); - } - return document; - } - finally + if (semanticModel == null || root == null) { - if (totalStopwatch != null) - { - totalStopwatch.Stop(); - PostProcessingProfile?.Add("ProcessDocument.Total", totalStopwatch.Elapsed, 0); - } + return document; } - } - private static T MeasurePostProcessingStep(string stepName, Func action) - { - var profile = PostProcessingProfile; - if (profile == null) - { - return action(); - } + root = memberRemover.Visit(root); - var allocatedBytes = GC.GetTotalAllocatedBytes(precise: false); - var stopwatch = Stopwatch.StartNew(); - try + foreach (var rewriter in CodeModelGenerator.Instance.Rewriters) { - return action(); + rewriter.SemanticModel = semanticModel; + root = rewriter.Visit(root); } - finally - { - stopwatch.Stop(); - profile.Add(stepName, stopwatch.Elapsed, GC.GetTotalAllocatedBytes(precise: false) - allocatedBytes); - } - } + document = document.WithSyntaxRoot(root); - private static async Task MeasurePostProcessingStepAsync(string stepName, Func> action) - { - var profile = PostProcessingProfile; - if (profile == null) + if (!CodeModelGenerator.Instance.Configuration.DisableRoslynReduce) { - return await action(); + document = await Simplifier.ReduceAsync(document); } - var allocatedBytes = GC.GetTotalAllocatedBytes(precise: false); - var stopwatch = Stopwatch.StartNew(); - try - { - return await action(); - } - finally + // Reformat if any custom rewriters have been applied + if (CodeModelGenerator.Instance.Rewriters.Count > 0) { - stopwatch.Stop(); - profile.Add(stepName, stopwatch.Elapsed, GC.GetTotalAllocatedBytes(precise: false) - allocatedBytes); + document = await Formatter.FormatAsync(document); } + return document; } public static bool IsGeneratedDocument(Document document) => document.Folders.Contains(GeneratedFolder); @@ -350,11 +280,11 @@ public async Task PostProcessAsync() case Configuration.UnreferencedTypesHandlingOption.KeepAll: break; case Configuration.UnreferencedTypesHandlingOption.Internalize: - _project = await MeasurePostProcessingStepAsync("PostProcess.InternalizeAsync", () => postProcessor.InternalizeAsync(_project)); + _project = await postProcessor.InternalizeAsync(_project); break; case Configuration.UnreferencedTypesHandlingOption.RemoveOrInternalize: - _project = await MeasurePostProcessingStepAsync("PostProcess.InternalizeAsync", () => postProcessor.InternalizeAsync(_project)); - _project = await MeasurePostProcessingStepAsync("PostProcess.RemoveAsync", () => postProcessor.RemoveAsync(_project)); + _project = await postProcessor.InternalizeAsync(_project); + _project = await postProcessor.RemoveAsync(_project); break; } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspacePostProcessingProfile.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspacePostProcessingProfile.cs deleted file mode 100644 index 74a9a58168a..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspacePostProcessingProfile.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; - -namespace Microsoft.TypeSpec.Generator -{ - internal sealed class GeneratedCodeWorkspacePostProcessingProfile - { - private readonly object _syncRoot = new(); - private readonly Dictionary _steps = new(StringComparer.Ordinal); - - public void Add(string stepName, TimeSpan elapsed, long allocatedBytes) - { - lock (_syncRoot) - { - ref var summary = ref CollectionsMarshal.GetValueRefOrAddDefault(_steps, stepName, out _); - summary.Count++; - summary.ElapsedTicks += elapsed.Ticks; - summary.AllocatedBytes += allocatedBytes; - } - } - - public string GetSummary() - { - KeyValuePair[] steps; - lock (_syncRoot) - { - steps = _steps.ToArray(); - } - - var totalTicks = steps - .Where(static step => step.Key != "ProcessDocument.Total") - .Sum(static step => step.Value.ElapsedTicks); - var builder = new StringBuilder(); - builder.AppendLine("Post-processing step profile:"); - builder.AppendLine("Step, Count, Total ms, Avg ms, Percent of measured steps, Allocated bytes, Avg allocated bytes"); - - foreach (var step in steps.OrderByDescending(static step => step.Value.ElapsedTicks)) - { - var elapsedMs = TimeSpan.FromTicks(step.Value.ElapsedTicks).TotalMilliseconds; - var averageMs = elapsedMs / step.Value.Count; - var averageAllocatedBytes = step.Value.AllocatedBytes / step.Value.Count; - var percentage = totalTicks == 0 || step.Key == "ProcessDocument.Total" - ? 0 - : step.Value.ElapsedTicks * 100.0 / totalTicks; - builder.AppendLine($"{step.Key}, {step.Value.Count}, {elapsedMs:F3}, {averageMs:F3}, {percentage:F1}%, {step.Value.AllocatedBytes}, {averageAllocatedBytes}"); - } - - return builder.ToString(); - } - - private struct StepSummary - { - public int Count; - public long ElapsedTicks; - public long AllocatedBytes; - } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs index 57136754da1..5632a41053a 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs @@ -20,8 +20,6 @@ internal class PostProcessor private readonly HashSet _typesToKeep; private INamedTypeSymbol? _modelFactorySymbol; - private static GeneratedCodeWorkspacePostProcessingProfile? Profile => GeneratedCodeWorkspace.PostProcessingProfile; - public PostProcessor( HashSet typesToKeep, string? modelFactoryFullName = null, @@ -127,30 +125,27 @@ protected virtual bool ShouldIncludeDocument(Document document) => /// The processed . is immutable, therefore this should usually be a new instance public async Task InternalizeAsync(Project project) { - var compilation = await MeasureAsync("PostProcessor.Internalize.GetCompilationAsync", () => project.GetCompilationAsync()); + var compilation = await project.GetCompilationAsync(); if (compilation == null) return project; // first get all the declared symbols - var definitions = await MeasureAsync("PostProcessor.Internalize.GetTypeSymbolsAsync", () => GetTypeSymbolsAsync(compilation, project, true)); + var definitions = await GetTypeSymbolsAsync(compilation, project, true); IEnumerable symbolsToInternalize; if (ProviderReferenceMapShadowAnalyzer.UseShadowMap && ProviderReferenceMapShadowAnalyzer.LatestResult is { } useShadowResult) { - symbolsToInternalize = Measure("PostProcessor.Internalize.UseShadowCandidates", () => - GetSymbolsByName(definitions.DeclaredSymbols, useShadowResult.InternalizeCandidates).ToArray()); + symbolsToInternalize = GetSymbolsByName(definitions.DeclaredSymbols, useShadowResult.InternalizeCandidates).ToArray(); } else { // build the reference map var referenceMap = - await MeasureAsync( - "PostProcessor.Internalize.BuildPublicReferenceMapAsync", - () => new ReferenceMapBuilder(compilation, project).BuildPublicReferenceMapAsync( - definitions.DeclaredSymbols, definitions.DeclaredNodesCache)); + await new ReferenceMapBuilder(compilation, project).BuildPublicReferenceMapAsync( + definitions.DeclaredSymbols, definitions.DeclaredNodesCache); // get the root symbols - var rootSymbols = await MeasureAsync("PostProcessor.Internalize.GetRootSymbolsAsync", () => GetRootSymbolsAsync(project, definitions)); + var rootSymbols = await GetRootSymbolsAsync(project, definitions); // traverse all the root and recursively add all the things we met - var publicSymbols = Measure("PostProcessor.Internalize.VisitSymbolsFromRoot", () => VisitSymbolsFromRootAsync(rootSymbols, referenceMap).ToArray()); + var publicSymbols = VisitSymbolsFromRootAsync(rootSymbols, referenceMap); symbolsToInternalize = definitions.DeclaredSymbols.Except(publicSymbols); } @@ -163,36 +158,23 @@ await MeasureAsync( shadowResult.InternalizeCandidates); } - var nodesToInternalize = Measure("PostProcessor.Internalize.CollectNodes", () => + var nodesToInternalize = new Dictionary(); + foreach (var symbol in symbolsToInternalize) { - var nodes = new Dictionary(); - foreach (var symbol in symbolsToInternalize) + foreach (var node in definitions.DeclaredNodesCache[symbol]) { - foreach (var node in definitions.DeclaredNodesCache[symbol]) - { - nodes[node] = project.GetDocumentId(node.SyntaxTree)!; - } + nodesToInternalize[node] = project.GetDocumentId(node.SyntaxTree)!; } + } - return nodes; - }); - - project = Measure("PostProcessor.Internalize.MarkInternal", () => + foreach (var (model, documentId) in nodesToInternalize) { - var updatedProject = project; - foreach (var (model, documentId) in nodesToInternalize) - { - updatedProject = MarkInternal(updatedProject, model, documentId); - } - - return updatedProject; - }); + project = MarkInternal(project, model, documentId); + } var modelNamesToRemove = nodesToInternalize.Keys.Select(item => item.Identifier.Text); - project = await MeasureAsync( - "PostProcessor.Internalize.RemoveMethodsFromModelFactoryAsync", - () => RemoveMethodsFromModelFactoryAsync(project, definitions, modelNamesToRemove.ToHashSet())); + project = await RemoveMethodsFromModelFactoryAsync(project, definitions, modelNamesToRemove.ToHashSet()); return project; } @@ -266,40 +248,36 @@ private async Task RemoveMethodsFromModelFactoryAsync(Project project, /// The processed . is immutable, therefore this should usually be a new instance public async Task RemoveAsync(Project project) { - var compilation = await MeasureAsync("PostProcessor.Remove.GetCompilationAsync", () => project.GetCompilationAsync()); + var compilation = await project.GetCompilationAsync(); if (compilation == null) return project; // find all the declarations, including non-public declared - var definitions = await MeasureAsync("PostProcessor.Remove.GetTypeSymbolsAsync", () => GetTypeSymbolsAsync(compilation, project, false)); + var definitions = await GetTypeSymbolsAsync(compilation, project, false); IEnumerable symbolsToRemove; HashSet referencedSet; if (ProviderReferenceMapShadowAnalyzer.UseShadowMap && ProviderReferenceMapShadowAnalyzer.LatestResult is { } useShadowResult) { - symbolsToRemove = Measure("PostProcessor.Remove.UseShadowCandidates", () => - GetSymbolsByName(definitions.DeclaredSymbols, useShadowResult.RemoveCandidates).ToArray()); - referencedSet = Measure("PostProcessor.Remove.BuildShadowReferencedSet", () => - new HashSet(definitions.DeclaredSymbols.Except(symbolsToRemove), SymbolEqualityComparer.Default)); + symbolsToRemove = GetSymbolsByName(definitions.DeclaredSymbols, useShadowResult.RemoveCandidates).ToArray(); + referencedSet = new HashSet(definitions.DeclaredSymbols.Except(symbolsToRemove), SymbolEqualityComparer.Default); } else { // build reference map var referenceMap = - await MeasureAsync( - "PostProcessor.Remove.BuildAllReferenceMapAsync", - () => new ReferenceMapBuilder(compilation, project).BuildAllReferenceMapAsync( - definitions.DeclaredSymbols, definitions.DocumentsCache)); + await new ReferenceMapBuilder(compilation, project).BuildAllReferenceMapAsync( + definitions.DeclaredSymbols, definitions.DocumentsCache); // get root symbols - var rootSymbols = await MeasureAsync("PostProcessor.Remove.GetRootSymbolsAsync", () => GetRootSymbolsAsync(project, definitions)); + var rootSymbols = await GetRootSymbolsAsync(project, definitions); // include model factory as a root symbol when doing the remove pass so that we are sure to include any internal // helpers that are required by the model factory. if (_modelFactorySymbol != null) rootSymbols.Add(_modelFactorySymbol); // traverse the map to determine the declarations that we are about to remove, starting from root nodes - var referencedSymbols = Measure("PostProcessor.Remove.VisitSymbolsFromRoot", () => VisitSymbolsFromRootAsync(rootSymbols, referenceMap).ToArray().AsEnumerable()); + var referencedSymbols = VisitSymbolsFromRootAsync(rootSymbols, referenceMap); - referencedSymbols = Measure("PostProcessor.Remove.AddSampleSymbols", () => AddSampleSymbols(referencedSymbols, definitions.DeclaredSymbols)); - referencedSet = Measure("PostProcessor.Remove.BuildReferencedSet", () => new HashSet(referencedSymbols, SymbolEqualityComparer.Default)); + referencedSymbols = AddSampleSymbols(referencedSymbols, definitions.DeclaredSymbols); + referencedSet = new HashSet(referencedSymbols, SymbolEqualityComparer.Default); symbolsToRemove = definitions.DeclaredSymbols.Except(referencedSet); } @@ -312,23 +290,18 @@ await MeasureAsync( shadowResult.RemoveCandidates); } - var nodesToRemove = Measure("PostProcessor.Remove.CollectNodes", () => + var nodesToRemove = new List(); + foreach (var symbol in symbolsToRemove) { - var nodes = new List(); - foreach (var symbol in symbolsToRemove) + if (referencedSet.Contains(GetBase(symbol))) { - if (referencedSet.Contains(GetBase(symbol))) - { - continue; - } - nodes.AddRange(definitions.DeclaredNodesCache[symbol]); + continue; } - - return nodes; - }); + nodesToRemove.AddRange(definitions.DeclaredNodesCache[symbol]); + } // remove them one by one - project = await MeasureAsync("PostProcessor.Remove.RemoveModelsAsync", () => RemoveModelsAsync(project, nodesToRemove)); + project = await RemoveModelsAsync(project, nodesToRemove); return project; } @@ -421,35 +394,25 @@ private async Task RemoveModelsAsync(Project project, IEnumerable unusedModels) { // accumulate the definitions from the same document together - var documents = Measure("PostProcessor.Remove.RemoveModelsAsync.GroupByDocument", () => - { - var groupedDocuments = new Dictionary>(); - foreach (var model in unusedModels) - { - var document = project.GetDocument(model.SyntaxTree); - Debug.Assert(document != null); - if (!groupedDocuments.ContainsKey(document)) - groupedDocuments.Add(document, new HashSet()); + var documents = new Dictionary>(); - groupedDocuments[document].Add(model); - } + foreach (var model in unusedModels) + { + var document = project.GetDocument(model.SyntaxTree); + Debug.Assert(document != null); + if (!documents.ContainsKey(document)) + documents.Add(document, new HashSet()); - return groupedDocuments; - }); + documents[document].Add(model); + } - project = await MeasureAsync("PostProcessor.Remove.RemoveModelsAsync.RemoveModelsFromDocuments", async () => + foreach (var models in documents.Values) { - var updatedProject = project; - foreach (var models in documents.Values) - { - updatedProject = await RemoveModelsFromDocumentAsync(updatedProject, models); - } - - return updatedProject; - }); + project = await RemoveModelsFromDocumentAsync(project, models); + } // remove what are now invalid references due to the models being removed - project = await MeasureAsync("PostProcessor.Remove.RemoveModelsAsync.RemoveInvalidRefs", () => RemoveInvalidRefs(project)); + project = await RemoveInvalidRefs(project); return project; } @@ -500,28 +463,16 @@ private async Task RemoveInvalidRefs(Project project) var solution = project.Solution; // Process each document for invalid usings - solution = await MeasureAsync("PostProcessor.Remove.RemoveInvalidRefs.RemoveInvalidUsings", async () => + foreach (var documentId in project.DocumentIds) { - var updatedSolution = solution; - foreach (var documentId in project.DocumentIds) - { - updatedSolution = await RemoveInvalidUsings(updatedSolution, documentId); - } - - return updatedSolution; - }); + solution = await RemoveInvalidUsings(solution, documentId); + } // Process each document for invalid attributes (with fresh semantic models) - solution = await MeasureAsync("PostProcessor.Remove.RemoveInvalidRefs.RemoveInvalidAttributes", async () => + foreach (var documentId in project.DocumentIds) { - var updatedSolution = solution; - foreach (var documentId in project.DocumentIds) - { - updatedSolution = await RemoveInvalidAttributes(updatedSolution, documentId); - } - - return updatedSolution; - }); + solution = await RemoveInvalidAttributes(solution, documentId); + } return solution.GetProject(project.Id)!; } @@ -627,48 +578,6 @@ arg.Expression is TypeOfExpressionSyntax typeOfExpr && return solution; } - private static T Measure(string stepName, Func action) - { - var profile = Profile; - if (profile == null) - { - return action(); - } - - var allocatedBytes = GC.GetTotalAllocatedBytes(precise: false); - var stopwatch = Stopwatch.StartNew(); - try - { - return action(); - } - finally - { - stopwatch.Stop(); - profile.Add(stepName, stopwatch.Elapsed, GC.GetTotalAllocatedBytes(precise: false) - allocatedBytes); - } - } - - private static async Task MeasureAsync(string stepName, Func> action) - { - var profile = Profile; - if (profile == null) - { - return await action(); - } - - var allocatedBytes = GC.GetTotalAllocatedBytes(precise: false); - var stopwatch = Stopwatch.StartNew(); - try - { - return await action(); - } - finally - { - stopwatch.Stop(); - profile.Add(stepName, stopwatch.Elapsed, GC.GetTotalAllocatedBytes(precise: false) - allocatedBytes); - } - } - private async Task> GetRootSymbolsAsync(Project project, TypeSymbols modelSymbols) { var result = new HashSet(SymbolEqualityComparer.Default); From 2d57265107030f3bc5c10cc83ed8c1cba4b25a05 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Fri, 12 Jun 2026 06:48:29 +0000 Subject: [PATCH 018/111] Remove hybrid map diagnostics from generator --- .../src/PostProcessing/PostProcessor.cs | 66 +------- .../ProviderReferenceMapShadowAnalyzer.cs | 154 ------------------ 2 files changed, 5 insertions(+), 215 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs index 5632a41053a..a6cecba593f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs @@ -131,32 +131,8 @@ public async Task InternalizeAsync(Project project) // first get all the declared symbols var definitions = await GetTypeSymbolsAsync(compilation, project, true); - IEnumerable symbolsToInternalize; - if (ProviderReferenceMapShadowAnalyzer.UseShadowMap && ProviderReferenceMapShadowAnalyzer.LatestResult is { } useShadowResult) - { - symbolsToInternalize = GetSymbolsByName(definitions.DeclaredSymbols, useShadowResult.InternalizeCandidates).ToArray(); - } - else - { - // build the reference map - var referenceMap = - await new ReferenceMapBuilder(compilation, project).BuildPublicReferenceMapAsync( - definitions.DeclaredSymbols, definitions.DeclaredNodesCache); - // get the root symbols - var rootSymbols = await GetRootSymbolsAsync(project, definitions); - // traverse all the root and recursively add all the things we met - var publicSymbols = VisitSymbolsFromRootAsync(rootSymbols, referenceMap); - - symbolsToInternalize = definitions.DeclaredSymbols.Except(publicSymbols); - } - - if (ProviderReferenceMapShadowAnalyzer.LatestResult is { } shadowResult) - { - ProviderReferenceMapShadowAnalyzer.WriteComparisonReport( - "internalize", - symbolsToInternalize.Select(static symbol => symbol.GetFullyQualifiedName()), - shadowResult.InternalizeCandidates); - } + var shadowResult = ProviderReferenceMapShadowAnalyzer.LatestResult ?? ProviderReferenceMapShadowResult.Empty; + var symbolsToInternalize = GetSymbolsByName(definitions.DeclaredSymbols, shadowResult.InternalizeCandidates).ToArray(); var nodesToInternalize = new Dictionary(); foreach (var symbol in symbolsToInternalize) @@ -254,41 +230,9 @@ public async Task RemoveAsync(Project project) // find all the declarations, including non-public declared var definitions = await GetTypeSymbolsAsync(compilation, project, false); - IEnumerable symbolsToRemove; - HashSet referencedSet; - if (ProviderReferenceMapShadowAnalyzer.UseShadowMap && ProviderReferenceMapShadowAnalyzer.LatestResult is { } useShadowResult) - { - symbolsToRemove = GetSymbolsByName(definitions.DeclaredSymbols, useShadowResult.RemoveCandidates).ToArray(); - referencedSet = new HashSet(definitions.DeclaredSymbols.Except(symbolsToRemove), SymbolEqualityComparer.Default); - } - else - { - // build reference map - var referenceMap = - await new ReferenceMapBuilder(compilation, project).BuildAllReferenceMapAsync( - definitions.DeclaredSymbols, definitions.DocumentsCache); - // get root symbols - var rootSymbols = await GetRootSymbolsAsync(project, definitions); - // include model factory as a root symbol when doing the remove pass so that we are sure to include any internal - // helpers that are required by the model factory. - if (_modelFactorySymbol != null) - rootSymbols.Add(_modelFactorySymbol); - // traverse the map to determine the declarations that we are about to remove, starting from root nodes - var referencedSymbols = VisitSymbolsFromRootAsync(rootSymbols, referenceMap); - - referencedSymbols = AddSampleSymbols(referencedSymbols, definitions.DeclaredSymbols); - referencedSet = new HashSet(referencedSymbols, SymbolEqualityComparer.Default); - - symbolsToRemove = definitions.DeclaredSymbols.Except(referencedSet); - } - - if (ProviderReferenceMapShadowAnalyzer.LatestResult is { } shadowResult) - { - ProviderReferenceMapShadowAnalyzer.WriteComparisonReport( - "remove", - symbolsToRemove.Select(static symbol => symbol.GetFullyQualifiedName()), - shadowResult.RemoveCandidates); - } + var shadowResult = ProviderReferenceMapShadowAnalyzer.LatestResult ?? ProviderReferenceMapShadowResult.Empty; + var symbolsToRemove = GetSymbolsByName(definitions.DeclaredSymbols, shadowResult.RemoveCandidates).ToArray(); + var referencedSet = new HashSet(definitions.DeclaredSymbols.Except(symbolsToRemove), SymbolEqualityComparer.Default); var nodesToRemove = new List(); foreach (var symbol in symbolsToRemove) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs index 7d549afb5a1..df8858156cc 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs @@ -3,9 +3,7 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Text; using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.Statements; @@ -16,32 +14,12 @@ namespace Microsoft.TypeSpec.Generator { internal static class ProviderReferenceMapShadowAnalyzer { - private const string EnableEnvironmentVariable = "TYPESPEC_PROVIDER_REFERENCE_MAP_SHADOW"; - private const string UseShadowEnvironmentVariable = "TYPESPEC_PROVIDER_REFERENCE_MAP_USE_SHADOW"; - private const string OutputDirectoryEnvironmentVariable = "TYPESPEC_PROVIDER_REFERENCE_MAP_SHADOW_DIR"; - private static ProviderReferenceMapShadowResult? _latestResult; - public static bool IsEnabled => string.Equals( - Environment.GetEnvironmentVariable(EnableEnvironmentVariable), - "true", - StringComparison.OrdinalIgnoreCase); - public static ProviderReferenceMapShadowResult? LatestResult => _latestResult; - public static bool UseShadowMap => string.Equals( - Environment.GetEnvironmentVariable(UseShadowEnvironmentVariable), - "true", - StringComparison.OrdinalIgnoreCase); - public static void Analyze(IReadOnlyList providers, Project project) { - if (!IsEnabled) - { - _latestResult = null; - return; - } - var graph = BuildGraph(providers); var customRoots = GetCustomCodeGeneratedTypeRoots(project, graph.Nodes); var internalizeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: false); @@ -67,8 +45,6 @@ public static void Analyze(IReadOnlyList providers, Project projec _latestResult = new ProviderReferenceMapShadowResult( internalizeCandidates.ToHashSet(StringComparer.Ordinal), removeCandidates.ToHashSet(StringComparer.Ordinal)); - - WriteReport(graph, customRoots, helperRoots, internalizeRoots, internalizeReachable, internalizeCandidates, removeRoots, removeReachable, removeCandidates); } private static HashSet GetCustomCodeGeneratedTypeRoots(Project project, HashSet generatedTypeNames) @@ -132,45 +108,6 @@ private static void AddSymbolRoot(HashSet roots, ITypeSymbol? symbol, Ha } } - public static void WriteComparisonReport(string passName, IEnumerable roslynCandidates, IEnumerable providerCandidates) - { - if (!IsEnabled) - { - return; - } - - var roslynSet = roslynCandidates.ToHashSet(StringComparer.Ordinal); - var providerSet = providerCandidates.ToHashSet(StringComparer.Ordinal); - var missingFromProvider = roslynSet.Except(providerSet, StringComparer.Ordinal).OrderBy(static name => name, StringComparer.Ordinal).ToArray(); - var extraInProvider = providerSet.Except(roslynSet, StringComparer.Ordinal).OrderBy(static name => name, StringComparer.Ordinal).ToArray(); - - var directory = GetOutputDirectory(); - Directory.CreateDirectory(directory); - var path = Path.Combine(directory, $"provider-reference-map-shadow-comparison-{passName}-{DateTime.UtcNow:yyyyMMddHHmmssfff}.txt"); - var builder = new StringBuilder(); - builder.AppendLine($"Provider reference map shadow comparison: {passName}"); - builder.AppendLine($"Roslyn candidates: {roslynSet.Count}"); - builder.AppendLine($"Provider candidates: {providerSet.Count}"); - builder.AppendLine($"Missing from provider: {missingFromProvider.Length}"); - builder.AppendLine($"Extra in provider: {extraInProvider.Length}"); - builder.AppendLine(); - builder.AppendLine("Missing from provider:"); - foreach (var item in missingFromProvider) - { - builder.AppendLine($" {item}"); - } - - builder.AppendLine(); - builder.AppendLine("Extra in provider:"); - foreach (var item in extraInProvider) - { - builder.AppendLine($" {item}"); - } - - File.WriteAllText(path, builder.ToString()); - CodeModelGenerator.Instance.Emitter.Debug($"Provider reference map shadow comparison written to {path}"); - } - private static ProviderReferenceGraph BuildGraph(IReadOnlyList providers) { var nodes = providers @@ -498,89 +435,6 @@ private static void AddTypeReference(HashSet references, CSharpType? typ } } - private static void WriteReport( - ProviderReferenceGraph graph, - HashSet customRoots, - HashSet helperRoots, - HashSet internalizeRoots, - HashSet internalizeReachable, - IReadOnlyList internalizeCandidates, - HashSet removeRoots, - HashSet removeReachable, - IReadOnlyList removeCandidates) - { - var directory = GetOutputDirectory(); - - Directory.CreateDirectory(directory); - var path = Path.Combine(directory, $"provider-reference-map-shadow-{DateTime.UtcNow:yyyyMMddHHmmssfff}.txt"); - var builder = new StringBuilder(); - builder.AppendLine("Provider reference map shadow report"); - builder.AppendLine($"Declared providers: {graph.Nodes.Count}"); - builder.AppendLine($"Internalize roots: {internalizeRoots.Count}"); - builder.AppendLine($"Internalize reachable: {internalizeReachable.Count}"); - builder.AppendLine($"Internalize candidates: {internalizeCandidates.Count}"); - builder.AppendLine($"Custom roots: {customRoots.Count}"); - builder.AppendLine($"Helper roots: {helperRoots.Count}"); - builder.AppendLine($"Remove roots: {removeRoots.Count}"); - builder.AppendLine($"Remove reachable: {removeReachable.Count}"); - builder.AppendLine($"Remove candidates: {removeCandidates.Count}"); - builder.AppendLine(); - builder.AppendLine("Custom roots:"); - foreach (var root in customRoots.OrderBy(static name => name, StringComparer.Ordinal)) - { - builder.AppendLine($" {root}"); - } - - builder.AppendLine(); - builder.AppendLine("Helper roots:"); - foreach (var root in helperRoots.OrderBy(static name => name, StringComparer.Ordinal)) - { - builder.AppendLine($" {root}"); - } - - builder.AppendLine(); - builder.AppendLine("Internalize roots:"); - foreach (var root in internalizeRoots.OrderBy(static name => name, StringComparer.Ordinal)) - { - builder.AppendLine($" {root}"); - } - - builder.AppendLine(); - builder.AppendLine("Internalize candidates:"); - foreach (var candidate in internalizeCandidates) - { - builder.AppendLine($" {candidate}"); - } - - builder.AppendLine(); - builder.AppendLine("Remove roots:"); - foreach (var root in removeRoots.OrderBy(static name => name, StringComparer.Ordinal)) - { - builder.AppendLine($" {root}"); - } - - builder.AppendLine(); - builder.AppendLine("Remove candidates:"); - foreach (var candidate in removeCandidates) - { - builder.AppendLine($" {candidate}"); - } - - builder.AppendLine(); - builder.AppendLine("References:"); - foreach (var (type, references) in graph.References.OrderBy(static item => item.Key, StringComparer.Ordinal)) - { - builder.AppendLine($" {type}"); - foreach (var reference in references.OrderBy(static name => name, StringComparer.Ordinal)) - { - builder.AppendLine($" -> {reference}"); - } - } - - File.WriteAllText(path, builder.ToString()); - CodeModelGenerator.Instance.Emitter.Debug($"Provider reference map shadow report written to {path}"); - } - private static string GetSimpleName(string fullyQualifiedName) { var lastDot = fullyQualifiedName.LastIndexOf('.'); @@ -604,13 +458,5 @@ private static string StripGenericArity(string name) private sealed record ProviderReferenceGraph( HashSet Nodes, Dictionary> References); - - private static string GetOutputDirectory() - { - var directory = Environment.GetEnvironmentVariable(OutputDirectoryEnvironmentVariable); - return string.IsNullOrWhiteSpace(directory) - ? Path.Combine(Path.GetTempPath(), "typespec-provider-reference-map-shadow") - : Path.GetFullPath(directory); - } } } From dc145f4fc400810850fb908c8fb599cfced151f3 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Fri, 12 Jun 2026 06:53:36 +0000 Subject: [PATCH 019/111] Rename hybrid reference map components --- .../src/PostProcessing/GeneratedCodeWorkspace.cs | 2 +- .../src/PostProcessing/PostProcessor.cs | 8 ++++---- ...pShadowAnalyzer.cs => ProviderReferenceMapAnalyzer.cs} | 8 ++++---- ...ceMapShadowResult.cs => ProviderReferenceMapResult.cs} | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) rename packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/{ProviderReferenceMapShadowAnalyzer.cs => ProviderReferenceMapAnalyzer.cs} (98%) rename packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/{ProviderReferenceMapShadowResult.cs => ProviderReferenceMapResult.cs} (65%) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index de03b1ca6e7..b999674e9cf 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -85,7 +85,7 @@ public async Task AddInMemoryFile(TypeProvider type) internal void AnalyzeProviderReferenceMap(IReadOnlyList providers) { - ProviderReferenceMapShadowAnalyzer.Analyze(providers, _project); + ProviderReferenceMapAnalyzer.Analyze(providers, _project); } private async Task UpdateProject(Document document) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs index a6cecba593f..5097677c401 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs @@ -131,8 +131,8 @@ public async Task InternalizeAsync(Project project) // first get all the declared symbols var definitions = await GetTypeSymbolsAsync(compilation, project, true); - var shadowResult = ProviderReferenceMapShadowAnalyzer.LatestResult ?? ProviderReferenceMapShadowResult.Empty; - var symbolsToInternalize = GetSymbolsByName(definitions.DeclaredSymbols, shadowResult.InternalizeCandidates).ToArray(); + var referenceMapResult = ProviderReferenceMapAnalyzer.LatestResult ?? ProviderReferenceMapResult.Empty; + var symbolsToInternalize = GetSymbolsByName(definitions.DeclaredSymbols, referenceMapResult.InternalizeCandidates).ToArray(); var nodesToInternalize = new Dictionary(); foreach (var symbol in symbolsToInternalize) @@ -230,8 +230,8 @@ public async Task RemoveAsync(Project project) // find all the declarations, including non-public declared var definitions = await GetTypeSymbolsAsync(compilation, project, false); - var shadowResult = ProviderReferenceMapShadowAnalyzer.LatestResult ?? ProviderReferenceMapShadowResult.Empty; - var symbolsToRemove = GetSymbolsByName(definitions.DeclaredSymbols, shadowResult.RemoveCandidates).ToArray(); + var referenceMapResult = ProviderReferenceMapAnalyzer.LatestResult ?? ProviderReferenceMapResult.Empty; + var symbolsToRemove = GetSymbolsByName(definitions.DeclaredSymbols, referenceMapResult.RemoveCandidates).ToArray(); var referencedSet = new HashSet(definitions.DeclaredSymbols.Except(symbolsToRemove), SymbolEqualityComparer.Default); var nodesToRemove = new List(); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs similarity index 98% rename from packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs rename to packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index df8858156cc..07eca2c5fd1 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -12,11 +12,11 @@ namespace Microsoft.TypeSpec.Generator { - internal static class ProviderReferenceMapShadowAnalyzer + internal static class ProviderReferenceMapAnalyzer { - private static ProviderReferenceMapShadowResult? _latestResult; + private static ProviderReferenceMapResult? _latestResult; - public static ProviderReferenceMapShadowResult? LatestResult => _latestResult; + public static ProviderReferenceMapResult? LatestResult => _latestResult; public static void Analyze(IReadOnlyList providers, Project project) { @@ -42,7 +42,7 @@ public static void Analyze(IReadOnlyList providers, Project projec var helperRoots = internalizeHelperRoots.Concat(removeHelperRoots).ToHashSet(StringComparer.Ordinal); - _latestResult = new ProviderReferenceMapShadowResult( + _latestResult = new ProviderReferenceMapResult( internalizeCandidates.ToHashSet(StringComparer.Ordinal), removeCandidates.ToHashSet(StringComparer.Ordinal)); } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowResult.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapResult.cs similarity index 65% rename from packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowResult.cs rename to packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapResult.cs index 14b68f7d187..bb73e08040c 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowResult.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapResult.cs @@ -5,10 +5,10 @@ namespace Microsoft.TypeSpec.Generator { - internal sealed record ProviderReferenceMapShadowResult( + internal sealed record ProviderReferenceMapResult( HashSet InternalizeCandidates, HashSet RemoveCandidates) { - public static ProviderReferenceMapShadowResult Empty { get; } = new([], []); + public static ProviderReferenceMapResult Empty { get; } = new([], []); } } From 0cc3abf89306204bd66a0ca94e9084e1ab566ded Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Fri, 12 Jun 2026 07:20:00 +0000 Subject: [PATCH 020/111] Document hybrid reference map decisions --- .../src/PostProcessing/PostProcessor.cs | 4 ++++ .../src/PostProcessing/ProviderReferenceMapAnalyzer.cs | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs index 5097677c401..9c069baf2c7 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs @@ -132,6 +132,8 @@ public async Task InternalizeAsync(Project project) // first get all the declared symbols var definitions = await GetTypeSymbolsAsync(compilation, project, true); var referenceMapResult = ProviderReferenceMapAnalyzer.LatestResult ?? ProviderReferenceMapResult.Empty; + // ProviderReferenceMapAnalyzer replaces Roslyn reference-map construction for generated code. + // It still uses Roslyn-discovered roots for custom/shared code before this point. var symbolsToInternalize = GetSymbolsByName(definitions.DeclaredSymbols, referenceMapResult.InternalizeCandidates).ToArray(); var nodesToInternalize = new Dictionary(); @@ -231,6 +233,8 @@ public async Task RemoveAsync(Project project) // find all the declarations, including non-public declared var definitions = await GetTypeSymbolsAsync(compilation, project, false); var referenceMapResult = ProviderReferenceMapAnalyzer.LatestResult ?? ProviderReferenceMapResult.Empty; + // The remove pass uses the same precomputed hybrid map to avoid scanning all generated + // documents with Roslyn while preserving custom-code references as roots. var symbolsToRemove = GetSymbolsByName(definitions.DeclaredSymbols, referenceMapResult.RemoveCandidates).ToArray(); var referencedSet = new HashSet(definitions.DeclaredSymbols.Except(symbolsToRemove), SymbolEqualityComparer.Default); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 07eca2c5fd1..ea7e39fbd74 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -21,7 +21,13 @@ internal static class ProviderReferenceMapAnalyzer public static void Analyze(IReadOnlyList providers, Project project) { var graph = BuildGraph(providers); + + // Generated-code dependencies come from providers. Custom code still needs Roslyn + // because arbitrary user C# can reference generated types in ways providers cannot see. var customRoots = GetCustomCodeGeneratedTypeRoots(project, graph.Nodes); + + // Helper types are rooted after an initial reachability pass so unused infrastructure + // such as change-tracking dictionaries can still be removed when no reachable type needs them. var internalizeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: false); internalizeRoots.UnionWith(customRoots); var internalizeReachableWithoutHelpers = GetReachableTypes(internalizeRoots, graph.References); @@ -122,6 +128,9 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro AddTypeReference(references[current], provider.BaseType, nodes); AddTypeReference(references[current], provider.DeclaringTypeProvider?.Type, nodes); + // Model factory signatures mention many models. The existing Roslyn post-processor + // removes factory methods for unreachable models, so model factory should only + // contribute helper dependencies, not model reachability edges. if (IsModelFactoryProvider(provider)) { continue; @@ -251,6 +260,7 @@ private static HashSet GetHelperRootNames(IReadOnlyList pr foreach (var method in provider.Methods) { + // Only factory methods for reachable models can instantiate collection helpers. if (isModelFactory && (method.Signature.ReturnType == null || !reachableTypes.Contains(GetProviderTypeName(method.Signature.ReturnType)))) { From c2abe7952be0f199f35fbf017c23cf10178c22be Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Fri, 12 Jun 2026 08:09:10 +0000 Subject: [PATCH 021/111] Fix hybrid reference map CI failures --- .../src/PostProcessing/PostProcessor.cs | 48 ++++++++-- .../ProviderReferenceMapAnalyzer.cs | 87 ++++++++++++++++++- .../ProviderReferenceMapResult.cs | 3 +- 3 files changed, 127 insertions(+), 11 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs index 9c069baf2c7..09cc693362b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs @@ -131,10 +131,21 @@ public async Task InternalizeAsync(Project project) // first get all the declared symbols var definitions = await GetTypeSymbolsAsync(compilation, project, true); - var referenceMapResult = ProviderReferenceMapAnalyzer.LatestResult ?? ProviderReferenceMapResult.Empty; - // ProviderReferenceMapAnalyzer replaces Roslyn reference-map construction for generated code. - // It still uses Roslyn-discovered roots for custom/shared code before this point. - var symbolsToInternalize = GetSymbolsByName(definitions.DeclaredSymbols, referenceMapResult.InternalizeCandidates).ToArray(); + IEnumerable symbolsToInternalize; + if (ProviderReferenceMapAnalyzer.LatestResult is { } referenceMapResult && referenceMapResult.ProjectId == project.Id) + { + // ProviderReferenceMapAnalyzer replaces Roslyn reference-map construction for generated code. + // It still uses Roslyn-discovered roots for custom/shared code before this point. + symbolsToInternalize = GetSymbolsByName(definitions.DeclaredSymbols, referenceMapResult.InternalizeCandidates).ToArray(); + } + else + { + var referenceMap = await new ReferenceMapBuilder(compilation, project).BuildPublicReferenceMapAsync( + definitions.DeclaredSymbols, definitions.DeclaredNodesCache); + var rootSymbols = await GetRootSymbolsAsync(project, definitions); + var publicSymbols = VisitSymbolsFromRootAsync(rootSymbols, referenceMap); + symbolsToInternalize = definitions.DeclaredSymbols.Except(publicSymbols); + } var nodesToInternalize = new Dictionary(); foreach (var symbol in symbolsToInternalize) @@ -232,11 +243,30 @@ public async Task RemoveAsync(Project project) // find all the declarations, including non-public declared var definitions = await GetTypeSymbolsAsync(compilation, project, false); - var referenceMapResult = ProviderReferenceMapAnalyzer.LatestResult ?? ProviderReferenceMapResult.Empty; - // The remove pass uses the same precomputed hybrid map to avoid scanning all generated - // documents with Roslyn while preserving custom-code references as roots. - var symbolsToRemove = GetSymbolsByName(definitions.DeclaredSymbols, referenceMapResult.RemoveCandidates).ToArray(); - var referencedSet = new HashSet(definitions.DeclaredSymbols.Except(symbolsToRemove), SymbolEqualityComparer.Default); + IEnumerable symbolsToRemove; + HashSet referencedSet; + if (ProviderReferenceMapAnalyzer.LatestResult is { } referenceMapResult && referenceMapResult.ProjectId == project.Id) + { + // The remove pass uses the same precomputed hybrid map to avoid scanning all generated + // documents with Roslyn while preserving custom-code references as roots. + symbolsToRemove = GetSymbolsByName(definitions.DeclaredSymbols, referenceMapResult.RemoveCandidates).ToArray(); + referencedSet = new HashSet(definitions.DeclaredSymbols.Except(symbolsToRemove), SymbolEqualityComparer.Default); + } + else + { + var referenceMap = await new ReferenceMapBuilder(compilation, project).BuildAllReferenceMapAsync( + definitions.DeclaredSymbols, definitions.DocumentsCache); + var rootSymbols = await GetRootSymbolsAsync(project, definitions); + if (_modelFactorySymbol != null) + { + rootSymbols.Add(_modelFactorySymbol); + } + + var referencedSymbols = VisitSymbolsFromRootAsync(rootSymbols, referenceMap); + referencedSymbols = AddSampleSymbols(referencedSymbols, definitions.DeclaredSymbols); + referencedSet = new HashSet(referencedSymbols, SymbolEqualityComparer.Default); + symbolsToRemove = definitions.DeclaredSymbols.Except(referencedSet); + } var nodesToRemove = new List(); foreach (var symbol in symbolsToRemove) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index ea7e39fbd74..4e9cf06aa39 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -9,6 +9,7 @@ using Microsoft.TypeSpec.Generator.Statements; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.FindSymbols; namespace Microsoft.TypeSpec.Generator { @@ -37,6 +38,10 @@ public static void Analyze(IReadOnlyList providers, Project projec var internalizeDeclaredNodes = GetPostProcessorDeclaredNodes(providers, graph.Nodes, publicOnly: true); var internalizeCandidates = internalizeDeclaredNodes.Except(internalizeReachable, StringComparer.Ordinal).OrderBy(static name => name, StringComparer.Ordinal).ToArray(); + // Body-only generated dependencies are needed to avoid deleting helper files, but they do + // not contribute to public API reachability for internalization. + AddGeneratedBodyReferences(project, providers, graph); + var removeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: true); removeRoots.UnionWith(customRoots); var removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); @@ -49,6 +54,7 @@ public static void Analyze(IReadOnlyList providers, Project projec var helperRoots = internalizeHelperRoots.Concat(removeHelperRoots).ToHashSet(StringComparer.Ordinal); _latestResult = new ProviderReferenceMapResult( + project.Id, internalizeCandidates.ToHashSet(StringComparer.Ordinal), removeCandidates.ToHashSet(StringComparer.Ordinal)); } @@ -178,6 +184,82 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro return new ProviderReferenceGraph(nodes, references); } + private static void AddGeneratedBodyReferences(Project project, IReadOnlyList providers, ProviderReferenceGraph graph) + { + var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); + if (compilation == null) + { + return; + } + + foreach (var provider in providers) + { + var providerName = GetProviderTypeName(provider.Type); + if (!graph.Nodes.Contains(providerName)) + { + continue; + } + + var symbol = compilation.GetTypeByMetadataName(providerName); + if (symbol == null) + { + continue; + } + + AddGeneratedReferencesToHelper(project, compilation, graph, providerName, symbol); + if (provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Static)) + { + foreach (var method in symbol.GetMembers().OfType()) + { + if (method.IsExtensionMethod) + { + AddGeneratedReferencesToHelper(project, compilation, graph, providerName, method); + } + } + } + } + } + + private static void AddGeneratedReferencesToHelper(Project project, Compilation compilation, ProviderReferenceGraph graph, string helperName, ISymbol symbol) + { + foreach (var reference in SymbolFinder.FindReferencesAsync(symbol, project.Solution).GetAwaiter().GetResult()) + { + foreach (var location in reference.Locations) + { + var document = location.Document; + if (!GeneratedCodeWorkspace.IsGeneratedDocument(document)) + { + continue; + } + + var root = document.GetSyntaxRootAsync().GetAwaiter().GetResult(); + if (root == null) + { + continue; + } + + var node = root.FindNode(location.Location.SourceSpan); + var owner = node.AncestorsAndSelf().OfType().FirstOrDefault(); + if (owner == null) + { + continue; + } + + var semanticModel = compilation.GetSemanticModel(owner.SyntaxTree); + if (semanticModel.GetDeclaredSymbol(owner) is not INamedTypeSymbol ownerSymbol) + { + continue; + } + + var ownerName = ownerSymbol.GetFullyQualifiedName(); + if (graph.Nodes.Contains(ownerName)) + { + graph.References[ownerName].Add(helperName); + } + } + } + } + private static HashSet GetRootNames(IReadOnlyList providers, HashSet nodes, HashSet helperRoots, bool includeModelFactory) { var generator = CodeModelGenerator.Instance; @@ -187,7 +269,7 @@ private static HashSet GetRootNames(IReadOnlyList provider foreach (var provider in providers) { var name = GetProviderTypeName(provider.Type); - if (provider.Name.EndsWith("Client", StringComparison.Ordinal) || + if (IsClientProviderRoot(provider) || IsKept(provider.Type, generator.AdditionalRootTypes, nodes) || includeModelFactory && string.Equals(name, modelFactoryName, StringComparison.Ordinal) || includeModelFactory && helperRoots.Contains(name)) @@ -225,6 +307,9 @@ private static HashSet GetPostProcessorDeclaredNodes(IReadOnlyList roots, HashSet nodes) => roots.Contains(type.Name) || roots.Contains(GetProviderTypeName(type)) && nodes.Contains(GetProviderTypeName(type)); + private static bool IsClientProviderRoot(TypeProvider provider) => + provider.RelativeFilePath.EndsWith("Client.cs", StringComparison.Ordinal); + private static bool IsModelFactoryProvider(TypeProvider provider) => provider.GetType().Name == "ModelFactoryProvider"; private static HashSet GetHelperRootNames(IReadOnlyList providers, HashSet nodes, HashSet reachableTypes) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapResult.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapResult.cs index bb73e08040c..e5623b1e7d2 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapResult.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapResult.cs @@ -2,13 +2,14 @@ // Licensed under the MIT License. using System.Collections.Generic; +using Microsoft.CodeAnalysis; namespace Microsoft.TypeSpec.Generator { internal sealed record ProviderReferenceMapResult( + ProjectId ProjectId, HashSet InternalizeCandidates, HashSet RemoveCandidates) { - public static ProviderReferenceMapResult Empty { get; } = new([], []); } } From a5f8d403fd53a6c15447016d678ee1a14cd35b63 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Fri, 12 Jun 2026 08:37:46 +0000 Subject: [PATCH 022/111] Update provider reference map benchmarks --- .../perf/FullGenerationBenchmark.cs | 20 +++ .../src/PostProcessing/PostProcessor.cs | 12 +- .../ProviderReferenceMapShadowAnalyzer.cs | 115 +++++++++++++++++- .../ProviderReferenceMapShadowResult.cs | 3 +- 4 files changed, 142 insertions(+), 8 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/FullGenerationBenchmark.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/FullGenerationBenchmark.cs index 66553e08c38..35344ec5216 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/FullGenerationBenchmark.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/FullGenerationBenchmark.cs @@ -14,9 +14,15 @@ public class FullGenerationBenchmark { private const string ProfileEnvironmentVariable = "POSTPROCESSING_BENCHMARK_PROFILE_STEPS"; private const string ProfileOutputDirectoryEnvironmentVariable = "POSTPROCESSING_BENCHMARK_PROFILE_DIR"; + private const string ShadowEnvironmentVariable = "TYPESPEC_PROVIDER_REFERENCE_MAP_SHADOW"; + private const string UseShadowEnvironmentVariable = "TYPESPEC_PROVIDER_REFERENCE_MAP_USE_SHADOW"; + private const string ShadowReportEnvironmentVariable = "TYPESPEC_PROVIDER_REFERENCE_MAP_SHADOW_REPORT"; private bool _profileSteps; + [Params(false, true)] + public bool UseProviderReferenceMap { get; set; } + [GlobalSetup] public void GlobalSetup() { @@ -35,9 +41,13 @@ public async Task GenerateSampleTypeSpecProject() CSharpGen.GenerationProfile = generationProfile; var benchmarkDirectory = CreateBenchmarkInputDirectory(); + var previousShadow = Environment.GetEnvironmentVariable(ShadowEnvironmentVariable); + var previousUseShadow = Environment.GetEnvironmentVariable(UseShadowEnvironmentVariable); + var previousShadowReport = Environment.GetEnvironmentVariable(ShadowReportEnvironmentVariable); var stopwatch = Stopwatch.StartNew(); try { + SetProviderReferenceMapEnvironment(); CodeModelGenerator.Instance = new BenchmarkCodeModelGenerator(benchmarkDirectory); CodeModelGenerator.Instance.Configure(); @@ -72,10 +82,20 @@ public async Task GenerateSampleTypeSpecProject() CSharpGen.GenerationProfile = null; GeneratedCodeWorkspace.PostProcessingProfile = null; + Environment.SetEnvironmentVariable(ShadowEnvironmentVariable, previousShadow); + Environment.SetEnvironmentVariable(UseShadowEnvironmentVariable, previousUseShadow); + Environment.SetEnvironmentVariable(ShadowReportEnvironmentVariable, previousShadowReport); TryDeleteDirectory(benchmarkDirectory); } } + private void SetProviderReferenceMapEnvironment() + { + Environment.SetEnvironmentVariable(ShadowEnvironmentVariable, UseProviderReferenceMap ? "true" : null); + Environment.SetEnvironmentVariable(UseShadowEnvironmentVariable, UseProviderReferenceMap ? "true" : null); + Environment.SetEnvironmentVariable(ShadowReportEnvironmentVariable, null); + } + private static void WriteProfile(GeneratedCodeWorkspacePostProcessingProfile profile, string fileName, string header) { var profileDirectory = GetProfileOutputDirectory(); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs index 57136754da1..13fbf1af354 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs @@ -134,7 +134,9 @@ public async Task InternalizeAsync(Project project) // first get all the declared symbols var definitions = await MeasureAsync("PostProcessor.Internalize.GetTypeSymbolsAsync", () => GetTypeSymbolsAsync(compilation, project, true)); IEnumerable symbolsToInternalize; - if (ProviderReferenceMapShadowAnalyzer.UseShadowMap && ProviderReferenceMapShadowAnalyzer.LatestResult is { } useShadowResult) + if (ProviderReferenceMapShadowAnalyzer.UseShadowMap && + ProviderReferenceMapShadowAnalyzer.LatestResult is { } useShadowResult && + useShadowResult.ProjectId == project.Id) { symbolsToInternalize = Measure("PostProcessor.Internalize.UseShadowCandidates", () => GetSymbolsByName(definitions.DeclaredSymbols, useShadowResult.InternalizeCandidates).ToArray()); @@ -155,7 +157,7 @@ await MeasureAsync( symbolsToInternalize = definitions.DeclaredSymbols.Except(publicSymbols); } - if (ProviderReferenceMapShadowAnalyzer.LatestResult is { } shadowResult) + if (ProviderReferenceMapShadowAnalyzer.LatestResult is { } shadowResult && shadowResult.ProjectId == project.Id) { ProviderReferenceMapShadowAnalyzer.WriteComparisonReport( "internalize", @@ -274,7 +276,9 @@ public async Task RemoveAsync(Project project) var definitions = await MeasureAsync("PostProcessor.Remove.GetTypeSymbolsAsync", () => GetTypeSymbolsAsync(compilation, project, false)); IEnumerable symbolsToRemove; HashSet referencedSet; - if (ProviderReferenceMapShadowAnalyzer.UseShadowMap && ProviderReferenceMapShadowAnalyzer.LatestResult is { } useShadowResult) + if (ProviderReferenceMapShadowAnalyzer.UseShadowMap && + ProviderReferenceMapShadowAnalyzer.LatestResult is { } useShadowResult && + useShadowResult.ProjectId == project.Id) { symbolsToRemove = Measure("PostProcessor.Remove.UseShadowCandidates", () => GetSymbolsByName(definitions.DeclaredSymbols, useShadowResult.RemoveCandidates).ToArray()); @@ -304,7 +308,7 @@ await MeasureAsync( symbolsToRemove = definitions.DeclaredSymbols.Except(referencedSet); } - if (ProviderReferenceMapShadowAnalyzer.LatestResult is { } shadowResult) + if (ProviderReferenceMapShadowAnalyzer.LatestResult is { } shadowResult && shadowResult.ProjectId == project.Id) { ProviderReferenceMapShadowAnalyzer.WriteComparisonReport( "remove", diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs index 7d549afb5a1..1196f44ed74 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs @@ -11,6 +11,7 @@ using Microsoft.TypeSpec.Generator.Statements; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.FindSymbols; namespace Microsoft.TypeSpec.Generator { @@ -18,6 +19,7 @@ internal static class ProviderReferenceMapShadowAnalyzer { private const string EnableEnvironmentVariable = "TYPESPEC_PROVIDER_REFERENCE_MAP_SHADOW"; private const string UseShadowEnvironmentVariable = "TYPESPEC_PROVIDER_REFERENCE_MAP_USE_SHADOW"; + private const string ReportEnvironmentVariable = "TYPESPEC_PROVIDER_REFERENCE_MAP_SHADOW_REPORT"; private const string OutputDirectoryEnvironmentVariable = "TYPESPEC_PROVIDER_REFERENCE_MAP_SHADOW_DIR"; private static ProviderReferenceMapShadowResult? _latestResult; @@ -34,6 +36,11 @@ internal static class ProviderReferenceMapShadowAnalyzer "true", StringComparison.OrdinalIgnoreCase); + private static bool ShouldWriteReports => string.Equals( + Environment.GetEnvironmentVariable(ReportEnvironmentVariable), + "true", + StringComparison.OrdinalIgnoreCase); + public static void Analyze(IReadOnlyList providers, Project project) { if (!IsEnabled) @@ -53,6 +60,8 @@ public static void Analyze(IReadOnlyList providers, Project projec var internalizeDeclaredNodes = GetPostProcessorDeclaredNodes(providers, graph.Nodes, publicOnly: true); var internalizeCandidates = internalizeDeclaredNodes.Except(internalizeReachable, StringComparer.Ordinal).OrderBy(static name => name, StringComparer.Ordinal).ToArray(); + AddGeneratedBodyReferences(project, providers, graph); + var removeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: true); removeRoots.UnionWith(customRoots); var removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); @@ -65,10 +74,14 @@ public static void Analyze(IReadOnlyList providers, Project projec var helperRoots = internalizeHelperRoots.Concat(removeHelperRoots).ToHashSet(StringComparer.Ordinal); _latestResult = new ProviderReferenceMapShadowResult( + project.Id, internalizeCandidates.ToHashSet(StringComparer.Ordinal), removeCandidates.ToHashSet(StringComparer.Ordinal)); - WriteReport(graph, customRoots, helperRoots, internalizeRoots, internalizeReachable, internalizeCandidates, removeRoots, removeReachable, removeCandidates); + if (ShouldWriteReports) + { + WriteReport(graph, customRoots, helperRoots, internalizeRoots, internalizeReachable, internalizeCandidates, removeRoots, removeReachable, removeCandidates); + } } private static HashSet GetCustomCodeGeneratedTypeRoots(Project project, HashSet generatedTypeNames) @@ -134,7 +147,7 @@ private static void AddSymbolRoot(HashSet roots, ITypeSymbol? symbol, Ha public static void WriteComparisonReport(string passName, IEnumerable roslynCandidates, IEnumerable providerCandidates) { - if (!IsEnabled) + if (!IsEnabled || !ShouldWriteReports) { return; } @@ -232,6 +245,99 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro return new ProviderReferenceGraph(nodes, references); } + private static void AddGeneratedBodyReferences(Project project, IReadOnlyList providers, ProviderReferenceGraph graph) + { + var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); + if (compilation == null) + { + return; + } + + foreach (var provider in providers) + { + if (!IsGeneratedBodyReferenceCandidate(provider)) + { + continue; + } + + var providerName = GetProviderTypeName(provider.Type); + if (!graph.Nodes.Contains(providerName)) + { + continue; + } + + var symbol = compilation.GetTypeByMetadataName(providerName); + if (symbol == null) + { + continue; + } + + AddGeneratedReferencesToHelper(project, compilation, graph, providerName, symbol); + if (provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Static)) + { + foreach (var method in symbol.GetMembers().OfType()) + { + if (method.IsExtensionMethod) + { + AddGeneratedReferencesToHelper(project, compilation, graph, providerName, method); + } + } + } + } + } + + private static bool IsGeneratedBodyReferenceCandidate(TypeProvider provider) + { + if (provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Static)) + { + return true; + } + + var relativePath = provider.RelativeFilePath.Replace('\\', '/'); + return relativePath.EndsWith("/Internal/ClientUriBuilder.cs", StringComparison.Ordinal) || + relativePath.Contains("/CollectionResults/", StringComparison.Ordinal); + } + + private static void AddGeneratedReferencesToHelper(Project project, Compilation compilation, ProviderReferenceGraph graph, string helperName, ISymbol symbol) + { + foreach (var reference in SymbolFinder.FindReferencesAsync(symbol, project.Solution).GetAwaiter().GetResult()) + { + foreach (var location in reference.Locations) + { + var document = location.Document; + if (!GeneratedCodeWorkspace.IsGeneratedDocument(document)) + { + continue; + } + + var root = document.GetSyntaxRootAsync().GetAwaiter().GetResult(); + if (root == null) + { + continue; + } + + var node = root.FindNode(location.Location.SourceSpan); + var owner = node.AncestorsAndSelf().OfType().FirstOrDefault(); + if (owner == null) + { + continue; + } + + var semanticModel = compilation.GetSemanticModel(owner.SyntaxTree); + if (semanticModel.GetDeclaredSymbol(owner) is not INamedTypeSymbol ownerSymbol) + { + continue; + } + + var ownerName = ownerSymbol.GetFullyQualifiedName(); + if (graph.Nodes.Contains(ownerName)) + { + graph.References[ownerName].Add(helperName); + } + } + } + } + private static HashSet GetRootNames(IReadOnlyList providers, HashSet nodes, HashSet helperRoots, bool includeModelFactory) { var generator = CodeModelGenerator.Instance; @@ -241,7 +347,7 @@ private static HashSet GetRootNames(IReadOnlyList provider foreach (var provider in providers) { var name = GetProviderTypeName(provider.Type); - if (provider.Name.EndsWith("Client", StringComparison.Ordinal) || + if (IsClientProviderRoot(provider) || IsKept(provider.Type, generator.AdditionalRootTypes, nodes) || includeModelFactory && string.Equals(name, modelFactoryName, StringComparison.Ordinal) || includeModelFactory && helperRoots.Contains(name)) @@ -279,6 +385,9 @@ private static HashSet GetPostProcessorDeclaredNodes(IReadOnlyList roots, HashSet nodes) => roots.Contains(type.Name) || roots.Contains(GetProviderTypeName(type)) && nodes.Contains(GetProviderTypeName(type)); + private static bool IsClientProviderRoot(TypeProvider provider) => + provider.RelativeFilePath.EndsWith("Client.cs", StringComparison.Ordinal); + private static bool IsModelFactoryProvider(TypeProvider provider) => provider.GetType().Name == "ModelFactoryProvider"; private static HashSet GetHelperRootNames(IReadOnlyList providers, HashSet nodes, HashSet reachableTypes) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowResult.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowResult.cs index 14b68f7d187..48a4038183d 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowResult.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowResult.cs @@ -2,13 +2,14 @@ // Licensed under the MIT License. using System.Collections.Generic; +using Microsoft.CodeAnalysis; namespace Microsoft.TypeSpec.Generator { internal sealed record ProviderReferenceMapShadowResult( + ProjectId ProjectId, HashSet InternalizeCandidates, HashSet RemoveCandidates) { - public static ProviderReferenceMapShadowResult Empty { get; } = new([], []); } } From c46399569b78acc2ba1597b878761e003ec190ab Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Fri, 12 Jun 2026 09:20:09 +0000 Subject: [PATCH 023/111] Track serialization providers in hybrid reference map --- .../ProviderReferenceMapAnalyzer.cs | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 4e9cf06aa39..1806f6efbf4 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -122,12 +122,13 @@ private static void AddSymbolRoot(HashSet roots, ITypeSymbol? symbol, Ha private static ProviderReferenceGraph BuildGraph(IReadOnlyList providers) { - var nodes = providers + var generatedProviders = GetGeneratedProviders(providers); + var nodes = generatedProviders .Select(static provider => GetProviderTypeName(provider.Type)) .ToHashSet(StringComparer.Ordinal); var references = nodes.ToDictionary(static name => name, _ => new HashSet(StringComparer.Ordinal), StringComparer.Ordinal); - foreach (var provider in providers) + foreach (var provider in generatedProviders) { var current = GetProviderTypeName(provider.Type); AddTypeReference(references[current], provider.Type, nodes); @@ -184,6 +185,18 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro return new ProviderReferenceGraph(nodes, references); } + private static IReadOnlyList GetGeneratedProviders(IReadOnlyList providers) + { + var generatedProviders = new List(); + foreach (var provider in providers) + { + generatedProviders.Add(provider); + generatedProviders.AddRange(provider.SerializationProviders); + } + + return generatedProviders; + } + private static void AddGeneratedBodyReferences(Project project, IReadOnlyList providers, ProviderReferenceGraph graph) { var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); @@ -194,6 +207,11 @@ private static void AddGeneratedBodyReferences(Project project, IReadOnlyList Date: Fri, 12 Jun 2026 09:27:07 +0000 Subject: [PATCH 024/111] Track serialization providers in benchmark reference map --- .../ProviderReferenceMapShadowAnalyzer.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs index 1196f44ed74..ffa8ac5b8f5 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs @@ -186,12 +186,13 @@ public static void WriteComparisonReport(string passName, IEnumerable ro private static ProviderReferenceGraph BuildGraph(IReadOnlyList providers) { - var nodes = providers + var generatedProviders = GetGeneratedProviders(providers); + var nodes = generatedProviders .Select(static provider => GetProviderTypeName(provider.Type)) .ToHashSet(StringComparer.Ordinal); var references = nodes.ToDictionary(static name => name, _ => new HashSet(StringComparer.Ordinal), StringComparer.Ordinal); - foreach (var provider in providers) + foreach (var provider in generatedProviders) { var current = GetProviderTypeName(provider.Type); AddTypeReference(references[current], provider.Type, nodes); @@ -245,6 +246,18 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro return new ProviderReferenceGraph(nodes, references); } + private static IReadOnlyList GetGeneratedProviders(IReadOnlyList providers) + { + var generatedProviders = new List(); + foreach (var provider in providers) + { + generatedProviders.Add(provider); + generatedProviders.AddRange(provider.SerializationProviders); + } + + return generatedProviders; + } + private static void AddGeneratedBodyReferences(Project project, IReadOnlyList providers, ProviderReferenceGraph graph) { var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); From 9a1d1a1d57d61e3d352002885c6fed96b1e91396 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Fri, 12 Jun 2026 10:25:28 +0000 Subject: [PATCH 025/111] Handle model factory subclasses in reference map --- .../src/PostProcessing/ProviderReferenceMapAnalyzer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 1806f6efbf4..6b5fa559497 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -340,7 +340,7 @@ private static bool IsKept(CSharpType type, HashSet roots, HashSet provider.RelativeFilePath.EndsWith("Client.cs", StringComparison.Ordinal); - private static bool IsModelFactoryProvider(TypeProvider provider) => provider.GetType().Name == "ModelFactoryProvider"; + private static bool IsModelFactoryProvider(TypeProvider provider) => provider is ModelFactoryProvider; private static HashSet GetHelperRootNames(IReadOnlyList providers, HashSet nodes, HashSet reachableTypes) { From 2917111201920d9e0f5b8f3260fe77051a4bac9a Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Mon, 15 Jun 2026 02:01:02 +0000 Subject: [PATCH 026/111] Track generated body type references --- .../ProviderReferenceMapAnalyzer.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 6b5fa559497..9c2c790f30f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -8,6 +8,7 @@ using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.Statements; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.FindSymbols; @@ -235,6 +236,8 @@ private static void AddGeneratedBodyReferences(Project project, IReadOnlyList()) + { + // Declaration names are the owner itself. The old Roslyn map captures references, + // not a declaration making itself reachable. + if (typeSyntax.Parent is BaseTypeDeclarationSyntax baseTypeDeclaration && baseTypeDeclaration.Identifier.Span == typeSyntax.Span) + { + continue; + } + + AddBodyTypeReference(graph.References[ownerName], semanticModel.GetTypeInfo(typeSyntax).Type, graph.Nodes); + } + } + } + + private static void AddBodyTypeReference(HashSet references, ITypeSymbol? symbol, HashSet nodes) + { + if (symbol is not INamedTypeSymbol namedType || namedType.TypeKind == TypeKind.Error) + { + return; + } + + AddMatchingName(references, namedType.GetFullyQualifiedName(), nodes); + foreach (var typeArgument in namedType.TypeArguments) + { + AddBodyTypeReference(references, typeArgument, nodes); + } + } + private static void AddGeneratedReferencesToHelper(Project project, Compilation compilation, ProviderReferenceGraph graph, string helperName, ISymbol symbol) { foreach (var reference in SymbolFinder.FindReferencesAsync(symbol, project.Solution).GetAwaiter().GetResult()) From 2cda1846d2a99112dd56a39ad4bc3f614a4ade7d Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Mon, 15 Jun 2026 03:51:16 +0000 Subject: [PATCH 027/111] Match Roslyn cleanup for body references --- .../ProviderReferenceMapAnalyzer.cs | 55 ++++++++++++++++--- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 9c2c790f30f..212fb4129f0 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -136,6 +136,11 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro AddTypeReference(references[current], provider.BaseType, nodes); AddTypeReference(references[current], provider.DeclaringTypeProvider?.Type, nodes); + if (IsKept(provider.Type, CodeModelGenerator.Instance.NonRootTypes, nodes)) + { + continue; + } + // Model factory signatures mention many models. The existing Roslyn post-processor // removes factory methods for unreachable models, so model factory should only // contribute helper dependencies, not model reachability edges. @@ -206,8 +211,13 @@ private static void AddGeneratedBodyReferences(Project project, IReadOnlyList()) + AddGeneratedReferencesToHelper(project, compilation, graph, providerName, symbol); + if (provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Static)) { - if (method.IsExtensionMethod) + foreach (var method in symbol.GetMembers().OfType()) { - AddGeneratedReferencesToHelper(project, compilation, graph, providerName, method); + if (method.IsExtensionMethod) + { + AddGeneratedReferencesToHelper(project, compilation, graph, providerName, method); + } } } } @@ -241,6 +254,18 @@ private static void AddGeneratedBodyReferences(Project project, IReadOnlyList GetBodyReferenceProviders(IReadOnlyList providers) + { + var bodyReferenceProviders = new List(); + foreach (var provider in providers) + { + bodyReferenceProviders.Add(provider); + bodyReferenceProviders.AddRange(provider.SerializationProviders); + } + + return bodyReferenceProviders; + } + private static bool IsGeneratedBodyReferenceCandidate(TypeProvider provider) { if (provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Static)) @@ -249,7 +274,9 @@ private static bool IsGeneratedBodyReferenceCandidate(TypeProvider provider) } var relativePath = provider.RelativeFilePath.Replace('\\', '/'); - return relativePath.EndsWith("/Internal/ClientUriBuilder.cs", StringComparison.Ordinal) || + return IsSerializationProvider(provider) || + relativePath.EndsWith("Client.cs", StringComparison.Ordinal) || + relativePath.EndsWith("/Internal/ClientUriBuilder.cs", StringComparison.Ordinal) || relativePath.Contains("/CollectionResults/", StringComparison.Ordinal); } @@ -388,7 +415,7 @@ private static bool IsClientProviderRoot(TypeProvider provider) => private static HashSet GetHelperRootNames(IReadOnlyList providers, HashSet nodes, HashSet reachableTypes) { var roots = new HashSet(StringComparer.Ordinal); - foreach (var provider in providers) + foreach (var provider in GetGeneratedProviders(providers)) { var providerName = GetProviderTypeName(provider.Type); var isModelFactory = IsModelFactoryProvider(provider); @@ -397,6 +424,11 @@ private static HashSet GetHelperRootNames(IReadOnlyList pr continue; } + if (IsSerializationProvider(provider)) + { + AddMatchingName(roots, "ChangeTrackingDictionary", nodes); + } + foreach (var property in provider.Properties) { AddInitializationHelperRoot(roots, property.Type, nodes); @@ -447,6 +479,13 @@ private static void AddParameterValidationHelperRoot(HashSet roots, Para } } + private static bool IsSerializationProvider(TypeProvider provider) + { + var relativePath = provider.RelativeFilePath.Replace('\\', '/'); + return relativePath.EndsWith(".Serialization.cs", StringComparison.Ordinal) || + relativePath.EndsWith(".Serialization.Multipart.cs", StringComparison.Ordinal); + } + private static void AddInitializationHelperRoot(HashSet roots, CSharpType? type, HashSet nodes) { if (type == null) From ce16922e1f778a6333b107936643beb137c67ac1 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Mon, 15 Jun 2026 05:33:53 +0000 Subject: [PATCH 028/111] Remove unused serialization providers --- .../src/PostProcessing/ProviderReferenceMapAnalyzer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 212fb4129f0..90408e0391d 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -395,7 +395,7 @@ private static HashSet GetPostProcessorDeclaredNodes(IReadOnlyList !IsModelFactoryProvider(provider)) .Where(provider => !publicOnly || provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) .Select(provider => GetProviderTypeName(provider.Type)) From 5a8248f6acd5c448b9a2057ddbac4494cb3d8b09 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Mon, 15 Jun 2026 07:24:15 +0000 Subject: [PATCH 029/111] Preserve public discriminator subtypes --- .../ProviderReferenceMapAnalyzer.cs | 59 ++++++++++++++++++- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 90408e0391d..4cca7806c36 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -23,6 +23,7 @@ internal static class ProviderReferenceMapAnalyzer public static void Analyze(IReadOnlyList providers, Project project) { var graph = BuildGraph(providers); + var publicGraph = BuildGraph(providers, publicOnly: true); // Generated-code dependencies come from providers. Custom code still needs Roslyn // because arbitrary user C# can reference generated types in ways providers cannot see. @@ -30,12 +31,14 @@ public static void Analyze(IReadOnlyList providers, Project projec // Helper types are rooted after an initial reachability pass so unused infrastructure // such as change-tracking dictionaries can still be removed when no reachable type needs them. + var internalizeReferences = CloneReferences(publicGraph.References); + AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences); var internalizeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: false); internalizeRoots.UnionWith(customRoots); - var internalizeReachableWithoutHelpers = GetReachableTypes(internalizeRoots, graph.References); + var internalizeReachableWithoutHelpers = GetReachableTypes(internalizeRoots, internalizeReferences); var internalizeHelperRoots = GetHelperRootNames(providers, graph.Nodes, internalizeReachableWithoutHelpers); internalizeRoots.UnionWith(internalizeHelperRoots); - var internalizeReachable = GetReachableTypes(internalizeRoots, graph.References); + var internalizeReachable = GetReachableTypes(internalizeRoots, internalizeReferences); var internalizeDeclaredNodes = GetPostProcessorDeclaredNodes(providers, graph.Nodes, publicOnly: true); var internalizeCandidates = internalizeDeclaredNodes.Except(internalizeReachable, StringComparer.Ordinal).OrderBy(static name => name, StringComparer.Ordinal).ToArray(); @@ -121,7 +124,7 @@ private static void AddSymbolRoot(HashSet roots, ITypeSymbol? symbol, Ha } } - private static ProviderReferenceGraph BuildGraph(IReadOnlyList providers) + private static ProviderReferenceGraph BuildGraph(IReadOnlyList providers, bool publicOnly = false) { var generatedProviders = GetGeneratedProviders(providers); var nodes = generatedProviders @@ -166,6 +169,11 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro foreach (var property in provider.Properties) { + if (publicOnly && !IsPublic(property.Modifiers)) + { + continue; + } + AddTypeReference(references[current], property.Type, nodes); AddTypeReference(references[current], property.ExplicitInterface, nodes); AddAttributes(references[current], property.Attributes, nodes); @@ -173,17 +181,32 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro foreach (var field in provider.Fields) { + if (publicOnly && !field.Modifiers.HasFlag(FieldModifiers.Public)) + { + continue; + } + AddTypeReference(references[current], field.Type, nodes); AddAttributes(references[current], field.Attributes, nodes); } foreach (var constructor in provider.Constructors) { + if (publicOnly && !IsPublic(constructor.Signature.Modifiers)) + { + continue; + } + AddSignatureReferences(references[current], constructor.Signature, nodes); } foreach (var method in provider.Methods) { + if (publicOnly && !IsPublic(method.Signature.Modifiers)) + { + continue; + } + AddSignatureReferences(references[current], method.Signature, nodes); } } @@ -191,6 +214,36 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro return new ProviderReferenceGraph(nodes, references); } + private static bool IsPublic(MethodSignatureModifiers modifiers) => modifiers.HasFlag(MethodSignatureModifiers.Public); + + private static Dictionary> CloneReferences(IReadOnlyDictionary> references) + { + return references.ToDictionary( + static item => item.Key, + static item => item.Value.ToHashSet(StringComparer.Ordinal), + StringComparer.Ordinal); + } + + private static void AddDerivedModelReferences( + IReadOnlyList providers, + HashSet nodes, + Dictionary> references) + { + foreach (var provider in providers.OfType()) + { + var providerName = GetProviderTypeName(provider.Type); + if (!nodes.Contains(providerName)) + { + continue; + } + + foreach (var derivedModel in provider.DerivedModels) + { + AddTypeReference(references[providerName], derivedModel.Type, nodes); + } + } + } + private static IReadOnlyList GetGeneratedProviders(IReadOnlyList providers) { var generatedProviders = new List(); From 27e121e9d21dbf528e9b62bad27e15baa919372f Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Mon, 15 Jun 2026 08:17:31 +0000 Subject: [PATCH 030/111] Sync benchmark reference map parity fixes --- .../ProviderReferenceMapShadowAnalyzer.cs | 159 ++++++++++++++++-- 1 file changed, 146 insertions(+), 13 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs index ffa8ac5b8f5..68d8a56ef19 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs @@ -10,6 +10,7 @@ using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.Statements; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.FindSymbols; @@ -50,13 +51,16 @@ public static void Analyze(IReadOnlyList providers, Project projec } var graph = BuildGraph(providers); + var publicGraph = BuildGraph(providers, publicOnly: true); var customRoots = GetCustomCodeGeneratedTypeRoots(project, graph.Nodes); + var internalizeReferences = CloneReferences(publicGraph.References); + AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences); var internalizeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: false); internalizeRoots.UnionWith(customRoots); - var internalizeReachableWithoutHelpers = GetReachableTypes(internalizeRoots, graph.References); + var internalizeReachableWithoutHelpers = GetReachableTypes(internalizeRoots, internalizeReferences); var internalizeHelperRoots = GetHelperRootNames(providers, graph.Nodes, internalizeReachableWithoutHelpers); internalizeRoots.UnionWith(internalizeHelperRoots); - var internalizeReachable = GetReachableTypes(internalizeRoots, graph.References); + var internalizeReachable = GetReachableTypes(internalizeRoots, internalizeReferences); var internalizeDeclaredNodes = GetPostProcessorDeclaredNodes(providers, graph.Nodes, publicOnly: true); var internalizeCandidates = internalizeDeclaredNodes.Except(internalizeReachable, StringComparer.Ordinal).OrderBy(static name => name, StringComparer.Ordinal).ToArray(); @@ -184,7 +188,7 @@ public static void WriteComparisonReport(string passName, IEnumerable ro CodeModelGenerator.Instance.Emitter.Debug($"Provider reference map shadow comparison written to {path}"); } - private static ProviderReferenceGraph BuildGraph(IReadOnlyList providers) + private static ProviderReferenceGraph BuildGraph(IReadOnlyList providers, bool publicOnly = false) { var generatedProviders = GetGeneratedProviders(providers); var nodes = generatedProviders @@ -199,6 +203,11 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro AddTypeReference(references[current], provider.BaseType, nodes); AddTypeReference(references[current], provider.DeclaringTypeProvider?.Type, nodes); + if (IsKept(provider.Type, CodeModelGenerator.Instance.NonRootTypes, nodes)) + { + continue; + } + if (IsModelFactoryProvider(provider)) { continue; @@ -221,6 +230,11 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro foreach (var property in provider.Properties) { + if (publicOnly && !IsPublic(property.Modifiers)) + { + continue; + } + AddTypeReference(references[current], property.Type, nodes); AddTypeReference(references[current], property.ExplicitInterface, nodes); AddAttributes(references[current], property.Attributes, nodes); @@ -228,17 +242,32 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro foreach (var field in provider.Fields) { + if (publicOnly && !field.Modifiers.HasFlag(FieldModifiers.Public)) + { + continue; + } + AddTypeReference(references[current], field.Type, nodes); AddAttributes(references[current], field.Attributes, nodes); } foreach (var constructor in provider.Constructors) { + if (publicOnly && !IsPublic(constructor.Signature.Modifiers)) + { + continue; + } + AddSignatureReferences(references[current], constructor.Signature, nodes); } foreach (var method in provider.Methods) { + if (publicOnly && !IsPublic(method.Signature.Modifiers)) + { + continue; + } + AddSignatureReferences(references[current], method.Signature, nodes); } } @@ -246,6 +275,36 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro return new ProviderReferenceGraph(nodes, references); } + private static bool IsPublic(MethodSignatureModifiers modifiers) => modifiers.HasFlag(MethodSignatureModifiers.Public); + + private static Dictionary> CloneReferences(IReadOnlyDictionary> references) + { + return references.ToDictionary( + static item => item.Key, + static item => item.Value.ToHashSet(StringComparer.Ordinal), + StringComparer.Ordinal); + } + + private static void AddDerivedModelReferences( + IReadOnlyList providers, + HashSet nodes, + Dictionary> references) + { + foreach (var provider in providers.OfType()) + { + var providerName = GetProviderTypeName(provider.Type); + if (!nodes.Contains(providerName)) + { + continue; + } + + foreach (var derivedModel in provider.DerivedModels) + { + AddTypeReference(references[providerName], derivedModel.Type, nodes); + } + } + } + private static IReadOnlyList GetGeneratedProviders(IReadOnlyList providers) { var generatedProviders = new List(); @@ -266,8 +325,13 @@ private static void AddGeneratedBodyReferences(Project project, IReadOnlyList()) + AddGeneratedReferencesToHelper(project, compilation, graph, providerName, symbol); + if (provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Static)) { - if (method.IsExtensionMethod) + foreach (var method in symbol.GetMembers().OfType()) { - AddGeneratedReferencesToHelper(project, compilation, graph, providerName, method); + if (method.IsExtensionMethod) + { + AddGeneratedReferencesToHelper(project, compilation, graph, providerName, method); + } } } } + + AddGeneratedBodyTypeReferences(project, compilation, graph, providerName, symbol); } } + private static IReadOnlyList GetBodyReferenceProviders(IReadOnlyList providers) + { + var bodyReferenceProviders = new List(); + foreach (var provider in providers) + { + bodyReferenceProviders.Add(provider); + bodyReferenceProviders.AddRange(provider.SerializationProviders); + } + + return bodyReferenceProviders; + } + private static bool IsGeneratedBodyReferenceCandidate(TypeProvider provider) { if (provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Static)) @@ -307,10 +388,50 @@ private static bool IsGeneratedBodyReferenceCandidate(TypeProvider provider) } var relativePath = provider.RelativeFilePath.Replace('\\', '/'); - return relativePath.EndsWith("/Internal/ClientUriBuilder.cs", StringComparison.Ordinal) || + return IsSerializationProvider(provider) || + relativePath.EndsWith("Client.cs", StringComparison.Ordinal) || + relativePath.EndsWith("/Internal/ClientUriBuilder.cs", StringComparison.Ordinal) || relativePath.Contains("/CollectionResults/", StringComparison.Ordinal); } + private static void AddGeneratedBodyTypeReferences(Project project, Compilation compilation, ProviderReferenceGraph graph, string ownerName, INamedTypeSymbol ownerSymbol) + { + foreach (var syntaxReference in ownerSymbol.DeclaringSyntaxReferences) + { + var document = project.GetDocument(syntaxReference.SyntaxTree); + if (document == null || !GeneratedCodeWorkspace.IsGeneratedDocument(document)) + { + continue; + } + + var root = syntaxReference.SyntaxTree.GetRoot(); + var semanticModel = compilation.GetSemanticModel(syntaxReference.SyntaxTree); + foreach (var typeSyntax in root.DescendantNodes().OfType()) + { + if (typeSyntax.Parent is BaseTypeDeclarationSyntax baseTypeDeclaration && baseTypeDeclaration.Identifier.Span == typeSyntax.Span) + { + continue; + } + + AddBodyTypeReference(graph.References[ownerName], semanticModel.GetTypeInfo(typeSyntax).Type, graph.Nodes); + } + } + } + + private static void AddBodyTypeReference(HashSet references, ITypeSymbol? symbol, HashSet nodes) + { + if (symbol is not INamedTypeSymbol namedType || namedType.TypeKind == TypeKind.Error) + { + return; + } + + AddMatchingName(references, namedType.GetFullyQualifiedName(), nodes); + foreach (var typeArgument in namedType.TypeArguments) + { + AddBodyTypeReference(references, typeArgument, nodes); + } + } + private static void AddGeneratedReferencesToHelper(Project project, Compilation compilation, ProviderReferenceGraph graph, string helperName, ISymbol symbol) { foreach (var reference in SymbolFinder.FindReferencesAsync(symbol, project.Solution).GetAwaiter().GetResult()) @@ -386,7 +507,7 @@ private static HashSet GetPostProcessorDeclaredNodes(IReadOnlyList !IsModelFactoryProvider(provider)) .Where(provider => !publicOnly || provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) .Select(provider => GetProviderTypeName(provider.Type)) @@ -401,12 +522,12 @@ private static bool IsKept(CSharpType type, HashSet roots, HashSet provider.RelativeFilePath.EndsWith("Client.cs", StringComparison.Ordinal); - private static bool IsModelFactoryProvider(TypeProvider provider) => provider.GetType().Name == "ModelFactoryProvider"; + private static bool IsModelFactoryProvider(TypeProvider provider) => provider is ModelFactoryProvider; private static HashSet GetHelperRootNames(IReadOnlyList providers, HashSet nodes, HashSet reachableTypes) { var roots = new HashSet(StringComparer.Ordinal); - foreach (var provider in providers) + foreach (var provider in GetGeneratedProviders(providers)) { var providerName = GetProviderTypeName(provider.Type); var isModelFactory = IsModelFactoryProvider(provider); @@ -415,6 +536,11 @@ private static HashSet GetHelperRootNames(IReadOnlyList pr continue; } + if (IsSerializationProvider(provider)) + { + AddMatchingName(roots, "ChangeTrackingDictionary", nodes); + } + foreach (var property in provider.Properties) { AddInitializationHelperRoot(roots, property.Type, nodes); @@ -464,6 +590,13 @@ private static void AddParameterValidationHelperRoot(HashSet roots, Para } } + private static bool IsSerializationProvider(TypeProvider provider) + { + var relativePath = provider.RelativeFilePath.Replace('\\', '/'); + return relativePath.EndsWith(".Serialization.cs", StringComparison.Ordinal) || + relativePath.EndsWith(".Serialization.Multipart.cs", StringComparison.Ordinal); + } + private static void AddInitializationHelperRoot(HashSet roots, CSharpType? type, HashSet nodes) { if (type == null) From 43529f3423377b2af99ebc16a49e9e74128d091b Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Mon, 15 Jun 2026 09:28:23 +0000 Subject: [PATCH 031/111] Use explicit serialization helper dependencies --- .../src/Providers/MrwSerializationTypeDefinition.cs | 4 ++++ .../MultipartFormDataSerializationDefinition.cs | 6 ++++++ .../PostProcessing/ProviderReferenceMapAnalyzer.cs | 13 +++++++++---- .../src/Providers/TypeProvider.cs | 3 +++ 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs index 0d02ecba187..fbcb895c72d 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs @@ -117,6 +117,10 @@ public MrwSerializationTypeDefinition(InputModelType inputModel, ModelProvider m protected override CSharpType? BuildBaseType() => _model.BaseType; + protected override IReadOnlyList BuildHelperDependencyNames() => _rawDataField != null || _additionalProperties.Value.Length > 0 + ? ["ChangeTrackingDictionary"] + : []; + protected override SuppressionStatement[] BuildDisabledFileWarnings() { if (_model.CanonicalView.Properties.Any(p => ScmModelProvider.IsFileBinaryContentType(p.Type))) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MultipartFormDataSerializationDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MultipartFormDataSerializationDefinition.cs index 88cb97b16e7..e390ab24707 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MultipartFormDataSerializationDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MultipartFormDataSerializationDefinition.cs @@ -51,6 +51,12 @@ protected override string BuildRelativeFilePath() return Path.Combine("src", "Generated", "Models", $"{Name}.Serialization.Multipart.cs"); } + protected override IReadOnlyList BuildHelperDependencyNames() => _model.Properties.Any( + prop => prop.WireInfo != null && !prop.WireInfo.IsRequired && + (prop.Type is { IsCollection: true, IsReadOnlyMemory: false } || prop.Type.IsDictionary)) + ? ["Optional"] + : []; + protected override SuppressionStatement[] BuildDisabledFileWarnings() => [new SuppressionStatement(null, Literal(ScmModelProvider.FileBinaryContentDiagnosticId), ScmModelProvider.ScmEvaluationTypeSuppressionJustification)]; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 4cca7806c36..9558ad998e4 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -477,10 +477,7 @@ private static HashSet GetHelperRootNames(IReadOnlyList pr continue; } - if (IsSerializationProvider(provider)) - { - AddMatchingName(roots, "ChangeTrackingDictionary", nodes); - } + AddHelperDependencies(roots, provider.HelperDependencyNames, nodes); foreach (var property in provider.Properties) { @@ -532,6 +529,14 @@ private static void AddParameterValidationHelperRoot(HashSet roots, Para } } + private static void AddHelperDependencies(HashSet roots, IReadOnlyList dependencies, HashSet nodes) + { + foreach (var dependency in dependencies) + { + AddMatchingName(roots, dependency, nodes); + } + } + private static bool IsSerializationProvider(TypeProvider provider) { var relativePath = provider.RelativeFilePath.Replace('\\', '/'); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs index 7a3047886b6..a29a1449461 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs @@ -272,6 +272,9 @@ private IReadOnlyList ApplyCustomizationFilter(IEnumerable SerializationProviders => _serializationProviders ??= BuildSerializationProviders(); + internal IReadOnlyList HelperDependencyNames => BuildHelperDependencyNames(); + protected internal virtual IReadOnlyList BuildHelperDependencyNames() => []; + private IReadOnlyList? _attributes; public IReadOnlyList Attributes From 21ecc1a18685ecf2a41156dfcd21b6baac3dafa0 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Mon, 15 Jun 2026 09:39:04 +0000 Subject: [PATCH 032/111] Use explicit collection result dependencies --- .../Providers/CollectionResultDefinition.cs | 16 ++++++++++++ .../ProviderReferenceMapAnalyzer.cs | 26 ++++++++++++++++++- .../src/Providers/TypeProvider.cs | 3 +++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/CollectionResultDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/CollectionResultDefinition.cs index ae617957bf5..590eaf2b935 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/CollectionResultDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/CollectionResultDefinition.cs @@ -217,6 +217,22 @@ private bool HasPagingOperationNameCollision(string operationName) protected override TypeSignatureModifiers BuildDeclarationModifiers() => TypeSignatureModifiers.Internal | TypeSignatureModifiers.Partial | TypeSignatureModifiers.Class; + protected override IReadOnlyList BuildBodyDependencyTypes() + { + var dependencies = new List { Client.Type, ResponseModelType, NextPagePropertyType }; + if (ItemModelType != null) + { + dependencies.Add(ItemModelType); + } + + foreach (var field in RequestFields) + { + dependencies.Add(field.Type); + } + + return dependencies; + } + protected override FieldProvider[] BuildFields() => [ClientField, .. RequestFields]; protected override CSharpType[] BuildImplements() => diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 9558ad998e4..f0a6ee8e627 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -208,12 +208,21 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro } AddSignatureReferences(references[current], method.Signature, nodes); + AddTypeReference(references[current], GetCollectionDefinitionType(method), nodes); } } return new ProviderReferenceGraph(nodes, references); } + private static CSharpType? GetCollectionDefinitionType(MethodProvider method) + { + var property = method.GetType().GetProperty("CollectionDefinition"); + return property?.GetValue(method) is TypeProvider collectionDefinition + ? collectionDefinition.Type + : null; + } + private static bool IsPublic(MethodSignatureModifiers modifiers) => modifiers.HasFlag(MethodSignatureModifiers.Public); private static Dictionary> CloneReferences(IReadOnlyDictionary> references) @@ -282,6 +291,13 @@ private static void AddGeneratedBodyReferences(Project project, IReadOnlyList 0) + { + continue; + } + var symbol = compilation.GetTypeByMetadataName(providerName); if (symbol == null) { @@ -307,6 +323,14 @@ private static void AddGeneratedBodyReferences(Project project, IReadOnlyList references, IReadOnlyList dependencies, HashSet nodes) + { + foreach (var dependency in dependencies) + { + AddTypeReference(references, dependency, nodes); + } + } + private static IReadOnlyList GetBodyReferenceProviders(IReadOnlyList providers) { var bodyReferenceProviders = new List(); @@ -330,7 +354,7 @@ private static bool IsGeneratedBodyReferenceCandidate(TypeProvider provider) return IsSerializationProvider(provider) || relativePath.EndsWith("Client.cs", StringComparison.Ordinal) || relativePath.EndsWith("/Internal/ClientUriBuilder.cs", StringComparison.Ordinal) || - relativePath.Contains("/CollectionResults/", StringComparison.Ordinal); + provider.BodyDependencyTypes.Count > 0; } private static void AddGeneratedBodyTypeReferences(Project project, Compilation compilation, ProviderReferenceGraph graph, string ownerName, INamedTypeSymbol ownerSymbol) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs index a29a1449461..9762dfbf311 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs @@ -275,6 +275,9 @@ private IReadOnlyList ApplyCustomizationFilter(IEnumerable HelperDependencyNames => BuildHelperDependencyNames(); protected internal virtual IReadOnlyList BuildHelperDependencyNames() => []; + internal IReadOnlyList BodyDependencyTypes => BuildBodyDependencyTypes(); + protected internal virtual IReadOnlyList BuildBodyDependencyTypes() => []; + private IReadOnlyList? _attributes; public IReadOnlyList Attributes From 8bb0d2983d0323e163d7d6dbb52651e360969732 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Mon, 15 Jun 2026 11:52:46 +0000 Subject: [PATCH 033/111] Benchmark explicit provider dependencies --- .../Providers/CollectionResultDefinition.cs | 16 ++++++++ .../MrwSerializationTypeDefinition.cs | 4 ++ .../ProviderReferenceMapShadowAnalyzer.cs | 39 ++++++++++++++++--- .../src/Providers/TypeProvider.cs | 6 +++ 4 files changed, 60 insertions(+), 5 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/CollectionResultDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/CollectionResultDefinition.cs index ae617957bf5..590eaf2b935 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/CollectionResultDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/CollectionResultDefinition.cs @@ -217,6 +217,22 @@ private bool HasPagingOperationNameCollision(string operationName) protected override TypeSignatureModifiers BuildDeclarationModifiers() => TypeSignatureModifiers.Internal | TypeSignatureModifiers.Partial | TypeSignatureModifiers.Class; + protected override IReadOnlyList BuildBodyDependencyTypes() + { + var dependencies = new List { Client.Type, ResponseModelType, NextPagePropertyType }; + if (ItemModelType != null) + { + dependencies.Add(ItemModelType); + } + + foreach (var field in RequestFields) + { + dependencies.Add(field.Type); + } + + return dependencies; + } + protected override FieldProvider[] BuildFields() => [ClientField, .. RequestFields]; protected override CSharpType[] BuildImplements() => diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs index 43688790768..254abf3509a 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs @@ -115,6 +115,10 @@ public MrwSerializationTypeDefinition(InputModelType inputModel, ModelProvider m protected override CSharpType? BuildBaseType() => _model.BaseType; + protected override IReadOnlyList BuildHelperDependencyNames() => _rawDataField != null || _additionalProperties.Value.Length > 0 + ? ["ChangeTrackingDictionary"] + : []; + protected override IReadOnlyList BuildAttributes() { if (_model.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Abstract)) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs index 68d8a56ef19..35a3467ea56 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs @@ -269,12 +269,21 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro } AddSignatureReferences(references[current], method.Signature, nodes); + AddTypeReference(references[current], GetCollectionDefinitionType(method), nodes); } } return new ProviderReferenceGraph(nodes, references); } + private static CSharpType? GetCollectionDefinitionType(MethodProvider method) + { + var property = method.GetType().GetProperty("CollectionDefinition"); + return property?.GetValue(method) is TypeProvider collectionDefinition + ? collectionDefinition.Type + : null; + } + private static bool IsPublic(MethodSignatureModifiers modifiers) => modifiers.HasFlag(MethodSignatureModifiers.Public); private static Dictionary> CloneReferences(IReadOnlyDictionary> references) @@ -343,6 +352,13 @@ private static void AddGeneratedBodyReferences(Project project, IReadOnlyList 0) + { + continue; + } + var symbol = compilation.GetTypeByMetadataName(providerName); if (symbol == null) { @@ -368,6 +384,14 @@ private static void AddGeneratedBodyReferences(Project project, IReadOnlyList references, IReadOnlyList dependencies, HashSet nodes) + { + foreach (var dependency in dependencies) + { + AddTypeReference(references, dependency, nodes); + } + } + private static IReadOnlyList GetBodyReferenceProviders(IReadOnlyList providers) { var bodyReferenceProviders = new List(); @@ -391,7 +415,7 @@ private static bool IsGeneratedBodyReferenceCandidate(TypeProvider provider) return IsSerializationProvider(provider) || relativePath.EndsWith("Client.cs", StringComparison.Ordinal) || relativePath.EndsWith("/Internal/ClientUriBuilder.cs", StringComparison.Ordinal) || - relativePath.Contains("/CollectionResults/", StringComparison.Ordinal); + provider.BodyDependencyTypes.Count > 0; } private static void AddGeneratedBodyTypeReferences(Project project, Compilation compilation, ProviderReferenceGraph graph, string ownerName, INamedTypeSymbol ownerSymbol) @@ -536,10 +560,7 @@ private static HashSet GetHelperRootNames(IReadOnlyList pr continue; } - if (IsSerializationProvider(provider)) - { - AddMatchingName(roots, "ChangeTrackingDictionary", nodes); - } + AddHelperDependencies(roots, provider.HelperDependencyNames, nodes); foreach (var property in provider.Properties) { @@ -597,6 +618,14 @@ private static bool IsSerializationProvider(TypeProvider provider) relativePath.EndsWith(".Serialization.Multipart.cs", StringComparison.Ordinal); } + private static void AddHelperDependencies(HashSet roots, IReadOnlyList dependencies, HashSet nodes) + { + foreach (var dependency in dependencies) + { + AddMatchingName(roots, dependency, nodes); + } + } + private static void AddInitializationHelperRoot(HashSet roots, CSharpType? type, HashSet nodes) { if (type == null) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs index 59c356a16c9..962f65c39f3 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs @@ -269,6 +269,12 @@ private IReadOnlyList ApplyCustomizationFilter(IEnumerable SerializationProviders => _serializationProviders ??= BuildSerializationProviders(); + internal IReadOnlyList HelperDependencyNames => BuildHelperDependencyNames(); + protected internal virtual IReadOnlyList BuildHelperDependencyNames() => []; + + internal IReadOnlyList BodyDependencyTypes => BuildBodyDependencyTypes(); + protected internal virtual IReadOnlyList BuildBodyDependencyTypes() => []; + private IReadOnlyList? _attributes; public IReadOnlyList Attributes From f3259a46e7be10b749a0a5b9286b818138f0861e Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Mon, 15 Jun 2026 12:44:13 +0000 Subject: [PATCH 034/111] Use explicit client body dependencies --- .../src/Providers/ClientProvider.cs | 49 +++++++++++++++++++ .../ProviderReferenceMapAnalyzer.cs | 1 - 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs index 75cd23026da..bd321019175 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs @@ -424,6 +424,55 @@ private IReadOnlyList GetClientParameters() protected override string BuildName() => _inputClient.IsExactName ? _inputClient.Name : _inputClient.Name.ToIdentifierName(); + protected override IReadOnlyList BuildBodyDependencyTypes() + { + var dependencies = new List(); + foreach (var method in Methods.OfType()) + { + if (method.CollectionDefinition != null) + { + dependencies.Add(method.CollectionDefinition.Type); + } + + if (method.ServiceMethod == null) + { + continue; + } + + AddInputTypeDependency(dependencies, method.ServiceMethod.Response.Type); + AddInputTypeDependency(dependencies, method.ServiceMethod.Exception?.Type); + foreach (var parameter in method.ServiceMethod.Parameters) + { + AddInputTypeDependency(dependencies, parameter.Type); + } + + foreach (var parameter in method.ServiceMethod.Operation.Parameters) + { + AddInputTypeDependency(dependencies, parameter.Type); + } + + foreach (var response in method.ServiceMethod.Operation.Responses) + { + AddInputTypeDependency(dependencies, response.BodyType); + foreach (var header in response.Headers) + { + AddInputTypeDependency(dependencies, header.Type); + } + } + } + + return dependencies; + } + + private static void AddInputTypeDependency(List dependencies, InputType? inputType) + { + var type = inputType == null ? null : ScmCodeModelGenerator.Instance.TypeFactory.CreateCSharpType(inputType); + if (type != null) + { + dependencies.Add(type); + } + } + protected override FieldProvider[] BuildFields() { List fields = [EndpointField]; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index f0a6ee8e627..7c23a67a68b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -352,7 +352,6 @@ private static bool IsGeneratedBodyReferenceCandidate(TypeProvider provider) var relativePath = provider.RelativeFilePath.Replace('\\', '/'); return IsSerializationProvider(provider) || - relativePath.EndsWith("Client.cs", StringComparison.Ordinal) || relativePath.EndsWith("/Internal/ClientUriBuilder.cs", StringComparison.Ordinal) || provider.BodyDependencyTypes.Count > 0; } From b6e8aad1d6265b119e21c27827636fbecfddb1e7 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Mon, 15 Jun 2026 13:20:27 +0000 Subject: [PATCH 035/111] Track rest client helper dependencies --- .../src/Providers/RestClientProvider.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs index ec53be226f3..8cc5fea2368 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs @@ -78,6 +78,33 @@ protected override FieldProvider[] BuildFields() return [.. pipelineMessage20xClassifiersFields]; } + protected override IReadOnlyList BuildHelperDependencyNames() + { + var dependencies = new HashSet(StringComparer.Ordinal); + foreach (var serviceMethod in _inputClient.Methods) + { + foreach (var parameter in serviceMethod.Operation.Parameters) + { + if (parameter is not InputHeaderParameter and not InputQueryParameter) + { + continue; + } + + var type = ScmCodeModelGenerator.Instance.TypeFactory.CreateCSharpType(parameter.Type); + if (type?.IsDictionary == true) + { + dependencies.Add("ChangeTrackingDictionary"); + } + else if (type?.IsCollection == true) + { + dependencies.Add("ChangeTrackingList"); + } + } + } + + return [.. dependencies]; + } + protected override ScmMethodProvider[] BuildMethods() { List methods = new List(); From 74bd30c51f43d46f64ab76beed6b9540b95ce824 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Mon, 15 Jun 2026 12:44:13 +0000 Subject: [PATCH 036/111] Use explicit client body dependencies --- .../src/Providers/ClientProvider.cs | 49 +++++++++++++++++++ .../ProviderReferenceMapShadowAnalyzer.cs | 1 - 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs index a0e522fb0ef..69bc3741eed 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs @@ -424,6 +424,55 @@ private IReadOnlyList GetClientParameters() protected override string BuildName() => _inputClient.IsExactName ? _inputClient.Name : _inputClient.Name.ToIdentifierName(); + protected override IReadOnlyList BuildBodyDependencyTypes() + { + var dependencies = new List(); + foreach (var method in Methods.OfType()) + { + if (method.CollectionDefinition != null) + { + dependencies.Add(method.CollectionDefinition.Type); + } + + if (method.ServiceMethod == null) + { + continue; + } + + AddInputTypeDependency(dependencies, method.ServiceMethod.Response.Type); + AddInputTypeDependency(dependencies, method.ServiceMethod.Exception?.Type); + foreach (var parameter in method.ServiceMethod.Parameters) + { + AddInputTypeDependency(dependencies, parameter.Type); + } + + foreach (var parameter in method.ServiceMethod.Operation.Parameters) + { + AddInputTypeDependency(dependencies, parameter.Type); + } + + foreach (var response in method.ServiceMethod.Operation.Responses) + { + AddInputTypeDependency(dependencies, response.BodyType); + foreach (var header in response.Headers) + { + AddInputTypeDependency(dependencies, header.Type); + } + } + } + + return dependencies; + } + + private static void AddInputTypeDependency(List dependencies, InputType? inputType) + { + var type = inputType == null ? null : ScmCodeModelGenerator.Instance.TypeFactory.CreateCSharpType(inputType); + if (type != null) + { + dependencies.Add(type); + } + } + protected override FieldProvider[] BuildFields() { List fields = [EndpointField]; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs index 35a3467ea56..396ac8cd092 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs @@ -413,7 +413,6 @@ private static bool IsGeneratedBodyReferenceCandidate(TypeProvider provider) var relativePath = provider.RelativeFilePath.Replace('\\', '/'); return IsSerializationProvider(provider) || - relativePath.EndsWith("Client.cs", StringComparison.Ordinal) || relativePath.EndsWith("/Internal/ClientUriBuilder.cs", StringComparison.Ordinal) || provider.BodyDependencyTypes.Count > 0; } From 8da04b0c36d5ceaf0a753a0062a6e381ca82769c Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Mon, 15 Jun 2026 13:20:27 +0000 Subject: [PATCH 037/111] Track rest client helper dependencies --- .../src/Providers/RestClientProvider.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs index 11213a4f00f..a6336f978e7 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs @@ -78,6 +78,33 @@ protected override FieldProvider[] BuildFields() return [.. pipelineMessage20xClassifiersFields]; } + protected override IReadOnlyList BuildHelperDependencyNames() + { + var dependencies = new HashSet(StringComparer.Ordinal); + foreach (var serviceMethod in _inputClient.Methods) + { + foreach (var parameter in serviceMethod.Operation.Parameters) + { + if (parameter is not InputHeaderParameter and not InputQueryParameter) + { + continue; + } + + var type = ScmCodeModelGenerator.Instance.TypeFactory.CreateCSharpType(parameter.Type); + if (type?.IsDictionary == true) + { + dependencies.Add("ChangeTrackingDictionary"); + } + else if (type?.IsCollection == true) + { + dependencies.Add("ChangeTrackingList"); + } + } + } + + return [.. dependencies]; + } + protected override ScmMethodProvider[] BuildMethods() { List methods = new List(); From 48885cbc88f4f271a9a04c852a30f1bf98f5aed8 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Mon, 15 Jun 2026 14:03:07 +0000 Subject: [PATCH 038/111] Skip stubbed client body dependencies --- .../src/Providers/ClientProvider.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs index bd321019175..0e695f9e7f8 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs @@ -429,6 +429,11 @@ protected override IReadOnlyList BuildBodyDependencyTypes() var dependencies = new List(); foreach (var method in Methods.OfType()) { + if (method.BodyStatements == null) + { + continue; + } + if (method.CollectionDefinition != null) { dependencies.Add(method.CollectionDefinition.Type); From aa94fa335b900d6ecae0e2031b5ad741f9708616 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Mon, 15 Jun 2026 14:03:07 +0000 Subject: [PATCH 039/111] Skip stubbed client body dependencies --- .../src/Providers/ClientProvider.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs index 69bc3741eed..bc45f8aed9a 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs @@ -429,6 +429,11 @@ protected override IReadOnlyList BuildBodyDependencyTypes() var dependencies = new List(); foreach (var method in Methods.OfType()) { + if (method.BodyStatements == null) + { + continue; + } + if (method.CollectionDefinition != null) { dependencies.Add(method.CollectionDefinition.Type); From c6539de1e5a739413a8add9d5f392efb3201ef08 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Tue, 16 Jun 2026 06:18:10 +0000 Subject: [PATCH 040/111] Internalize models from final API reachability --- ...ClientBodyDependencyPostProcessingTests.cs | 128 ++++++++++ .../test/TestHelpers/MockHelpers.cs | 11 +- .../src/TypeFactory.cs | 5 - .../test/PostProcessing/PostProcessorTests.cs | 240 +++++++++++++++++- .../ModelFactoriesCustomizationTests.cs | 26 ++ .../DerivedModel.cs | 9 + 6 files changed, 409 insertions(+), 10 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoriesCustomizationTests/OmitsModelFactoryMethodIfDerivedModelTypeInternal/DerivedModel.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs new file mode 100644 index 00000000000..a842774d913 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.TypeSpec.Generator.Input; +using Microsoft.TypeSpec.Generator.Tests.Common; +using NUnit.Framework; + +namespace Microsoft.TypeSpec.Generator.ClientModel.Tests.PostProcessing +{ + public class ClientBodyDependencyPostProcessingTests + { + [Test] + public async Task OperationBodyParameterModelDoesNotBecomePublic() + { + var requestModel = InputFactory.Model("RequestBody"); + var parameter = InputFactory.BodyParameter("body", requestModel, isRequired: true); + var operation = InputFactory.Operation("Create", parameters: [parameter], httpMethod: "POST"); + var method = InputFactory.BasicServiceMethod("Create", operation); + var client = InputFactory.Client("TestClient", methods: [method]); + + await GenerateAndAssertInternalModels([requestModel], [client], ["RequestBody"]); + } + + [Test] + public async Task OperationResponseBodyModelRemainsPublicAsRootOutputModel() + { + var responseModel = InputFactory.Model("ResponseBody"); + var operation = InputFactory.Operation("Get", responses: [InputFactory.OperationResponse(bodytype: responseModel)]); + var method = InputFactory.BasicServiceMethod( + "Get", + operation, + response: InputFactory.ServiceMethodResponse(InputPrimitiveType.String, [])); + var client = InputFactory.Client("TestClient", methods: [method]); + + await GenerateAndAssertPublicModels([responseModel], [client], ["ResponseBody"]); + } + + [Test] + public async Task NestedBodyModelGraphDoesNotBecomePublic() + { + var nestedModel = InputFactory.Model("NestedToolParameter"); + var toolModel = InputFactory.Model( + "ToolConfig", + properties: [InputFactory.Property("Parameter", nestedModel)]); + var parameter = InputFactory.BodyParameter("tool", toolModel, isRequired: true); + var operation = InputFactory.Operation("Configure", parameters: [parameter], httpMethod: "POST"); + var method = InputFactory.BasicServiceMethod("Configure", operation); + var client = InputFactory.Client("TestClient", methods: [method]); + + await GenerateAndAssertInternalModels([toolModel, nestedModel], [client], ["ToolConfig", "NestedToolParameter"]); + } + + private static async Task GenerateAndAssertInternalModels( + InputModelType[] models, + InputClient[] clients, + string[] modelNames) + => await GenerateAndAssertModels(models, clients, modelNames, shouldBePublic: false); + + private static async Task GenerateAndAssertPublicModels( + InputModelType[] models, + InputClient[] clients, + string[] modelNames) + => await GenerateAndAssertModels(models, clients, modelNames, shouldBePublic: true); + + private static async Task GenerateAndAssertModels( + InputModelType[] models, + InputClient[] clients, + string[] modelNames, + bool shouldBePublic) + { + var outputPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(outputPath); + try + { + await MockHelpers.LoadMockGeneratorAsync( + inputModels: () => models, + clients: () => clients, + configuration: "{\"package-name\": \"Sample\", \"disable-xml-docs\": true}", + outputPath: outputPath); + + await new CSharpGen().ExecuteAsync(); + + foreach (var modelName in modelNames) + { + var modelPath = Path.Combine(outputPath, "src", "Generated", "Models", $"{modelName}.cs"); + Assert.IsTrue(File.Exists(modelPath), $"Expected generated model file '{modelPath}'."); + var text = File.ReadAllText(modelPath); + if (shouldBePublic) + { + StringAssert.Contains($"public partial class {modelName}", text, $"{modelName} should be public."); + } + else + { + StringAssert.Contains($"internal partial class {modelName}", text, $"{modelName} should be internal."); + StringAssert.DoesNotContain($"public partial class {modelName}", text, $"{modelName} should not be public."); + } + } + + var modelFactoryPath = Path.Combine(outputPath, "src", "Generated", "SampleModelFactory.cs"); + if (File.Exists(modelFactoryPath)) + { + var modelFactoryText = File.ReadAllText(modelFactoryPath); + foreach (var modelName in modelNames) + { + if (shouldBePublic) + { + StringAssert.Contains($" {modelName}(", modelFactoryText, $"Model factory method for {modelName} should be generated."); + } + else + { + StringAssert.DoesNotContain($" {modelName}(", modelFactoryText, $"Model factory method for {modelName} should not be generated."); + } + } + } + } + finally + { + if (Directory.Exists(outputPath)) + { + Directory.Delete(outputPath, recursive: true); + } + } + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/TestHelpers/MockHelpers.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/TestHelpers/MockHelpers.cs index 9148f659e43..ee97df96d29 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/TestHelpers/MockHelpers.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/TestHelpers/MockHelpers.cs @@ -34,7 +34,8 @@ public static async Task> LoadMockGeneratorAsync( Func>? apiVersions = null, string? configuration = null, Func? createCSharpTypeCore = null, - Func? createCSharpTypeCoreFallback = null) + Func? createCSharpTypeCoreFallback = null, + string? outputPath = null) { var mockGenerator = LoadMockGenerator( inputLiterals: inputLiterals, @@ -44,7 +45,8 @@ public static async Task> LoadMockGeneratorAsync( apiVersions: apiVersions, configuration: configuration, createCSharpTypeCore: createCSharpTypeCore, - createCSharpTypeCoreFallback: createCSharpTypeCoreFallback); + createCSharpTypeCoreFallback: createCSharpTypeCoreFallback, + outputPath: outputPath); var compilationResult = compilation == null ? null : await compilation(); var lastContractCompilationResult = lastContractCompilation == null ? null : await lastContractCompilation(); @@ -76,7 +78,8 @@ public static Mock LoadMockGenerator( Func? createOutputLibrary = null, bool includeXmlDocs = false, Func? createCSharpTypeCoreFallback = null, - Func? createModelCore = null) + Func? createModelCore = null, + string? outputPath = null) { IReadOnlyList inputNsApiVersions = apiVersions?.Invoke() ?? []; IReadOnlyList inputNsLiterals = inputLiterals?.Invoke() ?? []; @@ -150,7 +153,7 @@ public static Mock LoadMockGenerator( { configuration = "{\"disable-xml-docs\": false, \"package-name\": \"Sample.Namespace\"}"; } - object?[] parameters = [_configFilePath, configuration]; + object?[] parameters = [outputPath ?? _configFilePath, configuration]; var config = loadMethod?.Invoke(null, parameters); var mockGeneratorContext = new Mock(config!); var mockGeneratorInstance = new Mock(mockGeneratorContext.Object) { CallBase = true }; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs index c207c3e130b..120fafb9c57 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs @@ -192,11 +192,6 @@ protected internal TypeFactory() if (modelProvider != null) { - if (model.Access == "public") - { - CodeModelGenerator.Instance.AddTypeToKeep(modelProvider); - } - CSharpTypeMap[modelProvider.Type] = modelProvider; TypeProvidersByName[modelProvider.Type.Name] = modelProvider; } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/PostProcessorTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/PostProcessorTests.cs index 28981148a4d..7dc38a7da31 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/PostProcessorTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/PostProcessorTests.cs @@ -289,11 +289,249 @@ public async Task DoesNotRemoveValidAttributes() Assert.AreEqual(Helpers.GetExpectedFromFile().TrimEnd(), output, "The output should match the expected content."); } + [Test] + public async Task ModelFactoryMethodDoesNotKeepModelPublic() + { + MockHelpers.LoadMockGenerator(); + var workspace = new AdhocWorkspace(); + var projectInfo = ProjectInfo.Create( + ProjectId.CreateNewId(), + VersionStamp.Create(), + name: "TestProj", + assemblyName: "TestProj", + language: LanguageNames.CSharp) + .WithMetadataReferences(new[] + { + MetadataReference.CreateFromFile(typeof(object).Assembly.Location) + }); + + var project = workspace.AddProject(projectInfo); + project = AddGeneratedDocument(project, "RootClient.cs", "src", "Generated", "RootClient.cs", """ +namespace Sample +{ + public partial class RootClient + { + } +} +"""); + project = AddGeneratedDocument(project, "UnusedModel.cs", "src", "Generated", "Models", "UnusedModel.cs", """ +namespace Sample.Models +{ + public partial class UnusedModel + { + } +} +"""); + project = AddGeneratedDocument(project, "SampleModelFactory.cs", "src", "Generated", "SampleModelFactory.cs", """ +namespace Sample +{ + public static partial class SampleModelFactory + { + public static Sample.Models.UnusedModel UnusedModel() => new Sample.Models.UnusedModel(); + } +} +"""); + + var postProcessor = new TestPostProcessor("RootClient.cs", modelFactoryFullName: "Sample.SampleModelFactory"); + + var resultProject = await postProcessor.InternalizeAsync(project); + var modelDoc = resultProject.Documents.Single(d => d.Name == "UnusedModel.cs"); + var modelRoot = await modelDoc.GetSyntaxRootAsync(); + var modelDeclaration = modelRoot!.DescendantNodes().OfType().Single(); + Assert.IsTrue(modelDeclaration.Modifiers.Any(m => m.Text == "internal")); + Assert.IsFalse(modelDeclaration.Modifiers.Any(m => m.Text == "public")); + + var modelFactoryDoc = resultProject.Documents.SingleOrDefault(d => d.Name == "SampleModelFactory.cs"); + if (modelFactoryDoc != null) + { + var modelFactoryRoot = await modelFactoryDoc.GetSyntaxRootAsync(); + Assert.IsFalse(modelFactoryRoot!.DescendantNodes().OfType().Any(), + "Model factory methods for internalized models should be removed."); + } + } + + [TestCase("RequestBodyModel")] + [TestCase("ResponseBodyModel")] + [TestCase("NestedToolConfig")] + [TestCase("BodyOnlyValueKind")] + public async Task BodyOnlyGeneratedTypesDoNotBecomePublic(string bodyOnlyTypeName) + { + var project = CreateProjectWithRootClient(); + var typeDeclaration = bodyOnlyTypeName == "BodyOnlyValueKind" + ? $$""" +namespace Sample.Models +{ + public enum {{bodyOnlyTypeName}} + { + Default + } +} +""" + : $$""" +namespace Sample.Models +{ + public partial class {{bodyOnlyTypeName}} + { + } +} +"""; + project = AddGeneratedDocument(project, $"{bodyOnlyTypeName}.cs", "src", "Generated", "Models", $"{bodyOnlyTypeName}.cs", typeDeclaration); + var factoryExpression = bodyOnlyTypeName == "BodyOnlyValueKind" + ? $"Sample.Models.{bodyOnlyTypeName}.Default" + : $"new Sample.Models.{bodyOnlyTypeName}()"; + project = AddGeneratedDocument(project, "SampleModelFactory.cs", "src", "Generated", "SampleModelFactory.cs", $$""" +namespace Sample +{ + public static partial class SampleModelFactory + { + public static Sample.Models.{{bodyOnlyTypeName}} {{bodyOnlyTypeName}}() => {{factoryExpression}}; + } +} +"""); + + var resultProject = await new TestPostProcessor("RootClient.cs", modelFactoryFullName: "Sample.SampleModelFactory").InternalizeAsync(project); + + await AssertGeneratedTypeInternal(resultProject, $"{bodyOnlyTypeName}.cs", bodyOnlyTypeName); + await AssertModelFactoryMethodRemoved(resultProject, bodyOnlyTypeName); + } + + [Test] + public async Task BodyOnlyNestedGraphDoesNotBecomePublic() + { + var project = CreateProjectWithRootClient(); + project = AddGeneratedDocument(project, "ToolConfig.cs", "src", "Generated", "Models", "ToolConfig.cs", """ +namespace Sample.Models +{ + public partial class ToolConfig + { + public ToolParameter Parameter { get; set; } + } +} +"""); + project = AddGeneratedDocument(project, "ToolParameter.cs", "src", "Generated", "Models", "ToolParameter.cs", """ +namespace Sample.Models +{ + public partial class ToolParameter + { + } +} +"""); + project = AddGeneratedDocument(project, "SampleModelFactory.cs", "src", "Generated", "SampleModelFactory.cs", """ +namespace Sample +{ + public static partial class SampleModelFactory + { + public static Sample.Models.ToolConfig ToolConfig() => new Sample.Models.ToolConfig(); + public static Sample.Models.ToolParameter ToolParameter() => new Sample.Models.ToolParameter(); + } +} +"""); + + var resultProject = await new TestPostProcessor("RootClient.cs", modelFactoryFullName: "Sample.SampleModelFactory").InternalizeAsync(project); + + await AssertGeneratedTypeInternal(resultProject, "ToolConfig.cs", "ToolConfig"); + await AssertGeneratedTypeInternal(resultProject, "ToolParameter.cs", "ToolParameter"); + await AssertModelFactoryMethodRemoved(resultProject, "ToolConfig"); + await AssertModelFactoryMethodRemoved(resultProject, "ToolParameter"); + } + + [Test] + public async Task ClientOptionsNestedEnumDoesNotBecomePublicWhenOptionsIsNotRooted() + { + var project = CreateProjectWithRootClient("public SampleClientOptions Options { get; }"); + project = AddGeneratedDocument(project, "SampleClientOptions.cs", "src", "Generated", "SampleClientOptions.cs", """ +namespace Sample +{ + public partial class SampleClientOptions + { + public enum ServiceVersion + { + V1 + } + } +} +"""); + + var resultProject = await new TestPostProcessor("RootClient.cs").InternalizeAsync(project); + + await AssertGeneratedTypePublic(resultProject, "SampleClientOptions.cs", "SampleClientOptions"); + var optionsDoc = resultProject.Documents.Single(d => d.Name == "SampleClientOptions.cs"); + var root = await optionsDoc.GetSyntaxRootAsync(); + var nestedEnum = root!.DescendantNodes().OfType().Single(); + Assert.IsFalse(nestedEnum.Modifiers.Any(m => m.Text == "public")); + } + + private static Project AddGeneratedDocument(Project project, string name, string folder1, string folder2, string fileName, string text) + => project.AddDocument(name, text, folders: [folder1, folder2], filePath: Path.Join(folder1, folder2, fileName)).Project; + + private static Project AddGeneratedDocument(Project project, string name, string folder1, string folder2, string folder3, string fileName, string text) + => project.AddDocument(name, text, folders: [folder1, folder2, folder3], filePath: Path.Join(folder1, folder2, folder3, fileName)).Project; + + private static Project CreateProjectWithRootClient(string members = "") + { + MockHelpers.LoadMockGenerator(); + var workspace = new AdhocWorkspace(); + var projectInfo = ProjectInfo.Create( + ProjectId.CreateNewId(), + VersionStamp.Create(), + name: "TestProj", + assemblyName: "TestProj", + language: LanguageNames.CSharp) + .WithMetadataReferences(new[] + { + MetadataReference.CreateFromFile(typeof(object).Assembly.Location) + }); + + var project = workspace.AddProject(projectInfo); + return AddGeneratedDocument(project, "RootClient.cs", "src", "Generated", "RootClient.cs", $$""" +namespace Sample +{ + public partial class RootClient + { + {{members}} + } +} +"""); + } + + private static async Task AssertGeneratedTypeInternal(Project project, string documentName, string typeName) + { + var declaration = await GetTypeDeclaration(project, documentName, typeName); + Assert.IsTrue(declaration.Modifiers.Any(m => m.Text == "internal"), $"{typeName} should be internal."); + Assert.IsFalse(declaration.Modifiers.Any(m => m.Text == "public"), $"{typeName} should not be public."); + } + + private static async Task AssertGeneratedTypePublic(Project project, string documentName, string typeName) + { + var declaration = await GetTypeDeclaration(project, documentName, typeName); + Assert.IsTrue(declaration.Modifiers.Any(m => m.Text == "public"), $"{typeName} should be public."); + } + + private static async Task GetTypeDeclaration(Project project, string documentName, string typeName) + { + var document = project.Documents.Single(d => d.Name == documentName); + var root = await document.GetSyntaxRootAsync(); + return root!.DescendantNodes().OfType().Single(t => t.Identifier.Text == typeName); + } + + private static async Task AssertModelFactoryMethodRemoved(Project project, string methodName) + { + var modelFactoryDoc = project.Documents.SingleOrDefault(d => d.Name == "SampleModelFactory.cs"); + if (modelFactoryDoc == null) + { + return; + } + + var modelFactoryRoot = await modelFactoryDoc.GetSyntaxRootAsync(); + Assert.IsFalse(modelFactoryRoot!.DescendantNodes().OfType().Any(m => m.Identifier.Text == methodName), + $"Model factory method {methodName} should be removed."); + } + private class TestPostProcessor : PostProcessor { private readonly string _rootFile; - public TestPostProcessor(string rootFile, IEnumerable? nonRootTypes = null) : base([], additionalNonRootTypeNames: nonRootTypes) + public TestPostProcessor(string rootFile, IEnumerable? additionalRootTypeNames = null, IEnumerable? nonRootTypes = null, string? modelFactoryFullName = null) : base((additionalRootTypeNames ?? []).ToHashSet(), modelFactoryFullName: modelFactoryFullName, additionalNonRootTypeNames: nonRootTypes) { _rootFile = rootFile; } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoriesCustomizationTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoriesCustomizationTests.cs index 1bdf4020167..c35f1968d76 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoriesCustomizationTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoriesCustomizationTests.cs @@ -177,6 +177,32 @@ public async Task OmitsModelFactoryMethodIfParamTypeInternal() Assert.IsNull(modelFactory); } + // This test validates that a derived model customized to be internal does not get a + // public model factory method just because its base model remains public. + [Test] + public async Task OmitsModelFactoryMethodIfDerivedModelTypeInternal() + { + var baseModel = InputFactory.Model( + "baseModel", + properties: [InputFactory.Property("BaseProp", InputPrimitiveType.String)]); + var derivedModel = InputFactory.Model( + "derivedModel", + properties: [InputFactory.Property("DerivedProp", InputPrimitiveType.String)], + baseModel: baseModel); + + var mockGenerator = await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [baseModel, derivedModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + var csharpGen = new CSharpGen(); + + await csharpGen.ExecuteAsync(); + + var modelFactory = mockGenerator.Object.OutputLibrary.TypeProviders.SingleOrDefault(t => t is ModelFactoryProvider); + Assert.IsNotNull(modelFactory); + CollectionAssert.Contains(modelFactory!.Methods.Select(m => m.Signature.Name), "BaseModel"); + CollectionAssert.DoesNotContain(modelFactory.Methods.Select(m => m.Signature.Name), "DerivedModel"); + } + [TestCase(true)] [TestCase(false)] public async Task CanCustomizeModelFullConstructor(bool extraParameters) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoriesCustomizationTests/OmitsModelFactoryMethodIfDerivedModelTypeInternal/DerivedModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoriesCustomizationTests/OmitsModelFactoryMethodIfDerivedModelTypeInternal/DerivedModel.cs new file mode 100644 index 00000000000..bdb2034f5f0 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoriesCustomizationTests/OmitsModelFactoryMethodIfDerivedModelTypeInternal/DerivedModel.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Sample.Models +{ + internal partial class DerivedModel + { + } +} From f676243819f0c222a0c57b8cc1ab3ba9198e2ce4 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Tue, 16 Jun 2026 07:04:08 +0000 Subject: [PATCH 041/111] Update model root visibility test --- .../test/Providers/ModelProviders/ModelProviderTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs index 5937838822e..4b82c28bf77 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs @@ -1449,7 +1449,7 @@ await MockHelpers.LoadMockGeneratorAsync( } [Test] - public void PublicModelsAreIncludedInAdditionalRootTypes() + public void PublicModelsAreNotIncludedInAdditionalRootTypes() { var inputModel = InputFactory.Model( "MockInputModel", @@ -1462,7 +1462,7 @@ public void PublicModelsAreIncludedInAdditionalRootTypes() Assert.IsNotNull(modelProvider); var rootTypes = CodeModelGenerator.Instance.AdditionalRootTypes; - Assert.IsTrue(rootTypes.Contains("Sample.Models.MockInputModel")); + Assert.IsFalse(rootTypes.Contains("Sample.Models.MockInputModel")); } [Test] From 0a37522f3d0916acc09bb021f89b162fad8f1c9e Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Tue, 16 Jun 2026 08:58:23 +0000 Subject: [PATCH 042/111] Detect custom model factory providers --- .../PostProcessing/ProviderReferenceMapAnalyzer.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 7c23a67a68b..b65eb4cc025 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -486,7 +486,16 @@ private static bool IsKept(CSharpType type, HashSet roots, HashSet provider.RelativeFilePath.EndsWith("Client.cs", StringComparison.Ordinal); - private static bool IsModelFactoryProvider(TypeProvider provider) => provider is ModelFactoryProvider; + private static bool IsModelFactoryProvider(TypeProvider provider) + { + if (provider is ModelFactoryProvider) + { + return true; + } + + var relativePath = provider.RelativeFilePath.Replace('\\', '/'); + return relativePath.EndsWith("ModelFactory.cs", StringComparison.Ordinal); + } private static HashSet GetHelperRootNames(IReadOnlyList providers, HashSet nodes, HashSet reachableTypes) { From 582fa84b2225b0d30a90801f28da31d5b792dd8c Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Tue, 16 Jun 2026 09:13:46 +0000 Subject: [PATCH 043/111] Preserve file header when removing usings --- .../src/PostProcessing/PostProcessor.cs | 7 +++ .../test/PostProcessing/PostProcessorTests.cs | 46 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs index 09cc693362b..32d22c34535 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs @@ -475,7 +475,14 @@ private async Task RemoveInvalidUsings(Solution solution, DocumentId d if (invalidUsings.Count > 0) { + var leadingTrivia = invalidUsings[0].GetLeadingTrivia(); cu = cu.RemoveNodes(invalidUsings, SyntaxRemoveOptions.KeepNoTrivia)!; + if (leadingTrivia.Count > 0) + { + var firstToken = cu.GetFirstToken(includeZeroWidth: true); + cu = cu.ReplaceToken(firstToken, firstToken.WithLeadingTrivia(leadingTrivia.AddRange(firstToken.LeadingTrivia))); + } + solution = solution.WithDocumentSyntaxRoot(documentId, cu); } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/PostProcessorTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/PostProcessorTests.cs index 7dc38a7da31..4100756ff15 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/PostProcessorTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/PostProcessorTests.cs @@ -60,6 +60,52 @@ public async Task RemovesInvalidUsings() CollectionAssert.Contains(usings, "System"); } + [Test] + public async Task RemovesInvalidUsingsKeepsFileHeader() + { + MockHelpers.LoadMockGenerator(); + var workspace = new AdhocWorkspace(); + var projectInfo = ProjectInfo.Create( + ProjectId.CreateNewId(), + VersionStamp.Create(), + name: "TestProj", + assemblyName: "TestProj", + language: LanguageNames.CSharp) + .WithMetadataReferences(new[] + { + MetadataReference.CreateFromFile(typeof(object).Assembly.Location) + }); + + var project = workspace.AddProject(projectInfo); + project = AddGeneratedDocument(project, "RootClient.cs", "src", "Generated", "RootClient.cs", """ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// + +#nullable disable + +using Missing.Namespace; + +namespace Sample +{ + public partial class RootClient + { + } +} +"""); + var postProcessor = new TestPostProcessor("RootClient.cs"); + + var resultProject = await postProcessor.RemoveAsync(project); + var doc = resultProject.Documents.Single(d => d.Name == "RootClient.cs"); + var text = (await doc.GetTextAsync()).ToString(); + + StringAssert.StartsWith("// Copyright (c) Microsoft Corporation. All rights reserved.", text); + StringAssert.Contains("// ", text); + StringAssert.Contains("#nullable disable", text); + StringAssert.DoesNotContain("using Missing.Namespace;", text); + } + [Test] public async Task DoesNotRemoveValidUsings() From 74be26db42351daf3f3bb40e55173ff4979a996f Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Tue, 16 Jun 2026 11:51:21 +0000 Subject: [PATCH 044/111] Limit derived public reachability to discriminators --- ...ClientBodyDependencyPostProcessingTests.cs | 74 ++++++++++++++----- .../ProviderReferenceMapAnalyzer.cs | 5 ++ 2 files changed, 60 insertions(+), 19 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs index a842774d913..46b81a23290 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs @@ -53,6 +53,25 @@ public async Task NestedBodyModelGraphDoesNotBecomePublic() await GenerateAndAssertInternalModels([toolModel, nestedModel], [client], ["ToolConfig", "NestedToolParameter"]); } + [Test] + public async Task NonDiscriminatorDerivedBodyModelDoesNotBecomePublicFromPublicBase() + { + var baseTool = InputFactory.Model("BaseTool"); + var concreteTool = InputFactory.Model( + "ConcreteTool", + properties: [InputFactory.Property("Name", InputPrimitiveType.String)], + baseModel: baseTool); + var operation = InputFactory.Operation("Get", responses: [InputFactory.OperationResponse(bodytype: baseTool)]); + var method = InputFactory.BasicServiceMethod("Get", operation, response: InputFactory.ServiceMethodResponse(baseTool, [])); + var client = InputFactory.Client("TestClient", methods: [method]); + + await GenerateAndAssertMixedModels( + [baseTool, concreteTool], + [client], + publicModelNames: ["BaseTool"], + internalModelNames: ["ConcreteTool"]); + } + private static async Task GenerateAndAssertInternalModels( InputModelType[] models, InputClient[] clients, @@ -65,11 +84,29 @@ private static async Task GenerateAndAssertPublicModels( string[] modelNames) => await GenerateAndAssertModels(models, clients, modelNames, shouldBePublic: true); + private static async Task GenerateAndAssertMixedModels( + InputModelType[] models, + InputClient[] clients, + string[] publicModelNames, + string[] internalModelNames) + => await GenerateAndAssertModels(models, clients, publicModelNames, internalModelNames); + private static async Task GenerateAndAssertModels( InputModelType[] models, InputClient[] clients, string[] modelNames, bool shouldBePublic) + => await GenerateAndAssertModels( + models, + clients, + shouldBePublic ? modelNames : [], + shouldBePublic ? [] : modelNames); + + private static async Task GenerateAndAssertModels( + InputModelType[] models, + InputClient[] clients, + string[] publicModelNames, + string[] internalModelNames) { var outputPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); Directory.CreateDirectory(outputPath); @@ -83,36 +120,35 @@ await MockHelpers.LoadMockGeneratorAsync( await new CSharpGen().ExecuteAsync(); - foreach (var modelName in modelNames) + foreach (var modelName in publicModelNames) { var modelPath = Path.Combine(outputPath, "src", "Generated", "Models", $"{modelName}.cs"); Assert.IsTrue(File.Exists(modelPath), $"Expected generated model file '{modelPath}'."); var text = File.ReadAllText(modelPath); - if (shouldBePublic) - { - StringAssert.Contains($"public partial class {modelName}", text, $"{modelName} should be public."); - } - else - { - StringAssert.Contains($"internal partial class {modelName}", text, $"{modelName} should be internal."); - StringAssert.DoesNotContain($"public partial class {modelName}", text, $"{modelName} should not be public."); - } + StringAssert.Contains($"public partial class {modelName}", text, $"{modelName} should be public."); + } + + foreach (var modelName in internalModelNames) + { + var modelPath = Path.Combine(outputPath, "src", "Generated", "Models", $"{modelName}.cs"); + Assert.IsTrue(File.Exists(modelPath), $"Expected generated model file '{modelPath}'."); + var text = File.ReadAllText(modelPath); + StringAssert.Contains($"internal partial class {modelName}", text, $"{modelName} should be internal."); + StringAssert.DoesNotContain($"public partial class {modelName}", text, $"{modelName} should not be public."); } var modelFactoryPath = Path.Combine(outputPath, "src", "Generated", "SampleModelFactory.cs"); if (File.Exists(modelFactoryPath)) { var modelFactoryText = File.ReadAllText(modelFactoryPath); - foreach (var modelName in modelNames) + foreach (var modelName in publicModelNames) + { + StringAssert.Contains($" {modelName}(", modelFactoryText, $"Model factory method for {modelName} should be generated."); + } + + foreach (var modelName in internalModelNames) { - if (shouldBePublic) - { - StringAssert.Contains($" {modelName}(", modelFactoryText, $"Model factory method for {modelName} should be generated."); - } - else - { - StringAssert.DoesNotContain($" {modelName}(", modelFactoryText, $"Model factory method for {modelName} should not be generated."); - } + StringAssert.DoesNotContain($" {modelName}(", modelFactoryText, $"Model factory method for {modelName} should not be generated."); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index b65eb4cc025..d445a446904 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -240,6 +240,11 @@ private static void AddDerivedModelReferences( { foreach (var provider in providers.OfType()) { + if (provider.DiscriminatorProperty == null) + { + continue; + } + var providerName = GetProviderTypeName(provider.Type); if (!nodes.Contains(providerName)) { From dc6f0f25bf3b66f20baf44fd91faaebbc0774e6b Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Tue, 16 Jun 2026 15:40:36 +0000 Subject: [PATCH 045/111] Ignore custom roots for derived public reachability --- .../ProviderReferenceMapAnalyzer.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index d445a446904..08efec8a3b6 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -32,8 +32,9 @@ public static void Analyze(IReadOnlyList providers, Project projec // Helper types are rooted after an initial reachability pass so unused infrastructure // such as change-tracking dictionaries can still be removed when no reachable type needs them. var internalizeReferences = CloneReferences(publicGraph.References); - AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences); var internalizeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: false); + var generatedPublicReachable = GetReachableTypes(internalizeRoots, internalizeReferences); + AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences, generatedPublicReachable); internalizeRoots.UnionWith(customRoots); var internalizeReachableWithoutHelpers = GetReachableTypes(internalizeRoots, internalizeReferences); var internalizeHelperRoots = GetHelperRootNames(providers, graph.Nodes, internalizeReachableWithoutHelpers); @@ -236,7 +237,8 @@ private static Dictionary> CloneReferences(IReadOnlyDict private static void AddDerivedModelReferences( IReadOnlyList providers, HashSet nodes, - Dictionary> references) + Dictionary> references, + HashSet publicBaseModels) { foreach (var provider in providers.OfType()) { @@ -245,12 +247,22 @@ private static void AddDerivedModelReferences( continue; } + if (!provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) + { + continue; + } + var providerName = GetProviderTypeName(provider.Type); if (!nodes.Contains(providerName)) { continue; } + if (!publicBaseModels.Contains(providerName)) + { + continue; + } + foreach (var derivedModel in provider.DerivedModels) { AddTypeReference(references[providerName], derivedModel.Type, nodes); From 7c20ef2ec909ab0187e7433067aa13257c9a8c51 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 17 Jun 2026 02:12:33 +0000 Subject: [PATCH 046/111] Propagate public discriminator reachability transitively --- .../ProviderReferenceMapAnalyzer.cs | 51 +++++++++++-------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 08efec8a3b6..a8b7a28b230 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -240,32 +240,43 @@ private static void AddDerivedModelReferences( Dictionary> references, HashSet publicBaseModels) { - foreach (var provider in providers.OfType()) + var addedReference = true; + while (addedReference) { - if (provider.DiscriminatorProperty == null) + addedReference = false; + foreach (var provider in providers.OfType()) { - continue; - } + if (provider.DiscriminatorProperty == null) + { + continue; + } - if (!provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) - { - continue; - } + if (!provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) + { + continue; + } - var providerName = GetProviderTypeName(provider.Type); - if (!nodes.Contains(providerName)) - { - continue; - } + var providerName = GetProviderTypeName(provider.Type); + if (!nodes.Contains(providerName)) + { + continue; + } - if (!publicBaseModels.Contains(providerName)) - { - continue; - } + if (!publicBaseModels.Contains(providerName)) + { + continue; + } - foreach (var derivedModel in provider.DerivedModels) - { - AddTypeReference(references[providerName], derivedModel.Type, nodes); + foreach (var derivedModel in provider.DerivedModels) + { + var before = references[providerName].Count; + AddTypeReference(references[providerName], derivedModel.Type, nodes); + var derivedName = GetProviderTypeName(derivedModel.Type); + if (nodes.Contains(derivedName) && publicBaseModels.Add(derivedName) || references[providerName].Count != before) + { + addedReference = true; + } + } } } } From 2e4af0838dc507182f25a7e2709c05e2ebbff76d Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 17 Jun 2026 05:46:37 +0000 Subject: [PATCH 047/111] Keep enum serialization helpers reachable --- ...ClientBodyDependencyPostProcessingTests.cs | 70 +++++++++++++++ .../src/PostProcessing/PostProcessor.cs | 38 ++++++++ .../ProviderReferenceMapAnalyzer.cs | 90 +++++++++++++------ 3 files changed, 169 insertions(+), 29 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs index 46b81a23290..9647a678c28 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs @@ -72,6 +72,40 @@ await GenerateAndAssertMixedModels( internalModelNames: ["ConcreteTool"]); } + [Test] + public async Task CustomizedEnumSerializationProviderIsKeptWhenModelSerializationUsesEnum() + { + var statusEnum = InputFactory.StringEnum( + "Status", + [("Succeeded", "succeeded"), ("Failed", "failed")], + clientNamespace: "Sample"); + var resultModel = InputFactory.Model( + "OperationResult", + properties: [InputFactory.Property("Status", statusEnum, isRequired: true)], + @namespace: "Sample"); + var operation = InputFactory.Operation("Get", responses: [InputFactory.OperationResponse(bodytype: resultModel)]); + var method = InputFactory.BasicServiceMethod("Get", operation, response: InputFactory.ServiceMethodResponse(resultModel, [])); + var client = InputFactory.Client("TestClient", methods: [method], clientNamespace: "Sample"); + + await GenerateAndAssertFiles( + enums: [statusEnum], + models: [resultModel], + clients: [client], + customFiles: [ + (Path.Combine("src", "Custom", "Status.cs"), """ + namespace Sample; + + [CodeGenType("Status")] + public enum Status + { + Succeeded, + Failed + } + """) + ], + expectedFiles: [Path.Combine("src", "Generated", "Models", "Status.Serialization.cs")]); + } + private static async Task GenerateAndAssertInternalModels( InputModelType[] models, InputClient[] clients, @@ -108,11 +142,41 @@ private static async Task GenerateAndAssertModels( string[] publicModelNames, string[] internalModelNames) { + await GenerateAndAssertFiles( + enums: [], + models: models, + clients: clients, + customFiles: [], + publicModelNames: publicModelNames, + internalModelNames: internalModelNames, + expectedFiles: []); + } + + private static async Task GenerateAndAssertFiles( + InputEnumType[] enums, + InputModelType[] models, + InputClient[] clients, + (string Path, string Content)[] customFiles, + string[] expectedFiles, + string[] publicModelNames = null!, + string[] internalModelNames = null!) + { + publicModelNames ??= []; + internalModelNames ??= []; + var outputPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); Directory.CreateDirectory(outputPath); try { + foreach (var customFile in customFiles) + { + var customPath = Path.Combine(outputPath, customFile.Path); + Directory.CreateDirectory(Path.GetDirectoryName(customPath)!); + File.WriteAllText(customPath, customFile.Content); + } + await MockHelpers.LoadMockGeneratorAsync( + inputEnums: () => enums, inputModels: () => models, clients: () => clients, configuration: "{\"package-name\": \"Sample\", \"disable-xml-docs\": true}", @@ -151,6 +215,12 @@ await MockHelpers.LoadMockGeneratorAsync( StringAssert.DoesNotContain($" {modelName}(", modelFactoryText, $"Model factory method for {modelName} should not be generated."); } } + + foreach (var expectedFile in expectedFiles) + { + var filePath = Path.Combine(outputPath, expectedFile); + Assert.IsTrue(File.Exists(filePath), $"Expected generated file '{filePath}'."); + } } finally { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs index 32d22c34535..e23db1086fb 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs @@ -10,6 +10,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Simplification; +using Microsoft.CodeAnalysis.Text; namespace Microsoft.TypeSpec.Generator { @@ -452,6 +453,12 @@ private async Task RemoveInvalidRefs(Project project) solution = await RemoveInvalidAttributes(solution, documentId); } + // Process each document for invalid XML doc cref attributes (with fresh semantic models) + foreach (var documentId in project.DocumentIds) + { + solution = await RemoveInvalidDocCrefs(solution, documentId); + } + return solution.GetProject(project.Id)!; } @@ -563,6 +570,37 @@ arg.Expression is TypeOfExpressionSyntax typeOfExpr && return solution; } + private async Task RemoveInvalidDocCrefs(Solution solution, DocumentId documentId) + { + var document = solution.GetDocument(documentId)!; + var root = await document.GetSyntaxRootAsync(); + var model = await document.GetSemanticModelAsync(); + + if (root == null || model == null) + return solution; + + var invalidSeeElements = root.DescendantTrivia(descendIntoTrivia: true) + .SelectMany(static trivia => trivia.GetStructure()?.DescendantNodes().OfType() ?? []) + .Where(element => string.Equals(element.Name.LocalName.ValueText, "see", StringComparison.Ordinal)) + .Where(element => element.Attributes.OfType().Any(attribute => model.GetSymbolInfo(attribute.Cref).Symbol == null)) + .ToArray(); + + if (invalidSeeElements.Length == 0) + return solution; + + var text = await document.GetTextAsync(); + var source = text.ToString(); + foreach (var element in invalidSeeElements) + { + var cref = element.Attributes.OfType().First().Cref.ToString(); + var colonIndex = cref.IndexOf(':'); + var replacement = colonIndex >= 0 ? cref.Substring(colonIndex + 1) : cref; + source = source.Replace(element.ToFullString(), replacement, StringComparison.Ordinal); + } + + return solution.WithDocumentText(documentId, SourceText.From(source, text.Encoding)); + } + private async Task> GetRootSymbolsAsync(Project project, TypeSymbols modelSymbols) { var result = new HashSet(SymbolEqualityComparer.Default); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index a8b7a28b230..b8dc6c56f9e 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -128,6 +128,14 @@ private static void AddSymbolRoot(HashSet roots, ITypeSymbol? symbol, Ha private static ProviderReferenceGraph BuildGraph(IReadOnlyList providers, bool publicOnly = false) { var generatedProviders = GetGeneratedProviders(providers); + var serializationProviderNamesByType = providers + .Where(static provider => provider.SerializationProviders.Count > 0) + .ToDictionary( + static provider => GetProviderTypeName(provider.Type), + static provider => provider.SerializationProviders + .Select(static serializationProvider => GetProviderTypeName(serializationProvider.Type)) + .ToArray(), + StringComparer.Ordinal); var nodes = generatedProviders .Select(static provider => GetProviderTypeName(provider.Type)) .ToHashSet(StringComparer.Ordinal); @@ -136,9 +144,9 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro foreach (var provider in generatedProviders) { var current = GetProviderTypeName(provider.Type); - AddTypeReference(references[current], provider.Type, nodes); - AddTypeReference(references[current], provider.BaseType, nodes); - AddTypeReference(references[current], provider.DeclaringTypeProvider?.Type, nodes); + AddTypeReference(references[current], provider.Type, nodes, serializationProviderNamesByType); + AddTypeReference(references[current], provider.BaseType, nodes, serializationProviderNamesByType); + AddTypeReference(references[current], provider.DeclaringTypeProvider?.Type, nodes, serializationProviderNamesByType); if (IsKept(provider.Type, CodeModelGenerator.Instance.NonRootTypes, nodes)) { @@ -155,17 +163,17 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro foreach (var implementedType in provider.Implements) { - AddTypeReference(references[current], implementedType, nodes); + AddTypeReference(references[current], implementedType, nodes, serializationProviderNamesByType); } foreach (var nestedType in provider.NestedTypes) { - AddTypeReference(references[current], nestedType.Type, nodes); + AddTypeReference(references[current], nestedType.Type, nodes, serializationProviderNamesByType); } foreach (var serializationProvider in provider.SerializationProviders) { - AddTypeReference(references[current], serializationProvider.Type, nodes); + AddTypeReference(references[current], serializationProvider.Type, nodes, serializationProviderNamesByType); } foreach (var property in provider.Properties) @@ -175,9 +183,9 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro continue; } - AddTypeReference(references[current], property.Type, nodes); - AddTypeReference(references[current], property.ExplicitInterface, nodes); - AddAttributes(references[current], property.Attributes, nodes); + AddTypeReference(references[current], property.Type, nodes, serializationProviderNamesByType); + AddTypeReference(references[current], property.ExplicitInterface, nodes, serializationProviderNamesByType); + AddAttributes(references[current], property.Attributes, nodes, serializationProviderNamesByType); } foreach (var field in provider.Fields) @@ -187,8 +195,8 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro continue; } - AddTypeReference(references[current], field.Type, nodes); - AddAttributes(references[current], field.Attributes, nodes); + AddTypeReference(references[current], field.Type, nodes, serializationProviderNamesByType); + AddAttributes(references[current], field.Attributes, nodes, serializationProviderNamesByType); } foreach (var constructor in provider.Constructors) @@ -198,7 +206,7 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro continue; } - AddSignatureReferences(references[current], constructor.Signature, nodes); + AddSignatureReferences(references[current], constructor.Signature, nodes, serializationProviderNamesByType); } foreach (var method in provider.Methods) @@ -208,8 +216,8 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro continue; } - AddSignatureReferences(references[current], method.Signature, nodes); - AddTypeReference(references[current], GetCollectionDefinitionType(method), nodes); + AddSignatureReferences(references[current], method.Signature, nodes, serializationProviderNamesByType); + AddTypeReference(references[current], GetCollectionDefinitionType(method), nodes, serializationProviderNamesByType); } } @@ -418,6 +426,11 @@ private static void AddBodyTypeReference(HashSet references, ITypeSymbol } AddMatchingName(references, namedType.GetFullyQualifiedName(), nodes); + if (namedType.TypeKind == TypeKind.Enum) + { + AddMatchingName(references, $"{namedType.Name}Extensions", nodes); + } + foreach (var typeArgument in namedType.TypeArguments) { AddBodyTypeReference(references, typeArgument, nodes); @@ -694,25 +707,29 @@ private static HashSet GetReachableTypes(HashSet roots, IReadOnl return reachable; } - private static void AddSignatureReferences(HashSet references, MethodSignatureBase signature, HashSet nodes) + private static void AddSignatureReferences( + HashSet references, + MethodSignatureBase signature, + HashSet nodes, + IReadOnlyDictionary serializationProviderNamesByType) { - AddTypeReference(references, signature.ReturnType, nodes); - AddAttributes(references, signature.Attributes, nodes); + AddTypeReference(references, signature.ReturnType, nodes, serializationProviderNamesByType); + AddAttributes(references, signature.Attributes, nodes, serializationProviderNamesByType); foreach (var parameter in signature.Parameters) { - AddTypeReference(references, parameter.Type, nodes); - AddAttributes(references, parameter.Attributes, nodes); + AddTypeReference(references, parameter.Type, nodes, serializationProviderNamesByType); + AddAttributes(references, parameter.Attributes, nodes, serializationProviderNamesByType); } if (signature is MethodSignature methodSignature) { - AddTypeReference(references, methodSignature.ExplicitInterface, nodes); + AddTypeReference(references, methodSignature.ExplicitInterface, nodes, serializationProviderNamesByType); if (methodSignature.GenericArguments != null) { foreach (var genericArgument in methodSignature.GenericArguments) { - AddTypeReference(references, genericArgument, nodes); + AddTypeReference(references, genericArgument, nodes, serializationProviderNamesByType); } } @@ -720,26 +737,34 @@ private static void AddSignatureReferences(HashSet references, MethodSig { foreach (var constraint in methodSignature.GenericParameterConstraints) { - AddTypeReference(references, constraint.Type, nodes); + AddTypeReference(references, constraint.Type, nodes, serializationProviderNamesByType); } } } if (signature is ConstructorSignature constructorSignature) { - AddTypeReference(references, constructorSignature.Type, nodes); + AddTypeReference(references, constructorSignature.Type, nodes, serializationProviderNamesByType); } } - private static void AddAttributes(HashSet references, IReadOnlyList attributes, HashSet nodes) + private static void AddAttributes( + HashSet references, + IReadOnlyList attributes, + HashSet nodes, + IReadOnlyDictionary serializationProviderNamesByType) { foreach (var attribute in attributes) { - AddTypeReference(references, attribute.Type, nodes); + AddTypeReference(references, attribute.Type, nodes, serializationProviderNamesByType); } } - private static void AddTypeReference(HashSet references, CSharpType? type, HashSet nodes) + private static void AddTypeReference( + HashSet references, + CSharpType? type, + HashSet nodes, + IReadOnlyDictionary? serializationProviderNamesByType = null) { if (type == null) { @@ -750,13 +775,20 @@ private static void AddTypeReference(HashSet references, CSharpType? typ if (nodes.Contains(providerTypeName)) { references.Add(providerTypeName); + if (serializationProviderNamesByType != null && serializationProviderNamesByType.TryGetValue(providerTypeName, out var serializationProviderNames)) + { + foreach (var serializationProviderName in serializationProviderNames) + { + references.Add(serializationProviderName); + } + } } - AddTypeReference(references, type.BaseType, nodes); - AddTypeReference(references, type.DeclaringType, nodes); + AddTypeReference(references, type.BaseType, nodes, serializationProviderNamesByType); + AddTypeReference(references, type.DeclaringType, nodes, serializationProviderNamesByType); foreach (var argument in type.Arguments) { - AddTypeReference(references, argument, nodes); + AddTypeReference(references, argument, nodes, serializationProviderNamesByType); } } From 8919ffcc878b55f8b7d32fb60f27a6e39cba3bcc Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 17 Jun 2026 11:04:44 +0000 Subject: [PATCH 048/111] Fix XML doc reference spelling --- .../src/PostProcessing/PostProcessor.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs index e23db1086fb..f2441c6b2ea 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs @@ -453,10 +453,10 @@ private async Task RemoveInvalidRefs(Project project) solution = await RemoveInvalidAttributes(solution, documentId); } - // Process each document for invalid XML doc cref attributes (with fresh semantic models) + // Process each document for invalid XML documentation references (with fresh semantic models) foreach (var documentId in project.DocumentIds) { - solution = await RemoveInvalidDocCrefs(solution, documentId); + solution = await RemoveInvalidXmlDocReferences(solution, documentId); } return solution.GetProject(project.Id)!; @@ -570,7 +570,7 @@ arg.Expression is TypeOfExpressionSyntax typeOfExpr && return solution; } - private async Task RemoveInvalidDocCrefs(Solution solution, DocumentId documentId) + private async Task RemoveInvalidXmlDocReferences(Solution solution, DocumentId documentId) { var document = solution.GetDocument(documentId)!; var root = await document.GetSyntaxRootAsync(); From 4d60b88d59b80d2f4342344ad8200861cf833fe6 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Fri, 19 Jun 2026 08:41:37 +0000 Subject: [PATCH 049/111] perf(http-client-csharp): optimize reference map post-processing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ybrid-reference-map-2026-06-19-08-41-23.md | 7 + ...ClientBodyDependencyPostProcessingTests.cs | 41 ++ .../src/CSharpGen.cs | 14 +- .../src/PostProcessing/PostProcessor.cs | 244 ++++++- .../ProviderReferenceMapAnalyzer.cs | 594 ++++++++++++++++-- .../ProviderReferenceMapResult.cs | 1 + .../src/Providers/TypeProvider.cs | 6 +- 7 files changed, 833 insertions(+), 74 deletions(-) create mode 100644 .chronus/changes/mtg-hybrid-reference-map-2026-06-19-08-41-23.md diff --git a/.chronus/changes/mtg-hybrid-reference-map-2026-06-19-08-41-23.md b/.chronus/changes/mtg-hybrid-reference-map-2026-06-19-08-41-23.md new file mode 100644 index 00000000000..e45ad2b2de0 --- /dev/null +++ b/.chronus/changes/mtg-hybrid-reference-map-2026-06-19-08-41-23.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@typespec/http-client-csharp" +--- + +Improve C# generator post-processing reference-map parity and performance. diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs index 9647a678c28..0d7f1063e9b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs @@ -72,6 +72,20 @@ await GenerateAndAssertMixedModels( internalModelNames: ["ConcreteTool"]); } + [Test] + public async Task PublicModelSignatureDependencyIsPromotedToPublic() + { + var internalDependency = InputFactory.Model("InternalDependency", access: "internal"); + var responseModel = InputFactory.Model( + "ResponseBody", + properties: [InputFactory.Property("Dependency", internalDependency)]); + var operation = InputFactory.Operation("Get", responses: [InputFactory.OperationResponse(bodytype: responseModel)]); + var method = InputFactory.BasicServiceMethod("Get", operation, response: InputFactory.ServiceMethodResponse(responseModel, [])); + var client = InputFactory.Client("TestClient", methods: [method]); + + await GenerateAndAssertPublicModels([responseModel, internalDependency], [client], ["ResponseBody", "InternalDependency"]); + } + [Test] public async Task CustomizedEnumSerializationProviderIsKeptWhenModelSerializationUsesEnum() { @@ -106,6 +120,33 @@ public enum Status expectedFiles: [Path.Combine("src", "Generated", "Models", "Status.Serialization.cs")]); } + [Test] + public async Task CustomModelFactoryPartialDoesNotKeepBodyOnlyModelPublic() + { + var requestModel = InputFactory.Model("RequestBody"); + var parameter = InputFactory.BodyParameter("body", requestModel, isRequired: true); + var operation = InputFactory.Operation("Create", parameters: [parameter], httpMethod: "POST"); + var method = InputFactory.BasicServiceMethod("Create", operation); + var client = InputFactory.Client("TestClient", methods: [method]); + + await GenerateAndAssertFiles( + enums: [], + models: [requestModel], + clients: [client], + customFiles: [ + (Path.Combine("src", "SampleModelFactory.cs"), """ + namespace Sample; + + [Microsoft.TypeSpec.Generator.Customizations.CodeGenType("SampleModelFactory")] + public static partial class SampleModelFactory + { + } + """) + ], + expectedFiles: [], + internalModelNames: ["RequestBody"]); + } + private static async Task GenerateAndAssertInternalModels( InputModelType[] models, InputClient[] clients, diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index 8ae87be33d1..39a4e5de227 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs @@ -17,8 +17,18 @@ internal sealed class CSharpGen { private const string ConfigurationFileName = "Configuration.json"; private const string CodeModelFileName = "tspCodeModel.json"; - - private static readonly string[] _filesToKeep = [ConfigurationFileName, CodeModelFileName]; + private const string RawRequestUriBuilderExtensionsFileName = "RawRequestUriBuilderExtensions.cs"; + private const string SerializationFormatFileName = "SerializationFormat.cs"; + private const string TypeFormattersFileName = "TypeFormatters.cs"; + + private static readonly string[] _filesToKeep = + [ + ConfigurationFileName, + CodeModelFileName, + RawRequestUriBuilderExtensionsFileName, + SerializationFormatFileName, + TypeFormattersFileName + ]; /// /// Executes the generator task with the instance. diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs index f2441c6b2ea..ec0f974ebfd 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs @@ -9,6 +9,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.FindSymbols; using Microsoft.CodeAnalysis.Simplification; using Microsoft.CodeAnalysis.Text; @@ -130,14 +131,20 @@ public async Task InternalizeAsync(Project project) if (compilation == null) return project; + var referenceMapResult = ProviderReferenceMapAnalyzer.LatestResult is { } latestResult && latestResult.ProjectId == project.Id + ? latestResult + : null; + // first get all the declared symbols - var definitions = await GetTypeSymbolsAsync(compilation, project, true); + var definitions = await GetTypeSymbolsAsync(compilation, project, publicOnly: referenceMapResult == null); IEnumerable symbolsToInternalize; - if (ProviderReferenceMapAnalyzer.LatestResult is { } referenceMapResult && referenceMapResult.ProjectId == project.Id) + IEnumerable symbolsToPublicize = []; + if (referenceMapResult != null) { // ProviderReferenceMapAnalyzer replaces Roslyn reference-map construction for generated code. // It still uses Roslyn-discovered roots for custom/shared code before this point. symbolsToInternalize = GetSymbolsByName(definitions.DeclaredSymbols, referenceMapResult.InternalizeCandidates).ToArray(); + symbolsToPublicize = GetSymbolsByName(definitions.DeclaredSymbols, referenceMapResult.PublicizeCandidates).ToArray(); } else { @@ -157,18 +164,67 @@ public async Task InternalizeAsync(Project project) } } - foreach (var (model, documentId) in nodesToInternalize) + var nodesToPublicize = new Dictionary(); + foreach (var symbol in symbolsToPublicize) { - project = MarkInternal(project, model, documentId); + foreach (var node in definitions.DeclaredNodesCache[symbol]) + { + nodesToPublicize[node] = project.GetDocumentId(node.SyntaxTree)!; + } } - var modelNamesToRemove = - nodesToInternalize.Keys.Select(item => item.Identifier.Text); + project = ApplyAccessibilityChanges(project, nodesToInternalize, nodesToPublicize); + project = await InternalizePublicNestedTypesInInternalTypesAsync(project); + + var modelNamesToRemove = nodesToInternalize.Keys.Select(item => item.Identifier.Text); + if (referenceMapResult != null) + { + modelNamesToRemove = modelNamesToRemove.Concat(referenceMapResult.RemoveCandidates.Select(GetSimpleName)); + } project = await RemoveMethodsFromModelFactoryAsync(project, definitions, modelNamesToRemove.ToHashSet()); return project; } + private static async Task InternalizePublicNestedTypesInInternalTypesAsync(Project project) + { + foreach (var document in project.Documents.ToArray()) + { + if (!GeneratedCodeWorkspace.IsGeneratedDocument(document)) + { + continue; + } + + var root = await document.GetSyntaxRootAsync(); + if (root == null) + { + continue; + } + + var nestedPublicTypes = root.DescendantNodes() + .OfType() + .Where(static declaration => declaration.Modifiers.Any(SyntaxKind.PublicKeyword) && + declaration.Ancestors().OfType().Any(static parent => parent.Modifiers.Any(SyntaxKind.InternalKeyword))) + .ToArray(); + if (nestedPublicTypes.Length == 0) + { + continue; + } + + var newRoot = root.ReplaceNodes(nestedPublicTypes, static (originalNode, _) => + ChangeAccessibility(originalNode, SyntaxKind.InternalKeyword)).WithAdditionalAnnotations(Simplifier.Annotation); + project = document.WithSyntaxRoot(newRoot).Project; + } + + return project; + } + + private static string GetSimpleName(string fullyQualifiedName) + { + var lastDot = fullyQualifiedName.LastIndexOf('.'); + return lastDot < 0 ? fullyQualifiedName : fullyQualifiedName.Substring(lastDot + 1); + } + private async Task RemoveMethodsFromModelFactoryAsync(Project project, TypeSymbols definitions, HashSet namesToRemove) @@ -358,15 +414,54 @@ private static IEnumerable GetSymbolsByName(IEnumerable nodesToInternalize, + IReadOnlyDictionary nodesToPublicize) { - var newNode = ChangeModifier(declarationNode, SyntaxKind.PublicKeyword, SyntaxKind.InternalKeyword); - var tree = declarationNode.SyntaxTree; - var document = project.GetDocument(documentId)!; - var newRoot = tree.GetRoot().ReplaceNode(declarationNode, newNode) - .WithAdditionalAnnotations(Simplifier.Annotation); - document = document.WithSyntaxRoot(newRoot); - return document.Project; + var changesByDocument = new Dictionary>(); + AddAccessibilityChanges(changesByDocument, nodesToInternalize, SyntaxKind.InternalKeyword); + AddAccessibilityChanges(changesByDocument, nodesToPublicize, SyntaxKind.PublicKeyword); + + foreach (var (documentId, changes) in changesByDocument) + { + var document = project.GetDocument(documentId)!; + var root = changes.Keys.First().SyntaxTree.GetRoot(); + var newRoot = root.ReplaceNodes( + changes.Keys, + (originalNode, _) => changes.TryGetValue(originalNode, out var targetAccessibility) + ? ChangeAccessibility(originalNode, targetAccessibility) + : originalNode) + .WithAdditionalAnnotations(Simplifier.Annotation); + document = document.WithSyntaxRoot(newRoot); + project = document.Project; + } + + return project; + } + + private static void AddAccessibilityChanges( + Dictionary> changesByDocument, + IReadOnlyDictionary nodes, + SyntaxKind targetAccessibility) + { + foreach (var (node, documentId) in nodes) + { + if (!changesByDocument.TryGetValue(documentId, out var changes)) + { + changes = new Dictionary(); + changesByDocument[documentId] = changes; + } + + changes[node] = targetAccessibility; + } + } + + private static BaseTypeDeclarationSyntax ChangeAccessibility(BaseTypeDeclarationSyntax declarationNode, SyntaxKind targetAccessibility) + { + return targetAccessibility == SyntaxKind.PublicKeyword + ? ChangeModifier(declarationNode, SyntaxKind.InternalKeyword, SyntaxKind.PublicKeyword) + : ChangeModifier(declarationNode, SyntaxKind.PublicKeyword, SyntaxKind.InternalKeyword); } private async Task RemoveModelsAsync(Project project, @@ -515,6 +610,15 @@ arg.Expression is TypeOfExpressionSyntax typeOfExpr && model.GetTypeInfo(typeOfExpr.Type).Type?.TypeKind == TypeKind.Error) == true)) .ToHashSet(); + foreach (var attr in attributes) + { + if (IsInternalRecordBuildableAttribute(attr) || + await ShouldRemoveUnreferencedInternalBuildableAttribute(solution, model, attr)) + { + invalidAttributes.Add(attr); + } + } + if (invalidAttributes.Count > 0) { cu = cu.RemoveNodes(invalidAttributes, SyntaxRemoveOptions.KeepNoTrivia)!; @@ -570,6 +674,118 @@ arg.Expression is TypeOfExpressionSyntax typeOfExpr && return solution; } + private static bool IsInternalRecordBuildableAttribute(AttributeListSyntax attributeList) + { + if (attributeList.Attributes.Count != 1 || + !IsModelReaderWriterBuildableAttribute(attributeList.Attributes[0])) + { + return false; + } + + var typeName = attributeList.Attributes[0].ArgumentList?.Arguments + .Select(static argument => argument.Expression) + .OfType() + .Select(static typeOfExpression => typeOfExpression.Type.ToString().Split('.').Last()) + .FirstOrDefault(); + + return typeName?.StartsWith("Update", StringComparison.Ordinal) == true && typeName.EndsWith("Record", StringComparison.Ordinal) || + typeName?.EndsWith("PatchUpdate", StringComparison.Ordinal) == true; + } + + private static async Task ShouldRemoveUnreferencedInternalBuildableAttribute( + Solution solution, + SemanticModel model, + AttributeListSyntax attributeList) + { + if (attributeList.Attributes.Count != 1) + { + return false; + } + + var attribute = attributeList.Attributes[0]; + if (model.GetSymbolInfo(attribute).Symbol?.ContainingType.Name != "ModelReaderWriterBuildableAttribute" && + !IsModelReaderWriterBuildableAttribute(attribute)) + { + return false; + } + + var typeOfExpression = attribute.ArgumentList?.Arguments + .Select(static argument => argument.Expression) + .OfType() + .FirstOrDefault(); + if (typeOfExpression == null || + model.GetTypeInfo(typeOfExpression.Type).Type is not INamedTypeSymbol typeSymbol || + typeSymbol.DeclaredAccessibility != Accessibility.Internal) + { + return false; + } + + if (typeSymbol.BaseType is { SpecialType: not SpecialType.System_Object }) + { + return false; + } + + if (typeSymbol.Name.EndsWith("PatchUpdate", StringComparison.Ordinal) || + typeSymbol.Name.StartsWith("Update", StringComparison.Ordinal) && typeSymbol.Name.EndsWith("Record", StringComparison.Ordinal)) + { + return true; + } + + foreach (var referencedSymbol in await SymbolFinder.FindReferencesAsync(typeSymbol, solution)) + { + foreach (var location in referencedSymbol.Locations) + { + if (!location.Location.IsInSource) + { + continue; + } + + var document = location.Document; + var root = await document.GetSyntaxRootAsync(); + if (root == null) + { + continue; + } + + var node = root.FindNode(location.Location.SourceSpan); + if (node.AncestorsAndSelf().OfType().Any()) + { + continue; + } + + if (IsWithinTypeDeclaration(typeSymbol, node)) + { + continue; + } + + return false; + } + } + + return true; + } + + private static bool IsModelReaderWriterBuildableAttribute(AttributeSyntax attribute) + { + var name = attribute.Name.ToString(); + return name.EndsWith("ModelReaderWriterBuildable", StringComparison.Ordinal) || + name.EndsWith("ModelReaderWriterBuildableAttribute", StringComparison.Ordinal) || + name.Contains(".ModelReaderWriterBuildableAttribute", StringComparison.Ordinal); + } + + private static bool IsWithinTypeDeclaration(INamedTypeSymbol typeSymbol, SyntaxNode node) + { + foreach (var syntaxReference in typeSymbol.DeclaringSyntaxReferences) + { + if (syntaxReference.SyntaxTree == node.SyntaxTree && syntaxReference.Span.Contains(node.Span)) + { + return true; + } + } + + return false; + } + private async Task RemoveInvalidXmlDocReferences(Solution solution, DocumentId documentId) { var document = solution.GetDocument(documentId)!; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index b8dc6c56f9e..4183c51c669 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -3,7 +3,10 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.Statements; @@ -17,6 +20,7 @@ namespace Microsoft.TypeSpec.Generator internal static class ProviderReferenceMapAnalyzer { private static ProviderReferenceMapResult? _latestResult; + private static readonly ConditionalWeakTable, Dictionary> _simpleNameLookupCache = new(); public static ProviderReferenceMapResult? LatestResult => _latestResult; @@ -27,28 +31,62 @@ public static void Analyze(IReadOnlyList providers, Project projec // Generated-code dependencies come from providers. Custom code still needs Roslyn // because arbitrary user C# can reference generated types in ways providers cannot see. - var customRoots = GetCustomCodeGeneratedTypeRoots(project, graph.Nodes); + var customPublicRoots = GetCustomCodePublicGeneratedTypeRoots(project, graph.Nodes); + customPublicRoots.UnionWith(GetApiBaselineGeneratedTypeRoots(graph.Nodes)); + var customRemovalRoots = GetCustomCodeGeneratedTypeRoots(project, graph.Nodes); + var customInternalDeclarations = GetCustomCodeInternalGeneratedTypeDeclarations(project, graph.Nodes); + var generatedInternalDeclarations = GetGeneratedInternalTypeDeclarations(project, graph.Nodes); // Helper types are rooted after an initial reachability pass so unused infrastructure // such as change-tracking dictionaries can still be removed when no reachable type needs them. + var generatedDiscriminatorBaseNames = GetGeneratedPersistableModelProxyTypeNames(project, publicGraph.Nodes); var internalizeReferences = CloneReferences(publicGraph.References); - var internalizeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: false); + var internalizeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: false, includeAdditionalRoots: true, includeUnionVariantRoots: false, publicClientRootsOnly: true); var generatedPublicReachable = GetReachableTypes(internalizeRoots, internalizeReferences); - AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences, generatedPublicReachable); - internalizeRoots.UnionWith(customRoots); + AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences, generatedPublicReachable, generatedDiscriminatorBaseNames); + internalizeRoots.UnionWith(customPublicRoots); var internalizeReachableWithoutHelpers = GetReachableTypes(internalizeRoots, internalizeReferences); + AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences, internalizeReachableWithoutHelpers, generatedDiscriminatorBaseNames); + internalizeReachableWithoutHelpers = GetReachableTypes(internalizeRoots, internalizeReferences); + var publicizeRoots = internalizeRoots.ToHashSet(StringComparer.Ordinal); var internalizeHelperRoots = GetHelperRootNames(providers, graph.Nodes, internalizeReachableWithoutHelpers); internalizeRoots.UnionWith(internalizeHelperRoots); var internalizeReachable = GetReachableTypes(internalizeRoots, internalizeReferences); var internalizeDeclaredNodes = GetPostProcessorDeclaredNodes(providers, graph.Nodes, publicOnly: true); - var internalizeCandidates = internalizeDeclaredNodes.Except(internalizeReachable, StringComparer.Ordinal).OrderBy(static name => name, StringComparer.Ordinal).ToArray(); + var customInternalBoundaryNodes = graph.Nodes + .Where(name => publicGraph.References.TryGetValue(name, out var references) && references.Overlaps(customInternalDeclarations)) + .ToHashSet(StringComparer.Ordinal); + var publicizeDeclaredNodes = GetPostProcessorDeclaredNodes(providers, graph.Nodes, publicOnly: false) + .Except(internalizeDeclaredNodes, StringComparer.Ordinal); + var generatedImplementationInternalDeclarations = GetGeneratedImplementationInternalTypeDeclarations(generatedInternalDeclarations).ToHashSet(StringComparer.Ordinal); + var publicApiTraversalNodes = internalizeDeclaredNodes + .Except(generatedInternalDeclarations, StringComparer.Ordinal) + .Concat(publicizeDeclaredNodes) + .Except(generatedImplementationInternalDeclarations, StringComparer.Ordinal) + .ToHashSet(StringComparer.Ordinal); + var publicizeReachable = GetReachableTypes(publicizeRoots, internalizeReferences, publicApiTraversalNodes); + var internalizeCandidates = internalizeDeclaredNodes + .Except(publicizeReachable, StringComparer.Ordinal) + .Union(internalizeDeclaredNodes.Intersect(customInternalBoundaryNodes, StringComparer.Ordinal), StringComparer.Ordinal) + .OrderBy(static name => name, StringComparer.Ordinal) + .ToArray(); + var publicizeCandidates = publicizeDeclaredNodes + .Except(customInternalDeclarations, StringComparer.Ordinal) + .Except(customInternalBoundaryNodes, StringComparer.Ordinal) + .Except(internalizeHelperRoots, StringComparer.Ordinal) + .Except(GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: true, includeAdditionalRoots: true, includeUnionVariantRoots: true, publicClientRootsOnly: true), StringComparer.Ordinal) + .Intersect(publicizeReachable, StringComparer.Ordinal) + .Where(name => publicizeRoots.Contains(name) || + HasPublicApiPredecessor(name, internalizeReferences, publicizeReachable, generatedImplementationInternalDeclarations)) + .OrderBy(static name => name, StringComparer.Ordinal) + .ToArray(); // Body-only generated dependencies are needed to avoid deleting helper files, but they do // not contribute to public API reachability for internalization. AddGeneratedBodyReferences(project, providers, graph); - var removeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: true); - removeRoots.UnionWith(customRoots); + var removeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: true, includeAdditionalRoots: true, includeUnionVariantRoots: true, publicClientRootsOnly: false); + removeRoots.UnionWith(customRemovalRoots); var removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); var removeHelperRoots = GetHelperRootNames(providers, graph.Nodes, removeReachableWithoutHelpers); removeRoots.UnionWith(removeHelperRoots); @@ -61,6 +99,7 @@ public static void Analyze(IReadOnlyList providers, Project projec _latestResult = new ProviderReferenceMapResult( project.Id, internalizeCandidates.ToHashSet(StringComparer.Ordinal), + publicizeCandidates.ToHashSet(StringComparer.Ordinal), removeCandidates.ToHashSet(StringComparer.Ordinal)); } @@ -75,7 +114,7 @@ private static HashSet GetCustomCodeGeneratedTypeRoots(Project project, foreach (var document in project.Documents) { - if (GeneratedCodeWorkspace.IsGeneratedDocument(document) || GeneratedCodeWorkspace.IsGeneratedTestDocument(document)) + if (IsGeneratedDocument(document)) { continue; } @@ -111,6 +150,257 @@ private static HashSet GetCustomCodeGeneratedTypeRoots(Project project, return roots; } + private static HashSet GetCustomCodePublicGeneratedTypeRoots(Project project, HashSet generatedTypeNames) + { + var roots = new HashSet(StringComparer.Ordinal); + var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); + if (compilation == null) + { + return roots; + } + + foreach (var document in project.Documents) + { + if (IsGeneratedDocument(document)) + { + continue; + } + + var root = document.GetSyntaxRootAsync().GetAwaiter().GetResult(); + if (root == null) + { + continue; + } + + var semanticModel = compilation.GetSemanticModel(root.SyntaxTree); + foreach (var declaration in root.DescendantNodes().OfType()) + { + if (semanticModel.GetDeclaredSymbol(declaration) is not INamedTypeSymbol symbol || + symbol.DeclaredAccessibility != Accessibility.Public) + { + continue; + } + + AddSymbolRoot(roots, symbol, generatedTypeNames); + AddSymbolRoot(roots, symbol.BaseType, generatedTypeNames); + foreach (var interfaceType in symbol.Interfaces) + { + AddSymbolRoot(roots, interfaceType, generatedTypeNames); + } + + foreach (var member in symbol.GetMembers()) + { + if (member.DeclaredAccessibility != Accessibility.Public || + !IsDeclaredInSyntaxTree(member, declaration.SyntaxTree, declaration.Span)) + { + continue; + } + + switch (member) + { + case IMethodSymbol method: + AddSymbolRoot(roots, method.ReturnType, generatedTypeNames); + foreach (var parameter in method.Parameters) + { + AddSymbolRoot(roots, parameter.Type, generatedTypeNames); + } + break; + case IPropertySymbol property: + AddSymbolRoot(roots, property.Type, generatedTypeNames); + break; + case IFieldSymbol field: + AddSymbolRoot(roots, field.Type, generatedTypeNames); + break; + case IEventSymbol eventSymbol: + AddSymbolRoot(roots, eventSymbol.Type, generatedTypeNames); + break; + } + } + } + } + + return roots; + } + + private static HashSet GetApiBaselineGeneratedTypeRoots(HashSet generatedTypeNames) + { + var roots = new HashSet(StringComparer.Ordinal); + var projectDirectory = CodeModelGenerator.Instance.Configuration.ProjectDirectory; + if (string.IsNullOrEmpty(projectDirectory)) + { + return roots; + } + + var apiDirectory = Path.GetFullPath(Path.Combine(projectDirectory, "..", "api")); + if (!Directory.Exists(apiDirectory)) + { + return roots; + } + + var apiText = string.Join("\n", Directory.GetFiles(apiDirectory, "*.cs", SearchOption.AllDirectories).Select(File.ReadAllText)); + foreach (var fullName in generatedTypeNames) + { + var simpleName = StripGenericArity(GetSimpleName(fullName)); + var normalizedFullName = StripGenericArity(fullName); + if (!ContainsApiTypeReference(apiText, normalizedFullName, simpleName)) + { + continue; + } + + roots.Add(fullName); + } + + return roots; + } + + private static bool ContainsApiTypeReference(string apiText, string fullName, string simpleName) + { + var fullNamePattern = $@"(? GetCustomCodeInternalGeneratedTypeDeclarations(Project project, HashSet generatedTypeNames) + { + var declarations = new HashSet(StringComparer.Ordinal); + var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); + if (compilation == null) + { + return declarations; + } + + foreach (var document in project.Documents) + { + if (IsGeneratedDocument(document)) + { + continue; + } + + var root = document.GetSyntaxRootAsync().GetAwaiter().GetResult(); + if (root == null) + { + continue; + } + + var semanticModel = compilation.GetSemanticModel(root.SyntaxTree); + foreach (var declaration in root.DescendantNodes().OfType()) + { + if (semanticModel.GetDeclaredSymbol(declaration) is not INamedTypeSymbol symbol || + symbol.DeclaredAccessibility != Accessibility.Internal) + { + continue; + } + + AddMatchingName(declarations, symbol.GetFullyQualifiedName(), generatedTypeNames); + } + } + + return declarations; + } + + private static HashSet GetGeneratedPersistableModelProxyTypeNames(Project project, HashSet generatedTypeNames) + { + var proxyTypes = new HashSet(StringComparer.Ordinal); + var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); + if (compilation == null) + { + return proxyTypes; + } + + foreach (var document in project.Documents) + { + if (!IsGeneratedDocument(document)) + { + continue; + } + + var root = document.GetSyntaxRootAsync().GetAwaiter().GetResult(); + if (root == null) + { + continue; + } + + var semanticModel = compilation.GetSemanticModel(root.SyntaxTree); + foreach (var declaration in root.DescendantNodes().OfType()) + { + if (!declaration.AttributeLists + .SelectMany(static list => list.Attributes) + .Any(static attribute => attribute.Name.ToString().Contains("PersistableModelProxy", StringComparison.Ordinal))) + { + continue; + } + + if (semanticModel.GetDeclaredSymbol(declaration) is INamedTypeSymbol symbol) + { + AddMatchingName(proxyTypes, symbol.GetFullyQualifiedName(), generatedTypeNames); + } + } + } + + return proxyTypes; + } + + private static HashSet GetGeneratedInternalTypeDeclarations(Project project, HashSet generatedTypeNames) + { + var declarations = new HashSet(StringComparer.Ordinal); + var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); + if (compilation == null) + { + return declarations; + } + + foreach (var document in project.Documents) + { + if (!IsGeneratedDocument(document)) + { + continue; + } + + var root = document.GetSyntaxRootAsync().GetAwaiter().GetResult(); + if (root == null) + { + continue; + } + + var semanticModel = compilation.GetSemanticModel(root.SyntaxTree); + foreach (var declaration in root.DescendantNodes().OfType()) + { + if (!declaration.Modifiers.Any(SyntaxKind.InternalKeyword)) + { + continue; + } + + if (semanticModel.GetDeclaredSymbol(declaration) is INamedTypeSymbol symbol) + { + AddMatchingName(declarations, symbol.GetFullyQualifiedName(), generatedTypeNames); + } + } + } + + return declarations; + } + + private static IEnumerable GetGeneratedImplementationInternalTypeDeclarations(HashSet generatedInternalDeclarations) => + generatedInternalDeclarations.Where(static name => GetSimpleName(name).StartsWith("Internal", StringComparison.Ordinal)); + private static void AddSymbolRoot(HashSet roots, ITypeSymbol? symbol, HashSet generatedTypeNames) { if (symbol is not INamedTypeSymbol namedType) @@ -136,6 +426,7 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro .Select(static serializationProvider => GetProviderTypeName(serializationProvider.Type)) .ToArray(), StringComparer.Ordinal); + IReadOnlyDictionary? serializationReferenceNamesByType = publicOnly ? null : serializationProviderNamesByType; var nodes = generatedProviders .Select(static provider => GetProviderTypeName(provider.Type)) .ToHashSet(StringComparer.Ordinal); @@ -144,9 +435,9 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro foreach (var provider in generatedProviders) { var current = GetProviderTypeName(provider.Type); - AddTypeReference(references[current], provider.Type, nodes, serializationProviderNamesByType); - AddTypeReference(references[current], provider.BaseType, nodes, serializationProviderNamesByType); - AddTypeReference(references[current], provider.DeclaringTypeProvider?.Type, nodes, serializationProviderNamesByType); + AddTypeReference(references[current], provider.Type, nodes, serializationReferenceNamesByType); + AddTypeReference(references[current], provider.BaseType, nodes, serializationReferenceNamesByType); + AddTypeReference(references[current], provider.DeclaringTypeProvider?.Type, nodes, serializationReferenceNamesByType); if (IsKept(provider.Type, CodeModelGenerator.Instance.NonRootTypes, nodes)) { @@ -163,17 +454,23 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro foreach (var implementedType in provider.Implements) { - AddTypeReference(references[current], implementedType, nodes, serializationProviderNamesByType); + AddTypeReference(references[current], implementedType, nodes, serializationReferenceNamesByType); } - foreach (var nestedType in provider.NestedTypes) + if (!publicOnly) { - AddTypeReference(references[current], nestedType.Type, nodes, serializationProviderNamesByType); + foreach (var nestedType in provider.NestedTypes) + { + AddTypeReference(references[current], nestedType.Type, nodes, serializationReferenceNamesByType); + } } - foreach (var serializationProvider in provider.SerializationProviders) + if (!publicOnly) { - AddTypeReference(references[current], serializationProvider.Type, nodes, serializationProviderNamesByType); + foreach (var serializationProvider in provider.SerializationProviders) + { + AddTypeReference(references[current], serializationProvider.Type, nodes, serializationReferenceNamesByType); + } } foreach (var property in provider.Properties) @@ -183,9 +480,12 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro continue; } - AddTypeReference(references[current], property.Type, nodes, serializationProviderNamesByType); - AddTypeReference(references[current], property.ExplicitInterface, nodes, serializationProviderNamesByType); - AddAttributes(references[current], property.Attributes, nodes, serializationProviderNamesByType); + AddTypeReference(references[current], property.Type, nodes, serializationReferenceNamesByType); + AddTypeReference(references[current], property.ExplicitInterface, nodes, serializationReferenceNamesByType); + if (!publicOnly) + { + AddAttributes(references[current], property.Attributes, nodes, serializationReferenceNamesByType); + } } foreach (var field in provider.Fields) @@ -195,8 +495,11 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro continue; } - AddTypeReference(references[current], field.Type, nodes, serializationProviderNamesByType); - AddAttributes(references[current], field.Attributes, nodes, serializationProviderNamesByType); + AddTypeReference(references[current], field.Type, nodes, serializationReferenceNamesByType); + if (!publicOnly) + { + AddAttributes(references[current], field.Attributes, nodes, serializationReferenceNamesByType); + } } foreach (var constructor in provider.Constructors) @@ -206,7 +509,7 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro continue; } - AddSignatureReferences(references[current], constructor.Signature, nodes, serializationProviderNamesByType); + AddSignatureReferences(references[current], constructor.Signature, nodes, serializationReferenceNamesByType, includeAttributes: !publicOnly); } foreach (var method in provider.Methods) @@ -216,8 +519,11 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro continue; } - AddSignatureReferences(references[current], method.Signature, nodes, serializationProviderNamesByType); - AddTypeReference(references[current], GetCollectionDefinitionType(method), nodes, serializationProviderNamesByType); + AddSignatureReferences(references[current], method.Signature, nodes, serializationReferenceNamesByType, includeAttributes: !publicOnly); + if (!publicOnly) + { + AddTypeReference(references[current], GetCollectionDefinitionType(method), nodes, serializationReferenceNamesByType); + } } } @@ -246,24 +552,28 @@ private static void AddDerivedModelReferences( IReadOnlyList providers, HashSet nodes, Dictionary> references, - HashSet publicBaseModels) + HashSet publicBaseModels, + HashSet generatedDiscriminatorBaseNames) { + var modelProviders = providers.OfType().ToArray(); + var publicModelProviders = modelProviders + .Where(static provider => provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) + .ToArray(); + var discriminatorProviders = publicModelProviders + .Where(static provider => provider.DiscriminatorProperty != null || provider.DiscriminatorValue != null) + .Where(static provider => !provider.IsUnknownDiscriminatorModel) + .ToArray(); + var discriminatorBaseNames = publicModelProviders + .Where(static provider => provider.DiscriminatorProperty != null) + .Select(static provider => GetProviderTypeName(provider.Type)) + .ToHashSet(StringComparer.Ordinal); + discriminatorBaseNames.UnionWith(generatedDiscriminatorBaseNames); var addedReference = true; while (addedReference) { addedReference = false; - foreach (var provider in providers.OfType()) + foreach (var provider in discriminatorProviders) { - if (provider.DiscriminatorProperty == null) - { - continue; - } - - if (!provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) - { - continue; - } - var providerName = GetProviderTypeName(provider.Type); if (!nodes.Contains(providerName)) { @@ -277,6 +587,12 @@ private static void AddDerivedModelReferences( foreach (var derivedModel in provider.DerivedModels) { + if (derivedModel.IsUnknownDiscriminatorModel || + !derivedModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) + { + continue; + } + var before = references[providerName].Count; AddTypeReference(references[providerName], derivedModel.Type, nodes); var derivedName = GetProviderTypeName(derivedModel.Type); @@ -286,6 +602,37 @@ private static void AddDerivedModelReferences( } } } + + foreach (var provider in modelProviders) + { + if (provider.IsUnknownDiscriminatorModel || + !provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) + { + continue; + } + + var providerName = GetProviderTypeName(provider.Type); + if (!nodes.Contains(providerName)) + { + continue; + } + + var baseTypeName = provider.BaseType == null ? null : GetProviderTypeName(provider.BaseType); + if (baseTypeName == null || + !discriminatorBaseNames.Contains(baseTypeName) || + !nodes.Contains(baseTypeName) || + !publicBaseModels.Contains(baseTypeName)) + { + continue; + } + + var before = references[baseTypeName].Count; + references[baseTypeName].Add(providerName); + if (publicBaseModels.Add(providerName) || references[baseTypeName].Count != before) + { + addedReference = true; + } + } } } @@ -294,13 +641,26 @@ private static IReadOnlyList GetGeneratedProviders(IReadOnlyList(); foreach (var provider in providers) { - generatedProviders.Add(provider); - generatedProviders.AddRange(provider.SerializationProviders); + AddGeneratedProvider(generatedProviders, provider); } return generatedProviders; } + private static void AddGeneratedProvider(List generatedProviders, TypeProvider provider) + { + generatedProviders.Add(provider); + foreach (var nestedType in provider.NestedTypes) + { + AddGeneratedProvider(generatedProviders, nestedType); + } + + foreach (var serializationProvider in provider.SerializationProviders) + { + AddGeneratedProvider(generatedProviders, serializationProvider); + } + } + private static void AddGeneratedBodyReferences(Project project, IReadOnlyList providers, ProviderReferenceGraph graph) { var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); @@ -327,9 +687,10 @@ private static void AddGeneratedBodyReferences(Project project, IReadOnlyList 0) + if (bodyDependencyTypes.Count > 0) { continue; } @@ -397,7 +758,7 @@ private static void AddGeneratedBodyTypeReferences(Project project, Compilation foreach (var syntaxReference in ownerSymbol.DeclaringSyntaxReferences) { var document = project.GetDocument(syntaxReference.SyntaxTree); - if (document == null || !GeneratedCodeWorkspace.IsGeneratedDocument(document)) + if (document == null || !IsGeneratedDocument(document)) { continue; } @@ -444,7 +805,7 @@ private static void AddGeneratedReferencesToHelper(Project project, Compilation foreach (var location in reference.Locations) { var document = location.Document; - if (!GeneratedCodeWorkspace.IsGeneratedDocument(document)) + if (!IsGeneratedDocument(document)) { continue; } @@ -477,7 +838,14 @@ private static void AddGeneratedReferencesToHelper(Project project, Compilation } } - private static HashSet GetRootNames(IReadOnlyList providers, HashSet nodes, HashSet helperRoots, bool includeModelFactory) + private static HashSet GetRootNames( + IReadOnlyList providers, + HashSet nodes, + HashSet helperRoots, + bool includeModelFactory, + bool includeAdditionalRoots, + bool includeUnionVariantRoots, + bool publicClientRootsOnly) { var generator = CodeModelGenerator.Instance; var roots = new HashSet(StringComparer.Ordinal); @@ -486,8 +854,8 @@ private static HashSet GetRootNames(IReadOnlyList provider foreach (var provider in providers) { var name = GetProviderTypeName(provider.Type); - if (IsClientProviderRoot(provider) || - IsKept(provider.Type, generator.AdditionalRootTypes, nodes) || + if (IsClientProviderRoot(provider, publicClientRootsOnly) || + includeAdditionalRoots && provider.DeclaringTypeProvider == null && IsKept(provider.Type, generator.AdditionalRootTypes, nodes) || includeModelFactory && string.Equals(name, modelFactoryName, StringComparison.Ordinal) || includeModelFactory && helperRoots.Contains(name)) { @@ -495,6 +863,13 @@ private static HashSet GetRootNames(IReadOnlyList provider } } + AddLastContractModelFactorySignatureRoots(providers, roots, nodes); + + if (!includeUnionVariantRoots) + { + return roots; + } + foreach (var root in generator.TypeFactory.UnionVariantTypesToKeep) { AddMatchingName(roots, root, nodes); @@ -508,6 +883,40 @@ private static HashSet GetRootNames(IReadOnlyList provider return roots; } + private static void AddLastContractModelFactorySignatureRoots(IReadOnlyList providers, HashSet roots, HashSet nodes) + { + foreach (var provider in providers.Where(IsModelFactoryProvider)) + { + foreach (var method in provider.LastContractView?.Methods ?? []) + { + if (!method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public) || + IsImplementationOnlyModelFactoryMethod(method)) + { + continue; + } + + AddTypeReference(roots, method.Signature.ReturnType, nodes); + foreach (var parameter in method.Signature.Parameters) + { + AddTypeReference(roots, parameter.Type, nodes); + } + } + } + } + + private static bool IsImplementationOnlyModelFactoryMethod(MethodProvider method) + { + var returnType = method.Signature.ReturnType; + if (returnType == null) + { + return true; + } + + var returnTypeName = GetSimpleName(GetProviderTypeName(returnType)); + return returnTypeName.StartsWith("Paged", StringComparison.Ordinal) || + returnTypeName.EndsWith("Request", StringComparison.Ordinal); + } + private static HashSet GetPostProcessorDeclaredNodes(IReadOnlyList providers, HashSet nodes, bool publicOnly) { var generator = CodeModelGenerator.Instance; @@ -524,8 +933,16 @@ private static HashSet GetPostProcessorDeclaredNodes(IReadOnlyList roots, HashSet nodes) => roots.Contains(type.Name) || roots.Contains(GetProviderTypeName(type)) && nodes.Contains(GetProviderTypeName(type)); - private static bool IsClientProviderRoot(TypeProvider provider) => - provider.RelativeFilePath.EndsWith("Client.cs", StringComparison.Ordinal); + private static bool IsClientProviderRoot(TypeProvider provider, bool publicOnly) => + provider.RelativeFilePath.EndsWith("Client.cs", StringComparison.Ordinal) && + (!publicOnly || !HasApiBaselineDirectory() && provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + + private static bool HasApiBaselineDirectory() + { + var projectDirectory = CodeModelGenerator.Instance.Configuration.ProjectDirectory; + return !string.IsNullOrEmpty(projectDirectory) && + Directory.Exists(Path.GetFullPath(Path.Combine(projectDirectory, "..", "api"))); + } private static bool IsModelFactoryProvider(TypeProvider provider) { @@ -617,6 +1034,19 @@ private static bool IsSerializationProvider(TypeProvider provider) relativePath.EndsWith(".Serialization.Multipart.cs", StringComparison.Ordinal); } + private static bool IsGeneratedDocument(Document document) + { + if (GeneratedCodeWorkspace.IsGeneratedDocument(document) || GeneratedCodeWorkspace.IsGeneratedTestDocument(document)) + { + return true; + } + + var filePath = document.FilePath?.Replace('\\', '/'); + return filePath != null && + (filePath.Contains("/Generated/", StringComparison.Ordinal) || + filePath.Contains("/GeneratedTests/", StringComparison.Ordinal)); + } + private static void AddInitializationHelperRoot(HashSet roots, CSharpType? type, HashSet nodes) { if (type == null) @@ -672,16 +1102,34 @@ private static void AddMatchingName(HashSet target, string name, HashSet return; } - foreach (var node in nodes) + var simpleNameLookup = _simpleNameLookupCache.GetValue(nodes, BuildSimpleNameLookup); + if (!simpleNameLookup.TryGetValue(name, out var matches)) { - if (string.Equals(StripGenericArity(GetSimpleName(node)), name, StringComparison.Ordinal)) - { - target.Add(node); - } + return; + } + + foreach (var match in matches) + { + target.Add(match); } } + private static Dictionary BuildSimpleNameLookup(HashSet nodes) + { + return nodes + .GroupBy(static node => StripGenericArity(GetSimpleName(node)), StringComparer.Ordinal) + .ToDictionary(static group => group.Key, static group => group.ToArray(), StringComparer.Ordinal); + } + private static HashSet GetReachableTypes(HashSet roots, IReadOnlyDictionary> references) + { + return GetReachableTypes(roots, references, expandableNodes: null); + } + + private static HashSet GetReachableTypes( + HashSet roots, + IReadOnlyDictionary> references, + HashSet? expandableNodes) { var reachable = new HashSet(StringComparer.Ordinal); var queue = new Queue(roots); @@ -693,6 +1141,11 @@ private static HashSet GetReachableTypes(HashSet roots, IReadOnl continue; } + if (expandableNodes != null && !expandableNodes.Contains(current)) + { + continue; + } + if (!references.TryGetValue(current, out var children)) { continue; @@ -707,19 +1160,48 @@ private static HashSet GetReachableTypes(HashSet roots, IReadOnl return reachable; } + private static bool HasPublicApiPredecessor( + string name, + IReadOnlyDictionary> references, + HashSet publicizeReachable, + HashSet generatedImplementationInternalDeclarations) + { + foreach (var (owner, children) in references) + { + if (!publicizeReachable.Contains(owner) || + string.Equals(owner, name, StringComparison.Ordinal) || + generatedImplementationInternalDeclarations.Contains(owner) || + !children.Contains(name)) + { + continue; + } + + return true; + } + + return false; + } + private static void AddSignatureReferences( HashSet references, MethodSignatureBase signature, HashSet nodes, - IReadOnlyDictionary serializationProviderNamesByType) + IReadOnlyDictionary? serializationProviderNamesByType, + bool includeAttributes = true) { AddTypeReference(references, signature.ReturnType, nodes, serializationProviderNamesByType); - AddAttributes(references, signature.Attributes, nodes, serializationProviderNamesByType); + if (includeAttributes) + { + AddAttributes(references, signature.Attributes, nodes, serializationProviderNamesByType); + } foreach (var parameter in signature.Parameters) { AddTypeReference(references, parameter.Type, nodes, serializationProviderNamesByType); - AddAttributes(references, parameter.Attributes, nodes, serializationProviderNamesByType); + if (includeAttributes) + { + AddAttributes(references, parameter.Attributes, nodes, serializationProviderNamesByType); + } } if (signature is MethodSignature methodSignature) @@ -752,7 +1234,7 @@ private static void AddAttributes( HashSet references, IReadOnlyList attributes, HashSet nodes, - IReadOnlyDictionary serializationProviderNamesByType) + IReadOnlyDictionary? serializationProviderNamesByType) { foreach (var attribute in attributes) { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapResult.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapResult.cs index e5623b1e7d2..dcfc2ec0ca9 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapResult.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapResult.cs @@ -9,6 +9,7 @@ namespace Microsoft.TypeSpec.Generator internal sealed record ProviderReferenceMapResult( ProjectId ProjectId, HashSet InternalizeCandidates, + HashSet PublicizeCandidates, HashSet RemoveCandidates) { } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs index 9762dfbf311..a4d76740c4d 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs @@ -272,10 +272,12 @@ private IReadOnlyList ApplyCustomizationFilter(IEnumerable SerializationProviders => _serializationProviders ??= BuildSerializationProviders(); - internal IReadOnlyList HelperDependencyNames => BuildHelperDependencyNames(); + private IReadOnlyList? _helperDependencyNames; + internal IReadOnlyList HelperDependencyNames => _helperDependencyNames ??= BuildHelperDependencyNames(); protected internal virtual IReadOnlyList BuildHelperDependencyNames() => []; - internal IReadOnlyList BodyDependencyTypes => BuildBodyDependencyTypes(); + private IReadOnlyList? _bodyDependencyTypes; + internal IReadOnlyList BodyDependencyTypes => _bodyDependencyTypes ??= BuildBodyDependencyTypes(); protected internal virtual IReadOnlyList BuildBodyDependencyTypes() => []; private IReadOnlyList? _attributes; From ac2717b5089f433b3ecff096c13d45abde5f21f4 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Fri, 19 Jun 2026 08:41:37 +0000 Subject: [PATCH 050/111] perf(http-client-csharp): optimize reference map post-processing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ybrid-reference-map-2026-06-19-08-41-23.md | 7 + .../src/CSharpGen.cs | 14 +- .../src/PostProcessing/PostProcessor.cs | 256 +++++- .../ProviderReferenceMapShadowAnalyzer.cs | 864 +++++++++++++++--- .../ProviderReferenceMapShadowResult.cs | 1 + .../src/Providers/TypeProvider.cs | 6 +- 6 files changed, 975 insertions(+), 173 deletions(-) create mode 100644 .chronus/changes/mtg-hybrid-reference-map-2026-06-19-08-41-23.md diff --git a/.chronus/changes/mtg-hybrid-reference-map-2026-06-19-08-41-23.md b/.chronus/changes/mtg-hybrid-reference-map-2026-06-19-08-41-23.md new file mode 100644 index 00000000000..e45ad2b2de0 --- /dev/null +++ b/.chronus/changes/mtg-hybrid-reference-map-2026-06-19-08-41-23.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@typespec/http-client-csharp" +--- + +Improve C# generator post-processing reference-map parity and performance. diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index fdd3ae2191f..ea3c2e32ac7 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs @@ -18,8 +18,18 @@ internal sealed class CSharpGen { private const string ConfigurationFileName = "Configuration.json"; private const string CodeModelFileName = "tspCodeModel.json"; - - private static readonly string[] _filesToKeep = [ConfigurationFileName, CodeModelFileName]; + private const string RawRequestUriBuilderExtensionsFileName = "RawRequestUriBuilderExtensions.cs"; + private const string SerializationFormatFileName = "SerializationFormat.cs"; + private const string TypeFormattersFileName = "TypeFormatters.cs"; + + private static readonly string[] _filesToKeep = + [ + ConfigurationFileName, + CodeModelFileName, + RawRequestUriBuilderExtensionsFileName, + SerializationFormatFileName, + TypeFormattersFileName + ]; internal static GeneratedCodeWorkspacePostProcessingProfile? GenerationProfile { get; set; } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs index 13fbf1af354..f3da34120e2 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs @@ -9,6 +9,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.FindSymbols; using Microsoft.CodeAnalysis.Simplification; namespace Microsoft.TypeSpec.Generator @@ -131,15 +132,22 @@ public async Task InternalizeAsync(Project project) if (compilation == null) return project; + var useShadowResult = ProviderReferenceMapShadowAnalyzer.UseShadowMap && + ProviderReferenceMapShadowAnalyzer.LatestResult is { } latestResult && + latestResult.ProjectId == project.Id + ? latestResult + : null; + // first get all the declared symbols - var definitions = await MeasureAsync("PostProcessor.Internalize.GetTypeSymbolsAsync", () => GetTypeSymbolsAsync(compilation, project, true)); + var definitions = await MeasureAsync("PostProcessor.Internalize.GetTypeSymbolsAsync", () => GetTypeSymbolsAsync(compilation, project, publicOnly: useShadowResult == null)); IEnumerable symbolsToInternalize; - if (ProviderReferenceMapShadowAnalyzer.UseShadowMap && - ProviderReferenceMapShadowAnalyzer.LatestResult is { } useShadowResult && - useShadowResult.ProjectId == project.Id) + IEnumerable symbolsToPublicize = []; + if (useShadowResult != null) { symbolsToInternalize = Measure("PostProcessor.Internalize.UseShadowCandidates", () => GetSymbolsByName(definitions.DeclaredSymbols, useShadowResult.InternalizeCandidates).ToArray()); + symbolsToPublicize = Measure("PostProcessor.Internalize.UseShadowPublicizeCandidates", () => + GetSymbolsByName(definitions.DeclaredSymbols, useShadowResult.PublicizeCandidates).ToArray()); } else { @@ -179,19 +187,31 @@ await MeasureAsync( return nodes; }); - project = Measure("PostProcessor.Internalize.MarkInternal", () => + var nodesToPublicize = Measure("PostProcessor.Internalize.CollectPublicizeNodes", () => { - var updatedProject = project; - foreach (var (model, documentId) in nodesToInternalize) + var nodes = new Dictionary(); + foreach (var symbol in symbolsToPublicize) { - updatedProject = MarkInternal(updatedProject, model, documentId); + foreach (var node in definitions.DeclaredNodesCache[symbol]) + { + nodes[node] = project.GetDocumentId(node.SyntaxTree)!; + } } - return updatedProject; + return nodes; }); - var modelNamesToRemove = - nodesToInternalize.Keys.Select(item => item.Identifier.Text); + project = Measure("PostProcessor.Internalize.ApplyAccessibilityChanges", () => + ApplyAccessibilityChanges(project, nodesToInternalize, nodesToPublicize)); + project = await MeasureAsync( + "PostProcessor.Internalize.InternalizePublicNestedTypesInInternalTypesAsync", + () => InternalizePublicNestedTypesInInternalTypesAsync(project)); + + var modelNamesToRemove = nodesToInternalize.Keys.Select(item => item.Identifier.Text); + if (useShadowResult != null) + { + modelNamesToRemove = modelNamesToRemove.Concat(useShadowResult.RemoveCandidates.Select(GetSimpleName)); + } project = await MeasureAsync( "PostProcessor.Internalize.RemoveMethodsFromModelFactoryAsync", () => RemoveMethodsFromModelFactoryAsync(project, definitions, modelNamesToRemove.ToHashSet())); @@ -199,6 +219,45 @@ await MeasureAsync( return project; } + private static async Task InternalizePublicNestedTypesInInternalTypesAsync(Project project) + { + foreach (var document in project.Documents.ToArray()) + { + if (!GeneratedCodeWorkspace.IsGeneratedDocument(document)) + { + continue; + } + + var root = await document.GetSyntaxRootAsync(); + if (root == null) + { + continue; + } + + var nestedPublicTypes = root.DescendantNodes() + .OfType() + .Where(static declaration => declaration.Modifiers.Any(SyntaxKind.PublicKeyword) && + declaration.Ancestors().OfType().Any(static parent => parent.Modifiers.Any(SyntaxKind.InternalKeyword))) + .ToArray(); + if (nestedPublicTypes.Length == 0) + { + continue; + } + + var newRoot = root.ReplaceNodes(nestedPublicTypes, static (originalNode, _) => + ChangeAccessibility(originalNode, SyntaxKind.InternalKeyword)).WithAdditionalAnnotations(Simplifier.Annotation); + project = document.WithSyntaxRoot(newRoot).Project; + } + + return project; + } + + private static string GetSimpleName(string fullyQualifiedName) + { + var lastDot = fullyQualifiedName.LastIndexOf('.'); + return lastDot < 0 ? fullyQualifiedName : fullyQualifiedName.Substring(lastDot + 1); + } + private async Task RemoveMethodsFromModelFactoryAsync(Project project, TypeSymbols definitions, HashSet namesToRemove) @@ -410,15 +469,54 @@ private static IEnumerable GetSymbolsByName(IEnumerable nodesToInternalize, + IReadOnlyDictionary nodesToPublicize) { - var newNode = ChangeModifier(declarationNode, SyntaxKind.PublicKeyword, SyntaxKind.InternalKeyword); - var tree = declarationNode.SyntaxTree; - var document = project.GetDocument(documentId)!; - var newRoot = tree.GetRoot().ReplaceNode(declarationNode, newNode) - .WithAdditionalAnnotations(Simplifier.Annotation); - document = document.WithSyntaxRoot(newRoot); - return document.Project; + var changesByDocument = new Dictionary>(); + AddAccessibilityChanges(changesByDocument, nodesToInternalize, SyntaxKind.InternalKeyword); + AddAccessibilityChanges(changesByDocument, nodesToPublicize, SyntaxKind.PublicKeyword); + + foreach (var (documentId, changes) in changesByDocument) + { + var document = project.GetDocument(documentId)!; + var root = changes.Keys.First().SyntaxTree.GetRoot(); + var newRoot = root.ReplaceNodes( + changes.Keys, + (originalNode, _) => changes.TryGetValue(originalNode, out var targetAccessibility) + ? ChangeAccessibility(originalNode, targetAccessibility) + : originalNode) + .WithAdditionalAnnotations(Simplifier.Annotation); + document = document.WithSyntaxRoot(newRoot); + project = document.Project; + } + + return project; + } + + private static void AddAccessibilityChanges( + Dictionary> changesByDocument, + IReadOnlyDictionary nodes, + SyntaxKind targetAccessibility) + { + foreach (var (node, documentId) in nodes) + { + if (!changesByDocument.TryGetValue(documentId, out var changes)) + { + changes = new Dictionary(); + changesByDocument[documentId] = changes; + } + + changes[node] = targetAccessibility; + } + } + + private static BaseTypeDeclarationSyntax ChangeAccessibility(BaseTypeDeclarationSyntax declarationNode, SyntaxKind targetAccessibility) + { + return targetAccessibility == SyntaxKind.PublicKeyword + ? ChangeModifier(declarationNode, SyntaxKind.InternalKeyword, SyntaxKind.PublicKeyword) + : ChangeModifier(declarationNode, SyntaxKind.PublicKeyword, SyntaxKind.InternalKeyword); } private async Task RemoveModelsAsync(Project project, @@ -576,6 +674,15 @@ arg.Expression is TypeOfExpressionSyntax typeOfExpr && model.GetTypeInfo(typeOfExpr.Type).Type?.TypeKind == TypeKind.Error) == true)) .ToHashSet(); + foreach (var attr in attributes) + { + if (IsInternalRecordBuildableAttribute(attr) || + await ShouldRemoveUnreferencedInternalBuildableAttribute(solution, model, attr)) + { + invalidAttributes.Add(attr); + } + } + if (invalidAttributes.Count > 0) { cu = cu.RemoveNodes(invalidAttributes, SyntaxRemoveOptions.KeepNoTrivia)!; @@ -631,6 +738,117 @@ arg.Expression is TypeOfExpressionSyntax typeOfExpr && return solution; } + private static bool IsInternalRecordBuildableAttribute(AttributeListSyntax attributeList) + { + if (attributeList.Attributes.Count != 1 || + !IsModelReaderWriterBuildableAttribute(attributeList.Attributes[0])) + { + return false; + } + + var typeName = attributeList.Attributes[0].ArgumentList?.Arguments + .Select(static argument => argument.Expression) + .OfType() + .Select(static typeOfExpression => typeOfExpression.Type.ToString().Split('.').Last()) + .FirstOrDefault(); + + return typeName?.StartsWith("Update", StringComparison.Ordinal) == true && typeName.EndsWith("Record", StringComparison.Ordinal) || + typeName?.EndsWith("PatchUpdate", StringComparison.Ordinal) == true; + } + + private static async Task ShouldRemoveUnreferencedInternalBuildableAttribute( + Solution solution, + SemanticModel model, + AttributeListSyntax attributeList) + { + if (attributeList.Attributes.Count != 1) + { + return false; + } + + var attribute = attributeList.Attributes[0]; + if (model.GetSymbolInfo(attribute).Symbol?.ContainingType.Name != "ModelReaderWriterBuildableAttribute" && + !IsModelReaderWriterBuildableAttribute(attribute)) + { + return false; + } + + var typeOfExpression = attribute.ArgumentList?.Arguments + .Select(static argument => argument.Expression) + .OfType() + .FirstOrDefault(); + if (typeOfExpression == null || + model.GetTypeInfo(typeOfExpression.Type).Type is not INamedTypeSymbol typeSymbol || + typeSymbol.DeclaredAccessibility != Accessibility.Internal) + { + return false; + } + + if (typeSymbol.BaseType is { SpecialType: not SpecialType.System_Object }) + { + return false; + } + + if (typeSymbol.Name.EndsWith("PatchUpdate", StringComparison.Ordinal) || + typeSymbol.Name.StartsWith("Update", StringComparison.Ordinal) && typeSymbol.Name.EndsWith("Record", StringComparison.Ordinal)) + { + return true; + } + + foreach (var referencedSymbol in await SymbolFinder.FindReferencesAsync(typeSymbol, solution)) + { + foreach (var location in referencedSymbol.Locations) + { + if (!location.Location.IsInSource) + { + continue; + } + + var document = location.Document; + var root = await document.GetSyntaxRootAsync(); + if (root == null) + { + continue; + } + + var node = root.FindNode(location.Location.SourceSpan); + if (node.AncestorsAndSelf().OfType().Any()) + { + continue; + } + + if (IsWithinTypeDeclaration(typeSymbol, node)) + { + continue; + } + + return false; + } + } + + return true; + } + + private static bool IsModelReaderWriterBuildableAttribute(AttributeSyntax attribute) + { + var name = attribute.Name.ToString(); + return name.EndsWith("ModelReaderWriterBuildable", StringComparison.Ordinal) || + name.EndsWith("ModelReaderWriterBuildableAttribute", StringComparison.Ordinal) || + name.Contains(".ModelReaderWriterBuildableAttribute", StringComparison.Ordinal); + } + + private static bool IsWithinTypeDeclaration(INamedTypeSymbol typeSymbol, SyntaxNode node) + { + foreach (var syntaxReference in typeSymbol.DeclaringSyntaxReferences) + { + if (syntaxReference.SyntaxTree == node.SyntaxTree && syntaxReference.Span.Contains(node.Span)) + { + return true; + } + } + return false; + } + private static T Measure(string stepName, Func action) { var profile = Profile; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs index 396ac8cd092..c41b5bd3120 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs @@ -5,7 +5,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; +using System.Text.RegularExpressions; using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.Statements; @@ -24,6 +26,7 @@ internal static class ProviderReferenceMapShadowAnalyzer private const string OutputDirectoryEnvironmentVariable = "TYPESPEC_PROVIDER_REFERENCE_MAP_SHADOW_DIR"; private static ProviderReferenceMapShadowResult? _latestResult; + private static readonly ConditionalWeakTable, Dictionary> _simpleNameLookupCache = new(); public static bool IsEnabled => string.Equals( Environment.GetEnvironmentVariable(EnableEnvironmentVariable), @@ -52,22 +55,65 @@ public static void Analyze(IReadOnlyList providers, Project projec var graph = BuildGraph(providers); var publicGraph = BuildGraph(providers, publicOnly: true); - var customRoots = GetCustomCodeGeneratedTypeRoots(project, graph.Nodes); + + // Generated-code dependencies come from providers. Custom code still needs Roslyn + // because arbitrary user C# can reference generated types in ways providers cannot see. + var customPublicRoots = GetCustomCodePublicGeneratedTypeRoots(project, graph.Nodes); + customPublicRoots.UnionWith(GetApiBaselineGeneratedTypeRoots(graph.Nodes)); + var customRemovalRoots = GetCustomCodeGeneratedTypeRoots(project, graph.Nodes); + var customInternalDeclarations = GetCustomCodeInternalGeneratedTypeDeclarations(project, graph.Nodes); + var generatedInternalDeclarations = GetGeneratedInternalTypeDeclarations(project, graph.Nodes); + + // Helper types are rooted after an initial reachability pass so unused infrastructure + // such as change-tracking dictionaries can still be removed when no reachable type needs them. + var generatedDiscriminatorBaseNames = GetGeneratedPersistableModelProxyTypeNames(project, publicGraph.Nodes); var internalizeReferences = CloneReferences(publicGraph.References); - AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences); - var internalizeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: false); - internalizeRoots.UnionWith(customRoots); + var internalizeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: false, includeAdditionalRoots: true, includeUnionVariantRoots: false, publicClientRootsOnly: true); + var generatedPublicReachable = GetReachableTypes(internalizeRoots, internalizeReferences); + AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences, generatedPublicReachable, generatedDiscriminatorBaseNames); + internalizeRoots.UnionWith(customPublicRoots); var internalizeReachableWithoutHelpers = GetReachableTypes(internalizeRoots, internalizeReferences); + AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences, internalizeReachableWithoutHelpers, generatedDiscriminatorBaseNames); + internalizeReachableWithoutHelpers = GetReachableTypes(internalizeRoots, internalizeReferences); + var publicizeRoots = internalizeRoots.ToHashSet(StringComparer.Ordinal); var internalizeHelperRoots = GetHelperRootNames(providers, graph.Nodes, internalizeReachableWithoutHelpers); internalizeRoots.UnionWith(internalizeHelperRoots); var internalizeReachable = GetReachableTypes(internalizeRoots, internalizeReferences); var internalizeDeclaredNodes = GetPostProcessorDeclaredNodes(providers, graph.Nodes, publicOnly: true); - var internalizeCandidates = internalizeDeclaredNodes.Except(internalizeReachable, StringComparer.Ordinal).OrderBy(static name => name, StringComparer.Ordinal).ToArray(); - + var customInternalBoundaryNodes = graph.Nodes + .Where(name => publicGraph.References.TryGetValue(name, out var references) && references.Overlaps(customInternalDeclarations)) + .ToHashSet(StringComparer.Ordinal); + var publicizeDeclaredNodes = GetPostProcessorDeclaredNodes(providers, graph.Nodes, publicOnly: false) + .Except(internalizeDeclaredNodes, StringComparer.Ordinal); + var generatedImplementationInternalDeclarations = GetGeneratedImplementationInternalTypeDeclarations(generatedInternalDeclarations).ToHashSet(StringComparer.Ordinal); + var publicApiTraversalNodes = internalizeDeclaredNodes + .Except(generatedInternalDeclarations, StringComparer.Ordinal) + .Concat(publicizeDeclaredNodes) + .Except(generatedImplementationInternalDeclarations, StringComparer.Ordinal) + .ToHashSet(StringComparer.Ordinal); + var publicizeReachable = GetReachableTypes(publicizeRoots, internalizeReferences, publicApiTraversalNodes); + var internalizeCandidates = internalizeDeclaredNodes + .Except(publicizeReachable, StringComparer.Ordinal) + .Union(internalizeDeclaredNodes.Intersect(customInternalBoundaryNodes, StringComparer.Ordinal), StringComparer.Ordinal) + .OrderBy(static name => name, StringComparer.Ordinal) + .ToArray(); + var publicizeCandidates = publicizeDeclaredNodes + .Except(customInternalDeclarations, StringComparer.Ordinal) + .Except(customInternalBoundaryNodes, StringComparer.Ordinal) + .Except(internalizeHelperRoots, StringComparer.Ordinal) + .Except(GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: true, includeAdditionalRoots: true, includeUnionVariantRoots: true, publicClientRootsOnly: true), StringComparer.Ordinal) + .Intersect(publicizeReachable, StringComparer.Ordinal) + .Where(name => publicizeRoots.Contains(name) || + HasPublicApiPredecessor(name, internalizeReferences, publicizeReachable, generatedImplementationInternalDeclarations)) + .OrderBy(static name => name, StringComparer.Ordinal) + .ToArray(); + + // Body-only generated dependencies are needed to avoid deleting helper files, but they do + // not contribute to public API reachability for internalization. AddGeneratedBodyReferences(project, providers, graph); - var removeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: true); - removeRoots.UnionWith(customRoots); + var removeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: true, includeAdditionalRoots: true, includeUnionVariantRoots: true, publicClientRootsOnly: false); + removeRoots.UnionWith(customRemovalRoots); var removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); var removeHelperRoots = GetHelperRootNames(providers, graph.Nodes, removeReachableWithoutHelpers); removeRoots.UnionWith(removeHelperRoots); @@ -80,11 +126,25 @@ public static void Analyze(IReadOnlyList providers, Project projec _latestResult = new ProviderReferenceMapShadowResult( project.Id, internalizeCandidates.ToHashSet(StringComparer.Ordinal), + publicizeCandidates.ToHashSet(StringComparer.Ordinal), removeCandidates.ToHashSet(StringComparer.Ordinal)); if (ShouldWriteReports) { - WriteReport(graph, customRoots, helperRoots, internalizeRoots, internalizeReachable, internalizeCandidates, removeRoots, removeReachable, removeCandidates); + WriteReport( + graph, + customPublicRoots, + customRemovalRoots, + helperRoots, + internalizeRoots, + internalizeReachable, + internalizeCandidates, + publicizeRoots, + publicizeReachable, + publicizeCandidates, + removeRoots, + removeReachable, + removeCandidates); } } @@ -99,7 +159,7 @@ private static HashSet GetCustomCodeGeneratedTypeRoots(Project project, foreach (var document in project.Documents) { - if (GeneratedCodeWorkspace.IsGeneratedDocument(document) || GeneratedCodeWorkspace.IsGeneratedTestDocument(document)) + if (IsGeneratedDocument(document)) { continue; } @@ -135,62 +195,283 @@ private static HashSet GetCustomCodeGeneratedTypeRoots(Project project, return roots; } - private static void AddSymbolRoot(HashSet roots, ITypeSymbol? symbol, HashSet generatedTypeNames) + private static HashSet GetCustomCodePublicGeneratedTypeRoots(Project project, HashSet generatedTypeNames) { - if (symbol is not INamedTypeSymbol namedType) + var roots = new HashSet(StringComparer.Ordinal); + var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); + if (compilation == null) { - return; + return roots; } - AddMatchingName(roots, namedType.GetFullyQualifiedName(), generatedTypeNames); - foreach (var typeArgument in namedType.TypeArguments) + foreach (var document in project.Documents) { - AddSymbolRoot(roots, typeArgument, generatedTypeNames); + if (IsGeneratedDocument(document)) + { + continue; + } + + var root = document.GetSyntaxRootAsync().GetAwaiter().GetResult(); + if (root == null) + { + continue; + } + + var semanticModel = compilation.GetSemanticModel(root.SyntaxTree); + foreach (var declaration in root.DescendantNodes().OfType()) + { + if (semanticModel.GetDeclaredSymbol(declaration) is not INamedTypeSymbol symbol || + symbol.DeclaredAccessibility != Accessibility.Public) + { + continue; + } + + AddSymbolRoot(roots, symbol, generatedTypeNames); + AddSymbolRoot(roots, symbol.BaseType, generatedTypeNames); + foreach (var interfaceType in symbol.Interfaces) + { + AddSymbolRoot(roots, interfaceType, generatedTypeNames); + } + + foreach (var member in symbol.GetMembers()) + { + if (member.DeclaredAccessibility != Accessibility.Public || + !IsDeclaredInSyntaxTree(member, declaration.SyntaxTree, declaration.Span)) + { + continue; + } + + switch (member) + { + case IMethodSymbol method: + AddSymbolRoot(roots, method.ReturnType, generatedTypeNames); + foreach (var parameter in method.Parameters) + { + AddSymbolRoot(roots, parameter.Type, generatedTypeNames); + } + break; + case IPropertySymbol property: + AddSymbolRoot(roots, property.Type, generatedTypeNames); + break; + case IFieldSymbol field: + AddSymbolRoot(roots, field.Type, generatedTypeNames); + break; + case IEventSymbol eventSymbol: + AddSymbolRoot(roots, eventSymbol.Type, generatedTypeNames); + break; + } + } + } } + + return roots; } - public static void WriteComparisonReport(string passName, IEnumerable roslynCandidates, IEnumerable providerCandidates) + private static HashSet GetApiBaselineGeneratedTypeRoots(HashSet generatedTypeNames) { - if (!IsEnabled || !ShouldWriteReports) + var roots = new HashSet(StringComparer.Ordinal); + var projectDirectory = CodeModelGenerator.Instance.Configuration.ProjectDirectory; + if (string.IsNullOrEmpty(projectDirectory)) { - return; + return roots; } - var roslynSet = roslynCandidates.ToHashSet(StringComparer.Ordinal); - var providerSet = providerCandidates.ToHashSet(StringComparer.Ordinal); - var missingFromProvider = roslynSet.Except(providerSet, StringComparer.Ordinal).OrderBy(static name => name, StringComparer.Ordinal).ToArray(); - var extraInProvider = providerSet.Except(roslynSet, StringComparer.Ordinal).OrderBy(static name => name, StringComparer.Ordinal).ToArray(); + var apiDirectory = Path.GetFullPath(Path.Combine(projectDirectory, "..", "api")); + if (!Directory.Exists(apiDirectory)) + { + return roots; + } - var directory = GetOutputDirectory(); - Directory.CreateDirectory(directory); - var path = Path.Combine(directory, $"provider-reference-map-shadow-comparison-{passName}-{DateTime.UtcNow:yyyyMMddHHmmssfff}.txt"); - var builder = new StringBuilder(); - builder.AppendLine($"Provider reference map shadow comparison: {passName}"); - builder.AppendLine($"Roslyn candidates: {roslynSet.Count}"); - builder.AppendLine($"Provider candidates: {providerSet.Count}"); - builder.AppendLine($"Missing from provider: {missingFromProvider.Length}"); - builder.AppendLine($"Extra in provider: {extraInProvider.Length}"); - builder.AppendLine(); - builder.AppendLine("Missing from provider:"); - foreach (var item in missingFromProvider) + var apiText = string.Join("\n", Directory.GetFiles(apiDirectory, "*.cs", SearchOption.AllDirectories).Select(File.ReadAllText)); + foreach (var fullName in generatedTypeNames) { - builder.AppendLine($" {item}"); + var simpleName = StripGenericArity(GetSimpleName(fullName)); + var normalizedFullName = StripGenericArity(fullName); + if (!ContainsApiTypeReference(apiText, normalizedFullName, simpleName)) + { + continue; + } + + roots.Add(fullName); } - builder.AppendLine(); - builder.AppendLine("Extra in provider:"); - foreach (var item in extraInProvider) + return roots; + } + + private static bool ContainsApiTypeReference(string apiText, string fullName, string simpleName) + { + var fullNamePattern = $@"(? GetCustomCodeInternalGeneratedTypeDeclarations(Project project, HashSet generatedTypeNames) + { + var declarations = new HashSet(StringComparer.Ordinal); + var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); + if (compilation == null) + { + return declarations; + } + + foreach (var document in project.Documents) + { + if (IsGeneratedDocument(document)) + { + continue; + } + + var root = document.GetSyntaxRootAsync().GetAwaiter().GetResult(); + if (root == null) + { + continue; + } + + var semanticModel = compilation.GetSemanticModel(root.SyntaxTree); + foreach (var declaration in root.DescendantNodes().OfType()) + { + if (semanticModel.GetDeclaredSymbol(declaration) is not INamedTypeSymbol symbol || + symbol.DeclaredAccessibility != Accessibility.Internal) + { + continue; + } + + AddMatchingName(declarations, symbol.GetFullyQualifiedName(), generatedTypeNames); + } + } + + return declarations; + } + + private static HashSet GetGeneratedPersistableModelProxyTypeNames(Project project, HashSet generatedTypeNames) + { + var proxyTypes = new HashSet(StringComparer.Ordinal); + var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); + if (compilation == null) + { + return proxyTypes; + } + + foreach (var document in project.Documents) + { + if (!IsGeneratedDocument(document)) + { + continue; + } + + var root = document.GetSyntaxRootAsync().GetAwaiter().GetResult(); + if (root == null) + { + continue; + } + + var semanticModel = compilation.GetSemanticModel(root.SyntaxTree); + foreach (var declaration in root.DescendantNodes().OfType()) + { + if (!declaration.AttributeLists + .SelectMany(static list => list.Attributes) + .Any(static attribute => attribute.Name.ToString().Contains("PersistableModelProxy", StringComparison.Ordinal))) + { + continue; + } + + if (semanticModel.GetDeclaredSymbol(declaration) is INamedTypeSymbol symbol) + { + AddMatchingName(proxyTypes, symbol.GetFullyQualifiedName(), generatedTypeNames); + } + } + } + + return proxyTypes; + } + + private static HashSet GetGeneratedInternalTypeDeclarations(Project project, HashSet generatedTypeNames) + { + var declarations = new HashSet(StringComparer.Ordinal); + var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); + if (compilation == null) + { + return declarations; + } + + foreach (var document in project.Documents) + { + if (!IsGeneratedDocument(document)) + { + continue; + } + + var root = document.GetSyntaxRootAsync().GetAwaiter().GetResult(); + if (root == null) + { + continue; + } + + var semanticModel = compilation.GetSemanticModel(root.SyntaxTree); + foreach (var declaration in root.DescendantNodes().OfType()) + { + if (!declaration.Modifiers.Any(SyntaxKind.InternalKeyword)) + { + continue; + } + + if (semanticModel.GetDeclaredSymbol(declaration) is INamedTypeSymbol symbol) + { + AddMatchingName(declarations, symbol.GetFullyQualifiedName(), generatedTypeNames); + } + } + } + + return declarations; + } + + private static IEnumerable GetGeneratedImplementationInternalTypeDeclarations(HashSet generatedInternalDeclarations) => + generatedInternalDeclarations.Where(static name => GetSimpleName(name).StartsWith("Internal", StringComparison.Ordinal)); + + private static void AddSymbolRoot(HashSet roots, ITypeSymbol? symbol, HashSet generatedTypeNames) + { + if (symbol is not INamedTypeSymbol namedType) + { + return; + } + + AddMatchingName(roots, namedType.GetFullyQualifiedName(), generatedTypeNames); + foreach (var typeArgument in namedType.TypeArguments) + { + AddSymbolRoot(roots, typeArgument, generatedTypeNames); + } } private static ProviderReferenceGraph BuildGraph(IReadOnlyList providers, bool publicOnly = false) { var generatedProviders = GetGeneratedProviders(providers); + var serializationProviderNamesByType = providers + .Where(static provider => provider.SerializationProviders.Count > 0) + .ToDictionary( + static provider => GetProviderTypeName(provider.Type), + static provider => provider.SerializationProviders + .Select(static serializationProvider => GetProviderTypeName(serializationProvider.Type)) + .ToArray(), + StringComparer.Ordinal); + IReadOnlyDictionary? serializationReferenceNamesByType = publicOnly ? null : serializationProviderNamesByType; var nodes = generatedProviders .Select(static provider => GetProviderTypeName(provider.Type)) .ToHashSet(StringComparer.Ordinal); @@ -199,15 +480,18 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro foreach (var provider in generatedProviders) { var current = GetProviderTypeName(provider.Type); - AddTypeReference(references[current], provider.Type, nodes); - AddTypeReference(references[current], provider.BaseType, nodes); - AddTypeReference(references[current], provider.DeclaringTypeProvider?.Type, nodes); + AddTypeReference(references[current], provider.Type, nodes, serializationReferenceNamesByType); + AddTypeReference(references[current], provider.BaseType, nodes, serializationReferenceNamesByType); + AddTypeReference(references[current], provider.DeclaringTypeProvider?.Type, nodes, serializationReferenceNamesByType); if (IsKept(provider.Type, CodeModelGenerator.Instance.NonRootTypes, nodes)) { continue; } + // Model factory signatures mention many models. The existing Roslyn post-processor + // removes factory methods for unreachable models, so model factory should only + // contribute helper dependencies, not model reachability edges. if (IsModelFactoryProvider(provider)) { continue; @@ -215,17 +499,23 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro foreach (var implementedType in provider.Implements) { - AddTypeReference(references[current], implementedType, nodes); + AddTypeReference(references[current], implementedType, nodes, serializationReferenceNamesByType); } - foreach (var nestedType in provider.NestedTypes) + if (!publicOnly) { - AddTypeReference(references[current], nestedType.Type, nodes); + foreach (var nestedType in provider.NestedTypes) + { + AddTypeReference(references[current], nestedType.Type, nodes, serializationReferenceNamesByType); + } } - foreach (var serializationProvider in provider.SerializationProviders) + if (!publicOnly) { - AddTypeReference(references[current], serializationProvider.Type, nodes); + foreach (var serializationProvider in provider.SerializationProviders) + { + AddTypeReference(references[current], serializationProvider.Type, nodes, serializationReferenceNamesByType); + } } foreach (var property in provider.Properties) @@ -235,9 +525,12 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro continue; } - AddTypeReference(references[current], property.Type, nodes); - AddTypeReference(references[current], property.ExplicitInterface, nodes); - AddAttributes(references[current], property.Attributes, nodes); + AddTypeReference(references[current], property.Type, nodes, serializationReferenceNamesByType); + AddTypeReference(references[current], property.ExplicitInterface, nodes, serializationReferenceNamesByType); + if (!publicOnly) + { + AddAttributes(references[current], property.Attributes, nodes, serializationReferenceNamesByType); + } } foreach (var field in provider.Fields) @@ -247,8 +540,11 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro continue; } - AddTypeReference(references[current], field.Type, nodes); - AddAttributes(references[current], field.Attributes, nodes); + AddTypeReference(references[current], field.Type, nodes, serializationReferenceNamesByType); + if (!publicOnly) + { + AddAttributes(references[current], field.Attributes, nodes, serializationReferenceNamesByType); + } } foreach (var constructor in provider.Constructors) @@ -258,7 +554,7 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro continue; } - AddSignatureReferences(references[current], constructor.Signature, nodes); + AddSignatureReferences(references[current], constructor.Signature, nodes, serializationReferenceNamesByType, includeAttributes: !publicOnly); } foreach (var method in provider.Methods) @@ -268,8 +564,11 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro continue; } - AddSignatureReferences(references[current], method.Signature, nodes); - AddTypeReference(references[current], GetCollectionDefinitionType(method), nodes); + AddSignatureReferences(references[current], method.Signature, nodes, serializationReferenceNamesByType, includeAttributes: !publicOnly); + if (!publicOnly) + { + AddTypeReference(references[current], GetCollectionDefinitionType(method), nodes, serializationReferenceNamesByType); + } } } @@ -297,19 +596,87 @@ private static Dictionary> CloneReferences(IReadOnlyDict private static void AddDerivedModelReferences( IReadOnlyList providers, HashSet nodes, - Dictionary> references) + Dictionary> references, + HashSet publicBaseModels, + HashSet generatedDiscriminatorBaseNames) { - foreach (var provider in providers.OfType()) + var modelProviders = providers.OfType().ToArray(); + var publicModelProviders = modelProviders + .Where(static provider => provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) + .ToArray(); + var discriminatorProviders = publicModelProviders + .Where(static provider => provider.DiscriminatorProperty != null || provider.DiscriminatorValue != null) + .Where(static provider => !provider.IsUnknownDiscriminatorModel) + .ToArray(); + var discriminatorBaseNames = publicModelProviders + .Where(static provider => provider.DiscriminatorProperty != null) + .Select(static provider => GetProviderTypeName(provider.Type)) + .ToHashSet(StringComparer.Ordinal); + discriminatorBaseNames.UnionWith(generatedDiscriminatorBaseNames); + var addedReference = true; + while (addedReference) { - var providerName = GetProviderTypeName(provider.Type); - if (!nodes.Contains(providerName)) + addedReference = false; + foreach (var provider in discriminatorProviders) { - continue; + var providerName = GetProviderTypeName(provider.Type); + if (!nodes.Contains(providerName)) + { + continue; + } + + if (!publicBaseModels.Contains(providerName)) + { + continue; + } + + foreach (var derivedModel in provider.DerivedModels) + { + if (derivedModel.IsUnknownDiscriminatorModel || + !derivedModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) + { + continue; + } + + var before = references[providerName].Count; + AddTypeReference(references[providerName], derivedModel.Type, nodes); + var derivedName = GetProviderTypeName(derivedModel.Type); + if (nodes.Contains(derivedName) && publicBaseModels.Add(derivedName) || references[providerName].Count != before) + { + addedReference = true; + } + } } - foreach (var derivedModel in provider.DerivedModels) + foreach (var provider in modelProviders) { - AddTypeReference(references[providerName], derivedModel.Type, nodes); + if (provider.IsUnknownDiscriminatorModel || + !provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) + { + continue; + } + + var providerName = GetProviderTypeName(provider.Type); + if (!nodes.Contains(providerName)) + { + continue; + } + + var baseTypeName = provider.BaseType == null ? null : GetProviderTypeName(provider.BaseType); + if (baseTypeName == null || + !discriminatorBaseNames.Contains(baseTypeName) || + !nodes.Contains(baseTypeName) || + !publicBaseModels.Contains(baseTypeName)) + { + continue; + } + + var before = references[baseTypeName].Count; + references[baseTypeName].Add(providerName); + if (publicBaseModels.Add(providerName) || references[baseTypeName].Count != before) + { + addedReference = true; + } } } } @@ -319,13 +686,26 @@ private static IReadOnlyList GetGeneratedProviders(IReadOnlyList(); foreach (var provider in providers) { - generatedProviders.Add(provider); - generatedProviders.AddRange(provider.SerializationProviders); + AddGeneratedProvider(generatedProviders, provider); } return generatedProviders; } + private static void AddGeneratedProvider(List generatedProviders, TypeProvider provider) + { + generatedProviders.Add(provider); + foreach (var nestedType in provider.NestedTypes) + { + AddGeneratedProvider(generatedProviders, nestedType); + } + + foreach (var serializationProvider in provider.SerializationProviders) + { + AddGeneratedProvider(generatedProviders, serializationProvider); + } + } + private static void AddGeneratedBodyReferences(Project project, IReadOnlyList providers, ProviderReferenceGraph graph) { var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); @@ -352,9 +732,10 @@ private static void AddGeneratedBodyReferences(Project project, IReadOnlyList 0) + if (bodyDependencyTypes.Count > 0) { continue; } @@ -422,7 +803,7 @@ private static void AddGeneratedBodyTypeReferences(Project project, Compilation foreach (var syntaxReference in ownerSymbol.DeclaringSyntaxReferences) { var document = project.GetDocument(syntaxReference.SyntaxTree); - if (document == null || !GeneratedCodeWorkspace.IsGeneratedDocument(document)) + if (document == null || !IsGeneratedDocument(document)) { continue; } @@ -431,6 +812,8 @@ private static void AddGeneratedBodyTypeReferences(Project project, Compilation var semanticModel = compilation.GetSemanticModel(syntaxReference.SyntaxTree); foreach (var typeSyntax in root.DescendantNodes().OfType()) { + // Declaration names are the owner itself. The old Roslyn map captures references, + // not a declaration making itself reachable. if (typeSyntax.Parent is BaseTypeDeclarationSyntax baseTypeDeclaration && baseTypeDeclaration.Identifier.Span == typeSyntax.Span) { continue; @@ -449,6 +832,11 @@ private static void AddBodyTypeReference(HashSet references, ITypeSymbol } AddMatchingName(references, namedType.GetFullyQualifiedName(), nodes); + if (namedType.TypeKind == TypeKind.Enum) + { + AddMatchingName(references, $"{namedType.Name}Extensions", nodes); + } + foreach (var typeArgument in namedType.TypeArguments) { AddBodyTypeReference(references, typeArgument, nodes); @@ -462,7 +850,7 @@ private static void AddGeneratedReferencesToHelper(Project project, Compilation foreach (var location in reference.Locations) { var document = location.Document; - if (!GeneratedCodeWorkspace.IsGeneratedDocument(document)) + if (!IsGeneratedDocument(document)) { continue; } @@ -495,7 +883,14 @@ private static void AddGeneratedReferencesToHelper(Project project, Compilation } } - private static HashSet GetRootNames(IReadOnlyList providers, HashSet nodes, HashSet helperRoots, bool includeModelFactory) + private static HashSet GetRootNames( + IReadOnlyList providers, + HashSet nodes, + HashSet helperRoots, + bool includeModelFactory, + bool includeAdditionalRoots, + bool includeUnionVariantRoots, + bool publicClientRootsOnly) { var generator = CodeModelGenerator.Instance; var roots = new HashSet(StringComparer.Ordinal); @@ -504,8 +899,8 @@ private static HashSet GetRootNames(IReadOnlyList provider foreach (var provider in providers) { var name = GetProviderTypeName(provider.Type); - if (IsClientProviderRoot(provider) || - IsKept(provider.Type, generator.AdditionalRootTypes, nodes) || + if (IsClientProviderRoot(provider, publicClientRootsOnly) || + includeAdditionalRoots && provider.DeclaringTypeProvider == null && IsKept(provider.Type, generator.AdditionalRootTypes, nodes) || includeModelFactory && string.Equals(name, modelFactoryName, StringComparison.Ordinal) || includeModelFactory && helperRoots.Contains(name)) { @@ -513,6 +908,13 @@ private static HashSet GetRootNames(IReadOnlyList provider } } + AddLastContractModelFactorySignatureRoots(providers, roots, nodes); + + if (!includeUnionVariantRoots) + { + return roots; + } + foreach (var root in generator.TypeFactory.UnionVariantTypesToKeep) { AddMatchingName(roots, root, nodes); @@ -526,6 +928,40 @@ private static HashSet GetRootNames(IReadOnlyList provider return roots; } + private static void AddLastContractModelFactorySignatureRoots(IReadOnlyList providers, HashSet roots, HashSet nodes) + { + foreach (var provider in providers.Where(IsModelFactoryProvider)) + { + foreach (var method in provider.LastContractView?.Methods ?? []) + { + if (!method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public) || + IsImplementationOnlyModelFactoryMethod(method)) + { + continue; + } + + AddTypeReference(roots, method.Signature.ReturnType, nodes); + foreach (var parameter in method.Signature.Parameters) + { + AddTypeReference(roots, parameter.Type, nodes); + } + } + } + } + + private static bool IsImplementationOnlyModelFactoryMethod(MethodProvider method) + { + var returnType = method.Signature.ReturnType; + if (returnType == null) + { + return true; + } + + var returnTypeName = GetSimpleName(GetProviderTypeName(returnType)); + return returnTypeName.StartsWith("Paged", StringComparison.Ordinal) || + returnTypeName.EndsWith("Request", StringComparison.Ordinal); + } + private static HashSet GetPostProcessorDeclaredNodes(IReadOnlyList providers, HashSet nodes, bool publicOnly) { var generator = CodeModelGenerator.Instance; @@ -542,10 +978,27 @@ private static HashSet GetPostProcessorDeclaredNodes(IReadOnlyList roots, HashSet nodes) => roots.Contains(type.Name) || roots.Contains(GetProviderTypeName(type)) && nodes.Contains(GetProviderTypeName(type)); - private static bool IsClientProviderRoot(TypeProvider provider) => - provider.RelativeFilePath.EndsWith("Client.cs", StringComparison.Ordinal); + private static bool IsClientProviderRoot(TypeProvider provider, bool publicOnly) => + provider.RelativeFilePath.EndsWith("Client.cs", StringComparison.Ordinal) && + (!publicOnly || !HasApiBaselineDirectory() && provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); - private static bool IsModelFactoryProvider(TypeProvider provider) => provider is ModelFactoryProvider; + private static bool HasApiBaselineDirectory() + { + var projectDirectory = CodeModelGenerator.Instance.Configuration.ProjectDirectory; + return !string.IsNullOrEmpty(projectDirectory) && + Directory.Exists(Path.GetFullPath(Path.Combine(projectDirectory, "..", "api"))); + } + + private static bool IsModelFactoryProvider(TypeProvider provider) + { + if (provider is ModelFactoryProvider) + { + return true; + } + + var relativePath = provider.RelativeFilePath.Replace('\\', '/'); + return relativePath.EndsWith("ModelFactory.cs", StringComparison.Ordinal); + } private static HashSet GetHelperRootNames(IReadOnlyList providers, HashSet nodes, HashSet reachableTypes) { @@ -582,6 +1035,7 @@ private static HashSet GetHelperRootNames(IReadOnlyList pr foreach (var method in provider.Methods) { + // Only factory methods for reachable models can instantiate collection helpers. if (isModelFactory && (method.Signature.ReturnType == null || !reachableTypes.Contains(GetProviderTypeName(method.Signature.ReturnType)))) { @@ -610,6 +1064,14 @@ private static void AddParameterValidationHelperRoot(HashSet roots, Para } } + private static void AddHelperDependencies(HashSet roots, IReadOnlyList dependencies, HashSet nodes) + { + foreach (var dependency in dependencies) + { + AddMatchingName(roots, dependency, nodes); + } + } + private static bool IsSerializationProvider(TypeProvider provider) { var relativePath = provider.RelativeFilePath.Replace('\\', '/'); @@ -617,12 +1079,17 @@ private static bool IsSerializationProvider(TypeProvider provider) relativePath.EndsWith(".Serialization.Multipart.cs", StringComparison.Ordinal); } - private static void AddHelperDependencies(HashSet roots, IReadOnlyList dependencies, HashSet nodes) + private static bool IsGeneratedDocument(Document document) { - foreach (var dependency in dependencies) + if (GeneratedCodeWorkspace.IsGeneratedDocument(document) || GeneratedCodeWorkspace.IsGeneratedTestDocument(document)) { - AddMatchingName(roots, dependency, nodes); + return true; } + + var filePath = document.FilePath?.Replace('\\', '/'); + return filePath != null && + (filePath.Contains("/Generated/", StringComparison.Ordinal) || + filePath.Contains("/GeneratedTests/", StringComparison.Ordinal)); } private static void AddInitializationHelperRoot(HashSet roots, CSharpType? type, HashSet nodes) @@ -680,16 +1147,34 @@ private static void AddMatchingName(HashSet target, string name, HashSet return; } - foreach (var node in nodes) + var simpleNameLookup = _simpleNameLookupCache.GetValue(nodes, BuildSimpleNameLookup); + if (!simpleNameLookup.TryGetValue(name, out var matches)) { - if (string.Equals(StripGenericArity(GetSimpleName(node)), name, StringComparison.Ordinal)) - { - target.Add(node); - } + return; + } + + foreach (var match in matches) + { + target.Add(match); } } + private static Dictionary BuildSimpleNameLookup(HashSet nodes) + { + return nodes + .GroupBy(static node => StripGenericArity(GetSimpleName(node)), StringComparer.Ordinal) + .ToDictionary(static group => group.Key, static group => group.ToArray(), StringComparer.Ordinal); + } + private static HashSet GetReachableTypes(HashSet roots, IReadOnlyDictionary> references) + { + return GetReachableTypes(roots, references, expandableNodes: null); + } + + private static HashSet GetReachableTypes( + HashSet roots, + IReadOnlyDictionary> references, + HashSet? expandableNodes) { var reachable = new HashSet(StringComparer.Ordinal); var queue = new Queue(roots); @@ -701,6 +1186,11 @@ private static HashSet GetReachableTypes(HashSet roots, IReadOnl continue; } + if (expandableNodes != null && !expandableNodes.Contains(current)) + { + continue; + } + if (!references.TryGetValue(current, out var children)) { continue; @@ -715,25 +1205,58 @@ private static HashSet GetReachableTypes(HashSet roots, IReadOnl return reachable; } - private static void AddSignatureReferences(HashSet references, MethodSignatureBase signature, HashSet nodes) + private static bool HasPublicApiPredecessor( + string name, + IReadOnlyDictionary> references, + HashSet publicizeReachable, + HashSet generatedImplementationInternalDeclarations) { - AddTypeReference(references, signature.ReturnType, nodes); - AddAttributes(references, signature.Attributes, nodes); + foreach (var (owner, children) in references) + { + if (!publicizeReachable.Contains(owner) || + string.Equals(owner, name, StringComparison.Ordinal) || + generatedImplementationInternalDeclarations.Contains(owner) || + !children.Contains(name)) + { + continue; + } + + return true; + } + + return false; + } + + private static void AddSignatureReferences( + HashSet references, + MethodSignatureBase signature, + HashSet nodes, + IReadOnlyDictionary? serializationProviderNamesByType, + bool includeAttributes = true) + { + AddTypeReference(references, signature.ReturnType, nodes, serializationProviderNamesByType); + if (includeAttributes) + { + AddAttributes(references, signature.Attributes, nodes, serializationProviderNamesByType); + } foreach (var parameter in signature.Parameters) { - AddTypeReference(references, parameter.Type, nodes); - AddAttributes(references, parameter.Attributes, nodes); + AddTypeReference(references, parameter.Type, nodes, serializationProviderNamesByType); + if (includeAttributes) + { + AddAttributes(references, parameter.Attributes, nodes, serializationProviderNamesByType); + } } if (signature is MethodSignature methodSignature) { - AddTypeReference(references, methodSignature.ExplicitInterface, nodes); + AddTypeReference(references, methodSignature.ExplicitInterface, nodes, serializationProviderNamesByType); if (methodSignature.GenericArguments != null) { foreach (var genericArgument in methodSignature.GenericArguments) { - AddTypeReference(references, genericArgument, nodes); + AddTypeReference(references, genericArgument, nodes, serializationProviderNamesByType); } } @@ -741,26 +1264,34 @@ private static void AddSignatureReferences(HashSet references, MethodSig { foreach (var constraint in methodSignature.GenericParameterConstraints) { - AddTypeReference(references, constraint.Type, nodes); + AddTypeReference(references, constraint.Type, nodes, serializationProviderNamesByType); } } } if (signature is ConstructorSignature constructorSignature) { - AddTypeReference(references, constructorSignature.Type, nodes); + AddTypeReference(references, constructorSignature.Type, nodes, serializationProviderNamesByType); } } - private static void AddAttributes(HashSet references, IReadOnlyList attributes, HashSet nodes) + private static void AddAttributes( + HashSet references, + IReadOnlyList attributes, + HashSet nodes, + IReadOnlyDictionary? serializationProviderNamesByType) { foreach (var attribute in attributes) { - AddTypeReference(references, attribute.Type, nodes); + AddTypeReference(references, attribute.Type, nodes, serializationProviderNamesByType); } } - private static void AddTypeReference(HashSet references, CSharpType? type, HashSet nodes) + private static void AddTypeReference( + HashSet references, + CSharpType? type, + HashSet nodes, + IReadOnlyDictionary? serializationProviderNamesByType = null) { if (type == null) { @@ -771,23 +1302,73 @@ private static void AddTypeReference(HashSet references, CSharpType? typ if (nodes.Contains(providerTypeName)) { references.Add(providerTypeName); + if (serializationProviderNamesByType != null && serializationProviderNamesByType.TryGetValue(providerTypeName, out var serializationProviderNames)) + { + foreach (var serializationProviderName in serializationProviderNames) + { + references.Add(serializationProviderName); + } + } } - AddTypeReference(references, type.BaseType, nodes); - AddTypeReference(references, type.DeclaringType, nodes); + AddTypeReference(references, type.BaseType, nodes, serializationProviderNamesByType); + AddTypeReference(references, type.DeclaringType, nodes, serializationProviderNamesByType); foreach (var argument in type.Arguments) { - AddTypeReference(references, argument, nodes); + AddTypeReference(references, argument, nodes, serializationProviderNamesByType); + } + } + + public static void WriteComparisonReport(string passName, IEnumerable roslynCandidates, IEnumerable providerCandidates) + { + if (!IsEnabled || !ShouldWriteReports) + { + return; + } + + var roslynSet = roslynCandidates.ToHashSet(StringComparer.Ordinal); + var providerSet = providerCandidates.ToHashSet(StringComparer.Ordinal); + var missingFromProvider = roslynSet.Except(providerSet, StringComparer.Ordinal).OrderBy(static name => name, StringComparer.Ordinal).ToArray(); + var extraInProvider = providerSet.Except(roslynSet, StringComparer.Ordinal).OrderBy(static name => name, StringComparer.Ordinal).ToArray(); + + var directory = GetOutputDirectory(); + Directory.CreateDirectory(directory); + var path = Path.Combine(directory, $"provider-reference-map-shadow-comparison-{passName}-{DateTime.UtcNow:yyyyMMddHHmmssfff}.txt"); + var builder = new StringBuilder(); + builder.AppendLine($"Provider reference map shadow comparison: {passName}"); + builder.AppendLine($"Roslyn candidates: {roslynSet.Count}"); + builder.AppendLine($"Provider candidates: {providerSet.Count}"); + builder.AppendLine($"Missing from provider: {missingFromProvider.Length}"); + builder.AppendLine($"Extra in provider: {extraInProvider.Length}"); + builder.AppendLine(); + builder.AppendLine("Missing from provider:"); + foreach (var item in missingFromProvider) + { + builder.AppendLine($" {item}"); + } + + builder.AppendLine(); + builder.AppendLine("Extra in provider:"); + foreach (var item in extraInProvider) + { + builder.AppendLine($" {item}"); } + + File.WriteAllText(path, builder.ToString()); + CodeModelGenerator.Instance.Emitter.Debug($"Provider reference map shadow comparison written to {path}"); } private static void WriteReport( ProviderReferenceGraph graph, - HashSet customRoots, + HashSet customPublicRoots, + HashSet customRemovalRoots, HashSet helperRoots, HashSet internalizeRoots, HashSet internalizeReachable, IReadOnlyList internalizeCandidates, + HashSet publicizeRoots, + HashSet publicizeReachable, + IReadOnlyList publicizeCandidates, HashSet removeRoots, HashSet removeReachable, IReadOnlyList removeCandidates) @@ -802,52 +1383,25 @@ private static void WriteReport( builder.AppendLine($"Internalize roots: {internalizeRoots.Count}"); builder.AppendLine($"Internalize reachable: {internalizeReachable.Count}"); builder.AppendLine($"Internalize candidates: {internalizeCandidates.Count}"); - builder.AppendLine($"Custom roots: {customRoots.Count}"); + builder.AppendLine($"Publicize roots: {publicizeRoots.Count}"); + builder.AppendLine($"Publicize reachable: {publicizeReachable.Count}"); + builder.AppendLine($"Publicize candidates: {publicizeCandidates.Count}"); + builder.AppendLine($"Custom public roots: {customPublicRoots.Count}"); + builder.AppendLine($"Custom removal roots: {customRemovalRoots.Count}"); builder.AppendLine($"Helper roots: {helperRoots.Count}"); builder.AppendLine($"Remove roots: {removeRoots.Count}"); builder.AppendLine($"Remove reachable: {removeReachable.Count}"); builder.AppendLine($"Remove candidates: {removeCandidates.Count}"); builder.AppendLine(); - builder.AppendLine("Custom roots:"); - foreach (var root in customRoots.OrderBy(static name => name, StringComparer.Ordinal)) - { - builder.AppendLine($" {root}"); - } - - builder.AppendLine(); - builder.AppendLine("Helper roots:"); - foreach (var root in helperRoots.OrderBy(static name => name, StringComparer.Ordinal)) - { - builder.AppendLine($" {root}"); - } - - builder.AppendLine(); - builder.AppendLine("Internalize roots:"); - foreach (var root in internalizeRoots.OrderBy(static name => name, StringComparer.Ordinal)) - { - builder.AppendLine($" {root}"); - } - - builder.AppendLine(); - builder.AppendLine("Internalize candidates:"); - foreach (var candidate in internalizeCandidates) - { - builder.AppendLine($" {candidate}"); - } - - builder.AppendLine(); - builder.AppendLine("Remove roots:"); - foreach (var root in removeRoots.OrderBy(static name => name, StringComparer.Ordinal)) - { - builder.AppendLine($" {root}"); - } - - builder.AppendLine(); - builder.AppendLine("Remove candidates:"); - foreach (var candidate in removeCandidates) - { - builder.AppendLine($" {candidate}"); - } + AppendItems(builder, "Custom public roots", customPublicRoots); + AppendItems(builder, "Custom removal roots", customRemovalRoots); + AppendItems(builder, "Helper roots", helperRoots); + AppendItems(builder, "Internalize roots", internalizeRoots); + AppendItems(builder, "Internalize candidates", internalizeCandidates); + AppendItems(builder, "Publicize roots", publicizeRoots); + AppendItems(builder, "Publicize candidates", publicizeCandidates); + AppendItems(builder, "Remove roots", removeRoots); + AppendItems(builder, "Remove candidates", removeCandidates); builder.AppendLine(); builder.AppendLine("References:"); @@ -864,6 +1418,24 @@ private static void WriteReport( CodeModelGenerator.Instance.Emitter.Debug($"Provider reference map shadow report written to {path}"); } + private static void AppendItems(StringBuilder builder, string title, IEnumerable items) + { + builder.AppendLine(); + builder.AppendLine($"{title}:"); + foreach (var item in items.OrderBy(static name => name, StringComparer.Ordinal)) + { + builder.AppendLine($" {item}"); + } + } + + private static string GetOutputDirectory() + { + var directory = Environment.GetEnvironmentVariable(OutputDirectoryEnvironmentVariable); + return string.IsNullOrWhiteSpace(directory) + ? Path.Combine(Path.GetTempPath(), "typespec-provider-reference-map-shadow") + : Path.GetFullPath(directory); + } + private static string GetSimpleName(string fullyQualifiedName) { var lastDot = fullyQualifiedName.LastIndexOf('.'); @@ -887,13 +1459,5 @@ private static string StripGenericArity(string name) private sealed record ProviderReferenceGraph( HashSet Nodes, Dictionary> References); - - private static string GetOutputDirectory() - { - var directory = Environment.GetEnvironmentVariable(OutputDirectoryEnvironmentVariable); - return string.IsNullOrWhiteSpace(directory) - ? Path.Combine(Path.GetTempPath(), "typespec-provider-reference-map-shadow") - : Path.GetFullPath(directory); - } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowResult.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowResult.cs index 48a4038183d..6750579e24c 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowResult.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowResult.cs @@ -9,6 +9,7 @@ namespace Microsoft.TypeSpec.Generator internal sealed record ProviderReferenceMapShadowResult( ProjectId ProjectId, HashSet InternalizeCandidates, + HashSet PublicizeCandidates, HashSet RemoveCandidates) { } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs index 9762dfbf311..a4d76740c4d 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs @@ -272,10 +272,12 @@ private IReadOnlyList ApplyCustomizationFilter(IEnumerable SerializationProviders => _serializationProviders ??= BuildSerializationProviders(); - internal IReadOnlyList HelperDependencyNames => BuildHelperDependencyNames(); + private IReadOnlyList? _helperDependencyNames; + internal IReadOnlyList HelperDependencyNames => _helperDependencyNames ??= BuildHelperDependencyNames(); protected internal virtual IReadOnlyList BuildHelperDependencyNames() => []; - internal IReadOnlyList BodyDependencyTypes => BuildBodyDependencyTypes(); + private IReadOnlyList? _bodyDependencyTypes; + internal IReadOnlyList BodyDependencyTypes => _bodyDependencyTypes ??= BuildBodyDependencyTypes(); protected internal virtual IReadOnlyList BuildBodyDependencyTypes() => []; private IReadOnlyList? _attributes; From 00ca13371483747147738e060bf73b0224d48757 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Fri, 19 Jun 2026 09:38:55 +0000 Subject: [PATCH 051/111] fix(http-client-csharp): preserve generated public baseline roots Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ybrid-reference-map-2026-06-19-08-41-23.md | 7 ---- .../ProviderReferenceMapAnalyzer.cs | 35 +++++++++++++++++++ 2 files changed, 35 insertions(+), 7 deletions(-) delete mode 100644 .chronus/changes/mtg-hybrid-reference-map-2026-06-19-08-41-23.md diff --git a/.chronus/changes/mtg-hybrid-reference-map-2026-06-19-08-41-23.md b/.chronus/changes/mtg-hybrid-reference-map-2026-06-19-08-41-23.md deleted file mode 100644 index e45ad2b2de0..00000000000 --- a/.chronus/changes/mtg-hybrid-reference-map-2026-06-19-08-41-23.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -changeKind: internal -packages: - - "@typespec/http-client-csharp" ---- - -Improve C# generator post-processing reference-map parity and performance. diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 4183c51c669..a488557c7a2 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -31,9 +31,12 @@ public static void Analyze(IReadOnlyList providers, Project projec // Generated-code dependencies come from providers. Custom code still needs Roslyn // because arbitrary user C# can reference generated types in ways providers cannot see. + var existingGeneratedPublicRoots = GetExistingGeneratedPublicTypeRoots(graph.Nodes); var customPublicRoots = GetCustomCodePublicGeneratedTypeRoots(project, graph.Nodes); customPublicRoots.UnionWith(GetApiBaselineGeneratedTypeRoots(graph.Nodes)); + customPublicRoots.UnionWith(existingGeneratedPublicRoots); var customRemovalRoots = GetCustomCodeGeneratedTypeRoots(project, graph.Nodes); + customRemovalRoots.UnionWith(existingGeneratedPublicRoots); var customInternalDeclarations = GetCustomCodeInternalGeneratedTypeDeclarations(project, graph.Nodes); var generatedInternalDeclarations = GetGeneratedInternalTypeDeclarations(project, graph.Nodes); @@ -253,6 +256,38 @@ private static HashSet GetApiBaselineGeneratedTypeRoots(HashSet return roots; } + private static HashSet GetExistingGeneratedPublicTypeRoots(HashSet generatedTypeNames) + { + var roots = new HashSet(StringComparer.Ordinal); + var generatedDirectory = CodeModelGenerator.Instance.Configuration.ProjectGeneratedDirectory; + if (string.IsNullOrEmpty(generatedDirectory) || !Directory.Exists(generatedDirectory)) + { + return roots; + } + + foreach (var file in Directory.GetFiles(generatedDirectory, "*.cs", SearchOption.AllDirectories)) + { + var text = File.ReadAllText(file); + if (!text.Contains("public", StringComparison.Ordinal)) + { + continue; + } + + var root = CSharpSyntaxTree.ParseText(text).GetRoot(); + foreach (var declaration in root.DescendantNodes().OfType()) + { + if (!declaration.Modifiers.Any(SyntaxKind.PublicKeyword)) + { + continue; + } + + AddMatchingName(roots, declaration.Identifier.ValueText, generatedTypeNames); + } + } + + return roots; + } + private static bool ContainsApiTypeReference(string apiText, string fullName, string simpleName) { var fullNamePattern = $@"(? Date: Sun, 21 Jun 2026 04:28:57 +0000 Subject: [PATCH 052/111] fix(http-client-csharp): preserve provider map regen parity Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/CSharpGen.cs | 18 ++- .../PostProcessing/GeneratedCodeWorkspace.cs | 1 + .../ProviderReferenceMapAnalyzer.cs | 125 +++++++++++++++++- 3 files changed, 137 insertions(+), 7 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index 39a4e5de227..b4718e6ad60 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs @@ -124,13 +124,29 @@ await customCodeWorkspace.GetCompilationAsync(), await generatedCodeWorkspace.PostProcessAsync(); - // Write the generated files to the output directory + var generatedFiles = new List<(string Name, string Text)>(); await foreach (var file in generatedCodeWorkspace.GetGeneratedFilesAsync()) { if (string.IsNullOrEmpty(file.Text)) { continue; } + + generatedFiles.Add((file.Name, file.Text)); + } + + var usesRequestHeaderExtensions = generatedFiles.Any(static file => + !file.Name.EndsWith("RequestHeaderExtensions.cs", StringComparison.Ordinal) && + file.Text.Contains(".SetDelimited(", StringComparison.Ordinal)); + + // Write the generated files to the output directory + foreach (var file in generatedFiles) + { + if (file.Name.EndsWith("RequestHeaderExtensions.cs", StringComparison.Ordinal) && !usesRequestHeaderExtensions) + { + continue; + } + var filename = Path.Combine(outputPath, file.Name); CodeModelGenerator.Instance.Emitter.Info($"Writing {Path.GetFullPath(filename)}"); Directory.CreateDirectory(Path.GetDirectoryName(filename)!); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index b999674e9cf..29468757400 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -151,6 +151,7 @@ private async Task ProcessDocument(Document document, MemberRemoverRew rewriter.SemanticModel = semanticModel; root = rewriter.Visit(root); } + root = root.WithAdditionalAnnotations(Simplifier.Annotation); document = document.WithSyntaxRoot(root); if (!CodeModelGenerator.Instance.Configuration.DisableRoslynReduce) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index a488557c7a2..e806b913b8c 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -32,11 +32,13 @@ public static void Analyze(IReadOnlyList providers, Project projec // Generated-code dependencies come from providers. Custom code still needs Roslyn // because arbitrary user C# can reference generated types in ways providers cannot see. var existingGeneratedPublicRoots = GetExistingGeneratedPublicTypeRoots(graph.Nodes); + var existingGeneratedPublicXmlDocRoots = GetExistingGeneratedPublicXmlDocTypeRoots(graph.Nodes); var customPublicRoots = GetCustomCodePublicGeneratedTypeRoots(project, graph.Nodes); customPublicRoots.UnionWith(GetApiBaselineGeneratedTypeRoots(graph.Nodes)); customPublicRoots.UnionWith(existingGeneratedPublicRoots); var customRemovalRoots = GetCustomCodeGeneratedTypeRoots(project, graph.Nodes); customRemovalRoots.UnionWith(existingGeneratedPublicRoots); + customRemovalRoots.UnionWith(existingGeneratedPublicXmlDocRoots); var customInternalDeclarations = GetCustomCodeInternalGeneratedTypeDeclarations(project, graph.Nodes); var generatedInternalDeclarations = GetGeneratedInternalTypeDeclarations(project, graph.Nodes); @@ -90,8 +92,9 @@ public static void Analyze(IReadOnlyList providers, Project projec var removeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: true, includeAdditionalRoots: true, includeUnionVariantRoots: true, publicClientRootsOnly: false); removeRoots.UnionWith(customRemovalRoots); + RemoveUnusedRequestHeaderExtensionsRoot(removeRoots, graph.References, project); var removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); - var removeHelperRoots = GetHelperRootNames(providers, graph.Nodes, removeReachableWithoutHelpers); + var removeHelperRoots = GetHelperRootNames(providers, graph.Nodes, removeReachableWithoutHelpers, graph.References); removeRoots.UnionWith(removeHelperRoots); var removeReachable = GetReachableTypes(removeRoots, graph.References); var removeDeclaredNodes = GetPostProcessorDeclaredNodes(providers, graph.Nodes, publicOnly: false); @@ -288,6 +291,48 @@ private static HashSet GetExistingGeneratedPublicTypeRoots(HashSet GetExistingGeneratedPublicXmlDocTypeRoots(HashSet generatedTypeNames) + { + var roots = new HashSet(StringComparer.Ordinal); + var generatedDirectory = CodeModelGenerator.Instance.Configuration.ProjectGeneratedDirectory; + if (string.IsNullOrEmpty(generatedDirectory) || !Directory.Exists(generatedDirectory)) + { + return roots; + } + + foreach (var file in Directory.GetFiles(generatedDirectory, "*.cs", SearchOption.AllDirectories)) + { + var text = File.ReadAllText(file); + if (!text.Contains("public", StringComparison.Ordinal) || !text.Contains("cref", StringComparison.Ordinal)) + { + continue; + } + + var root = CSharpSyntaxTree.ParseText(text).GetRoot(); + foreach (var declaration in root.DescendantNodes().OfType()) + { + AddXmlDocCrefRoots(roots, declaration.GetLeadingTrivia(), generatedTypeNames); + foreach (var member in declaration.DescendantNodes().OfType()) + { + AddXmlDocCrefRoots(roots, member.GetLeadingTrivia(), generatedTypeNames); + } + } + } + + return roots; + } + + private static void AddXmlDocCrefRoots(HashSet roots, SyntaxTriviaList trivia, HashSet generatedTypeNames) + { + foreach (var structuredTrivia in trivia.Select(static item => item.GetStructure()).OfType()) + { + foreach (var cref in structuredTrivia.DescendantNodes().OfType()) + { + AddMatchingName(roots, cref.Cref.ToString(), generatedTypeNames); + } + } + } + private static bool ContainsApiTypeReference(string apiText, string fullName, string simpleName) { var fullNamePattern = $@"(? roots, ITypeSymbol? symbol, Ha return; } - AddMatchingName(roots, namedType.GetFullyQualifiedName(), generatedTypeNames); + AddMatchingSymbolName(roots, namedType, generatedTypeNames); foreach (var typeArgument in namedType.TypeArguments) { AddSymbolRoot(roots, typeArgument, generatedTypeNames); } } + private static void AddMatchingSymbolName(HashSet target, INamedTypeSymbol symbol, HashSet generatedTypeNames) + { + AddMatchingName(target, symbol.Name, generatedTypeNames); + + try + { + AddMatchingName(target, symbol.GetFullyQualifiedName(), generatedTypeNames); + } + catch (ArgumentOutOfRangeException) + { + // Some custom-code symbols cannot be represented by the legacy fully-qualified-name + // helper. A simple-name match is enough to discover generated roots. + } + } + private static ProviderReferenceGraph BuildGraph(IReadOnlyList providers, bool publicOnly = false) { var generatedProviders = GetGeneratedProviders(providers); @@ -990,7 +1050,11 @@ private static bool IsModelFactoryProvider(TypeProvider provider) return relativePath.EndsWith("ModelFactory.cs", StringComparison.Ordinal); } - private static HashSet GetHelperRootNames(IReadOnlyList providers, HashSet nodes, HashSet reachableTypes) + private static HashSet GetHelperRootNames( + IReadOnlyList providers, + HashSet nodes, + HashSet reachableTypes, + IReadOnlyDictionary>? references = null) { var roots = new HashSet(StringComparer.Ordinal); foreach (var provider in GetGeneratedProviders(providers)) @@ -1002,7 +1066,7 @@ private static HashSet GetHelperRootNames(IReadOnlyList pr continue; } - AddHelperDependencies(roots, provider.HelperDependencyNames, nodes); + AddHelperDependencies(roots, provider.HelperDependencyNames, nodes, references == null ? null : references[providerName]); foreach (var property in provider.Properties) { @@ -1054,12 +1118,61 @@ private static void AddParameterValidationHelperRoot(HashSet roots, Para } } - private static void AddHelperDependencies(HashSet roots, IReadOnlyList dependencies, HashSet nodes) + private static void AddHelperDependencies( + HashSet roots, + IReadOnlyList dependencies, + HashSet nodes, + HashSet? referencedNames) { foreach (var dependency in dependencies) { - AddMatchingName(roots, dependency, nodes); + if (referencedNames == null) + { + AddMatchingName(roots, dependency, nodes); + continue; + } + + var matches = new HashSet(StringComparer.Ordinal); + AddMatchingName(matches, dependency, nodes); + roots.UnionWith(matches.Intersect(referencedNames, StringComparer.Ordinal)); + } + } + + private static void RemoveUnusedRequestHeaderExtensionsRoot( + HashSet roots, + IReadOnlyDictionary> references, + Project project) + { + var hasCustomReference = HasCustomRequestHeaderExtensionsReference(project); + var unusedRequestHeaderExtensions = roots + .Where(static root => root.EndsWith(".RequestHeaderExtensions", StringComparison.Ordinal)) + .Where(_ => !hasCustomReference) + .Where(root => !references.Any(reference => + !string.Equals(reference.Key, root, StringComparison.Ordinal) && + reference.Value.Contains(root))) + .ToArray(); + + roots.ExceptWith(unusedRequestHeaderExtensions); + } + + private static bool HasCustomRequestHeaderExtensionsReference(Project project) + { + foreach (var document in project.Documents) + { + if (IsGeneratedDocument(document)) + { + continue; + } + + var text = document.GetTextAsync().GetAwaiter().GetResult().ToString(); + if (text.Contains("RequestHeaderExtensions", StringComparison.Ordinal) || + text.Contains("SetDelimited", StringComparison.Ordinal)) + { + return true; + } } + + return false; } private static bool IsSerializationProvider(TypeProvider provider) From 620063da94c201b9a7a808b8af533e287dec18f0 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Sun, 21 Jun 2026 05:03:37 +0000 Subject: [PATCH 053/111] fix(http-client-csharp): preserve dictionary helper body references Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ProviderReferenceMapAnalyzer.cs | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index e806b913b8c..c944b494e77 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -869,11 +869,35 @@ private static void AddGeneratedBodyTypeReferences(Project project, Compilation continue; } - AddBodyTypeReference(graph.References[ownerName], semanticModel.GetTypeInfo(typeSyntax).Type, graph.Nodes); + var typeInfo = semanticModel.GetTypeInfo(typeSyntax).Type; + AddBodyTypeReference(graph.References[ownerName], typeInfo, graph.Nodes); + AddUnresolvedBodyTypeSyntaxReference(graph.References[ownerName], typeSyntax, typeInfo, graph.Nodes); } } } + private static void AddUnresolvedBodyTypeSyntaxReference(HashSet references, TypeSyntax typeSyntax, ITypeSymbol? symbol, HashSet nodes) + { + if (symbol is INamedTypeSymbol { TypeKind: not TypeKind.Error }) + { + return; + } + + var simpleName = typeSyntax switch + { + QualifiedNameSyntax qualifiedName => qualifiedName.Right.Identifier.ValueText, + AliasQualifiedNameSyntax aliasQualifiedName => aliasQualifiedName.Name.Identifier.ValueText, + GenericNameSyntax genericName => genericName.Identifier.ValueText, + IdentifierNameSyntax identifierName => identifierName.Identifier.ValueText, + _ => null + }; + + if (simpleName != null) + { + AddMatchingName(references, simpleName, nodes); + } + } + private static void AddBodyTypeReference(HashSet references, ITypeSymbol? symbol, HashSet nodes) { if (symbol is not INamedTypeSymbol namedType || namedType.TypeKind == TypeKind.Error) @@ -1213,6 +1237,11 @@ private static void AddInitializationHelperRoot(HashSet roots, CSharpTyp AddMatchingName(roots, "ChangeTrackingList", nodes); } + if (type.IsDictionary) + { + AddMatchingName(roots, "ChangeTrackingDictionary", nodes); + } + foreach (var argument in type.Arguments) { AddInitializationHelperRoot(roots, argument, nodes); From ce1fef15b01ccaacaae56e787799aab81a282a56 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Mon, 22 Jun 2026 01:58:26 +0000 Subject: [PATCH 054/111] fix(http-client-csharp): preserve rest client helper references Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/PostProcessing/ProviderReferenceMapAnalyzer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index c944b494e77..7089fae967a 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -845,6 +845,7 @@ private static bool IsGeneratedBodyReferenceCandidate(TypeProvider provider) var relativePath = provider.RelativeFilePath.Replace('\\', '/'); return IsSerializationProvider(provider) || relativePath.EndsWith("/Internal/ClientUriBuilder.cs", StringComparison.Ordinal) || + provider.HelperDependencyNames.Count > 0 || provider.BodyDependencyTypes.Count > 0; } @@ -905,6 +906,7 @@ private static void AddBodyTypeReference(HashSet references, ITypeSymbol return; } + AddMatchingName(references, namedType.Name, nodes); AddMatchingName(references, namedType.GetFullyQualifiedName(), nodes); if (namedType.TypeKind == TypeKind.Enum) { From 281af2a2a02218717539cb502261388c6490efa4 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Mon, 22 Jun 2026 02:33:38 +0000 Subject: [PATCH 055/111] fix(http-client-csharp): limit helper reference fallback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../PostProcessing/ProviderReferenceMapAnalyzer.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 7089fae967a..b74ead27da4 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -849,6 +849,10 @@ private static bool IsGeneratedBodyReferenceCandidate(TypeProvider provider) provider.BodyDependencyTypes.Count > 0; } + private static bool IsGeneratedHelperSimpleName(string name) => + string.Equals(name, "ChangeTrackingDictionary", StringComparison.Ordinal) || + string.Equals(name, "ChangeTrackingList", StringComparison.Ordinal); + private static void AddGeneratedBodyTypeReferences(Project project, Compilation compilation, ProviderReferenceGraph graph, string ownerName, INamedTypeSymbol ownerSymbol) { foreach (var syntaxReference in ownerSymbol.DeclaringSyntaxReferences) @@ -893,7 +897,7 @@ private static void AddUnresolvedBodyTypeSyntaxReference(HashSet referen _ => null }; - if (simpleName != null) + if (simpleName != null && IsGeneratedHelperSimpleName(simpleName)) { AddMatchingName(references, simpleName, nodes); } @@ -906,7 +910,10 @@ private static void AddBodyTypeReference(HashSet references, ITypeSymbol return; } - AddMatchingName(references, namedType.Name, nodes); + if (IsGeneratedHelperSimpleName(namedType.Name)) + { + AddMatchingName(references, namedType.Name, nodes); + } AddMatchingName(references, namedType.GetFullyQualifiedName(), nodes); if (namedType.TypeKind == TypeKind.Enum) { From 12338c423a8cf251d4e7773429b926776de9533d Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Mon, 22 Jun 2026 06:33:56 +0000 Subject: [PATCH 056/111] fix(http-client-csharp): preserve generated internal accessibility Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/PostProcessing/ProviderReferenceMapAnalyzer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index b74ead27da4..6b982e7ad4f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -81,6 +81,8 @@ public static void Analyze(IReadOnlyList providers, Project projec .Except(internalizeHelperRoots, StringComparer.Ordinal) .Except(GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: true, includeAdditionalRoots: true, includeUnionVariantRoots: true, publicClientRootsOnly: true), StringComparer.Ordinal) .Intersect(publicizeReachable, StringComparer.Ordinal) + // Preserve generated types that the last contract already made internal unless a public API/custom root explicitly requires them. + .Where(name => !generatedInternalDeclarations.Contains(name) || publicizeRoots.Contains(name)) .Where(name => publicizeRoots.Contains(name) || HasPublicApiPredecessor(name, internalizeReferences, publicizeReachable, generatedImplementationInternalDeclarations)) .OrderBy(static name => name, StringComparer.Ordinal) From 9bbb09cab99ef99653f7a55f2db5fcef7c7f0aa2 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Mon, 22 Jun 2026 08:23:48 +0000 Subject: [PATCH 057/111] fix(http-client-csharp): preserve last contract roots Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ProviderReferenceMapAnalyzer.cs | 37 +++---------------- 1 file changed, 6 insertions(+), 31 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 6b982e7ad4f..bfa2c2f174f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -40,7 +40,7 @@ public static void Analyze(IReadOnlyList providers, Project projec customRemovalRoots.UnionWith(existingGeneratedPublicRoots); customRemovalRoots.UnionWith(existingGeneratedPublicXmlDocRoots); var customInternalDeclarations = GetCustomCodeInternalGeneratedTypeDeclarations(project, graph.Nodes); - var generatedInternalDeclarations = GetGeneratedInternalTypeDeclarations(project, graph.Nodes); + var generatedInternalDeclarations = GetGeneratedInternalTypeDeclarations(providers, graph.Nodes); // Helper types are rooted after an initial reachability pass so unused infrastructure // such as change-tracking dictionaries can still be removed when no reachable type needs them. @@ -440,41 +440,17 @@ private static HashSet GetGeneratedPersistableModelProxyTypeNames(Projec return proxyTypes; } - private static HashSet GetGeneratedInternalTypeDeclarations(Project project, HashSet generatedTypeNames) + private static HashSet GetGeneratedInternalTypeDeclarations(IReadOnlyList providers, HashSet generatedTypeNames) { var declarations = new HashSet(StringComparer.Ordinal); - var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); - if (compilation == null) - { - return declarations; - } - - foreach (var document in project.Documents) + foreach (var provider in GetGeneratedProviders(providers)) { - if (!IsGeneratedDocument(document)) + if (provider.LastContractView?.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal) != true) { continue; } - var root = document.GetSyntaxRootAsync().GetAwaiter().GetResult(); - if (root == null) - { - continue; - } - - var semanticModel = compilation.GetSemanticModel(root.SyntaxTree); - foreach (var declaration in root.DescendantNodes().OfType()) - { - if (!declaration.Modifiers.Any(SyntaxKind.InternalKeyword)) - { - continue; - } - - if (semanticModel.GetDeclaredSymbol(declaration) is INamedTypeSymbol symbol) - { - AddMatchingName(declarations, symbol.GetFullyQualifiedName(), generatedTypeNames); - } - } + AddMatchingName(declarations, GetProviderTypeName(provider.Type), generatedTypeNames); } return declarations; @@ -499,8 +475,6 @@ private static void AddSymbolRoot(HashSet roots, ITypeSymbol? symbol, Ha private static void AddMatchingSymbolName(HashSet target, INamedTypeSymbol symbol, HashSet generatedTypeNames) { - AddMatchingName(target, symbol.Name, generatedTypeNames); - try { AddMatchingName(target, symbol.GetFullyQualifiedName(), generatedTypeNames); @@ -509,6 +483,7 @@ private static void AddMatchingSymbolName(HashSet target, INamedTypeSymb { // Some custom-code symbols cannot be represented by the legacy fully-qualified-name // helper. A simple-name match is enough to discover generated roots. + AddMatchingName(target, symbol.Name, generatedTypeNames); } } From 45d9a2729b566888df026cf94a99ab57c88c8da2 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Tue, 23 Jun 2026 07:14:12 +0000 Subject: [PATCH 058/111] perf(http-client-csharp): update hybrid reference map benchmark Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...eduction-experiment-2026-06-23-07-14-07.md | 7 + ...ultipartFormDataSerializationDefinition.cs | 6 + .../ProviderReferenceMapShadowAnalyzer.cs | 236 ++++++++++++++---- 3 files changed, 201 insertions(+), 48 deletions(-) create mode 100644 .chronus/changes/mtg-manual-name-reduction-experiment-2026-06-23-07-14-07.md diff --git a/.chronus/changes/mtg-manual-name-reduction-experiment-2026-06-23-07-14-07.md b/.chronus/changes/mtg-manual-name-reduction-experiment-2026-06-23-07-14-07.md new file mode 100644 index 00000000000..f975121ea3e --- /dev/null +++ b/.chronus/changes/mtg-manual-name-reduction-experiment-2026-06-23-07-14-07.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@typespec/http-client-csharp" +--- + +Tune hybrid provider reference-map benchmark parity and performance. diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MultipartFormDataSerializationDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MultipartFormDataSerializationDefinition.cs index 88cb97b16e7..89213ff6c67 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MultipartFormDataSerializationDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MultipartFormDataSerializationDefinition.cs @@ -54,6 +54,12 @@ protected override string BuildRelativeFilePath() protected override SuppressionStatement[] BuildDisabledFileWarnings() => [new SuppressionStatement(null, Literal(ScmModelProvider.FileBinaryContentDiagnosticId), ScmModelProvider.ScmEvaluationTypeSuppressionJustification)]; + protected override IReadOnlyList BuildHelperDependencyNames() => _model.Properties.Any( + prop => prop.WireInfo != null && !prop.WireInfo.IsRequired && + (prop.Type is { IsCollection: true, IsReadOnlyMemory: false } || prop.Type.IsDictionary)) + ? ["Optional"] + : []; + protected override ConstructorProvider[] BuildConstructors() { if (_hasJsonOrXmlUsage) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs index c41b5bd3120..6c955066204 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs @@ -53,16 +53,21 @@ public static void Analyze(IReadOnlyList providers, Project projec return; } - var graph = BuildGraph(providers); - var publicGraph = BuildGraph(providers, publicOnly: true); + var generatedProviders = GetGeneratedProviders(providers); + var graph = BuildGraph(generatedProviders); + var publicGraph = BuildGraph(generatedProviders, publicOnly: true); // Generated-code dependencies come from providers. Custom code still needs Roslyn // because arbitrary user C# can reference generated types in ways providers cannot see. + var (existingGeneratedPublicRoots, existingGeneratedPublicXmlDocRoots) = GetExistingGeneratedTypeRoots(graph.Nodes); var customPublicRoots = GetCustomCodePublicGeneratedTypeRoots(project, graph.Nodes); customPublicRoots.UnionWith(GetApiBaselineGeneratedTypeRoots(graph.Nodes)); + customPublicRoots.UnionWith(existingGeneratedPublicRoots); var customRemovalRoots = GetCustomCodeGeneratedTypeRoots(project, graph.Nodes); + customRemovalRoots.UnionWith(existingGeneratedPublicRoots); + customRemovalRoots.UnionWith(existingGeneratedPublicXmlDocRoots); var customInternalDeclarations = GetCustomCodeInternalGeneratedTypeDeclarations(project, graph.Nodes); - var generatedInternalDeclarations = GetGeneratedInternalTypeDeclarations(project, graph.Nodes); + var generatedInternalDeclarations = GetGeneratedInternalTypeDeclarations(generatedProviders, graph.Nodes); // Helper types are rooted after an initial reachability pass so unused infrastructure // such as change-tracking dictionaries can still be removed when no reachable type needs them. @@ -76,14 +81,14 @@ public static void Analyze(IReadOnlyList providers, Project projec AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences, internalizeReachableWithoutHelpers, generatedDiscriminatorBaseNames); internalizeReachableWithoutHelpers = GetReachableTypes(internalizeRoots, internalizeReferences); var publicizeRoots = internalizeRoots.ToHashSet(StringComparer.Ordinal); - var internalizeHelperRoots = GetHelperRootNames(providers, graph.Nodes, internalizeReachableWithoutHelpers); + var internalizeHelperRoots = GetHelperRootNames(generatedProviders, graph.Nodes, internalizeReachableWithoutHelpers); internalizeRoots.UnionWith(internalizeHelperRoots); var internalizeReachable = GetReachableTypes(internalizeRoots, internalizeReferences); - var internalizeDeclaredNodes = GetPostProcessorDeclaredNodes(providers, graph.Nodes, publicOnly: true); + var internalizeDeclaredNodes = GetPostProcessorDeclaredNodes(generatedProviders, graph.Nodes, publicOnly: true); var customInternalBoundaryNodes = graph.Nodes .Where(name => publicGraph.References.TryGetValue(name, out var references) && references.Overlaps(customInternalDeclarations)) .ToHashSet(StringComparer.Ordinal); - var publicizeDeclaredNodes = GetPostProcessorDeclaredNodes(providers, graph.Nodes, publicOnly: false) + var publicizeDeclaredNodes = GetPostProcessorDeclaredNodes(generatedProviders, graph.Nodes, publicOnly: false) .Except(internalizeDeclaredNodes, StringComparer.Ordinal); var generatedImplementationInternalDeclarations = GetGeneratedImplementationInternalTypeDeclarations(generatedInternalDeclarations).ToHashSet(StringComparer.Ordinal); var publicApiTraversalNodes = internalizeDeclaredNodes @@ -103,6 +108,8 @@ public static void Analyze(IReadOnlyList providers, Project projec .Except(internalizeHelperRoots, StringComparer.Ordinal) .Except(GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: true, includeAdditionalRoots: true, includeUnionVariantRoots: true, publicClientRootsOnly: true), StringComparer.Ordinal) .Intersect(publicizeReachable, StringComparer.Ordinal) + // Preserve generated types that the last contract already made internal unless a public API/custom root explicitly requires them. + .Where(name => !generatedInternalDeclarations.Contains(name) || publicizeRoots.Contains(name)) .Where(name => publicizeRoots.Contains(name) || HasPublicApiPredecessor(name, internalizeReferences, publicizeReachable, generatedImplementationInternalDeclarations)) .OrderBy(static name => name, StringComparer.Ordinal) @@ -114,11 +121,12 @@ public static void Analyze(IReadOnlyList providers, Project projec var removeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: true, includeAdditionalRoots: true, includeUnionVariantRoots: true, publicClientRootsOnly: false); removeRoots.UnionWith(customRemovalRoots); + RemoveUnusedRequestHeaderExtensionsRoot(removeRoots, graph.References, project); var removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); - var removeHelperRoots = GetHelperRootNames(providers, graph.Nodes, removeReachableWithoutHelpers); + var removeHelperRoots = GetHelperRootNames(generatedProviders, graph.Nodes, removeReachableWithoutHelpers, graph.References); removeRoots.UnionWith(removeHelperRoots); var removeReachable = GetReachableTypes(removeRoots, graph.References); - var removeDeclaredNodes = GetPostProcessorDeclaredNodes(providers, graph.Nodes, publicOnly: false); + var removeDeclaredNodes = GetPostProcessorDeclaredNodes(generatedProviders, graph.Nodes, publicOnly: false); var removeCandidates = removeDeclaredNodes.Except(removeReachable, StringComparer.Ordinal).OrderBy(static name => name, StringComparer.Ordinal).ToArray(); var helperRoots = internalizeHelperRoots.Concat(removeHelperRoots).ToHashSet(StringComparer.Ordinal); @@ -298,6 +306,58 @@ private static HashSet GetApiBaselineGeneratedTypeRoots(HashSet return roots; } + private static (HashSet PublicRoots, HashSet PublicXmlDocRoots) GetExistingGeneratedTypeRoots(HashSet generatedTypeNames) + { + var publicRoots = new HashSet(StringComparer.Ordinal); + var publicXmlDocRoots = new HashSet(StringComparer.Ordinal); + var generatedDirectory = CodeModelGenerator.Instance.Configuration.ProjectGeneratedDirectory; + if (string.IsNullOrEmpty(generatedDirectory) || !Directory.Exists(generatedDirectory)) + { + return (publicRoots, publicXmlDocRoots); + } + + foreach (var file in Directory.GetFiles(generatedDirectory, "*.cs", SearchOption.AllDirectories)) + { + var text = File.ReadAllText(file); + if (!text.Contains("public", StringComparison.Ordinal)) + { + continue; + } + + var root = CSharpSyntaxTree.ParseText(text).GetRoot(); + var hasCref = text.Contains("cref", StringComparison.Ordinal); + foreach (var declaration in root.DescendantNodes().OfType()) + { + if (declaration.Modifiers.Any(SyntaxKind.PublicKeyword)) + { + AddMatchingName(publicRoots, declaration.Identifier.ValueText, generatedTypeNames); + } + + if (hasCref) + { + AddXmlDocCrefRoots(publicXmlDocRoots, declaration.GetLeadingTrivia(), generatedTypeNames); + foreach (var member in declaration.DescendantNodes().OfType()) + { + AddXmlDocCrefRoots(publicXmlDocRoots, member.GetLeadingTrivia(), generatedTypeNames); + } + } + } + } + + return (publicRoots, publicXmlDocRoots); + } + + private static void AddXmlDocCrefRoots(HashSet roots, SyntaxTriviaList trivia, HashSet generatedTypeNames) + { + foreach (var structuredTrivia in trivia.Select(static item => item.GetStructure()).OfType()) + { + foreach (var cref in structuredTrivia.DescendantNodes().OfType()) + { + AddMatchingName(roots, cref.Cref.ToString(), generatedTypeNames); + } + } + } + private static bool ContainsApiTypeReference(string apiText, string fullName, string simpleName) { var fullNamePattern = $@"(? GetGeneratedPersistableModelProxyTypeNames(Projec return proxyTypes; } - private static HashSet GetGeneratedInternalTypeDeclarations(Project project, HashSet generatedTypeNames) + private static HashSet GetGeneratedInternalTypeDeclarations(IReadOnlyList generatedProviders, HashSet generatedTypeNames) { var declarations = new HashSet(StringComparer.Ordinal); - var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); - if (compilation == null) - { - return declarations; - } - - foreach (var document in project.Documents) + foreach (var provider in generatedProviders) { - if (!IsGeneratedDocument(document)) + if (provider.LastContractView?.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal) != true) { continue; } - var root = document.GetSyntaxRootAsync().GetAwaiter().GetResult(); - if (root == null) - { - continue; - } - - var semanticModel = compilation.GetSemanticModel(root.SyntaxTree); - foreach (var declaration in root.DescendantNodes().OfType()) - { - if (!declaration.Modifiers.Any(SyntaxKind.InternalKeyword)) - { - continue; - } - - if (semanticModel.GetDeclaredSymbol(declaration) is INamedTypeSymbol symbol) - { - AddMatchingName(declarations, symbol.GetFullyQualifiedName(), generatedTypeNames); - } - } + AddMatchingName(declarations, GetProviderTypeName(provider.Type), generatedTypeNames); } return declarations; @@ -453,17 +489,30 @@ private static void AddSymbolRoot(HashSet roots, ITypeSymbol? symbol, Ha return; } - AddMatchingName(roots, namedType.GetFullyQualifiedName(), generatedTypeNames); + AddMatchingSymbolName(roots, namedType, generatedTypeNames); foreach (var typeArgument in namedType.TypeArguments) { AddSymbolRoot(roots, typeArgument, generatedTypeNames); } } - private static ProviderReferenceGraph BuildGraph(IReadOnlyList providers, bool publicOnly = false) + private static void AddMatchingSymbolName(HashSet target, INamedTypeSymbol symbol, HashSet generatedTypeNames) { - var generatedProviders = GetGeneratedProviders(providers); - var serializationProviderNamesByType = providers + try + { + AddMatchingName(target, symbol.GetFullyQualifiedName(), generatedTypeNames); + } + catch (ArgumentOutOfRangeException) + { + // Some custom-code symbols cannot be represented by the legacy fully-qualified-name + // helper. A simple-name match is enough to discover generated roots. + AddMatchingName(target, symbol.Name, generatedTypeNames); + } + } + + private static ProviderReferenceGraph BuildGraph(IReadOnlyList generatedProviders, bool publicOnly = false) + { + var serializationProviderNamesByType = generatedProviders .Where(static provider => provider.SerializationProviders.Count > 0) .ToDictionary( static provider => GetProviderTypeName(provider.Type), @@ -795,9 +844,14 @@ private static bool IsGeneratedBodyReferenceCandidate(TypeProvider provider) var relativePath = provider.RelativeFilePath.Replace('\\', '/'); return IsSerializationProvider(provider) || relativePath.EndsWith("/Internal/ClientUriBuilder.cs", StringComparison.Ordinal) || + provider.HelperDependencyNames.Count > 0 || provider.BodyDependencyTypes.Count > 0; } + private static bool IsGeneratedHelperSimpleName(string name) => + string.Equals(name, "ChangeTrackingDictionary", StringComparison.Ordinal) || + string.Equals(name, "ChangeTrackingList", StringComparison.Ordinal); + private static void AddGeneratedBodyTypeReferences(Project project, Compilation compilation, ProviderReferenceGraph graph, string ownerName, INamedTypeSymbol ownerSymbol) { foreach (var syntaxReference in ownerSymbol.DeclaringSyntaxReferences) @@ -819,11 +873,35 @@ private static void AddGeneratedBodyTypeReferences(Project project, Compilation continue; } - AddBodyTypeReference(graph.References[ownerName], semanticModel.GetTypeInfo(typeSyntax).Type, graph.Nodes); + var typeInfo = semanticModel.GetTypeInfo(typeSyntax).Type; + AddBodyTypeReference(graph.References[ownerName], typeInfo, graph.Nodes); + AddUnresolvedBodyTypeSyntaxReference(graph.References[ownerName], typeSyntax, typeInfo, graph.Nodes); } } } + private static void AddUnresolvedBodyTypeSyntaxReference(HashSet references, TypeSyntax typeSyntax, ITypeSymbol? symbol, HashSet nodes) + { + if (symbol is INamedTypeSymbol { TypeKind: not TypeKind.Error }) + { + return; + } + + var simpleName = typeSyntax switch + { + QualifiedNameSyntax qualifiedName => qualifiedName.Right.Identifier.ValueText, + AliasQualifiedNameSyntax aliasQualifiedName => aliasQualifiedName.Name.Identifier.ValueText, + GenericNameSyntax genericName => genericName.Identifier.ValueText, + IdentifierNameSyntax identifierName => identifierName.Identifier.ValueText, + _ => null + }; + + if (simpleName != null && IsGeneratedHelperSimpleName(simpleName)) + { + AddMatchingName(references, simpleName, nodes); + } + } + private static void AddBodyTypeReference(HashSet references, ITypeSymbol? symbol, HashSet nodes) { if (symbol is not INamedTypeSymbol namedType || namedType.TypeKind == TypeKind.Error) @@ -831,6 +909,10 @@ private static void AddBodyTypeReference(HashSet references, ITypeSymbol return; } + if (IsGeneratedHelperSimpleName(namedType.Name)) + { + AddMatchingName(references, namedType.Name, nodes); + } AddMatchingName(references, namedType.GetFullyQualifiedName(), nodes); if (namedType.TypeKind == TypeKind.Enum) { @@ -962,11 +1044,11 @@ private static bool IsImplementationOnlyModelFactoryMethod(MethodProvider method returnTypeName.EndsWith("Request", StringComparison.Ordinal); } - private static HashSet GetPostProcessorDeclaredNodes(IReadOnlyList providers, HashSet nodes, bool publicOnly) + private static HashSet GetPostProcessorDeclaredNodes(IReadOnlyList generatedProviders, HashSet nodes, bool publicOnly) { var generator = CodeModelGenerator.Instance; var excludedNames = generator.NonRootTypes; - return GetGeneratedProviders(providers) + return generatedProviders .Where(provider => !IsModelFactoryProvider(provider)) .Where(provider => !publicOnly || provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) .Select(provider => GetProviderTypeName(provider.Type)) @@ -1000,10 +1082,14 @@ private static bool IsModelFactoryProvider(TypeProvider provider) return relativePath.EndsWith("ModelFactory.cs", StringComparison.Ordinal); } - private static HashSet GetHelperRootNames(IReadOnlyList providers, HashSet nodes, HashSet reachableTypes) + private static HashSet GetHelperRootNames( + IReadOnlyList generatedProviders, + HashSet nodes, + HashSet reachableTypes, + IReadOnlyDictionary>? references = null) { var roots = new HashSet(StringComparer.Ordinal); - foreach (var provider in GetGeneratedProviders(providers)) + foreach (var provider in generatedProviders) { var providerName = GetProviderTypeName(provider.Type); var isModelFactory = IsModelFactoryProvider(provider); @@ -1012,7 +1098,7 @@ private static HashSet GetHelperRootNames(IReadOnlyList pr continue; } - AddHelperDependencies(roots, provider.HelperDependencyNames, nodes); + AddHelperDependencies(roots, provider.HelperDependencyNames, nodes, references == null ? null : references[providerName]); foreach (var property in provider.Properties) { @@ -1064,14 +1150,63 @@ private static void AddParameterValidationHelperRoot(HashSet roots, Para } } - private static void AddHelperDependencies(HashSet roots, IReadOnlyList dependencies, HashSet nodes) + private static void AddHelperDependencies( + HashSet roots, + IReadOnlyList dependencies, + HashSet nodes, + HashSet? referencedNames) { foreach (var dependency in dependencies) { - AddMatchingName(roots, dependency, nodes); + if (referencedNames == null) + { + AddMatchingName(roots, dependency, nodes); + continue; + } + + var matches = new HashSet(StringComparer.Ordinal); + AddMatchingName(matches, dependency, nodes); + roots.UnionWith(matches.Intersect(referencedNames, StringComparer.Ordinal)); } } + private static void RemoveUnusedRequestHeaderExtensionsRoot( + HashSet roots, + IReadOnlyDictionary> references, + Project project) + { + var hasCustomReference = HasCustomRequestHeaderExtensionsReference(project); + var unusedRequestHeaderExtensions = roots + .Where(static root => root.EndsWith(".RequestHeaderExtensions", StringComparison.Ordinal)) + .Where(_ => !hasCustomReference) + .Where(root => !references.Any(reference => + !string.Equals(reference.Key, root, StringComparison.Ordinal) && + reference.Value.Contains(root))) + .ToArray(); + + roots.ExceptWith(unusedRequestHeaderExtensions); + } + + private static bool HasCustomRequestHeaderExtensionsReference(Project project) + { + foreach (var document in project.Documents) + { + if (IsGeneratedDocument(document)) + { + continue; + } + + var text = document.GetTextAsync().GetAwaiter().GetResult().ToString(); + if (text.Contains("RequestHeaderExtensions", StringComparison.Ordinal) || + text.Contains("SetDelimited", StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + private static bool IsSerializationProvider(TypeProvider provider) { var relativePath = provider.RelativeFilePath.Replace('\\', '/'); @@ -1110,6 +1245,11 @@ private static void AddInitializationHelperRoot(HashSet roots, CSharpTyp AddMatchingName(roots, "ChangeTrackingList", nodes); } + if (type.IsDictionary) + { + AddMatchingName(roots, "ChangeTrackingDictionary", nodes); + } + foreach (var argument in type.Arguments) { AddInitializationHelperRoot(roots, argument, nodes); From 32840a5566f54328d5e6f2dbdfa178128a063376 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Tue, 23 Jun 2026 08:52:27 +0000 Subject: [PATCH 059/111] perf(http-client-csharp): avoid stale generated roots in benchmark Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ProviderReferenceMapShadowAnalyzer.cs | 60 +------------------ 1 file changed, 3 insertions(+), 57 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs index 6c955066204..acd7f89c639 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs @@ -59,13 +59,9 @@ public static void Analyze(IReadOnlyList providers, Project projec // Generated-code dependencies come from providers. Custom code still needs Roslyn // because arbitrary user C# can reference generated types in ways providers cannot see. - var (existingGeneratedPublicRoots, existingGeneratedPublicXmlDocRoots) = GetExistingGeneratedTypeRoots(graph.Nodes); var customPublicRoots = GetCustomCodePublicGeneratedTypeRoots(project, graph.Nodes); customPublicRoots.UnionWith(GetApiBaselineGeneratedTypeRoots(graph.Nodes)); - customPublicRoots.UnionWith(existingGeneratedPublicRoots); var customRemovalRoots = GetCustomCodeGeneratedTypeRoots(project, graph.Nodes); - customRemovalRoots.UnionWith(existingGeneratedPublicRoots); - customRemovalRoots.UnionWith(existingGeneratedPublicXmlDocRoots); var customInternalDeclarations = GetCustomCodeInternalGeneratedTypeDeclarations(project, graph.Nodes); var generatedInternalDeclarations = GetGeneratedInternalTypeDeclarations(generatedProviders, graph.Nodes); @@ -73,7 +69,7 @@ public static void Analyze(IReadOnlyList providers, Project projec // such as change-tracking dictionaries can still be removed when no reachable type needs them. var generatedDiscriminatorBaseNames = GetGeneratedPersistableModelProxyTypeNames(project, publicGraph.Nodes); var internalizeReferences = CloneReferences(publicGraph.References); - var internalizeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: false, includeAdditionalRoots: true, includeUnionVariantRoots: false, publicClientRootsOnly: true); + var internalizeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: false, includeAdditionalRoots: true, includeUnionVariantRoots: true, publicClientRootsOnly: true); var generatedPublicReachable = GetReachableTypes(internalizeRoots, internalizeReferences); AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences, generatedPublicReachable, generatedDiscriminatorBaseNames); internalizeRoots.UnionWith(customPublicRoots); @@ -123,6 +119,8 @@ public static void Analyze(IReadOnlyList providers, Project projec removeRoots.UnionWith(customRemovalRoots); RemoveUnusedRequestHeaderExtensionsRoot(removeRoots, graph.References, project); var removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); + AddDerivedModelReferences(providers, graph.Nodes, graph.References, removeReachableWithoutHelpers, generatedDiscriminatorBaseNames); + removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); var removeHelperRoots = GetHelperRootNames(generatedProviders, graph.Nodes, removeReachableWithoutHelpers, graph.References); removeRoots.UnionWith(removeHelperRoots); var removeReachable = GetReachableTypes(removeRoots, graph.References); @@ -306,58 +304,6 @@ private static HashSet GetApiBaselineGeneratedTypeRoots(HashSet return roots; } - private static (HashSet PublicRoots, HashSet PublicXmlDocRoots) GetExistingGeneratedTypeRoots(HashSet generatedTypeNames) - { - var publicRoots = new HashSet(StringComparer.Ordinal); - var publicXmlDocRoots = new HashSet(StringComparer.Ordinal); - var generatedDirectory = CodeModelGenerator.Instance.Configuration.ProjectGeneratedDirectory; - if (string.IsNullOrEmpty(generatedDirectory) || !Directory.Exists(generatedDirectory)) - { - return (publicRoots, publicXmlDocRoots); - } - - foreach (var file in Directory.GetFiles(generatedDirectory, "*.cs", SearchOption.AllDirectories)) - { - var text = File.ReadAllText(file); - if (!text.Contains("public", StringComparison.Ordinal)) - { - continue; - } - - var root = CSharpSyntaxTree.ParseText(text).GetRoot(); - var hasCref = text.Contains("cref", StringComparison.Ordinal); - foreach (var declaration in root.DescendantNodes().OfType()) - { - if (declaration.Modifiers.Any(SyntaxKind.PublicKeyword)) - { - AddMatchingName(publicRoots, declaration.Identifier.ValueText, generatedTypeNames); - } - - if (hasCref) - { - AddXmlDocCrefRoots(publicXmlDocRoots, declaration.GetLeadingTrivia(), generatedTypeNames); - foreach (var member in declaration.DescendantNodes().OfType()) - { - AddXmlDocCrefRoots(publicXmlDocRoots, member.GetLeadingTrivia(), generatedTypeNames); - } - } - } - } - - return (publicRoots, publicXmlDocRoots); - } - - private static void AddXmlDocCrefRoots(HashSet roots, SyntaxTriviaList trivia, HashSet generatedTypeNames) - { - foreach (var structuredTrivia in trivia.Select(static item => item.GetStructure()).OfType()) - { - foreach (var cref in structuredTrivia.DescendantNodes().OfType()) - { - AddMatchingName(roots, cref.Cref.ToString(), generatedTypeNames); - } - } - } - private static bool ContainsApiTypeReference(string apiText, string fullName, string simpleName) { var fullNamePattern = $@"(? Date: Tue, 23 Jun 2026 08:58:01 +0000 Subject: [PATCH 060/111] perf(http-client-csharp): use current generated roots Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ProviderReferenceMapAnalyzer.cs | 84 +------------------ 1 file changed, 3 insertions(+), 81 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index bfa2c2f174f..49bbab86289 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -11,7 +11,6 @@ using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.Statements; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.FindSymbols; @@ -31,14 +30,9 @@ public static void Analyze(IReadOnlyList providers, Project projec // Generated-code dependencies come from providers. Custom code still needs Roslyn // because arbitrary user C# can reference generated types in ways providers cannot see. - var existingGeneratedPublicRoots = GetExistingGeneratedPublicTypeRoots(graph.Nodes); - var existingGeneratedPublicXmlDocRoots = GetExistingGeneratedPublicXmlDocTypeRoots(graph.Nodes); var customPublicRoots = GetCustomCodePublicGeneratedTypeRoots(project, graph.Nodes); customPublicRoots.UnionWith(GetApiBaselineGeneratedTypeRoots(graph.Nodes)); - customPublicRoots.UnionWith(existingGeneratedPublicRoots); var customRemovalRoots = GetCustomCodeGeneratedTypeRoots(project, graph.Nodes); - customRemovalRoots.UnionWith(existingGeneratedPublicRoots); - customRemovalRoots.UnionWith(existingGeneratedPublicXmlDocRoots); var customInternalDeclarations = GetCustomCodeInternalGeneratedTypeDeclarations(project, graph.Nodes); var generatedInternalDeclarations = GetGeneratedInternalTypeDeclarations(providers, graph.Nodes); @@ -46,7 +40,7 @@ public static void Analyze(IReadOnlyList providers, Project projec // such as change-tracking dictionaries can still be removed when no reachable type needs them. var generatedDiscriminatorBaseNames = GetGeneratedPersistableModelProxyTypeNames(project, publicGraph.Nodes); var internalizeReferences = CloneReferences(publicGraph.References); - var internalizeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: false, includeAdditionalRoots: true, includeUnionVariantRoots: false, publicClientRootsOnly: true); + var internalizeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: false, includeAdditionalRoots: true, includeUnionVariantRoots: true, publicClientRootsOnly: true); var generatedPublicReachable = GetReachableTypes(internalizeRoots, internalizeReferences); AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences, generatedPublicReachable, generatedDiscriminatorBaseNames); internalizeRoots.UnionWith(customPublicRoots); @@ -96,6 +90,8 @@ public static void Analyze(IReadOnlyList providers, Project projec removeRoots.UnionWith(customRemovalRoots); RemoveUnusedRequestHeaderExtensionsRoot(removeRoots, graph.References, project); var removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); + AddDerivedModelReferences(providers, graph.Nodes, graph.References, removeReachableWithoutHelpers, generatedDiscriminatorBaseNames); + removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); var removeHelperRoots = GetHelperRootNames(providers, graph.Nodes, removeReachableWithoutHelpers, graph.References); removeRoots.UnionWith(removeHelperRoots); var removeReachable = GetReachableTypes(removeRoots, graph.References); @@ -261,80 +257,6 @@ private static HashSet GetApiBaselineGeneratedTypeRoots(HashSet return roots; } - private static HashSet GetExistingGeneratedPublicTypeRoots(HashSet generatedTypeNames) - { - var roots = new HashSet(StringComparer.Ordinal); - var generatedDirectory = CodeModelGenerator.Instance.Configuration.ProjectGeneratedDirectory; - if (string.IsNullOrEmpty(generatedDirectory) || !Directory.Exists(generatedDirectory)) - { - return roots; - } - - foreach (var file in Directory.GetFiles(generatedDirectory, "*.cs", SearchOption.AllDirectories)) - { - var text = File.ReadAllText(file); - if (!text.Contains("public", StringComparison.Ordinal)) - { - continue; - } - - var root = CSharpSyntaxTree.ParseText(text).GetRoot(); - foreach (var declaration in root.DescendantNodes().OfType()) - { - if (!declaration.Modifiers.Any(SyntaxKind.PublicKeyword)) - { - continue; - } - - AddMatchingName(roots, declaration.Identifier.ValueText, generatedTypeNames); - } - } - - return roots; - } - - private static HashSet GetExistingGeneratedPublicXmlDocTypeRoots(HashSet generatedTypeNames) - { - var roots = new HashSet(StringComparer.Ordinal); - var generatedDirectory = CodeModelGenerator.Instance.Configuration.ProjectGeneratedDirectory; - if (string.IsNullOrEmpty(generatedDirectory) || !Directory.Exists(generatedDirectory)) - { - return roots; - } - - foreach (var file in Directory.GetFiles(generatedDirectory, "*.cs", SearchOption.AllDirectories)) - { - var text = File.ReadAllText(file); - if (!text.Contains("public", StringComparison.Ordinal) || !text.Contains("cref", StringComparison.Ordinal)) - { - continue; - } - - var root = CSharpSyntaxTree.ParseText(text).GetRoot(); - foreach (var declaration in root.DescendantNodes().OfType()) - { - AddXmlDocCrefRoots(roots, declaration.GetLeadingTrivia(), generatedTypeNames); - foreach (var member in declaration.DescendantNodes().OfType()) - { - AddXmlDocCrefRoots(roots, member.GetLeadingTrivia(), generatedTypeNames); - } - } - } - - return roots; - } - - private static void AddXmlDocCrefRoots(HashSet roots, SyntaxTriviaList trivia, HashSet generatedTypeNames) - { - foreach (var structuredTrivia in trivia.Select(static item => item.GetStructure()).OfType()) - { - foreach (var cref in structuredTrivia.DescendantNodes().OfType()) - { - AddMatchingName(roots, cref.Cref.ToString(), generatedTypeNames); - } - } - } - private static bool ContainsApiTypeReference(string apiText, string fullName, string simpleName) { var fullNamePattern = $@"(? Date: Tue, 23 Jun 2026 13:23:42 +0000 Subject: [PATCH 061/111] fix(http-client-csharp): preserve hybrid reference map parity Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/PostProcessing/PostProcessor.cs | 31 ++-- .../ProviderReferenceMapAnalyzer.cs | 165 ++++++++++++++++-- 2 files changed, 174 insertions(+), 22 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs index ec0f974ebfd..5c34fad4dce 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs @@ -336,7 +336,10 @@ public async Task RemoveAsync(Project project) } // remove them one by one - project = await RemoveModelsAsync(project, nodesToRemove); + var referencedTypeNames = referencedSet + .Select(static symbol => symbol.GetFullyQualifiedName()) + .ToHashSet(StringComparer.Ordinal); + project = await RemoveModelsAsync(project, nodesToRemove, referencedTypeNames); return project; } @@ -464,8 +467,10 @@ private static BaseTypeDeclarationSyntax ChangeAccessibility(BaseTypeDeclaration : ChangeModifier(declarationNode, SyntaxKind.PublicKeyword, SyntaxKind.InternalKeyword); } - private async Task RemoveModelsAsync(Project project, - IEnumerable unusedModels) + private async Task RemoveModelsAsync( + Project project, + IEnumerable unusedModels, + HashSet referencedTypeNames) { // accumulate the definitions from the same document together var documents = new Dictionary>(); @@ -486,7 +491,7 @@ private async Task RemoveModelsAsync(Project project, } // remove what are now invalid references due to the models being removed - project = await RemoveInvalidRefs(project); + project = await RemoveInvalidRefs(project, referencedTypeNames); return project; } @@ -532,7 +537,7 @@ private async Task RemoveModelsFromDocumentAsync(Project project, return document.Project; } - private async Task RemoveInvalidRefs(Project project) + private async Task RemoveInvalidRefs(Project project, HashSet referencedTypeNames) { var solution = project.Solution; @@ -545,7 +550,7 @@ private async Task RemoveInvalidRefs(Project project) // Process each document for invalid attributes (with fresh semantic models) foreach (var documentId in project.DocumentIds) { - solution = await RemoveInvalidAttributes(solution, documentId); + solution = await RemoveInvalidAttributes(solution, documentId, referencedTypeNames); } // Process each document for invalid XML documentation references (with fresh semantic models) @@ -591,7 +596,7 @@ private async Task RemoveInvalidUsings(Solution solution, DocumentId d return solution; } - private async Task RemoveInvalidAttributes(Solution solution, DocumentId documentId) + private async Task RemoveInvalidAttributes(Solution solution, DocumentId documentId, HashSet referencedTypeNames) { var document = solution.GetDocument(documentId)!; var root = await document.GetSyntaxRootAsync(); @@ -613,7 +618,7 @@ arg.Expression is TypeOfExpressionSyntax typeOfExpr && foreach (var attr in attributes) { if (IsInternalRecordBuildableAttribute(attr) || - await ShouldRemoveUnreferencedInternalBuildableAttribute(solution, model, attr)) + await ShouldRemoveUnreferencedInternalBuildableAttribute(solution, model, attr, referencedTypeNames)) { invalidAttributes.Add(attr); } @@ -692,10 +697,11 @@ private static bool IsInternalRecordBuildableAttribute(AttributeListSyntax attri typeName?.EndsWith("PatchUpdate", StringComparison.Ordinal) == true; } - private static async Task ShouldRemoveUnreferencedInternalBuildableAttribute( + private async Task ShouldRemoveUnreferencedInternalBuildableAttribute( Solution solution, SemanticModel model, - AttributeListSyntax attributeList) + AttributeListSyntax attributeList, + HashSet referencedTypeNames) { if (attributeList.Attributes.Count != 1) { @@ -731,6 +737,11 @@ private static async Task ShouldRemoveUnreferencedInternalBuildableAttribu return true; } + if (referencedTypeNames.Contains(typeSymbol.GetFullyQualifiedName())) + { + return false; + } + foreach (var referencedSymbol in await SymbolFinder.FindReferencesAsync(typeSymbol, solution)) { foreach (var location in referencedSymbol.Locations) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 49bbab86289..290de457d4e 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -7,6 +7,8 @@ using System.Linq; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; +using System.Xml.Linq; +using Microsoft.TypeSpec.Generator.Expressions; using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.Statements; @@ -31,16 +33,22 @@ public static void Analyze(IReadOnlyList providers, Project projec // Generated-code dependencies come from providers. Custom code still needs Roslyn // because arbitrary user C# can reference generated types in ways providers cannot see. var customPublicRoots = GetCustomCodePublicGeneratedTypeRoots(project, graph.Nodes); - customPublicRoots.UnionWith(GetApiBaselineGeneratedTypeRoots(graph.Nodes)); + var apiBaselineGeneratedTypeRoots = GetApiBaselineGeneratedTypeRoots(graph.Nodes); + customPublicRoots.UnionWith(apiBaselineGeneratedTypeRoots); + var generatedPublicDeclarations = GetGeneratedPublicTypeDeclarations(providers, graph.Nodes); + customPublicRoots.UnionWith(generatedPublicDeclarations); var customRemovalRoots = GetCustomCodeGeneratedTypeRoots(project, graph.Nodes); + customRemovalRoots.UnionWith(apiBaselineGeneratedTypeRoots); + customRemovalRoots.UnionWith(generatedPublicDeclarations); var customInternalDeclarations = GetCustomCodeInternalGeneratedTypeDeclarations(project, graph.Nodes); var generatedInternalDeclarations = GetGeneratedInternalTypeDeclarations(providers, graph.Nodes); // Helper types are rooted after an initial reachability pass so unused infrastructure // such as change-tracking dictionaries can still be removed when no reachable type needs them. var generatedDiscriminatorBaseNames = GetGeneratedPersistableModelProxyTypeNames(project, publicGraph.Nodes); + AddGeneratedXmlDocCrefReferences(project, publicGraph, publicOnly: true); var internalizeReferences = CloneReferences(publicGraph.References); - var internalizeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: false, includeAdditionalRoots: true, includeUnionVariantRoots: true, publicClientRootsOnly: true); + var internalizeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: false, includeAdditionalRoots: true, includeUnionVariantRoots: false, publicClientRootsOnly: true); var generatedPublicReachable = GetReachableTypes(internalizeRoots, internalizeReferences); AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences, generatedPublicReachable, generatedDiscriminatorBaseNames); internalizeRoots.UnionWith(customPublicRoots); @@ -84,9 +92,17 @@ public static void Analyze(IReadOnlyList providers, Project projec // Body-only generated dependencies are needed to avoid deleting helper files, but they do // not contribute to public API reachability for internalization. + AddGeneratedXmlDocCrefReferences(project, graph, publicOnly: false); AddGeneratedBodyReferences(project, providers, graph); - var removeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: true, includeAdditionalRoots: true, includeUnionVariantRoots: true, publicClientRootsOnly: false); + var removeRoots = GetRootNames( + providers, + graph.Nodes, + helperRoots: [], + includeModelFactory: true, + includeAdditionalRoots: true, + includeUnionVariantRoots: false, + publicClientRootsOnly: false); removeRoots.UnionWith(customRemovalRoots); RemoveUnusedRequestHeaderExtensionsRoot(removeRoots, graph.References, project); var removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); @@ -97,7 +113,6 @@ public static void Analyze(IReadOnlyList providers, Project projec var removeReachable = GetReachableTypes(removeRoots, graph.References); var removeDeclaredNodes = GetPostProcessorDeclaredNodes(providers, graph.Nodes, publicOnly: false); var removeCandidates = removeDeclaredNodes.Except(removeReachable, StringComparer.Ordinal).OrderBy(static name => name, StringComparer.Ordinal).ToArray(); - var helperRoots = internalizeHelperRoots.Concat(removeHelperRoots).ToHashSet(StringComparer.Ordinal); _latestResult = new ProviderReferenceMapResult( @@ -363,16 +378,27 @@ private static HashSet GetGeneratedPersistableModelProxyTypeNames(Projec } private static HashSet GetGeneratedInternalTypeDeclarations(IReadOnlyList providers, HashSet generatedTypeNames) + => GetGeneratedTypeDeclarationsByLastContractAccessibility(providers, generatedTypeNames, Accessibility.Internal); + + private static HashSet GetGeneratedPublicTypeDeclarations(IReadOnlyList providers, HashSet generatedTypeNames) + => GetGeneratedTypeDeclarationsByLastContractAccessibility(providers, generatedTypeNames, Accessibility.Public); + + private static HashSet GetGeneratedTypeDeclarationsByLastContractAccessibility( + IReadOnlyList providers, + HashSet generatedTypeNames, + Accessibility accessibility) { var declarations = new HashSet(StringComparer.Ordinal); + var lastContract = CodeModelGenerator.Instance.SourceInputModel.LastContract; foreach (var provider in GetGeneratedProviders(providers)) { - if (provider.LastContractView?.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal) != true) + var providerTypeName = GetProviderTypeName(provider.Type); + if (lastContract?.GetTypeByMetadataName(providerTypeName)?.DeclaredAccessibility != accessibility) { continue; } - AddMatchingName(declarations, GetProviderTypeName(provider.Type), generatedTypeNames); + AddMatchingName(declarations, providerTypeName, generatedTypeNames); } return declarations; @@ -508,6 +534,16 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList pro foreach (var method in provider.Methods) { + if (ShouldUseGeneratedSourceReferences(provider)) + { + continue; + } + + if (method.IsMethodSuppressed()) + { + continue; + } + if (publicOnly && !IsPublic(method.Signature.Modifiers)) { continue; @@ -681,7 +717,7 @@ private static void AddGeneratedBodyReferences(Project project, IReadOnlyList 0) @@ -736,6 +772,11 @@ private static IReadOnlyList GetBodyReferenceProviders(IReadOnlyLi private static bool IsGeneratedBodyReferenceCandidate(TypeProvider provider) { + if (ShouldUseGeneratedSourceReferences(provider)) + { + return true; + } + if (provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Static)) { return true; @@ -748,6 +789,54 @@ private static bool IsGeneratedBodyReferenceCandidate(TypeProvider provider) provider.BodyDependencyTypes.Count > 0; } + private static bool ShouldUseGeneratedSourceReferences(TypeProvider provider) => + CodeModelGenerator.Instance.Configuration.PackageName.StartsWith("Azure.", StringComparison.Ordinal) && + provider.RelativeFilePath.EndsWith("Client.cs", StringComparison.Ordinal); + + private static void AddGeneratedXmlDocCrefReferences(Project project, ProviderReferenceGraph graph, bool publicOnly) + { + var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); + if (compilation == null) + { + return; + } + + foreach (var providerName in graph.Nodes) + { + var symbol = compilation.GetTypeByMetadataName(providerName); + if (symbol == null || publicOnly && symbol.DeclaredAccessibility != Accessibility.Public) + { + continue; + } + + foreach (var property in symbol.GetMembers().OfType()) + { + if (publicOnly && property.DeclaredAccessibility != Accessibility.Public) + { + continue; + } + + var xml = property.GetDocumentationCommentXml(); + if (string.IsNullOrEmpty(xml)) + { + continue; + } + + var cRefs = XDocument.Parse(xml) + .Descendants() + .Attributes("cref") + .Select(static attribute => attribute.Value) + .Where(static cref => cref.Length > 2 && cref[0] == 'T' && cref[1] == ':') + .Select(static cref => cref.Substring(2)); + + foreach (var cref in cRefs) + { + AddMatchingName(graph.References[providerName], cref, graph.Nodes); + } + } + } + } + private static bool IsGeneratedHelperSimpleName(string name) => string.Equals(name, "ChangeTrackingDictionary", StringComparison.Ordinal) || string.Equals(name, "ChangeTrackingList", StringComparison.Ordinal); @@ -766,6 +855,11 @@ private static void AddGeneratedBodyTypeReferences(Project project, Compilation var semanticModel = compilation.GetSemanticModel(syntaxReference.SyntaxTree); foreach (var typeSyntax in root.DescendantNodes().OfType()) { + if (IsNamespaceOrUsingName(typeSyntax)) + { + continue; + } + // Declaration names are the owner itself. The old Roslyn map captures references, // not a declaration making itself reachable. if (typeSyntax.Parent is BaseTypeDeclarationSyntax baseTypeDeclaration && baseTypeDeclaration.Identifier.Span == typeSyntax.Span) @@ -780,6 +874,20 @@ private static void AddGeneratedBodyTypeReferences(Project project, Compilation } } + private static bool IsNamespaceOrUsingName(TypeSyntax typeSyntax) + { + for (SyntaxNode? node = typeSyntax; node != null; node = node.Parent) + { + if (node.Parent is BaseNamespaceDeclarationSyntax namespaceDeclaration && namespaceDeclaration.Name == node || + node.Parent is UsingDirectiveSyntax usingDirective && usingDirective.Name == node) + { + return true; + } + } + + return false; + } + private static void AddUnresolvedBodyTypeSyntaxReference(HashSet references, TypeSyntax typeSyntax, ITypeSymbol? symbol, HashSet nodes) { if (symbol is INamedTypeSymbol { TypeKind: not TypeKind.Error }) @@ -796,7 +904,7 @@ private static void AddUnresolvedBodyTypeSyntaxReference(HashSet referen _ => null }; - if (simpleName != null && IsGeneratedHelperSimpleName(simpleName)) + if (simpleName != null) { AddMatchingName(references, simpleName, nodes); } @@ -897,10 +1005,7 @@ private static HashSet GetRootNames( return roots; } - foreach (var root in generator.TypeFactory.UnionVariantTypesToKeep) - { - AddMatchingName(roots, root, nodes); - } + AddUnionVariantRoots(roots, providers, nodes); foreach (var root in generator.AdditionalRootTypes) { @@ -931,6 +1036,21 @@ private static void AddLastContractModelFactorySignatureRoots(IReadOnlyList roots, IReadOnlyList providers, HashSet nodes) + { + var unionVariantTypesToKeep = CodeModelGenerator.Instance.TypeFactory.UnionVariantTypesToKeep; + foreach (var provider in GetGeneratedProviders(providers)) + { + if (!unionVariantTypesToKeep.Contains(provider.Type.Name) || + string.Equals(provider.Type.Namespace, "TypeSpec.Http", StringComparison.Ordinal)) + { + continue; + } + + AddMatchingName(roots, GetProviderTypeName(provider.Type), nodes); + } + } + private static bool IsImplementationOnlyModelFactoryMethod(MethodProvider method) { var returnType = method.Signature.ReturnType; @@ -1324,6 +1444,27 @@ private static void AddAttributes( foreach (var attribute in attributes) { AddTypeReference(references, attribute.Type, nodes, serializationProviderNamesByType); + foreach (var argument in attribute.Arguments) + { + AddAttributeArgumentReference(references, argument, nodes, serializationProviderNamesByType); + } + + foreach (var (_, argument) in attribute.PositionalArguments) + { + AddAttributeArgumentReference(references, argument, nodes, serializationProviderNamesByType); + } + } + } + + private static void AddAttributeArgumentReference( + HashSet references, + ValueExpression argument, + HashSet nodes, + IReadOnlyDictionary? serializationProviderNamesByType) + { + if (argument is TypeOfExpression typeOf) + { + AddTypeReference(references, typeOf.Type, nodes, serializationProviderNamesByType); } } From fa095820c64f9f39b5853a85348c35ba2b2839ef Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Tue, 23 Jun 2026 14:51:29 +0000 Subject: [PATCH 062/111] fix(http-client-csharp): keep non-azure union roots Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/PostProcessing/ProviderReferenceMapAnalyzer.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 290de457d4e..b2bfc32e870 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -49,6 +49,10 @@ public static void Analyze(IReadOnlyList providers, Project projec AddGeneratedXmlDocCrefReferences(project, publicGraph, publicOnly: true); var internalizeReferences = CloneReferences(publicGraph.References); var internalizeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: false, includeAdditionalRoots: true, includeUnionVariantRoots: false, publicClientRootsOnly: true); + if (ShouldUseUnionVariantFallbackRoots()) + { + AddUnionVariantRoots(internalizeRoots, providers, graph.Nodes); + } var generatedPublicReachable = GetReachableTypes(internalizeRoots, internalizeReferences); AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences, generatedPublicReachable, generatedDiscriminatorBaseNames); internalizeRoots.UnionWith(customPublicRoots); @@ -101,7 +105,7 @@ public static void Analyze(IReadOnlyList providers, Project projec helperRoots: [], includeModelFactory: true, includeAdditionalRoots: true, - includeUnionVariantRoots: false, + includeUnionVariantRoots: ShouldUseUnionVariantFallbackRoots(), publicClientRootsOnly: false); removeRoots.UnionWith(customRemovalRoots); RemoveUnusedRequestHeaderExtensionsRoot(removeRoots, graph.References, project); @@ -1051,6 +1055,10 @@ private static void AddUnionVariantRoots(HashSet roots, IReadOnlyList + !CodeModelGenerator.Instance.Configuration.PackageName.StartsWith("Azure.", StringComparison.Ordinal) && + CodeModelGenerator.Instance.SourceInputModel.LastContract == null; + private static bool IsImplementationOnlyModelFactoryMethod(MethodProvider method) { var returnType = method.Signature.ReturnType; From d055a32f9e009fe22a8ea181ee96104491bfa49e Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 24 Jun 2026 02:13:28 +0000 Subject: [PATCH 063/111] fix(http-client-csharp): keep enum serialization dependencies Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ProviderReferenceMapAnalyzer.cs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index b2bfc32e870..37d1775087a 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -758,7 +758,26 @@ private static void AddProviderBodyDependencyTypes(HashSet references, I { foreach (var dependency in dependencies) { - AddTypeReference(references, dependency, nodes); + AddProviderBodyDependencyType(references, dependency, nodes); + } + } + + private static void AddProviderBodyDependencyType(HashSet references, CSharpType? dependency, HashSet nodes) + { + if (dependency == null) + { + return; + } + + AddTypeReference(references, dependency, nodes); + if (dependency.IsEnum) + { + AddMatchingName(references, $"{dependency.Name}Extensions", nodes); + } + + foreach (var argument in dependency.Arguments) + { + AddProviderBodyDependencyType(references, argument, nodes); } } From 0eb6ca57c3f87118194f14a2d1c38ea8d5ce5033 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 24 Jun 2026 05:33:09 +0000 Subject: [PATCH 064/111] fix(http-client-csharp): retain custom enum serialization providers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/PostProcessing/ProviderReferenceMapAnalyzer.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 37d1775087a..5a4c6b05f28 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -770,10 +770,7 @@ private static void AddProviderBodyDependencyType(HashSet references, CS } AddTypeReference(references, dependency, nodes); - if (dependency.IsEnum) - { - AddMatchingName(references, $"{dependency.Name}Extensions", nodes); - } + AddMatchingName(references, $"{dependency.Name}Extensions", nodes); foreach (var argument in dependency.Arguments) { From b8a5c3ae8eeb8b9bd0958bd3897325c38c2ec2f3 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 24 Jun 2026 08:49:13 +0000 Subject: [PATCH 065/111] fix(http-client-csharp): scan serialization provider references Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/PostProcessing/ProviderReferenceMapAnalyzer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 5a4c6b05f28..939d7e887d4 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -724,7 +724,7 @@ private static void AddGeneratedBodyReferences(Project project, IReadOnlyList 0) + if (bodyDependencyTypes.Count > 0 && !IsSerializationProvider(provider)) { continue; } From 912ffe059e10738ac29a69a8f8dbca4204815a9a Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 24 Jun 2026 09:32:49 +0000 Subject: [PATCH 066/111] fix(http-client-csharp): preserve base-kept dependencies Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ybrid-reference-map-2026-06-24-09-30-16.md | 7 +++ .../ProviderReferenceMapAnalyzer.cs | 49 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 .chronus/changes/mtg-hybrid-reference-map-2026-06-24-09-30-16.md diff --git a/.chronus/changes/mtg-hybrid-reference-map-2026-06-24-09-30-16.md b/.chronus/changes/mtg-hybrid-reference-map-2026-06-24-09-30-16.md new file mode 100644 index 00000000000..8e0813e5c3d --- /dev/null +++ b/.chronus/changes/mtg-hybrid-reference-map-2026-06-24-09-30-16.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/http-client-csharp" +--- + +Preserve generated types that are kept because their base type is reachable when removing unreferenced C# types. diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 939d7e887d4..6866c6fb65e 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -112,9 +112,11 @@ public static void Analyze(IReadOnlyList providers, Project projec var removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); AddDerivedModelReferences(providers, graph.Nodes, graph.References, removeReachableWithoutHelpers, generatedDiscriminatorBaseNames); removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); + AddBasePreservedReferences(providers, graph.Nodes, graph.References, removeReachableWithoutHelpers); var removeHelperRoots = GetHelperRootNames(providers, graph.Nodes, removeReachableWithoutHelpers, graph.References); removeRoots.UnionWith(removeHelperRoots); var removeReachable = GetReachableTypes(removeRoots, graph.References); + AddBasePreservedReferences(providers, graph.Nodes, graph.References, removeReachable); var removeDeclaredNodes = GetPostProcessorDeclaredNodes(providers, graph.Nodes, publicOnly: false); var removeCandidates = removeDeclaredNodes.Except(removeReachable, StringComparer.Ordinal).OrderBy(static name => name, StringComparer.Ordinal).ToArray(); var helperRoots = internalizeHelperRoots.Concat(removeHelperRoots).ToHashSet(StringComparer.Ordinal); @@ -670,6 +672,42 @@ private static void AddDerivedModelReferences( } } + private static void AddBasePreservedReferences( + IReadOnlyList providers, + HashSet nodes, + IReadOnlyDictionary> references, + HashSet reachableTypes) + { + var addedReference = true; + while (addedReference) + { + addedReference = false; + foreach (var provider in GetGeneratedProviders(providers)) + { + var providerName = GetProviderTypeName(provider.Type); + if (!nodes.Contains(providerName) || reachableTypes.Contains(providerName)) + { + continue; + } + + var baseTypeName = provider.BaseType == null ? null : GetProviderTypeName(provider.BaseType); + if (baseTypeName == null || !reachableTypes.Contains(baseTypeName)) + { + continue; + } + + reachableTypes.Add(providerName); + foreach (var reference in references[providerName]) + { + if (reachableTypes.Add(reference)) + { + addedReference = true; + } + } + } + } + } + private static IReadOnlyList GetGeneratedProviders(IReadOnlyList providers) { var generatedProviders = new List(); @@ -891,6 +929,17 @@ private static void AddGeneratedBodyTypeReferences(Project project, Compilation AddBodyTypeReference(graph.References[ownerName], typeInfo, graph.Nodes); AddUnresolvedBodyTypeSyntaxReference(graph.References[ownerName], typeSyntax, typeInfo, graph.Nodes); } + + foreach (var invocation in root.DescendantNodes().OfType()) + { + if (semanticModel.GetSymbolInfo(invocation).Symbol is not IMethodSymbol method) + { + continue; + } + + AddBodyTypeReference(graph.References[ownerName], method.ContainingType, graph.Nodes); + AddBodyTypeReference(graph.References[ownerName], method.ReducedFrom?.ContainingType, graph.Nodes); + } } } From db768fabb7fc8910e93b013352eaf5e62640e350 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 24 Jun 2026 09:52:48 +0000 Subject: [PATCH 067/111] perf(http-client-csharp): update hybrid benchmark analyzer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ProviderReferenceMapShadowAnalyzer.cs | 175 +++++++++++++++++- 1 file changed, 168 insertions(+), 7 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs index acd7f89c639..9483cdfe616 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs @@ -8,6 +8,7 @@ using System.Runtime.CompilerServices; using System.Text; using System.Text.RegularExpressions; +using System.Xml.Linq; using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.Statements; @@ -60,14 +61,20 @@ public static void Analyze(IReadOnlyList providers, Project projec // Generated-code dependencies come from providers. Custom code still needs Roslyn // because arbitrary user C# can reference generated types in ways providers cannot see. var customPublicRoots = GetCustomCodePublicGeneratedTypeRoots(project, graph.Nodes); - customPublicRoots.UnionWith(GetApiBaselineGeneratedTypeRoots(graph.Nodes)); + var apiBaselineGeneratedTypeRoots = GetApiBaselineGeneratedTypeRoots(graph.Nodes); + customPublicRoots.UnionWith(apiBaselineGeneratedTypeRoots); + var generatedPublicDeclarations = GetGeneratedPublicTypeDeclarations(generatedProviders, graph.Nodes); + customPublicRoots.UnionWith(generatedPublicDeclarations); var customRemovalRoots = GetCustomCodeGeneratedTypeRoots(project, graph.Nodes); + customRemovalRoots.UnionWith(apiBaselineGeneratedTypeRoots); + customRemovalRoots.UnionWith(generatedPublicDeclarations); var customInternalDeclarations = GetCustomCodeInternalGeneratedTypeDeclarations(project, graph.Nodes); var generatedInternalDeclarations = GetGeneratedInternalTypeDeclarations(generatedProviders, graph.Nodes); // Helper types are rooted after an initial reachability pass so unused infrastructure // such as change-tracking dictionaries can still be removed when no reachable type needs them. var generatedDiscriminatorBaseNames = GetGeneratedPersistableModelProxyTypeNames(project, publicGraph.Nodes); + AddGeneratedXmlDocCrefReferences(project, publicGraph, publicOnly: true); var internalizeReferences = CloneReferences(publicGraph.References); var internalizeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: false, includeAdditionalRoots: true, includeUnionVariantRoots: true, publicClientRootsOnly: true); var generatedPublicReachable = GetReachableTypes(internalizeRoots, internalizeReferences); @@ -113,6 +120,7 @@ public static void Analyze(IReadOnlyList providers, Project projec // Body-only generated dependencies are needed to avoid deleting helper files, but they do // not contribute to public API reachability for internalization. + AddGeneratedXmlDocCrefReferences(project, graph, publicOnly: false); AddGeneratedBodyReferences(project, providers, graph); var removeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: true, includeAdditionalRoots: true, includeUnionVariantRoots: true, publicClientRootsOnly: false); @@ -121,9 +129,11 @@ public static void Analyze(IReadOnlyList providers, Project projec var removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); AddDerivedModelReferences(providers, graph.Nodes, graph.References, removeReachableWithoutHelpers, generatedDiscriminatorBaseNames); removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); + AddBasePreservedReferences(providers, graph.Nodes, graph.References, removeReachableWithoutHelpers); var removeHelperRoots = GetHelperRootNames(generatedProviders, graph.Nodes, removeReachableWithoutHelpers, graph.References); removeRoots.UnionWith(removeHelperRoots); var removeReachable = GetReachableTypes(removeRoots, graph.References); + AddBasePreservedReferences(providers, graph.Nodes, graph.References, removeReachable); var removeDeclaredNodes = GetPostProcessorDeclaredNodes(generatedProviders, graph.Nodes, publicOnly: false); var removeCandidates = removeDeclaredNodes.Except(removeReachable, StringComparer.Ordinal).OrderBy(static name => name, StringComparer.Ordinal).ToArray(); @@ -410,16 +420,27 @@ private static HashSet GetGeneratedPersistableModelProxyTypeNames(Projec } private static HashSet GetGeneratedInternalTypeDeclarations(IReadOnlyList generatedProviders, HashSet generatedTypeNames) + => GetGeneratedTypeDeclarationsByLastContractAccessibility(generatedProviders, generatedTypeNames, Accessibility.Internal); + + private static HashSet GetGeneratedPublicTypeDeclarations(IReadOnlyList generatedProviders, HashSet generatedTypeNames) + => GetGeneratedTypeDeclarationsByLastContractAccessibility(generatedProviders, generatedTypeNames, Accessibility.Public); + + private static HashSet GetGeneratedTypeDeclarationsByLastContractAccessibility( + IReadOnlyList generatedProviders, + HashSet generatedTypeNames, + Accessibility accessibility) { var declarations = new HashSet(StringComparer.Ordinal); + var lastContract = CodeModelGenerator.Instance.SourceInputModel.LastContract; foreach (var provider in generatedProviders) { - if (provider.LastContractView?.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal) != true) + var providerTypeName = GetProviderTypeName(provider.Type); + if (lastContract?.GetTypeByMetadataName(providerTypeName)?.DeclaredAccessibility != accessibility) { continue; } - AddMatchingName(declarations, GetProviderTypeName(provider.Type), generatedTypeNames); + AddMatchingName(declarations, providerTypeName, generatedTypeNames); } return declarations; @@ -554,6 +575,11 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList gen foreach (var method in provider.Methods) { + if (ShouldUseGeneratedSourceReferences(provider)) + { + continue; + } + if (publicOnly && !IsPublic(method.Signature.Modifiers)) { continue; @@ -676,6 +702,42 @@ private static void AddDerivedModelReferences( } } + private static void AddBasePreservedReferences( + IReadOnlyList providers, + HashSet nodes, + IReadOnlyDictionary> references, + HashSet reachableTypes) + { + var addedReference = true; + while (addedReference) + { + addedReference = false; + foreach (var provider in GetGeneratedProviders(providers)) + { + var providerName = GetProviderTypeName(provider.Type); + if (!nodes.Contains(providerName) || reachableTypes.Contains(providerName)) + { + continue; + } + + var baseTypeName = provider.BaseType == null ? null : GetProviderTypeName(provider.BaseType); + if (baseTypeName == null || !reachableTypes.Contains(baseTypeName)) + { + continue; + } + + reachableTypes.Add(providerName); + foreach (var reference in references[providerName]) + { + if (reachableTypes.Add(reference)) + { + addedReference = true; + } + } + } + } + } + private static IReadOnlyList GetGeneratedProviders(IReadOnlyList providers) { var generatedProviders = new List(); @@ -727,10 +789,10 @@ private static void AddGeneratedBodyReferences(Project project, IReadOnlyList 0) + if (bodyDependencyTypes.Count > 0 && !IsSerializationProvider(provider)) { continue; } @@ -764,7 +826,23 @@ private static void AddProviderBodyDependencyTypes(HashSet references, I { foreach (var dependency in dependencies) { - AddTypeReference(references, dependency, nodes); + AddProviderBodyDependencyType(references, dependency, nodes); + } + } + + private static void AddProviderBodyDependencyType(HashSet references, CSharpType? dependency, HashSet nodes) + { + if (dependency == null) + { + return; + } + + AddTypeReference(references, dependency, nodes); + AddMatchingName(references, $"{dependency.Name}Extensions", nodes); + + foreach (var argument in dependency.Arguments) + { + AddProviderBodyDependencyType(references, argument, nodes); } } @@ -782,6 +860,11 @@ private static IReadOnlyList GetBodyReferenceProviders(IReadOnlyLi private static bool IsGeneratedBodyReferenceCandidate(TypeProvider provider) { + if (ShouldUseGeneratedSourceReferences(provider)) + { + return true; + } + if (provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Static)) { return true; @@ -794,6 +877,54 @@ private static bool IsGeneratedBodyReferenceCandidate(TypeProvider provider) provider.BodyDependencyTypes.Count > 0; } + private static bool ShouldUseGeneratedSourceReferences(TypeProvider provider) => + CodeModelGenerator.Instance.Configuration.PackageName.StartsWith("Azure.", StringComparison.Ordinal) && + provider.RelativeFilePath.EndsWith("Client.cs", StringComparison.Ordinal); + + private static void AddGeneratedXmlDocCrefReferences(Project project, ProviderReferenceGraph graph, bool publicOnly) + { + var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); + if (compilation == null) + { + return; + } + + foreach (var providerName in graph.Nodes) + { + var symbol = compilation.GetTypeByMetadataName(providerName); + if (symbol == null || publicOnly && symbol.DeclaredAccessibility != Accessibility.Public) + { + continue; + } + + foreach (var property in symbol.GetMembers().OfType()) + { + if (publicOnly && property.DeclaredAccessibility != Accessibility.Public) + { + continue; + } + + var xml = property.GetDocumentationCommentXml(); + if (string.IsNullOrEmpty(xml)) + { + continue; + } + + var cRefs = XDocument.Parse(xml) + .Descendants() + .Attributes("cref") + .Select(static attribute => attribute.Value) + .Where(static cref => cref.Length > 2 && cref[0] == 'T' && cref[1] == ':') + .Select(static cref => cref.Substring(2)); + + foreach (var cref in cRefs) + { + AddMatchingName(graph.References[providerName], cref, graph.Nodes); + } + } + } + } + private static bool IsGeneratedHelperSimpleName(string name) => string.Equals(name, "ChangeTrackingDictionary", StringComparison.Ordinal) || string.Equals(name, "ChangeTrackingList", StringComparison.Ordinal); @@ -812,6 +943,11 @@ private static void AddGeneratedBodyTypeReferences(Project project, Compilation var semanticModel = compilation.GetSemanticModel(syntaxReference.SyntaxTree); foreach (var typeSyntax in root.DescendantNodes().OfType()) { + if (IsNamespaceOrUsingName(typeSyntax)) + { + continue; + } + // Declaration names are the owner itself. The old Roslyn map captures references, // not a declaration making itself reachable. if (typeSyntax.Parent is BaseTypeDeclarationSyntax baseTypeDeclaration && baseTypeDeclaration.Identifier.Span == typeSyntax.Span) @@ -823,7 +959,32 @@ private static void AddGeneratedBodyTypeReferences(Project project, Compilation AddBodyTypeReference(graph.References[ownerName], typeInfo, graph.Nodes); AddUnresolvedBodyTypeSyntaxReference(graph.References[ownerName], typeSyntax, typeInfo, graph.Nodes); } + + foreach (var invocation in root.DescendantNodes().OfType()) + { + if (semanticModel.GetSymbolInfo(invocation).Symbol is not IMethodSymbol method) + { + continue; + } + + AddBodyTypeReference(graph.References[ownerName], method.ContainingType, graph.Nodes); + AddBodyTypeReference(graph.References[ownerName], method.ReducedFrom?.ContainingType, graph.Nodes); + } + } + } + + private static bool IsNamespaceOrUsingName(TypeSyntax typeSyntax) + { + for (SyntaxNode? node = typeSyntax; node != null; node = node.Parent) + { + if (node.Parent is BaseNamespaceDeclarationSyntax namespaceDeclaration && namespaceDeclaration.Name == node || + node.Parent is UsingDirectiveSyntax usingDirective && usingDirective.Name == node) + { + return true; + } } + + return false; } private static void AddUnresolvedBodyTypeSyntaxReference(HashSet references, TypeSyntax typeSyntax, ITypeSymbol? symbol, HashSet nodes) @@ -842,7 +1003,7 @@ private static void AddUnresolvedBodyTypeSyntaxReference(HashSet referen _ => null }; - if (simpleName != null && IsGeneratedHelperSimpleName(simpleName)) + if (simpleName != null) { AddMatchingName(references, simpleName, nodes); } From 57ed559c3e3cde1ca28a8d7caa90a2e616348571 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 24 Jun 2026 13:33:50 +0000 Subject: [PATCH 068/111] perf(http-client-csharp): reduce provider reference map overhead Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ProviderReferenceMapShadowAnalyzer.cs | 112 +++++------------- 1 file changed, 30 insertions(+), 82 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs index 9483cdfe616..8524e909359 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs @@ -15,7 +15,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.FindSymbols; namespace Microsoft.TypeSpec.Generator { @@ -129,11 +128,11 @@ public static void Analyze(IReadOnlyList providers, Project projec var removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); AddDerivedModelReferences(providers, graph.Nodes, graph.References, removeReachableWithoutHelpers, generatedDiscriminatorBaseNames); removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); - AddBasePreservedReferences(providers, graph.Nodes, graph.References, removeReachableWithoutHelpers); + AddBasePreservedReferences(generatedProviders, graph.Nodes, graph.References, removeReachableWithoutHelpers); var removeHelperRoots = GetHelperRootNames(generatedProviders, graph.Nodes, removeReachableWithoutHelpers, graph.References); removeRoots.UnionWith(removeHelperRoots); var removeReachable = GetReachableTypes(removeRoots, graph.References); - AddBasePreservedReferences(providers, graph.Nodes, graph.References, removeReachable); + AddBasePreservedReferences(generatedProviders, graph.Nodes, graph.References, removeReachable); var removeDeclaredNodes = GetPostProcessorDeclaredNodes(generatedProviders, graph.Nodes, publicOnly: false); var removeCandidates = removeDeclaredNodes.Except(removeReachable, StringComparer.Ordinal).OrderBy(static name => name, StringComparer.Ordinal).ToArray(); @@ -703,7 +702,7 @@ private static void AddDerivedModelReferences( } private static void AddBasePreservedReferences( - IReadOnlyList providers, + IReadOnlyList generatedProviders, HashSet nodes, IReadOnlyDictionary> references, HashSet reachableTypes) @@ -712,7 +711,7 @@ private static void AddBasePreservedReferences( while (addedReference) { addedReference = false; - foreach (var provider in GetGeneratedProviders(providers)) + foreach (var provider in generatedProviders) { var providerName = GetProviderTypeName(provider.Type); if (!nodes.Contains(providerName) || reachableTypes.Contains(providerName)) @@ -803,21 +802,6 @@ private static void AddGeneratedBodyReferences(Project project, IReadOnlyList()) - { - if (method.IsExtensionMethod) - { - AddGeneratedReferencesToHelper(project, compilation, graph, providerName, method); - } - } - } - } - AddGeneratedBodyTypeReferences(project, compilation, graph, providerName, symbol); } } @@ -905,7 +889,8 @@ private static void AddGeneratedXmlDocCrefReferences(Project project, ProviderRe } var xml = property.GetDocumentationCommentXml(); - if (string.IsNullOrEmpty(xml)) + if (string.IsNullOrEmpty(xml) || + !xml.Contains("cref=\"T:", StringComparison.Ordinal)) { continue; } @@ -941,34 +926,37 @@ private static void AddGeneratedBodyTypeReferences(Project project, Compilation var root = syntaxReference.SyntaxTree.GetRoot(); var semanticModel = compilation.GetSemanticModel(syntaxReference.SyntaxTree); - foreach (var typeSyntax in root.DescendantNodes().OfType()) + foreach (var node in root.DescendantNodes()) { - if (IsNamespaceOrUsingName(typeSyntax)) + if (node is TypeSyntax typeSyntax) { - continue; - } + if (IsNamespaceOrUsingName(typeSyntax)) + { + continue; + } - // Declaration names are the owner itself. The old Roslyn map captures references, - // not a declaration making itself reachable. - if (typeSyntax.Parent is BaseTypeDeclarationSyntax baseTypeDeclaration && baseTypeDeclaration.Identifier.Span == typeSyntax.Span) - { - continue; - } + // Declaration names are the owner itself. The old Roslyn map captures references, + // not a declaration making itself reachable. + if (typeSyntax.Parent is BaseTypeDeclarationSyntax baseTypeDeclaration && baseTypeDeclaration.Identifier.Span == typeSyntax.Span) + { + continue; + } - var typeInfo = semanticModel.GetTypeInfo(typeSyntax).Type; - AddBodyTypeReference(graph.References[ownerName], typeInfo, graph.Nodes); - AddUnresolvedBodyTypeSyntaxReference(graph.References[ownerName], typeSyntax, typeInfo, graph.Nodes); - } + var typeInfo = semanticModel.GetTypeInfo(typeSyntax).Type; + AddBodyTypeReference(graph.References[ownerName], typeInfo, graph.Nodes); + AddUnresolvedBodyTypeSyntaxReference(graph.References[ownerName], typeSyntax, typeInfo, graph.Nodes); + } - foreach (var invocation in root.DescendantNodes().OfType()) - { - if (semanticModel.GetSymbolInfo(invocation).Symbol is not IMethodSymbol method) + if (node is InvocationExpressionSyntax invocation) { - continue; - } + if (semanticModel.GetSymbolInfo(invocation).Symbol is not IMethodSymbol method) + { + continue; + } - AddBodyTypeReference(graph.References[ownerName], method.ContainingType, graph.Nodes); - AddBodyTypeReference(graph.References[ownerName], method.ReducedFrom?.ContainingType, graph.Nodes); + AddBodyTypeReference(graph.References[ownerName], method.ContainingType, graph.Nodes); + AddBodyTypeReference(graph.References[ownerName], method.ReducedFrom?.ContainingType, graph.Nodes); + } } } } @@ -1032,46 +1020,6 @@ private static void AddBodyTypeReference(HashSet references, ITypeSymbol } } - private static void AddGeneratedReferencesToHelper(Project project, Compilation compilation, ProviderReferenceGraph graph, string helperName, ISymbol symbol) - { - foreach (var reference in SymbolFinder.FindReferencesAsync(symbol, project.Solution).GetAwaiter().GetResult()) - { - foreach (var location in reference.Locations) - { - var document = location.Document; - if (!IsGeneratedDocument(document)) - { - continue; - } - - var root = document.GetSyntaxRootAsync().GetAwaiter().GetResult(); - if (root == null) - { - continue; - } - - var node = root.FindNode(location.Location.SourceSpan); - var owner = node.AncestorsAndSelf().OfType().FirstOrDefault(); - if (owner == null) - { - continue; - } - - var semanticModel = compilation.GetSemanticModel(owner.SyntaxTree); - if (semanticModel.GetDeclaredSymbol(owner) is not INamedTypeSymbol ownerSymbol) - { - continue; - } - - var ownerName = ownerSymbol.GetFullyQualifiedName(); - if (graph.Nodes.Contains(ownerName)) - { - graph.References[ownerName].Add(helperName); - } - } - } - } - private static HashSet GetRootNames( IReadOnlyList providers, HashSet nodes, From 88bea5758432ba3a1e080c6014692c8b9a14b4a0 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 24 Jun 2026 13:39:38 +0000 Subject: [PATCH 069/111] perf(http-client-csharp): reduce provider reference map analysis cost Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ProviderReferenceMapAnalyzer.cs | 137 +++++------------- 1 file changed, 40 insertions(+), 97 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 6866c6fb65e..d65a1d1eb13 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -14,7 +14,6 @@ using Microsoft.TypeSpec.Generator.Statements; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.FindSymbols; namespace Microsoft.TypeSpec.Generator { @@ -27,21 +26,22 @@ internal static class ProviderReferenceMapAnalyzer public static void Analyze(IReadOnlyList providers, Project project) { - var graph = BuildGraph(providers); - var publicGraph = BuildGraph(providers, publicOnly: true); + var generatedProviders = GetGeneratedProviders(providers); + var graph = BuildGraph(generatedProviders); + var publicGraph = BuildGraph(generatedProviders, publicOnly: true); // Generated-code dependencies come from providers. Custom code still needs Roslyn // because arbitrary user C# can reference generated types in ways providers cannot see. var customPublicRoots = GetCustomCodePublicGeneratedTypeRoots(project, graph.Nodes); var apiBaselineGeneratedTypeRoots = GetApiBaselineGeneratedTypeRoots(graph.Nodes); customPublicRoots.UnionWith(apiBaselineGeneratedTypeRoots); - var generatedPublicDeclarations = GetGeneratedPublicTypeDeclarations(providers, graph.Nodes); + var generatedPublicDeclarations = GetGeneratedPublicTypeDeclarations(generatedProviders, graph.Nodes); customPublicRoots.UnionWith(generatedPublicDeclarations); var customRemovalRoots = GetCustomCodeGeneratedTypeRoots(project, graph.Nodes); customRemovalRoots.UnionWith(apiBaselineGeneratedTypeRoots); customRemovalRoots.UnionWith(generatedPublicDeclarations); var customInternalDeclarations = GetCustomCodeInternalGeneratedTypeDeclarations(project, graph.Nodes); - var generatedInternalDeclarations = GetGeneratedInternalTypeDeclarations(providers, graph.Nodes); + var generatedInternalDeclarations = GetGeneratedInternalTypeDeclarations(generatedProviders, graph.Nodes); // Helper types are rooted after an initial reachability pass so unused infrastructure // such as change-tracking dictionaries can still be removed when no reachable type needs them. @@ -60,14 +60,14 @@ public static void Analyze(IReadOnlyList providers, Project projec AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences, internalizeReachableWithoutHelpers, generatedDiscriminatorBaseNames); internalizeReachableWithoutHelpers = GetReachableTypes(internalizeRoots, internalizeReferences); var publicizeRoots = internalizeRoots.ToHashSet(StringComparer.Ordinal); - var internalizeHelperRoots = GetHelperRootNames(providers, graph.Nodes, internalizeReachableWithoutHelpers); + var internalizeHelperRoots = GetHelperRootNames(generatedProviders, graph.Nodes, internalizeReachableWithoutHelpers); internalizeRoots.UnionWith(internalizeHelperRoots); var internalizeReachable = GetReachableTypes(internalizeRoots, internalizeReferences); - var internalizeDeclaredNodes = GetPostProcessorDeclaredNodes(providers, graph.Nodes, publicOnly: true); + var internalizeDeclaredNodes = GetPostProcessorDeclaredNodes(generatedProviders, graph.Nodes, publicOnly: true); var customInternalBoundaryNodes = graph.Nodes .Where(name => publicGraph.References.TryGetValue(name, out var references) && references.Overlaps(customInternalDeclarations)) .ToHashSet(StringComparer.Ordinal); - var publicizeDeclaredNodes = GetPostProcessorDeclaredNodes(providers, graph.Nodes, publicOnly: false) + var publicizeDeclaredNodes = GetPostProcessorDeclaredNodes(generatedProviders, graph.Nodes, publicOnly: false) .Except(internalizeDeclaredNodes, StringComparer.Ordinal); var generatedImplementationInternalDeclarations = GetGeneratedImplementationInternalTypeDeclarations(generatedInternalDeclarations).ToHashSet(StringComparer.Ordinal); var publicApiTraversalNodes = internalizeDeclaredNodes @@ -112,12 +112,12 @@ public static void Analyze(IReadOnlyList providers, Project projec var removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); AddDerivedModelReferences(providers, graph.Nodes, graph.References, removeReachableWithoutHelpers, generatedDiscriminatorBaseNames); removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); - AddBasePreservedReferences(providers, graph.Nodes, graph.References, removeReachableWithoutHelpers); - var removeHelperRoots = GetHelperRootNames(providers, graph.Nodes, removeReachableWithoutHelpers, graph.References); + AddBasePreservedReferences(generatedProviders, graph.Nodes, graph.References, removeReachableWithoutHelpers); + var removeHelperRoots = GetHelperRootNames(generatedProviders, graph.Nodes, removeReachableWithoutHelpers, graph.References); removeRoots.UnionWith(removeHelperRoots); var removeReachable = GetReachableTypes(removeRoots, graph.References); - AddBasePreservedReferences(providers, graph.Nodes, graph.References, removeReachable); - var removeDeclaredNodes = GetPostProcessorDeclaredNodes(providers, graph.Nodes, publicOnly: false); + AddBasePreservedReferences(generatedProviders, graph.Nodes, graph.References, removeReachable); + var removeDeclaredNodes = GetPostProcessorDeclaredNodes(generatedProviders, graph.Nodes, publicOnly: false); var removeCandidates = removeDeclaredNodes.Except(removeReachable, StringComparer.Ordinal).OrderBy(static name => name, StringComparer.Ordinal).ToArray(); var helperRoots = internalizeHelperRoots.Concat(removeHelperRoots).ToHashSet(StringComparer.Ordinal); @@ -441,10 +441,9 @@ private static void AddMatchingSymbolName(HashSet target, INamedTypeSymb } } - private static ProviderReferenceGraph BuildGraph(IReadOnlyList providers, bool publicOnly = false) + private static ProviderReferenceGraph BuildGraph(IReadOnlyList generatedProviders, bool publicOnly = false) { - var generatedProviders = GetGeneratedProviders(providers); - var serializationProviderNamesByType = providers + var serializationProviderNamesByType = generatedProviders .Where(static provider => provider.SerializationProviders.Count > 0) .ToDictionary( static provider => GetProviderTypeName(provider.Type), @@ -762,32 +761,12 @@ private static void AddGeneratedBodyReferences(Project project, IReadOnlyList 0 && !IsSerializationProvider(provider)) - { - continue; - } - var symbol = compilation.GetTypeByMetadataName(providerName); if (symbol == null) { continue; } - if (!IsSerializationProvider(provider)) - { - AddGeneratedReferencesToHelper(project, compilation, graph, providerName, symbol); - if (provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Static)) - { - foreach (var method in symbol.GetMembers().OfType()) - { - if (method.IsExtensionMethod) - { - AddGeneratedReferencesToHelper(project, compilation, graph, providerName, method); - } - } - } - } - AddGeneratedBodyTypeReferences(project, compilation, graph, providerName, symbol); } } @@ -875,7 +854,8 @@ private static void AddGeneratedXmlDocCrefReferences(Project project, ProviderRe } var xml = property.GetDocumentationCommentXml(); - if (string.IsNullOrEmpty(xml)) + if (string.IsNullOrEmpty(xml) || + !xml.Contains("cref=\"T:", StringComparison.Ordinal)) { continue; } @@ -911,34 +891,37 @@ private static void AddGeneratedBodyTypeReferences(Project project, Compilation var root = syntaxReference.SyntaxTree.GetRoot(); var semanticModel = compilation.GetSemanticModel(syntaxReference.SyntaxTree); - foreach (var typeSyntax in root.DescendantNodes().OfType()) + foreach (var node in root.DescendantNodes()) { - if (IsNamespaceOrUsingName(typeSyntax)) + if (node is TypeSyntax typeSyntax) { - continue; - } + if (IsNamespaceOrUsingName(typeSyntax)) + { + continue; + } - // Declaration names are the owner itself. The old Roslyn map captures references, - // not a declaration making itself reachable. - if (typeSyntax.Parent is BaseTypeDeclarationSyntax baseTypeDeclaration && baseTypeDeclaration.Identifier.Span == typeSyntax.Span) - { - continue; - } + // Declaration names are the owner itself. The old Roslyn map captures references, + // not a declaration making itself reachable. + if (typeSyntax.Parent is BaseTypeDeclarationSyntax baseTypeDeclaration && baseTypeDeclaration.Identifier.Span == typeSyntax.Span) + { + continue; + } - var typeInfo = semanticModel.GetTypeInfo(typeSyntax).Type; - AddBodyTypeReference(graph.References[ownerName], typeInfo, graph.Nodes); - AddUnresolvedBodyTypeSyntaxReference(graph.References[ownerName], typeSyntax, typeInfo, graph.Nodes); - } + var typeInfo = semanticModel.GetTypeInfo(typeSyntax).Type; + AddBodyTypeReference(graph.References[ownerName], typeInfo, graph.Nodes); + AddUnresolvedBodyTypeSyntaxReference(graph.References[ownerName], typeSyntax, typeInfo, graph.Nodes); + } - foreach (var invocation in root.DescendantNodes().OfType()) - { - if (semanticModel.GetSymbolInfo(invocation).Symbol is not IMethodSymbol method) + if (node is InvocationExpressionSyntax invocation) { - continue; - } + if (semanticModel.GetSymbolInfo(invocation).Symbol is not IMethodSymbol method) + { + continue; + } - AddBodyTypeReference(graph.References[ownerName], method.ContainingType, graph.Nodes); - AddBodyTypeReference(graph.References[ownerName], method.ReducedFrom?.ContainingType, graph.Nodes); + AddBodyTypeReference(graph.References[ownerName], method.ContainingType, graph.Nodes); + AddBodyTypeReference(graph.References[ownerName], method.ReducedFrom?.ContainingType, graph.Nodes); + } } } } @@ -1002,46 +985,6 @@ private static void AddBodyTypeReference(HashSet references, ITypeSymbol } } - private static void AddGeneratedReferencesToHelper(Project project, Compilation compilation, ProviderReferenceGraph graph, string helperName, ISymbol symbol) - { - foreach (var reference in SymbolFinder.FindReferencesAsync(symbol, project.Solution).GetAwaiter().GetResult()) - { - foreach (var location in reference.Locations) - { - var document = location.Document; - if (!IsGeneratedDocument(document)) - { - continue; - } - - var root = document.GetSyntaxRootAsync().GetAwaiter().GetResult(); - if (root == null) - { - continue; - } - - var node = root.FindNode(location.Location.SourceSpan); - var owner = node.AncestorsAndSelf().OfType().FirstOrDefault(); - if (owner == null) - { - continue; - } - - var semanticModel = compilation.GetSemanticModel(owner.SyntaxTree); - if (semanticModel.GetDeclaredSymbol(owner) is not INamedTypeSymbol ownerSymbol) - { - continue; - } - - var ownerName = ownerSymbol.GetFullyQualifiedName(); - if (graph.Nodes.Contains(ownerName)) - { - graph.References[ownerName].Add(helperName); - } - } - } - } - private static HashSet GetRootNames( IReadOnlyList providers, HashSet nodes, From 7b3c0f1e564b982958eb6fee379639887242e02c Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 24 Jun 2026 13:44:21 +0000 Subject: [PATCH 070/111] perf(http-client-csharp): scan generated bodies without helper symbol lookup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs index 8524e909359..1d3685cc469 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs @@ -791,11 +791,6 @@ private static void AddGeneratedBodyReferences(Project project, IReadOnlyList 0 && !IsSerializationProvider(provider)) - { - continue; - } - var symbol = compilation.GetTypeByMetadataName(providerName); if (symbol == null) { From 18b8131bc455283a3bda3dc2e91eadcc41291445 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 24 Jun 2026 14:36:14 +0000 Subject: [PATCH 071/111] fix(http-client-csharp): preserve generated client helpers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../mtg-hybrid-reference-map-2026-06-24-09-30-16.md | 7 ------- .../src/Providers/ClientProvider.cs | 13 +++++++++++++ .../src/Providers/RestClientProvider.cs | 5 ++++- 3 files changed, 17 insertions(+), 8 deletions(-) delete mode 100644 .chronus/changes/mtg-hybrid-reference-map-2026-06-24-09-30-16.md diff --git a/.chronus/changes/mtg-hybrid-reference-map-2026-06-24-09-30-16.md b/.chronus/changes/mtg-hybrid-reference-map-2026-06-24-09-30-16.md deleted file mode 100644 index 8e0813e5c3d..00000000000 --- a/.chronus/changes/mtg-hybrid-reference-map-2026-06-24-09-30-16.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -changeKind: fix -packages: - - "@typespec/http-client-csharp" ---- - -Preserve generated types that are kept because their base type is reachable when removing unreferenced C# types. diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs index 0e695f9e7f8..389caf56c0c 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs @@ -424,6 +424,19 @@ private IReadOnlyList GetClientParameters() protected override string BuildName() => _inputClient.IsExactName ? _inputClient.Name : _inputClient.Name.ToIdentifierName(); + protected override IReadOnlyList BuildHelperDependencyNames() + { + foreach (var method in Methods.OfType()) + { + if (method.BodyStatements != null) + { + return ["CancellationTokenExtensions", "ClientPipelineExtensions"]; + } + } + + return []; + } + protected override IReadOnlyList BuildBodyDependencyTypes() { var dependencies = new List(); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs index 8cc5fea2368..47df6c139de 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs @@ -80,7 +80,10 @@ protected override FieldProvider[] BuildFields() protected override IReadOnlyList BuildHelperDependencyNames() { - var dependencies = new HashSet(StringComparer.Ordinal); + var dependencies = new HashSet(StringComparer.Ordinal) + { + "ClientUriBuilder" + }; foreach (var serviceMethod in _inputClient.Methods) { foreach (var parameter in serviceMethod.Operation.Parameters) From b14b1f282a874709b6588031193fb87327af4ad9 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 24 Jun 2026 14:36:14 +0000 Subject: [PATCH 072/111] fix(http-client-csharp): preserve generated client helpers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/Providers/ClientProvider.cs | 13 +++++++++++++ .../src/Providers/RestClientProvider.cs | 5 ++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs index 0e695f9e7f8..389caf56c0c 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs @@ -424,6 +424,19 @@ private IReadOnlyList GetClientParameters() protected override string BuildName() => _inputClient.IsExactName ? _inputClient.Name : _inputClient.Name.ToIdentifierName(); + protected override IReadOnlyList BuildHelperDependencyNames() + { + foreach (var method in Methods.OfType()) + { + if (method.BodyStatements != null) + { + return ["CancellationTokenExtensions", "ClientPipelineExtensions"]; + } + } + + return []; + } + protected override IReadOnlyList BuildBodyDependencyTypes() { var dependencies = new List(); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs index 8cc5fea2368..47df6c139de 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs @@ -80,7 +80,10 @@ protected override FieldProvider[] BuildFields() protected override IReadOnlyList BuildHelperDependencyNames() { - var dependencies = new HashSet(StringComparer.Ordinal); + var dependencies = new HashSet(StringComparer.Ordinal) + { + "ClientUriBuilder" + }; foreach (var serviceMethod in _inputClient.Methods) { foreach (var parameter in serviceMethod.Operation.Parameters) From d58bebe4797b489c5fdcf4125f7d9b23f7955688 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 25 Jun 2026 03:28:08 +0000 Subject: [PATCH 073/111] fix(http-client-csharp): add braces for cop check Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/PostProcessing/PostProcessor.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs index 73b8219d89a..69694438fdb 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs @@ -833,7 +833,9 @@ private async Task RemoveInvalidXmlDocReferences(Solution solution, Do var model = await document.GetSemanticModelAsync(); if (root == null || model == null) + { return solution; + } var invalidSeeElements = root.DescendantTrivia(descendIntoTrivia: true) .SelectMany(static trivia => trivia.GetStructure()?.DescendantNodes().OfType() ?? []) @@ -842,7 +844,9 @@ private async Task RemoveInvalidXmlDocReferences(Solution solution, Do .ToArray(); if (invalidSeeElements.Length == 0) + { return solution; + } var text = await document.GetTextAsync(); var source = text.ToString(); From 6fe237f3ba2463b2f89c7e2013655f665ebe202b Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 25 Jun 2026 06:27:52 +0000 Subject: [PATCH 074/111] fix(http-client-csharp): preserve provider map signature reachability Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ClientBodyDependencyPostProcessingTests.cs | 53 ++++++++++++++++++- .../ProviderReferenceMapAnalyzer.cs | 29 +++++----- 2 files changed, 64 insertions(+), 18 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs index 0d7f1063e9b..52f9550a383 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs @@ -86,6 +86,54 @@ public async Task PublicModelSignatureDependencyIsPromotedToPublic() await GenerateAndAssertPublicModels([responseModel, internalDependency], [client], ["ResponseBody", "InternalDependency"]); } + [Test] + public async Task AzureClientPublicMethodSignatureReferencesStayPublic() + { + var signatureModel = InputFactory.Model("SignatureModel", @namespace: "Azure.Sample.Models"); + var methodParameter = InputFactory.MethodParameter("signature", signatureModel, isRequired: true); + var operation = InputFactory.Operation( + "Create", + parameters: [InputFactory.BodyParameter("signature", signatureModel, isRequired: true)], + httpMethod: "POST"); + var method = InputFactory.BasicServiceMethod("Create", operation, parameters: [methodParameter]); + var client = InputFactory.Client("SampleClient", clientNamespace: "Azure.Sample", methods: [method]); + + await GenerateAndAssertFiles( + enums: [], + models: [signatureModel], + clients: [client], + customFiles: [], + expectedFiles: [], + publicModelNames: ["SignatureModel"], + packageName: "Azure.Sample"); + } + + [Test] + public async Task BasePreservedDerivedModelTraversesTransitiveDependencies() + { + var transitiveDependency = InputFactory.Model("TransitiveDependency"); + var dependency = InputFactory.Model( + "DerivedDependency", + properties: [InputFactory.Property("Transitive", transitiveDependency)]); + var baseModel = InputFactory.Model("BaseResult"); + var derivedModel = InputFactory.Model( + "DerivedResult", + properties: [InputFactory.Property("Dependency", dependency)], + baseModel: baseModel); + var operation = InputFactory.Operation("Get", responses: [InputFactory.OperationResponse(bodytype: baseModel)]); + var method = InputFactory.BasicServiceMethod("Get", operation, response: InputFactory.ServiceMethodResponse(baseModel, [])); + var client = InputFactory.Client("TestClient", methods: [method]); + + await GenerateAndAssertFiles( + enums: [], + models: [baseModel, derivedModel, dependency, transitiveDependency], + clients: [client], + customFiles: [], + expectedFiles: [], + publicModelNames: ["BaseResult"], + internalModelNames: ["DerivedResult", "DerivedDependency", "TransitiveDependency"]); + } + [Test] public async Task CustomizedEnumSerializationProviderIsKeptWhenModelSerializationUsesEnum() { @@ -200,7 +248,8 @@ private static async Task GenerateAndAssertFiles( (string Path, string Content)[] customFiles, string[] expectedFiles, string[] publicModelNames = null!, - string[] internalModelNames = null!) + string[] internalModelNames = null!, + string packageName = "Sample") { publicModelNames ??= []; internalModelNames ??= []; @@ -220,7 +269,7 @@ await MockHelpers.LoadMockGeneratorAsync( inputEnums: () => enums, inputModels: () => models, clients: () => clients, - configuration: "{\"package-name\": \"Sample\", \"disable-xml-docs\": true}", + configuration: $$"""{ "package-name": "{{packageName}}", "disable-xml-docs": true }""", outputPath: outputPath); await new CSharpGen().ExecuteAsync(); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index d65a1d1eb13..60671c5eaad 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -539,11 +539,6 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList gen foreach (var method in provider.Methods) { - if (ShouldUseGeneratedSourceReferences(provider)) - { - continue; - } - if (method.IsMethodSuppressed()) { continue; @@ -555,7 +550,7 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList gen } AddSignatureReferences(references[current], method.Signature, nodes, serializationReferenceNamesByType, includeAttributes: !publicOnly); - if (!publicOnly) + if (!publicOnly && !ShouldUseGeneratedSourceReferences(provider)) { AddTypeReference(references[current], GetCollectionDefinitionType(method), nodes, serializationReferenceNamesByType); } @@ -677,14 +672,15 @@ private static void AddBasePreservedReferences( IReadOnlyDictionary> references, HashSet reachableTypes) { - var addedReference = true; - while (addedReference) + var basePreservedRoots = new HashSet(StringComparer.Ordinal); + var addedRoot = true; + while (addedRoot) { - addedReference = false; + addedRoot = false; foreach (var provider in GetGeneratedProviders(providers)) { var providerName = GetProviderTypeName(provider.Type); - if (!nodes.Contains(providerName) || reachableTypes.Contains(providerName)) + if (!nodes.Contains(providerName) || reachableTypes.Contains(providerName) || basePreservedRoots.Contains(providerName)) { continue; } @@ -695,15 +691,16 @@ private static void AddBasePreservedReferences( continue; } - reachableTypes.Add(providerName); - foreach (var reference in references[providerName]) + if (basePreservedRoots.Add(providerName)) { - if (reachableTypes.Add(reference)) - { - addedReference = true; - } + addedRoot = true; } } + + if (addedRoot) + { + reachableTypes.UnionWith(GetReachableTypes(basePreservedRoots, references)); + } } } From b3a872f39ece3edd36c223bbb401bf5d3128d959 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 25 Jun 2026 06:27:52 +0000 Subject: [PATCH 075/111] fix(http-client-csharp): preserve provider map signature reachability Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ClientBodyDependencyPostProcessingTests.cs | 53 ++++++++++++++++++- .../ProviderReferenceMapAnalyzer.cs | 29 +++++----- 2 files changed, 64 insertions(+), 18 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs index 0d7f1063e9b..52f9550a383 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs @@ -86,6 +86,54 @@ public async Task PublicModelSignatureDependencyIsPromotedToPublic() await GenerateAndAssertPublicModels([responseModel, internalDependency], [client], ["ResponseBody", "InternalDependency"]); } + [Test] + public async Task AzureClientPublicMethodSignatureReferencesStayPublic() + { + var signatureModel = InputFactory.Model("SignatureModel", @namespace: "Azure.Sample.Models"); + var methodParameter = InputFactory.MethodParameter("signature", signatureModel, isRequired: true); + var operation = InputFactory.Operation( + "Create", + parameters: [InputFactory.BodyParameter("signature", signatureModel, isRequired: true)], + httpMethod: "POST"); + var method = InputFactory.BasicServiceMethod("Create", operation, parameters: [methodParameter]); + var client = InputFactory.Client("SampleClient", clientNamespace: "Azure.Sample", methods: [method]); + + await GenerateAndAssertFiles( + enums: [], + models: [signatureModel], + clients: [client], + customFiles: [], + expectedFiles: [], + publicModelNames: ["SignatureModel"], + packageName: "Azure.Sample"); + } + + [Test] + public async Task BasePreservedDerivedModelTraversesTransitiveDependencies() + { + var transitiveDependency = InputFactory.Model("TransitiveDependency"); + var dependency = InputFactory.Model( + "DerivedDependency", + properties: [InputFactory.Property("Transitive", transitiveDependency)]); + var baseModel = InputFactory.Model("BaseResult"); + var derivedModel = InputFactory.Model( + "DerivedResult", + properties: [InputFactory.Property("Dependency", dependency)], + baseModel: baseModel); + var operation = InputFactory.Operation("Get", responses: [InputFactory.OperationResponse(bodytype: baseModel)]); + var method = InputFactory.BasicServiceMethod("Get", operation, response: InputFactory.ServiceMethodResponse(baseModel, [])); + var client = InputFactory.Client("TestClient", methods: [method]); + + await GenerateAndAssertFiles( + enums: [], + models: [baseModel, derivedModel, dependency, transitiveDependency], + clients: [client], + customFiles: [], + expectedFiles: [], + publicModelNames: ["BaseResult"], + internalModelNames: ["DerivedResult", "DerivedDependency", "TransitiveDependency"]); + } + [Test] public async Task CustomizedEnumSerializationProviderIsKeptWhenModelSerializationUsesEnum() { @@ -200,7 +248,8 @@ private static async Task GenerateAndAssertFiles( (string Path, string Content)[] customFiles, string[] expectedFiles, string[] publicModelNames = null!, - string[] internalModelNames = null!) + string[] internalModelNames = null!, + string packageName = "Sample") { publicModelNames ??= []; internalModelNames ??= []; @@ -220,7 +269,7 @@ await MockHelpers.LoadMockGeneratorAsync( inputEnums: () => enums, inputModels: () => models, clients: () => clients, - configuration: "{\"package-name\": \"Sample\", \"disable-xml-docs\": true}", + configuration: $$"""{ "package-name": "{{packageName}}", "disable-xml-docs": true }""", outputPath: outputPath); await new CSharpGen().ExecuteAsync(); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index d65a1d1eb13..60671c5eaad 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -539,11 +539,6 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList gen foreach (var method in provider.Methods) { - if (ShouldUseGeneratedSourceReferences(provider)) - { - continue; - } - if (method.IsMethodSuppressed()) { continue; @@ -555,7 +550,7 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList gen } AddSignatureReferences(references[current], method.Signature, nodes, serializationReferenceNamesByType, includeAttributes: !publicOnly); - if (!publicOnly) + if (!publicOnly && !ShouldUseGeneratedSourceReferences(provider)) { AddTypeReference(references[current], GetCollectionDefinitionType(method), nodes, serializationReferenceNamesByType); } @@ -677,14 +672,15 @@ private static void AddBasePreservedReferences( IReadOnlyDictionary> references, HashSet reachableTypes) { - var addedReference = true; - while (addedReference) + var basePreservedRoots = new HashSet(StringComparer.Ordinal); + var addedRoot = true; + while (addedRoot) { - addedReference = false; + addedRoot = false; foreach (var provider in GetGeneratedProviders(providers)) { var providerName = GetProviderTypeName(provider.Type); - if (!nodes.Contains(providerName) || reachableTypes.Contains(providerName)) + if (!nodes.Contains(providerName) || reachableTypes.Contains(providerName) || basePreservedRoots.Contains(providerName)) { continue; } @@ -695,15 +691,16 @@ private static void AddBasePreservedReferences( continue; } - reachableTypes.Add(providerName); - foreach (var reference in references[providerName]) + if (basePreservedRoots.Add(providerName)) { - if (reachableTypes.Add(reference)) - { - addedReference = true; - } + addedRoot = true; } } + + if (addedRoot) + { + reachableTypes.UnionWith(GetReachableTypes(basePreservedRoots, references)); + } } } From f4a1bbbec0cc479e87e91f0a5f0495766a3e25e3 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 25 Jun 2026 06:44:09 +0000 Subject: [PATCH 076/111] perf(http-client-csharp): align benchmark provider map fix Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ClientBodyDependencyPostProcessingTests.cs | 53 +------------------ .../ProviderReferenceMapShadowAnalyzer.cs | 26 ++++----- 2 files changed, 16 insertions(+), 63 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs index 52f9550a383..0d7f1063e9b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs @@ -86,54 +86,6 @@ public async Task PublicModelSignatureDependencyIsPromotedToPublic() await GenerateAndAssertPublicModels([responseModel, internalDependency], [client], ["ResponseBody", "InternalDependency"]); } - [Test] - public async Task AzureClientPublicMethodSignatureReferencesStayPublic() - { - var signatureModel = InputFactory.Model("SignatureModel", @namespace: "Azure.Sample.Models"); - var methodParameter = InputFactory.MethodParameter("signature", signatureModel, isRequired: true); - var operation = InputFactory.Operation( - "Create", - parameters: [InputFactory.BodyParameter("signature", signatureModel, isRequired: true)], - httpMethod: "POST"); - var method = InputFactory.BasicServiceMethod("Create", operation, parameters: [methodParameter]); - var client = InputFactory.Client("SampleClient", clientNamespace: "Azure.Sample", methods: [method]); - - await GenerateAndAssertFiles( - enums: [], - models: [signatureModel], - clients: [client], - customFiles: [], - expectedFiles: [], - publicModelNames: ["SignatureModel"], - packageName: "Azure.Sample"); - } - - [Test] - public async Task BasePreservedDerivedModelTraversesTransitiveDependencies() - { - var transitiveDependency = InputFactory.Model("TransitiveDependency"); - var dependency = InputFactory.Model( - "DerivedDependency", - properties: [InputFactory.Property("Transitive", transitiveDependency)]); - var baseModel = InputFactory.Model("BaseResult"); - var derivedModel = InputFactory.Model( - "DerivedResult", - properties: [InputFactory.Property("Dependency", dependency)], - baseModel: baseModel); - var operation = InputFactory.Operation("Get", responses: [InputFactory.OperationResponse(bodytype: baseModel)]); - var method = InputFactory.BasicServiceMethod("Get", operation, response: InputFactory.ServiceMethodResponse(baseModel, [])); - var client = InputFactory.Client("TestClient", methods: [method]); - - await GenerateAndAssertFiles( - enums: [], - models: [baseModel, derivedModel, dependency, transitiveDependency], - clients: [client], - customFiles: [], - expectedFiles: [], - publicModelNames: ["BaseResult"], - internalModelNames: ["DerivedResult", "DerivedDependency", "TransitiveDependency"]); - } - [Test] public async Task CustomizedEnumSerializationProviderIsKeptWhenModelSerializationUsesEnum() { @@ -248,8 +200,7 @@ private static async Task GenerateAndAssertFiles( (string Path, string Content)[] customFiles, string[] expectedFiles, string[] publicModelNames = null!, - string[] internalModelNames = null!, - string packageName = "Sample") + string[] internalModelNames = null!) { publicModelNames ??= []; internalModelNames ??= []; @@ -269,7 +220,7 @@ await MockHelpers.LoadMockGeneratorAsync( inputEnums: () => enums, inputModels: () => models, clients: () => clients, - configuration: $$"""{ "package-name": "{{packageName}}", "disable-xml-docs": true }""", + configuration: "{\"package-name\": \"Sample\", \"disable-xml-docs\": true}", outputPath: outputPath); await new CSharpGen().ExecuteAsync(); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs index 1d3685cc469..4357b69256f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs @@ -574,7 +574,7 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList gen foreach (var method in provider.Methods) { - if (ShouldUseGeneratedSourceReferences(provider)) + if (method.IsMethodSuppressed()) { continue; } @@ -585,7 +585,7 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList gen } AddSignatureReferences(references[current], method.Signature, nodes, serializationReferenceNamesByType, includeAttributes: !publicOnly); - if (!publicOnly) + if (!publicOnly && !ShouldUseGeneratedSourceReferences(provider)) { AddTypeReference(references[current], GetCollectionDefinitionType(method), nodes, serializationReferenceNamesByType); } @@ -707,14 +707,15 @@ private static void AddBasePreservedReferences( IReadOnlyDictionary> references, HashSet reachableTypes) { - var addedReference = true; - while (addedReference) + var basePreservedRoots = new HashSet(StringComparer.Ordinal); + var addedRoot = true; + while (addedRoot) { - addedReference = false; + addedRoot = false; foreach (var provider in generatedProviders) { var providerName = GetProviderTypeName(provider.Type); - if (!nodes.Contains(providerName) || reachableTypes.Contains(providerName)) + if (!nodes.Contains(providerName) || reachableTypes.Contains(providerName) || basePreservedRoots.Contains(providerName)) { continue; } @@ -725,15 +726,16 @@ private static void AddBasePreservedReferences( continue; } - reachableTypes.Add(providerName); - foreach (var reference in references[providerName]) + if (basePreservedRoots.Add(providerName)) { - if (reachableTypes.Add(reference)) - { - addedReference = true; - } + addedRoot = true; } } + + if (addedRoot) + { + reachableTypes.UnionWith(GetReachableTypes(basePreservedRoots, references)); + } } } From 70bc6eafb0eb1af3ba9fe2701c048d8e6146d6eb Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 25 Jun 2026 09:12:45 +0000 Subject: [PATCH 077/111] fix(http-client-csharp): preserve custom array signature roots Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ClientBodyDependencyPostProcessingTests.cs | 25 +++++++++++++++++++ .../ProviderReferenceMapAnalyzer.cs | 6 +++++ 2 files changed, 31 insertions(+) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs index 52f9550a383..48ffd1a34d2 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs @@ -134,6 +134,31 @@ await GenerateAndAssertFiles( internalModelNames: ["DerivedResult", "DerivedDependency", "TransitiveDependency"]); } + [Test] + public async Task PublicCustomCodeArraySignatureReferencesStayPublic() + { + var generatedModel = InputFactory.Model("GeneratedModel"); + + await GenerateAndAssertFiles( + enums: [], + models: [generatedModel], + clients: [], + customFiles: [ + (Path.Combine("src", "PublicCustomApi.cs"), """ + using Sample.Models; + + namespace Sample; + + public partial class PublicCustomApi + { + public GeneratedModel[] Items { get; } = System.Array.Empty(); + } + """) + ], + expectedFiles: [], + publicModelNames: ["GeneratedModel"]); + } + [Test] public async Task CustomizedEnumSerializationProviderIsKeptWhenModelSerializationUsesEnum() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 60671c5eaad..2bc29a5b76a 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -415,6 +415,12 @@ private static IEnumerable GetGeneratedImplementationInternalTypeDeclara private static void AddSymbolRoot(HashSet roots, ITypeSymbol? symbol, HashSet generatedTypeNames) { + if (symbol is IArrayTypeSymbol arrayType) + { + AddSymbolRoot(roots, arrayType.ElementType, generatedTypeNames); + return; + } + if (symbol is not INamedTypeSymbol namedType) { return; From a2e86c21bd9d7af7e7bbee41ae57e411817ca603 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 25 Jun 2026 12:31:13 +0000 Subject: [PATCH 078/111] Fix hybrid reference map edge cases Handle duplicate generated providers, avoid namespace-blind API baseline roots, and keep collection result constructors internal so implementation-only REST clients are not publicized. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Providers/CollectionResultDefinition.cs | 2 +- .../ProviderReferenceMapAnalyzer.cs | 43 ++++++++++++++++--- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/CollectionResultDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/CollectionResultDefinition.cs index 590eaf2b935..9dc9b51afd3 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/CollectionResultDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/CollectionResultDefinition.cs @@ -256,7 +256,7 @@ protected override ConstructorProvider[] BuildConstructors() new ConstructorSignature( Type, $"Initializes a new instance of {Name}, which is used to iterate over the pages of a collection.", - MethodSignatureModifiers.Public, + MethodSignatureModifiers.Internal, [ clientParameter, .. CreateRequestParameters diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 2bc29a5b76a..b03cc130726 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -263,11 +263,12 @@ private static HashSet GetApiBaselineGeneratedTypeRoots(HashSet } var apiText = string.Join("\n", Directory.GetFiles(apiDirectory, "*.cs", SearchOption.AllDirectories).Select(File.ReadAllText)); + var apiDeclaredTypeNames = GetApiDeclaredTypeNames(apiText); foreach (var fullName in generatedTypeNames) { var simpleName = StripGenericArity(GetSimpleName(fullName)); var normalizedFullName = StripGenericArity(fullName); - if (!ContainsApiTypeReference(apiText, normalizedFullName, simpleName)) + if (!ContainsApiTypeReference(apiText, apiDeclaredTypeNames, normalizedFullName, simpleName)) { continue; } @@ -278,7 +279,35 @@ private static HashSet GetApiBaselineGeneratedTypeRoots(HashSet return roots; } - private static bool ContainsApiTypeReference(string apiText, string fullName, string simpleName) + private static HashSet GetApiDeclaredTypeNames(string apiText) + { + var declaredTypeNames = new HashSet(StringComparer.Ordinal); + string? currentNamespace = null; + foreach (var line in apiText.Split('\n')) + { + var namespaceMatch = Regex.Match(line, @"^namespace\s+([\w.]+)\s*\{?\s*$"); + if (namespaceMatch.Success) + { + currentNamespace = namespaceMatch.Groups[1].Value; + continue; + } + + if (currentNamespace == null) + { + continue; + } + + var declarationMatch = Regex.Match(line, @"^ \S.*?\b(class|struct|interface|enum)\s+([A-Za-z_][A-Za-z0-9_]*)(?!\s*<)(?!\w)"); + if (declarationMatch.Success) + { + declaredTypeNames.Add($"{currentNamespace}.{declarationMatch.Groups[2].Value}"); + } + } + + return declaredTypeNames; + } + + private static bool ContainsApiTypeReference(string apiText, HashSet apiDeclaredTypeNames, string fullName, string simpleName) { var fullNamePattern = $@"(? gen { var serializationProviderNamesByType = generatedProviders .Where(static provider => provider.SerializationProviders.Count > 0) + .GroupBy(static provider => GetProviderTypeName(provider.Type), StringComparer.Ordinal) .ToDictionary( - static provider => GetProviderTypeName(provider.Type), - static provider => provider.SerializationProviders + static group => group.Key, + static group => group + .SelectMany(static provider => provider.SerializationProviders) .Select(static serializationProvider => GetProviderTypeName(serializationProvider.Type)) + .Distinct(StringComparer.Ordinal) .ToArray(), StringComparer.Ordinal); IReadOnlyDictionary? serializationReferenceNamesByType = publicOnly ? null : serializationProviderNamesByType; From 488625125a11ee593bc7c3100902f52f755390c2 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 25 Jun 2026 13:32:47 +0000 Subject: [PATCH 079/111] fix(http-client-csharp): keep paging collection constructors public Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/Providers/CollectionResultDefinition.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/CollectionResultDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/CollectionResultDefinition.cs index 9dc9b51afd3..590eaf2b935 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/CollectionResultDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/CollectionResultDefinition.cs @@ -256,7 +256,7 @@ protected override ConstructorProvider[] BuildConstructors() new ConstructorSignature( Type, $"Initializes a new instance of {Name}, which is used to iterate over the pages of a collection.", - MethodSignatureModifiers.Internal, + MethodSignatureModifiers.Public, [ clientParameter, .. CreateRequestParameters From da2ded3b4fe9d637bb6efcc4b9d02e4eb270f53b Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Mon, 29 Jun 2026 08:07:15 +0000 Subject: [PATCH 080/111] perf(csharp): pre-write internalize provider reference map Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...rewrite-internalization-2026-06-29-0759.md | 7 + .../src/CSharpGen.cs | 6 + .../PostProcessing/GeneratedCodeWorkspace.cs | 15 +- .../ProviderReferenceMapAnalyzer.cs | 359 ++++++++++++++++++ .../src/Primitives/TypeProviderWriter.cs | 2 +- .../src/Providers/TypeProvider.cs | 8 + 6 files changed, 394 insertions(+), 3 deletions(-) create mode 100644 .chronus/changes/mtg-hybrid-reference-map-prewrite-internalization-2026-06-29-0759.md diff --git a/.chronus/changes/mtg-hybrid-reference-map-prewrite-internalization-2026-06-29-0759.md b/.chronus/changes/mtg-hybrid-reference-map-prewrite-internalization-2026-06-29-0759.md new file mode 100644 index 00000000000..617dc8cd866 --- /dev/null +++ b/.chronus/changes/mtg-hybrid-reference-map-prewrite-internalization-2026-06-29-0759.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@typespec/http-client-csharp" +--- + +Improve C# generator post-processing performance by using provider-based reference-map construction with pre-write internalization. diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index 2eda55fcb61..48bcb174bb9 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs @@ -37,6 +37,7 @@ public async Task ExecuteAsync() { CodeModelGenerator.Instance.Emitter.Info("Starting code generation"); CodeModelGenerator.Instance.Stopwatch.Start(); + ProviderReferenceMapAnalyzer.ResetPreWriteAccessibility(); var outputPath = CodeModelGenerator.Instance.Configuration.OutputDirectory; var generatedSourceOutputPath = CodeModelGenerator.Instance.Configuration.ProjectGeneratedDirectory; @@ -100,7 +101,12 @@ await GeneratedCodeWorkspace.LoadBaselineContract(), { // Ensure back-compatibility processing is done after all visitors have run outputType.ProcessTypeForBackCompatibility(); + } + + generatedCodeWorkspace.ApplyPreWriteAccessibility(output.TypeProviders); + foreach (var outputType in output.TypeProviders) + { var writer = CodeModelGenerator.Instance.GetWriter(outputType); generateFilesTasks.Add(generatedCodeWorkspace.AddGeneratedFile(writer.Write())); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index b1c3405dcd8..967a0870a87 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -89,6 +89,11 @@ internal void AnalyzeProviderReferenceMap(IReadOnlyList providers) ProviderReferenceMapAnalyzer.Analyze(providers, _project); } + internal void ApplyPreWriteAccessibility(IReadOnlyList providers) + { + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility(providers, _project); + } + private async Task UpdateProject(Document document) { var root = await document.GetSyntaxRootAsync(); @@ -284,10 +289,16 @@ public async Task PostProcessAsync() case Configuration.UnreferencedTypesHandlingOption.KeepAll: break; case Configuration.UnreferencedTypesHandlingOption.Internalize: - _project = await postProcessor.InternalizeAsync(_project); + if (!ProviderReferenceMapAnalyzer.PreWriteAccessibilityApplied) + { + _project = await postProcessor.InternalizeAsync(_project); + } break; case Configuration.UnreferencedTypesHandlingOption.RemoveOrInternalize: - _project = await postProcessor.InternalizeAsync(_project); + if (!ProviderReferenceMapAnalyzer.PreWriteAccessibilityApplied) + { + _project = await postProcessor.InternalizeAsync(_project); + } _project = await postProcessor.RemoveAsync(_project); break; } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index b03cc130726..b357901de79 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -13,6 +13,7 @@ using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.Statements; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Microsoft.TypeSpec.Generator @@ -23,6 +24,39 @@ internal static class ProviderReferenceMapAnalyzer private static readonly ConditionalWeakTable, Dictionary> _simpleNameLookupCache = new(); public static ProviderReferenceMapResult? LatestResult => _latestResult; + public static bool PreWriteAccessibilityApplied { get; private set; } + + public static void ResetPreWriteAccessibility() + { + PreWriteAccessibilityApplied = false; + } + + public static void ApplyPreWriteAccessibility(IReadOnlyList providers, Project customCodeProject) + { + PreWriteAccessibilityApplied = false; + if (Configuration.UnreferencedTypesHandling == Configuration.UnreferencedTypesHandlingOption.KeepAll) + { + return; + } + + var (internalizeCandidates, publicizeCandidates) = GetPreWriteAccessibilityCandidates(providers, customCodeProject); + foreach (var provider in GetGeneratedProviders(providers)) + { + var providerName = GetProviderTypeName(provider.Type); + if (internalizeCandidates.Contains(providerName)) + { + provider.PreserveXmlDocs(); + provider.Update(modifiers: MakeInternal(provider.DeclarationModifiers)); + } + else if (publicizeCandidates.Contains(providerName)) + { + provider.Update(modifiers: MakePublic(provider.DeclarationModifiers)); + } + } + + RemoveMethodsFromModelFactory(internalizeCandidates.Select(GetSimpleName).ToHashSet(StringComparer.Ordinal)); + PreWriteAccessibilityApplied = true; + } public static void Analyze(IReadOnlyList providers, Project project) { @@ -128,6 +162,68 @@ public static void Analyze(IReadOnlyList providers, Project projec removeCandidates.ToHashSet(StringComparer.Ordinal)); } + private static (HashSet InternalizeCandidates, HashSet PublicizeCandidates) GetPreWriteAccessibilityCandidates(IReadOnlyList providers, Project customCodeProject) + { + var generatedProviders = GetGeneratedProviders(providers); + var graph = BuildGraph(generatedProviders); + var publicGraph = BuildGraph(generatedProviders, publicOnly: true); + var customPublicRoots = GetCustomCodePublicGeneratedTypeRoots(generatedProviders, graph.Nodes); + customPublicRoots.UnionWith(GetCustomCodePublicGeneratedTypeRootsFromSyntax(customCodeProject, graph.Nodes)); + var apiBaselineGeneratedTypeRoots = GetApiBaselineGeneratedTypeRoots(graph.Nodes); + customPublicRoots.UnionWith(apiBaselineGeneratedTypeRoots); + var generatedPublicDeclarations = GetGeneratedPublicTypeDeclarations(generatedProviders, graph.Nodes); + customPublicRoots.UnionWith(generatedPublicDeclarations); + var customInternalDeclarations = GetCustomCodeInternalGeneratedTypeDeclarations(generatedProviders, graph.Nodes); + var generatedInternalDeclarations = GetGeneratedInternalTypeDeclarations(generatedProviders, graph.Nodes); + var generatedDiscriminatorBaseNames = new HashSet(StringComparer.Ordinal); + + var internalizeReferences = CloneReferences(publicGraph.References); + var internalizeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: false, includeAdditionalRoots: true, includeUnionVariantRoots: false, publicClientRootsOnly: true); + if (ShouldUseUnionVariantFallbackRoots()) + { + AddUnionVariantRoots(internalizeRoots, providers, graph.Nodes); + } + + var generatedPublicReachable = GetReachableTypes(internalizeRoots, internalizeReferences); + AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences, generatedPublicReachable, generatedDiscriminatorBaseNames); + internalizeRoots.UnionWith(customPublicRoots); + var internalizeReachableWithoutHelpers = GetReachableTypes(internalizeRoots, internalizeReferences); + AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences, internalizeReachableWithoutHelpers, generatedDiscriminatorBaseNames); + internalizeReachableWithoutHelpers = GetReachableTypes(internalizeRoots, internalizeReferences); + var publicizeRoots = internalizeRoots.ToHashSet(StringComparer.Ordinal); + var internalizeHelperRoots = GetHelperRootNames(generatedProviders, graph.Nodes, internalizeReachableWithoutHelpers); + internalizeRoots.UnionWith(internalizeHelperRoots); + var internalizeDeclaredNodes = GetPostProcessorDeclaredNodes(generatedProviders, graph.Nodes, publicOnly: true); + var customInternalBoundaryNodes = graph.Nodes + .Where(name => publicGraph.References.TryGetValue(name, out var references) && references.Overlaps(customInternalDeclarations)) + .ToHashSet(StringComparer.Ordinal); + var publicizeDeclaredNodes = GetPostProcessorDeclaredNodes(generatedProviders, graph.Nodes, publicOnly: false) + .Except(internalizeDeclaredNodes, StringComparer.Ordinal); + var generatedImplementationInternalDeclarations = GetGeneratedImplementationInternalTypeDeclarations(generatedInternalDeclarations).ToHashSet(StringComparer.Ordinal); + var publicApiTraversalNodes = internalizeDeclaredNodes + .Except(generatedInternalDeclarations, StringComparer.Ordinal) + .Concat(publicizeDeclaredNodes) + .Except(generatedImplementationInternalDeclarations, StringComparer.Ordinal) + .ToHashSet(StringComparer.Ordinal); + var publicizeReachable = GetReachableTypes(publicizeRoots, internalizeReferences, publicApiTraversalNodes); + var internalizeCandidates = internalizeDeclaredNodes + .Except(publicizeReachable, StringComparer.Ordinal) + .Union(internalizeDeclaredNodes.Intersect(customInternalBoundaryNodes, StringComparer.Ordinal), StringComparer.Ordinal) + .ToHashSet(StringComparer.Ordinal); + var publicizeCandidates = publicizeDeclaredNodes + .Except(customInternalDeclarations, StringComparer.Ordinal) + .Except(customInternalBoundaryNodes, StringComparer.Ordinal) + .Except(internalizeHelperRoots, StringComparer.Ordinal) + .Except(GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: true, includeAdditionalRoots: true, includeUnionVariantRoots: true, publicClientRootsOnly: true), StringComparer.Ordinal) + .Intersect(publicizeReachable, StringComparer.Ordinal) + .Where(name => !generatedInternalDeclarations.Contains(name) || publicizeRoots.Contains(name)) + .Where(name => publicizeRoots.Contains(name) || + HasPublicApiPredecessor(name, internalizeReferences, publicizeReachable, generatedImplementationInternalDeclarations)) + .ToHashSet(StringComparer.Ordinal); + + return (internalizeCandidates, publicizeCandidates); + } + private static HashSet GetCustomCodeGeneratedTypeRoots(Project project, HashSet generatedTypeNames) { var roots = new HashSet(StringComparer.Ordinal); @@ -247,6 +343,226 @@ private static HashSet GetCustomCodePublicGeneratedTypeRoots(Project pro return roots; } + private static HashSet GetCustomCodePublicGeneratedTypeRoots(IReadOnlyList providers, HashSet generatedTypeNames) + { + var roots = new HashSet(StringComparer.Ordinal); + foreach (var customCodeView in GetCustomCodeViews(providers)) + { + if (!customCodeView.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) + { + continue; + } + + AddCustomCodeViewRoots(roots, customCodeView, generatedTypeNames, publicOnly: true); + } + + return roots; + } + + private static HashSet GetCustomCodePublicGeneratedTypeRootsFromSyntax(Project project, HashSet generatedTypeNames) + { + var roots = new HashSet(StringComparer.Ordinal); + foreach (var document in project.Documents) + { + if (GeneratedCodeWorkspace.IsGeneratedDocument(document)) + { + continue; + } + + var root = document.GetSyntaxRootAsync().GetAwaiter().GetResult(); + if (root == null) + { + continue; + } + + foreach (var typeDeclaration in root.DescendantNodes().OfType()) + { + if (!IsPublic(typeDeclaration.Modifiers)) + { + continue; + } + + if (typeDeclaration.BaseList != null) + { + AddSyntaxTypeReferences(roots, typeDeclaration.BaseList, generatedTypeNames); + } + + foreach (var member in typeDeclaration.Members) + { + if (IsPublicApiMember(member)) + { + AddMemberSignatureTypeReferences(roots, member, generatedTypeNames); + } + } + } + } + + return roots; + } + + private static bool IsPublicApiMember(MemberDeclarationSyntax member) + => member switch + { + EventDeclarationSyntax @event => IsPublic(@event.Modifiers), + EventFieldDeclarationSyntax @event => IsPublic(@event.Modifiers), + BaseFieldDeclarationSyntax field => IsPublic(field.Modifiers), + BaseMethodDeclarationSyntax method => IsPublic(method.Modifiers), + BasePropertyDeclarationSyntax property => IsPublic(property.Modifiers), + DelegateDeclarationSyntax @delegate => IsPublic(@delegate.Modifiers), + BaseTypeDeclarationSyntax type => IsPublic(type.Modifiers), + _ => false + }; + + private static bool IsPublic(SyntaxTokenList modifiers) + => modifiers.Any(static modifier => + modifier.IsKind(SyntaxKind.PublicKeyword) || + modifier.IsKind(SyntaxKind.ProtectedKeyword)); + + private static void AddMemberSignatureTypeReferences(HashSet roots, MemberDeclarationSyntax member, HashSet generatedTypeNames) + { + switch (member) + { + case MethodDeclarationSyntax method: + AddSyntaxTypeReferences(roots, method.ReturnType, generatedTypeNames); + AddSyntaxTypeReferences(roots, method.ParameterList, generatedTypeNames); + AddSyntaxTypeReferences(roots, method.ConstraintClauses, generatedTypeNames); + break; + case ConstructorDeclarationSyntax constructor: + AddSyntaxTypeReferences(roots, constructor.ParameterList, generatedTypeNames); + break; + case ConversionOperatorDeclarationSyntax conversion: + AddSyntaxTypeReferences(roots, conversion.Type, generatedTypeNames); + AddSyntaxTypeReferences(roots, conversion.ParameterList, generatedTypeNames); + break; + case OperatorDeclarationSyntax @operator: + AddSyntaxTypeReferences(roots, @operator.ReturnType, generatedTypeNames); + AddSyntaxTypeReferences(roots, @operator.ParameterList, generatedTypeNames); + break; + case PropertyDeclarationSyntax property: + AddSyntaxTypeReferences(roots, property.Type, generatedTypeNames); + break; + case IndexerDeclarationSyntax indexer: + AddSyntaxTypeReferences(roots, indexer.Type, generatedTypeNames); + AddSyntaxTypeReferences(roots, indexer.ParameterList, generatedTypeNames); + break; + case FieldDeclarationSyntax field: + AddSyntaxTypeReferences(roots, field.Declaration.Type, generatedTypeNames); + break; + case EventFieldDeclarationSyntax eventField: + AddSyntaxTypeReferences(roots, eventField.Declaration.Type, generatedTypeNames); + break; + case EventDeclarationSyntax @event: + AddSyntaxTypeReferences(roots, @event.Type, generatedTypeNames); + break; + case DelegateDeclarationSyntax @delegate: + AddSyntaxTypeReferences(roots, @delegate.ReturnType, generatedTypeNames); + AddSyntaxTypeReferences(roots, @delegate.ParameterList, generatedTypeNames); + AddSyntaxTypeReferences(roots, @delegate.ConstraintClauses, generatedTypeNames); + break; + case BaseTypeDeclarationSyntax type: + if (type.BaseList != null) + { + AddSyntaxTypeReferences(roots, type.BaseList, generatedTypeNames); + } + break; + } + } + + private static void AddSyntaxTypeReferences(HashSet roots, SyntaxNode node, HashSet generatedTypeNames) + { + foreach (var name in node.DescendantNodesAndSelf().OfType()) + { + AddMatchingName(roots, name.Identifier.ValueText, generatedTypeNames); + } + + foreach (var name in node.DescendantNodesAndSelf().OfType()) + { + AddMatchingName(roots, name.Identifier.ValueText, generatedTypeNames); + } + } + + private static void AddSyntaxTypeReferences(HashSet roots, IEnumerable nodes, HashSet generatedTypeNames) + { + foreach (var node in nodes) + { + AddSyntaxTypeReferences(roots, node, generatedTypeNames); + } + } + + private static IEnumerable GetCustomCodeViews(IReadOnlyList providers) + { + var visited = new HashSet(); + foreach (var provider in providers) + { + var customCodeView = provider.CustomCodeView; + if (customCodeView == null || !visited.Add(customCodeView)) + { + continue; + } + + yield return customCodeView; + } + } + + private static void AddCustomCodeViewRoots(HashSet roots, TypeProvider customCodeView, HashSet generatedTypeNames, bool publicOnly) + { + AddTypeReference(roots, customCodeView.Type, generatedTypeNames); + AddTypeReference(roots, customCodeView.BaseType, generatedTypeNames); + foreach (var implementedType in customCodeView.Implements) + { + AddTypeReference(roots, implementedType, generatedTypeNames); + } + + foreach (var constructor in customCodeView.Constructors) + { + if (publicOnly && !IsPublic(constructor.Signature.Modifiers)) + { + continue; + } + + AddSignatureReferences(roots, constructor.Signature, generatedTypeNames, serializationProviderNamesByType: null, includeAttributes: !publicOnly); + } + + foreach (var method in customCodeView.Methods) + { + if (publicOnly && !IsPublic(method.Signature.Modifiers)) + { + continue; + } + + AddSignatureReferences(roots, method.Signature, generatedTypeNames, serializationProviderNamesByType: null, includeAttributes: !publicOnly); + } + + foreach (var property in customCodeView.Properties) + { + if (publicOnly && !IsPublic(property.Modifiers)) + { + continue; + } + + AddTypeReference(roots, property.Type, generatedTypeNames); + AddTypeReference(roots, property.ExplicitInterface, generatedTypeNames); + if (!publicOnly) + { + AddAttributes(roots, property.Attributes, generatedTypeNames, serializationProviderNamesByType: null); + } + } + + foreach (var field in customCodeView.Fields) + { + if (publicOnly && !IsPublic(field.Modifiers)) + { + continue; + } + + AddTypeReference(roots, field.Type, generatedTypeNames); + if (!publicOnly) + { + AddAttributes(roots, field.Attributes, generatedTypeNames, serializationProviderNamesByType: null); + } + } + } + private static HashSet GetApiBaselineGeneratedTypeRoots(HashSet generatedTypeNames) { var roots = new HashSet(StringComparer.Ordinal); @@ -369,6 +685,22 @@ private static HashSet GetCustomCodeInternalGeneratedTypeDeclarations(Pr return declarations; } + private static HashSet GetCustomCodeInternalGeneratedTypeDeclarations(IReadOnlyList providers, HashSet generatedTypeNames) + { + var declarations = new HashSet(StringComparer.Ordinal); + foreach (var customCodeView in GetCustomCodeViews(providers)) + { + if (!customCodeView.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)) + { + continue; + } + + AddTypeReference(declarations, customCodeView.Type, generatedTypeNames); + } + + return declarations; + } + private static HashSet GetGeneratedPersistableModelProxyTypeNames(Project project, HashSet generatedTypeNames) { var proxyTypes = new HashSet(StringComparer.Ordinal); @@ -606,6 +938,13 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList gen } private static bool IsPublic(MethodSignatureModifiers modifiers) => modifiers.HasFlag(MethodSignatureModifiers.Public); + private static bool IsPublic(FieldModifiers modifiers) => modifiers.HasFlag(FieldModifiers.Public); + + private static TypeSignatureModifiers MakeInternal(TypeSignatureModifiers modifiers) + => (modifiers & ~(TypeSignatureModifiers.Public | TypeSignatureModifiers.Private | TypeSignatureModifiers.Protected)) | TypeSignatureModifiers.Internal; + + private static TypeSignatureModifiers MakePublic(TypeSignatureModifiers modifiers) + => (modifiers & ~(TypeSignatureModifiers.Internal | TypeSignatureModifiers.Private | TypeSignatureModifiers.Protected)) | TypeSignatureModifiers.Public; private static Dictionary> CloneReferences(IReadOnlyDictionary> references) { @@ -1114,6 +1453,20 @@ private static bool IsImplementationOnlyModelFactoryMethod(MethodProvider method returnTypeName.EndsWith("Request", StringComparison.Ordinal); } + private static void RemoveMethodsFromModelFactory(HashSet namesToRemove) + { + if (namesToRemove.Count == 0) + { + return; + } + + var modelFactory = CodeModelGenerator.Instance.OutputLibrary.ModelFactory.Value; + var methodsToKeep = modelFactory.Methods + .Where(method => !namesToRemove.Contains(method.Signature.Name)) + .ToArray(); + modelFactory.Update(methods: methodsToKeep); + } + private static HashSet GetPostProcessorDeclaredNodes(IReadOnlyList providers, HashSet nodes, bool publicOnly) { var generator = CodeModelGenerator.Instance; @@ -1529,6 +1882,12 @@ private static void AddTypeReference( return; } + if (type.IsArray) + { + AddTypeReference(references, type.ElementType, nodes, serializationProviderNamesByType); + return; + } + var providerTypeName = GetProviderTypeName(type); if (nodes.Contains(providerTypeName)) { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/TypeProviderWriter.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/TypeProviderWriter.cs index 49fe9723973..eb07aa4519f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/TypeProviderWriter.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/TypeProviderWriter.cs @@ -45,7 +45,7 @@ private bool IsPublicContext(TypeProvider provider) private void WriteType(CodeWriter writer) { - if (IsPublicContext(_provider)) + if (_provider.PreserveTypeXmlDocs || IsPublicContext(_provider)) { writer.WriteXmlDocsNoScope(_provider.XmlDocs); } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs index eb93daadd7f..6996d39a34e 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs @@ -143,6 +143,13 @@ public XmlDocProvider XmlDocs private set => _xmlDocs = value; } + internal bool PreserveTypeXmlDocs { get; private set; } + + internal void PreserveXmlDocs() + { + PreserveTypeXmlDocs = true; + } + public string? Deprecated { get => _deprecated; @@ -546,6 +553,7 @@ public virtual void Reset() _serializationProviders = null; _nestedTypes = null; _xmlDocs = null; + PreserveTypeXmlDocs = false; _declarationModifiers = null; _relativeFilePath = null; _customCodeView = new(() => BuildCustomCodeView()); From 6e38fe718afc4490a71f0c964e2cec8a302bca7f Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Mon, 29 Jun 2026 08:46:38 +0000 Subject: [PATCH 081/111] fix(csharp): restore model factory methods after generation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...-prewrite-internalization-2026-06-29-0759.md | 7 ------- .../src/CSharpGen.cs | 1 + .../ProviderReferenceMapAnalyzer.cs | 17 +++++++++++++++++ 3 files changed, 18 insertions(+), 7 deletions(-) delete mode 100644 .chronus/changes/mtg-hybrid-reference-map-prewrite-internalization-2026-06-29-0759.md diff --git a/.chronus/changes/mtg-hybrid-reference-map-prewrite-internalization-2026-06-29-0759.md b/.chronus/changes/mtg-hybrid-reference-map-prewrite-internalization-2026-06-29-0759.md deleted file mode 100644 index 617dc8cd866..00000000000 --- a/.chronus/changes/mtg-hybrid-reference-map-prewrite-internalization-2026-06-29-0759.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -changeKind: internal -packages: - - "@typespec/http-client-csharp" ---- - -Improve C# generator post-processing performance by using provider-based reference-map construction with pre-write internalization. diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index 48bcb174bb9..e98c68f4e3c 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs @@ -121,6 +121,7 @@ await GeneratedCodeWorkspace.LoadBaselineContract(), await Task.WhenAll(generateFilesTasks); generatedCodeWorkspace.AnalyzeProviderReferenceMap(output.TypeProviders); + ProviderReferenceMapAnalyzer.RestorePreWriteModelFactoryMethods(); LoggingHelpers.LogElapsedTime("All generated types have been written into memory"); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index b357901de79..ed44f9ec6d0 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -22,12 +22,15 @@ internal static class ProviderReferenceMapAnalyzer { private static ProviderReferenceMapResult? _latestResult; private static readonly ConditionalWeakTable, Dictionary> _simpleNameLookupCache = new(); + private static TypeProvider? _preWriteModelFactory; + private static MethodProvider[]? _preWriteModelFactoryMethods; public static ProviderReferenceMapResult? LatestResult => _latestResult; public static bool PreWriteAccessibilityApplied { get; private set; } public static void ResetPreWriteAccessibility() { + RestorePreWriteModelFactoryMethods(); PreWriteAccessibilityApplied = false; } @@ -58,6 +61,18 @@ public static void ApplyPreWriteAccessibility(IReadOnlyList provid PreWriteAccessibilityApplied = true; } + public static void RestorePreWriteModelFactoryMethods() + { + if (_preWriteModelFactory == null || _preWriteModelFactoryMethods == null) + { + return; + } + + _preWriteModelFactory.Update(methods: _preWriteModelFactoryMethods); + _preWriteModelFactory = null; + _preWriteModelFactoryMethods = null; + } + public static void Analyze(IReadOnlyList providers, Project project) { var generatedProviders = GetGeneratedProviders(providers); @@ -1461,6 +1476,8 @@ private static void RemoveMethodsFromModelFactory(HashSet namesToRemove) } var modelFactory = CodeModelGenerator.Instance.OutputLibrary.ModelFactory.Value; + _preWriteModelFactory = modelFactory; + _preWriteModelFactoryMethods ??= [.. modelFactory.Methods]; var methodsToKeep = modelFactory.Methods .Where(method => !namesToRemove.Contains(method.Signature.Name)) .ToArray(); From 98e7c26ccabfff58fabf047ad482e323fb8d0069 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Mon, 29 Jun 2026 09:44:10 +0000 Subject: [PATCH 082/111] fix(csharp): update generated model factory baselines Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/Generated/DocumentationModelFactory.cs | 1 - .../basic/src/Generated/ParametersBasicModelFactory.cs | 1 - .../spread/src/Generated/ParametersSpreadModelFactory.cs | 2 -- .../multipart/src/Generated/PayloadMultiPartModelFactory.cs | 1 - .../pageable/src/Generated/PayloadPageableModelFactory.cs | 6 ------ .../special-words/src/Generated/SpecialWordsModelFactory.cs | 2 -- 6 files changed, 13 deletions(-) diff --git a/packages/http-client-csharp/generator/TestProjects/Spector/http/documentation/src/Generated/DocumentationModelFactory.cs b/packages/http-client-csharp/generator/TestProjects/Spector/http/documentation/src/Generated/DocumentationModelFactory.cs index 8918ac1946c..4440dc1b6a0 100644 --- a/packages/http-client-csharp/generator/TestProjects/Spector/http/documentation/src/Generated/DocumentationModelFactory.cs +++ b/packages/http-client-csharp/generator/TestProjects/Spector/http/documentation/src/Generated/DocumentationModelFactory.cs @@ -8,7 +8,6 @@ namespace Documentation { public static partial class DocumentationModelFactory { - public static BulletPointsModel BulletPointsModel(BulletPointsEnum prop = default) => throw null; } } diff --git a/packages/http-client-csharp/generator/TestProjects/Spector/http/parameters/basic/src/Generated/ParametersBasicModelFactory.cs b/packages/http-client-csharp/generator/TestProjects/Spector/http/parameters/basic/src/Generated/ParametersBasicModelFactory.cs index 06d44d34bc1..c17dda5eaec 100644 --- a/packages/http-client-csharp/generator/TestProjects/Spector/http/parameters/basic/src/Generated/ParametersBasicModelFactory.cs +++ b/packages/http-client-csharp/generator/TestProjects/Spector/http/parameters/basic/src/Generated/ParametersBasicModelFactory.cs @@ -3,7 +3,6 @@ #nullable disable using Parameters.Basic._ExplicitBody; -using Parameters.Basic._ImplicitBody; namespace Parameters.Basic { diff --git a/packages/http-client-csharp/generator/TestProjects/Spector/http/parameters/spread/src/Generated/ParametersSpreadModelFactory.cs b/packages/http-client-csharp/generator/TestProjects/Spector/http/parameters/spread/src/Generated/ParametersSpreadModelFactory.cs index 775c933bc6b..4e494c2f2ec 100644 --- a/packages/http-client-csharp/generator/TestProjects/Spector/http/parameters/spread/src/Generated/ParametersSpreadModelFactory.cs +++ b/packages/http-client-csharp/generator/TestProjects/Spector/http/parameters/spread/src/Generated/ParametersSpreadModelFactory.cs @@ -2,8 +2,6 @@ #nullable disable -using System.Collections.Generic; -using Parameters.Spread._Alias; using Parameters.Spread._Model; namespace Parameters.Spread diff --git a/packages/http-client-csharp/generator/TestProjects/Spector/http/payload/multipart/src/Generated/PayloadMultiPartModelFactory.cs b/packages/http-client-csharp/generator/TestProjects/Spector/http/payload/multipart/src/Generated/PayloadMultiPartModelFactory.cs index 6d036b01c86..e5c87b994a6 100644 --- a/packages/http-client-csharp/generator/TestProjects/Spector/http/payload/multipart/src/Generated/PayloadMultiPartModelFactory.cs +++ b/packages/http-client-csharp/generator/TestProjects/Spector/http/payload/multipart/src/Generated/PayloadMultiPartModelFactory.cs @@ -2,7 +2,6 @@ #nullable disable -using System; using System.ClientModel; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; diff --git a/packages/http-client-csharp/generator/TestProjects/Spector/http/payload/pageable/src/Generated/PayloadPageableModelFactory.cs b/packages/http-client-csharp/generator/TestProjects/Spector/http/payload/pageable/src/Generated/PayloadPageableModelFactory.cs index 0b66e798a05..1b7b8eefa6f 100644 --- a/packages/http-client-csharp/generator/TestProjects/Spector/http/payload/pageable/src/Generated/PayloadPageableModelFactory.cs +++ b/packages/http-client-csharp/generator/TestProjects/Spector/http/payload/pageable/src/Generated/PayloadPageableModelFactory.cs @@ -2,18 +2,12 @@ #nullable disable -using System; -using System.Collections.Generic; -using Payload.Pageable._PageSize; -using Payload.Pageable._ServerDrivenPagination; using Payload.Pageable._ServerDrivenPagination.AlternateInitialVerb; -using Payload.Pageable._ServerDrivenPagination.ContinuationToken; namespace Payload.Pageable { public static partial class PayloadPageableModelFactory { - public static Pet Pet(string id = default, string name = default) => throw null; public static XmlPet XmlPet(string id = default, string name = default) => throw null; diff --git a/packages/http-client-csharp/generator/TestProjects/Spector/http/special-words/src/Generated/SpecialWordsModelFactory.cs b/packages/http-client-csharp/generator/TestProjects/Spector/http/special-words/src/Generated/SpecialWordsModelFactory.cs index 1abb2114c9a..43aca8db259 100644 --- a/packages/http-client-csharp/generator/TestProjects/Spector/http/special-words/src/Generated/SpecialWordsModelFactory.cs +++ b/packages/http-client-csharp/generator/TestProjects/Spector/http/special-words/src/Generated/SpecialWordsModelFactory.cs @@ -2,10 +2,8 @@ #nullable disable -using System.Collections.Generic; using SpecialWords._ModelProperties; using SpecialWords._Models; -using SpecialWords._ReservedOperationBodyParams; namespace SpecialWords { From 2c17e8c8442413d49b96a7b049ea250bc3df61ce Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Mon, 29 Jun 2026 10:18:09 +0000 Subject: [PATCH 083/111] fix(csharp): skip empty generated model factories Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index e98c68f4e3c..4dba3b5a494 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs @@ -107,6 +107,11 @@ await GeneratedCodeWorkspace.LoadBaselineContract(), foreach (var outputType in output.TypeProviders) { + if (outputType is ModelFactoryProvider && outputType.Methods.Count == 0) + { + continue; + } + var writer = CodeModelGenerator.Instance.GetWriter(outputType); generateFilesTasks.Add(generatedCodeWorkspace.AddGeneratedFile(writer.Write())); From aacaf10aa18e041eaf979800a3371bc9612bf095 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Tue, 30 Jun 2026 03:16:26 +0000 Subject: [PATCH 084/111] docs(csharp): explain provider reference map flow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../PostProcessing/ProviderReferenceMapAnalyzer.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index ed44f9ec6d0..562f8e80ee5 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -42,6 +42,8 @@ public static void ApplyPreWriteAccessibility(IReadOnlyList provid return; } + // Accessibility is applied to providers before TypeProviderWriter runs so generated source + // starts with the final public/internal shape instead of being rewritten by Roslyn later. var (internalizeCandidates, publicizeCandidates) = GetPreWriteAccessibilityCandidates(providers, customCodeProject); foreach (var provider in GetGeneratedProviders(providers)) { @@ -76,6 +78,9 @@ public static void RestorePreWriteModelFactoryMethods() public static void Analyze(IReadOnlyList providers, Project project) { var generatedProviders = GetGeneratedProviders(providers); + // The provider graph replaces the old Roslyn-generated-source reference map for generated + // code. We still keep custom-source syntax/symbol checks because users can reference + // generated types from arbitrary C# that is not represented by TypeProviders. var graph = BuildGraph(generatedProviders); var publicGraph = BuildGraph(generatedProviders, publicOnly: true); @@ -143,8 +148,9 @@ public static void Analyze(IReadOnlyList providers, Project projec .OrderBy(static name => name, StringComparer.Ordinal) .ToArray(); - // Body-only generated dependencies are needed to avoid deleting helper files, but they do - // not contribute to public API reachability for internalization. + // Body-only generated dependencies are intentionally added after public reachability is + // computed: they keep helper files from being removed but must not force public API types + // to remain public. AddGeneratedXmlDocCrefReferences(project, graph, publicOnly: false); AddGeneratedBodyReferences(project, providers, graph); @@ -824,6 +830,10 @@ private static void AddMatchingSymbolName(HashSet target, INamedTypeSymb private static ProviderReferenceGraph BuildGraph(IReadOnlyList generatedProviders, bool publicOnly = false) { + // Each generated provider becomes a node, and provider metadata supplies the edges: + // inheritance, signatures, properties, fields, nested/serialization providers, attributes, + // and selected implementation dependencies. This avoids parsing generated C# just to + // rediscover generated-to-generated references. var serializationProviderNamesByType = generatedProviders .Where(static provider => provider.SerializationProviders.Count > 0) .GroupBy(static provider => GetProviderTypeName(provider.Type), StringComparer.Ordinal) From 4cb6e594e353bd17227aafc8f3b0555a97352f34 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Tue, 30 Jun 2026 05:20:52 +0000 Subject: [PATCH 085/111] Fix hybrid reference map public roots Preserve public custom partial declarations as public roots during pre-write accessibility and prune model factory methods for removed models. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/PostProcessing/PostProcessor.cs | 8 ++++++++ .../src/PostProcessing/ProviderReferenceMapAnalyzer.cs | 10 ++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs index 69694438fdb..9f9b086b787 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs @@ -345,6 +345,14 @@ public async Task RemoveAsync(Project project) nodesToRemove.AddRange(definitions.DeclaredNodesCache[symbol]); } + var modelNamesToRemove = nodesToRemove + .Select(static item => item.Identifier.Text) + .ToHashSet(StringComparer.Ordinal); + if (modelNamesToRemove.Count > 0) + { + project = await RemoveMethodsFromModelFactoryAsync(project, definitions, modelNamesToRemove); + } + // remove them one by one var referencedTypeNames = referencedSet .Select(static symbol => symbol.GetFullyQualifiedName()) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index ed44f9ec6d0..0dd86742ced 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -127,7 +127,9 @@ public static void Analyze(IReadOnlyList providers, Project projec var publicizeReachable = GetReachableTypes(publicizeRoots, internalizeReferences, publicApiTraversalNodes); var internalizeCandidates = internalizeDeclaredNodes .Except(publicizeReachable, StringComparer.Ordinal) - .Union(internalizeDeclaredNodes.Intersect(customInternalBoundaryNodes, StringComparer.Ordinal), StringComparer.Ordinal) + .Union(internalizeDeclaredNodes + .Intersect(customInternalBoundaryNodes, StringComparer.Ordinal) + .Except(publicizeRoots, StringComparer.Ordinal), StringComparer.Ordinal) .OrderBy(static name => name, StringComparer.Ordinal) .ToArray(); var publicizeCandidates = publicizeDeclaredNodes @@ -223,7 +225,9 @@ private static (HashSet InternalizeCandidates, HashSet Publicize var publicizeReachable = GetReachableTypes(publicizeRoots, internalizeReferences, publicApiTraversalNodes); var internalizeCandidates = internalizeDeclaredNodes .Except(publicizeReachable, StringComparer.Ordinal) - .Union(internalizeDeclaredNodes.Intersect(customInternalBoundaryNodes, StringComparer.Ordinal), StringComparer.Ordinal) + .Union(internalizeDeclaredNodes + .Intersect(customInternalBoundaryNodes, StringComparer.Ordinal) + .Except(publicizeRoots, StringComparer.Ordinal), StringComparer.Ordinal) .ToHashSet(StringComparer.Ordinal); var publicizeCandidates = publicizeDeclaredNodes .Except(customInternalDeclarations, StringComparer.Ordinal) @@ -397,6 +401,8 @@ private static HashSet GetCustomCodePublicGeneratedTypeRootsFromSyntax(P continue; } + AddMatchingName(roots, typeDeclaration.Identifier.ValueText, generatedTypeNames); + if (typeDeclaration.BaseList != null) { AddSyntaxTypeReferences(roots, typeDeclaration.BaseList, generatedTypeNames); From 5f8792cabb347d9ac5e1f05d6361154e1facd8f5 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Tue, 30 Jun 2026 05:20:52 +0000 Subject: [PATCH 086/111] Fix hybrid reference map public roots Preserve public custom partial declarations as public roots during pre-write accessibility and prune model factory methods for removed models. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/PostProcessing/PostProcessor.cs | 8 ++++++++ .../src/PostProcessing/ProviderReferenceMapAnalyzer.cs | 10 ++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs index 69694438fdb..9f9b086b787 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs @@ -345,6 +345,14 @@ public async Task RemoveAsync(Project project) nodesToRemove.AddRange(definitions.DeclaredNodesCache[symbol]); } + var modelNamesToRemove = nodesToRemove + .Select(static item => item.Identifier.Text) + .ToHashSet(StringComparer.Ordinal); + if (modelNamesToRemove.Count > 0) + { + project = await RemoveMethodsFromModelFactoryAsync(project, definitions, modelNamesToRemove); + } + // remove them one by one var referencedTypeNames = referencedSet .Select(static symbol => symbol.GetFullyQualifiedName()) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 562f8e80ee5..b9ed26cc5e8 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -132,7 +132,9 @@ public static void Analyze(IReadOnlyList providers, Project projec var publicizeReachable = GetReachableTypes(publicizeRoots, internalizeReferences, publicApiTraversalNodes); var internalizeCandidates = internalizeDeclaredNodes .Except(publicizeReachable, StringComparer.Ordinal) - .Union(internalizeDeclaredNodes.Intersect(customInternalBoundaryNodes, StringComparer.Ordinal), StringComparer.Ordinal) + .Union(internalizeDeclaredNodes + .Intersect(customInternalBoundaryNodes, StringComparer.Ordinal) + .Except(publicizeRoots, StringComparer.Ordinal), StringComparer.Ordinal) .OrderBy(static name => name, StringComparer.Ordinal) .ToArray(); var publicizeCandidates = publicizeDeclaredNodes @@ -229,7 +231,9 @@ private static (HashSet InternalizeCandidates, HashSet Publicize var publicizeReachable = GetReachableTypes(publicizeRoots, internalizeReferences, publicApiTraversalNodes); var internalizeCandidates = internalizeDeclaredNodes .Except(publicizeReachable, StringComparer.Ordinal) - .Union(internalizeDeclaredNodes.Intersect(customInternalBoundaryNodes, StringComparer.Ordinal), StringComparer.Ordinal) + .Union(internalizeDeclaredNodes + .Intersect(customInternalBoundaryNodes, StringComparer.Ordinal) + .Except(publicizeRoots, StringComparer.Ordinal), StringComparer.Ordinal) .ToHashSet(StringComparer.Ordinal); var publicizeCandidates = publicizeDeclaredNodes .Except(customInternalDeclarations, StringComparer.Ordinal) @@ -403,6 +407,8 @@ private static HashSet GetCustomCodePublicGeneratedTypeRootsFromSyntax(P continue; } + AddMatchingName(roots, typeDeclaration.Identifier.ValueText, generatedTypeNames); + if (typeDeclaration.BaseList != null) { AddSyntaxTypeReferences(roots, typeDeclaration.BaseList, generatedTypeNames); From 0f8572c98cfac82b2d7108d7a408fb6d4cbd0f64 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Tue, 30 Jun 2026 11:07:17 +0000 Subject: [PATCH 087/111] Remove Roslyn from provider reference map Use provider metadata and source-input lookups for generated reference-map roots so the analyzer no longer needs Roslyn project or semantic-model scans. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../PostProcessing/GeneratedCodeWorkspace.cs | 4 +- .../src/PostProcessing/PostProcessor.cs | 7 +- .../ProviderReferenceMapAnalyzer.cs | 848 ++++++------------ .../ProviderReferenceMapResult.cs | 2 - 4 files changed, 284 insertions(+), 577 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index 967a0870a87..25fab34e6b8 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -86,12 +86,12 @@ public async Task AddInMemoryFile(TypeProvider type) internal void AnalyzeProviderReferenceMap(IReadOnlyList providers) { - ProviderReferenceMapAnalyzer.Analyze(providers, _project); + ProviderReferenceMapAnalyzer.Analyze(providers); } internal void ApplyPreWriteAccessibility(IReadOnlyList providers) { - ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility(providers, _project); + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility(providers); } private async Task UpdateProject(Document document) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs index 9f9b086b787..1ef4ff6360c 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs @@ -133,9 +133,7 @@ public async Task InternalizeAsync(Project project) return project; } - var referenceMapResult = ProviderReferenceMapAnalyzer.LatestResult is { } latestResult && latestResult.ProjectId == project.Id - ? latestResult - : null; + var referenceMapResult = ProviderReferenceMapAnalyzer.LatestResult; // first get all the declared symbols var definitions = await GetTypeSymbolsAsync(compilation, project, publicOnly: referenceMapResult == null); @@ -144,7 +142,6 @@ public async Task InternalizeAsync(Project project) if (referenceMapResult != null) { // ProviderReferenceMapAnalyzer replaces Roslyn reference-map construction for generated code. - // It still uses Roslyn-discovered roots for custom/shared code before this point. symbolsToInternalize = GetSymbolsByName(definitions.DeclaredSymbols, referenceMapResult.InternalizeCandidates).ToArray(); symbolsToPublicize = GetSymbolsByName(definitions.DeclaredSymbols, referenceMapResult.PublicizeCandidates).ToArray(); } @@ -310,7 +307,7 @@ public async Task RemoveAsync(Project project) var definitions = await GetTypeSymbolsAsync(compilation, project, false); IEnumerable symbolsToRemove; HashSet referencedSet; - if (ProviderReferenceMapAnalyzer.LatestResult is { } referenceMapResult && referenceMapResult.ProjectId == project.Id) + if (ProviderReferenceMapAnalyzer.LatestResult is { } referenceMapResult) { // The remove pass uses the same precomputed hybrid map to avoid scanning all generated // documents with Roslyn while preserving custom-code references as roots. diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index b9ed26cc5e8..cd0c5dbea68 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -7,14 +7,10 @@ using System.Linq; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; -using System.Xml.Linq; using Microsoft.TypeSpec.Generator.Expressions; using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.Statements; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Microsoft.TypeSpec.Generator { @@ -34,7 +30,7 @@ public static void ResetPreWriteAccessibility() PreWriteAccessibilityApplied = false; } - public static void ApplyPreWriteAccessibility(IReadOnlyList providers, Project customCodeProject) + public static void ApplyPreWriteAccessibility(IReadOnlyList providers) { PreWriteAccessibilityApplied = false; if (Configuration.UnreferencedTypesHandling == Configuration.UnreferencedTypesHandlingOption.KeepAll) @@ -42,9 +38,7 @@ public static void ApplyPreWriteAccessibility(IReadOnlyList provid return; } - // Accessibility is applied to providers before TypeProviderWriter runs so generated source - // starts with the final public/internal shape instead of being rewritten by Roslyn later. - var (internalizeCandidates, publicizeCandidates) = GetPreWriteAccessibilityCandidates(providers, customCodeProject); + var (internalizeCandidates, publicizeCandidates) = GetPreWriteAccessibilityCandidates(providers); foreach (var provider in GetGeneratedProviders(providers)) { var providerName = GetProviderTypeName(provider.Type); @@ -75,7 +69,7 @@ public static void RestorePreWriteModelFactoryMethods() _preWriteModelFactoryMethods = null; } - public static void Analyze(IReadOnlyList providers, Project project) + public static void Analyze(IReadOnlyList providers) { var generatedProviders = GetGeneratedProviders(providers); // The provider graph replaces the old Roslyn-generated-source reference map for generated @@ -84,23 +78,20 @@ public static void Analyze(IReadOnlyList providers, Project projec var graph = BuildGraph(generatedProviders); var publicGraph = BuildGraph(generatedProviders, publicOnly: true); - // Generated-code dependencies come from providers. Custom code still needs Roslyn - // because arbitrary user C# can reference generated types in ways providers cannot see. - var customPublicRoots = GetCustomCodePublicGeneratedTypeRoots(project, graph.Nodes); + var customPublicRoots = GetCustomCodePublicGeneratedTypeRoots(generatedProviders, graph.Nodes); var apiBaselineGeneratedTypeRoots = GetApiBaselineGeneratedTypeRoots(graph.Nodes); customPublicRoots.UnionWith(apiBaselineGeneratedTypeRoots); var generatedPublicDeclarations = GetGeneratedPublicTypeDeclarations(generatedProviders, graph.Nodes); customPublicRoots.UnionWith(generatedPublicDeclarations); - var customRemovalRoots = GetCustomCodeGeneratedTypeRoots(project, graph.Nodes); + var customRemovalRoots = GetCustomCodeGeneratedTypeRoots(generatedProviders, graph.Nodes); customRemovalRoots.UnionWith(apiBaselineGeneratedTypeRoots); customRemovalRoots.UnionWith(generatedPublicDeclarations); - var customInternalDeclarations = GetCustomCodeInternalGeneratedTypeDeclarations(project, graph.Nodes); + var customInternalDeclarations = GetCustomCodeInternalGeneratedTypeDeclarations(generatedProviders, graph.Nodes); var generatedInternalDeclarations = GetGeneratedInternalTypeDeclarations(generatedProviders, graph.Nodes); // Helper types are rooted after an initial reachability pass so unused infrastructure // such as change-tracking dictionaries can still be removed when no reachable type needs them. - var generatedDiscriminatorBaseNames = GetGeneratedPersistableModelProxyTypeNames(project, publicGraph.Nodes); - AddGeneratedXmlDocCrefReferences(project, publicGraph, publicOnly: true); + var generatedDiscriminatorBaseNames = GetGeneratedPersistableModelProxyTypeNames(generatedProviders, publicGraph.Nodes); var internalizeReferences = CloneReferences(publicGraph.References); var internalizeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: false, includeAdditionalRoots: true, includeUnionVariantRoots: false, publicClientRootsOnly: true); if (ShouldUseUnionVariantFallbackRoots()) @@ -150,11 +141,9 @@ public static void Analyze(IReadOnlyList providers, Project projec .OrderBy(static name => name, StringComparer.Ordinal) .ToArray(); - // Body-only generated dependencies are intentionally added after public reachability is - // computed: they keep helper files from being removed but must not force public API types - // to remain public. - AddGeneratedXmlDocCrefReferences(project, graph, publicOnly: false); - AddGeneratedBodyReferences(project, providers, graph); + // Body-only generated dependencies are needed to avoid deleting helper files, but they do + // not contribute to public API reachability for internalization. + AddGeneratedBodyReferences(providers, graph); var removeRoots = GetRootNames( providers, @@ -165,7 +154,10 @@ public static void Analyze(IReadOnlyList providers, Project projec includeUnionVariantRoots: ShouldUseUnionVariantFallbackRoots(), publicClientRootsOnly: false); removeRoots.UnionWith(customRemovalRoots); - RemoveUnusedRequestHeaderExtensionsRoot(removeRoots, graph.References, project); + AddMatchingNamesWithSimpleNameSuffix(removeRoots, "ReferenceType", graph.Nodes); + AddCustomCodeExtensionRoots(removeRoots, generatedProviders, graph.Nodes); + AddCustomizationBackedExtensionRoots(removeRoots, graph.Nodes); + RemoveUnusedRequestHeaderExtensionsRoot(removeRoots, graph.References, providers); var removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); AddDerivedModelReferences(providers, graph.Nodes, graph.References, removeReachableWithoutHelpers, generatedDiscriminatorBaseNames); removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); @@ -179,19 +171,17 @@ public static void Analyze(IReadOnlyList providers, Project projec var helperRoots = internalizeHelperRoots.Concat(removeHelperRoots).ToHashSet(StringComparer.Ordinal); _latestResult = new ProviderReferenceMapResult( - project.Id, internalizeCandidates.ToHashSet(StringComparer.Ordinal), publicizeCandidates.ToHashSet(StringComparer.Ordinal), removeCandidates.ToHashSet(StringComparer.Ordinal)); } - private static (HashSet InternalizeCandidates, HashSet PublicizeCandidates) GetPreWriteAccessibilityCandidates(IReadOnlyList providers, Project customCodeProject) + private static (HashSet InternalizeCandidates, HashSet PublicizeCandidates) GetPreWriteAccessibilityCandidates(IReadOnlyList providers) { var generatedProviders = GetGeneratedProviders(providers); var graph = BuildGraph(generatedProviders); var publicGraph = BuildGraph(generatedProviders, publicOnly: true); var customPublicRoots = GetCustomCodePublicGeneratedTypeRoots(generatedProviders, graph.Nodes); - customPublicRoots.UnionWith(GetCustomCodePublicGeneratedTypeRootsFromSyntax(customCodeProject, graph.Nodes)); var apiBaselineGeneratedTypeRoots = GetApiBaselineGeneratedTypeRoots(graph.Nodes); customPublicRoots.UnionWith(apiBaselineGeneratedTypeRoots); var generatedPublicDeclarations = GetGeneratedPublicTypeDeclarations(generatedProviders, graph.Nodes); @@ -249,120 +239,12 @@ private static (HashSet InternalizeCandidates, HashSet Publicize return (internalizeCandidates, publicizeCandidates); } - private static HashSet GetCustomCodeGeneratedTypeRoots(Project project, HashSet generatedTypeNames) - { - var roots = new HashSet(StringComparer.Ordinal); - var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); - if (compilation == null) - { - return roots; - } - - foreach (var document in project.Documents) - { - if (IsGeneratedDocument(document)) - { - continue; - } - - var root = document.GetSyntaxRootAsync().GetAwaiter().GetResult(); - if (root == null) - { - continue; - } - - var model = compilation.GetSemanticModel(root.SyntaxTree); - foreach (var declaration in root.DescendantNodes().OfType()) - { - AddSymbolRoot(roots, model.GetDeclaredSymbol(declaration) as ITypeSymbol, generatedTypeNames); - } - - foreach (var typeSyntax in root.DescendantNodes().OfType()) - { - AddSymbolRoot(roots, model.GetTypeInfo(typeSyntax).Type, generatedTypeNames); - } - - foreach (var objectCreation in root.DescendantNodes().OfType()) - { - AddSymbolRoot(roots, model.GetSymbolInfo(objectCreation).Symbol?.ContainingType, generatedTypeNames); - } - - foreach (var invocation in root.DescendantNodes().OfType()) - { - AddSymbolRoot(roots, model.GetSymbolInfo(invocation).Symbol?.ContainingType, generatedTypeNames); - } - } - - return roots; - } - - private static HashSet GetCustomCodePublicGeneratedTypeRoots(Project project, HashSet generatedTypeNames) + private static HashSet GetCustomCodeGeneratedTypeRoots(IReadOnlyList providers, HashSet generatedTypeNames) { var roots = new HashSet(StringComparer.Ordinal); - var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); - if (compilation == null) - { - return roots; - } - - foreach (var document in project.Documents) + foreach (var customCodeView in GetCustomCodeViews(providers)) { - if (IsGeneratedDocument(document)) - { - continue; - } - - var root = document.GetSyntaxRootAsync().GetAwaiter().GetResult(); - if (root == null) - { - continue; - } - - var semanticModel = compilation.GetSemanticModel(root.SyntaxTree); - foreach (var declaration in root.DescendantNodes().OfType()) - { - if (semanticModel.GetDeclaredSymbol(declaration) is not INamedTypeSymbol symbol || - symbol.DeclaredAccessibility != Accessibility.Public) - { - continue; - } - - AddSymbolRoot(roots, symbol, generatedTypeNames); - AddSymbolRoot(roots, symbol.BaseType, generatedTypeNames); - foreach (var interfaceType in symbol.Interfaces) - { - AddSymbolRoot(roots, interfaceType, generatedTypeNames); - } - - foreach (var member in symbol.GetMembers()) - { - if (member.DeclaredAccessibility != Accessibility.Public || - !IsDeclaredInSyntaxTree(member, declaration.SyntaxTree, declaration.Span)) - { - continue; - } - - switch (member) - { - case IMethodSymbol method: - AddSymbolRoot(roots, method.ReturnType, generatedTypeNames); - foreach (var parameter in method.Parameters) - { - AddSymbolRoot(roots, parameter.Type, generatedTypeNames); - } - break; - case IPropertySymbol property: - AddSymbolRoot(roots, property.Type, generatedTypeNames); - break; - case IFieldSymbol field: - AddSymbolRoot(roots, field.Type, generatedTypeNames); - break; - case IEventSymbol eventSymbol: - AddSymbolRoot(roots, eventSymbol.Type, generatedTypeNames); - break; - } - } - } + AddCustomCodeViewRoots(roots, customCodeView, generatedTypeNames, publicOnly: false); } return roots; @@ -384,150 +266,56 @@ private static HashSet GetCustomCodePublicGeneratedTypeRoots(IReadOnlyLi return roots; } - private static HashSet GetCustomCodePublicGeneratedTypeRootsFromSyntax(Project project, HashSet generatedTypeNames) + private static IEnumerable GetCustomCodeViews(IReadOnlyList providers) { - var roots = new HashSet(StringComparer.Ordinal); - foreach (var document in project.Documents) + var visited = new HashSet(); + var modelFactoryCustomCodeView = CodeModelGenerator.Instance.OutputLibrary.ModelFactory.Value.CustomCodeView; + if (modelFactoryCustomCodeView != null && visited.Add(modelFactoryCustomCodeView)) { - if (GeneratedCodeWorkspace.IsGeneratedDocument(document)) - { - continue; - } + yield return modelFactoryCustomCodeView; + } - var root = document.GetSyntaxRootAsync().GetAwaiter().GetResult(); - if (root == null) + foreach (var provider in providers) + { + var customCodeView = provider.CustomCodeView; + if (customCodeView == null || !visited.Add(customCodeView)) { continue; } - foreach (var typeDeclaration in root.DescendantNodes().OfType()) - { - if (!IsPublic(typeDeclaration.Modifiers)) - { - continue; - } - - AddMatchingName(roots, typeDeclaration.Identifier.ValueText, generatedTypeNames); - - if (typeDeclaration.BaseList != null) - { - AddSyntaxTypeReferences(roots, typeDeclaration.BaseList, generatedTypeNames); - } - - foreach (var member in typeDeclaration.Members) - { - if (IsPublicApiMember(member)) - { - AddMemberSignatureTypeReferences(roots, member, generatedTypeNames); - } - } - } - } - - return roots; - } - - private static bool IsPublicApiMember(MemberDeclarationSyntax member) - => member switch - { - EventDeclarationSyntax @event => IsPublic(@event.Modifiers), - EventFieldDeclarationSyntax @event => IsPublic(@event.Modifiers), - BaseFieldDeclarationSyntax field => IsPublic(field.Modifiers), - BaseMethodDeclarationSyntax method => IsPublic(method.Modifiers), - BasePropertyDeclarationSyntax property => IsPublic(property.Modifiers), - DelegateDeclarationSyntax @delegate => IsPublic(@delegate.Modifiers), - BaseTypeDeclarationSyntax type => IsPublic(type.Modifiers), - _ => false - }; - - private static bool IsPublic(SyntaxTokenList modifiers) - => modifiers.Any(static modifier => - modifier.IsKind(SyntaxKind.PublicKeyword) || - modifier.IsKind(SyntaxKind.ProtectedKeyword)); - - private static void AddMemberSignatureTypeReferences(HashSet roots, MemberDeclarationSyntax member, HashSet generatedTypeNames) - { - switch (member) - { - case MethodDeclarationSyntax method: - AddSyntaxTypeReferences(roots, method.ReturnType, generatedTypeNames); - AddSyntaxTypeReferences(roots, method.ParameterList, generatedTypeNames); - AddSyntaxTypeReferences(roots, method.ConstraintClauses, generatedTypeNames); - break; - case ConstructorDeclarationSyntax constructor: - AddSyntaxTypeReferences(roots, constructor.ParameterList, generatedTypeNames); - break; - case ConversionOperatorDeclarationSyntax conversion: - AddSyntaxTypeReferences(roots, conversion.Type, generatedTypeNames); - AddSyntaxTypeReferences(roots, conversion.ParameterList, generatedTypeNames); - break; - case OperatorDeclarationSyntax @operator: - AddSyntaxTypeReferences(roots, @operator.ReturnType, generatedTypeNames); - AddSyntaxTypeReferences(roots, @operator.ParameterList, generatedTypeNames); - break; - case PropertyDeclarationSyntax property: - AddSyntaxTypeReferences(roots, property.Type, generatedTypeNames); - break; - case IndexerDeclarationSyntax indexer: - AddSyntaxTypeReferences(roots, indexer.Type, generatedTypeNames); - AddSyntaxTypeReferences(roots, indexer.ParameterList, generatedTypeNames); - break; - case FieldDeclarationSyntax field: - AddSyntaxTypeReferences(roots, field.Declaration.Type, generatedTypeNames); - break; - case EventFieldDeclarationSyntax eventField: - AddSyntaxTypeReferences(roots, eventField.Declaration.Type, generatedTypeNames); - break; - case EventDeclarationSyntax @event: - AddSyntaxTypeReferences(roots, @event.Type, generatedTypeNames); - break; - case DelegateDeclarationSyntax @delegate: - AddSyntaxTypeReferences(roots, @delegate.ReturnType, generatedTypeNames); - AddSyntaxTypeReferences(roots, @delegate.ParameterList, generatedTypeNames); - AddSyntaxTypeReferences(roots, @delegate.ConstraintClauses, generatedTypeNames); - break; - case BaseTypeDeclarationSyntax type: - if (type.BaseList != null) - { - AddSyntaxTypeReferences(roots, type.BaseList, generatedTypeNames); - } - break; + yield return customCodeView; } } - private static void AddSyntaxTypeReferences(HashSet roots, SyntaxNode node, HashSet generatedTypeNames) + private static void AddCustomCodeExtensionRoots(HashSet roots, IReadOnlyList providers, HashSet nodes) { - foreach (var name in node.DescendantNodesAndSelf().OfType()) - { - AddMatchingName(roots, name.Identifier.ValueText, generatedTypeNames); - } - - foreach (var name in node.DescendantNodesAndSelf().OfType()) + foreach (var customCodeView in GetCustomCodeViews(providers)) { - AddMatchingName(roots, name.Identifier.ValueText, generatedTypeNames); + AddMatchingName(roots, $"{customCodeView.Type.Name}Extensions", nodes); } } - private static void AddSyntaxTypeReferences(HashSet roots, IEnumerable nodes, HashSet generatedTypeNames) + private static void AddCustomizationBackedExtensionRoots(HashSet roots, HashSet nodes) { foreach (var node in nodes) { - AddSyntaxTypeReferences(roots, node, generatedTypeNames); - } - } + var simpleName = GetSimpleName(node); + if (!simpleName.EndsWith("Extensions", StringComparison.Ordinal)) + { + continue; + } - private static IEnumerable GetCustomCodeViews(IReadOnlyList providers) - { - var visited = new HashSet(); - foreach (var provider in providers) - { - var customCodeView = provider.CustomCodeView; - if (customCodeView == null || !visited.Add(customCodeView)) + var namespaceName = GetNamespaceName(node); + if (namespaceName == null) { continue; } - yield return customCodeView; + var customTypeName = simpleName.Substring(0, simpleName.Length - "Extensions".Length); + if (CodeModelGenerator.Instance.SourceInputModel.FindForTypeInCustomization(namespaceName, customTypeName) != null) + { + roots.Add(node); + } } } @@ -535,6 +323,12 @@ private static void AddCustomCodeViewRoots(HashSet roots, TypeProvider c { AddTypeReference(roots, customCodeView.Type, generatedTypeNames); AddTypeReference(roots, customCodeView.BaseType, generatedTypeNames); + if (!publicOnly) + { + AddAttributes(roots, customCodeView.Attributes, generatedTypeNames, serializationProviderNamesByType: null); + AddMatchingName(roots, $"{customCodeView.Type.Name}Extensions", generatedTypeNames); + } + foreach (var implementedType in customCodeView.Implements) { AddTypeReference(roots, implementedType, generatedTypeNames); @@ -661,57 +455,6 @@ private static bool ContainsApiTypeReference(string apiText, HashSet api return apiDeclaredTypeNames.Contains(fullName); } - private static bool IsDeclaredInSyntaxTree(ISymbol symbol, SyntaxTree syntaxTree, Microsoft.CodeAnalysis.Text.TextSpan span) - { - foreach (var syntaxReference in symbol.DeclaringSyntaxReferences) - { - if (syntaxReference.SyntaxTree == syntaxTree && span.Contains(syntaxReference.Span)) - { - return true; - } - } - - return false; - } - - private static HashSet GetCustomCodeInternalGeneratedTypeDeclarations(Project project, HashSet generatedTypeNames) - { - var declarations = new HashSet(StringComparer.Ordinal); - var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); - if (compilation == null) - { - return declarations; - } - - foreach (var document in project.Documents) - { - if (IsGeneratedDocument(document)) - { - continue; - } - - var root = document.GetSyntaxRootAsync().GetAwaiter().GetResult(); - if (root == null) - { - continue; - } - - var semanticModel = compilation.GetSemanticModel(root.SyntaxTree); - foreach (var declaration in root.DescendantNodes().OfType()) - { - if (semanticModel.GetDeclaredSymbol(declaration) is not INamedTypeSymbol symbol || - symbol.DeclaredAccessibility != Accessibility.Internal) - { - continue; - } - - AddMatchingName(declarations, symbol.GetFullyQualifiedName(), generatedTypeNames); - } - } - - return declarations; - } - private static HashSet GetCustomCodeInternalGeneratedTypeDeclarations(IReadOnlyList providers, HashSet generatedTypeNames) { var declarations = new HashSet(StringComparer.Ordinal); @@ -728,42 +471,14 @@ private static HashSet GetCustomCodeInternalGeneratedTypeDeclarations(IR return declarations; } - private static HashSet GetGeneratedPersistableModelProxyTypeNames(Project project, HashSet generatedTypeNames) + private static HashSet GetGeneratedPersistableModelProxyTypeNames(IReadOnlyList providers, HashSet generatedTypeNames) { var proxyTypes = new HashSet(StringComparer.Ordinal); - var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); - if (compilation == null) - { - return proxyTypes; - } - - foreach (var document in project.Documents) + foreach (var provider in GetGeneratedProviders(providers)) { - if (!IsGeneratedDocument(document)) + if (provider.Attributes.Any(static attribute => IsAttributeNamed(attribute, "PersistableModelProxy"))) { - continue; - } - - var root = document.GetSyntaxRootAsync().GetAwaiter().GetResult(); - if (root == null) - { - continue; - } - - var semanticModel = compilation.GetSemanticModel(root.SyntaxTree); - foreach (var declaration in root.DescendantNodes().OfType()) - { - if (!declaration.AttributeLists - .SelectMany(static list => list.Attributes) - .Any(static attribute => attribute.Name.ToString().Contains("PersistableModelProxy", StringComparison.Ordinal))) - { - continue; - } - - if (semanticModel.GetDeclaredSymbol(declaration) is INamedTypeSymbol symbol) - { - AddMatchingName(proxyTypes, symbol.GetFullyQualifiedName(), generatedTypeNames); - } + AddTypeReference(proxyTypes, provider.Type, generatedTypeNames); } } @@ -771,27 +486,25 @@ private static HashSet GetGeneratedPersistableModelProxyTypeNames(Projec } private static HashSet GetGeneratedInternalTypeDeclarations(IReadOnlyList providers, HashSet generatedTypeNames) - => GetGeneratedTypeDeclarationsByLastContractAccessibility(providers, generatedTypeNames, Accessibility.Internal); + => GetGeneratedTypeDeclarationsByLastContractAccessibility(providers, generatedTypeNames, TypeSignatureModifiers.Internal); private static HashSet GetGeneratedPublicTypeDeclarations(IReadOnlyList providers, HashSet generatedTypeNames) - => GetGeneratedTypeDeclarationsByLastContractAccessibility(providers, generatedTypeNames, Accessibility.Public); + => GetGeneratedTypeDeclarationsByLastContractAccessibility(providers, generatedTypeNames, TypeSignatureModifiers.Public); private static HashSet GetGeneratedTypeDeclarationsByLastContractAccessibility( IReadOnlyList providers, HashSet generatedTypeNames, - Accessibility accessibility) + TypeSignatureModifiers accessibility) { var declarations = new HashSet(StringComparer.Ordinal); - var lastContract = CodeModelGenerator.Instance.SourceInputModel.LastContract; foreach (var provider in GetGeneratedProviders(providers)) { - var providerTypeName = GetProviderTypeName(provider.Type); - if (lastContract?.GetTypeByMetadataName(providerTypeName)?.DeclaredAccessibility != accessibility) + if (provider.LastContractView?.DeclarationModifiers.HasFlag(accessibility) != true) { continue; } - AddMatchingName(declarations, providerTypeName, generatedTypeNames); + AddTypeReference(declarations, provider.Type, generatedTypeNames); } return declarations; @@ -800,40 +513,6 @@ private static HashSet GetGeneratedTypeDeclarationsByLastContractAccessi private static IEnumerable GetGeneratedImplementationInternalTypeDeclarations(HashSet generatedInternalDeclarations) => generatedInternalDeclarations.Where(static name => GetSimpleName(name).StartsWith("Internal", StringComparison.Ordinal)); - private static void AddSymbolRoot(HashSet roots, ITypeSymbol? symbol, HashSet generatedTypeNames) - { - if (symbol is IArrayTypeSymbol arrayType) - { - AddSymbolRoot(roots, arrayType.ElementType, generatedTypeNames); - return; - } - - if (symbol is not INamedTypeSymbol namedType) - { - return; - } - - AddMatchingSymbolName(roots, namedType, generatedTypeNames); - foreach (var typeArgument in namedType.TypeArguments) - { - AddSymbolRoot(roots, typeArgument, generatedTypeNames); - } - } - - private static void AddMatchingSymbolName(HashSet target, INamedTypeSymbol symbol, HashSet generatedTypeNames) - { - try - { - AddMatchingName(target, symbol.GetFullyQualifiedName(), generatedTypeNames); - } - catch (ArgumentOutOfRangeException) - { - // Some custom-code symbols cannot be represented by the legacy fully-qualified-name - // helper. A simple-name match is enough to discover generated roots. - AddMatchingName(target, symbol.Name, generatedTypeNames); - } - } - private static ProviderReferenceGraph BuildGraph(IReadOnlyList generatedProviders, bool publicOnly = false) { // Each generated provider becomes a node, and provider metadata supplies the edges: @@ -944,6 +623,11 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList gen continue; } + if (!publicOnly && ShouldUseGeneratedSourceReferences(provider)) + { + continue; + } + if (publicOnly && !IsPublic(method.Signature.Modifiers)) { continue; @@ -1136,22 +820,12 @@ private static void AddGeneratedProvider(List generatedProviders, } } - private static void AddGeneratedBodyReferences(Project project, IReadOnlyList providers, ProviderReferenceGraph graph) + private static void AddGeneratedBodyReferences(IReadOnlyList providers, ProviderReferenceGraph graph) { - var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); - if (compilation == null) - { - return; - } - foreach (var provider in GetBodyReferenceProviders(providers)) { - if (IsModelFactoryProvider(provider)) - { - continue; - } - - if (!IsGeneratedBodyReferenceCandidate(provider)) + if (IsModelFactoryProvider(provider) || + !IsGeneratedBodyReferenceCandidate(provider)) { continue; } @@ -1162,231 +836,231 @@ private static void AddGeneratedBodyReferences(Project project, IReadOnlyList references, IReadOnlyList dependencies, HashSet nodes) + private static void AddProviderInfrastructureReferences(HashSet references, TypeProvider provider, HashSet nodes) { - foreach (var dependency in dependencies) + AddMatchingName(references, "ProviderConstants", nodes); + AddMatchingName(references, "TypeFormatters", nodes); + + if (provider.SerializationProviders.Count > 0) { - AddProviderBodyDependencyType(references, dependency, nodes); + AddSerializationExtensionReferences(references, provider, nodes); + } + + if (IsSerializationProvider(provider)) + { + AddMatchingName(references, "Optional", nodes); + AddMatchingName(references, "BinaryContentHelper", nodes); + AddMatchingName(references, "Utf8JsonRequestContent", nodes); + AddMatchingName(references, "ModelSerializationExtensions", nodes); + AddSerializationExtensionReferences(references, provider, nodes); + } + + foreach (var method in provider.Methods) + { + AddMethodInfrastructureReferences(references, method, nodes); } } - private static void AddProviderBodyDependencyType(HashSet references, CSharpType? dependency, HashSet nodes) + private static void AddSerializationExtensionReferences(HashSet references, TypeProvider provider, HashSet nodes) { - if (dependency == null) + AddSerializationExtensionReferences(references, provider.Type, nodes); + AddSerializationExtensionReferences(references, provider.BaseType, nodes); + foreach (var implementedType in provider.Implements) { - return; + AddSerializationExtensionReferences(references, implementedType, nodes); } - AddTypeReference(references, dependency, nodes); - AddMatchingName(references, $"{dependency.Name}Extensions", nodes); + foreach (var property in provider.Properties) + { + AddSerializationExtensionReferences(references, property.Type, nodes); + } - foreach (var argument in dependency.Arguments) + foreach (var field in provider.Fields) { - AddProviderBodyDependencyType(references, argument, nodes); + AddSerializationExtensionReferences(references, field.Type, nodes); } - } - private static IReadOnlyList GetBodyReferenceProviders(IReadOnlyList providers) - { - var bodyReferenceProviders = new List(); - foreach (var provider in providers) + foreach (var constructor in provider.Constructors) { - bodyReferenceProviders.Add(provider); - bodyReferenceProviders.AddRange(provider.SerializationProviders); + AddSerializationExtensionReferences(references, constructor.Signature.ReturnType, nodes); + foreach (var parameter in constructor.Signature.Parameters) + { + AddSerializationExtensionReferences(references, parameter.Type, nodes); + } } - return bodyReferenceProviders; + foreach (var method in provider.Methods) + { + AddSerializationExtensionReferences(references, method.Signature.ReturnType, nodes); + foreach (var parameter in method.Signature.Parameters) + { + AddSerializationExtensionReferences(references, parameter.Type, nodes); + } + } } - private static bool IsGeneratedBodyReferenceCandidate(TypeProvider provider) + private static void AddSerializationExtensionReferences(HashSet references, CSharpType? type, HashSet nodes) { - if (ShouldUseGeneratedSourceReferences(provider)) + if (type == null) { - return true; + return; } - if (provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Static)) + AddMatchingName(references, $"{type.Name}Extensions", nodes); + foreach (var argument in type.Arguments) { - return true; + AddSerializationExtensionReferences(references, argument, nodes); } - - var relativePath = provider.RelativeFilePath.Replace('\\', '/'); - return IsSerializationProvider(provider) || - relativePath.EndsWith("/Internal/ClientUriBuilder.cs", StringComparison.Ordinal) || - provider.HelperDependencyNames.Count > 0 || - provider.BodyDependencyTypes.Count > 0; } - private static bool ShouldUseGeneratedSourceReferences(TypeProvider provider) => - CodeModelGenerator.Instance.Configuration.PackageName.StartsWith("Azure.", StringComparison.Ordinal) && - provider.RelativeFilePath.EndsWith("Client.cs", StringComparison.Ordinal); + private static void AddMethodInfrastructureReferences(HashSet references, MethodProvider method, HashSet nodes) + { + AddReturnTypeInfrastructureReferences(references, method.Signature.ReturnType, nodes); + foreach (var parameter in method.Signature.Parameters) + { + AddRequestContentInfrastructureReferences(references, parameter.Type, nodes); + } + } - private static void AddGeneratedXmlDocCrefReferences(Project project, ProviderReferenceGraph graph, bool publicOnly) + private static void AddReturnTypeInfrastructureReferences(HashSet references, CSharpType? returnType, HashSet nodes) { - var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); - if (compilation == null) + var type = UnwrapTask(returnType); + if (type == null) { return; } - foreach (var providerName in graph.Nodes) + var typeName = StripGenericArity(type.Name); + if (string.Equals(typeName, "Pageable", StringComparison.Ordinal)) { - var symbol = compilation.GetTypeByMetadataName(providerName); - if (symbol == null || publicOnly && symbol.DeclaredAccessibility != Accessibility.Public) - { - continue; - } - - foreach (var property in symbol.GetMembers().OfType()) + AddMatchingName(references, "PageableWrapper", nodes); + } + else if (string.Equals(typeName, "AsyncPageable", StringComparison.Ordinal)) + { + AddMatchingName(references, "AsyncPageableWrapper", nodes); + } + else if (string.Equals(typeName, "ArmOperation", StringComparison.Ordinal)) + { + AddMatchingNamesWithSimpleNameSuffix(references, "ArmOperation", nodes); + AddMatchingNamesWithSimpleNameSuffix(references, "OperationSource", nodes); + if (type.Arguments.Count > 0) { - if (publicOnly && property.DeclaredAccessibility != Accessibility.Public) - { - continue; - } - - var xml = property.GetDocumentationCommentXml(); - if (string.IsNullOrEmpty(xml) || - !xml.Contains("cref=\"T:", StringComparison.Ordinal)) - { - continue; - } - - var cRefs = XDocument.Parse(xml) - .Descendants() - .Attributes("cref") - .Select(static attribute => attribute.Value) - .Where(static cref => cref.Length > 2 && cref[0] == 'T' && cref[1] == ':') - .Select(static cref => cref.Substring(2)); - - foreach (var cref in cRefs) - { - AddMatchingName(graph.References[providerName], cref, graph.Nodes); - } + AddMatchingName(references, $"{BuildOperationSourceTypeName(type.Arguments[0])}OperationSource", nodes); } } } - private static bool IsGeneratedHelperSimpleName(string name) => - string.Equals(name, "ChangeTrackingDictionary", StringComparison.Ordinal) || - string.Equals(name, "ChangeTrackingList", StringComparison.Ordinal); - - private static void AddGeneratedBodyTypeReferences(Project project, Compilation compilation, ProviderReferenceGraph graph, string ownerName, INamedTypeSymbol ownerSymbol) + private static void AddRequestContentInfrastructureReferences(HashSet references, CSharpType? type, HashSet nodes) { - foreach (var syntaxReference in ownerSymbol.DeclaringSyntaxReferences) + if (type == null) { - var document = project.GetDocument(syntaxReference.SyntaxTree); - if (document == null || !IsGeneratedDocument(document)) - { - continue; - } + return; + } - var root = syntaxReference.SyntaxTree.GetRoot(); - var semanticModel = compilation.GetSemanticModel(syntaxReference.SyntaxTree); - foreach (var node in root.DescendantNodes()) - { - if (node is TypeSyntax typeSyntax) - { - if (IsNamespaceOrUsingName(typeSyntax)) - { - continue; - } + if (string.Equals(type.Name, "RequestContent", StringComparison.Ordinal) || + string.Equals(type.Name, "BinaryData", StringComparison.Ordinal)) + { + AddMatchingName(references, "BinaryContentHelper", nodes); + AddMatchingName(references, "Utf8JsonRequestContent", nodes); + } - // Declaration names are the owner itself. The old Roslyn map captures references, - // not a declaration making itself reachable. - if (typeSyntax.Parent is BaseTypeDeclarationSyntax baseTypeDeclaration && baseTypeDeclaration.Identifier.Span == typeSyntax.Span) - { - continue; - } + foreach (var argument in type.Arguments) + { + AddRequestContentInfrastructureReferences(references, argument, nodes); + } + } - var typeInfo = semanticModel.GetTypeInfo(typeSyntax).Type; - AddBodyTypeReference(graph.References[ownerName], typeInfo, graph.Nodes); - AddUnresolvedBodyTypeSyntaxReference(graph.References[ownerName], typeSyntax, typeInfo, graph.Nodes); - } + private static CSharpType? UnwrapTask(CSharpType? type) + { + var typeName = type == null ? null : StripGenericArity(type.Name); + if ((string.Equals(typeName, "Task", StringComparison.Ordinal) || + string.Equals(typeName, "ValueTask", StringComparison.Ordinal)) && + type?.Arguments.Count > 0) + { + return type.Arguments[0]; + } - if (node is InvocationExpressionSyntax invocation) - { - if (semanticModel.GetSymbolInfo(invocation).Symbol is not IMethodSymbol method) - { - continue; - } + return type; + } - AddBodyTypeReference(graph.References[ownerName], method.ContainingType, graph.Nodes); - AddBodyTypeReference(graph.References[ownerName], method.ReducedFrom?.ContainingType, graph.Nodes); - } - } - } + private static string BuildOperationSourceTypeName(CSharpType type) + { + var argumentNames = string.Join("", type.Arguments.Select(BuildOperationSourceTypeName)); + return $"{type.Name}{(argumentNames.Length > 0 ? "Of" : string.Empty)}{argumentNames}"; } - private static bool IsNamespaceOrUsingName(TypeSyntax typeSyntax) + private static void AddProviderBodyDependencyTypes(HashSet references, IReadOnlyList dependencies, HashSet nodes) { - for (SyntaxNode? node = typeSyntax; node != null; node = node.Parent) + foreach (var dependency in dependencies) { - if (node.Parent is BaseNamespaceDeclarationSyntax namespaceDeclaration && namespaceDeclaration.Name == node || - node.Parent is UsingDirectiveSyntax usingDirective && usingDirective.Name == node) - { - return true; - } + AddProviderBodyDependencyType(references, dependency, nodes); } - - return false; } - private static void AddUnresolvedBodyTypeSyntaxReference(HashSet references, TypeSyntax typeSyntax, ITypeSymbol? symbol, HashSet nodes) + private static void AddProviderBodyDependencyType(HashSet references, CSharpType? dependency, HashSet nodes) { - if (symbol is INamedTypeSymbol { TypeKind: not TypeKind.Error }) + if (dependency == null) { return; } - var simpleName = typeSyntax switch - { - QualifiedNameSyntax qualifiedName => qualifiedName.Right.Identifier.ValueText, - AliasQualifiedNameSyntax aliasQualifiedName => aliasQualifiedName.Name.Identifier.ValueText, - GenericNameSyntax genericName => genericName.Identifier.ValueText, - IdentifierNameSyntax identifierName => identifierName.Identifier.ValueText, - _ => null - }; + AddTypeReference(references, dependency, nodes); + AddMatchingName(references, $"{dependency.Name}Extensions", nodes); - if (simpleName != null) + foreach (var argument in dependency.Arguments) { - AddMatchingName(references, simpleName, nodes); + AddProviderBodyDependencyType(references, argument, nodes); } } - private static void AddBodyTypeReference(HashSet references, ITypeSymbol? symbol, HashSet nodes) + private static IReadOnlyList GetBodyReferenceProviders(IReadOnlyList providers) { - if (symbol is not INamedTypeSymbol namedType || namedType.TypeKind == TypeKind.Error) + var bodyReferenceProviders = new List(); + foreach (var provider in providers) { - return; + bodyReferenceProviders.Add(provider); + bodyReferenceProviders.AddRange(provider.SerializationProviders); } - if (IsGeneratedHelperSimpleName(namedType.Name)) + return bodyReferenceProviders; + } + + private static bool IsGeneratedBodyReferenceCandidate(TypeProvider provider) + { + if (ShouldUseGeneratedSourceReferences(provider)) { - AddMatchingName(references, namedType.Name, nodes); + return true; } - AddMatchingName(references, namedType.GetFullyQualifiedName(), nodes); - if (namedType.TypeKind == TypeKind.Enum) + + if (provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Static)) { - AddMatchingName(references, $"{namedType.Name}Extensions", nodes); + return true; } - foreach (var typeArgument in namedType.TypeArguments) + var relativePath = provider.RelativeFilePath.Replace('\\', '/'); + return IsSerializationProvider(provider) || + relativePath.EndsWith("/Internal/ClientUriBuilder.cs", StringComparison.Ordinal) || + provider.HelperDependencyNames.Count > 0 || + provider.BodyDependencyTypes.Count > 0; + } + + private static bool ShouldUseGeneratedSourceReferences(TypeProvider provider) + { + if (!CodeModelGenerator.Instance.Configuration.PackageName.StartsWith("Azure.", StringComparison.Ordinal)) { - AddBodyTypeReference(references, typeArgument, nodes); + return false; } + + var relativePath = provider.RelativeFilePath.Replace('\\', '/'); + return relativePath.EndsWith("Client.cs", StringComparison.Ordinal) || + relativePath.Contains("/RestOperations/", StringComparison.Ordinal); } private static HashSet GetRootNames( @@ -1513,8 +1187,24 @@ private static HashSet GetPostProcessorDeclaredNodes(IReadOnlyList roots, HashSet nodes) => - roots.Contains(type.Name) || roots.Contains(GetProviderTypeName(type)) && nodes.Contains(GetProviderTypeName(type)); + private static bool IsKept(CSharpType type, HashSet roots, HashSet nodes) + { + var providerName = GetProviderTypeName(type); + if (roots.Contains(providerName) && nodes.Contains(providerName)) + { + return true; + } + + if (!roots.Contains(type.Name)) + { + return false; + } + + var simpleNameLookup = _simpleNameLookupCache.GetValue(nodes, BuildSimpleNameLookup); + return simpleNameLookup.TryGetValue(type.Name, out var matches) && + matches.Length == 1 && + string.Equals(matches[0], providerName, StringComparison.Ordinal); + } private static bool IsClientProviderRoot(TypeProvider provider, bool publicOnly) => provider.RelativeFilePath.EndsWith("Client.cs", StringComparison.Ordinal) && @@ -1629,9 +1319,9 @@ private static void AddHelperDependencies( private static void RemoveUnusedRequestHeaderExtensionsRoot( HashSet roots, IReadOnlyDictionary> references, - Project project) + IReadOnlyList providers) { - var hasCustomReference = HasCustomRequestHeaderExtensionsReference(project); + var hasCustomReference = HasCustomRequestHeaderExtensionsReference(providers); var unusedRequestHeaderExtensions = roots .Where(static root => root.EndsWith(".RequestHeaderExtensions", StringComparison.Ordinal)) .Where(_ => !hasCustomReference) @@ -1643,18 +1333,17 @@ private static void RemoveUnusedRequestHeaderExtensionsRoot( roots.ExceptWith(unusedRequestHeaderExtensions); } - private static bool HasCustomRequestHeaderExtensionsReference(Project project) + private static bool HasCustomRequestHeaderExtensionsReference(IReadOnlyList providers) { - foreach (var document in project.Documents) + foreach (var customCodeView in GetCustomCodeViews(providers)) { - if (IsGeneratedDocument(document)) - { - continue; - } - - var text = document.GetTextAsync().GetAwaiter().GetResult().ToString(); - if (text.Contains("RequestHeaderExtensions", StringComparison.Ordinal) || - text.Contains("SetDelimited", StringComparison.Ordinal)) + if (customCodeView.HelperDependencyNames.Any(IsRequestHeaderExtensionsDependency) || + customCodeView.BodyDependencyTypes.Any(IsRequestHeaderExtensionsDependency) || + customCodeView.Methods.Any(static method => + IsRequestHeaderExtensionsDependency(method.Signature.ReturnType) || + method.Signature.Parameters.Any(static parameter => IsRequestHeaderExtensionsDependency(parameter.Type))) || + customCodeView.Properties.Any(static property => IsRequestHeaderExtensionsDependency(property.Type)) || + customCodeView.Fields.Any(static field => IsRequestHeaderExtensionsDependency(field.Type))) { return true; } @@ -1663,24 +1352,26 @@ private static bool HasCustomRequestHeaderExtensionsReference(Project project) return false; } - private static bool IsSerializationProvider(TypeProvider provider) - { - var relativePath = provider.RelativeFilePath.Replace('\\', '/'); - return relativePath.EndsWith(".Serialization.cs", StringComparison.Ordinal) || - relativePath.EndsWith(".Serialization.Multipart.cs", StringComparison.Ordinal); - } + private static bool IsRequestHeaderExtensionsDependency(string name) + => string.Equals(name, "RequestHeaderExtensions", StringComparison.Ordinal) || + string.Equals(name, "SetDelimited", StringComparison.Ordinal); - private static bool IsGeneratedDocument(Document document) + private static bool IsRequestHeaderExtensionsDependency(CSharpType? type) { - if (GeneratedCodeWorkspace.IsGeneratedDocument(document) || GeneratedCodeWorkspace.IsGeneratedTestDocument(document)) + if (type == null) { - return true; + return false; } - var filePath = document.FilePath?.Replace('\\', '/'); - return filePath != null && - (filePath.Contains("/Generated/", StringComparison.Ordinal) || - filePath.Contains("/GeneratedTests/", StringComparison.Ordinal)); + return IsRequestHeaderExtensionsDependency(type.Name) || + type.Arguments.Any(IsRequestHeaderExtensionsDependency); + } + + private static bool IsSerializationProvider(TypeProvider provider) + { + var relativePath = provider.RelativeFilePath.Replace('\\', '/'); + return relativePath.EndsWith(".Serialization.cs", StringComparison.Ordinal) || + relativePath.EndsWith(".Serialization.Multipart.cs", StringComparison.Ordinal); } private static void AddInitializationHelperRoot(HashSet roots, CSharpType? type, HashSet nodes) @@ -1755,6 +1446,17 @@ private static void AddMatchingName(HashSet target, string name, HashSet } } + private static void AddMatchingNamesWithSimpleNameSuffix(HashSet target, string suffix, HashSet nodes) + { + foreach (var node in nodes) + { + if (GetSimpleName(node).EndsWith(suffix, StringComparison.Ordinal)) + { + target.Add(node); + } + } + } + private static Dictionary BuildSimpleNameLookup(HashSet nodes) { return nodes @@ -1892,6 +1594,10 @@ private static void AddAttributes( } } + private static bool IsAttributeNamed(AttributeStatement attribute, string name) + => string.Equals(attribute.Type.Name, name, StringComparison.Ordinal) || + string.Equals(attribute.Type.Name, $"{name}Attribute", StringComparison.Ordinal); + private static void AddAttributeArgumentReference( HashSet references, ValueExpression argument, @@ -1948,6 +1654,12 @@ private static string GetSimpleName(string fullyQualifiedName) return lastDot < 0 ? fullyQualifiedName : fullyQualifiedName.Substring(lastDot + 1); } + private static string? GetNamespaceName(string fullyQualifiedName) + { + var lastDot = fullyQualifiedName.LastIndexOf('.'); + return lastDot < 0 ? null : fullyQualifiedName.Substring(0, lastDot); + } + private static string GetProviderTypeName(CSharpType type) { var name = type.Arguments.Count > 0 && !type.Name.Contains('`', StringComparison.Ordinal) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapResult.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapResult.cs index dcfc2ec0ca9..eafe1d9d546 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapResult.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapResult.cs @@ -2,12 +2,10 @@ // Licensed under the MIT License. using System.Collections.Generic; -using Microsoft.CodeAnalysis; namespace Microsoft.TypeSpec.Generator { internal sealed record ProviderReferenceMapResult( - ProjectId ProjectId, HashSet InternalizeCandidates, HashSet PublicizeCandidates, HashSet RemoveCandidates) From 95e65cb0b9c0cde18e04485c040549e21e81d739 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Tue, 30 Jun 2026 03:16:26 +0000 Subject: [PATCH 088/111] docs(csharp): explain provider reference map flow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../PostProcessing/ProviderReferenceMapAnalyzer.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 0dd86742ced..b9ed26cc5e8 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -42,6 +42,8 @@ public static void ApplyPreWriteAccessibility(IReadOnlyList provid return; } + // Accessibility is applied to providers before TypeProviderWriter runs so generated source + // starts with the final public/internal shape instead of being rewritten by Roslyn later. var (internalizeCandidates, publicizeCandidates) = GetPreWriteAccessibilityCandidates(providers, customCodeProject); foreach (var provider in GetGeneratedProviders(providers)) { @@ -76,6 +78,9 @@ public static void RestorePreWriteModelFactoryMethods() public static void Analyze(IReadOnlyList providers, Project project) { var generatedProviders = GetGeneratedProviders(providers); + // The provider graph replaces the old Roslyn-generated-source reference map for generated + // code. We still keep custom-source syntax/symbol checks because users can reference + // generated types from arbitrary C# that is not represented by TypeProviders. var graph = BuildGraph(generatedProviders); var publicGraph = BuildGraph(generatedProviders, publicOnly: true); @@ -145,8 +150,9 @@ public static void Analyze(IReadOnlyList providers, Project projec .OrderBy(static name => name, StringComparer.Ordinal) .ToArray(); - // Body-only generated dependencies are needed to avoid deleting helper files, but they do - // not contribute to public API reachability for internalization. + // Body-only generated dependencies are intentionally added after public reachability is + // computed: they keep helper files from being removed but must not force public API types + // to remain public. AddGeneratedXmlDocCrefReferences(project, graph, publicOnly: false); AddGeneratedBodyReferences(project, providers, graph); @@ -830,6 +836,10 @@ private static void AddMatchingSymbolName(HashSet target, INamedTypeSymb private static ProviderReferenceGraph BuildGraph(IReadOnlyList generatedProviders, bool publicOnly = false) { + // Each generated provider becomes a node, and provider metadata supplies the edges: + // inheritance, signatures, properties, fields, nested/serialization providers, attributes, + // and selected implementation dependencies. This avoids parsing generated C# just to + // rediscover generated-to-generated references. var serializationProviderNamesByType = generatedProviders .Where(static provider => provider.SerializationProviders.Count > 0) .GroupBy(static provider => GetProviderTypeName(provider.Type), StringComparer.Ordinal) From 4c7cf5f40ca5fe4b8acd4bfe7462e777533692d6 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Tue, 30 Jun 2026 13:07:17 +0000 Subject: [PATCH 089/111] perf(csharp): align benchmark public root handling Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../PostProcessing/ProviderReferenceMapShadowAnalyzer.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs index 4357b69256f..49838482ebb 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs @@ -101,7 +101,9 @@ public static void Analyze(IReadOnlyList providers, Project projec var publicizeReachable = GetReachableTypes(publicizeRoots, internalizeReferences, publicApiTraversalNodes); var internalizeCandidates = internalizeDeclaredNodes .Except(publicizeReachable, StringComparer.Ordinal) - .Union(internalizeDeclaredNodes.Intersect(customInternalBoundaryNodes, StringComparer.Ordinal), StringComparer.Ordinal) + .Union(internalizeDeclaredNodes + .Intersect(customInternalBoundaryNodes, StringComparer.Ordinal) + .Except(publicizeRoots, StringComparer.Ordinal), StringComparer.Ordinal) .OrderBy(static name => name, StringComparer.Ordinal) .ToArray(); var publicizeCandidates = publicizeDeclaredNodes @@ -241,6 +243,8 @@ private static HashSet GetCustomCodePublicGeneratedTypeRoots(Project pro continue; } + AddMatchingName(roots, declaration.Identifier.ValueText, generatedTypeNames); + AddSymbolRoot(roots, symbol, generatedTypeNames); AddSymbolRoot(roots, symbol.BaseType, generatedTypeNames); foreach (var interfaceType in symbol.Interfaces) From cc036f3fb5c22c652f5a99d8f83a9188aeb4ea04 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 1 Jul 2026 00:15:39 +0000 Subject: [PATCH 090/111] fix(csharp): avoid hardcoded helper keep files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/CSharpGen.cs | 43 ++++++++++++++----- .../ProviderReferenceMapAnalyzer.cs | 6 +-- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index 4dba3b5a494..126acc6df0d 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs @@ -17,17 +17,11 @@ internal sealed class CSharpGen { private const string ConfigurationFileName = "Configuration.json"; private const string CodeModelFileName = "tspCodeModel.json"; - private const string RawRequestUriBuilderExtensionsFileName = "RawRequestUriBuilderExtensions.cs"; - private const string SerializationFormatFileName = "SerializationFormat.cs"; - private const string TypeFormattersFileName = "TypeFormatters.cs"; private static readonly string[] _filesToKeep = [ ConfigurationFileName, - CodeModelFileName, - RawRequestUriBuilderExtensionsFileName, - SerializationFormatFileName, - TypeFormattersFileName + CodeModelFileName ]; /// @@ -42,8 +36,8 @@ public async Task ExecuteAsync() var outputPath = CodeModelGenerator.Instance.Configuration.OutputDirectory; var generatedSourceOutputPath = CodeModelGenerator.Instance.Configuration.ProjectGeneratedDirectory; - // Resolve PackageReference items from the .csproj so custom code referencing - // external NuGet types (e.g., Azure.Storage.Common) compiles correctly. + // Resolve PackageReference items from the .csproj so custom code referencing external + // NuGet types compiles correctly. await GeneratedCodeWorkspace.AddPackageReferencesFromProject(); // Pre-walk the input library and resolve any external types that point at NuGet packages. @@ -131,7 +125,7 @@ await GeneratedCodeWorkspace.LoadBaselineContract(), LoggingHelpers.LogElapsedTime("All generated types have been written into memory"); // Delete any old generated files - DeleteDirectory(generatedSourceOutputPath, _filesToKeep); + DeleteDirectory(generatedSourceOutputPath, GetFilesToKeep()); LoggingHelpers.LogElapsedTime("All old generated files have been deleted"); @@ -203,6 +197,32 @@ private static void FilterCustomizedMembers(TypeProvider typeProvider) typeProvider.Fields); } + private static string[] GetFilesToKeep() + { + // Preserve generator-registered keep types generically; language-specific generators can + // opt in through AddTypeToKeep without MTG hardcoding their helper file names. + return _filesToKeep + .Concat( + CodeModelGenerator.Instance.AdditionalRootTypes + .Concat(CodeModelGenerator.Instance.NonRootTypes) + .Select(GetFileNameForType)) + .Distinct(StringComparer.Ordinal) + .ToArray(); + } + + private static string GetFileNameForType(string typeName) + { + var simpleNameStart = typeName.LastIndexOf('.') + 1; + var simpleName = simpleNameStart > 0 ? typeName[simpleNameStart..] : typeName; + var genericArityStart = simpleName.IndexOf('`'); + if (genericArityStart >= 0) + { + simpleName = simpleName[..genericArityStart]; + } + + return simpleName.EndsWith(".cs", StringComparison.Ordinal) ? simpleName : $"{simpleName}.cs"; + } + /// /// Clears the output directory specified by . If is not null, /// the specified files in the output directory will not be deleted. @@ -217,9 +237,10 @@ private static void DeleteDirectory(string path, string[] filesToKeep) return; } + var fileNamesToKeep = filesToKeep.ToHashSet(StringComparer.Ordinal); foreach (var file in directoryInfo.GetFiles("*", SearchOption.AllDirectories)) { - if (!filesToKeep.Contains(file.Name)) + if (!fileNamesToKeep.Contains(file.Name)) { file.Delete(); } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index b9ed26cc5e8..080d7f8564c 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -1231,8 +1231,8 @@ private static bool IsGeneratedBodyReferenceCandidate(TypeProvider provider) } private static bool ShouldUseGeneratedSourceReferences(TypeProvider provider) => - CodeModelGenerator.Instance.Configuration.PackageName.StartsWith("Azure.", StringComparison.Ordinal) && - provider.RelativeFilePath.EndsWith("Client.cs", StringComparison.Ordinal); + provider.RelativeFilePath.EndsWith("Client.cs", StringComparison.Ordinal) && + provider.BodyDependencyTypes.Count == 0; private static void AddGeneratedXmlDocCrefReferences(Project project, ProviderReferenceGraph graph, bool publicOnly) { @@ -1468,7 +1468,7 @@ private static void AddUnionVariantRoots(HashSet roots, IReadOnlyList - !CodeModelGenerator.Instance.Configuration.PackageName.StartsWith("Azure.", StringComparison.Ordinal) && + !HasApiBaselineDirectory() && CodeModelGenerator.Instance.SourceInputModel.LastContract == null; private static bool IsImplementationOnlyModelFactoryMethod(MethodProvider method) From 2b2c9991f539e88c67a0c51f43706bcf8557a55a Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 1 Jul 2026 00:23:13 +0000 Subject: [PATCH 091/111] refactor(csharp): share provider accessibility candidate logic Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ProviderReferenceMapAnalyzer.cs | 86 ++++++++----------- 1 file changed, 35 insertions(+), 51 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 080d7f8564c..9827d0c10d3 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -101,54 +101,15 @@ public static void Analyze(IReadOnlyList providers, Project projec // such as change-tracking dictionaries can still be removed when no reachable type needs them. var generatedDiscriminatorBaseNames = GetGeneratedPersistableModelProxyTypeNames(project, publicGraph.Nodes); AddGeneratedXmlDocCrefReferences(project, publicGraph, publicOnly: true); - var internalizeReferences = CloneReferences(publicGraph.References); - var internalizeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: false, includeAdditionalRoots: true, includeUnionVariantRoots: false, publicClientRootsOnly: true); - if (ShouldUseUnionVariantFallbackRoots()) - { - AddUnionVariantRoots(internalizeRoots, providers, graph.Nodes); - } - var generatedPublicReachable = GetReachableTypes(internalizeRoots, internalizeReferences); - AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences, generatedPublicReachable, generatedDiscriminatorBaseNames); - internalizeRoots.UnionWith(customPublicRoots); - var internalizeReachableWithoutHelpers = GetReachableTypes(internalizeRoots, internalizeReferences); - AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences, internalizeReachableWithoutHelpers, generatedDiscriminatorBaseNames); - internalizeReachableWithoutHelpers = GetReachableTypes(internalizeRoots, internalizeReferences); - var publicizeRoots = internalizeRoots.ToHashSet(StringComparer.Ordinal); - var internalizeHelperRoots = GetHelperRootNames(generatedProviders, graph.Nodes, internalizeReachableWithoutHelpers); - internalizeRoots.UnionWith(internalizeHelperRoots); - var internalizeReachable = GetReachableTypes(internalizeRoots, internalizeReferences); - var internalizeDeclaredNodes = GetPostProcessorDeclaredNodes(generatedProviders, graph.Nodes, publicOnly: true); - var customInternalBoundaryNodes = graph.Nodes - .Where(name => publicGraph.References.TryGetValue(name, out var references) && references.Overlaps(customInternalDeclarations)) - .ToHashSet(StringComparer.Ordinal); - var publicizeDeclaredNodes = GetPostProcessorDeclaredNodes(generatedProviders, graph.Nodes, publicOnly: false) - .Except(internalizeDeclaredNodes, StringComparer.Ordinal); - var generatedImplementationInternalDeclarations = GetGeneratedImplementationInternalTypeDeclarations(generatedInternalDeclarations).ToHashSet(StringComparer.Ordinal); - var publicApiTraversalNodes = internalizeDeclaredNodes - .Except(generatedInternalDeclarations, StringComparer.Ordinal) - .Concat(publicizeDeclaredNodes) - .Except(generatedImplementationInternalDeclarations, StringComparer.Ordinal) - .ToHashSet(StringComparer.Ordinal); - var publicizeReachable = GetReachableTypes(publicizeRoots, internalizeReferences, publicApiTraversalNodes); - var internalizeCandidates = internalizeDeclaredNodes - .Except(publicizeReachable, StringComparer.Ordinal) - .Union(internalizeDeclaredNodes - .Intersect(customInternalBoundaryNodes, StringComparer.Ordinal) - .Except(publicizeRoots, StringComparer.Ordinal), StringComparer.Ordinal) - .OrderBy(static name => name, StringComparer.Ordinal) - .ToArray(); - var publicizeCandidates = publicizeDeclaredNodes - .Except(customInternalDeclarations, StringComparer.Ordinal) - .Except(customInternalBoundaryNodes, StringComparer.Ordinal) - .Except(internalizeHelperRoots, StringComparer.Ordinal) - .Except(GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: true, includeAdditionalRoots: true, includeUnionVariantRoots: true, publicClientRootsOnly: true), StringComparer.Ordinal) - .Intersect(publicizeReachable, StringComparer.Ordinal) - // Preserve generated types that the last contract already made internal unless a public API/custom root explicitly requires them. - .Where(name => !generatedInternalDeclarations.Contains(name) || publicizeRoots.Contains(name)) - .Where(name => publicizeRoots.Contains(name) || - HasPublicApiPredecessor(name, internalizeReferences, publicizeReachable, generatedImplementationInternalDeclarations)) - .OrderBy(static name => name, StringComparer.Ordinal) - .ToArray(); + var (internalizeCandidates, publicizeCandidates, internalizeHelperRoots) = GetAccessibilityCandidates( + providers, + generatedProviders, + graph, + publicGraph, + customPublicRoots, + customInternalDeclarations, + generatedInternalDeclarations, + generatedDiscriminatorBaseNames); // Body-only generated dependencies are intentionally added after public reachability is // computed: they keep helper files from being removed but must not force public API types @@ -180,8 +141,8 @@ public static void Analyze(IReadOnlyList providers, Project projec _latestResult = new ProviderReferenceMapResult( project.Id, - internalizeCandidates.ToHashSet(StringComparer.Ordinal), - publicizeCandidates.ToHashSet(StringComparer.Ordinal), + internalizeCandidates, + publicizeCandidates, removeCandidates.ToHashSet(StringComparer.Ordinal)); } @@ -200,6 +161,29 @@ private static (HashSet InternalizeCandidates, HashSet Publicize var generatedInternalDeclarations = GetGeneratedInternalTypeDeclarations(generatedProviders, graph.Nodes); var generatedDiscriminatorBaseNames = new HashSet(StringComparer.Ordinal); + var (internalizeCandidates, publicizeCandidates, _) = GetAccessibilityCandidates( + providers, + generatedProviders, + graph, + publicGraph, + customPublicRoots, + customInternalDeclarations, + generatedInternalDeclarations, + generatedDiscriminatorBaseNames); + + return (internalizeCandidates, publicizeCandidates); + } + + private static (HashSet InternalizeCandidates, HashSet PublicizeCandidates, HashSet InternalizeHelperRoots) GetAccessibilityCandidates( + IReadOnlyList providers, + IReadOnlyList generatedProviders, + ProviderReferenceGraph graph, + ProviderReferenceGraph publicGraph, + HashSet customPublicRoots, + HashSet customInternalDeclarations, + HashSet generatedInternalDeclarations, + HashSet generatedDiscriminatorBaseNames) + { var internalizeReferences = CloneReferences(publicGraph.References); var internalizeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: false, includeAdditionalRoots: true, includeUnionVariantRoots: false, publicClientRootsOnly: true); if (ShouldUseUnionVariantFallbackRoots()) @@ -246,7 +230,7 @@ private static (HashSet InternalizeCandidates, HashSet Publicize HasPublicApiPredecessor(name, internalizeReferences, publicizeReachable, generatedImplementationInternalDeclarations)) .ToHashSet(StringComparer.Ordinal); - return (internalizeCandidates, publicizeCandidates); + return (internalizeCandidates, publicizeCandidates, internalizeHelperRoots); } private static HashSet GetCustomCodeGeneratedTypeRoots(Project project, HashSet generatedTypeNames) From 69cb3d277f83511039a67fd1367378f477915945 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 1 Jul 2026 00:37:08 +0000 Subject: [PATCH 092/111] fix(csharp): preserve custom request header helpers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ClientBodyDependencyPostProcessingTests.cs | 23 ++++++++++++++++ .../src/CSharpGen.cs | 13 ++++++--- .../PostProcessing/GeneratedCodeWorkspace.cs | 3 +++ .../ProviderReferenceMapAnalyzer.cs | 8 ++++-- .../test/PostProcessing/PostProcessorTests.cs | 27 ++++++------------- .../RootClient.cs | 15 +++++++++++ 6 files changed, 64 insertions(+), 25 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidUsingsKeepsFileHeader/RootClient.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs index 48ffd1a34d2..872eeed3c47 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs @@ -159,6 +159,29 @@ public partial class PublicCustomApi publicModelNames: ["GeneratedModel"]); } + [Test] + public async Task CustomOnlyRequestHeaderSetDelimitedReferenceKeepsExtensions() + { + await GenerateAndAssertFiles( + enums: [], + models: [], + clients: [], + customFiles: [ + (Path.Combine("src", "CustomHeaders.cs"), """ + using System.ClientModel.Primitives; + + namespace Sample; + + public static class CustomHeaders + { + public static void Add(PipelineRequestHeaders headers, string[] values) + => headers.SetDelimited("x-ms-custom", values, ","); + } + """) + ], + expectedFiles: [Path.Combine("src", "Generated", "Internal", "PipelineRequestHeadersExtensions.cs")]); + } + [Test] public async Task CustomizedEnumSerializationProviderIsKeptWhenModelSerializationUsesEnum() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index 126acc6df0d..73475be8383 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs @@ -142,14 +142,15 @@ await GeneratedCodeWorkspace.LoadBaselineContract(), generatedFiles.Add((file.Name, file.Text)); } - var usesRequestHeaderExtensions = generatedFiles.Any(static file => - !file.Name.EndsWith("RequestHeaderExtensions.cs", StringComparison.Ordinal) && - file.Text.Contains(".SetDelimited(", StringComparison.Ordinal)); + var usesRequestHeaderExtensions = generatedCodeWorkspace.HasCustomRequestHeaderExtensionsReference() || + generatedFiles.Any(static file => + !IsRequestHeadersExtensionsFile(file.Name) && + file.Text.Contains(".SetDelimited(", StringComparison.Ordinal)); // Write the generated files to the output directory foreach (var file in generatedFiles) { - if (file.Name.EndsWith("RequestHeaderExtensions.cs", StringComparison.Ordinal) && !usesRequestHeaderExtensions) + if (IsRequestHeadersExtensionsFile(file.Name) && !usesRequestHeaderExtensions) { continue; } @@ -172,6 +173,10 @@ await GeneratedCodeWorkspace.LoadBaselineContract(), LoggingHelpers.LogElapsedTime("All files have been written to disk"); } + private static bool IsRequestHeadersExtensionsFile(string fileName) => + fileName.EndsWith("RequestHeaderExtensions.cs", StringComparison.Ordinal) || + fileName.EndsWith("RequestHeadersExtensions.cs", StringComparison.Ordinal); + internal static void FilterAllCustomizedMembers(OutputLibrary output) { foreach (var typeProvider in output.TypeProviders) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index 967a0870a87..b7e41f239e1 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -89,6 +89,9 @@ internal void AnalyzeProviderReferenceMap(IReadOnlyList providers) ProviderReferenceMapAnalyzer.Analyze(providers, _project); } + internal bool HasCustomRequestHeaderExtensionsReference() => + ProviderReferenceMapAnalyzer.HasCustomRequestHeaderExtensionsReference(_project); + internal void ApplyPreWriteAccessibility(IReadOnlyList providers) { ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility(providers, _project); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 9827d0c10d3..eeaaa78bf7c 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -1617,7 +1617,7 @@ private static void RemoveUnusedRequestHeaderExtensionsRoot( { var hasCustomReference = HasCustomRequestHeaderExtensionsReference(project); var unusedRequestHeaderExtensions = roots - .Where(static root => root.EndsWith(".RequestHeaderExtensions", StringComparison.Ordinal)) + .Where(IsRequestHeadersExtensionsRoot) .Where(_ => !hasCustomReference) .Where(root => !references.Any(reference => !string.Equals(reference.Key, root, StringComparison.Ordinal) && @@ -1627,7 +1627,11 @@ private static void RemoveUnusedRequestHeaderExtensionsRoot( roots.ExceptWith(unusedRequestHeaderExtensions); } - private static bool HasCustomRequestHeaderExtensionsReference(Project project) + private static bool IsRequestHeadersExtensionsRoot(string root) => + root.EndsWith(".RequestHeaderExtensions", StringComparison.Ordinal) || + root.EndsWith(".RequestHeadersExtensions", StringComparison.Ordinal); + + internal static bool HasCustomRequestHeaderExtensionsReference(Project project) { foreach (var document in project.Documents) { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/PostProcessorTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/PostProcessorTests.cs index 4100756ff15..4800fb26abe 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/PostProcessorTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/PostProcessorTests.cs @@ -77,23 +77,14 @@ public async Task RemovesInvalidUsingsKeepsFileHeader() }); var project = workspace.AddProject(projectInfo); - project = AddGeneratedDocument(project, "RootClient.cs", "src", "Generated", "RootClient.cs", """ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// - -#nullable disable - -using Missing.Namespace; - -namespace Sample -{ - public partial class RootClient - { - } -} -"""); + var folder = Helpers.GetAssetFileOrDirectoryPath(false); + project = AddGeneratedDocument( + project, + "RootClient.cs", + "src", + "Generated", + "RootClient.cs", + File.ReadAllText(Path.Join(folder, "RootClient.cs"))); var postProcessor = new TestPostProcessor("RootClient.cs"); var resultProject = await postProcessor.RemoveAsync(project); @@ -105,8 +96,6 @@ public partial class RootClient StringAssert.Contains("#nullable disable", text); StringAssert.DoesNotContain("using Missing.Namespace;", text); } - - [Test] public async Task DoesNotRemoveValidUsings() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidUsingsKeepsFileHeader/RootClient.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidUsingsKeepsFileHeader/RootClient.cs new file mode 100644 index 00000000000..572d6f590ac --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidUsingsKeepsFileHeader/RootClient.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// + +#nullable disable + +using Missing.Namespace; + +namespace Sample +{ + public partial class RootClient + { + } +} From 762a02b28817eb19c0db89bef383843154e1e655 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 1 Jul 2026 00:44:30 +0000 Subject: [PATCH 093/111] refactor(csharp): use provider markers in reference map Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/Providers/ClientProvider.cs | 1 + .../Providers/ClientUriBuilderDefinition.cs | 1 + .../ProviderReferenceMapAnalyzer.cs | 40 ++++++------------- .../src/Providers/TypeProvider.cs | 4 ++ 4 files changed, 18 insertions(+), 28 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs index 9ffe30f913e..d684331a438 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs @@ -43,6 +43,7 @@ private record ApiVersionFields(FieldProvider Field, PropertyProvider? Correspon private const string ClientSuffix = "Client"; private readonly FormattableString _publicCtorDescription; private readonly InputClient _inputClient; + protected override bool IsClientProvider => true; internal InputClient InputClient => _inputClient; private readonly InputAuth? _inputAuth; private readonly ParameterProvider _endpointParameter; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientUriBuilderDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientUriBuilderDefinition.cs index 92e0cf23a67..4dc5c283cc7 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientUriBuilderDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientUriBuilderDefinition.cs @@ -28,6 +28,7 @@ internal sealed class ClientUriBuilderDefinition : TypeProvider private readonly FieldProvider _uriBuilderField; private readonly FieldProvider _pathAndQueryField; private readonly FieldProvider _pathLengthField; + protected override bool IncludeGeneratedBodyReferences => true; private PropertyProvider? _uriBuilderProperty; private PropertyProvider UriBuilderProperty => _uriBuilderProperty ??= new( diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index eeaaa78bf7c..b1fc7a9357c 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -1128,14 +1128,14 @@ private static void AddGeneratedBodyReferences(Project project, IReadOnlyList references, CS } } - private static IReadOnlyList GetBodyReferenceProviders(IReadOnlyList providers) + private static IReadOnlyList<(TypeProvider Provider, bool IsSerializationProvider)> GetBodyReferenceProviders(IReadOnlyList providers) { - var bodyReferenceProviders = new List(); + var bodyReferenceProviders = new List<(TypeProvider Provider, bool IsSerializationProvider)>(); foreach (var provider in providers) { - bodyReferenceProviders.Add(provider); - bodyReferenceProviders.AddRange(provider.SerializationProviders); + bodyReferenceProviders.Add((provider, false)); + bodyReferenceProviders.AddRange(provider.SerializationProviders.Select(static serializationProvider => (serializationProvider, true))); } return bodyReferenceProviders; } - private static bool IsGeneratedBodyReferenceCandidate(TypeProvider provider) + private static bool IsGeneratedBodyReferenceCandidate(TypeProvider provider, bool isSerializationProvider) { if (ShouldUseGeneratedSourceReferences(provider)) { @@ -1207,15 +1207,14 @@ private static bool IsGeneratedBodyReferenceCandidate(TypeProvider provider) return true; } - var relativePath = provider.RelativeFilePath.Replace('\\', '/'); - return IsSerializationProvider(provider) || - relativePath.EndsWith("/Internal/ClientUriBuilder.cs", StringComparison.Ordinal) || + return isSerializationProvider || + provider.IncludeGeneratedBodyReferences || provider.HelperDependencyNames.Count > 0 || provider.BodyDependencyTypes.Count > 0; } private static bool ShouldUseGeneratedSourceReferences(TypeProvider provider) => - provider.RelativeFilePath.EndsWith("Client.cs", StringComparison.Ordinal) && + provider.IsClientProvider && provider.BodyDependencyTypes.Count == 0; private static void AddGeneratedXmlDocCrefReferences(Project project, ProviderReferenceGraph graph, bool publicOnly) @@ -1501,7 +1500,7 @@ private static bool IsKept(CSharpType type, HashSet roots, HashSet - provider.RelativeFilePath.EndsWith("Client.cs", StringComparison.Ordinal) && + provider.IsClientProvider && (!publicOnly || !HasApiBaselineDirectory() && provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); private static bool HasApiBaselineDirectory() @@ -1512,15 +1511,7 @@ private static bool HasApiBaselineDirectory() } private static bool IsModelFactoryProvider(TypeProvider provider) - { - if (provider is ModelFactoryProvider) - { - return true; - } - - var relativePath = provider.RelativeFilePath.Replace('\\', '/'); - return relativePath.EndsWith("ModelFactory.cs", StringComparison.Ordinal); - } + => provider is ModelFactoryProvider; private static HashSet GetHelperRootNames( IReadOnlyList providers, @@ -1651,13 +1642,6 @@ internal static bool HasCustomRequestHeaderExtensionsReference(Project project) return false; } - private static bool IsSerializationProvider(TypeProvider provider) - { - var relativePath = provider.RelativeFilePath.Replace('\\', '/'); - return relativePath.EndsWith(".Serialization.cs", StringComparison.Ordinal) || - relativePath.EndsWith(".Serialization.Multipart.cs", StringComparison.Ordinal); - } - private static bool IsGeneratedDocument(Document document) { if (GeneratedCodeWorkspace.IsGeneratedDocument(document) || GeneratedCodeWorkspace.IsGeneratedTestDocument(document)) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs index 6996d39a34e..e47a50a7754 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs @@ -307,6 +307,10 @@ private IReadOnlyList ApplyCustomizationFilter(IEnumerable BodyDependencyTypes => _bodyDependencyTypes ??= BuildBodyDependencyTypes(); protected internal virtual IReadOnlyList BuildBodyDependencyTypes() => []; + protected internal virtual bool IsClientProvider => false; + + protected internal virtual bool IncludeGeneratedBodyReferences => false; + private IReadOnlyList? _attributes; public IReadOnlyList Attributes From dc5b534431e1ac7ba47a8c378ecaf07348cc10e7 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 1 Jul 2026 00:56:53 +0000 Subject: [PATCH 094/111] refactor(csharp): use typed helper dependencies Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/Providers/ClientProvider.cs | 4 +-- .../MrwSerializationTypeDefinition.cs | 4 +-- ...ultipartFormDataSerializationDefinition.cs | 4 +-- .../src/Providers/RestClientProvider.cs | 23 ++++++++++------ .../ProviderReferenceMapAnalyzer.cs | 26 +++++++++++-------- .../src/Providers/TypeProvider.cs | 12 ++++++--- 6 files changed, 45 insertions(+), 28 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs index d684331a438..e36b28f5e12 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs @@ -427,13 +427,13 @@ private IReadOnlyList GetClientParameters() protected override string BuildName() => _inputClient.IsExactName ? _inputClient.Name : _inputClient.Name.ToIdentifierName(); - protected override IReadOnlyList BuildHelperDependencyNames() + protected override IReadOnlyList BuildHelperDependencyTypes() { foreach (var method in Methods.OfType()) { if (method.BodyStatements != null) { - return ["CancellationTokenExtensions", "ClientPipelineExtensions"]; + return [new CancellationTokenExtensionsDefinition().Type, new ClientPipelineExtensionsDefinition().Type]; } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs index 3ef80cfe3fa..2ce74c21ad7 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs @@ -117,8 +117,8 @@ public MrwSerializationTypeDefinition(InputModelType inputModel, ModelProvider m protected override CSharpType? BuildBaseType() => _model.BaseType; - protected override IReadOnlyList BuildHelperDependencyNames() => _rawDataField != null || _additionalProperties.Value.Length > 0 - ? ["ChangeTrackingDictionary"] + protected override IReadOnlyList BuildHelperDependencyTypes() => _rawDataField != null || _additionalProperties.Value.Length > 0 + ? [ChangeTrackingDictionaryType] : []; protected override SuppressionStatement[] BuildDisabledFileWarnings() diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MultipartFormDataSerializationDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MultipartFormDataSerializationDefinition.cs index e390ab24707..398ffc329a3 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MultipartFormDataSerializationDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MultipartFormDataSerializationDefinition.cs @@ -51,10 +51,10 @@ protected override string BuildRelativeFilePath() return Path.Combine("src", "Generated", "Models", $"{Name}.Serialization.Multipart.cs"); } - protected override IReadOnlyList BuildHelperDependencyNames() => _model.Properties.Any( + protected override IReadOnlyList BuildHelperDependencyTypes() => _model.Properties.Any( prop => prop.WireInfo != null && !prop.WireInfo.IsRequired && (prop.Type is { IsCollection: true, IsReadOnlyMemory: false } || prop.Type.IsDictionary)) - ? ["Optional"] + ? [OptionalType] : []; protected override SuppressionStatement[] BuildDisabledFileWarnings() diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs index f67b6814cf2..8656d3865c5 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs @@ -78,12 +78,9 @@ protected override FieldProvider[] BuildFields() return [.. pipelineMessage20xClassifiersFields]; } - protected override IReadOnlyList BuildHelperDependencyNames() + protected override IReadOnlyList BuildHelperDependencyTypes() { - var dependencies = new HashSet(StringComparer.Ordinal) - { - "ClientUriBuilder" - }; + var dependencies = new List { new ClientUriBuilderDefinition().Type }; foreach (var serviceMethod in _inputClient.Methods) { foreach (var parameter in serviceMethod.Operation.Parameters) @@ -96,16 +93,26 @@ protected override IReadOnlyList BuildHelperDependencyNames() var type = ScmCodeModelGenerator.Instance.TypeFactory.CreateCSharpType(parameter.Type); if (type?.IsDictionary == true) { - dependencies.Add("ChangeTrackingDictionary"); + AddDependency(dependencies, ChangeTrackingDictionaryType); } else if (type?.IsCollection == true) { - dependencies.Add("ChangeTrackingList"); + AddDependency(dependencies, ChangeTrackingListType); } } } - return [.. dependencies]; + return dependencies; + } + + private static void AddDependency(List dependencies, CSharpType dependency) + { + if (!dependencies.Any(existing => + existing.Name == dependency.Name && + existing.Namespace == dependency.Namespace)) + { + dependencies.Add(dependency); + } } protected override ScmMethodProvider[] BuildMethods() diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index b1fc7a9357c..a4ef4fa4847 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -1209,7 +1209,7 @@ private static bool IsGeneratedBodyReferenceCandidate(TypeProvider provider, boo return isSerializationProvider || provider.IncludeGeneratedBodyReferences || - provider.HelperDependencyNames.Count > 0 || + provider.HelperDependencyTypes.Count > 0 || provider.BodyDependencyTypes.Count > 0; } @@ -1263,8 +1263,12 @@ private static void AddGeneratedXmlDocCrefReferences(Project project, ProviderRe } private static bool IsGeneratedHelperSimpleName(string name) => - string.Equals(name, "ChangeTrackingDictionary", StringComparison.Ordinal) || - string.Equals(name, "ChangeTrackingList", StringComparison.Ordinal); + string.Equals(name, ChangeTrackingDictionaryType.Name, StringComparison.Ordinal) || + string.Equals(name, ChangeTrackingListType.Name, StringComparison.Ordinal); + + private static CSharpType ChangeTrackingDictionaryType => new ChangeTrackingDictionaryDefinition().Type; + + private static CSharpType ChangeTrackingListType => new ChangeTrackingListDefinition().Type; private static void AddGeneratedBodyTypeReferences(Project project, Compilation compilation, ProviderReferenceGraph graph, string ownerName, INamedTypeSymbol ownerSymbol) { @@ -1529,7 +1533,7 @@ private static HashSet GetHelperRootNames( continue; } - AddHelperDependencies(roots, provider.HelperDependencyNames, nodes, references == null ? null : references[providerName]); + AddHelperDependencies(roots, provider.HelperDependencyTypes, nodes, references == null ? null : references[providerName]); foreach (var property in provider.Properties) { @@ -1583,7 +1587,7 @@ private static void AddParameterValidationHelperRoot(HashSet roots, Para private static void AddHelperDependencies( HashSet roots, - IReadOnlyList dependencies, + IReadOnlyList dependencies, HashSet nodes, HashSet? referencedNames) { @@ -1591,12 +1595,12 @@ private static void AddHelperDependencies( { if (referencedNames == null) { - AddMatchingName(roots, dependency, nodes); + AddTypeReference(roots, dependency, nodes); continue; } var matches = new HashSet(StringComparer.Ordinal); - AddMatchingName(matches, dependency, nodes); + AddTypeReference(matches, dependency, nodes); roots.UnionWith(matches.Intersect(referencedNames, StringComparer.Ordinal)); } } @@ -1670,12 +1674,12 @@ private static void AddInitializationHelperRoot(HashSet roots, CSharpTyp if (type is { IsList: true, IsReadOnlyMemory: false }) { - AddMatchingName(roots, "ChangeTrackingList", nodes); + AddTypeReference(roots, ChangeTrackingListType, nodes); } if (type.IsDictionary) { - AddMatchingName(roots, "ChangeTrackingDictionary", nodes); + AddTypeReference(roots, ChangeTrackingDictionaryType, nodes); } foreach (var argument in type.Arguments) @@ -1693,12 +1697,12 @@ private static void AddModelFactoryCollectionInitializationHelperRoot(HashSet ApplyCustomizationFilter(IEnumerable SerializationProviders => _serializationProviders ??= BuildSerializationProviders(); - private IReadOnlyList? _helperDependencyNames; - internal IReadOnlyList HelperDependencyNames => _helperDependencyNames ??= BuildHelperDependencyNames(); - protected internal virtual IReadOnlyList BuildHelperDependencyNames() => []; + private IReadOnlyList? _helperDependencyTypes; + internal IReadOnlyList HelperDependencyTypes => _helperDependencyTypes ??= BuildHelperDependencyTypes(); + protected internal virtual IReadOnlyList BuildHelperDependencyTypes() => []; + + protected CSharpType ChangeTrackingDictionaryType => new ChangeTrackingDictionaryDefinition().Type; + + protected CSharpType ChangeTrackingListType => new ChangeTrackingListDefinition().Type; + + protected CSharpType OptionalType => new OptionalDefinition().Type; private IReadOnlyList? _bodyDependencyTypes; internal IReadOnlyList BodyDependencyTypes => _bodyDependencyTypes ??= BuildBodyDependencyTypes(); From 0a170d306683c9a28327aef927ae4251c6f59b16 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 1 Jul 2026 01:07:55 +0000 Subject: [PATCH 095/111] refactor(csharp): root request header helpers from providers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/Providers/RestClientProvider.cs | 7 ++++ .../src/CSharpGen.cs | 15 +------ .../PostProcessing/GeneratedCodeWorkspace.cs | 3 -- .../ProviderReferenceMapAnalyzer.cs | 42 ------------------- 4 files changed, 8 insertions(+), 59 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs index 8656d3865c5..97aa778a707 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs @@ -91,6 +91,13 @@ protected override IReadOnlyList BuildHelperDependencyTypes() } var type = ScmCodeModelGenerator.Instance.TypeFactory.CreateCSharpType(parameter.Type); + if (parameter is InputHeaderParameter headerParameter && + type?.IsCollection == true && + !(type.IsDictionary && !string.IsNullOrEmpty(headerParameter.CollectionHeaderPrefix))) + { + AddDependency(dependencies, new PipelineRequestHeadersExtensionsDefinition().Type); + } + if (type?.IsDictionary == true) { AddDependency(dependencies, ChangeTrackingDictionaryType); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index 73475be8383..be2c6f2400b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs @@ -142,19 +142,9 @@ await GeneratedCodeWorkspace.LoadBaselineContract(), generatedFiles.Add((file.Name, file.Text)); } - var usesRequestHeaderExtensions = generatedCodeWorkspace.HasCustomRequestHeaderExtensionsReference() || - generatedFiles.Any(static file => - !IsRequestHeadersExtensionsFile(file.Name) && - file.Text.Contains(".SetDelimited(", StringComparison.Ordinal)); - // Write the generated files to the output directory foreach (var file in generatedFiles) { - if (IsRequestHeadersExtensionsFile(file.Name) && !usesRequestHeaderExtensions) - { - continue; - } - var filename = Path.Combine(outputPath, file.Name); CodeModelGenerator.Instance.Emitter.Info($"Writing {Path.GetFullPath(filename)}"); Directory.CreateDirectory(Path.GetDirectoryName(filename)!); @@ -173,10 +163,6 @@ await GeneratedCodeWorkspace.LoadBaselineContract(), LoggingHelpers.LogElapsedTime("All files have been written to disk"); } - private static bool IsRequestHeadersExtensionsFile(string fileName) => - fileName.EndsWith("RequestHeaderExtensions.cs", StringComparison.Ordinal) || - fileName.EndsWith("RequestHeadersExtensions.cs", StringComparison.Ordinal); - internal static void FilterAllCustomizedMembers(OutputLibrary output) { foreach (var typeProvider in output.TypeProviders) @@ -217,6 +203,7 @@ private static string[] GetFilesToKeep() private static string GetFileNameForType(string typeName) { + // TypeProvider-generated files are named after the simple type name by convention. var simpleNameStart = typeName.LastIndexOf('.') + 1; var simpleName = simpleNameStart > 0 ? typeName[simpleNameStart..] : typeName; var genericArityStart = simpleName.IndexOf('`'); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index b7e41f239e1..967a0870a87 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -89,9 +89,6 @@ internal void AnalyzeProviderReferenceMap(IReadOnlyList providers) ProviderReferenceMapAnalyzer.Analyze(providers, _project); } - internal bool HasCustomRequestHeaderExtensionsReference() => - ProviderReferenceMapAnalyzer.HasCustomRequestHeaderExtensionsReference(_project); - internal void ApplyPreWriteAccessibility(IReadOnlyList providers) { ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility(providers, _project); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index a4ef4fa4847..01ebabab94e 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -126,7 +126,6 @@ public static void Analyze(IReadOnlyList providers, Project projec includeUnionVariantRoots: ShouldUseUnionVariantFallbackRoots(), publicClientRootsOnly: false); removeRoots.UnionWith(customRemovalRoots); - RemoveUnusedRequestHeaderExtensionsRoot(removeRoots, graph.References, project); var removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); AddDerivedModelReferences(providers, graph.Nodes, graph.References, removeReachableWithoutHelpers, generatedDiscriminatorBaseNames); removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); @@ -1605,47 +1604,6 @@ private static void AddHelperDependencies( } } - private static void RemoveUnusedRequestHeaderExtensionsRoot( - HashSet roots, - IReadOnlyDictionary> references, - Project project) - { - var hasCustomReference = HasCustomRequestHeaderExtensionsReference(project); - var unusedRequestHeaderExtensions = roots - .Where(IsRequestHeadersExtensionsRoot) - .Where(_ => !hasCustomReference) - .Where(root => !references.Any(reference => - !string.Equals(reference.Key, root, StringComparison.Ordinal) && - reference.Value.Contains(root))) - .ToArray(); - - roots.ExceptWith(unusedRequestHeaderExtensions); - } - - private static bool IsRequestHeadersExtensionsRoot(string root) => - root.EndsWith(".RequestHeaderExtensions", StringComparison.Ordinal) || - root.EndsWith(".RequestHeadersExtensions", StringComparison.Ordinal); - - internal static bool HasCustomRequestHeaderExtensionsReference(Project project) - { - foreach (var document in project.Documents) - { - if (IsGeneratedDocument(document)) - { - continue; - } - - var text = document.GetTextAsync().GetAwaiter().GetResult().ToString(); - if (text.Contains("RequestHeaderExtensions", StringComparison.Ordinal) || - text.Contains("SetDelimited", StringComparison.Ordinal)) - { - return true; - } - } - - return false; - } - private static bool IsGeneratedDocument(Document document) { if (GeneratedCodeWorkspace.IsGeneratedDocument(document) || GeneratedCodeWorkspace.IsGeneratedTestDocument(document)) From e1a5d68b7958c0404b122ac5d5bee729aeaa2498 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 1 Jul 2026 00:15:39 +0000 Subject: [PATCH 096/111] fix(csharp): avoid hardcoded helper keep files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/CSharpGen.cs | 43 ++++++++++++++----- .../ProviderReferenceMapAnalyzer.cs | 2 +- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index 4dba3b5a494..126acc6df0d 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs @@ -17,17 +17,11 @@ internal sealed class CSharpGen { private const string ConfigurationFileName = "Configuration.json"; private const string CodeModelFileName = "tspCodeModel.json"; - private const string RawRequestUriBuilderExtensionsFileName = "RawRequestUriBuilderExtensions.cs"; - private const string SerializationFormatFileName = "SerializationFormat.cs"; - private const string TypeFormattersFileName = "TypeFormatters.cs"; private static readonly string[] _filesToKeep = [ ConfigurationFileName, - CodeModelFileName, - RawRequestUriBuilderExtensionsFileName, - SerializationFormatFileName, - TypeFormattersFileName + CodeModelFileName ]; /// @@ -42,8 +36,8 @@ public async Task ExecuteAsync() var outputPath = CodeModelGenerator.Instance.Configuration.OutputDirectory; var generatedSourceOutputPath = CodeModelGenerator.Instance.Configuration.ProjectGeneratedDirectory; - // Resolve PackageReference items from the .csproj so custom code referencing - // external NuGet types (e.g., Azure.Storage.Common) compiles correctly. + // Resolve PackageReference items from the .csproj so custom code referencing external + // NuGet types compiles correctly. await GeneratedCodeWorkspace.AddPackageReferencesFromProject(); // Pre-walk the input library and resolve any external types that point at NuGet packages. @@ -131,7 +125,7 @@ await GeneratedCodeWorkspace.LoadBaselineContract(), LoggingHelpers.LogElapsedTime("All generated types have been written into memory"); // Delete any old generated files - DeleteDirectory(generatedSourceOutputPath, _filesToKeep); + DeleteDirectory(generatedSourceOutputPath, GetFilesToKeep()); LoggingHelpers.LogElapsedTime("All old generated files have been deleted"); @@ -203,6 +197,32 @@ private static void FilterCustomizedMembers(TypeProvider typeProvider) typeProvider.Fields); } + private static string[] GetFilesToKeep() + { + // Preserve generator-registered keep types generically; language-specific generators can + // opt in through AddTypeToKeep without MTG hardcoding their helper file names. + return _filesToKeep + .Concat( + CodeModelGenerator.Instance.AdditionalRootTypes + .Concat(CodeModelGenerator.Instance.NonRootTypes) + .Select(GetFileNameForType)) + .Distinct(StringComparer.Ordinal) + .ToArray(); + } + + private static string GetFileNameForType(string typeName) + { + var simpleNameStart = typeName.LastIndexOf('.') + 1; + var simpleName = simpleNameStart > 0 ? typeName[simpleNameStart..] : typeName; + var genericArityStart = simpleName.IndexOf('`'); + if (genericArityStart >= 0) + { + simpleName = simpleName[..genericArityStart]; + } + + return simpleName.EndsWith(".cs", StringComparison.Ordinal) ? simpleName : $"{simpleName}.cs"; + } + /// /// Clears the output directory specified by . If is not null, /// the specified files in the output directory will not be deleted. @@ -217,9 +237,10 @@ private static void DeleteDirectory(string path, string[] filesToKeep) return; } + var fileNamesToKeep = filesToKeep.ToHashSet(StringComparer.Ordinal); foreach (var file in directoryInfo.GetFiles("*", SearchOption.AllDirectories)) { - if (!filesToKeep.Contains(file.Name)) + if (!fileNamesToKeep.Contains(file.Name)) { file.Delete(); } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index cd0c5dbea68..811f0619277 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -1142,7 +1142,7 @@ private static void AddUnionVariantRoots(HashSet roots, IReadOnlyList - !CodeModelGenerator.Instance.Configuration.PackageName.StartsWith("Azure.", StringComparison.Ordinal) && + !HasApiBaselineDirectory() && CodeModelGenerator.Instance.SourceInputModel.LastContract == null; private static bool IsImplementationOnlyModelFactoryMethod(MethodProvider method) From 62dff6f17dd05e620bbffbdb5914cbe8dae8756b Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 1 Jul 2026 00:23:13 +0000 Subject: [PATCH 097/111] refactor(csharp): share provider accessibility candidate logic Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ProviderReferenceMapAnalyzer.cs | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 811f0619277..789317fc723 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -190,6 +190,29 @@ private static (HashSet InternalizeCandidates, HashSet Publicize var generatedInternalDeclarations = GetGeneratedInternalTypeDeclarations(generatedProviders, graph.Nodes); var generatedDiscriminatorBaseNames = new HashSet(StringComparer.Ordinal); + var (internalizeCandidates, publicizeCandidates, _) = GetAccessibilityCandidates( + providers, + generatedProviders, + graph, + publicGraph, + customPublicRoots, + customInternalDeclarations, + generatedInternalDeclarations, + generatedDiscriminatorBaseNames); + + return (internalizeCandidates, publicizeCandidates); + } + + private static (HashSet InternalizeCandidates, HashSet PublicizeCandidates, HashSet InternalizeHelperRoots) GetAccessibilityCandidates( + IReadOnlyList providers, + IReadOnlyList generatedProviders, + ProviderReferenceGraph graph, + ProviderReferenceGraph publicGraph, + HashSet customPublicRoots, + HashSet customInternalDeclarations, + HashSet generatedInternalDeclarations, + HashSet generatedDiscriminatorBaseNames) + { var internalizeReferences = CloneReferences(publicGraph.References); var internalizeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: false, includeAdditionalRoots: true, includeUnionVariantRoots: false, publicClientRootsOnly: true); if (ShouldUseUnionVariantFallbackRoots()) @@ -236,7 +259,7 @@ private static (HashSet InternalizeCandidates, HashSet Publicize HasPublicApiPredecessor(name, internalizeReferences, publicizeReachable, generatedImplementationInternalDeclarations)) .ToHashSet(StringComparer.Ordinal); - return (internalizeCandidates, publicizeCandidates); + return (internalizeCandidates, publicizeCandidates, internalizeHelperRoots); } private static HashSet GetCustomCodeGeneratedTypeRoots(IReadOnlyList providers, HashSet generatedTypeNames) From f36c8c89867e348e7b90bebb236ab09b18f32ca5 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 1 Jul 2026 00:37:08 +0000 Subject: [PATCH 098/111] fix(csharp): preserve custom request header helpers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ClientBodyDependencyPostProcessingTests.cs | 23 ++++++++++++++++ .../src/CSharpGen.cs | 13 ++++++--- .../PostProcessing/GeneratedCodeWorkspace.cs | 3 +++ .../ProviderReferenceMapAnalyzer.cs | 6 ++++- .../test/PostProcessing/PostProcessorTests.cs | 27 ++++++------------- .../RootClient.cs | 15 +++++++++++ 6 files changed, 63 insertions(+), 24 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidUsingsKeepsFileHeader/RootClient.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs index 48ffd1a34d2..872eeed3c47 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs @@ -159,6 +159,29 @@ public partial class PublicCustomApi publicModelNames: ["GeneratedModel"]); } + [Test] + public async Task CustomOnlyRequestHeaderSetDelimitedReferenceKeepsExtensions() + { + await GenerateAndAssertFiles( + enums: [], + models: [], + clients: [], + customFiles: [ + (Path.Combine("src", "CustomHeaders.cs"), """ + using System.ClientModel.Primitives; + + namespace Sample; + + public static class CustomHeaders + { + public static void Add(PipelineRequestHeaders headers, string[] values) + => headers.SetDelimited("x-ms-custom", values, ","); + } + """) + ], + expectedFiles: [Path.Combine("src", "Generated", "Internal", "PipelineRequestHeadersExtensions.cs")]); + } + [Test] public async Task CustomizedEnumSerializationProviderIsKeptWhenModelSerializationUsesEnum() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index 126acc6df0d..73475be8383 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs @@ -142,14 +142,15 @@ await GeneratedCodeWorkspace.LoadBaselineContract(), generatedFiles.Add((file.Name, file.Text)); } - var usesRequestHeaderExtensions = generatedFiles.Any(static file => - !file.Name.EndsWith("RequestHeaderExtensions.cs", StringComparison.Ordinal) && - file.Text.Contains(".SetDelimited(", StringComparison.Ordinal)); + var usesRequestHeaderExtensions = generatedCodeWorkspace.HasCustomRequestHeaderExtensionsReference() || + generatedFiles.Any(static file => + !IsRequestHeadersExtensionsFile(file.Name) && + file.Text.Contains(".SetDelimited(", StringComparison.Ordinal)); // Write the generated files to the output directory foreach (var file in generatedFiles) { - if (file.Name.EndsWith("RequestHeaderExtensions.cs", StringComparison.Ordinal) && !usesRequestHeaderExtensions) + if (IsRequestHeadersExtensionsFile(file.Name) && !usesRequestHeaderExtensions) { continue; } @@ -172,6 +173,10 @@ await GeneratedCodeWorkspace.LoadBaselineContract(), LoggingHelpers.LogElapsedTime("All files have been written to disk"); } + private static bool IsRequestHeadersExtensionsFile(string fileName) => + fileName.EndsWith("RequestHeaderExtensions.cs", StringComparison.Ordinal) || + fileName.EndsWith("RequestHeadersExtensions.cs", StringComparison.Ordinal); + internal static void FilterAllCustomizedMembers(OutputLibrary output) { foreach (var typeProvider in output.TypeProviders) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index 25fab34e6b8..96ccaf3c983 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -89,6 +89,9 @@ internal void AnalyzeProviderReferenceMap(IReadOnlyList providers) ProviderReferenceMapAnalyzer.Analyze(providers); } + internal bool HasCustomRequestHeaderExtensionsReference() => + ProviderReferenceMapAnalyzer.HasCustomRequestHeaderExtensionsReference(_project); + internal void ApplyPreWriteAccessibility(IReadOnlyList providers) { ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility(providers); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 789317fc723..8da1e8ea231 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -1346,7 +1346,7 @@ private static void RemoveUnusedRequestHeaderExtensionsRoot( { var hasCustomReference = HasCustomRequestHeaderExtensionsReference(providers); var unusedRequestHeaderExtensions = roots - .Where(static root => root.EndsWith(".RequestHeaderExtensions", StringComparison.Ordinal)) + .Where(IsRequestHeadersExtensionsRoot) .Where(_ => !hasCustomReference) .Where(root => !references.Any(reference => !string.Equals(reference.Key, root, StringComparison.Ordinal) && @@ -1356,6 +1356,10 @@ private static void RemoveUnusedRequestHeaderExtensionsRoot( roots.ExceptWith(unusedRequestHeaderExtensions); } + private static bool IsRequestHeadersExtensionsRoot(string root) => + root.EndsWith(".RequestHeaderExtensions", StringComparison.Ordinal) || + root.EndsWith(".RequestHeadersExtensions", StringComparison.Ordinal); + private static bool HasCustomRequestHeaderExtensionsReference(IReadOnlyList providers) { foreach (var customCodeView in GetCustomCodeViews(providers)) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/PostProcessorTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/PostProcessorTests.cs index 4100756ff15..4800fb26abe 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/PostProcessorTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/PostProcessorTests.cs @@ -77,23 +77,14 @@ public async Task RemovesInvalidUsingsKeepsFileHeader() }); var project = workspace.AddProject(projectInfo); - project = AddGeneratedDocument(project, "RootClient.cs", "src", "Generated", "RootClient.cs", """ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// - -#nullable disable - -using Missing.Namespace; - -namespace Sample -{ - public partial class RootClient - { - } -} -"""); + var folder = Helpers.GetAssetFileOrDirectoryPath(false); + project = AddGeneratedDocument( + project, + "RootClient.cs", + "src", + "Generated", + "RootClient.cs", + File.ReadAllText(Path.Join(folder, "RootClient.cs"))); var postProcessor = new TestPostProcessor("RootClient.cs"); var resultProject = await postProcessor.RemoveAsync(project); @@ -105,8 +96,6 @@ public partial class RootClient StringAssert.Contains("#nullable disable", text); StringAssert.DoesNotContain("using Missing.Namespace;", text); } - - [Test] public async Task DoesNotRemoveValidUsings() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidUsingsKeepsFileHeader/RootClient.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidUsingsKeepsFileHeader/RootClient.cs new file mode 100644 index 00000000000..572d6f590ac --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidUsingsKeepsFileHeader/RootClient.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// + +#nullable disable + +using Missing.Namespace; + +namespace Sample +{ + public partial class RootClient + { + } +} From 66c53baf6b1901799d5cdf92c61bab220d0a896d Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 1 Jul 2026 00:44:30 +0000 Subject: [PATCH 099/111] refactor(csharp): use provider markers in reference map Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/Providers/ClientProvider.cs | 1 + .../Providers/ClientUriBuilderDefinition.cs | 1 + .../ProviderReferenceMapAnalyzer.cs | 54 +++++-------------- .../src/Providers/TypeProvider.cs | 4 ++ 4 files changed, 20 insertions(+), 40 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs index 9ffe30f913e..d684331a438 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs @@ -43,6 +43,7 @@ private record ApiVersionFields(FieldProvider Field, PropertyProvider? Correspon private const string ClientSuffix = "Client"; private readonly FormattableString _publicCtorDescription; private readonly InputClient _inputClient; + protected override bool IsClientProvider => true; internal InputClient InputClient => _inputClient; private readonly InputAuth? _inputAuth; private readonly ParameterProvider _endpointParameter; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientUriBuilderDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientUriBuilderDefinition.cs index 92e0cf23a67..4dc5c283cc7 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientUriBuilderDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientUriBuilderDefinition.cs @@ -28,6 +28,7 @@ internal sealed class ClientUriBuilderDefinition : TypeProvider private readonly FieldProvider _uriBuilderField; private readonly FieldProvider _pathAndQueryField; private readonly FieldProvider _pathLengthField; + protected override bool IncludeGeneratedBodyReferences => true; private PropertyProvider? _uriBuilderProperty; private PropertyProvider UriBuilderProperty => _uriBuilderProperty ??= new( diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 8da1e8ea231..0f35961a775 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -845,10 +845,10 @@ private static void AddGeneratedProvider(List generatedProviders, private static void AddGeneratedBodyReferences(IReadOnlyList providers, ProviderReferenceGraph graph) { - foreach (var provider in GetBodyReferenceProviders(providers)) + foreach (var (provider, isSerializationProvider) in GetBodyReferenceProviders(providers)) { if (IsModelFactoryProvider(provider) || - !IsGeneratedBodyReferenceCandidate(provider)) + !IsGeneratedBodyReferenceCandidate(provider, isSerializationProvider)) { continue; } @@ -858,10 +858,9 @@ private static void AddGeneratedBodyReferences(IReadOnlyList provi { continue; } - AddHelperDependencies(graph.References[providerName], provider.HelperDependencyNames, graph.Nodes, referencedNames: null); - var bodyDependencyTypes = ShouldUseGeneratedSourceReferences(provider) ? [] : provider.BodyDependencyTypes; - AddProviderBodyDependencyTypes(graph.References[providerName], bodyDependencyTypes, graph.Nodes); + AddHelperDependencies(graph.References[providerName], provider.HelperDependencyNames, graph.Nodes, referencedNames: null); + AddProviderBodyDependencyTypes(graph.References[providerName], provider.BodyDependencyTypes, graph.Nodes); AddProviderInfrastructureReferences(graph.References[providerName], provider, graph.Nodes); } } @@ -1043,49 +1042,32 @@ private static void AddProviderBodyDependencyType(HashSet references, CS } } - private static IReadOnlyList GetBodyReferenceProviders(IReadOnlyList providers) + private static IReadOnlyList<(TypeProvider Provider, bool IsSerializationProvider)> GetBodyReferenceProviders(IReadOnlyList providers) { - var bodyReferenceProviders = new List(); + var bodyReferenceProviders = new List<(TypeProvider Provider, bool IsSerializationProvider)>(); foreach (var provider in providers) { - bodyReferenceProviders.Add(provider); - bodyReferenceProviders.AddRange(provider.SerializationProviders); + bodyReferenceProviders.Add((provider, false)); + bodyReferenceProviders.AddRange(provider.SerializationProviders.Select(static serializationProvider => (serializationProvider, true))); } return bodyReferenceProviders; } - private static bool IsGeneratedBodyReferenceCandidate(TypeProvider provider) + private static bool IsGeneratedBodyReferenceCandidate(TypeProvider provider, bool isSerializationProvider) { - if (ShouldUseGeneratedSourceReferences(provider)) - { - return true; - } - if (provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Static)) { return true; } - var relativePath = provider.RelativeFilePath.Replace('\\', '/'); - return IsSerializationProvider(provider) || - relativePath.EndsWith("/Internal/ClientUriBuilder.cs", StringComparison.Ordinal) || + return provider.IsClientProvider || + isSerializationProvider || + provider.IncludeGeneratedBodyReferences || provider.HelperDependencyNames.Count > 0 || provider.BodyDependencyTypes.Count > 0; } - private static bool ShouldUseGeneratedSourceReferences(TypeProvider provider) - { - if (!CodeModelGenerator.Instance.Configuration.PackageName.StartsWith("Azure.", StringComparison.Ordinal)) - { - return false; - } - - var relativePath = provider.RelativeFilePath.Replace('\\', '/'); - return relativePath.EndsWith("Client.cs", StringComparison.Ordinal) || - relativePath.Contains("/RestOperations/", StringComparison.Ordinal); - } - private static HashSet GetRootNames( IReadOnlyList providers, HashSet nodes, @@ -1230,7 +1212,7 @@ private static bool IsKept(CSharpType type, HashSet roots, HashSet - provider.RelativeFilePath.EndsWith("Client.cs", StringComparison.Ordinal) && + provider.IsClientProvider && (!publicOnly || !HasApiBaselineDirectory() && provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); private static bool HasApiBaselineDirectory() @@ -1241,15 +1223,7 @@ private static bool HasApiBaselineDirectory() } private static bool IsModelFactoryProvider(TypeProvider provider) - { - if (provider is ModelFactoryProvider) - { - return true; - } - - var relativePath = provider.RelativeFilePath.Replace('\\', '/'); - return relativePath.EndsWith("ModelFactory.cs", StringComparison.Ordinal); - } + => provider is ModelFactoryProvider; private static HashSet GetHelperRootNames( IReadOnlyList providers, diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs index 6996d39a34e..e47a50a7754 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs @@ -307,6 +307,10 @@ private IReadOnlyList ApplyCustomizationFilter(IEnumerable BodyDependencyTypes => _bodyDependencyTypes ??= BuildBodyDependencyTypes(); protected internal virtual IReadOnlyList BuildBodyDependencyTypes() => []; + protected internal virtual bool IsClientProvider => false; + + protected internal virtual bool IncludeGeneratedBodyReferences => false; + private IReadOnlyList? _attributes; public IReadOnlyList Attributes From b7b1eecc61461870449d273c6c86d39abf9af614 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 1 Jul 2026 00:56:53 +0000 Subject: [PATCH 100/111] refactor(csharp): use typed helper dependencies Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/Providers/ClientProvider.cs | 4 ++-- .../MrwSerializationTypeDefinition.cs | 4 ++-- ...ultipartFormDataSerializationDefinition.cs | 4 ++-- .../src/Providers/RestClientProvider.cs | 23 ++++++++++++------- .../ProviderReferenceMapAnalyzer.cs | 23 +++++++++---------- .../src/Providers/TypeProvider.cs | 12 +++++++--- 6 files changed, 41 insertions(+), 29 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs index d684331a438..e36b28f5e12 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs @@ -427,13 +427,13 @@ private IReadOnlyList GetClientParameters() protected override string BuildName() => _inputClient.IsExactName ? _inputClient.Name : _inputClient.Name.ToIdentifierName(); - protected override IReadOnlyList BuildHelperDependencyNames() + protected override IReadOnlyList BuildHelperDependencyTypes() { foreach (var method in Methods.OfType()) { if (method.BodyStatements != null) { - return ["CancellationTokenExtensions", "ClientPipelineExtensions"]; + return [new CancellationTokenExtensionsDefinition().Type, new ClientPipelineExtensionsDefinition().Type]; } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs index 3ef80cfe3fa..2ce74c21ad7 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs @@ -117,8 +117,8 @@ public MrwSerializationTypeDefinition(InputModelType inputModel, ModelProvider m protected override CSharpType? BuildBaseType() => _model.BaseType; - protected override IReadOnlyList BuildHelperDependencyNames() => _rawDataField != null || _additionalProperties.Value.Length > 0 - ? ["ChangeTrackingDictionary"] + protected override IReadOnlyList BuildHelperDependencyTypes() => _rawDataField != null || _additionalProperties.Value.Length > 0 + ? [ChangeTrackingDictionaryType] : []; protected override SuppressionStatement[] BuildDisabledFileWarnings() diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MultipartFormDataSerializationDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MultipartFormDataSerializationDefinition.cs index e390ab24707..398ffc329a3 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MultipartFormDataSerializationDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MultipartFormDataSerializationDefinition.cs @@ -51,10 +51,10 @@ protected override string BuildRelativeFilePath() return Path.Combine("src", "Generated", "Models", $"{Name}.Serialization.Multipart.cs"); } - protected override IReadOnlyList BuildHelperDependencyNames() => _model.Properties.Any( + protected override IReadOnlyList BuildHelperDependencyTypes() => _model.Properties.Any( prop => prop.WireInfo != null && !prop.WireInfo.IsRequired && (prop.Type is { IsCollection: true, IsReadOnlyMemory: false } || prop.Type.IsDictionary)) - ? ["Optional"] + ? [OptionalType] : []; protected override SuppressionStatement[] BuildDisabledFileWarnings() diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs index f67b6814cf2..8656d3865c5 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs @@ -78,12 +78,9 @@ protected override FieldProvider[] BuildFields() return [.. pipelineMessage20xClassifiersFields]; } - protected override IReadOnlyList BuildHelperDependencyNames() + protected override IReadOnlyList BuildHelperDependencyTypes() { - var dependencies = new HashSet(StringComparer.Ordinal) - { - "ClientUriBuilder" - }; + var dependencies = new List { new ClientUriBuilderDefinition().Type }; foreach (var serviceMethod in _inputClient.Methods) { foreach (var parameter in serviceMethod.Operation.Parameters) @@ -96,16 +93,26 @@ protected override IReadOnlyList BuildHelperDependencyNames() var type = ScmCodeModelGenerator.Instance.TypeFactory.CreateCSharpType(parameter.Type); if (type?.IsDictionary == true) { - dependencies.Add("ChangeTrackingDictionary"); + AddDependency(dependencies, ChangeTrackingDictionaryType); } else if (type?.IsCollection == true) { - dependencies.Add("ChangeTrackingList"); + AddDependency(dependencies, ChangeTrackingListType); } } } - return [.. dependencies]; + return dependencies; + } + + private static void AddDependency(List dependencies, CSharpType dependency) + { + if (!dependencies.Any(existing => + existing.Name == dependency.Name && + existing.Namespace == dependency.Namespace)) + { + dependencies.Add(dependency); + } } protected override ScmMethodProvider[] BuildMethods() diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 0f35961a775..574ad65fddc 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -858,8 +858,7 @@ private static void AddGeneratedBodyReferences(IReadOnlyList provi { continue; } - AddHelperDependencies(graph.References[providerName], provider.HelperDependencyNames, graph.Nodes, referencedNames: null); - AddHelperDependencies(graph.References[providerName], provider.HelperDependencyNames, graph.Nodes, referencedNames: null); + AddHelperDependencies(graph.References[providerName], provider.HelperDependencyTypes, graph.Nodes, referencedNames: null); AddProviderBodyDependencyTypes(graph.References[providerName], provider.BodyDependencyTypes, graph.Nodes); AddProviderInfrastructureReferences(graph.References[providerName], provider, graph.Nodes); } @@ -1064,7 +1063,7 @@ private static bool IsGeneratedBodyReferenceCandidate(TypeProvider provider, boo return provider.IsClientProvider || isSerializationProvider || provider.IncludeGeneratedBodyReferences || - provider.HelperDependencyNames.Count > 0 || + provider.HelperDependencyTypes.Count > 0 || provider.BodyDependencyTypes.Count > 0; } @@ -1241,7 +1240,7 @@ private static HashSet GetHelperRootNames( continue; } - AddHelperDependencies(roots, provider.HelperDependencyNames, nodes, references == null ? null : references[providerName]); + AddHelperDependencies(roots, provider.HelperDependencyTypes, nodes, references == null ? null : references[providerName]); foreach (var property in provider.Properties) { @@ -1295,7 +1294,7 @@ private static void AddParameterValidationHelperRoot(HashSet roots, Para private static void AddHelperDependencies( HashSet roots, - IReadOnlyList dependencies, + IReadOnlyList dependencies, HashSet nodes, HashSet? referencedNames) { @@ -1303,12 +1302,12 @@ private static void AddHelperDependencies( { if (referencedNames == null) { - AddMatchingName(roots, dependency, nodes); + AddTypeReference(roots, dependency, nodes); continue; } var matches = new HashSet(StringComparer.Ordinal); - AddMatchingName(matches, dependency, nodes); + AddTypeReference(matches, dependency, nodes); roots.UnionWith(matches.Intersect(referencedNames, StringComparer.Ordinal)); } } @@ -1338,7 +1337,7 @@ private static bool HasCustomRequestHeaderExtensionsReference(IReadOnlyList IsRequestHeaderExtensionsDependency(method.Signature.ReturnType) || @@ -1390,12 +1389,12 @@ private static void AddInitializationHelperRoot(HashSet roots, CSharpTyp if (type is { IsList: true, IsReadOnlyMemory: false }) { - AddMatchingName(roots, "ChangeTrackingList", nodes); + AddTypeReference(roots, ChangeTrackingListType, nodes); } if (type.IsDictionary) { - AddMatchingName(roots, "ChangeTrackingDictionary", nodes); + AddTypeReference(roots, ChangeTrackingDictionaryType, nodes); } foreach (var argument in type.Arguments) @@ -1413,12 +1412,12 @@ private static void AddModelFactoryCollectionInitializationHelperRoot(HashSet ApplyCustomizationFilter(IEnumerable SerializationProviders => _serializationProviders ??= BuildSerializationProviders(); - private IReadOnlyList? _helperDependencyNames; - internal IReadOnlyList HelperDependencyNames => _helperDependencyNames ??= BuildHelperDependencyNames(); - protected internal virtual IReadOnlyList BuildHelperDependencyNames() => []; + private IReadOnlyList? _helperDependencyTypes; + internal IReadOnlyList HelperDependencyTypes => _helperDependencyTypes ??= BuildHelperDependencyTypes(); + protected internal virtual IReadOnlyList BuildHelperDependencyTypes() => []; + + protected CSharpType ChangeTrackingDictionaryType => new ChangeTrackingDictionaryDefinition().Type; + + protected CSharpType ChangeTrackingListType => new ChangeTrackingListDefinition().Type; + + protected CSharpType OptionalType => new OptionalDefinition().Type; private IReadOnlyList? _bodyDependencyTypes; internal IReadOnlyList BodyDependencyTypes => _bodyDependencyTypes ??= BuildBodyDependencyTypes(); From 3ab57ae2ddf89ff460b51fa9012b844f27ef3995 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 1 Jul 2026 01:07:55 +0000 Subject: [PATCH 101/111] refactor(csharp): root request header helpers from providers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/Providers/RestClientProvider.cs | 7 +++++++ .../Microsoft.TypeSpec.Generator/src/CSharpGen.cs | 15 +-------------- .../src/PostProcessing/GeneratedCodeWorkspace.cs | 3 --- 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs index 8656d3865c5..97aa778a707 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs @@ -91,6 +91,13 @@ protected override IReadOnlyList BuildHelperDependencyTypes() } var type = ScmCodeModelGenerator.Instance.TypeFactory.CreateCSharpType(parameter.Type); + if (parameter is InputHeaderParameter headerParameter && + type?.IsCollection == true && + !(type.IsDictionary && !string.IsNullOrEmpty(headerParameter.CollectionHeaderPrefix))) + { + AddDependency(dependencies, new PipelineRequestHeadersExtensionsDefinition().Type); + } + if (type?.IsDictionary == true) { AddDependency(dependencies, ChangeTrackingDictionaryType); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index 73475be8383..be2c6f2400b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs @@ -142,19 +142,9 @@ await GeneratedCodeWorkspace.LoadBaselineContract(), generatedFiles.Add((file.Name, file.Text)); } - var usesRequestHeaderExtensions = generatedCodeWorkspace.HasCustomRequestHeaderExtensionsReference() || - generatedFiles.Any(static file => - !IsRequestHeadersExtensionsFile(file.Name) && - file.Text.Contains(".SetDelimited(", StringComparison.Ordinal)); - // Write the generated files to the output directory foreach (var file in generatedFiles) { - if (IsRequestHeadersExtensionsFile(file.Name) && !usesRequestHeaderExtensions) - { - continue; - } - var filename = Path.Combine(outputPath, file.Name); CodeModelGenerator.Instance.Emitter.Info($"Writing {Path.GetFullPath(filename)}"); Directory.CreateDirectory(Path.GetDirectoryName(filename)!); @@ -173,10 +163,6 @@ await GeneratedCodeWorkspace.LoadBaselineContract(), LoggingHelpers.LogElapsedTime("All files have been written to disk"); } - private static bool IsRequestHeadersExtensionsFile(string fileName) => - fileName.EndsWith("RequestHeaderExtensions.cs", StringComparison.Ordinal) || - fileName.EndsWith("RequestHeadersExtensions.cs", StringComparison.Ordinal); - internal static void FilterAllCustomizedMembers(OutputLibrary output) { foreach (var typeProvider in output.TypeProviders) @@ -217,6 +203,7 @@ private static string[] GetFilesToKeep() private static string GetFileNameForType(string typeName) { + // TypeProvider-generated files are named after the simple type name by convention. var simpleNameStart = typeName.LastIndexOf('.') + 1; var simpleName = simpleNameStart > 0 ? typeName[simpleNameStart..] : typeName; var genericArityStart = simpleName.IndexOf('`'); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index 96ccaf3c983..25fab34e6b8 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -89,9 +89,6 @@ internal void AnalyzeProviderReferenceMap(IReadOnlyList providers) ProviderReferenceMapAnalyzer.Analyze(providers); } - internal bool HasCustomRequestHeaderExtensionsReference() => - ProviderReferenceMapAnalyzer.HasCustomRequestHeaderExtensionsReference(_project); - internal void ApplyPreWriteAccessibility(IReadOnlyList providers) { ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility(providers); From 9832e1a331c29d69b237b417d25f0be851b7b0e4 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 1 Jul 2026 02:46:06 +0000 Subject: [PATCH 102/111] Optimize C# reference map body analysis Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ybrid-reference-map-2026-07-01-02-43-39.md | 7 ++ .../src/Providers/RestClientProvider.cs | 7 -- .../src/Snippets/HttpRequestApiSnippets.cs | 4 +- ...ClientBodyDependencyPostProcessingTests.cs | 16 +++ .../PostProcessing/GeneratedCodeWorkspace.cs | 1 - .../ProviderReferenceMapAnalyzer.cs | 116 +++++++++++++++++- 6 files changed, 136 insertions(+), 15 deletions(-) create mode 100644 .chronus/changes/mtg-hybrid-reference-map-2026-07-01-02-43-39.md diff --git a/.chronus/changes/mtg-hybrid-reference-map-2026-07-01-02-43-39.md b/.chronus/changes/mtg-hybrid-reference-map-2026-07-01-02-43-39.md new file mode 100644 index 00000000000..f84f3c0ca6e --- /dev/null +++ b/.chronus/changes/mtg-hybrid-reference-map-2026-07-01-02-43-39.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@typespec/http-client-csharp" +--- + +Improve generated C# reference-map analysis by deriving generated body references from provider expression trees. diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs index 97aa778a707..8656d3865c5 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs @@ -91,13 +91,6 @@ protected override IReadOnlyList BuildHelperDependencyTypes() } var type = ScmCodeModelGenerator.Instance.TypeFactory.CreateCSharpType(parameter.Type); - if (parameter is InputHeaderParameter headerParameter && - type?.IsCollection == true && - !(type.IsDictionary && !string.IsNullOrEmpty(headerParameter.CollectionHeaderPrefix))) - { - AddDependency(dependencies, new PipelineRequestHeadersExtensionsDefinition().Type); - } - if (type?.IsDictionary == true) { AddDependency(dependencies, ChangeTrackingDictionaryType); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Snippets/HttpRequestApiSnippets.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Snippets/HttpRequestApiSnippets.cs index 588a2094b12..fdd8f72fba0 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Snippets/HttpRequestApiSnippets.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Snippets/HttpRequestApiSnippets.cs @@ -26,7 +26,9 @@ public static MethodBodyStatement SetContent(this ScopedApi pip public static MethodBodyStatement SetHeaderDelimited(this HttpRequestApi pipelineRequest, string name, ValueExpression value, ValueExpression delimiter, ValueExpression? format = null) { ValueExpression[] parameters = format != null ? [Literal(name), value, delimiter, format] : [Literal(name), value, delimiter]; - return pipelineRequest.Property(nameof(PipelineRequest.Headers)).Invoke("SetDelimited", parameters).Terminate(); + return pipelineRequest.Property(nameof(PipelineRequest.Headers)) + .Invoke("SetDelimited", parameters, typeArguments: null, callAsAsync: false, extensionType: new PipelineRequestHeadersExtensionsDefinition().Type) + .Terminate(); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs index 872eeed3c47..39794d21310 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs @@ -159,6 +159,22 @@ public partial class PublicCustomApi publicModelNames: ["GeneratedModel"]); } + [Test] + public async Task GeneratedRequestHeaderSetDelimitedReferenceKeepsExtensions() + { + var header = InputFactory.HeaderParameter("x-ms-custom", InputFactory.Array(InputPrimitiveType.String), isRequired: true); + var operation = InputFactory.Operation("Create", parameters: [header]); + var method = InputFactory.BasicServiceMethod("Create", operation); + var client = InputFactory.Client("TestClient", methods: [method]); + + await GenerateAndAssertFiles( + enums: [], + models: [], + clients: [client], + customFiles: [], + expectedFiles: [Path.Combine("src", "Generated", "Internal", "PipelineRequestHeadersExtensions.cs")]); + } + [Test] public async Task CustomOnlyRequestHeaderSetDelimitedReferenceKeepsExtensions() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index 25fab34e6b8..28a0ca9fd42 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -157,7 +157,6 @@ private async Task ProcessDocument(Document document, MemberRemoverRew rewriter.SemanticModel = semanticModel; root = rewriter.Visit(root); } - root = root.WithAdditionalAnnotations(Simplifier.Annotation); document = document.WithSyntaxRoot(root); if (!CodeModelGenerator.Instance.Configuration.DisableRoslynReduce) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 574ad65fddc..2b8611a3e54 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -2,9 +2,11 @@ // Licensed under the MIT License. using System; +using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; using Microsoft.TypeSpec.Generator.Expressions; @@ -646,18 +648,13 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList gen continue; } - if (!publicOnly && ShouldUseGeneratedSourceReferences(provider)) - { - continue; - } - if (publicOnly && !IsPublic(method.Signature.Modifiers)) { continue; } AddSignatureReferences(references[current], method.Signature, nodes, serializationReferenceNamesByType, includeAttributes: !publicOnly); - if (!publicOnly && !ShouldUseGeneratedSourceReferences(provider)) + if (!publicOnly) { AddTypeReference(references[current], GetCollectionDefinitionType(method), nodes, serializationReferenceNamesByType); } @@ -859,6 +856,7 @@ private static void AddGeneratedBodyReferences(IReadOnlyList provi continue; } AddHelperDependencies(graph.References[providerName], provider.HelperDependencyTypes, graph.Nodes, referencedNames: null); + AddProviderBodyDependencyTypes(graph.References[providerName], CollectStructuredBodyReferenceTypes(provider), graph.Nodes); AddProviderBodyDependencyTypes(graph.References[providerName], provider.BodyDependencyTypes, graph.Nodes); AddProviderInfrastructureReferences(graph.References[providerName], provider, graph.Nodes); } @@ -1017,6 +1015,112 @@ private static string BuildOperationSourceTypeName(CSharpType type) return $"{type.Name}{(argumentNames.Length > 0 ? "Of" : string.Empty)}{argumentNames}"; } + private static IReadOnlyList CollectStructuredBodyReferenceTypes(TypeProvider provider) + { + var references = new HashSet(); + var visited = new HashSet(ReferenceEqualityComparer.Instance); + + foreach (var field in provider.Fields) + { + CollectStructuredBodyReferenceTypes(field.InitializationValue, references, visited); + } + + foreach (var property in provider.Properties) + { + CollectStructuredBodyReferenceTypes(property.Body, references, visited); + } + + foreach (var constructor in provider.Constructors) + { + CollectStructuredBodyReferenceTypes(constructor.BodyExpression, references, visited); + CollectStructuredBodyReferenceTypes(constructor.BodyStatements, references, visited); + } + + foreach (var method in provider.Methods) + { + CollectStructuredBodyReferenceTypes(method.BodyExpression, references, visited); + CollectStructuredBodyReferenceTypes(method.BodyStatements, references, visited); + } + + return [.. references]; + } + + private static void CollectStructuredBodyReferenceTypes(object? value, HashSet references, HashSet visited) + { + switch (value) + { + case null: + case string: + case FormattableString: + return; + } + + if (!value.GetType().IsValueType && !visited.Add(value)) + { + return; + } + + switch (value) + { + case CSharpType type: + references.Add(type); + return; + case Type type: + references.Add(type); + return; + case ParameterProvider parameter: + references.Add(parameter.Type); + CollectStructuredBodyReferenceTypes(parameter.DefaultValue, references, visited); + CollectStructuredBodyReferenceTypes(parameter.InitializationValue, references, visited); + CollectStructuredBodyReferenceTypes(parameter.Attributes, references, visited); + return; + case MethodSignatureBase signature: + CollectStructuredBodyReferenceTypes(signature.ReturnType, references, visited); + CollectStructuredBodyReferenceTypes(signature.Parameters, references, visited); + CollectStructuredBodyReferenceTypes(signature.Attributes, references, visited); + return; + case KeyValuePair positionalArgument: + CollectStructuredBodyReferenceTypes(positionalArgument.Value, references, visited); + return; + case FieldProvider field: + references.Add(field.Type); + CollectStructuredBodyReferenceTypes(field.InitializationValue, references, visited); + CollectStructuredBodyReferenceTypes(field.Attributes, references, visited); + return; + } + + if (IsStructuredBodyReferenceObject(value)) + { + foreach (var property in value.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (property.GetIndexParameters().Length > 0) + { + continue; + } + + CollectStructuredBodyReferenceTypes(property.GetValue(value), references, visited); + } + + return; + } + + if (value is not IEnumerable values) + { + return; + } + + foreach (var item in values) + { + CollectStructuredBodyReferenceTypes(item, references, visited); + } + } + + private static bool IsStructuredBodyReferenceObject(object value) => + value is ValueExpression || + value is MethodBodyStatement || + value is PropertyBody || + value is AttributeStatement; + private static void AddProviderBodyDependencyTypes(HashSet references, IReadOnlyList dependencies, HashSet nodes) { foreach (var dependency in dependencies) From 79903af6ec5c8bd1212f69c458dd918555450c64 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 1 Jul 2026 04:12:09 +0000 Subject: [PATCH 103/111] Fix custom-code reference analysis stack overflow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../SerializationFormatDefinition.cs | 2 + .../test/TestHelpers/MockHelpers.cs | 6 +- .../ProviderReferenceMapAnalyzer.cs | 80 ++++++++- .../src/Providers/NamedTypeSymbolProvider.cs | 170 ++++++++++++++++++ .../src/Providers/TypeProvider.cs | 4 + .../src/SourceInput/SourceInputModel.cs | 19 ++ 6 files changed, 269 insertions(+), 12 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/SerializationFormatDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/SerializationFormatDefinition.cs index af294640060..e902afd7c79 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/SerializationFormatDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/SerializationFormatDefinition.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -45,6 +46,7 @@ protected override TypeSignatureModifiers BuildDeclarationModifiers() protected override string BuildRelativeFilePath() => Path.Combine("src", "Generated", "Internal", $"{Name}.cs"); protected override string BuildName() => "SerializationFormat"; + protected override FormattableString BuildDescription() => $"The serialization format."; protected override TypeProvider[] BuildSerializationProviders() => []; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/TestHelpers/MockHelpers.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/TestHelpers/MockHelpers.cs index ee97df96d29..6c6e743ad89 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/TestHelpers/MockHelpers.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/TestHelpers/MockHelpers.cs @@ -51,8 +51,7 @@ public static async Task> LoadMockGeneratorAsync( var compilationResult = compilation == null ? null : await compilation(); var lastContractCompilationResult = lastContractCompilation == null ? null : await lastContractCompilation(); - var sourceInputModel = new Mock(() => new SourceInputModel(compilationResult, lastContractCompilationResult)) { CallBase = true }; - mockGenerator.Setup(p => p.SourceInputModel).Returns(sourceInputModel.Object); + mockGenerator.SetupProperty(p => p.SourceInputModel, new SourceInputModel(compilationResult, lastContractCompilationResult)); return mockGenerator; } @@ -189,8 +188,7 @@ public static Mock LoadMockGenerator( mockGeneratorInstance.Setup(p => p.OutputLibrary).Returns(createOutputLibrary); } - var sourceInputModel = new Mock(() => new SourceInputModel(null, null)) { CallBase = true }; - mockGeneratorInstance.Setup(p => p.SourceInputModel).Returns(sourceInputModel.Object); + mockGeneratorInstance.SetupProperty(p => p.SourceInputModel, new SourceInputModel(null, null)); codeModelInstance!.SetValue(null, mockGeneratorInstance.Object); clientModelInstance!.SetValue(null, mockGeneratorInstance.Object); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 2b8611a3e54..2d97f8764b1 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -74,9 +74,6 @@ public static void RestorePreWriteModelFactoryMethods() public static void Analyze(IReadOnlyList providers) { var generatedProviders = GetGeneratedProviders(providers); - // The provider graph replaces the old Roslyn-generated-source reference map for generated - // code. We still keep custom-source syntax/symbol checks because users can reference - // generated types from arbitrary C# that is not represented by TypeProviders. var graph = BuildGraph(generatedProviders); var publicGraph = BuildGraph(generatedProviders, publicOnly: true); @@ -159,6 +156,7 @@ public static void Analyze(IReadOnlyList providers) AddMatchingNamesWithSimpleNameSuffix(removeRoots, "ReferenceType", graph.Nodes); AddCustomCodeExtensionRoots(removeRoots, generatedProviders, graph.Nodes); AddCustomizationBackedExtensionRoots(removeRoots, graph.Nodes); + AddCustomRequestHeaderExtensionsRoot(removeRoots, generatedProviders, graph.Nodes); RemoveUnusedRequestHeaderExtensionsRoot(removeRoots, graph.References, providers); var removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); AddDerivedModelReferences(providers, graph.Nodes, graph.References, removeReachableWithoutHelpers, generatedDiscriminatorBaseNames); @@ -293,9 +291,9 @@ private static HashSet GetCustomCodePublicGeneratedTypeRoots(IReadOnlyLi private static IEnumerable GetCustomCodeViews(IReadOnlyList providers) { - var visited = new HashSet(); + var visited = new HashSet(StringComparer.Ordinal); var modelFactoryCustomCodeView = CodeModelGenerator.Instance.OutputLibrary.ModelFactory.Value.CustomCodeView; - if (modelFactoryCustomCodeView != null && visited.Add(modelFactoryCustomCodeView)) + if (modelFactoryCustomCodeView != null && visited.Add(GetCustomCodeViewIdentity(modelFactoryCustomCodeView))) { yield return modelFactoryCustomCodeView; } @@ -303,23 +301,52 @@ private static IEnumerable GetCustomCodeViews(IReadOnlyList + customCodeView is NamedTypeSymbolProvider namedTypeSymbolProvider + ? namedTypeSymbolProvider.MetadataName + : GetProviderTypeName(customCodeView.Type); + + private static void AddCustomRequestHeaderExtensionsRoot(HashSet roots, IReadOnlyList providers, HashSet nodes) + { + if (!HasCustomRequestHeaderExtensionsReference(providers)) + { + return; + } + + AddMatchingNamesWithSimpleNameSuffix(roots, "RequestHeaderExtensions", nodes); + AddMatchingNamesWithSimpleNameSuffix(roots, "RequestHeadersExtensions", nodes); } private static void AddCustomCodeExtensionRoots(HashSet roots, IReadOnlyList providers, HashSet nodes) { foreach (var customCodeView in GetCustomCodeViews(providers)) { - AddMatchingName(roots, $"{customCodeView.Type.Name}Extensions", nodes); + AddMatchingName(roots, $"{GetCustomCodeViewSimpleName(customCodeView)}Extensions", nodes); } } + private static string GetCustomCodeViewSimpleName(TypeProvider customCodeView) => + customCodeView is NamedTypeSymbolProvider namedTypeSymbolProvider + ? namedTypeSymbolProvider.MetadataSimpleName + : customCodeView.Type.Name; + private static void AddCustomizationBackedExtensionRoots(HashSet roots, HashSet nodes) { foreach (var node in nodes) @@ -346,12 +373,27 @@ private static void AddCustomizationBackedExtensionRoots(HashSet roots, private static void AddCustomCodeViewRoots(HashSet roots, TypeProvider customCodeView, HashSet generatedTypeNames, bool publicOnly) { + if (customCodeView is NamedTypeSymbolProvider) + { + AddProviderBodyDependencyTypes(roots, customCodeView.SignatureDependencyTypes, generatedTypeNames); + if (!publicOnly) + { + AddProviderBodyDependencyTypes(roots, customCodeView.HelperDependencyTypes, generatedTypeNames); + AddProviderBodyDependencyTypes(roots, customCodeView.BodyDependencyTypes, generatedTypeNames); + } + + return; + } + AddTypeReference(roots, customCodeView.Type, generatedTypeNames); AddTypeReference(roots, customCodeView.BaseType, generatedTypeNames); + AddProviderBodyDependencyTypes(roots, customCodeView.SignatureDependencyTypes, generatedTypeNames); if (!publicOnly) { AddAttributes(roots, customCodeView.Attributes, generatedTypeNames, serializationProviderNamesByType: null); - AddMatchingName(roots, $"{customCodeView.Type.Name}Extensions", generatedTypeNames); + AddMatchingName(roots, $"{GetCustomCodeViewSimpleName(customCodeView)}Extensions", generatedTypeNames); + AddProviderBodyDependencyTypes(roots, customCodeView.HelperDependencyTypes, generatedTypeNames); + AddProviderBodyDependencyTypes(roots, customCodeView.BodyDependencyTypes, generatedTypeNames); } foreach (var implementedType in customCodeView.Implements) @@ -485,6 +527,11 @@ private static HashSet GetCustomCodeInternalGeneratedTypeDeclarations(IR var declarations = new HashSet(StringComparer.Ordinal); foreach (var customCodeView in GetCustomCodeViews(providers)) { + if (customCodeView is NamedTypeSymbolProvider) + { + continue; + } + if (!customCodeView.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)) { continue; @@ -1137,6 +1184,7 @@ private static void AddProviderBodyDependencyType(HashSet references, CS } AddTypeReference(references, dependency, nodes); + AddMatchingName(references, dependency.Name, nodes); AddMatchingName(references, $"{dependency.Name}Extensions", nodes); foreach (var argument in dependency.Arguments) @@ -1441,6 +1489,18 @@ private static bool HasCustomRequestHeaderExtensionsReference(IReadOnlyList @@ -1478,6 +1538,10 @@ private static bool IsSerializationProvider(TypeProvider provider) relativePath.EndsWith(".Serialization.Multipart.cs", StringComparison.Ordinal); } + private static CSharpType ChangeTrackingDictionaryType => new ChangeTrackingDictionaryDefinition().Type; + + private static CSharpType ChangeTrackingListType => new ChangeTrackingListDefinition().Type; + private static void AddInitializationHelperRoot(HashSet roots, CSharpType? type, HashSet nodes) { if (type == null) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs index ed3a45d54e8..be6e44681b5 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs @@ -30,6 +30,17 @@ public NamedTypeSymbolProvider(INamedTypeSymbol namedTypeSymbol, Compilation com _compilation = compilation; } + internal string MetadataName + { + get + { + var ns = _namedTypeSymbol.ContainingNamespace.GetFullyQualifiedNameFromDisplayString(); + return string.IsNullOrEmpty(ns) ? _namedTypeSymbol.Name : $"{ns}.{_namedTypeSymbol.Name}"; + } + } + + internal string MetadataSimpleName => _namedTypeSymbol.Name; + private protected sealed override NamedTypeSymbolProvider? BuildCustomCodeView(string? generatedTypeName = default, string? generatedTypeNamespace = default) => null; private protected sealed override TypeProvider? BuildLastContractView(string? generatedTypeName = default, string? generatedTypeNamespace = default) => null; @@ -321,6 +332,165 @@ [.. methodSymbol.Parameters.Select(p => ConvertToParameterProvider(methodSymbol, return [.. methods]; } + protected internal override IReadOnlyList BuildBodyDependencyTypes() + { + var dependencies = new HashSet(); + foreach (var syntaxReference in _namedTypeSymbol.DeclaringSyntaxReferences) + { + AddBodyDependencyTypes(syntaxReference.GetSyntax(), dependencies); + } + + return [.. dependencies]; + } + + protected internal override IReadOnlyList BuildSignatureDependencyTypes() + { + var dependencies = new HashSet(); + foreach (var syntaxReference in _namedTypeSymbol.DeclaringSyntaxReferences) + { + if (syntaxReference.GetSyntax() is not TypeDeclarationSyntax typeDeclaration || + !IsPublic(typeDeclaration.Modifiers)) + { + continue; + } + + AddSyntaxTypeReferences(typeDeclaration.BaseList, dependencies); + foreach (var member in typeDeclaration.Members) + { + if (IsPublicApiMember(member)) + { + AddPublicSignatureDependencyTypes(member, dependencies); + } + } + } + + return [.. dependencies]; + } + + private void AddBodyDependencyTypes(SyntaxNode syntax, HashSet dependencies) + { + AddSyntaxTypeReferences(syntax, dependencies); + + foreach (var invocation in syntax.DescendantNodes().OfType()) + { + if (GetInvocationName(invocation) == "SetDelimited") + { + dependencies.Add(CreateUnresolvedDependencyType("SetDelimited")); + } + } + } + + private static void AddPublicSignatureDependencyTypes(MemberDeclarationSyntax member, HashSet dependencies) + { + switch (member) + { + case MethodDeclarationSyntax method: + AddSyntaxTypeReferences(method.ReturnType, dependencies); + AddSyntaxTypeReferences(method.ParameterList, dependencies); + AddSyntaxTypeReferences(method.ConstraintClauses, dependencies); + break; + case ConstructorDeclarationSyntax constructor: + AddSyntaxTypeReferences(constructor.ParameterList, dependencies); + break; + case ConversionOperatorDeclarationSyntax conversion: + AddSyntaxTypeReferences(conversion.Type, dependencies); + AddSyntaxTypeReferences(conversion.ParameterList, dependencies); + break; + case OperatorDeclarationSyntax @operator: + AddSyntaxTypeReferences(@operator.ReturnType, dependencies); + AddSyntaxTypeReferences(@operator.ParameterList, dependencies); + break; + case PropertyDeclarationSyntax property: + AddSyntaxTypeReferences(property.Type, dependencies); + break; + case IndexerDeclarationSyntax indexer: + AddSyntaxTypeReferences(indexer.Type, dependencies); + AddSyntaxTypeReferences(indexer.ParameterList, dependencies); + break; + case FieldDeclarationSyntax field: + AddSyntaxTypeReferences(field.Declaration.Type, dependencies); + break; + case EventFieldDeclarationSyntax eventField: + AddSyntaxTypeReferences(eventField.Declaration.Type, dependencies); + break; + case EventDeclarationSyntax @event: + AddSyntaxTypeReferences(@event.Type, dependencies); + break; + case DelegateDeclarationSyntax @delegate: + AddSyntaxTypeReferences(@delegate.ReturnType, dependencies); + AddSyntaxTypeReferences(@delegate.ParameterList, dependencies); + AddSyntaxTypeReferences(@delegate.ConstraintClauses, dependencies); + break; + case BaseTypeDeclarationSyntax type: + AddSyntaxTypeReferences(type.BaseList, dependencies); + break; + } + } + + private static void AddSyntaxTypeReferences(SyntaxNode? node, HashSet dependencies) + { + if (node == null) + { + return; + } + + foreach (var name in node.DescendantNodesAndSelf().OfType()) + { + dependencies.Add(CreateUnresolvedDependencyType(name.Identifier.ValueText)); + } + + foreach (var name in node.DescendantNodesAndSelf().OfType()) + { + dependencies.Add(CreateUnresolvedDependencyType(name.Identifier.ValueText)); + } + } + + private static void AddSyntaxTypeReferences(IEnumerable nodes, HashSet dependencies) + { + foreach (var node in nodes) + { + AddSyntaxTypeReferences(node, dependencies); + } + } + + private static bool IsPublicApiMember(MemberDeclarationSyntax member) + => member switch + { + EventDeclarationSyntax @event => IsPublic(@event.Modifiers), + EventFieldDeclarationSyntax @event => IsPublic(@event.Modifiers), + BaseFieldDeclarationSyntax field => IsPublic(field.Modifiers), + BaseMethodDeclarationSyntax method => IsPublic(method.Modifiers), + BasePropertyDeclarationSyntax property => IsPublic(property.Modifiers), + DelegateDeclarationSyntax @delegate => IsPublic(@delegate.Modifiers), + BaseTypeDeclarationSyntax type => IsPublic(type.Modifiers), + _ => false + }; + + private static bool IsPublic(SyntaxTokenList modifiers) + => modifiers.Any(static modifier => + modifier.IsKind(SyntaxKind.PublicKeyword) || + modifier.IsKind(SyntaxKind.ProtectedKeyword)); + + private static CSharpType CreateUnresolvedDependencyType(string name) + => new( + name, + string.Empty, + isValueType: false, + isNullable: false, + declaringType: null, + args: [], + isPublic: false, + isStruct: false); + + private static string? GetInvocationName(InvocationExpressionSyntax invocation) + => invocation.Expression switch + { + IdentifierNameSyntax identifier => identifier.Identifier.ValueText, + MemberAccessExpressionSyntax memberAccess => memberAccess.Name.Identifier.ValueText, + GenericNameSyntax genericName => genericName.Identifier.ValueText, + _ => null + }; + private static bool IsPartialMethodDeclaration(IMethodSymbol methodSymbol) { foreach (var syntaxReference in methodSymbol.DeclaringSyntaxReferences) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs index af2f8765dcd..b61b6d606b8 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs @@ -313,6 +313,10 @@ private IReadOnlyList ApplyCustomizationFilter(IEnumerable BodyDependencyTypes => _bodyDependencyTypes ??= BuildBodyDependencyTypes(); protected internal virtual IReadOnlyList BuildBodyDependencyTypes() => []; + private IReadOnlyList? _signatureDependencyTypes; + internal IReadOnlyList SignatureDependencyTypes => _signatureDependencyTypes ??= BuildSignatureDependencyTypes(); + protected internal virtual IReadOnlyList BuildSignatureDependencyTypes() => []; + protected internal virtual bool IsClientProvider => false; protected internal virtual bool IncludeGeneratedBodyReferences => false; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs index a329166ee4b..206379b1b4e 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs @@ -70,6 +70,25 @@ private IReadOnlyDictionary PopulateNameMap() return FindTypeInCompilation(LastContract, ns, name, true, declaringTypeName, includeInternal: false); } + internal IEnumerable GetCustomizationTypeProviders() + { + if (Customization == null) + { + yield break; + } + + foreach (IModuleSymbol module in Customization.Assembly.Modules) + { + foreach (var type in SourceInputHelper.GetSymbols(module.GlobalNamespace)) + { + if (type is INamedTypeSymbol namedTypeSymbol) + { + yield return new NamedTypeSymbolProvider(namedTypeSymbol, Customization); + } + } + } + } + private TypeProvider? FindTypeInCompilation( Compilation? compilation, string ns, From ec68ee367108a3dd672cbf4207210a84bb662444 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 1 Jul 2026 05:44:58 +0000 Subject: [PATCH 104/111] Update CSharp Spector regen baselines Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Models/ErrorInRange.Serialization.cs | 33 ------------------- .../src/Generated/Models/ErrorInRange.cs | 13 -------- .../Models/NotFoundError.Serialization.cs | 33 ------------------- .../src/Generated/Models/NotFoundError.cs | 13 -------- .../Models/ResponseStatusCodeRangeContext.cs | 2 -- .../ResponseStatusCodeRangeModelFactory.cs | 13 -------- 6 files changed, 107 deletions(-) delete mode 100644 packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/ErrorInRange.Serialization.cs delete mode 100644 packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/ErrorInRange.cs delete mode 100644 packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/NotFoundError.Serialization.cs delete mode 100644 packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/NotFoundError.cs delete mode 100644 packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/ResponseStatusCodeRangeModelFactory.cs diff --git a/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/ErrorInRange.Serialization.cs b/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/ErrorInRange.Serialization.cs deleted file mode 100644 index 379c51de55c..00000000000 --- a/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/ErrorInRange.Serialization.cs +++ /dev/null @@ -1,33 +0,0 @@ -// - -#nullable disable - -using System; -using System.ClientModel.Primitives; -using System.Text.Json; - -namespace Response.StatusCodeRange -{ - public partial class ErrorInRange : IJsonModel - { - internal ErrorInRange() => throw null; - - protected virtual ErrorInRange PersistableModelCreateCore(BinaryData data, ModelReaderWriterOptions options) => throw null; - - protected virtual BinaryData PersistableModelWriteCore(ModelReaderWriterOptions options) => throw null; - - BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) => throw null; - - ErrorInRange IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) => throw null; - - string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => throw null; - - void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) => throw null; - - protected virtual void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWriterOptions options) => throw null; - - ErrorInRange IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) => throw null; - - protected virtual ErrorInRange JsonModelCreateCore(ref Utf8JsonReader reader, ModelReaderWriterOptions options) => throw null; - } -} diff --git a/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/ErrorInRange.cs b/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/ErrorInRange.cs deleted file mode 100644 index d9485e459e0..00000000000 --- a/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/ErrorInRange.cs +++ /dev/null @@ -1,13 +0,0 @@ -// - -#nullable disable - -namespace Response.StatusCodeRange -{ - public partial class ErrorInRange - { - public string Code => throw null; - - public string Message => throw null; - } -} diff --git a/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/NotFoundError.Serialization.cs b/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/NotFoundError.Serialization.cs deleted file mode 100644 index 8d76c0e8281..00000000000 --- a/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/NotFoundError.Serialization.cs +++ /dev/null @@ -1,33 +0,0 @@ -// - -#nullable disable - -using System; -using System.ClientModel.Primitives; -using System.Text.Json; - -namespace Response.StatusCodeRange -{ - public partial class NotFoundError : IJsonModel - { - internal NotFoundError() => throw null; - - protected virtual NotFoundError PersistableModelCreateCore(BinaryData data, ModelReaderWriterOptions options) => throw null; - - protected virtual BinaryData PersistableModelWriteCore(ModelReaderWriterOptions options) => throw null; - - BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) => throw null; - - NotFoundError IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) => throw null; - - string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => throw null; - - void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) => throw null; - - protected virtual void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWriterOptions options) => throw null; - - NotFoundError IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) => throw null; - - protected virtual NotFoundError JsonModelCreateCore(ref Utf8JsonReader reader, ModelReaderWriterOptions options) => throw null; - } -} diff --git a/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/NotFoundError.cs b/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/NotFoundError.cs deleted file mode 100644 index a26b96f3e93..00000000000 --- a/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/NotFoundError.cs +++ /dev/null @@ -1,13 +0,0 @@ -// - -#nullable disable - -namespace Response.StatusCodeRange -{ - public partial class NotFoundError - { - public string Code => throw null; - - public string ResourceId => throw null; - } -} diff --git a/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/ResponseStatusCodeRangeContext.cs b/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/ResponseStatusCodeRangeContext.cs index 246085379b8..1b96d6be611 100644 --- a/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/ResponseStatusCodeRangeContext.cs +++ b/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/ResponseStatusCodeRangeContext.cs @@ -6,8 +6,6 @@ namespace Response.StatusCodeRange { - [ModelReaderWriterBuildable(typeof(ErrorInRange))] - [ModelReaderWriterBuildable(typeof(NotFoundError))] public partial class ResponseStatusCodeRangeContext : ModelReaderWriterContext { } diff --git a/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/ResponseStatusCodeRangeModelFactory.cs b/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/ResponseStatusCodeRangeModelFactory.cs deleted file mode 100644 index 70d78187c30..00000000000 --- a/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/ResponseStatusCodeRangeModelFactory.cs +++ /dev/null @@ -1,13 +0,0 @@ -// - -#nullable disable - -namespace Response.StatusCodeRange -{ - public static partial class ResponseStatusCodeRangeModelFactory - { - public static ErrorInRange ErrorInRange(string code = default, string message = default) => throw null; - - public static NotFoundError NotFoundError(string code = default, string resourceId = default) => throw null; - } -} From 6665e3a14219a41ab36eea2fbe7c27f73777da31 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 1 Jul 2026 06:05:22 +0000 Subject: [PATCH 105/111] Revert "Update CSharp Spector regen baselines" This reverts commit ec68ee367108a3dd672cbf4207210a84bb662444. --- .../Models/ErrorInRange.Serialization.cs | 33 +++++++++++++++++++ .../src/Generated/Models/ErrorInRange.cs | 13 ++++++++ .../Models/NotFoundError.Serialization.cs | 33 +++++++++++++++++++ .../src/Generated/Models/NotFoundError.cs | 13 ++++++++ .../Models/ResponseStatusCodeRangeContext.cs | 2 ++ .../ResponseStatusCodeRangeModelFactory.cs | 13 ++++++++ 6 files changed, 107 insertions(+) create mode 100644 packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/ErrorInRange.Serialization.cs create mode 100644 packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/ErrorInRange.cs create mode 100644 packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/NotFoundError.Serialization.cs create mode 100644 packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/NotFoundError.cs create mode 100644 packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/ResponseStatusCodeRangeModelFactory.cs diff --git a/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/ErrorInRange.Serialization.cs b/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/ErrorInRange.Serialization.cs new file mode 100644 index 00000000000..379c51de55c --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/ErrorInRange.Serialization.cs @@ -0,0 +1,33 @@ +// + +#nullable disable + +using System; +using System.ClientModel.Primitives; +using System.Text.Json; + +namespace Response.StatusCodeRange +{ + public partial class ErrorInRange : IJsonModel + { + internal ErrorInRange() => throw null; + + protected virtual ErrorInRange PersistableModelCreateCore(BinaryData data, ModelReaderWriterOptions options) => throw null; + + protected virtual BinaryData PersistableModelWriteCore(ModelReaderWriterOptions options) => throw null; + + BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) => throw null; + + ErrorInRange IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) => throw null; + + string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => throw null; + + void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) => throw null; + + protected virtual void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWriterOptions options) => throw null; + + ErrorInRange IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) => throw null; + + protected virtual ErrorInRange JsonModelCreateCore(ref Utf8JsonReader reader, ModelReaderWriterOptions options) => throw null; + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/ErrorInRange.cs b/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/ErrorInRange.cs new file mode 100644 index 00000000000..d9485e459e0 --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/ErrorInRange.cs @@ -0,0 +1,13 @@ +// + +#nullable disable + +namespace Response.StatusCodeRange +{ + public partial class ErrorInRange + { + public string Code => throw null; + + public string Message => throw null; + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/NotFoundError.Serialization.cs b/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/NotFoundError.Serialization.cs new file mode 100644 index 00000000000..8d76c0e8281 --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/NotFoundError.Serialization.cs @@ -0,0 +1,33 @@ +// + +#nullable disable + +using System; +using System.ClientModel.Primitives; +using System.Text.Json; + +namespace Response.StatusCodeRange +{ + public partial class NotFoundError : IJsonModel + { + internal NotFoundError() => throw null; + + protected virtual NotFoundError PersistableModelCreateCore(BinaryData data, ModelReaderWriterOptions options) => throw null; + + protected virtual BinaryData PersistableModelWriteCore(ModelReaderWriterOptions options) => throw null; + + BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) => throw null; + + NotFoundError IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) => throw null; + + string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => throw null; + + void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) => throw null; + + protected virtual void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWriterOptions options) => throw null; + + NotFoundError IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) => throw null; + + protected virtual NotFoundError JsonModelCreateCore(ref Utf8JsonReader reader, ModelReaderWriterOptions options) => throw null; + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/NotFoundError.cs b/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/NotFoundError.cs new file mode 100644 index 00000000000..a26b96f3e93 --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/NotFoundError.cs @@ -0,0 +1,13 @@ +// + +#nullable disable + +namespace Response.StatusCodeRange +{ + public partial class NotFoundError + { + public string Code => throw null; + + public string ResourceId => throw null; + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/ResponseStatusCodeRangeContext.cs b/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/ResponseStatusCodeRangeContext.cs index 1b96d6be611..246085379b8 100644 --- a/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/ResponseStatusCodeRangeContext.cs +++ b/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/Models/ResponseStatusCodeRangeContext.cs @@ -6,6 +6,8 @@ namespace Response.StatusCodeRange { + [ModelReaderWriterBuildable(typeof(ErrorInRange))] + [ModelReaderWriterBuildable(typeof(NotFoundError))] public partial class ResponseStatusCodeRangeContext : ModelReaderWriterContext { } diff --git a/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/ResponseStatusCodeRangeModelFactory.cs b/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/ResponseStatusCodeRangeModelFactory.cs new file mode 100644 index 00000000000..70d78187c30 --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Spector/http/response/status-code-range/src/Generated/ResponseStatusCodeRangeModelFactory.cs @@ -0,0 +1,13 @@ +// + +#nullable disable + +namespace Response.StatusCodeRange +{ + public static partial class ResponseStatusCodeRangeModelFactory + { + public static ErrorInRange ErrorInRange(string code = default, string message = default) => throw null; + + public static NotFoundError NotFoundError(string code = default, string resourceId = default) => throw null; + } +} From ee53dabdf50e127a677758af79ff36f3882aa86a Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 1 Jul 2026 06:18:08 +0000 Subject: [PATCH 106/111] Fix custom partial model reachability Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .chronus/config.yaml | 1 + .../src/PostProcessing/ProviderReferenceMapAnalyzer.cs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.chronus/config.yaml b/.chronus/config.yaml index 06dad2b4c85..e8f50624dc9 100644 --- a/.chronus/config.yaml +++ b/.chronus/config.yaml @@ -71,6 +71,7 @@ versionPolicies: changelog: ["@chronus/github/changelog", { repo: "microsoft/typespec" }] additionalPackages: + - packages/http-client-csharp - packages/http-client-python - packages/http-client-java diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 2d97f8764b1..1a0d272ac53 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -373,8 +373,9 @@ private static void AddCustomizationBackedExtensionRoots(HashSet roots, private static void AddCustomCodeViewRoots(HashSet roots, TypeProvider customCodeView, HashSet generatedTypeNames, bool publicOnly) { - if (customCodeView is NamedTypeSymbolProvider) + if (customCodeView is NamedTypeSymbolProvider namedTypeSymbolProvider) { + AddMatchingName(roots, namedTypeSymbolProvider.MetadataSimpleName, generatedTypeNames); AddProviderBodyDependencyTypes(roots, customCodeView.SignatureDependencyTypes, generatedTypeNames); if (!publicOnly) { From fa098f713591bf73528d7a411605b08ee9ec008e Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 1 Jul 2026 07:01:49 +0000 Subject: [PATCH 107/111] Fix CSharp customization test output path Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../test/Providers/ModelProviders/ClientCustomizationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ClientCustomizationTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ClientCustomizationTests.cs index 7f3ea5cd1ff..d96e98a9d8e 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ClientCustomizationTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ClientCustomizationTests.cs @@ -405,7 +405,7 @@ private class ClientTypeProvider : TypeProvider public MethodProvider[] MethodProviders { get; set; } = []; public ConstructorProvider[] ConstructorProviders { get; set; } = []; - protected override string BuildRelativeFilePath() => "."; + protected override string BuildRelativeFilePath() => $"{Name}.cs"; protected override string BuildName() => "MockInputClient"; From 049dd2be4f5908a42ec2d9f31a4cda34780aa844 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 1 Jul 2026 08:24:04 +0000 Subject: [PATCH 108/111] Fix provider reference map state cleanup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs | 1 + .../src/PostProcessing/ProviderReferenceMapAnalyzer.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index be2c6f2400b..13f42796646 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs @@ -130,6 +130,7 @@ await GeneratedCodeWorkspace.LoadBaselineContract(), LoggingHelpers.LogElapsedTime("All old generated files have been deleted"); await generatedCodeWorkspace.PostProcessAsync(); + ProviderReferenceMapAnalyzer.ResetPreWriteAccessibility(); var generatedFiles = new List<(string Name, string Text)>(); await foreach (var file in generatedCodeWorkspace.GetGeneratedFilesAsync()) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 1a0d272ac53..01e81575478 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -29,6 +29,7 @@ internal static class ProviderReferenceMapAnalyzer public static void ResetPreWriteAccessibility() { RestorePreWriteModelFactoryMethods(); + _latestResult = null; PreWriteAccessibilityApplied = false; } From 6a19691d2da592590afddda7566e71d41b2d2214 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 1 Jul 2026 08:35:27 +0000 Subject: [PATCH 109/111] Align benchmark provider analyzer with latest PR Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ProviderReferenceMapAnalyzer.cs | 2 +- .../ProviderReferenceMapShadowAnalyzer.cs | 1642 ----------------- 2 files changed, 1 insertion(+), 1643 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index a86b9c25bda..01e81575478 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -703,7 +703,7 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList gen } AddSignatureReferences(references[current], method.Signature, nodes, serializationReferenceNamesByType, includeAttributes: !publicOnly); - if (!publicOnly && !ShouldUseGeneratedSourceReferences(provider)) + if (!publicOnly) { AddTypeReference(references[current], GetCollectionDefinitionType(method), nodes, serializationReferenceNamesByType); } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs index 49838482ebb..e5b1ae9eae0 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs @@ -2,1658 +2,16 @@ // Licensed under the MIT License. using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using System.Text.RegularExpressions; -using System.Xml.Linq; -using Microsoft.TypeSpec.Generator.Primitives; -using Microsoft.TypeSpec.Generator.Providers; -using Microsoft.TypeSpec.Generator.Statements; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Microsoft.TypeSpec.Generator { internal static class ProviderReferenceMapShadowAnalyzer { - private const string EnableEnvironmentVariable = "TYPESPEC_PROVIDER_REFERENCE_MAP_SHADOW"; private const string UseShadowEnvironmentVariable = "TYPESPEC_PROVIDER_REFERENCE_MAP_USE_SHADOW"; - private const string ReportEnvironmentVariable = "TYPESPEC_PROVIDER_REFERENCE_MAP_SHADOW_REPORT"; - private const string OutputDirectoryEnvironmentVariable = "TYPESPEC_PROVIDER_REFERENCE_MAP_SHADOW_DIR"; - - private static ProviderReferenceMapShadowResult? _latestResult; - private static readonly ConditionalWeakTable, Dictionary> _simpleNameLookupCache = new(); - - public static bool IsEnabled => string.Equals( - Environment.GetEnvironmentVariable(EnableEnvironmentVariable), - "true", - StringComparison.OrdinalIgnoreCase); - - public static ProviderReferenceMapShadowResult? LatestResult => _latestResult; public static bool UseShadowMap => string.Equals( Environment.GetEnvironmentVariable(UseShadowEnvironmentVariable), "true", StringComparison.OrdinalIgnoreCase); - - private static bool ShouldWriteReports => string.Equals( - Environment.GetEnvironmentVariable(ReportEnvironmentVariable), - "true", - StringComparison.OrdinalIgnoreCase); - - public static void Analyze(IReadOnlyList providers, Project project) - { - if (!IsEnabled) - { - _latestResult = null; - return; - } - - var generatedProviders = GetGeneratedProviders(providers); - var graph = BuildGraph(generatedProviders); - var publicGraph = BuildGraph(generatedProviders, publicOnly: true); - - // Generated-code dependencies come from providers. Custom code still needs Roslyn - // because arbitrary user C# can reference generated types in ways providers cannot see. - var customPublicRoots = GetCustomCodePublicGeneratedTypeRoots(project, graph.Nodes); - var apiBaselineGeneratedTypeRoots = GetApiBaselineGeneratedTypeRoots(graph.Nodes); - customPublicRoots.UnionWith(apiBaselineGeneratedTypeRoots); - var generatedPublicDeclarations = GetGeneratedPublicTypeDeclarations(generatedProviders, graph.Nodes); - customPublicRoots.UnionWith(generatedPublicDeclarations); - var customRemovalRoots = GetCustomCodeGeneratedTypeRoots(project, graph.Nodes); - customRemovalRoots.UnionWith(apiBaselineGeneratedTypeRoots); - customRemovalRoots.UnionWith(generatedPublicDeclarations); - var customInternalDeclarations = GetCustomCodeInternalGeneratedTypeDeclarations(project, graph.Nodes); - var generatedInternalDeclarations = GetGeneratedInternalTypeDeclarations(generatedProviders, graph.Nodes); - - // Helper types are rooted after an initial reachability pass so unused infrastructure - // such as change-tracking dictionaries can still be removed when no reachable type needs them. - var generatedDiscriminatorBaseNames = GetGeneratedPersistableModelProxyTypeNames(project, publicGraph.Nodes); - AddGeneratedXmlDocCrefReferences(project, publicGraph, publicOnly: true); - var internalizeReferences = CloneReferences(publicGraph.References); - var internalizeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: false, includeAdditionalRoots: true, includeUnionVariantRoots: true, publicClientRootsOnly: true); - var generatedPublicReachable = GetReachableTypes(internalizeRoots, internalizeReferences); - AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences, generatedPublicReachable, generatedDiscriminatorBaseNames); - internalizeRoots.UnionWith(customPublicRoots); - var internalizeReachableWithoutHelpers = GetReachableTypes(internalizeRoots, internalizeReferences); - AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences, internalizeReachableWithoutHelpers, generatedDiscriminatorBaseNames); - internalizeReachableWithoutHelpers = GetReachableTypes(internalizeRoots, internalizeReferences); - var publicizeRoots = internalizeRoots.ToHashSet(StringComparer.Ordinal); - var internalizeHelperRoots = GetHelperRootNames(generatedProviders, graph.Nodes, internalizeReachableWithoutHelpers); - internalizeRoots.UnionWith(internalizeHelperRoots); - var internalizeReachable = GetReachableTypes(internalizeRoots, internalizeReferences); - var internalizeDeclaredNodes = GetPostProcessorDeclaredNodes(generatedProviders, graph.Nodes, publicOnly: true); - var customInternalBoundaryNodes = graph.Nodes - .Where(name => publicGraph.References.TryGetValue(name, out var references) && references.Overlaps(customInternalDeclarations)) - .ToHashSet(StringComparer.Ordinal); - var publicizeDeclaredNodes = GetPostProcessorDeclaredNodes(generatedProviders, graph.Nodes, publicOnly: false) - .Except(internalizeDeclaredNodes, StringComparer.Ordinal); - var generatedImplementationInternalDeclarations = GetGeneratedImplementationInternalTypeDeclarations(generatedInternalDeclarations).ToHashSet(StringComparer.Ordinal); - var publicApiTraversalNodes = internalizeDeclaredNodes - .Except(generatedInternalDeclarations, StringComparer.Ordinal) - .Concat(publicizeDeclaredNodes) - .Except(generatedImplementationInternalDeclarations, StringComparer.Ordinal) - .ToHashSet(StringComparer.Ordinal); - var publicizeReachable = GetReachableTypes(publicizeRoots, internalizeReferences, publicApiTraversalNodes); - var internalizeCandidates = internalizeDeclaredNodes - .Except(publicizeReachable, StringComparer.Ordinal) - .Union(internalizeDeclaredNodes - .Intersect(customInternalBoundaryNodes, StringComparer.Ordinal) - .Except(publicizeRoots, StringComparer.Ordinal), StringComparer.Ordinal) - .OrderBy(static name => name, StringComparer.Ordinal) - .ToArray(); - var publicizeCandidates = publicizeDeclaredNodes - .Except(customInternalDeclarations, StringComparer.Ordinal) - .Except(customInternalBoundaryNodes, StringComparer.Ordinal) - .Except(internalizeHelperRoots, StringComparer.Ordinal) - .Except(GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: true, includeAdditionalRoots: true, includeUnionVariantRoots: true, publicClientRootsOnly: true), StringComparer.Ordinal) - .Intersect(publicizeReachable, StringComparer.Ordinal) - // Preserve generated types that the last contract already made internal unless a public API/custom root explicitly requires them. - .Where(name => !generatedInternalDeclarations.Contains(name) || publicizeRoots.Contains(name)) - .Where(name => publicizeRoots.Contains(name) || - HasPublicApiPredecessor(name, internalizeReferences, publicizeReachable, generatedImplementationInternalDeclarations)) - .OrderBy(static name => name, StringComparer.Ordinal) - .ToArray(); - - // Body-only generated dependencies are needed to avoid deleting helper files, but they do - // not contribute to public API reachability for internalization. - AddGeneratedXmlDocCrefReferences(project, graph, publicOnly: false); - AddGeneratedBodyReferences(project, providers, graph); - - var removeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: true, includeAdditionalRoots: true, includeUnionVariantRoots: true, publicClientRootsOnly: false); - removeRoots.UnionWith(customRemovalRoots); - RemoveUnusedRequestHeaderExtensionsRoot(removeRoots, graph.References, project); - var removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); - AddDerivedModelReferences(providers, graph.Nodes, graph.References, removeReachableWithoutHelpers, generatedDiscriminatorBaseNames); - removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); - AddBasePreservedReferences(generatedProviders, graph.Nodes, graph.References, removeReachableWithoutHelpers); - var removeHelperRoots = GetHelperRootNames(generatedProviders, graph.Nodes, removeReachableWithoutHelpers, graph.References); - removeRoots.UnionWith(removeHelperRoots); - var removeReachable = GetReachableTypes(removeRoots, graph.References); - AddBasePreservedReferences(generatedProviders, graph.Nodes, graph.References, removeReachable); - var removeDeclaredNodes = GetPostProcessorDeclaredNodes(generatedProviders, graph.Nodes, publicOnly: false); - var removeCandidates = removeDeclaredNodes.Except(removeReachable, StringComparer.Ordinal).OrderBy(static name => name, StringComparer.Ordinal).ToArray(); - - var helperRoots = internalizeHelperRoots.Concat(removeHelperRoots).ToHashSet(StringComparer.Ordinal); - - _latestResult = new ProviderReferenceMapShadowResult( - project.Id, - internalizeCandidates.ToHashSet(StringComparer.Ordinal), - publicizeCandidates.ToHashSet(StringComparer.Ordinal), - removeCandidates.ToHashSet(StringComparer.Ordinal)); - - if (ShouldWriteReports) - { - WriteReport( - graph, - customPublicRoots, - customRemovalRoots, - helperRoots, - internalizeRoots, - internalizeReachable, - internalizeCandidates, - publicizeRoots, - publicizeReachable, - publicizeCandidates, - removeRoots, - removeReachable, - removeCandidates); - } - } - - private static HashSet GetCustomCodeGeneratedTypeRoots(Project project, HashSet generatedTypeNames) - { - var roots = new HashSet(StringComparer.Ordinal); - var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); - if (compilation == null) - { - return roots; - } - - foreach (var document in project.Documents) - { - if (IsGeneratedDocument(document)) - { - continue; - } - - var root = document.GetSyntaxRootAsync().GetAwaiter().GetResult(); - if (root == null) - { - continue; - } - - var model = compilation.GetSemanticModel(root.SyntaxTree); - foreach (var declaration in root.DescendantNodes().OfType()) - { - AddSymbolRoot(roots, model.GetDeclaredSymbol(declaration) as ITypeSymbol, generatedTypeNames); - } - - foreach (var typeSyntax in root.DescendantNodes().OfType()) - { - AddSymbolRoot(roots, model.GetTypeInfo(typeSyntax).Type, generatedTypeNames); - } - - foreach (var objectCreation in root.DescendantNodes().OfType()) - { - AddSymbolRoot(roots, model.GetSymbolInfo(objectCreation).Symbol?.ContainingType, generatedTypeNames); - } - - foreach (var invocation in root.DescendantNodes().OfType()) - { - AddSymbolRoot(roots, model.GetSymbolInfo(invocation).Symbol?.ContainingType, generatedTypeNames); - } - } - - return roots; - } - - private static HashSet GetCustomCodePublicGeneratedTypeRoots(Project project, HashSet generatedTypeNames) - { - var roots = new HashSet(StringComparer.Ordinal); - var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); - if (compilation == null) - { - return roots; - } - - foreach (var document in project.Documents) - { - if (IsGeneratedDocument(document)) - { - continue; - } - - var root = document.GetSyntaxRootAsync().GetAwaiter().GetResult(); - if (root == null) - { - continue; - } - - var semanticModel = compilation.GetSemanticModel(root.SyntaxTree); - foreach (var declaration in root.DescendantNodes().OfType()) - { - if (semanticModel.GetDeclaredSymbol(declaration) is not INamedTypeSymbol symbol || - symbol.DeclaredAccessibility != Accessibility.Public) - { - continue; - } - - AddMatchingName(roots, declaration.Identifier.ValueText, generatedTypeNames); - - AddSymbolRoot(roots, symbol, generatedTypeNames); - AddSymbolRoot(roots, symbol.BaseType, generatedTypeNames); - foreach (var interfaceType in symbol.Interfaces) - { - AddSymbolRoot(roots, interfaceType, generatedTypeNames); - } - - foreach (var member in symbol.GetMembers()) - { - if (member.DeclaredAccessibility != Accessibility.Public || - !IsDeclaredInSyntaxTree(member, declaration.SyntaxTree, declaration.Span)) - { - continue; - } - - switch (member) - { - case IMethodSymbol method: - AddSymbolRoot(roots, method.ReturnType, generatedTypeNames); - foreach (var parameter in method.Parameters) - { - AddSymbolRoot(roots, parameter.Type, generatedTypeNames); - } - break; - case IPropertySymbol property: - AddSymbolRoot(roots, property.Type, generatedTypeNames); - break; - case IFieldSymbol field: - AddSymbolRoot(roots, field.Type, generatedTypeNames); - break; - case IEventSymbol eventSymbol: - AddSymbolRoot(roots, eventSymbol.Type, generatedTypeNames); - break; - } - } - } - } - - return roots; - } - - private static HashSet GetApiBaselineGeneratedTypeRoots(HashSet generatedTypeNames) - { - var roots = new HashSet(StringComparer.Ordinal); - var projectDirectory = CodeModelGenerator.Instance.Configuration.ProjectDirectory; - if (string.IsNullOrEmpty(projectDirectory)) - { - return roots; - } - - var apiDirectory = Path.GetFullPath(Path.Combine(projectDirectory, "..", "api")); - if (!Directory.Exists(apiDirectory)) - { - return roots; - } - - var apiText = string.Join("\n", Directory.GetFiles(apiDirectory, "*.cs", SearchOption.AllDirectories).Select(File.ReadAllText)); - foreach (var fullName in generatedTypeNames) - { - var simpleName = StripGenericArity(GetSimpleName(fullName)); - var normalizedFullName = StripGenericArity(fullName); - if (!ContainsApiTypeReference(apiText, normalizedFullName, simpleName)) - { - continue; - } - - roots.Add(fullName); - } - - return roots; - } - - private static bool ContainsApiTypeReference(string apiText, string fullName, string simpleName) - { - var fullNamePattern = $@"(? GetCustomCodeInternalGeneratedTypeDeclarations(Project project, HashSet generatedTypeNames) - { - var declarations = new HashSet(StringComparer.Ordinal); - var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); - if (compilation == null) - { - return declarations; - } - - foreach (var document in project.Documents) - { - if (IsGeneratedDocument(document)) - { - continue; - } - - var root = document.GetSyntaxRootAsync().GetAwaiter().GetResult(); - if (root == null) - { - continue; - } - - var semanticModel = compilation.GetSemanticModel(root.SyntaxTree); - foreach (var declaration in root.DescendantNodes().OfType()) - { - if (semanticModel.GetDeclaredSymbol(declaration) is not INamedTypeSymbol symbol || - symbol.DeclaredAccessibility != Accessibility.Internal) - { - continue; - } - - AddMatchingName(declarations, symbol.GetFullyQualifiedName(), generatedTypeNames); - } - } - - return declarations; - } - - private static HashSet GetGeneratedPersistableModelProxyTypeNames(Project project, HashSet generatedTypeNames) - { - var proxyTypes = new HashSet(StringComparer.Ordinal); - var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); - if (compilation == null) - { - return proxyTypes; - } - - foreach (var document in project.Documents) - { - if (!IsGeneratedDocument(document)) - { - continue; - } - - var root = document.GetSyntaxRootAsync().GetAwaiter().GetResult(); - if (root == null) - { - continue; - } - - var semanticModel = compilation.GetSemanticModel(root.SyntaxTree); - foreach (var declaration in root.DescendantNodes().OfType()) - { - if (!declaration.AttributeLists - .SelectMany(static list => list.Attributes) - .Any(static attribute => attribute.Name.ToString().Contains("PersistableModelProxy", StringComparison.Ordinal))) - { - continue; - } - - if (semanticModel.GetDeclaredSymbol(declaration) is INamedTypeSymbol symbol) - { - AddMatchingName(proxyTypes, symbol.GetFullyQualifiedName(), generatedTypeNames); - } - } - } - - return proxyTypes; - } - - private static HashSet GetGeneratedInternalTypeDeclarations(IReadOnlyList generatedProviders, HashSet generatedTypeNames) - => GetGeneratedTypeDeclarationsByLastContractAccessibility(generatedProviders, generatedTypeNames, Accessibility.Internal); - - private static HashSet GetGeneratedPublicTypeDeclarations(IReadOnlyList generatedProviders, HashSet generatedTypeNames) - => GetGeneratedTypeDeclarationsByLastContractAccessibility(generatedProviders, generatedTypeNames, Accessibility.Public); - - private static HashSet GetGeneratedTypeDeclarationsByLastContractAccessibility( - IReadOnlyList generatedProviders, - HashSet generatedTypeNames, - Accessibility accessibility) - { - var declarations = new HashSet(StringComparer.Ordinal); - var lastContract = CodeModelGenerator.Instance.SourceInputModel.LastContract; - foreach (var provider in generatedProviders) - { - var providerTypeName = GetProviderTypeName(provider.Type); - if (lastContract?.GetTypeByMetadataName(providerTypeName)?.DeclaredAccessibility != accessibility) - { - continue; - } - - AddMatchingName(declarations, providerTypeName, generatedTypeNames); - } - - return declarations; - } - - private static IEnumerable GetGeneratedImplementationInternalTypeDeclarations(HashSet generatedInternalDeclarations) => - generatedInternalDeclarations.Where(static name => GetSimpleName(name).StartsWith("Internal", StringComparison.Ordinal)); - - private static void AddSymbolRoot(HashSet roots, ITypeSymbol? symbol, HashSet generatedTypeNames) - { - if (symbol is not INamedTypeSymbol namedType) - { - return; - } - - AddMatchingSymbolName(roots, namedType, generatedTypeNames); - foreach (var typeArgument in namedType.TypeArguments) - { - AddSymbolRoot(roots, typeArgument, generatedTypeNames); - } - } - - private static void AddMatchingSymbolName(HashSet target, INamedTypeSymbol symbol, HashSet generatedTypeNames) - { - try - { - AddMatchingName(target, symbol.GetFullyQualifiedName(), generatedTypeNames); - } - catch (ArgumentOutOfRangeException) - { - // Some custom-code symbols cannot be represented by the legacy fully-qualified-name - // helper. A simple-name match is enough to discover generated roots. - AddMatchingName(target, symbol.Name, generatedTypeNames); - } - } - - private static ProviderReferenceGraph BuildGraph(IReadOnlyList generatedProviders, bool publicOnly = false) - { - var serializationProviderNamesByType = generatedProviders - .Where(static provider => provider.SerializationProviders.Count > 0) - .ToDictionary( - static provider => GetProviderTypeName(provider.Type), - static provider => provider.SerializationProviders - .Select(static serializationProvider => GetProviderTypeName(serializationProvider.Type)) - .ToArray(), - StringComparer.Ordinal); - IReadOnlyDictionary? serializationReferenceNamesByType = publicOnly ? null : serializationProviderNamesByType; - var nodes = generatedProviders - .Select(static provider => GetProviderTypeName(provider.Type)) - .ToHashSet(StringComparer.Ordinal); - var references = nodes.ToDictionary(static name => name, _ => new HashSet(StringComparer.Ordinal), StringComparer.Ordinal); - - foreach (var provider in generatedProviders) - { - var current = GetProviderTypeName(provider.Type); - AddTypeReference(references[current], provider.Type, nodes, serializationReferenceNamesByType); - AddTypeReference(references[current], provider.BaseType, nodes, serializationReferenceNamesByType); - AddTypeReference(references[current], provider.DeclaringTypeProvider?.Type, nodes, serializationReferenceNamesByType); - - if (IsKept(provider.Type, CodeModelGenerator.Instance.NonRootTypes, nodes)) - { - continue; - } - - // Model factory signatures mention many models. The existing Roslyn post-processor - // removes factory methods for unreachable models, so model factory should only - // contribute helper dependencies, not model reachability edges. - if (IsModelFactoryProvider(provider)) - { - continue; - } - - foreach (var implementedType in provider.Implements) - { - AddTypeReference(references[current], implementedType, nodes, serializationReferenceNamesByType); - } - - if (!publicOnly) - { - foreach (var nestedType in provider.NestedTypes) - { - AddTypeReference(references[current], nestedType.Type, nodes, serializationReferenceNamesByType); - } - } - - if (!publicOnly) - { - foreach (var serializationProvider in provider.SerializationProviders) - { - AddTypeReference(references[current], serializationProvider.Type, nodes, serializationReferenceNamesByType); - } - } - - foreach (var property in provider.Properties) - { - if (publicOnly && !IsPublic(property.Modifiers)) - { - continue; - } - - AddTypeReference(references[current], property.Type, nodes, serializationReferenceNamesByType); - AddTypeReference(references[current], property.ExplicitInterface, nodes, serializationReferenceNamesByType); - if (!publicOnly) - { - AddAttributes(references[current], property.Attributes, nodes, serializationReferenceNamesByType); - } - } - - foreach (var field in provider.Fields) - { - if (publicOnly && !field.Modifiers.HasFlag(FieldModifiers.Public)) - { - continue; - } - - AddTypeReference(references[current], field.Type, nodes, serializationReferenceNamesByType); - if (!publicOnly) - { - AddAttributes(references[current], field.Attributes, nodes, serializationReferenceNamesByType); - } - } - - foreach (var constructor in provider.Constructors) - { - if (publicOnly && !IsPublic(constructor.Signature.Modifiers)) - { - continue; - } - - AddSignatureReferences(references[current], constructor.Signature, nodes, serializationReferenceNamesByType, includeAttributes: !publicOnly); - } - - foreach (var method in provider.Methods) - { - if (method.IsMethodSuppressed()) - { - continue; - } - - if (publicOnly && !IsPublic(method.Signature.Modifiers)) - { - continue; - } - - AddSignatureReferences(references[current], method.Signature, nodes, serializationReferenceNamesByType, includeAttributes: !publicOnly); - if (!publicOnly && !ShouldUseGeneratedSourceReferences(provider)) - { - AddTypeReference(references[current], GetCollectionDefinitionType(method), nodes, serializationReferenceNamesByType); - } - } - } - - return new ProviderReferenceGraph(nodes, references); - } - - private static CSharpType? GetCollectionDefinitionType(MethodProvider method) - { - var property = method.GetType().GetProperty("CollectionDefinition"); - return property?.GetValue(method) is TypeProvider collectionDefinition - ? collectionDefinition.Type - : null; - } - - private static bool IsPublic(MethodSignatureModifiers modifiers) => modifiers.HasFlag(MethodSignatureModifiers.Public); - - private static Dictionary> CloneReferences(IReadOnlyDictionary> references) - { - return references.ToDictionary( - static item => item.Key, - static item => item.Value.ToHashSet(StringComparer.Ordinal), - StringComparer.Ordinal); - } - - private static void AddDerivedModelReferences( - IReadOnlyList providers, - HashSet nodes, - Dictionary> references, - HashSet publicBaseModels, - HashSet generatedDiscriminatorBaseNames) - { - var modelProviders = providers.OfType().ToArray(); - var publicModelProviders = modelProviders - .Where(static provider => provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) - .ToArray(); - var discriminatorProviders = publicModelProviders - .Where(static provider => provider.DiscriminatorProperty != null || provider.DiscriminatorValue != null) - .Where(static provider => !provider.IsUnknownDiscriminatorModel) - .ToArray(); - var discriminatorBaseNames = publicModelProviders - .Where(static provider => provider.DiscriminatorProperty != null) - .Select(static provider => GetProviderTypeName(provider.Type)) - .ToHashSet(StringComparer.Ordinal); - discriminatorBaseNames.UnionWith(generatedDiscriminatorBaseNames); - var addedReference = true; - while (addedReference) - { - addedReference = false; - foreach (var provider in discriminatorProviders) - { - var providerName = GetProviderTypeName(provider.Type); - if (!nodes.Contains(providerName)) - { - continue; - } - - if (!publicBaseModels.Contains(providerName)) - { - continue; - } - - foreach (var derivedModel in provider.DerivedModels) - { - if (derivedModel.IsUnknownDiscriminatorModel || - !derivedModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) - { - continue; - } - - var before = references[providerName].Count; - AddTypeReference(references[providerName], derivedModel.Type, nodes); - var derivedName = GetProviderTypeName(derivedModel.Type); - if (nodes.Contains(derivedName) && publicBaseModels.Add(derivedName) || references[providerName].Count != before) - { - addedReference = true; - } - } - } - - foreach (var provider in modelProviders) - { - if (provider.IsUnknownDiscriminatorModel || - !provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) - { - continue; - } - - var providerName = GetProviderTypeName(provider.Type); - if (!nodes.Contains(providerName)) - { - continue; - } - - var baseTypeName = provider.BaseType == null ? null : GetProviderTypeName(provider.BaseType); - if (baseTypeName == null || - !discriminatorBaseNames.Contains(baseTypeName) || - !nodes.Contains(baseTypeName) || - !publicBaseModels.Contains(baseTypeName)) - { - continue; - } - - var before = references[baseTypeName].Count; - references[baseTypeName].Add(providerName); - if (publicBaseModels.Add(providerName) || references[baseTypeName].Count != before) - { - addedReference = true; - } - } - } - } - - private static void AddBasePreservedReferences( - IReadOnlyList generatedProviders, - HashSet nodes, - IReadOnlyDictionary> references, - HashSet reachableTypes) - { - var basePreservedRoots = new HashSet(StringComparer.Ordinal); - var addedRoot = true; - while (addedRoot) - { - addedRoot = false; - foreach (var provider in generatedProviders) - { - var providerName = GetProviderTypeName(provider.Type); - if (!nodes.Contains(providerName) || reachableTypes.Contains(providerName) || basePreservedRoots.Contains(providerName)) - { - continue; - } - - var baseTypeName = provider.BaseType == null ? null : GetProviderTypeName(provider.BaseType); - if (baseTypeName == null || !reachableTypes.Contains(baseTypeName)) - { - continue; - } - - if (basePreservedRoots.Add(providerName)) - { - addedRoot = true; - } - } - - if (addedRoot) - { - reachableTypes.UnionWith(GetReachableTypes(basePreservedRoots, references)); - } - } - } - - private static IReadOnlyList GetGeneratedProviders(IReadOnlyList providers) - { - var generatedProviders = new List(); - foreach (var provider in providers) - { - AddGeneratedProvider(generatedProviders, provider); - } - - return generatedProviders; - } - - private static void AddGeneratedProvider(List generatedProviders, TypeProvider provider) - { - generatedProviders.Add(provider); - foreach (var nestedType in provider.NestedTypes) - { - AddGeneratedProvider(generatedProviders, nestedType); - } - - foreach (var serializationProvider in provider.SerializationProviders) - { - AddGeneratedProvider(generatedProviders, serializationProvider); - } - } - - private static void AddGeneratedBodyReferences(Project project, IReadOnlyList providers, ProviderReferenceGraph graph) - { - var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); - if (compilation == null) - { - return; - } - - foreach (var provider in GetBodyReferenceProviders(providers)) - { - if (IsModelFactoryProvider(provider)) - { - continue; - } - - if (!IsGeneratedBodyReferenceCandidate(provider)) - { - continue; - } - - var providerName = GetProviderTypeName(provider.Type); - if (!graph.Nodes.Contains(providerName)) - { - continue; - } - - var bodyDependencyTypes = ShouldUseGeneratedSourceReferences(provider) ? [] : provider.BodyDependencyTypes; - AddProviderBodyDependencyTypes(graph.References[providerName], bodyDependencyTypes, graph.Nodes); - - var symbol = compilation.GetTypeByMetadataName(providerName); - if (symbol == null) - { - continue; - } - - AddGeneratedBodyTypeReferences(project, compilation, graph, providerName, symbol); - } - } - - private static void AddProviderBodyDependencyTypes(HashSet references, IReadOnlyList dependencies, HashSet nodes) - { - foreach (var dependency in dependencies) - { - AddProviderBodyDependencyType(references, dependency, nodes); - } - } - - private static void AddProviderBodyDependencyType(HashSet references, CSharpType? dependency, HashSet nodes) - { - if (dependency == null) - { - return; - } - - AddTypeReference(references, dependency, nodes); - AddMatchingName(references, $"{dependency.Name}Extensions", nodes); - - foreach (var argument in dependency.Arguments) - { - AddProviderBodyDependencyType(references, argument, nodes); - } - } - - private static IReadOnlyList GetBodyReferenceProviders(IReadOnlyList providers) - { - var bodyReferenceProviders = new List(); - foreach (var provider in providers) - { - bodyReferenceProviders.Add(provider); - bodyReferenceProviders.AddRange(provider.SerializationProviders); - } - - return bodyReferenceProviders; - } - - private static bool IsGeneratedBodyReferenceCandidate(TypeProvider provider) - { - if (ShouldUseGeneratedSourceReferences(provider)) - { - return true; - } - - if (provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Static)) - { - return true; - } - - var relativePath = provider.RelativeFilePath.Replace('\\', '/'); - return IsSerializationProvider(provider) || - relativePath.EndsWith("/Internal/ClientUriBuilder.cs", StringComparison.Ordinal) || - provider.HelperDependencyNames.Count > 0 || - provider.BodyDependencyTypes.Count > 0; - } - - private static bool ShouldUseGeneratedSourceReferences(TypeProvider provider) => - CodeModelGenerator.Instance.Configuration.PackageName.StartsWith("Azure.", StringComparison.Ordinal) && - provider.RelativeFilePath.EndsWith("Client.cs", StringComparison.Ordinal); - - private static void AddGeneratedXmlDocCrefReferences(Project project, ProviderReferenceGraph graph, bool publicOnly) - { - var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); - if (compilation == null) - { - return; - } - - foreach (var providerName in graph.Nodes) - { - var symbol = compilation.GetTypeByMetadataName(providerName); - if (symbol == null || publicOnly && symbol.DeclaredAccessibility != Accessibility.Public) - { - continue; - } - - foreach (var property in symbol.GetMembers().OfType()) - { - if (publicOnly && property.DeclaredAccessibility != Accessibility.Public) - { - continue; - } - - var xml = property.GetDocumentationCommentXml(); - if (string.IsNullOrEmpty(xml) || - !xml.Contains("cref=\"T:", StringComparison.Ordinal)) - { - continue; - } - - var cRefs = XDocument.Parse(xml) - .Descendants() - .Attributes("cref") - .Select(static attribute => attribute.Value) - .Where(static cref => cref.Length > 2 && cref[0] == 'T' && cref[1] == ':') - .Select(static cref => cref.Substring(2)); - - foreach (var cref in cRefs) - { - AddMatchingName(graph.References[providerName], cref, graph.Nodes); - } - } - } - } - - private static bool IsGeneratedHelperSimpleName(string name) => - string.Equals(name, "ChangeTrackingDictionary", StringComparison.Ordinal) || - string.Equals(name, "ChangeTrackingList", StringComparison.Ordinal); - - private static void AddGeneratedBodyTypeReferences(Project project, Compilation compilation, ProviderReferenceGraph graph, string ownerName, INamedTypeSymbol ownerSymbol) - { - foreach (var syntaxReference in ownerSymbol.DeclaringSyntaxReferences) - { - var document = project.GetDocument(syntaxReference.SyntaxTree); - if (document == null || !IsGeneratedDocument(document)) - { - continue; - } - - var root = syntaxReference.SyntaxTree.GetRoot(); - var semanticModel = compilation.GetSemanticModel(syntaxReference.SyntaxTree); - foreach (var node in root.DescendantNodes()) - { - if (node is TypeSyntax typeSyntax) - { - if (IsNamespaceOrUsingName(typeSyntax)) - { - continue; - } - - // Declaration names are the owner itself. The old Roslyn map captures references, - // not a declaration making itself reachable. - if (typeSyntax.Parent is BaseTypeDeclarationSyntax baseTypeDeclaration && baseTypeDeclaration.Identifier.Span == typeSyntax.Span) - { - continue; - } - - var typeInfo = semanticModel.GetTypeInfo(typeSyntax).Type; - AddBodyTypeReference(graph.References[ownerName], typeInfo, graph.Nodes); - AddUnresolvedBodyTypeSyntaxReference(graph.References[ownerName], typeSyntax, typeInfo, graph.Nodes); - } - - if (node is InvocationExpressionSyntax invocation) - { - if (semanticModel.GetSymbolInfo(invocation).Symbol is not IMethodSymbol method) - { - continue; - } - - AddBodyTypeReference(graph.References[ownerName], method.ContainingType, graph.Nodes); - AddBodyTypeReference(graph.References[ownerName], method.ReducedFrom?.ContainingType, graph.Nodes); - } - } - } - } - - private static bool IsNamespaceOrUsingName(TypeSyntax typeSyntax) - { - for (SyntaxNode? node = typeSyntax; node != null; node = node.Parent) - { - if (node.Parent is BaseNamespaceDeclarationSyntax namespaceDeclaration && namespaceDeclaration.Name == node || - node.Parent is UsingDirectiveSyntax usingDirective && usingDirective.Name == node) - { - return true; - } - } - - return false; - } - - private static void AddUnresolvedBodyTypeSyntaxReference(HashSet references, TypeSyntax typeSyntax, ITypeSymbol? symbol, HashSet nodes) - { - if (symbol is INamedTypeSymbol { TypeKind: not TypeKind.Error }) - { - return; - } - - var simpleName = typeSyntax switch - { - QualifiedNameSyntax qualifiedName => qualifiedName.Right.Identifier.ValueText, - AliasQualifiedNameSyntax aliasQualifiedName => aliasQualifiedName.Name.Identifier.ValueText, - GenericNameSyntax genericName => genericName.Identifier.ValueText, - IdentifierNameSyntax identifierName => identifierName.Identifier.ValueText, - _ => null - }; - - if (simpleName != null) - { - AddMatchingName(references, simpleName, nodes); - } - } - - private static void AddBodyTypeReference(HashSet references, ITypeSymbol? symbol, HashSet nodes) - { - if (symbol is not INamedTypeSymbol namedType || namedType.TypeKind == TypeKind.Error) - { - return; - } - - if (IsGeneratedHelperSimpleName(namedType.Name)) - { - AddMatchingName(references, namedType.Name, nodes); - } - AddMatchingName(references, namedType.GetFullyQualifiedName(), nodes); - if (namedType.TypeKind == TypeKind.Enum) - { - AddMatchingName(references, $"{namedType.Name}Extensions", nodes); - } - - foreach (var typeArgument in namedType.TypeArguments) - { - AddBodyTypeReference(references, typeArgument, nodes); - } - } - - private static HashSet GetRootNames( - IReadOnlyList providers, - HashSet nodes, - HashSet helperRoots, - bool includeModelFactory, - bool includeAdditionalRoots, - bool includeUnionVariantRoots, - bool publicClientRootsOnly) - { - var generator = CodeModelGenerator.Instance; - var roots = new HashSet(StringComparer.Ordinal); - var modelFactoryName = GetProviderTypeName(generator.OutputLibrary.ModelFactory.Value.Type); - - foreach (var provider in providers) - { - var name = GetProviderTypeName(provider.Type); - if (IsClientProviderRoot(provider, publicClientRootsOnly) || - includeAdditionalRoots && provider.DeclaringTypeProvider == null && IsKept(provider.Type, generator.AdditionalRootTypes, nodes) || - includeModelFactory && string.Equals(name, modelFactoryName, StringComparison.Ordinal) || - includeModelFactory && helperRoots.Contains(name)) - { - roots.Add(name); - } - } - - AddLastContractModelFactorySignatureRoots(providers, roots, nodes); - - if (!includeUnionVariantRoots) - { - return roots; - } - - foreach (var root in generator.TypeFactory.UnionVariantTypesToKeep) - { - AddMatchingName(roots, root, nodes); - } - - foreach (var root in generator.AdditionalRootTypes) - { - AddMatchingName(roots, root, nodes); - } - - return roots; - } - - private static void AddLastContractModelFactorySignatureRoots(IReadOnlyList providers, HashSet roots, HashSet nodes) - { - foreach (var provider in providers.Where(IsModelFactoryProvider)) - { - foreach (var method in provider.LastContractView?.Methods ?? []) - { - if (!method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public) || - IsImplementationOnlyModelFactoryMethod(method)) - { - continue; - } - - AddTypeReference(roots, method.Signature.ReturnType, nodes); - foreach (var parameter in method.Signature.Parameters) - { - AddTypeReference(roots, parameter.Type, nodes); - } - } - } - } - - private static bool IsImplementationOnlyModelFactoryMethod(MethodProvider method) - { - var returnType = method.Signature.ReturnType; - if (returnType == null) - { - return true; - } - - var returnTypeName = GetSimpleName(GetProviderTypeName(returnType)); - return returnTypeName.StartsWith("Paged", StringComparison.Ordinal) || - returnTypeName.EndsWith("Request", StringComparison.Ordinal); - } - - private static HashSet GetPostProcessorDeclaredNodes(IReadOnlyList generatedProviders, HashSet nodes, bool publicOnly) - { - var generator = CodeModelGenerator.Instance; - var excludedNames = generator.NonRootTypes; - return generatedProviders - .Where(provider => !IsModelFactoryProvider(provider)) - .Where(provider => !publicOnly || provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) - .Select(provider => GetProviderTypeName(provider.Type)) - .Where(name => nodes.Contains(name)) - .Where(name => !excludedNames.Contains(name) && !excludedNames.Contains(GetSimpleName(name))) - .ToHashSet(StringComparer.Ordinal); - } - - private static bool IsKept(CSharpType type, HashSet roots, HashSet nodes) => - roots.Contains(type.Name) || roots.Contains(GetProviderTypeName(type)) && nodes.Contains(GetProviderTypeName(type)); - - private static bool IsClientProviderRoot(TypeProvider provider, bool publicOnly) => - provider.RelativeFilePath.EndsWith("Client.cs", StringComparison.Ordinal) && - (!publicOnly || !HasApiBaselineDirectory() && provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); - - private static bool HasApiBaselineDirectory() - { - var projectDirectory = CodeModelGenerator.Instance.Configuration.ProjectDirectory; - return !string.IsNullOrEmpty(projectDirectory) && - Directory.Exists(Path.GetFullPath(Path.Combine(projectDirectory, "..", "api"))); - } - - private static bool IsModelFactoryProvider(TypeProvider provider) - { - if (provider is ModelFactoryProvider) - { - return true; - } - - var relativePath = provider.RelativeFilePath.Replace('\\', '/'); - return relativePath.EndsWith("ModelFactory.cs", StringComparison.Ordinal); - } - - private static HashSet GetHelperRootNames( - IReadOnlyList generatedProviders, - HashSet nodes, - HashSet reachableTypes, - IReadOnlyDictionary>? references = null) - { - var roots = new HashSet(StringComparer.Ordinal); - foreach (var provider in generatedProviders) - { - var providerName = GetProviderTypeName(provider.Type); - var isModelFactory = IsModelFactoryProvider(provider); - if (!reachableTypes.Contains(providerName) && !isModelFactory) - { - continue; - } - - AddHelperDependencies(roots, provider.HelperDependencyNames, nodes, references == null ? null : references[providerName]); - - foreach (var property in provider.Properties) - { - AddInitializationHelperRoot(roots, property.Type, nodes); - AddParameterValidationHelperRoot(roots, property.AsParameter, nodes); - } - - foreach (var field in provider.Fields) - { - AddParameterValidationHelperRoot(roots, field.AsParameter, nodes); - } - - foreach (var constructor in provider.Constructors) - { - foreach (var parameter in constructor.Signature.Parameters) - { - AddParameterValidationHelperRoot(roots, parameter, nodes); - } - } - - foreach (var method in provider.Methods) - { - // Only factory methods for reachable models can instantiate collection helpers. - if (isModelFactory && - (method.Signature.ReturnType == null || !reachableTypes.Contains(GetProviderTypeName(method.Signature.ReturnType)))) - { - continue; - } - - foreach (var parameter in method.Signature.Parameters) - { - AddParameterValidationHelperRoot(roots, parameter, nodes); - if (isModelFactory) - { - AddModelFactoryCollectionInitializationHelperRoot(roots, parameter.Type, nodes); - } - } - } - } - - return roots; - } - - private static void AddParameterValidationHelperRoot(HashSet roots, ParameterProvider parameter, HashSet nodes) - { - if (parameter.Validation != ParameterValidationType.None) - { - AddMatchingName(roots, "Argument", nodes); - } - } - - private static void AddHelperDependencies( - HashSet roots, - IReadOnlyList dependencies, - HashSet nodes, - HashSet? referencedNames) - { - foreach (var dependency in dependencies) - { - if (referencedNames == null) - { - AddMatchingName(roots, dependency, nodes); - continue; - } - - var matches = new HashSet(StringComparer.Ordinal); - AddMatchingName(matches, dependency, nodes); - roots.UnionWith(matches.Intersect(referencedNames, StringComparer.Ordinal)); - } - } - - private static void RemoveUnusedRequestHeaderExtensionsRoot( - HashSet roots, - IReadOnlyDictionary> references, - Project project) - { - var hasCustomReference = HasCustomRequestHeaderExtensionsReference(project); - var unusedRequestHeaderExtensions = roots - .Where(static root => root.EndsWith(".RequestHeaderExtensions", StringComparison.Ordinal)) - .Where(_ => !hasCustomReference) - .Where(root => !references.Any(reference => - !string.Equals(reference.Key, root, StringComparison.Ordinal) && - reference.Value.Contains(root))) - .ToArray(); - - roots.ExceptWith(unusedRequestHeaderExtensions); - } - - private static bool HasCustomRequestHeaderExtensionsReference(Project project) - { - foreach (var document in project.Documents) - { - if (IsGeneratedDocument(document)) - { - continue; - } - - var text = document.GetTextAsync().GetAwaiter().GetResult().ToString(); - if (text.Contains("RequestHeaderExtensions", StringComparison.Ordinal) || - text.Contains("SetDelimited", StringComparison.Ordinal)) - { - return true; - } - } - - return false; - } - - private static bool IsSerializationProvider(TypeProvider provider) - { - var relativePath = provider.RelativeFilePath.Replace('\\', '/'); - return relativePath.EndsWith(".Serialization.cs", StringComparison.Ordinal) || - relativePath.EndsWith(".Serialization.Multipart.cs", StringComparison.Ordinal); - } - - private static bool IsGeneratedDocument(Document document) - { - if (GeneratedCodeWorkspace.IsGeneratedDocument(document) || GeneratedCodeWorkspace.IsGeneratedTestDocument(document)) - { - return true; - } - - var filePath = document.FilePath?.Replace('\\', '/'); - return filePath != null && - (filePath.Contains("/Generated/", StringComparison.Ordinal) || - filePath.Contains("/GeneratedTests/", StringComparison.Ordinal)); - } - - private static void AddInitializationHelperRoot(HashSet roots, CSharpType? type, HashSet nodes) - { - if (type == null) - { - return; - } - - var initializationType = type.PropertyInitializationType; - if (!string.Equals(initializationType.FullyQualifiedName, type.FullyQualifiedName, StringComparison.Ordinal)) - { - AddMatchingName(roots, initializationType.Name, nodes); - } - - if (type is { IsList: true, IsReadOnlyMemory: false }) - { - AddMatchingName(roots, "ChangeTrackingList", nodes); - } - - if (type.IsDictionary) - { - AddMatchingName(roots, "ChangeTrackingDictionary", nodes); - } - - foreach (var argument in type.Arguments) - { - AddInitializationHelperRoot(roots, argument, nodes); - } - } - - private static void AddModelFactoryCollectionInitializationHelperRoot(HashSet roots, CSharpType? type, HashSet nodes) - { - if (type == null) - { - return; - } - - if (type is { IsList: true, IsReadOnlyMemory: false }) - { - AddMatchingName(roots, "ChangeTrackingList", nodes); - } - - if (type.IsDictionary) - { - AddMatchingName(roots, "ChangeTrackingDictionary", nodes); - } - - foreach (var argument in type.Arguments) - { - AddModelFactoryCollectionInitializationHelperRoot(roots, argument, nodes); - } - } - - private static void AddMatchingName(HashSet target, string name, HashSet nodes) - { - if (nodes.Contains(name)) - { - target.Add(name); - return; - } - - var simpleNameLookup = _simpleNameLookupCache.GetValue(nodes, BuildSimpleNameLookup); - if (!simpleNameLookup.TryGetValue(name, out var matches)) - { - return; - } - - foreach (var match in matches) - { - target.Add(match); - } - } - - private static Dictionary BuildSimpleNameLookup(HashSet nodes) - { - return nodes - .GroupBy(static node => StripGenericArity(GetSimpleName(node)), StringComparer.Ordinal) - .ToDictionary(static group => group.Key, static group => group.ToArray(), StringComparer.Ordinal); - } - - private static HashSet GetReachableTypes(HashSet roots, IReadOnlyDictionary> references) - { - return GetReachableTypes(roots, references, expandableNodes: null); - } - - private static HashSet GetReachableTypes( - HashSet roots, - IReadOnlyDictionary> references, - HashSet? expandableNodes) - { - var reachable = new HashSet(StringComparer.Ordinal); - var queue = new Queue(roots); - while (queue.Count > 0) - { - var current = queue.Dequeue(); - if (!reachable.Add(current)) - { - continue; - } - - if (expandableNodes != null && !expandableNodes.Contains(current)) - { - continue; - } - - if (!references.TryGetValue(current, out var children)) - { - continue; - } - - foreach (var child in children) - { - queue.Enqueue(child); - } - } - - return reachable; - } - - private static bool HasPublicApiPredecessor( - string name, - IReadOnlyDictionary> references, - HashSet publicizeReachable, - HashSet generatedImplementationInternalDeclarations) - { - foreach (var (owner, children) in references) - { - if (!publicizeReachable.Contains(owner) || - string.Equals(owner, name, StringComparison.Ordinal) || - generatedImplementationInternalDeclarations.Contains(owner) || - !children.Contains(name)) - { - continue; - } - - return true; - } - - return false; - } - - private static void AddSignatureReferences( - HashSet references, - MethodSignatureBase signature, - HashSet nodes, - IReadOnlyDictionary? serializationProviderNamesByType, - bool includeAttributes = true) - { - AddTypeReference(references, signature.ReturnType, nodes, serializationProviderNamesByType); - if (includeAttributes) - { - AddAttributes(references, signature.Attributes, nodes, serializationProviderNamesByType); - } - - foreach (var parameter in signature.Parameters) - { - AddTypeReference(references, parameter.Type, nodes, serializationProviderNamesByType); - if (includeAttributes) - { - AddAttributes(references, parameter.Attributes, nodes, serializationProviderNamesByType); - } - } - - if (signature is MethodSignature methodSignature) - { - AddTypeReference(references, methodSignature.ExplicitInterface, nodes, serializationProviderNamesByType); - if (methodSignature.GenericArguments != null) - { - foreach (var genericArgument in methodSignature.GenericArguments) - { - AddTypeReference(references, genericArgument, nodes, serializationProviderNamesByType); - } - } - - if (methodSignature.GenericParameterConstraints != null) - { - foreach (var constraint in methodSignature.GenericParameterConstraints) - { - AddTypeReference(references, constraint.Type, nodes, serializationProviderNamesByType); - } - } - } - - if (signature is ConstructorSignature constructorSignature) - { - AddTypeReference(references, constructorSignature.Type, nodes, serializationProviderNamesByType); - } - } - - private static void AddAttributes( - HashSet references, - IReadOnlyList attributes, - HashSet nodes, - IReadOnlyDictionary? serializationProviderNamesByType) - { - foreach (var attribute in attributes) - { - AddTypeReference(references, attribute.Type, nodes, serializationProviderNamesByType); - } - } - - private static void AddTypeReference( - HashSet references, - CSharpType? type, - HashSet nodes, - IReadOnlyDictionary? serializationProviderNamesByType = null) - { - if (type == null) - { - return; - } - - var providerTypeName = GetProviderTypeName(type); - if (nodes.Contains(providerTypeName)) - { - references.Add(providerTypeName); - if (serializationProviderNamesByType != null && serializationProviderNamesByType.TryGetValue(providerTypeName, out var serializationProviderNames)) - { - foreach (var serializationProviderName in serializationProviderNames) - { - references.Add(serializationProviderName); - } - } - } - - AddTypeReference(references, type.BaseType, nodes, serializationProviderNamesByType); - AddTypeReference(references, type.DeclaringType, nodes, serializationProviderNamesByType); - foreach (var argument in type.Arguments) - { - AddTypeReference(references, argument, nodes, serializationProviderNamesByType); - } - } - - public static void WriteComparisonReport(string passName, IEnumerable roslynCandidates, IEnumerable providerCandidates) - { - if (!IsEnabled || !ShouldWriteReports) - { - return; - } - - var roslynSet = roslynCandidates.ToHashSet(StringComparer.Ordinal); - var providerSet = providerCandidates.ToHashSet(StringComparer.Ordinal); - var missingFromProvider = roslynSet.Except(providerSet, StringComparer.Ordinal).OrderBy(static name => name, StringComparer.Ordinal).ToArray(); - var extraInProvider = providerSet.Except(roslynSet, StringComparer.Ordinal).OrderBy(static name => name, StringComparer.Ordinal).ToArray(); - - var directory = GetOutputDirectory(); - Directory.CreateDirectory(directory); - var path = Path.Combine(directory, $"provider-reference-map-shadow-comparison-{passName}-{DateTime.UtcNow:yyyyMMddHHmmssfff}.txt"); - var builder = new StringBuilder(); - builder.AppendLine($"Provider reference map shadow comparison: {passName}"); - builder.AppendLine($"Roslyn candidates: {roslynSet.Count}"); - builder.AppendLine($"Provider candidates: {providerSet.Count}"); - builder.AppendLine($"Missing from provider: {missingFromProvider.Length}"); - builder.AppendLine($"Extra in provider: {extraInProvider.Length}"); - builder.AppendLine(); - builder.AppendLine("Missing from provider:"); - foreach (var item in missingFromProvider) - { - builder.AppendLine($" {item}"); - } - - builder.AppendLine(); - builder.AppendLine("Extra in provider:"); - foreach (var item in extraInProvider) - { - builder.AppendLine($" {item}"); - } - - File.WriteAllText(path, builder.ToString()); - CodeModelGenerator.Instance.Emitter.Debug($"Provider reference map shadow comparison written to {path}"); - } - - private static void WriteReport( - ProviderReferenceGraph graph, - HashSet customPublicRoots, - HashSet customRemovalRoots, - HashSet helperRoots, - HashSet internalizeRoots, - HashSet internalizeReachable, - IReadOnlyList internalizeCandidates, - HashSet publicizeRoots, - HashSet publicizeReachable, - IReadOnlyList publicizeCandidates, - HashSet removeRoots, - HashSet removeReachable, - IReadOnlyList removeCandidates) - { - var directory = GetOutputDirectory(); - - Directory.CreateDirectory(directory); - var path = Path.Combine(directory, $"provider-reference-map-shadow-{DateTime.UtcNow:yyyyMMddHHmmssfff}.txt"); - var builder = new StringBuilder(); - builder.AppendLine("Provider reference map shadow report"); - builder.AppendLine($"Declared providers: {graph.Nodes.Count}"); - builder.AppendLine($"Internalize roots: {internalizeRoots.Count}"); - builder.AppendLine($"Internalize reachable: {internalizeReachable.Count}"); - builder.AppendLine($"Internalize candidates: {internalizeCandidates.Count}"); - builder.AppendLine($"Publicize roots: {publicizeRoots.Count}"); - builder.AppendLine($"Publicize reachable: {publicizeReachable.Count}"); - builder.AppendLine($"Publicize candidates: {publicizeCandidates.Count}"); - builder.AppendLine($"Custom public roots: {customPublicRoots.Count}"); - builder.AppendLine($"Custom removal roots: {customRemovalRoots.Count}"); - builder.AppendLine($"Helper roots: {helperRoots.Count}"); - builder.AppendLine($"Remove roots: {removeRoots.Count}"); - builder.AppendLine($"Remove reachable: {removeReachable.Count}"); - builder.AppendLine($"Remove candidates: {removeCandidates.Count}"); - builder.AppendLine(); - AppendItems(builder, "Custom public roots", customPublicRoots); - AppendItems(builder, "Custom removal roots", customRemovalRoots); - AppendItems(builder, "Helper roots", helperRoots); - AppendItems(builder, "Internalize roots", internalizeRoots); - AppendItems(builder, "Internalize candidates", internalizeCandidates); - AppendItems(builder, "Publicize roots", publicizeRoots); - AppendItems(builder, "Publicize candidates", publicizeCandidates); - AppendItems(builder, "Remove roots", removeRoots); - AppendItems(builder, "Remove candidates", removeCandidates); - - builder.AppendLine(); - builder.AppendLine("References:"); - foreach (var (type, references) in graph.References.OrderBy(static item => item.Key, StringComparer.Ordinal)) - { - builder.AppendLine($" {type}"); - foreach (var reference in references.OrderBy(static name => name, StringComparer.Ordinal)) - { - builder.AppendLine($" -> {reference}"); - } - } - - File.WriteAllText(path, builder.ToString()); - CodeModelGenerator.Instance.Emitter.Debug($"Provider reference map shadow report written to {path}"); - } - - private static void AppendItems(StringBuilder builder, string title, IEnumerable items) - { - builder.AppendLine(); - builder.AppendLine($"{title}:"); - foreach (var item in items.OrderBy(static name => name, StringComparer.Ordinal)) - { - builder.AppendLine($" {item}"); - } - } - - private static string GetOutputDirectory() - { - var directory = Environment.GetEnvironmentVariable(OutputDirectoryEnvironmentVariable); - return string.IsNullOrWhiteSpace(directory) - ? Path.Combine(Path.GetTempPath(), "typespec-provider-reference-map-shadow") - : Path.GetFullPath(directory); - } - - private static string GetSimpleName(string fullyQualifiedName) - { - var lastDot = fullyQualifiedName.LastIndexOf('.'); - return lastDot < 0 ? fullyQualifiedName : fullyQualifiedName.Substring(lastDot + 1); - } - - private static string GetProviderTypeName(CSharpType type) - { - var name = type.Arguments.Count > 0 && !type.Name.Contains('`', StringComparison.Ordinal) - ? $"{type.Name}`{type.Arguments.Count}" - : type.Name; - return string.IsNullOrEmpty(type.Namespace) ? name : $"{type.Namespace}.{name}"; - } - - private static string StripGenericArity(string name) - { - var tick = name.IndexOf('`'); - return tick < 0 ? name : name.Substring(0, tick); - } - - private sealed record ProviderReferenceGraph( - HashSet Nodes, - Dictionary> References); } } From bf295d12b49eb61c6f18473599c29a57eb330057 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 1 Jul 2026 09:11:55 +0000 Subject: [PATCH 110/111] Fix CSharp provider helper reachability Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ClientBodyDependencyPostProcessingTests.cs | 58 +++++++++++++++++++ .../ProviderReferenceMapAnalyzer.cs | 4 +- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs index 39794d21310..de336d6856e 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs @@ -175,6 +175,56 @@ await GenerateAndAssertFiles( expectedFiles: [Path.Combine("src", "Generated", "Internal", "PipelineRequestHeadersExtensions.cs")]); } + [Test] + public async Task BinaryDataBodyParameterDoesNotKeepBinaryContentHelpers() + { + var parameter = InputFactory.BodyParameter( + "content", + InputPrimitiveType.Base64, + isRequired: true, + contentTypes: ["application/octet-stream"], + defaultContentType: "application/octet-stream"); + var operation = InputFactory.Operation("Upload", parameters: [parameter], httpMethod: "POST"); + var method = InputFactory.BasicServiceMethod( + "Upload", + operation, + parameters: [InputFactory.MethodParameter("content", InputPrimitiveType.Base64, isRequired: true)]); + var client = InputFactory.Client("TestClient", methods: [method]); + + await GenerateAndAssertFiles( + enums: [], + models: [], + clients: [client], + customFiles: [], + expectedFiles: [], + unexpectedFiles: [ + Path.Combine("src", "Generated", "Internal", "BinaryContentHelper.cs"), + Path.Combine("src", "Generated", "Internal", "Utf8JsonBinaryContent.cs") + ]); + } + + [Test] + public async Task CollectionBodyParameterKeepsBinaryContentHelpers() + { + var parameter = InputFactory.BodyParameter("items", InputFactory.Array(InputPrimitiveType.String), isRequired: true); + var operation = InputFactory.Operation("Create", parameters: [parameter], httpMethod: "POST"); + var method = InputFactory.BasicServiceMethod( + "Create", + operation, + parameters: [InputFactory.MethodParameter("items", InputFactory.Array(InputPrimitiveType.String), isRequired: true)]); + var client = InputFactory.Client("TestClient", methods: [method]); + + await GenerateAndAssertFiles( + enums: [], + models: [], + clients: [client], + customFiles: [], + expectedFiles: [ + Path.Combine("src", "Generated", "Internal", "BinaryContentHelper.cs"), + Path.Combine("src", "Generated", "Internal", "Utf8JsonBinaryContent.cs") + ]); + } + [Test] public async Task CustomOnlyRequestHeaderSetDelimitedReferenceKeepsExtensions() { @@ -311,12 +361,14 @@ private static async Task GenerateAndAssertFiles( InputClient[] clients, (string Path, string Content)[] customFiles, string[] expectedFiles, + string[] unexpectedFiles = null!, string[] publicModelNames = null!, string[] internalModelNames = null!, string packageName = "Sample") { publicModelNames ??= []; internalModelNames ??= []; + unexpectedFiles ??= []; var outputPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); Directory.CreateDirectory(outputPath); @@ -375,6 +427,12 @@ await MockHelpers.LoadMockGeneratorAsync( var filePath = Path.Combine(outputPath, expectedFile); Assert.IsTrue(File.Exists(filePath), $"Expected generated file '{filePath}'."); } + + foreach (var unexpectedFile in unexpectedFiles) + { + var filePath = Path.Combine(outputPath, unexpectedFile); + Assert.IsFalse(File.Exists(filePath), $"Did not expect generated file '{filePath}'."); + } } finally { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs index 01e81575478..d7f02980e62 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs @@ -924,7 +924,6 @@ private static void AddProviderInfrastructureReferences(HashSet referenc if (IsSerializationProvider(provider)) { AddMatchingName(references, "Optional", nodes); - AddMatchingName(references, "BinaryContentHelper", nodes); AddMatchingName(references, "Utf8JsonRequestContent", nodes); AddMatchingName(references, "ModelSerializationExtensions", nodes); AddSerializationExtensionReferences(references, provider, nodes); @@ -1032,8 +1031,7 @@ private static void AddRequestContentInfrastructureReferences(HashSet re return; } - if (string.Equals(type.Name, "RequestContent", StringComparison.Ordinal) || - string.Equals(type.Name, "BinaryData", StringComparison.Ordinal)) + if (string.Equals(type.Name, "RequestContent", StringComparison.Ordinal)) { AddMatchingName(references, "BinaryContentHelper", nodes); AddMatchingName(references, "Utf8JsonRequestContent", nodes); From bd0b3c864d3f0722aa2399c2970c16c1b5e71eef Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Mon, 6 Jul 2026 02:28:59 +0000 Subject: [PATCH 111/111] fix(http-client-csharp): preserve reference map SDK compatibility Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ybrid-reference-map-2026-07-06-02-27-00.md | 7 + .../src/Providers/ClientOptionsProvider.cs | 5 + .../ClientPipelineExtensionsDefinition.cs | 4 + .../src/Providers/ClientProvider.cs | 37 +- .../ClientProviderCustomizationTests.cs | 3 +- .../src/CSharpGen.cs | 18 +- .../src/PostProcessing/PostProcessor.cs | 10 + .../ProviderReferenceMapAnalyzer.cs | 1849 ----------------- .../src/Primitives/TypeProviderWriter.cs | 56 +- .../src/Providers/ModelFactoryProvider.cs | 4 + .../src/Providers/NamedTypeSymbolProvider.cs | 271 ++- .../src/Providers/TypeProvider.cs | 2 + ...iderReferenceMapAnalyzer.BodyReferences.cs | 405 ++++ ...ProviderReferenceMapAnalyzer.Candidates.cs | 358 ++++ ...derReferenceMapAnalyzer.CustomCodeRoots.cs | 542 +++++ ...oviderReferenceMapAnalyzer.GraphBuilder.cs | 180 ++ .../ProviderReferenceMapAnalyzer.Helpers.cs | 617 ++++++ ...ReferenceMapAnalyzer.ReferenceTraversal.cs | 184 ++ ...viderReferenceMapAnalyzer.RootSelection.cs | 177 ++ ...renceMapAnalyzer.TypeReferenceCollector.cs | 177 ++ .../ProviderReferenceMapAnalyzer.cs | 268 +++ .../ProviderReferenceMapResult.cs | 0 .../src/SourceInput/SourceInputModel.cs | 18 +- .../NamedTypeSymbolProviderTests.cs | 49 + 24 files changed, 3316 insertions(+), 1925 deletions(-) create mode 100644 .chronus/changes/mtg-hybrid-reference-map-2026-07-06-02-27-00.md delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.BodyReferences.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.Candidates.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.CustomCodeRoots.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.GraphBuilder.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.Helpers.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.ReferenceTraversal.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.RootSelection.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.TypeReferenceCollector.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.cs rename packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/{PostProcessing => ReferenceMap}/ProviderReferenceMapResult.cs (100%) diff --git a/.chronus/changes/mtg-hybrid-reference-map-2026-07-06-02-27-00.md b/.chronus/changes/mtg-hybrid-reference-map-2026-07-06-02-27-00.md new file mode 100644 index 00000000000..59962113994 --- /dev/null +++ b/.chronus/changes/mtg-hybrid-reference-map-2026-07-06-02-27-00.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/http-client-csharp" +--- + +Preserve generated SDK compatibility when using the optimized C# reference map. diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientOptionsProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientOptionsProvider.cs index 24c20fd2c0e..390dab4824f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientOptionsProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientOptionsProvider.cs @@ -248,6 +248,11 @@ private static bool UseSingletonInstance(InputClient inputClient) protected override string BuildRelativeFilePath() => Path.Combine("src", "Generated", $"{Name}.cs"); + protected override bool ShouldWriteTypeXmlDocs => CustomCodeView is null; + + protected override TypeSignatureModifiers BuildDeclarationModifiers() + => _clientProvider.DeclarationModifiers & (TypeSignatureModifiers.Public | TypeSignatureModifiers.Internal | TypeSignatureModifiers.Protected | TypeSignatureModifiers.Private); + protected override string BuildName() { if (UseSingletonInstance(_inputClient)) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientPipelineExtensionsDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientPipelineExtensionsDefinition.cs index 02864633aca..a1a1c144489 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientPipelineExtensionsDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientPipelineExtensionsDefinition.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using Microsoft.TypeSpec.Generator.ClientModel.Snippets; @@ -52,6 +53,9 @@ protected override MethodProvider[] BuildMethods() ]; } + protected override IReadOnlyList BuildBodyDependencyTypes() + => [ErrorResultSnippets.ErrorResultType]; + private MethodProvider ProcessHeadAsBoolMessage() { MethodSignature signature = GetProcessHeadAsBoolMessageSignature(false); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs index e36b28f5e12..c6645b491f2 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs @@ -455,45 +455,14 @@ protected override IReadOnlyList BuildBodyDependencyTypes() dependencies.Add(method.CollectionDefinition.Type); } - if (method.ServiceMethod == null) - { - continue; - } - - AddInputTypeDependency(dependencies, method.ServiceMethod.Response.Type); - AddInputTypeDependency(dependencies, method.ServiceMethod.Exception?.Type); - foreach (var parameter in method.ServiceMethod.Parameters) - { - AddInputTypeDependency(dependencies, parameter.Type); - } - - foreach (var parameter in method.ServiceMethod.Operation.Parameters) - { - AddInputTypeDependency(dependencies, parameter.Type); - } - - foreach (var response in method.ServiceMethod.Operation.Responses) - { - AddInputTypeDependency(dependencies, response.BodyType); - foreach (var header in response.Headers) - { - AddInputTypeDependency(dependencies, header.Type); - } - } + // Service method metadata can mention wire-only request/response models that are not + // emitted in the generated method signature or body. The graph builder and structured + // body scanner capture the generated types that are actually referenced. } return dependencies; } - private static void AddInputTypeDependency(List dependencies, InputType? inputType) - { - var type = inputType == null ? null : ScmCodeModelGenerator.Instance.TypeFactory.CreateCSharpType(inputType); - if (type != null) - { - dependencies.Add(type); - } - } - protected override FieldProvider[] BuildFields() { List fields = [EndpointField]; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderCustomizationTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderCustomizationTests.cs index 72ee46c9658..e3877151fca 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderCustomizationTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderCustomizationTests.cs @@ -392,8 +392,7 @@ public async Task CanChangeClientAccessibility() // Find the client options provider var clientOptionsProvider = mockGenerator.Object.OutputLibrary.TypeProviders.SingleOrDefault(t => t is ClientOptionsProvider); Assert.IsNotNull(clientOptionsProvider); - // The client options were not customized - Assert.IsTrue(clientOptionsProvider!.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsTrue(clientOptionsProvider!.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); // ClientSettings should not be generated for internal clients Assert.IsNull(((ClientProvider)clientProvider).ClientSettings, diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index 07ca2a5bc18..1d9fd7e4ed2 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs @@ -130,6 +130,11 @@ await GeneratedCodeWorkspace.LoadBaselineContract(), { foreach (var outputType in output.TypeProviders) { + if (ProviderReferenceMapShadowAnalyzer.UseShadowMap && !ProviderReferenceMapAnalyzer.ShouldWriteProvider(outputType)) + { + continue; + } + if (outputType is ModelFactoryProvider && outputType.Methods.Count == 0) { continue; @@ -140,6 +145,11 @@ await GeneratedCodeWorkspace.LoadBaselineContract(), foreach (var serialization in outputType.SerializationProviders) { + if (ProviderReferenceMapShadowAnalyzer.UseShadowMap && !ProviderReferenceMapAnalyzer.ShouldWriteProvider(serialization)) + { + continue; + } + writer = CodeModelGenerator.Instance.GetWriter(serialization); generateFilesTasks.Add(generatedCodeWorkspace.AddGeneratedFile(writer.Write())); } @@ -326,9 +336,10 @@ private static void DeleteDirectory(string path, string[] filesToKeep) return; } + var fileNamesToKeep = filesToKeep.ToHashSet(StringComparer.Ordinal); foreach (var file in directoryInfo.GetFiles("*", SearchOption.AllDirectories)) { - if (!filesToKeep.Contains(file.Name)) + if (!ShouldKeepGeneratedFile(file, fileNamesToKeep)) { file.Delete(); } @@ -352,5 +363,10 @@ private static void DeleteDirectory(string path, string[] filesToKeep) directoryInfo.Delete(); } } + + private static bool ShouldKeepGeneratedFile(FileInfo file, HashSet fileNamesToKeep) + => fileNamesToKeep.Contains(file.Name) || + file.Extension.Equals(".cs", StringComparison.Ordinal) && + file.Name.EndsWith("Extensions.cs", StringComparison.Ordinal); } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs index dff87e95e1f..48731ffeabd 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs @@ -614,6 +614,11 @@ private async Task RemoveInvalidRefs(Project project, HashSet r var updatedSolution = solution; foreach (var documentId in project.DocumentIds) { + if (IsModelFactoryDocument(project.GetDocument(documentId))) + { + continue; + } + updatedSolution = await RemoveInvalidUsings(updatedSolution, documentId); } @@ -647,6 +652,11 @@ private async Task RemoveInvalidRefs(Project project, HashSet r return solution.GetProject(project.Id)!; } + private bool IsModelFactoryDocument(Document? document) + => document != null && + _modelFactoryFullName != null && + document.Name.EndsWith("ModelFactory.cs", StringComparison.Ordinal); + private async Task RemoveInvalidUsings(Solution solution, DocumentId documentId) { var document = solution.GetDocument(documentId)!; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs deleted file mode 100644 index d7f02980e62..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapAnalyzer.cs +++ /dev/null @@ -1,1849 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Text.RegularExpressions; -using Microsoft.TypeSpec.Generator.Expressions; -using Microsoft.TypeSpec.Generator.Primitives; -using Microsoft.TypeSpec.Generator.Providers; -using Microsoft.TypeSpec.Generator.Statements; - -namespace Microsoft.TypeSpec.Generator -{ - internal static class ProviderReferenceMapAnalyzer - { - private static ProviderReferenceMapResult? _latestResult; - private static readonly ConditionalWeakTable, Dictionary> _simpleNameLookupCache = new(); - private static TypeProvider? _preWriteModelFactory; - private static MethodProvider[]? _preWriteModelFactoryMethods; - - public static ProviderReferenceMapResult? LatestResult => _latestResult; - public static bool PreWriteAccessibilityApplied { get; private set; } - - public static void ResetPreWriteAccessibility() - { - RestorePreWriteModelFactoryMethods(); - _latestResult = null; - PreWriteAccessibilityApplied = false; - } - - public static void ApplyPreWriteAccessibility(IReadOnlyList providers) - { - PreWriteAccessibilityApplied = false; - if (Configuration.UnreferencedTypesHandling == Configuration.UnreferencedTypesHandlingOption.KeepAll) - { - return; - } - - var (internalizeCandidates, publicizeCandidates) = GetPreWriteAccessibilityCandidates(providers); - foreach (var provider in GetGeneratedProviders(providers)) - { - var providerName = GetProviderTypeName(provider.Type); - if (internalizeCandidates.Contains(providerName)) - { - provider.PreserveXmlDocs(); - provider.Update(modifiers: MakeInternal(provider.DeclarationModifiers)); - } - else if (publicizeCandidates.Contains(providerName)) - { - provider.Update(modifiers: MakePublic(provider.DeclarationModifiers)); - } - } - - RemoveMethodsFromModelFactory(internalizeCandidates.Select(GetSimpleName).ToHashSet(StringComparer.Ordinal)); - PreWriteAccessibilityApplied = true; - } - - public static void RestorePreWriteModelFactoryMethods() - { - if (_preWriteModelFactory == null || _preWriteModelFactoryMethods == null) - { - return; - } - - _preWriteModelFactory.Update(methods: _preWriteModelFactoryMethods); - _preWriteModelFactory = null; - _preWriteModelFactoryMethods = null; - } - - public static void Analyze(IReadOnlyList providers) - { - var generatedProviders = GetGeneratedProviders(providers); - var graph = BuildGraph(generatedProviders); - var publicGraph = BuildGraph(generatedProviders, publicOnly: true); - - var customPublicRoots = GetCustomCodePublicGeneratedTypeRoots(generatedProviders, graph.Nodes); - var apiBaselineGeneratedTypeRoots = GetApiBaselineGeneratedTypeRoots(graph.Nodes); - customPublicRoots.UnionWith(apiBaselineGeneratedTypeRoots); - var generatedPublicDeclarations = GetGeneratedPublicTypeDeclarations(generatedProviders, graph.Nodes); - customPublicRoots.UnionWith(generatedPublicDeclarations); - var customRemovalRoots = GetCustomCodeGeneratedTypeRoots(generatedProviders, graph.Nodes); - customRemovalRoots.UnionWith(apiBaselineGeneratedTypeRoots); - customRemovalRoots.UnionWith(generatedPublicDeclarations); - var customInternalDeclarations = GetCustomCodeInternalGeneratedTypeDeclarations(generatedProviders, graph.Nodes); - var generatedInternalDeclarations = GetGeneratedInternalTypeDeclarations(generatedProviders, graph.Nodes); - - // Helper types are rooted after an initial reachability pass so unused infrastructure - // such as change-tracking dictionaries can still be removed when no reachable type needs them. - var generatedDiscriminatorBaseNames = GetGeneratedPersistableModelProxyTypeNames(generatedProviders, publicGraph.Nodes); - var internalizeReferences = CloneReferences(publicGraph.References); - var internalizeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: false, includeAdditionalRoots: true, includeUnionVariantRoots: false, publicClientRootsOnly: true); - if (ShouldUseUnionVariantFallbackRoots()) - { - AddUnionVariantRoots(internalizeRoots, providers, graph.Nodes); - } - var generatedPublicReachable = GetReachableTypes(internalizeRoots, internalizeReferences); - AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences, generatedPublicReachable, generatedDiscriminatorBaseNames); - internalizeRoots.UnionWith(customPublicRoots); - var internalizeReachableWithoutHelpers = GetReachableTypes(internalizeRoots, internalizeReferences); - AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences, internalizeReachableWithoutHelpers, generatedDiscriminatorBaseNames); - internalizeReachableWithoutHelpers = GetReachableTypes(internalizeRoots, internalizeReferences); - var publicizeRoots = internalizeRoots.ToHashSet(StringComparer.Ordinal); - var internalizeHelperRoots = GetHelperRootNames(generatedProviders, graph.Nodes, internalizeReachableWithoutHelpers); - internalizeRoots.UnionWith(internalizeHelperRoots); - var internalizeReachable = GetReachableTypes(internalizeRoots, internalizeReferences); - var internalizeDeclaredNodes = GetPostProcessorDeclaredNodes(generatedProviders, graph.Nodes, publicOnly: true); - var customInternalBoundaryNodes = graph.Nodes - .Where(name => publicGraph.References.TryGetValue(name, out var references) && references.Overlaps(customInternalDeclarations)) - .ToHashSet(StringComparer.Ordinal); - var publicizeDeclaredNodes = GetPostProcessorDeclaredNodes(generatedProviders, graph.Nodes, publicOnly: false) - .Except(internalizeDeclaredNodes, StringComparer.Ordinal); - var generatedImplementationInternalDeclarations = GetGeneratedImplementationInternalTypeDeclarations(generatedInternalDeclarations).ToHashSet(StringComparer.Ordinal); - var publicApiTraversalNodes = internalizeDeclaredNodes - .Except(generatedInternalDeclarations, StringComparer.Ordinal) - .Concat(publicizeDeclaredNodes) - .Except(generatedImplementationInternalDeclarations, StringComparer.Ordinal) - .ToHashSet(StringComparer.Ordinal); - var publicizeReachable = GetReachableTypes(publicizeRoots, internalizeReferences, publicApiTraversalNodes); - var internalizeCandidates = internalizeDeclaredNodes - .Except(publicizeReachable, StringComparer.Ordinal) - .Union(internalizeDeclaredNodes - .Intersect(customInternalBoundaryNodes, StringComparer.Ordinal) - .Except(publicizeRoots, StringComparer.Ordinal), StringComparer.Ordinal) - .OrderBy(static name => name, StringComparer.Ordinal) - .ToArray(); - var publicizeCandidates = publicizeDeclaredNodes - .Except(customInternalDeclarations, StringComparer.Ordinal) - .Except(customInternalBoundaryNodes, StringComparer.Ordinal) - .Except(internalizeHelperRoots, StringComparer.Ordinal) - .Except(GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: true, includeAdditionalRoots: true, includeUnionVariantRoots: true, publicClientRootsOnly: true), StringComparer.Ordinal) - .Intersect(publicizeReachable, StringComparer.Ordinal) - // Preserve generated types that the last contract already made internal unless a public API/custom root explicitly requires them. - .Where(name => !generatedInternalDeclarations.Contains(name) || publicizeRoots.Contains(name)) - .Where(name => publicizeRoots.Contains(name) || - HasPublicApiPredecessor(name, internalizeReferences, publicizeReachable, generatedImplementationInternalDeclarations)) - .OrderBy(static name => name, StringComparer.Ordinal) - .ToArray(); - - // Body-only generated dependencies are needed to avoid deleting helper files, but they do - // not contribute to public API reachability for internalization. - AddGeneratedBodyReferences(providers, graph); - - var removeRoots = GetRootNames( - providers, - graph.Nodes, - helperRoots: [], - includeModelFactory: true, - includeAdditionalRoots: true, - includeUnionVariantRoots: ShouldUseUnionVariantFallbackRoots(), - publicClientRootsOnly: false); - removeRoots.UnionWith(customRemovalRoots); - AddMatchingNamesWithSimpleNameSuffix(removeRoots, "ReferenceType", graph.Nodes); - AddCustomCodeExtensionRoots(removeRoots, generatedProviders, graph.Nodes); - AddCustomizationBackedExtensionRoots(removeRoots, graph.Nodes); - AddCustomRequestHeaderExtensionsRoot(removeRoots, generatedProviders, graph.Nodes); - RemoveUnusedRequestHeaderExtensionsRoot(removeRoots, graph.References, providers); - var removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); - AddDerivedModelReferences(providers, graph.Nodes, graph.References, removeReachableWithoutHelpers, generatedDiscriminatorBaseNames); - removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); - AddBasePreservedReferences(generatedProviders, graph.Nodes, graph.References, removeReachableWithoutHelpers); - var removeHelperRoots = GetHelperRootNames(generatedProviders, graph.Nodes, removeReachableWithoutHelpers, graph.References); - removeRoots.UnionWith(removeHelperRoots); - var removeReachable = GetReachableTypes(removeRoots, graph.References); - AddBasePreservedReferences(generatedProviders, graph.Nodes, graph.References, removeReachable); - var removeDeclaredNodes = GetPostProcessorDeclaredNodes(generatedProviders, graph.Nodes, publicOnly: false); - var removeCandidates = removeDeclaredNodes.Except(removeReachable, StringComparer.Ordinal).OrderBy(static name => name, StringComparer.Ordinal).ToArray(); - var helperRoots = internalizeHelperRoots.Concat(removeHelperRoots).ToHashSet(StringComparer.Ordinal); - - _latestResult = new ProviderReferenceMapResult( - internalizeCandidates.ToHashSet(StringComparer.Ordinal), - publicizeCandidates.ToHashSet(StringComparer.Ordinal), - removeCandidates.ToHashSet(StringComparer.Ordinal)); - } - - private static (HashSet InternalizeCandidates, HashSet PublicizeCandidates) GetPreWriteAccessibilityCandidates(IReadOnlyList providers) - { - var generatedProviders = GetGeneratedProviders(providers); - var graph = BuildGraph(generatedProviders); - var publicGraph = BuildGraph(generatedProviders, publicOnly: true); - var customPublicRoots = GetCustomCodePublicGeneratedTypeRoots(generatedProviders, graph.Nodes); - var apiBaselineGeneratedTypeRoots = GetApiBaselineGeneratedTypeRoots(graph.Nodes); - customPublicRoots.UnionWith(apiBaselineGeneratedTypeRoots); - var generatedPublicDeclarations = GetGeneratedPublicTypeDeclarations(generatedProviders, graph.Nodes); - customPublicRoots.UnionWith(generatedPublicDeclarations); - var customInternalDeclarations = GetCustomCodeInternalGeneratedTypeDeclarations(generatedProviders, graph.Nodes); - var generatedInternalDeclarations = GetGeneratedInternalTypeDeclarations(generatedProviders, graph.Nodes); - var generatedDiscriminatorBaseNames = new HashSet(StringComparer.Ordinal); - - var (internalizeCandidates, publicizeCandidates, _) = GetAccessibilityCandidates( - providers, - generatedProviders, - graph, - publicGraph, - customPublicRoots, - customInternalDeclarations, - generatedInternalDeclarations, - generatedDiscriminatorBaseNames); - - return (internalizeCandidates, publicizeCandidates); - } - - private static (HashSet InternalizeCandidates, HashSet PublicizeCandidates, HashSet InternalizeHelperRoots) GetAccessibilityCandidates( - IReadOnlyList providers, - IReadOnlyList generatedProviders, - ProviderReferenceGraph graph, - ProviderReferenceGraph publicGraph, - HashSet customPublicRoots, - HashSet customInternalDeclarations, - HashSet generatedInternalDeclarations, - HashSet generatedDiscriminatorBaseNames) - { - var internalizeReferences = CloneReferences(publicGraph.References); - var internalizeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: false, includeAdditionalRoots: true, includeUnionVariantRoots: false, publicClientRootsOnly: true); - if (ShouldUseUnionVariantFallbackRoots()) - { - AddUnionVariantRoots(internalizeRoots, providers, graph.Nodes); - } - - var generatedPublicReachable = GetReachableTypes(internalizeRoots, internalizeReferences); - AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences, generatedPublicReachable, generatedDiscriminatorBaseNames); - internalizeRoots.UnionWith(customPublicRoots); - var internalizeReachableWithoutHelpers = GetReachableTypes(internalizeRoots, internalizeReferences); - AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences, internalizeReachableWithoutHelpers, generatedDiscriminatorBaseNames); - internalizeReachableWithoutHelpers = GetReachableTypes(internalizeRoots, internalizeReferences); - var publicizeRoots = internalizeRoots.ToHashSet(StringComparer.Ordinal); - var internalizeHelperRoots = GetHelperRootNames(generatedProviders, graph.Nodes, internalizeReachableWithoutHelpers); - internalizeRoots.UnionWith(internalizeHelperRoots); - var internalizeDeclaredNodes = GetPostProcessorDeclaredNodes(generatedProviders, graph.Nodes, publicOnly: true); - var customInternalBoundaryNodes = graph.Nodes - .Where(name => publicGraph.References.TryGetValue(name, out var references) && references.Overlaps(customInternalDeclarations)) - .ToHashSet(StringComparer.Ordinal); - var publicizeDeclaredNodes = GetPostProcessorDeclaredNodes(generatedProviders, graph.Nodes, publicOnly: false) - .Except(internalizeDeclaredNodes, StringComparer.Ordinal); - var generatedImplementationInternalDeclarations = GetGeneratedImplementationInternalTypeDeclarations(generatedInternalDeclarations).ToHashSet(StringComparer.Ordinal); - var publicApiTraversalNodes = internalizeDeclaredNodes - .Except(generatedInternalDeclarations, StringComparer.Ordinal) - .Concat(publicizeDeclaredNodes) - .Except(generatedImplementationInternalDeclarations, StringComparer.Ordinal) - .ToHashSet(StringComparer.Ordinal); - var publicizeReachable = GetReachableTypes(publicizeRoots, internalizeReferences, publicApiTraversalNodes); - var internalizeCandidates = internalizeDeclaredNodes - .Except(publicizeReachable, StringComparer.Ordinal) - .Union(internalizeDeclaredNodes - .Intersect(customInternalBoundaryNodes, StringComparer.Ordinal) - .Except(publicizeRoots, StringComparer.Ordinal), StringComparer.Ordinal) - .ToHashSet(StringComparer.Ordinal); - var publicizeCandidates = publicizeDeclaredNodes - .Except(customInternalDeclarations, StringComparer.Ordinal) - .Except(customInternalBoundaryNodes, StringComparer.Ordinal) - .Except(internalizeHelperRoots, StringComparer.Ordinal) - .Except(GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: true, includeAdditionalRoots: true, includeUnionVariantRoots: true, publicClientRootsOnly: true), StringComparer.Ordinal) - .Intersect(publicizeReachable, StringComparer.Ordinal) - .Where(name => !generatedInternalDeclarations.Contains(name) || publicizeRoots.Contains(name)) - .Where(name => publicizeRoots.Contains(name) || - HasPublicApiPredecessor(name, internalizeReferences, publicizeReachable, generatedImplementationInternalDeclarations)) - .ToHashSet(StringComparer.Ordinal); - - return (internalizeCandidates, publicizeCandidates, internalizeHelperRoots); - } - - private static HashSet GetCustomCodeGeneratedTypeRoots(IReadOnlyList providers, HashSet generatedTypeNames) - { - var roots = new HashSet(StringComparer.Ordinal); - foreach (var customCodeView in GetCustomCodeViews(providers)) - { - AddCustomCodeViewRoots(roots, customCodeView, generatedTypeNames, publicOnly: false); - } - - return roots; - } - - private static HashSet GetCustomCodePublicGeneratedTypeRoots(IReadOnlyList providers, HashSet generatedTypeNames) - { - var roots = new HashSet(StringComparer.Ordinal); - foreach (var customCodeView in GetCustomCodeViews(providers)) - { - if (!customCodeView.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) - { - continue; - } - - AddCustomCodeViewRoots(roots, customCodeView, generatedTypeNames, publicOnly: true); - } - - return roots; - } - - private static IEnumerable GetCustomCodeViews(IReadOnlyList providers) - { - var visited = new HashSet(StringComparer.Ordinal); - var modelFactoryCustomCodeView = CodeModelGenerator.Instance.OutputLibrary.ModelFactory.Value.CustomCodeView; - if (modelFactoryCustomCodeView != null && visited.Add(GetCustomCodeViewIdentity(modelFactoryCustomCodeView))) - { - yield return modelFactoryCustomCodeView; - } - - foreach (var provider in providers) - { - var customCodeView = provider.CustomCodeView; - if (customCodeView == null || !visited.Add(GetCustomCodeViewIdentity(customCodeView))) - { - continue; - } - - yield return customCodeView; - } - - foreach (var customTypeProvider in CodeModelGenerator.Instance.SourceInputModel.GetCustomizationTypeProviders()) - { - if (visited.Add(GetCustomCodeViewIdentity(customTypeProvider))) - { - yield return customTypeProvider; - } - } - } - - private static string GetCustomCodeViewIdentity(TypeProvider customCodeView) => - customCodeView is NamedTypeSymbolProvider namedTypeSymbolProvider - ? namedTypeSymbolProvider.MetadataName - : GetProviderTypeName(customCodeView.Type); - - private static void AddCustomRequestHeaderExtensionsRoot(HashSet roots, IReadOnlyList providers, HashSet nodes) - { - if (!HasCustomRequestHeaderExtensionsReference(providers)) - { - return; - } - - AddMatchingNamesWithSimpleNameSuffix(roots, "RequestHeaderExtensions", nodes); - AddMatchingNamesWithSimpleNameSuffix(roots, "RequestHeadersExtensions", nodes); - } - - private static void AddCustomCodeExtensionRoots(HashSet roots, IReadOnlyList providers, HashSet nodes) - { - foreach (var customCodeView in GetCustomCodeViews(providers)) - { - AddMatchingName(roots, $"{GetCustomCodeViewSimpleName(customCodeView)}Extensions", nodes); - } - } - - private static string GetCustomCodeViewSimpleName(TypeProvider customCodeView) => - customCodeView is NamedTypeSymbolProvider namedTypeSymbolProvider - ? namedTypeSymbolProvider.MetadataSimpleName - : customCodeView.Type.Name; - - private static void AddCustomizationBackedExtensionRoots(HashSet roots, HashSet nodes) - { - foreach (var node in nodes) - { - var simpleName = GetSimpleName(node); - if (!simpleName.EndsWith("Extensions", StringComparison.Ordinal)) - { - continue; - } - - var namespaceName = GetNamespaceName(node); - if (namespaceName == null) - { - continue; - } - - var customTypeName = simpleName.Substring(0, simpleName.Length - "Extensions".Length); - if (CodeModelGenerator.Instance.SourceInputModel.FindForTypeInCustomization(namespaceName, customTypeName) != null) - { - roots.Add(node); - } - } - } - - private static void AddCustomCodeViewRoots(HashSet roots, TypeProvider customCodeView, HashSet generatedTypeNames, bool publicOnly) - { - if (customCodeView is NamedTypeSymbolProvider namedTypeSymbolProvider) - { - AddMatchingName(roots, namedTypeSymbolProvider.MetadataSimpleName, generatedTypeNames); - AddProviderBodyDependencyTypes(roots, customCodeView.SignatureDependencyTypes, generatedTypeNames); - if (!publicOnly) - { - AddProviderBodyDependencyTypes(roots, customCodeView.HelperDependencyTypes, generatedTypeNames); - AddProviderBodyDependencyTypes(roots, customCodeView.BodyDependencyTypes, generatedTypeNames); - } - - return; - } - - AddTypeReference(roots, customCodeView.Type, generatedTypeNames); - AddTypeReference(roots, customCodeView.BaseType, generatedTypeNames); - AddProviderBodyDependencyTypes(roots, customCodeView.SignatureDependencyTypes, generatedTypeNames); - if (!publicOnly) - { - AddAttributes(roots, customCodeView.Attributes, generatedTypeNames, serializationProviderNamesByType: null); - AddMatchingName(roots, $"{GetCustomCodeViewSimpleName(customCodeView)}Extensions", generatedTypeNames); - AddProviderBodyDependencyTypes(roots, customCodeView.HelperDependencyTypes, generatedTypeNames); - AddProviderBodyDependencyTypes(roots, customCodeView.BodyDependencyTypes, generatedTypeNames); - } - - foreach (var implementedType in customCodeView.Implements) - { - AddTypeReference(roots, implementedType, generatedTypeNames); - } - - foreach (var constructor in customCodeView.Constructors) - { - if (publicOnly && !IsPublic(constructor.Signature.Modifiers)) - { - continue; - } - - AddSignatureReferences(roots, constructor.Signature, generatedTypeNames, serializationProviderNamesByType: null, includeAttributes: !publicOnly); - } - - foreach (var method in customCodeView.Methods) - { - if (publicOnly && !IsPublic(method.Signature.Modifiers)) - { - continue; - } - - AddSignatureReferences(roots, method.Signature, generatedTypeNames, serializationProviderNamesByType: null, includeAttributes: !publicOnly); - } - - foreach (var property in customCodeView.Properties) - { - if (publicOnly && !IsPublic(property.Modifiers)) - { - continue; - } - - AddTypeReference(roots, property.Type, generatedTypeNames); - AddTypeReference(roots, property.ExplicitInterface, generatedTypeNames); - if (!publicOnly) - { - AddAttributes(roots, property.Attributes, generatedTypeNames, serializationProviderNamesByType: null); - } - } - - foreach (var field in customCodeView.Fields) - { - if (publicOnly && !IsPublic(field.Modifiers)) - { - continue; - } - - AddTypeReference(roots, field.Type, generatedTypeNames); - if (!publicOnly) - { - AddAttributes(roots, field.Attributes, generatedTypeNames, serializationProviderNamesByType: null); - } - } - } - - private static HashSet GetApiBaselineGeneratedTypeRoots(HashSet generatedTypeNames) - { - var roots = new HashSet(StringComparer.Ordinal); - var projectDirectory = CodeModelGenerator.Instance.Configuration.ProjectDirectory; - if (string.IsNullOrEmpty(projectDirectory)) - { - return roots; - } - - var apiDirectory = Path.GetFullPath(Path.Combine(projectDirectory, "..", "api")); - if (!Directory.Exists(apiDirectory)) - { - return roots; - } - - var apiText = string.Join("\n", Directory.GetFiles(apiDirectory, "*.cs", SearchOption.AllDirectories).Select(File.ReadAllText)); - var apiDeclaredTypeNames = GetApiDeclaredTypeNames(apiText); - foreach (var fullName in generatedTypeNames) - { - var simpleName = StripGenericArity(GetSimpleName(fullName)); - var normalizedFullName = StripGenericArity(fullName); - if (!ContainsApiTypeReference(apiText, apiDeclaredTypeNames, normalizedFullName, simpleName)) - { - continue; - } - - roots.Add(fullName); - } - - return roots; - } - - private static HashSet GetApiDeclaredTypeNames(string apiText) - { - var declaredTypeNames = new HashSet(StringComparer.Ordinal); - string? currentNamespace = null; - foreach (var line in apiText.Split('\n')) - { - var namespaceMatch = Regex.Match(line, @"^namespace\s+([\w.]+)\s*\{?\s*$"); - if (namespaceMatch.Success) - { - currentNamespace = namespaceMatch.Groups[1].Value; - continue; - } - - if (currentNamespace == null) - { - continue; - } - - var declarationMatch = Regex.Match(line, @"^ \S.*?\b(class|struct|interface|enum)\s+([A-Za-z_][A-Za-z0-9_]*)(?!\s*<)(?!\w)"); - if (declarationMatch.Success) - { - declaredTypeNames.Add($"{currentNamespace}.{declarationMatch.Groups[2].Value}"); - } - } - - return declaredTypeNames; - } - - private static bool ContainsApiTypeReference(string apiText, HashSet apiDeclaredTypeNames, string fullName, string simpleName) - { - var fullNamePattern = $@"(? GetCustomCodeInternalGeneratedTypeDeclarations(IReadOnlyList providers, HashSet generatedTypeNames) - { - var declarations = new HashSet(StringComparer.Ordinal); - foreach (var customCodeView in GetCustomCodeViews(providers)) - { - if (customCodeView is NamedTypeSymbolProvider) - { - continue; - } - - if (!customCodeView.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)) - { - continue; - } - - AddTypeReference(declarations, customCodeView.Type, generatedTypeNames); - } - - return declarations; - } - - private static HashSet GetGeneratedPersistableModelProxyTypeNames(IReadOnlyList providers, HashSet generatedTypeNames) - { - var proxyTypes = new HashSet(StringComparer.Ordinal); - foreach (var provider in GetGeneratedProviders(providers)) - { - if (provider.Attributes.Any(static attribute => IsAttributeNamed(attribute, "PersistableModelProxy"))) - { - AddTypeReference(proxyTypes, provider.Type, generatedTypeNames); - } - } - - return proxyTypes; - } - - private static HashSet GetGeneratedInternalTypeDeclarations(IReadOnlyList providers, HashSet generatedTypeNames) - => GetGeneratedTypeDeclarationsByLastContractAccessibility(providers, generatedTypeNames, TypeSignatureModifiers.Internal); - - private static HashSet GetGeneratedPublicTypeDeclarations(IReadOnlyList providers, HashSet generatedTypeNames) - => GetGeneratedTypeDeclarationsByLastContractAccessibility(providers, generatedTypeNames, TypeSignatureModifiers.Public); - - private static HashSet GetGeneratedTypeDeclarationsByLastContractAccessibility( - IReadOnlyList providers, - HashSet generatedTypeNames, - TypeSignatureModifiers accessibility) - { - var declarations = new HashSet(StringComparer.Ordinal); - foreach (var provider in GetGeneratedProviders(providers)) - { - if (provider.LastContractView?.DeclarationModifiers.HasFlag(accessibility) != true) - { - continue; - } - - AddTypeReference(declarations, provider.Type, generatedTypeNames); - } - - return declarations; - } - - private static IEnumerable GetGeneratedImplementationInternalTypeDeclarations(HashSet generatedInternalDeclarations) => - generatedInternalDeclarations.Where(static name => GetSimpleName(name).StartsWith("Internal", StringComparison.Ordinal)); - - private static ProviderReferenceGraph BuildGraph(IReadOnlyList generatedProviders, bool publicOnly = false) - { - // Each generated provider becomes a node, and provider metadata supplies the edges: - // inheritance, signatures, properties, fields, nested/serialization providers, attributes, - // and selected implementation dependencies. This avoids parsing generated C# just to - // rediscover generated-to-generated references. - var serializationProviderNamesByType = generatedProviders - .Where(static provider => provider.SerializationProviders.Count > 0) - .GroupBy(static provider => GetProviderTypeName(provider.Type), StringComparer.Ordinal) - .ToDictionary( - static group => group.Key, - static group => group - .SelectMany(static provider => provider.SerializationProviders) - .Select(static serializationProvider => GetProviderTypeName(serializationProvider.Type)) - .Distinct(StringComparer.Ordinal) - .ToArray(), - StringComparer.Ordinal); - IReadOnlyDictionary? serializationReferenceNamesByType = publicOnly ? null : serializationProviderNamesByType; - var nodes = generatedProviders - .Select(static provider => GetProviderTypeName(provider.Type)) - .ToHashSet(StringComparer.Ordinal); - var references = nodes.ToDictionary(static name => name, _ => new HashSet(StringComparer.Ordinal), StringComparer.Ordinal); - - foreach (var provider in generatedProviders) - { - var current = GetProviderTypeName(provider.Type); - AddTypeReference(references[current], provider.Type, nodes, serializationReferenceNamesByType); - AddTypeReference(references[current], provider.BaseType, nodes, serializationReferenceNamesByType); - AddTypeReference(references[current], provider.DeclaringTypeProvider?.Type, nodes, serializationReferenceNamesByType); - - if (IsKept(provider.Type, CodeModelGenerator.Instance.NonRootTypes, nodes)) - { - continue; - } - - // Model factory signatures mention many models. The existing Roslyn post-processor - // removes factory methods for unreachable models, so model factory should only - // contribute helper dependencies, not model reachability edges. - if (IsModelFactoryProvider(provider)) - { - continue; - } - - foreach (var implementedType in provider.Implements) - { - AddTypeReference(references[current], implementedType, nodes, serializationReferenceNamesByType); - } - - if (!publicOnly) - { - foreach (var nestedType in provider.NestedTypes) - { - AddTypeReference(references[current], nestedType.Type, nodes, serializationReferenceNamesByType); - } - } - - if (!publicOnly) - { - foreach (var serializationProvider in provider.SerializationProviders) - { - AddTypeReference(references[current], serializationProvider.Type, nodes, serializationReferenceNamesByType); - } - } - - foreach (var property in provider.Properties) - { - if (publicOnly && !IsPublic(property.Modifiers)) - { - continue; - } - - AddTypeReference(references[current], property.Type, nodes, serializationReferenceNamesByType); - AddTypeReference(references[current], property.ExplicitInterface, nodes, serializationReferenceNamesByType); - if (!publicOnly) - { - AddAttributes(references[current], property.Attributes, nodes, serializationReferenceNamesByType); - } - } - - foreach (var field in provider.Fields) - { - if (publicOnly && !field.Modifiers.HasFlag(FieldModifiers.Public)) - { - continue; - } - - AddTypeReference(references[current], field.Type, nodes, serializationReferenceNamesByType); - if (!publicOnly) - { - AddAttributes(references[current], field.Attributes, nodes, serializationReferenceNamesByType); - } - } - - foreach (var constructor in provider.Constructors) - { - if (publicOnly && !IsPublic(constructor.Signature.Modifiers)) - { - continue; - } - - AddSignatureReferences(references[current], constructor.Signature, nodes, serializationReferenceNamesByType, includeAttributes: !publicOnly); - } - - foreach (var method in provider.Methods) - { - if (method.IsMethodSuppressed()) - { - continue; - } - - if (publicOnly && !IsPublic(method.Signature.Modifiers)) - { - continue; - } - - AddSignatureReferences(references[current], method.Signature, nodes, serializationReferenceNamesByType, includeAttributes: !publicOnly); - if (!publicOnly) - { - AddTypeReference(references[current], GetCollectionDefinitionType(method), nodes, serializationReferenceNamesByType); - } - } - } - - return new ProviderReferenceGraph(nodes, references); - } - - private static CSharpType? GetCollectionDefinitionType(MethodProvider method) - { - var property = method.GetType().GetProperty("CollectionDefinition"); - return property?.GetValue(method) is TypeProvider collectionDefinition - ? collectionDefinition.Type - : null; - } - - private static bool IsPublic(MethodSignatureModifiers modifiers) => modifiers.HasFlag(MethodSignatureModifiers.Public); - private static bool IsPublic(FieldModifiers modifiers) => modifiers.HasFlag(FieldModifiers.Public); - - private static TypeSignatureModifiers MakeInternal(TypeSignatureModifiers modifiers) - => (modifiers & ~(TypeSignatureModifiers.Public | TypeSignatureModifiers.Private | TypeSignatureModifiers.Protected)) | TypeSignatureModifiers.Internal; - - private static TypeSignatureModifiers MakePublic(TypeSignatureModifiers modifiers) - => (modifiers & ~(TypeSignatureModifiers.Internal | TypeSignatureModifiers.Private | TypeSignatureModifiers.Protected)) | TypeSignatureModifiers.Public; - - private static Dictionary> CloneReferences(IReadOnlyDictionary> references) - { - return references.ToDictionary( - static item => item.Key, - static item => item.Value.ToHashSet(StringComparer.Ordinal), - StringComparer.Ordinal); - } - - private static void AddDerivedModelReferences( - IReadOnlyList providers, - HashSet nodes, - Dictionary> references, - HashSet publicBaseModels, - HashSet generatedDiscriminatorBaseNames) - { - var modelProviders = providers.OfType().ToArray(); - var publicModelProviders = modelProviders - .Where(static provider => provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) - .ToArray(); - var discriminatorProviders = publicModelProviders - .Where(static provider => provider.DiscriminatorProperty != null || provider.DiscriminatorValue != null) - .Where(static provider => !provider.IsUnknownDiscriminatorModel) - .ToArray(); - var discriminatorBaseNames = publicModelProviders - .Where(static provider => provider.DiscriminatorProperty != null) - .Select(static provider => GetProviderTypeName(provider.Type)) - .ToHashSet(StringComparer.Ordinal); - discriminatorBaseNames.UnionWith(generatedDiscriminatorBaseNames); - var addedReference = true; - while (addedReference) - { - addedReference = false; - foreach (var provider in discriminatorProviders) - { - var providerName = GetProviderTypeName(provider.Type); - if (!nodes.Contains(providerName)) - { - continue; - } - - if (!publicBaseModels.Contains(providerName)) - { - continue; - } - - foreach (var derivedModel in provider.DerivedModels) - { - if (derivedModel.IsUnknownDiscriminatorModel || - !derivedModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) - { - continue; - } - - var before = references[providerName].Count; - AddTypeReference(references[providerName], derivedModel.Type, nodes); - var derivedName = GetProviderTypeName(derivedModel.Type); - if (nodes.Contains(derivedName) && publicBaseModels.Add(derivedName) || references[providerName].Count != before) - { - addedReference = true; - } - } - } - - foreach (var provider in modelProviders) - { - if (provider.IsUnknownDiscriminatorModel || - !provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) - { - continue; - } - - var providerName = GetProviderTypeName(provider.Type); - if (!nodes.Contains(providerName)) - { - continue; - } - - var baseTypeName = provider.BaseType == null ? null : GetProviderTypeName(provider.BaseType); - if (baseTypeName == null || - !discriminatorBaseNames.Contains(baseTypeName) || - !nodes.Contains(baseTypeName) || - !publicBaseModels.Contains(baseTypeName)) - { - continue; - } - - var before = references[baseTypeName].Count; - references[baseTypeName].Add(providerName); - if (publicBaseModels.Add(providerName) || references[baseTypeName].Count != before) - { - addedReference = true; - } - } - } - } - - private static void AddBasePreservedReferences( - IReadOnlyList providers, - HashSet nodes, - IReadOnlyDictionary> references, - HashSet reachableTypes) - { - var basePreservedRoots = new HashSet(StringComparer.Ordinal); - var addedRoot = true; - while (addedRoot) - { - addedRoot = false; - foreach (var provider in GetGeneratedProviders(providers)) - { - var providerName = GetProviderTypeName(provider.Type); - if (!nodes.Contains(providerName) || reachableTypes.Contains(providerName) || basePreservedRoots.Contains(providerName)) - { - continue; - } - - var baseTypeName = provider.BaseType == null ? null : GetProviderTypeName(provider.BaseType); - if (baseTypeName == null || !reachableTypes.Contains(baseTypeName)) - { - continue; - } - - if (basePreservedRoots.Add(providerName)) - { - addedRoot = true; - } - } - - if (addedRoot) - { - reachableTypes.UnionWith(GetReachableTypes(basePreservedRoots, references)); - } - } - } - - private static IReadOnlyList GetGeneratedProviders(IReadOnlyList providers) - { - var generatedProviders = new List(); - foreach (var provider in providers) - { - AddGeneratedProvider(generatedProviders, provider); - } - - return generatedProviders; - } - - private static void AddGeneratedProvider(List generatedProviders, TypeProvider provider) - { - generatedProviders.Add(provider); - foreach (var nestedType in provider.NestedTypes) - { - AddGeneratedProvider(generatedProviders, nestedType); - } - - foreach (var serializationProvider in provider.SerializationProviders) - { - AddGeneratedProvider(generatedProviders, serializationProvider); - } - } - - private static void AddGeneratedBodyReferences(IReadOnlyList providers, ProviderReferenceGraph graph) - { - foreach (var (provider, isSerializationProvider) in GetBodyReferenceProviders(providers)) - { - if (IsModelFactoryProvider(provider) || - !IsGeneratedBodyReferenceCandidate(provider, isSerializationProvider)) - { - continue; - } - - var providerName = GetProviderTypeName(provider.Type); - if (!graph.Nodes.Contains(providerName)) - { - continue; - } - AddHelperDependencies(graph.References[providerName], provider.HelperDependencyTypes, graph.Nodes, referencedNames: null); - AddProviderBodyDependencyTypes(graph.References[providerName], CollectStructuredBodyReferenceTypes(provider), graph.Nodes); - AddProviderBodyDependencyTypes(graph.References[providerName], provider.BodyDependencyTypes, graph.Nodes); - AddProviderInfrastructureReferences(graph.References[providerName], provider, graph.Nodes); - } - } - - private static void AddProviderInfrastructureReferences(HashSet references, TypeProvider provider, HashSet nodes) - { - AddMatchingName(references, "ProviderConstants", nodes); - AddMatchingName(references, "TypeFormatters", nodes); - - if (provider.SerializationProviders.Count > 0) - { - AddSerializationExtensionReferences(references, provider, nodes); - } - - if (IsSerializationProvider(provider)) - { - AddMatchingName(references, "Optional", nodes); - AddMatchingName(references, "Utf8JsonRequestContent", nodes); - AddMatchingName(references, "ModelSerializationExtensions", nodes); - AddSerializationExtensionReferences(references, provider, nodes); - } - - foreach (var method in provider.Methods) - { - AddMethodInfrastructureReferences(references, method, nodes); - } - } - - private static void AddSerializationExtensionReferences(HashSet references, TypeProvider provider, HashSet nodes) - { - AddSerializationExtensionReferences(references, provider.Type, nodes); - AddSerializationExtensionReferences(references, provider.BaseType, nodes); - foreach (var implementedType in provider.Implements) - { - AddSerializationExtensionReferences(references, implementedType, nodes); - } - - foreach (var property in provider.Properties) - { - AddSerializationExtensionReferences(references, property.Type, nodes); - } - - foreach (var field in provider.Fields) - { - AddSerializationExtensionReferences(references, field.Type, nodes); - } - - foreach (var constructor in provider.Constructors) - { - AddSerializationExtensionReferences(references, constructor.Signature.ReturnType, nodes); - foreach (var parameter in constructor.Signature.Parameters) - { - AddSerializationExtensionReferences(references, parameter.Type, nodes); - } - } - - foreach (var method in provider.Methods) - { - AddSerializationExtensionReferences(references, method.Signature.ReturnType, nodes); - foreach (var parameter in method.Signature.Parameters) - { - AddSerializationExtensionReferences(references, parameter.Type, nodes); - } - } - } - - private static void AddSerializationExtensionReferences(HashSet references, CSharpType? type, HashSet nodes) - { - if (type == null) - { - return; - } - - AddMatchingName(references, $"{type.Name}Extensions", nodes); - foreach (var argument in type.Arguments) - { - AddSerializationExtensionReferences(references, argument, nodes); - } - } - - private static void AddMethodInfrastructureReferences(HashSet references, MethodProvider method, HashSet nodes) - { - AddReturnTypeInfrastructureReferences(references, method.Signature.ReturnType, nodes); - foreach (var parameter in method.Signature.Parameters) - { - AddRequestContentInfrastructureReferences(references, parameter.Type, nodes); - } - } - - private static void AddReturnTypeInfrastructureReferences(HashSet references, CSharpType? returnType, HashSet nodes) - { - var type = UnwrapTask(returnType); - if (type == null) - { - return; - } - - var typeName = StripGenericArity(type.Name); - if (string.Equals(typeName, "Pageable", StringComparison.Ordinal)) - { - AddMatchingName(references, "PageableWrapper", nodes); - } - else if (string.Equals(typeName, "AsyncPageable", StringComparison.Ordinal)) - { - AddMatchingName(references, "AsyncPageableWrapper", nodes); - } - else if (string.Equals(typeName, "ArmOperation", StringComparison.Ordinal)) - { - AddMatchingNamesWithSimpleNameSuffix(references, "ArmOperation", nodes); - AddMatchingNamesWithSimpleNameSuffix(references, "OperationSource", nodes); - if (type.Arguments.Count > 0) - { - AddMatchingName(references, $"{BuildOperationSourceTypeName(type.Arguments[0])}OperationSource", nodes); - } - } - } - - private static void AddRequestContentInfrastructureReferences(HashSet references, CSharpType? type, HashSet nodes) - { - if (type == null) - { - return; - } - - if (string.Equals(type.Name, "RequestContent", StringComparison.Ordinal)) - { - AddMatchingName(references, "BinaryContentHelper", nodes); - AddMatchingName(references, "Utf8JsonRequestContent", nodes); - } - - foreach (var argument in type.Arguments) - { - AddRequestContentInfrastructureReferences(references, argument, nodes); - } - } - - private static CSharpType? UnwrapTask(CSharpType? type) - { - var typeName = type == null ? null : StripGenericArity(type.Name); - if ((string.Equals(typeName, "Task", StringComparison.Ordinal) || - string.Equals(typeName, "ValueTask", StringComparison.Ordinal)) && - type?.Arguments.Count > 0) - { - return type.Arguments[0]; - } - - return type; - } - - private static string BuildOperationSourceTypeName(CSharpType type) - { - var argumentNames = string.Join("", type.Arguments.Select(BuildOperationSourceTypeName)); - return $"{type.Name}{(argumentNames.Length > 0 ? "Of" : string.Empty)}{argumentNames}"; - } - - private static IReadOnlyList CollectStructuredBodyReferenceTypes(TypeProvider provider) - { - var references = new HashSet(); - var visited = new HashSet(ReferenceEqualityComparer.Instance); - - foreach (var field in provider.Fields) - { - CollectStructuredBodyReferenceTypes(field.InitializationValue, references, visited); - } - - foreach (var property in provider.Properties) - { - CollectStructuredBodyReferenceTypes(property.Body, references, visited); - } - - foreach (var constructor in provider.Constructors) - { - CollectStructuredBodyReferenceTypes(constructor.BodyExpression, references, visited); - CollectStructuredBodyReferenceTypes(constructor.BodyStatements, references, visited); - } - - foreach (var method in provider.Methods) - { - CollectStructuredBodyReferenceTypes(method.BodyExpression, references, visited); - CollectStructuredBodyReferenceTypes(method.BodyStatements, references, visited); - } - - return [.. references]; - } - - private static void CollectStructuredBodyReferenceTypes(object? value, HashSet references, HashSet visited) - { - switch (value) - { - case null: - case string: - case FormattableString: - return; - } - - if (!value.GetType().IsValueType && !visited.Add(value)) - { - return; - } - - switch (value) - { - case CSharpType type: - references.Add(type); - return; - case Type type: - references.Add(type); - return; - case ParameterProvider parameter: - references.Add(parameter.Type); - CollectStructuredBodyReferenceTypes(parameter.DefaultValue, references, visited); - CollectStructuredBodyReferenceTypes(parameter.InitializationValue, references, visited); - CollectStructuredBodyReferenceTypes(parameter.Attributes, references, visited); - return; - case MethodSignatureBase signature: - CollectStructuredBodyReferenceTypes(signature.ReturnType, references, visited); - CollectStructuredBodyReferenceTypes(signature.Parameters, references, visited); - CollectStructuredBodyReferenceTypes(signature.Attributes, references, visited); - return; - case KeyValuePair positionalArgument: - CollectStructuredBodyReferenceTypes(positionalArgument.Value, references, visited); - return; - case FieldProvider field: - references.Add(field.Type); - CollectStructuredBodyReferenceTypes(field.InitializationValue, references, visited); - CollectStructuredBodyReferenceTypes(field.Attributes, references, visited); - return; - } - - if (IsStructuredBodyReferenceObject(value)) - { - foreach (var property in value.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) - { - if (property.GetIndexParameters().Length > 0) - { - continue; - } - - CollectStructuredBodyReferenceTypes(property.GetValue(value), references, visited); - } - - return; - } - - if (value is not IEnumerable values) - { - return; - } - - foreach (var item in values) - { - CollectStructuredBodyReferenceTypes(item, references, visited); - } - } - - private static bool IsStructuredBodyReferenceObject(object value) => - value is ValueExpression || - value is MethodBodyStatement || - value is PropertyBody || - value is AttributeStatement; - - private static void AddProviderBodyDependencyTypes(HashSet references, IReadOnlyList dependencies, HashSet nodes) - { - foreach (var dependency in dependencies) - { - AddProviderBodyDependencyType(references, dependency, nodes); - } - } - - private static void AddProviderBodyDependencyType(HashSet references, CSharpType? dependency, HashSet nodes) - { - if (dependency == null) - { - return; - } - - AddTypeReference(references, dependency, nodes); - AddMatchingName(references, dependency.Name, nodes); - AddMatchingName(references, $"{dependency.Name}Extensions", nodes); - - foreach (var argument in dependency.Arguments) - { - AddProviderBodyDependencyType(references, argument, nodes); - } - } - - private static IReadOnlyList<(TypeProvider Provider, bool IsSerializationProvider)> GetBodyReferenceProviders(IReadOnlyList providers) - { - var bodyReferenceProviders = new List<(TypeProvider Provider, bool IsSerializationProvider)>(); - foreach (var provider in providers) - { - bodyReferenceProviders.Add((provider, false)); - bodyReferenceProviders.AddRange(provider.SerializationProviders.Select(static serializationProvider => (serializationProvider, true))); - } - - return bodyReferenceProviders; - } - - private static bool IsGeneratedBodyReferenceCandidate(TypeProvider provider, bool isSerializationProvider) - { - if (provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Static)) - { - return true; - } - - return provider.IsClientProvider || - isSerializationProvider || - provider.IncludeGeneratedBodyReferences || - provider.HelperDependencyTypes.Count > 0 || - provider.BodyDependencyTypes.Count > 0; - } - - private static HashSet GetRootNames( - IReadOnlyList providers, - HashSet nodes, - HashSet helperRoots, - bool includeModelFactory, - bool includeAdditionalRoots, - bool includeUnionVariantRoots, - bool publicClientRootsOnly) - { - var generator = CodeModelGenerator.Instance; - var roots = new HashSet(StringComparer.Ordinal); - var modelFactoryName = GetProviderTypeName(generator.OutputLibrary.ModelFactory.Value.Type); - - foreach (var provider in providers) - { - var name = GetProviderTypeName(provider.Type); - if (IsClientProviderRoot(provider, publicClientRootsOnly) || - includeAdditionalRoots && provider.DeclaringTypeProvider == null && IsKept(provider.Type, generator.AdditionalRootTypes, nodes) || - includeModelFactory && string.Equals(name, modelFactoryName, StringComparison.Ordinal) || - includeModelFactory && helperRoots.Contains(name)) - { - roots.Add(name); - } - } - - AddLastContractModelFactorySignatureRoots(providers, roots, nodes); - - if (!includeUnionVariantRoots) - { - return roots; - } - - AddUnionVariantRoots(roots, providers, nodes); - - foreach (var root in generator.AdditionalRootTypes) - { - AddMatchingName(roots, root, nodes); - } - - return roots; - } - - private static void AddLastContractModelFactorySignatureRoots(IReadOnlyList providers, HashSet roots, HashSet nodes) - { - foreach (var provider in providers.Where(IsModelFactoryProvider)) - { - foreach (var method in provider.LastContractView?.Methods ?? []) - { - if (!method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public) || - IsImplementationOnlyModelFactoryMethod(method)) - { - continue; - } - - AddTypeReference(roots, method.Signature.ReturnType, nodes); - foreach (var parameter in method.Signature.Parameters) - { - AddTypeReference(roots, parameter.Type, nodes); - } - } - } - } - - private static void AddUnionVariantRoots(HashSet roots, IReadOnlyList providers, HashSet nodes) - { - var unionVariantTypesToKeep = CodeModelGenerator.Instance.TypeFactory.UnionVariantTypesToKeep; - foreach (var provider in GetGeneratedProviders(providers)) - { - if (!unionVariantTypesToKeep.Contains(provider.Type.Name) || - string.Equals(provider.Type.Namespace, "TypeSpec.Http", StringComparison.Ordinal)) - { - continue; - } - - AddMatchingName(roots, GetProviderTypeName(provider.Type), nodes); - } - } - - private static bool ShouldUseUnionVariantFallbackRoots() => - !HasApiBaselineDirectory() && - CodeModelGenerator.Instance.SourceInputModel.LastContract == null; - - private static bool IsImplementationOnlyModelFactoryMethod(MethodProvider method) - { - var returnType = method.Signature.ReturnType; - if (returnType == null) - { - return true; - } - - var returnTypeName = GetSimpleName(GetProviderTypeName(returnType)); - return returnTypeName.StartsWith("Paged", StringComparison.Ordinal) || - returnTypeName.EndsWith("Request", StringComparison.Ordinal); - } - - private static void RemoveMethodsFromModelFactory(HashSet namesToRemove) - { - if (namesToRemove.Count == 0) - { - return; - } - - var modelFactory = CodeModelGenerator.Instance.OutputLibrary.ModelFactory.Value; - _preWriteModelFactory = modelFactory; - _preWriteModelFactoryMethods ??= [.. modelFactory.Methods]; - var methodsToKeep = modelFactory.Methods - .Where(method => !namesToRemove.Contains(method.Signature.Name)) - .ToArray(); - modelFactory.Update(methods: methodsToKeep); - } - - private static HashSet GetPostProcessorDeclaredNodes(IReadOnlyList providers, HashSet nodes, bool publicOnly) - { - var generator = CodeModelGenerator.Instance; - var excludedNames = generator.NonRootTypes; - return GetGeneratedProviders(providers) - .Where(provider => !IsModelFactoryProvider(provider)) - .Where(provider => !publicOnly || provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) - .Select(provider => GetProviderTypeName(provider.Type)) - .Where(name => nodes.Contains(name)) - .Where(name => !excludedNames.Contains(name) && !excludedNames.Contains(GetSimpleName(name))) - .ToHashSet(StringComparer.Ordinal); - } - - private static bool IsKept(CSharpType type, HashSet roots, HashSet nodes) - { - var providerName = GetProviderTypeName(type); - if (roots.Contains(providerName) && nodes.Contains(providerName)) - { - return true; - } - - if (!roots.Contains(type.Name)) - { - return false; - } - - var simpleNameLookup = _simpleNameLookupCache.GetValue(nodes, BuildSimpleNameLookup); - return simpleNameLookup.TryGetValue(type.Name, out var matches) && - matches.Length == 1 && - string.Equals(matches[0], providerName, StringComparison.Ordinal); - } - - private static bool IsClientProviderRoot(TypeProvider provider, bool publicOnly) => - provider.IsClientProvider && - (!publicOnly || !HasApiBaselineDirectory() && provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); - - private static bool HasApiBaselineDirectory() - { - var projectDirectory = CodeModelGenerator.Instance.Configuration.ProjectDirectory; - return !string.IsNullOrEmpty(projectDirectory) && - Directory.Exists(Path.GetFullPath(Path.Combine(projectDirectory, "..", "api"))); - } - - private static bool IsModelFactoryProvider(TypeProvider provider) - => provider is ModelFactoryProvider; - - private static HashSet GetHelperRootNames( - IReadOnlyList providers, - HashSet nodes, - HashSet reachableTypes, - IReadOnlyDictionary>? references = null) - { - var roots = new HashSet(StringComparer.Ordinal); - foreach (var provider in GetGeneratedProviders(providers)) - { - var providerName = GetProviderTypeName(provider.Type); - var isModelFactory = IsModelFactoryProvider(provider); - if (!reachableTypes.Contains(providerName) && !isModelFactory) - { - continue; - } - - AddHelperDependencies(roots, provider.HelperDependencyTypes, nodes, references == null ? null : references[providerName]); - - foreach (var property in provider.Properties) - { - AddInitializationHelperRoot(roots, property.Type, nodes); - AddParameterValidationHelperRoot(roots, property.AsParameter, nodes); - } - - foreach (var field in provider.Fields) - { - AddParameterValidationHelperRoot(roots, field.AsParameter, nodes); - } - - foreach (var constructor in provider.Constructors) - { - foreach (var parameter in constructor.Signature.Parameters) - { - AddParameterValidationHelperRoot(roots, parameter, nodes); - } - } - - foreach (var method in provider.Methods) - { - // Only factory methods for reachable models can instantiate collection helpers. - if (isModelFactory && - (method.Signature.ReturnType == null || !reachableTypes.Contains(GetProviderTypeName(method.Signature.ReturnType)))) - { - continue; - } - - foreach (var parameter in method.Signature.Parameters) - { - AddParameterValidationHelperRoot(roots, parameter, nodes); - if (isModelFactory) - { - AddModelFactoryCollectionInitializationHelperRoot(roots, parameter.Type, nodes); - } - } - } - } - - return roots; - } - - private static void AddParameterValidationHelperRoot(HashSet roots, ParameterProvider parameter, HashSet nodes) - { - if (parameter.Validation != ParameterValidationType.None) - { - AddMatchingName(roots, "Argument", nodes); - } - } - - private static void AddHelperDependencies( - HashSet roots, - IReadOnlyList dependencies, - HashSet nodes, - HashSet? referencedNames) - { - foreach (var dependency in dependencies) - { - if (referencedNames == null) - { - AddTypeReference(roots, dependency, nodes); - continue; - } - - var matches = new HashSet(StringComparer.Ordinal); - AddTypeReference(matches, dependency, nodes); - roots.UnionWith(matches.Intersect(referencedNames, StringComparer.Ordinal)); - } - } - - private static void RemoveUnusedRequestHeaderExtensionsRoot( - HashSet roots, - IReadOnlyDictionary> references, - IReadOnlyList providers) - { - var hasCustomReference = HasCustomRequestHeaderExtensionsReference(providers); - var unusedRequestHeaderExtensions = roots - .Where(IsRequestHeadersExtensionsRoot) - .Where(_ => !hasCustomReference) - .Where(root => !references.Any(reference => - !string.Equals(reference.Key, root, StringComparison.Ordinal) && - reference.Value.Contains(root))) - .ToArray(); - - roots.ExceptWith(unusedRequestHeaderExtensions); - } - - private static bool IsRequestHeadersExtensionsRoot(string root) => - root.EndsWith(".RequestHeaderExtensions", StringComparison.Ordinal) || - root.EndsWith(".RequestHeadersExtensions", StringComparison.Ordinal); - - private static bool HasCustomRequestHeaderExtensionsReference(IReadOnlyList providers) - { - foreach (var customCodeView in GetCustomCodeViews(providers)) - { - if (customCodeView is NamedTypeSymbolProvider) - { - if (customCodeView.HelperDependencyTypes.Any(IsRequestHeaderExtensionsDependency) || - customCodeView.BodyDependencyTypes.Any(IsRequestHeaderExtensionsDependency) || - customCodeView.SignatureDependencyTypes.Any(IsRequestHeaderExtensionsDependency)) - { - return true; - } - - continue; - } - - if (customCodeView.HelperDependencyTypes.Any(IsRequestHeaderExtensionsDependency) || - customCodeView.BodyDependencyTypes.Any(IsRequestHeaderExtensionsDependency) || - customCodeView.Methods.Any(static method => - IsRequestHeaderExtensionsDependency(method.Signature.ReturnType) || - method.Signature.Parameters.Any(static parameter => IsRequestHeaderExtensionsDependency(parameter.Type))) || - customCodeView.Properties.Any(static property => IsRequestHeaderExtensionsDependency(property.Type)) || - customCodeView.Fields.Any(static field => IsRequestHeaderExtensionsDependency(field.Type))) - { - return true; - } - } - - return false; - } - - private static bool IsRequestHeaderExtensionsDependency(string name) - => string.Equals(name, "RequestHeaderExtensions", StringComparison.Ordinal) || - string.Equals(name, "SetDelimited", StringComparison.Ordinal); - - private static bool IsRequestHeaderExtensionsDependency(CSharpType? type) - { - if (type == null) - { - return false; - } - - return IsRequestHeaderExtensionsDependency(type.Name) || - type.Arguments.Any(IsRequestHeaderExtensionsDependency); - } - - private static bool IsSerializationProvider(TypeProvider provider) - { - var relativePath = provider.RelativeFilePath.Replace('\\', '/'); - return relativePath.EndsWith(".Serialization.cs", StringComparison.Ordinal) || - relativePath.EndsWith(".Serialization.Multipart.cs", StringComparison.Ordinal); - } - - private static CSharpType ChangeTrackingDictionaryType => new ChangeTrackingDictionaryDefinition().Type; - - private static CSharpType ChangeTrackingListType => new ChangeTrackingListDefinition().Type; - - private static void AddInitializationHelperRoot(HashSet roots, CSharpType? type, HashSet nodes) - { - if (type == null) - { - return; - } - - var initializationType = type.PropertyInitializationType; - if (!string.Equals(initializationType.FullyQualifiedName, type.FullyQualifiedName, StringComparison.Ordinal)) - { - AddMatchingName(roots, initializationType.Name, nodes); - } - - if (type is { IsList: true, IsReadOnlyMemory: false }) - { - AddTypeReference(roots, ChangeTrackingListType, nodes); - } - - if (type.IsDictionary) - { - AddTypeReference(roots, ChangeTrackingDictionaryType, nodes); - } - - foreach (var argument in type.Arguments) - { - AddInitializationHelperRoot(roots, argument, nodes); - } - } - - private static void AddModelFactoryCollectionInitializationHelperRoot(HashSet roots, CSharpType? type, HashSet nodes) - { - if (type == null) - { - return; - } - - if (type is { IsList: true, IsReadOnlyMemory: false }) - { - AddTypeReference(roots, ChangeTrackingListType, nodes); - } - - if (type.IsDictionary) - { - AddTypeReference(roots, ChangeTrackingDictionaryType, nodes); - } - - foreach (var argument in type.Arguments) - { - AddModelFactoryCollectionInitializationHelperRoot(roots, argument, nodes); - } - } - - private static void AddMatchingName(HashSet target, string name, HashSet nodes) - { - if (nodes.Contains(name)) - { - target.Add(name); - return; - } - - var simpleNameLookup = _simpleNameLookupCache.GetValue(nodes, BuildSimpleNameLookup); - if (!simpleNameLookup.TryGetValue(name, out var matches)) - { - return; - } - - foreach (var match in matches) - { - target.Add(match); - } - } - - private static void AddMatchingNamesWithSimpleNameSuffix(HashSet target, string suffix, HashSet nodes) - { - foreach (var node in nodes) - { - if (GetSimpleName(node).EndsWith(suffix, StringComparison.Ordinal)) - { - target.Add(node); - } - } - } - - private static Dictionary BuildSimpleNameLookup(HashSet nodes) - { - return nodes - .GroupBy(static node => StripGenericArity(GetSimpleName(node)), StringComparer.Ordinal) - .ToDictionary(static group => group.Key, static group => group.ToArray(), StringComparer.Ordinal); - } - - private static HashSet GetReachableTypes(HashSet roots, IReadOnlyDictionary> references) - { - return GetReachableTypes(roots, references, expandableNodes: null); - } - - private static HashSet GetReachableTypes( - HashSet roots, - IReadOnlyDictionary> references, - HashSet? expandableNodes) - { - var reachable = new HashSet(StringComparer.Ordinal); - var queue = new Queue(roots); - while (queue.Count > 0) - { - var current = queue.Dequeue(); - if (!reachable.Add(current)) - { - continue; - } - - if (expandableNodes != null && !expandableNodes.Contains(current)) - { - continue; - } - - if (!references.TryGetValue(current, out var children)) - { - continue; - } - - foreach (var child in children) - { - queue.Enqueue(child); - } - } - - return reachable; - } - - private static bool HasPublicApiPredecessor( - string name, - IReadOnlyDictionary> references, - HashSet publicizeReachable, - HashSet generatedImplementationInternalDeclarations) - { - foreach (var (owner, children) in references) - { - if (!publicizeReachable.Contains(owner) || - string.Equals(owner, name, StringComparison.Ordinal) || - generatedImplementationInternalDeclarations.Contains(owner) || - !children.Contains(name)) - { - continue; - } - - return true; - } - - return false; - } - - private static void AddSignatureReferences( - HashSet references, - MethodSignatureBase signature, - HashSet nodes, - IReadOnlyDictionary? serializationProviderNamesByType, - bool includeAttributes = true) - { - AddTypeReference(references, signature.ReturnType, nodes, serializationProviderNamesByType); - if (includeAttributes) - { - AddAttributes(references, signature.Attributes, nodes, serializationProviderNamesByType); - } - - foreach (var parameter in signature.Parameters) - { - AddTypeReference(references, parameter.Type, nodes, serializationProviderNamesByType); - if (includeAttributes) - { - AddAttributes(references, parameter.Attributes, nodes, serializationProviderNamesByType); - } - } - - if (signature is MethodSignature methodSignature) - { - AddTypeReference(references, methodSignature.ExplicitInterface, nodes, serializationProviderNamesByType); - if (methodSignature.GenericArguments != null) - { - foreach (var genericArgument in methodSignature.GenericArguments) - { - AddTypeReference(references, genericArgument, nodes, serializationProviderNamesByType); - } - } - - if (methodSignature.GenericParameterConstraints != null) - { - foreach (var constraint in methodSignature.GenericParameterConstraints) - { - AddTypeReference(references, constraint.Type, nodes, serializationProviderNamesByType); - } - } - } - - if (signature is ConstructorSignature constructorSignature) - { - AddTypeReference(references, constructorSignature.Type, nodes, serializationProviderNamesByType); - } - } - - private static void AddAttributes( - HashSet references, - IReadOnlyList attributes, - HashSet nodes, - IReadOnlyDictionary? serializationProviderNamesByType) - { - foreach (var attribute in attributes) - { - AddTypeReference(references, attribute.Type, nodes, serializationProviderNamesByType); - foreach (var argument in attribute.Arguments) - { - AddAttributeArgumentReference(references, argument, nodes, serializationProviderNamesByType); - } - - foreach (var (_, argument) in attribute.PositionalArguments) - { - AddAttributeArgumentReference(references, argument, nodes, serializationProviderNamesByType); - } - } - } - - private static bool IsAttributeNamed(AttributeStatement attribute, string name) - => string.Equals(attribute.Type.Name, name, StringComparison.Ordinal) || - string.Equals(attribute.Type.Name, $"{name}Attribute", StringComparison.Ordinal); - - private static void AddAttributeArgumentReference( - HashSet references, - ValueExpression argument, - HashSet nodes, - IReadOnlyDictionary? serializationProviderNamesByType) - { - if (argument is TypeOfExpression typeOf) - { - AddTypeReference(references, typeOf.Type, nodes, serializationProviderNamesByType); - } - } - - private static void AddTypeReference( - HashSet references, - CSharpType? type, - HashSet nodes, - IReadOnlyDictionary? serializationProviderNamesByType = null) - { - if (type == null) - { - return; - } - - if (type.IsArray) - { - AddTypeReference(references, type.ElementType, nodes, serializationProviderNamesByType); - return; - } - - var providerTypeName = GetProviderTypeName(type); - if (nodes.Contains(providerTypeName)) - { - references.Add(providerTypeName); - if (serializationProviderNamesByType != null && serializationProviderNamesByType.TryGetValue(providerTypeName, out var serializationProviderNames)) - { - foreach (var serializationProviderName in serializationProviderNames) - { - references.Add(serializationProviderName); - } - } - } - - AddTypeReference(references, type.BaseType, nodes, serializationProviderNamesByType); - AddTypeReference(references, type.DeclaringType, nodes, serializationProviderNamesByType); - foreach (var argument in type.Arguments) - { - AddTypeReference(references, argument, nodes, serializationProviderNamesByType); - } - } - - private static string GetSimpleName(string fullyQualifiedName) - { - var lastDot = fullyQualifiedName.LastIndexOf('.'); - return lastDot < 0 ? fullyQualifiedName : fullyQualifiedName.Substring(lastDot + 1); - } - - private static string? GetNamespaceName(string fullyQualifiedName) - { - var lastDot = fullyQualifiedName.LastIndexOf('.'); - return lastDot < 0 ? null : fullyQualifiedName.Substring(0, lastDot); - } - - private static string GetProviderTypeName(CSharpType type) - { - var name = type.Arguments.Count > 0 && !type.Name.Contains('`', StringComparison.Ordinal) - ? $"{type.Name}`{type.Arguments.Count}" - : type.Name; - return string.IsNullOrEmpty(type.Namespace) ? name : $"{type.Namespace}.{name}"; - } - - private static string StripGenericArity(string name) - { - var tick = name.IndexOf('`'); - return tick < 0 ? name : name.Substring(0, tick); - } - - private sealed record ProviderReferenceGraph( - HashSet Nodes, - Dictionary> References); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/TypeProviderWriter.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/TypeProviderWriter.cs index eb07aa4519f..cf2921b951e 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/TypeProviderWriter.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/TypeProviderWriter.cs @@ -1,6 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; +using System.Collections.Generic; +using System.IO; using System.Linq; using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.Statements; @@ -30,6 +33,8 @@ public virtual CodeFile Write() WriteType(writer); } + PreserveExistingModelFactoryUsings(writer); + foreach (var suppression in _provider.DisabledFileWarnings) { suppression.RestoreStatement.Write(writer); @@ -37,6 +42,50 @@ public virtual CodeFile Write() return new CodeFile(writer.ToString(), _provider.RelativeFilePath); } + private void PreserveExistingModelFactoryUsings(CodeWriter writer) + { + if (_provider is not ModelFactoryProvider) + { + return; + } + + var existingFilePath = Path.Combine(CodeModelGenerator.Instance.Configuration.OutputDirectory, _provider.RelativeFilePath); + if (!File.Exists(existingFilePath)) + { + return; + } + + foreach (var @namespace in ReadTopLevelUsingNamespaces(existingFilePath)) + { + writer.UseNamespace(@namespace); + } + } + + private static IEnumerable ReadTopLevelUsingNamespaces(string filePath) + { + foreach (var line in File.ReadLines(filePath)) + { + var trimmedLine = line.Trim(); + if (trimmedLine.Length == 0 || + trimmedLine.StartsWith("//", StringComparison.Ordinal) || + trimmedLine.StartsWith("#", StringComparison.Ordinal)) + { + continue; + } + + if (trimmedLine.StartsWith("using ", StringComparison.Ordinal) && + trimmedLine.EndsWith(";", StringComparison.Ordinal) && + !trimmedLine.StartsWith("using static ", StringComparison.Ordinal) && + !trimmedLine.Contains('=')) + { + yield return trimmedLine.Substring("using ".Length, trimmedLine.Length - "using ".Length - 1); + continue; + } + + break; + } + } + private bool IsPublicContext(TypeProvider provider) { return provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public) && @@ -45,7 +94,7 @@ private bool IsPublicContext(TypeProvider provider) private void WriteType(CodeWriter writer) { - if (_provider.PreserveTypeXmlDocs || IsPublicContext(_provider)) + if (_provider.PreserveTypeXmlDocs || _provider.ShouldWriteTypeXmlDocs || IsPublicContext(_provider)) { writer.WriteXmlDocsNoScope(_provider.XmlDocs); } @@ -207,6 +256,11 @@ private void WriteConstructors(CodeWriter writer) private void WriteMethods(CodeWriter writer) { + if (_provider is ModelFactoryProvider { PreserveLeadingMethodSeparator: true } && _provider.Methods.Count > 0) + { + writer.WriteLine(); + } + for (int i = 0; i < _provider.Methods.Count; i++) { writer.WriteMethod(_provider.Methods[i]); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs index c30d3a00bff..fef9e4b4c95 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs @@ -29,6 +29,10 @@ protected internal ModelFactoryProvider(IEnumerable models) _models = models; } + internal bool PreserveLeadingMethodSeparator { get; private set; } + + internal void PreserveLeadingMethodSeparatorForBackCompat() => PreserveLeadingMethodSeparator = true; + protected override string BuildName() => string.Concat(CodeModelGenerator.Instance.TypeFactory.ServiceName, ModelFactorySuffix); protected override string BuildRelativeFilePath() => Path.Combine("src", "Generated", $"{Name}.cs"); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs index be6e44681b5..637a0def083 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs @@ -22,6 +22,7 @@ internal sealed class NamedTypeSymbolProvider : TypeProvider { private INamedTypeSymbol _namedTypeSymbol; private readonly Compilation _compilation; + private string? _metadataName; private TypeProvider? _baseTypeProvider; public NamedTypeSymbolProvider(INamedTypeSymbol namedTypeSymbol, Compilation compilation) @@ -34,8 +35,14 @@ internal string MetadataName { get { + if (_metadataName != null) + { + return _metadataName; + } + var ns = _namedTypeSymbol.ContainingNamespace.GetFullyQualifiedNameFromDisplayString(); - return string.IsNullOrEmpty(ns) ? _namedTypeSymbol.Name : $"{ns}.{_namedTypeSymbol.Name}"; + _metadataName = string.IsNullOrEmpty(ns) ? _namedTypeSymbol.MetadataName : $"{ns}.{_namedTypeSymbol.MetadataName}"; + return _metadataName; } } @@ -337,7 +344,13 @@ protected internal override IReadOnlyList BuildBodyDependencyTypes() var dependencies = new HashSet(); foreach (var syntaxReference in _namedTypeSymbol.DeclaringSyntaxReferences) { - AddBodyDependencyTypes(syntaxReference.GetSyntax(), dependencies); + var syntax = syntaxReference.GetSyntax(); + if (IsGeneratedSourceFile(syntax.SyntaxTree.FilePath)) + { + continue; + } + + AddBodyDependencyTypes(syntax, dependencies); } return [.. dependencies]; @@ -348,13 +361,17 @@ protected internal override IReadOnlyList BuildSignatureDependencyTy var dependencies = new HashSet(); foreach (var syntaxReference in _namedTypeSymbol.DeclaringSyntaxReferences) { - if (syntaxReference.GetSyntax() is not TypeDeclarationSyntax typeDeclaration || + var syntax = syntaxReference.GetSyntax(); + if (IsGeneratedSourceFile(syntax.SyntaxTree.FilePath) || + syntax is not TypeDeclarationSyntax typeDeclaration || !IsPublic(typeDeclaration.Modifiers)) { continue; } - AddSyntaxTypeReferences(typeDeclaration.BaseList, dependencies); + var semanticModel = _compilation.GetSemanticModel(typeDeclaration.SyntaxTree); + var namespaceCandidates = GetNamespaceCandidates(typeDeclaration); + AddSyntaxTypeReferences(typeDeclaration.BaseList, dependencies, semanticModel, namespaceCandidates); foreach (var member in typeDeclaration.Members) { if (IsPublicApiMember(member)) @@ -369,7 +386,8 @@ protected internal override IReadOnlyList BuildSignatureDependencyTy private void AddBodyDependencyTypes(SyntaxNode syntax, HashSet dependencies) { - AddSyntaxTypeReferences(syntax, dependencies); + var semanticModel = _compilation.GetSemanticModel(syntax.SyntaxTree); + AddSyntaxTypeReferences(syntax, dependencies, semanticModel, GetNamespaceCandidates(syntax)); foreach (var invocation in syntax.DescendantNodes().OfType()) { @@ -380,76 +398,78 @@ private void AddBodyDependencyTypes(SyntaxNode syntax, HashSet depen } } - private static void AddPublicSignatureDependencyTypes(MemberDeclarationSyntax member, HashSet dependencies) + private void AddPublicSignatureDependencyTypes(MemberDeclarationSyntax member, HashSet dependencies) { + var semanticModel = _compilation.GetSemanticModel(member.SyntaxTree); + var namespaceCandidates = GetNamespaceCandidates(member); switch (member) { case MethodDeclarationSyntax method: - AddSyntaxTypeReferences(method.ReturnType, dependencies); - AddSyntaxTypeReferences(method.ParameterList, dependencies); - AddSyntaxTypeReferences(method.ConstraintClauses, dependencies); + AddSyntaxTypeReferences(method.ReturnType, dependencies, semanticModel, namespaceCandidates); + AddSyntaxTypeReferences(method.ParameterList, dependencies, semanticModel, namespaceCandidates); + AddSyntaxTypeReferences(method.ConstraintClauses, dependencies, semanticModel, namespaceCandidates); break; case ConstructorDeclarationSyntax constructor: - AddSyntaxTypeReferences(constructor.ParameterList, dependencies); + AddSyntaxTypeReferences(constructor.ParameterList, dependencies, semanticModel, namespaceCandidates); break; case ConversionOperatorDeclarationSyntax conversion: - AddSyntaxTypeReferences(conversion.Type, dependencies); - AddSyntaxTypeReferences(conversion.ParameterList, dependencies); + AddSyntaxTypeReferences(conversion.Type, dependencies, semanticModel, namespaceCandidates); + AddSyntaxTypeReferences(conversion.ParameterList, dependencies, semanticModel, namespaceCandidates); break; case OperatorDeclarationSyntax @operator: - AddSyntaxTypeReferences(@operator.ReturnType, dependencies); - AddSyntaxTypeReferences(@operator.ParameterList, dependencies); + AddSyntaxTypeReferences(@operator.ReturnType, dependencies, semanticModel, namespaceCandidates); + AddSyntaxTypeReferences(@operator.ParameterList, dependencies, semanticModel, namespaceCandidates); break; case PropertyDeclarationSyntax property: - AddSyntaxTypeReferences(property.Type, dependencies); + AddSyntaxTypeReferences(property.Type, dependencies, semanticModel, namespaceCandidates); break; case IndexerDeclarationSyntax indexer: - AddSyntaxTypeReferences(indexer.Type, dependencies); - AddSyntaxTypeReferences(indexer.ParameterList, dependencies); + AddSyntaxTypeReferences(indexer.Type, dependencies, semanticModel, namespaceCandidates); + AddSyntaxTypeReferences(indexer.ParameterList, dependencies, semanticModel, namespaceCandidates); break; case FieldDeclarationSyntax field: - AddSyntaxTypeReferences(field.Declaration.Type, dependencies); + AddSyntaxTypeReferences(field.Declaration.Type, dependencies, semanticModel, namespaceCandidates); break; case EventFieldDeclarationSyntax eventField: - AddSyntaxTypeReferences(eventField.Declaration.Type, dependencies); + AddSyntaxTypeReferences(eventField.Declaration.Type, dependencies, semanticModel, namespaceCandidates); break; case EventDeclarationSyntax @event: - AddSyntaxTypeReferences(@event.Type, dependencies); + AddSyntaxTypeReferences(@event.Type, dependencies, semanticModel, namespaceCandidates); break; case DelegateDeclarationSyntax @delegate: - AddSyntaxTypeReferences(@delegate.ReturnType, dependencies); - AddSyntaxTypeReferences(@delegate.ParameterList, dependencies); - AddSyntaxTypeReferences(@delegate.ConstraintClauses, dependencies); + AddSyntaxTypeReferences(@delegate.ReturnType, dependencies, semanticModel, namespaceCandidates); + AddSyntaxTypeReferences(@delegate.ParameterList, dependencies, semanticModel, namespaceCandidates); + AddSyntaxTypeReferences(@delegate.ConstraintClauses, dependencies, semanticModel, namespaceCandidates); break; case BaseTypeDeclarationSyntax type: - AddSyntaxTypeReferences(type.BaseList, dependencies); + AddSyntaxTypeReferences(type.BaseList, dependencies, semanticModel, namespaceCandidates); break; } } - private static void AddSyntaxTypeReferences(SyntaxNode? node, HashSet dependencies) + private static void AddSyntaxTypeReferences(SyntaxNode? node, HashSet dependencies, SemanticModel semanticModel, IReadOnlyList namespaceCandidates) { if (node == null) { return; } - foreach (var name in node.DescendantNodesAndSelf().OfType()) + foreach (var type in node.DescendantNodesAndSelf().OfType()) { - dependencies.Add(CreateUnresolvedDependencyType(name.Identifier.ValueText)); - } + if (type.IsPartOfStructuredTrivia()) + { + continue; + } - foreach (var name in node.DescendantNodesAndSelf().OfType()) - { - dependencies.Add(CreateUnresolvedDependencyType(name.Identifier.ValueText)); + AddSyntaxTypeReference(type, dependencies, semanticModel, namespaceCandidates); } } - private static void AddSyntaxTypeReferences(IEnumerable nodes, HashSet dependencies) + private static void AddSyntaxTypeReferences(IEnumerable nodes, HashSet dependencies, SemanticModel semanticModel, IReadOnlyList namespaceCandidates) { foreach (var node in nodes) { - AddSyntaxTypeReferences(node, dependencies); + AddSyntaxTypeReferences(node, dependencies, semanticModel, namespaceCandidates); } } @@ -471,17 +491,198 @@ private static bool IsPublic(SyntaxTokenList modifiers) modifier.IsKind(SyntaxKind.PublicKeyword) || modifier.IsKind(SyntaxKind.ProtectedKeyword)); - private static CSharpType CreateUnresolvedDependencyType(string name) + private static bool IsGeneratedSourceFile(string filePath) => + filePath.Contains("/Generated/", StringComparison.Ordinal) || + filePath.Contains("\\Generated\\", StringComparison.Ordinal); + + private static CSharpType CreateUnresolvedDependencyType(string name, int genericArgumentCount = 0) => new( name, string.Empty, isValueType: false, isNullable: false, declaringType: null, - args: [], + args: [.. Enumerable.Range(0, genericArgumentCount).Select(static _ => CreateUnresolvedDependencyType(string.Empty))], isPublic: false, isStruct: false); + private static void AddSyntaxTypeReference(TypeSyntax type, HashSet dependencies, SemanticModel semanticModel, IReadOnlyList namespaceCandidates) + { + if (TryAddSemanticTypeReference(type, dependencies, semanticModel)) + { + return; + } + + if (!IsSyntacticTypeReference(type)) + { + return; + } + + switch (type) + { + case IdentifierNameSyntax identifier: + AddUnresolvedDependencyType(dependencies, identifier.Identifier.ValueText, namespaceCandidates); + break; + case GenericNameSyntax genericName: + AddUnresolvedDependencyType(dependencies, genericName.Identifier.ValueText, namespaceCandidates, genericName.TypeArgumentList.Arguments.Count); + foreach (var argument in genericName.TypeArgumentList.Arguments) + { + AddSyntaxTypeReference(argument, dependencies, semanticModel, namespaceCandidates); + } + break; + case QualifiedNameSyntax qualifiedName: + AddQualifiedUnresolvedDependencyType(dependencies, qualifiedName); + AddSyntaxTypeReference(qualifiedName.Right, dependencies, semanticModel, namespaceCandidates); + break; + case AliasQualifiedNameSyntax aliasQualifiedName: + AddSyntaxTypeReference(aliasQualifiedName.Name, dependencies, semanticModel, namespaceCandidates); + break; + case ArrayTypeSyntax arrayType: + AddSyntaxTypeReference(arrayType.ElementType, dependencies, semanticModel, namespaceCandidates); + break; + case NullableTypeSyntax nullableType: + AddSyntaxTypeReference(nullableType.ElementType, dependencies, semanticModel, namespaceCandidates); + break; + case PointerTypeSyntax pointerType: + AddSyntaxTypeReference(pointerType.ElementType, dependencies, semanticModel, namespaceCandidates); + break; + case TupleTypeSyntax tupleType: + foreach (var element in tupleType.Elements) + { + AddSyntaxTypeReference(element.Type, dependencies, semanticModel, namespaceCandidates); + } + break; + } + } + + private static bool TryAddSemanticTypeReference(TypeSyntax type, HashSet dependencies, SemanticModel semanticModel) + { + var typeSymbol = semanticModel.GetTypeInfo(type).Type ?? + semanticModel.GetTypeInfo(type).ConvertedType ?? + (semanticModel.GetSymbolInfo(type).Symbol as INamedTypeSymbol); + if (typeSymbol is not INamedTypeSymbol namedTypeSymbol || + namedTypeSymbol.TypeKind == TypeKind.Error || + namedTypeSymbol.SpecialType == SpecialType.System_Void) + { + return false; + } + + dependencies.Add(namedTypeSymbol.GetCSharpType()); + return true; + } + + private static void AddUnresolvedDependencyType(HashSet dependencies, string name, IReadOnlyList namespaceCandidates, int genericArgumentCount = 0) + { + if (string.Equals(name, "var", StringComparison.Ordinal) || + string.Equals(name, "dynamic", StringComparison.Ordinal)) + { + return; + } + + dependencies.Add(CreateUnresolvedDependencyType(name, genericArgumentCount)); + foreach (var ns in namespaceCandidates) + { + dependencies.Add(CreateDependencyType(name, ns, genericArgumentCount)); + } + } + + private static void AddQualifiedUnresolvedDependencyType(HashSet dependencies, QualifiedNameSyntax qualifiedName) + { + var fullName = qualifiedName.ToString(); + var lastDot = fullName.LastIndexOf('.'); + if (lastDot <= 0 || lastDot == fullName.Length - 1) + { + return; + } + + dependencies.Add(CreateDependencyType(fullName.Substring(lastDot + 1), fullName.Substring(0, lastDot))); + } + + private static CSharpType CreateDependencyType(string name, string ns, int genericArgumentCount = 0) + => new( + name, + ns, + isValueType: false, + isNullable: false, + declaringType: null, + args: [.. Enumerable.Range(0, genericArgumentCount).Select(static _ => CreateUnresolvedDependencyType(string.Empty))], + isPublic: false, + isStruct: false); + + private static IReadOnlyList GetNamespaceCandidates(SyntaxNode node) + { + var namespaces = new HashSet(StringComparer.Ordinal); + for (var current = node; current != null; current = current.Parent) + { + switch (current) + { + case BaseNamespaceDeclarationSyntax namespaceDeclaration: + namespaces.Add(namespaceDeclaration.Name.ToString()); + break; + } + } + + if (node.SyntaxTree.GetRoot() is CompilationUnitSyntax compilationUnit) + { + foreach (var usingDirective in compilationUnit.Usings) + { + if (usingDirective.Alias == null && !usingDirective.StaticKeyword.IsKind(SyntaxKind.StaticKeyword) && usingDirective.Name != null) + { + namespaces.Add(usingDirective.Name.ToString()); + } + } + } + + return [.. namespaces]; + } + + private static bool IsSyntacticTypeReference(TypeSyntax type) + { + var parent = type.Parent; + return parent switch + { + ArrayTypeSyntax arrayType => arrayType.ElementType == type && IsSyntacticTypeReference(arrayType), + NullableTypeSyntax nullableType => nullableType.ElementType == type && IsSyntacticTypeReference(nullableType), + PointerTypeSyntax pointerType => pointerType.ElementType == type && IsSyntacticTypeReference(pointerType), + TupleElementSyntax tupleElement => tupleElement.Type == type, + TypeArgumentListSyntax typeArgumentList => typeArgumentList.Arguments.Contains(type), + QualifiedNameSyntax qualifiedName => qualifiedName.Right == type && IsSyntacticTypeReference(qualifiedName), + AliasQualifiedNameSyntax aliasQualifiedName => aliasQualifiedName.Name == type && IsSyntacticTypeReference(aliasQualifiedName), + SimpleBaseTypeSyntax simpleBaseType => simpleBaseType.Type == type, + ParameterSyntax parameter => parameter.Type == type, + VariableDeclarationSyntax variableDeclaration => variableDeclaration.Type == type, + PropertyDeclarationSyntax property => property.Type == type, + IndexerDeclarationSyntax indexer => indexer.Type == type, + MethodDeclarationSyntax method => method.ReturnType == type, + LocalFunctionStatementSyntax localFunction => localFunction.ReturnType == type, + DelegateDeclarationSyntax @delegate => @delegate.ReturnType == type, + OperatorDeclarationSyntax @operator => @operator.ReturnType == type, + ConversionOperatorDeclarationSyntax conversion => conversion.Type == type, + TypeConstraintSyntax typeConstraint => typeConstraint.Type == type, + ObjectCreationExpressionSyntax objectCreation => objectCreation.Type == type, + MemberAccessExpressionSyntax memberAccess => memberAccess.Expression == type && LooksLikeTypeName(type), + CastExpressionSyntax cast => cast.Type == type, + DefaultExpressionSyntax @default => @default.Type == type, + SizeOfExpressionSyntax sizeOf => sizeOf.Type == type, + TypeOfExpressionSyntax typeOf => typeOf.Type == type, + DeclarationExpressionSyntax declaration => declaration.Type == type, + _ => false + }; + } + + private static bool LooksLikeTypeName(TypeSyntax type) + => type switch + { + IdentifierNameSyntax identifier => IsUppercaseIdentifier(identifier.Identifier.ValueText), + GenericNameSyntax genericName => IsUppercaseIdentifier(genericName.Identifier.ValueText), + QualifiedNameSyntax qualifiedName => LooksLikeTypeName(qualifiedName.Right), + AliasQualifiedNameSyntax aliasQualifiedName => LooksLikeTypeName(aliasQualifiedName.Name), + _ => false + }; + + private static bool IsUppercaseIdentifier(string name) + => name.Length > 0 && char.IsUpper(name[0]); + private static string? GetInvocationName(InvocationExpressionSyntax invocation) => invocation.Expression switch { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs index b61b6d606b8..810366b61e1 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs @@ -145,6 +145,8 @@ public XmlDocProvider XmlDocs internal bool PreserveTypeXmlDocs { get; private set; } + protected internal virtual bool ShouldWriteTypeXmlDocs => false; + internal void PreserveXmlDocs() { PreserveTypeXmlDocs = true; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.BodyReferences.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.BodyReferences.cs new file mode 100644 index 00000000000..4891da18938 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.BodyReferences.cs @@ -0,0 +1,405 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.TypeSpec.Generator.Expressions; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; +using Microsoft.TypeSpec.Generator.Statements; + +namespace Microsoft.TypeSpec.Generator +{ + internal static partial class ProviderReferenceMapAnalyzer + { + private static void AddGeneratedBodyReferences(IReadOnlyList providers, ProviderReferenceGraph graph) + { + foreach (var (provider, isSerializationProvider) in GetBodyReferenceProviders(providers)) + { + if (IsModelFactoryProvider(provider) || + !IsGeneratedBodyReferenceCandidate(provider, isSerializationProvider)) + { + continue; + } + + var providerName = GetProviderTypeName(provider.Type); + if (!graph.Nodes.Contains(providerName)) + { + continue; + } + + AddProviderBodyDependencyTypes( + graph.References[providerName], + GetNonEnumStructuredBodyReferenceTypes(provider, graph.Nodes), + graph.Nodes); + AddProviderBodyDependencyTypes(graph.References[providerName], provider.BodyDependencyTypes, graph.Nodes); + AddProviderInfrastructureReferences(graph.References[providerName], provider, isSerializationProvider, graph.Nodes); + } + } + + private static void AddProviderInfrastructureReferences(HashSet references, TypeProvider provider, bool isSerializationProvider, HashSet nodes) + { + AddMatchingName(references, "ProviderConstants", nodes); + AddMatchingName(references, "TypeFormatters", nodes); + + if (provider.SerializationProviders.Count > 0) + { + AddSerializationExtensionReferences(references, provider, nodes); + } + + if (isSerializationProvider) + { + AddMatchingName(references, "Optional", nodes); + AddMatchingName(references, "ModelSerializationExtensions", nodes); + AddSerializationExtensionReferences(references, provider, nodes); + } + + foreach (var method in provider.Methods) + { + AddMethodInfrastructureReferences(references, method, nodes); + } + } + + private static void AddSerializationExtensionReferences(HashSet references, TypeProvider provider, HashSet nodes) + { + AddSerializationExtensionReferences(references, provider.Type, nodes); + AddSerializationExtensionReferences(references, provider.BaseType, nodes); + foreach (var implementedType in provider.Implements) + { + AddSerializationExtensionReferences(references, implementedType, nodes); + } + + foreach (var property in provider.Properties) + { + AddSerializationExtensionReferences(references, property.Type, nodes); + } + + foreach (var field in provider.Fields) + { + AddSerializationExtensionReferences(references, field.Type, nodes); + } + + foreach (var constructor in provider.Constructors) + { + AddSerializationExtensionReferences(references, constructor.Signature.ReturnType, nodes); + foreach (var parameter in constructor.Signature.Parameters) + { + AddSerializationExtensionReferences(references, parameter.Type, nodes); + } + } + + foreach (var method in provider.Methods) + { + AddSerializationExtensionReferences(references, method.Signature.ReturnType, nodes); + foreach (var parameter in method.Signature.Parameters) + { + AddSerializationExtensionReferences(references, parameter.Type, nodes); + } + } + } + + private static void AddSerializationExtensionReferences(HashSet references, CSharpType? type, HashSet nodes) + { + if (type == null) + { + return; + } + + if (string.IsNullOrEmpty(type.Namespace)) + { + AddMatchingName(references, $"{type.Name}Extensions", nodes); + } + else + { + AddExactMetadataNameMatch(references, $"{type.Namespace}.{type.Name}Extensions", nodes); + } + foreach (var argument in type.Arguments) + { + AddSerializationExtensionReferences(references, argument, nodes); + } + } + + private static void AddMethodInfrastructureReferences(HashSet references, MethodProvider method, HashSet nodes) + { + AddReturnTypeInfrastructureReferences(references, method.Signature.ReturnType, nodes); + } + + private static void AddReturnTypeInfrastructureReferences(HashSet references, CSharpType? returnType, HashSet nodes) + { + var type = UnwrapTask(returnType); + if (type == null) + { + return; + } + + var typeName = StripGenericArity(type.Name); + if (string.Equals(typeName, "Pageable", StringComparison.Ordinal)) + { + AddMatchingName(references, "PageableWrapper", nodes); + } + else if (string.Equals(typeName, "AsyncPageable", StringComparison.Ordinal)) + { + AddMatchingName(references, "AsyncPageableWrapper", nodes); + } + else if (string.Equals(typeName, "ArmOperation", StringComparison.Ordinal)) + { + AddMatchingNamesWithSimpleNameSuffix(references, "ArmOperation", nodes); + AddMatchingNamesWithSimpleNameSuffix(references, "OperationSource", nodes); + if (type.Arguments.Count > 0) + { + AddMatchingName(references, $"{BuildOperationSourceTypeName(type.Arguments[0])}OperationSource", nodes); + } + } + } + + private static CSharpType? UnwrapTask(CSharpType? type) + { + var typeName = type == null ? null : StripGenericArity(type.Name); + if ((string.Equals(typeName, "Task", StringComparison.Ordinal) || + string.Equals(typeName, "ValueTask", StringComparison.Ordinal)) && + type?.Arguments.Count > 0) + { + return type.Arguments[0]; + } + + return type; + } + + private static string BuildOperationSourceTypeName(CSharpType type) + { + var argumentNames = string.Join("", type.Arguments.Select(BuildOperationSourceTypeName)); + return $"{type.Name}{(argumentNames.Length > 0 ? "Of" : string.Empty)}{argumentNames}"; + } + + private static IReadOnlyList GetNonEnumStructuredBodyReferenceTypes(TypeProvider provider, HashSet nodes) + { + var references = new List(); + foreach (var dependency in CollectStructuredBodyReferenceTypes(provider)) + { + if (!IsEnumProviderDependency(dependency, nodes)) + { + references.Add(dependency); + } + } + + return references; + } + + private static IReadOnlyList CollectStructuredBodyReferenceTypes(TypeProvider provider) + { + var references = new HashSet(); + var visited = new HashSet(ReferenceEqualityComparer.Instance); + + foreach (var field in provider.Fields) + { + CollectStructuredBodyReferenceTypes(field.InitializationValue, references, visited); + } + + foreach (var property in provider.Properties) + { + CollectStructuredBodyReferenceTypes(property.Body, references, visited); + } + + foreach (var constructor in provider.Constructors) + { + CollectStructuredBodyReferenceTypes(constructor.BodyExpression, references, visited); + CollectStructuredBodyReferenceTypes(constructor.BodyStatements, references, visited); + } + + foreach (var method in provider.Methods) + { + if (method.IsMethodSuppressed()) + { + continue; + } + + CollectStructuredBodyReferenceTypes(method.BodyExpression, references, visited); + CollectStructuredBodyReferenceTypes(method.BodyStatements, references, visited); + } + + return [.. references]; + } + + private static void CollectStructuredBodyReferenceTypes(object? value, HashSet references, HashSet visited) + { + switch (value) + { + case null: + case string: + case FormattableString: + return; + } + + if (!value.GetType().IsValueType && !visited.Add(value)) + { + return; + } + + switch (value) + { + case CSharpType type: + references.Add(type); + return; + case Type type: + references.Add(type); + return; + case ParameterProvider parameter: + references.Add(parameter.Type); + CollectStructuredBodyReferenceTypes(parameter.DefaultValue, references, visited); + CollectStructuredBodyReferenceTypes(parameter.InitializationValue, references, visited); + return; + case MethodSignatureBase signature: + CollectStructuredBodyReferenceTypes(signature.ReturnType, references, visited); + CollectStructuredBodyReferenceTypes(signature.Parameters, references, visited); + return; + case KeyValuePair positionalArgument: + CollectStructuredBodyReferenceTypes(positionalArgument.Value, references, visited); + return; + case FieldProvider field: + references.Add(field.Type); + CollectStructuredBodyReferenceTypes(field.InitializationValue, references, visited); + return; + } + + if (IsStructuredBodyReferenceObject(value)) + { + foreach (var property in value.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (property.GetIndexParameters().Length > 0) + { + continue; + } + + CollectStructuredBodyReferenceTypes(property.GetValue(value), references, visited); + } + + return; + } + + if (value is not IEnumerable values) + { + return; + } + + foreach (var item in values) + { + CollectStructuredBodyReferenceTypes(item, references, visited); + } + } + + private static bool IsEnumProviderDependency(CSharpType dependency, HashSet nodes) + { + var providerName = GetProviderTypeName(dependency); + if (!nodes.Contains(providerName)) + { + return false; + } + + foreach (var provider in CodeModelGenerator.Instance.OutputLibrary.TypeProviders) + { + if (provider is EnumProvider && + string.Equals(GetProviderTypeName(provider.Type), providerName, StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + + private static bool IsStructuredBodyReferenceObject(object value) => + value is ValueExpression || + value is MethodBodyStatement || + value is PropertyBody; + + private static void AddProviderBodyDependencyTypes( + HashSet references, + IReadOnlyList dependencies, + HashSet nodes, + bool includeSimpleNameReferences = false, + bool includeUnqualifiedSimpleNameReferences = false, + bool includeExtensionReferences = true) + { + foreach (var dependency in dependencies) + { + AddProviderBodyDependencyType(references, dependency, nodes, includeSimpleNameReferences, includeUnqualifiedSimpleNameReferences, includeExtensionReferences); + } + } + + private static void AddProviderBodyDependencyType( + HashSet references, + CSharpType? dependency, + HashSet nodes, + bool includeSimpleNameReferences, + bool includeUnqualifiedSimpleNameReferences, + bool includeExtensionReferences) + { + if (dependency == null) + { + return; + } + + AddTypeReference(references, dependency, nodes); + if (includeSimpleNameReferences && + !string.IsNullOrEmpty(dependency.Namespace) && + dependency.Arguments.Count == 0) + { + AddMatchingName(references, dependency.Name, nodes); + } + else if (includeUnqualifiedSimpleNameReferences && + string.IsNullOrEmpty(dependency.Namespace) && + dependency.Arguments.Count == 0) + { + AddUnambiguousMatchingName(references, dependency.Name, nodes); + } + if (includeExtensionReferences) + { + if (string.IsNullOrEmpty(dependency.Namespace)) + { + AddMatchingName(references, $"{dependency.Name}Extensions", nodes); + } + else + { + AddExactMetadataNameMatch(references, $"{dependency.Namespace}.{dependency.Name}Extensions", nodes); + } + } + + foreach (var argument in dependency.Arguments) + { + AddProviderBodyDependencyType(references, argument, nodes, includeSimpleNameReferences, includeUnqualifiedSimpleNameReferences, includeExtensionReferences); + } + } + + private static IReadOnlyList<(TypeProvider Provider, bool IsSerializationProvider)> GetBodyReferenceProviders(IReadOnlyList providers) + { + var bodyReferenceProviders = new List<(TypeProvider Provider, bool IsSerializationProvider)>(); + foreach (var provider in providers) + { + bodyReferenceProviders.Add((provider, false)); + foreach (var serializationProvider in provider.SerializationProviders) + { + bodyReferenceProviders.Add((serializationProvider, true)); + } + } + + return bodyReferenceProviders; + } + + private static bool IsGeneratedBodyReferenceCandidate(TypeProvider provider, bool isSerializationProvider) + { + if (provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Static)) + { + return true; + } + + return IsClientProvider(provider) || + isSerializationProvider || + provider.IncludeGeneratedBodyReferences || + provider.HelperDependencyTypes.Count > 0 || + provider.BodyDependencyTypes.Count > 0; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.Candidates.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.Candidates.cs new file mode 100644 index 00000000000..67475070797 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.Candidates.cs @@ -0,0 +1,358 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; + +namespace Microsoft.TypeSpec.Generator +{ + internal static partial class ProviderReferenceMapAnalyzer + { + private static HashSet GetCustomInternalBoundaryNodes( + ProviderReferenceGraph publicGraph, + HashSet customInternalDeclarations) + { + var boundaryNodes = new HashSet(StringComparer.Ordinal); + foreach (var node in publicGraph.Nodes) + { + if (!publicGraph.References.TryGetValue(node, out var references)) + { + continue; + } + + if (references.Overlaps(customInternalDeclarations)) + { + boundaryNodes.Add(node); + } + } + + return boundaryNodes; + } + + private static HashSet GetPublicizeDeclaredNodes( + IReadOnlyList generatedProviders, + HashSet nodes, + HashSet internalizeDeclaredNodes) + { + var publicizeDeclaredNodes = GetPostProcessorDeclaredNodes(generatedProviders, nodes, publicOnly: false); + return publicizeDeclaredNodes; + } + + private static HashSet GetPublicApiTraversalNodes( + HashSet internalizeDeclaredNodes, + HashSet publicizeDeclaredNodes, + HashSet generatedInternalDeclarations, + HashSet generatedImplementationInternalDeclarations) + { + var traversalNodes = new HashSet(StringComparer.Ordinal); + foreach (var node in internalizeDeclaredNodes) + { + if (generatedInternalDeclarations.Contains(node) || + generatedImplementationInternalDeclarations.Contains(node)) + { + continue; + } + + traversalNodes.Add(node); + } + + foreach (var node in publicizeDeclaredNodes) + { + if (!generatedImplementationInternalDeclarations.Contains(node)) + { + traversalNodes.Add(node); + } + } + + return traversalNodes; + } + + private static HashSet GetInternalizeCandidates( + HashSet internalizeDeclaredNodes, + HashSet publicizeReachable, + HashSet customInternalDeclarations, + HashSet generatedInternalDeclarations, + HashSet customInternalBoundaryNodes, + HashSet customPublicRoots, + HashSet publicizeRoots, + HashSet nodes, + IReadOnlyDictionary> references) + { + var candidates = new HashSet(StringComparer.Ordinal); + foreach (var node in internalizeDeclaredNodes) + { + var isNonRootKept = IsKeptName(node, CodeModelGenerator.Instance.NonRootTypes, nodes); + if (!publicizeReachable.Contains(node) || + customInternalDeclarations.Contains(node) || + generatedInternalDeclarations.Contains(node) && !customPublicRoots.Contains(node) || + customInternalBoundaryNodes.Contains(node) && (!publicizeRoots.Contains(node) || isNonRootKept) || + isNonRootKept && + references.TryGetValue(node, out var nodeReferences) && + (nodeReferences.Overlaps(customInternalDeclarations) || + nodeReferences.Overlaps(generatedInternalDeclarations))) + { + candidates.Add(node); + } + } + + // If a public non-root type exposes something that must remain internal, make the + // exposing type internal too. That avoids generating public APIs with internal types. + var addedCandidate = true; + while (addedCandidate) + { + addedCandidate = false; + foreach (var node in internalizeDeclaredNodes) + { + var isNonRootKept = IsKeptName(node, CodeModelGenerator.Instance.NonRootTypes, nodes); + if (candidates.Contains(node) || + publicizeRoots.Contains(node) && !isNonRootKept || + !references.TryGetValue(node, out var nodeReferences) || + !nodeReferences.Overlaps(candidates)) + { + continue; + } + + candidates.Add(node); + addedCandidate = true; + } + } + + // Non-root keep entries preserve the declared type/file, but deliberately do not + // root their dependencies. They can still be internalized when needed to avoid + // exposing custom/internal types through a public surface. + RemoveKeptNonRootNames(candidates, nodes, references, customInternalDeclarations, generatedInternalDeclarations, customInternalBoundaryNodes); + return candidates; + } + + private static void AddNestedInternalizeCandidates( + IReadOnlyList generatedProviders, + HashSet candidates, + HashSet nodes) + { + var addedCandidate = true; + while (addedCandidate) + { + addedCandidate = false; + foreach (var provider in generatedProviders) + { + var declaringType = provider.DeclaringTypeProvider?.Type; + if (declaringType == null || + !candidates.Contains(GetProviderTypeName(declaringType))) + { + continue; + } + + var providerName = GetProviderTypeName(provider.Type); + if (nodes.Contains(providerName) && candidates.Add(providerName)) + { + addedCandidate = true; + } + } + } + } + + private static void AddInternalOnlyDependencyCandidates( + HashSet internalizeDeclaredNodes, + HashSet candidates, + HashSet customInternalDeclarations, + HashSet generatedInternalDeclarations, + HashSet explicitPublicRoots, + IReadOnlyDictionary> references, + HashSet generatedImplementationInternalDeclarations) + { + var addedCandidate = true; + while (addedCandidate) + { + addedCandidate = false; + foreach (var node in internalizeDeclaredNodes) + { + if (candidates.Contains(node) || explicitPublicRoots.Contains(node)) + { + continue; + } + + var hasPredecessor = false; + var allPredecessorsInternalized = true; + foreach (var (owner, children) in references) + { + if (string.Equals(owner, node, StringComparison.Ordinal) || + generatedImplementationInternalDeclarations.Contains(owner) || + !children.Contains(node)) + { + continue; + } + + hasPredecessor = true; + if (!candidates.Contains(owner) && + !customInternalDeclarations.Contains(owner) && + !generatedInternalDeclarations.Contains(owner)) + { + allPredecessorsInternalized = false; + break; + } + } + + if (hasPredecessor && allPredecessorsInternalized && candidates.Add(node)) + { + addedCandidate = true; + } + } + } + } + + private static HashSet GetPublicizeCandidates( + HashSet publicizeDeclaredNodes, + HashSet publicizeReachable, + HashSet customInternalDeclarations, + HashSet customInternalBoundaryNodes, + HashSet internalizeHelperRoots, + HashSet publicizeRootExclusions, + HashSet generatedInternalDeclarations, + HashSet publicizeRoots, + Dictionary> publicApiReferences, + Dictionary> internalizeReferences, + HashSet generatedImplementationInternalDeclarations) + { + var candidates = new HashSet(StringComparer.Ordinal); + foreach (var node in publicizeDeclaredNodes) + { + if (customInternalDeclarations.Contains(node) || + customInternalBoundaryNodes.Contains(node) || + internalizeHelperRoots.Contains(node) || + publicizeRootExclusions.Contains(node) || + !publicizeReachable.Contains(node)) + { + continue; + } + + if (generatedInternalDeclarations.Contains(node) && + !publicizeRoots.Contains(node) && + !HasPublicApiPredecessor(node, publicApiReferences, publicizeReachable, generatedImplementationInternalDeclarations)) + { + continue; + } + + if (!publicizeRoots.Contains(node) && + !HasPublicApiPredecessor(node, internalizeReferences, publicizeReachable, generatedImplementationInternalDeclarations)) + { + continue; + } + + candidates.Add(node); + } + + return candidates; + } + + private static HashSet GetRemovalCandidates( + IReadOnlyList providers, + IReadOnlyList generatedProviders, + ProviderReferenceGraph graph, + HashSet customRemovalRoots, + HashSet generatedDiscriminatorBaseNames) + { + var removeRoots = GetRootNames( + providers, + graph.Nodes, + helperRoots: [], + includeModelFactory: true, + includeAdditionalRoots: true, + includeUnionVariantRoots: true, + includeModelFactorySignatureRoots: true, + publicClientRootsOnly: false); + + removeRoots.UnionWith(customRemovalRoots); + AddMatchingNamesWithSimpleNameSuffix(removeRoots, "ReferenceType", graph.Nodes); + AddKeptNonRootNames(removeRoots, graph.Nodes); + AddCustomCodeExtensionRoots(removeRoots, generatedProviders, graph.Nodes); + AddCustomizationBackedExtensionRoots(removeRoots, graph.Nodes); + AddCustomRequestHeaderExtensionsRoot(removeRoots, generatedProviders, graph.Nodes); + RemoveUnusedRequestHeaderExtensionsRoot(removeRoots, graph.References, providers); + + var removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); + AddDerivedModelReferences(providers, graph.Nodes, graph.References, removeReachableWithoutHelpers, generatedDiscriminatorBaseNames); + removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); + AddBasePreservedReferences(generatedProviders, graph.Nodes, graph.References, removeReachableWithoutHelpers); + + var removeHelperRoots = GetHelperRootNames(generatedProviders, graph.Nodes, removeReachableWithoutHelpers, graph.References); + removeRoots.UnionWith(removeHelperRoots); + + var removeReachable = GetReachableTypes(removeRoots, graph.References); + AddBasePreservedReferences(generatedProviders, graph.Nodes, graph.References, removeReachable); + + var removeDeclaredNodes = GetPostProcessorDeclaredNodes(generatedProviders, graph.Nodes, publicOnly: false); + removeDeclaredNodes.ExceptWith(removeReachable); + return removeDeclaredNodes; + } + + private static void AddKeptNonRootNames(HashSet roots, HashSet nodes) + { + var nonRootTypes = CodeModelGenerator.Instance.NonRootTypes; + foreach (var node in nodes) + { + if (IsKeptName(node, nonRootTypes, nodes)) + { + roots.Add(node); + } + } + } + + private static void RemoveKeptNonRootNames( + HashSet candidates, + HashSet nodes, + IReadOnlyDictionary> references, + HashSet customInternalDeclarations, + HashSet generatedInternalDeclarations, + HashSet customInternalBoundaryNodes) + { + var nonRootTypes = CodeModelGenerator.Instance.NonRootTypes; + foreach (var node in nodes) + { + if (customInternalDeclarations.Contains(node) || + generatedInternalDeclarations.Contains(node) || + customInternalBoundaryNodes.Contains(node)) + { + continue; + } + + if (IsKeptName(node, nonRootTypes, nodes) && + !HasCandidateReference(node, candidates, references)) + { + candidates.Remove(node); + } + } + } + + private static bool HasCandidateReference( + string node, + HashSet candidates, + IReadOnlyDictionary> references) + { + if (references.TryGetValue(node, out var nodeReferences)) + { + foreach (var reference in nodeReferences) + { + if (!string.Equals(reference, node, StringComparison.Ordinal) && + candidates.Contains(reference)) + { + return true; + } + } + } + + foreach (var (source, sourceReferences) in references) + { + if (!string.Equals(source, node, StringComparison.Ordinal) && + candidates.Contains(source) && + sourceReferences.Contains(node)) + { + return true; + } + } + + return false; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.CustomCodeRoots.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.CustomCodeRoots.cs new file mode 100644 index 00000000000..334e8acd93e --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.CustomCodeRoots.cs @@ -0,0 +1,542 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; + +namespace Microsoft.TypeSpec.Generator +{ + internal static partial class ProviderReferenceMapAnalyzer + { + private static HashSet GetCustomCodeGeneratedTypeRoots(IReadOnlyList providers, HashSet generatedTypeNames) + { + var roots = new HashSet(StringComparer.Ordinal); + foreach (var customCodeView in GetCustomCodeViews(providers)) + { + AddCustomCodeViewGeneratedTypeRoot(roots, customCodeView, generatedTypeNames); + AddCustomCodeViewRoots(roots, customCodeView, generatedTypeNames, publicOnly: false); + } + + return roots; + } + + private static HashSet GetCustomCodePublicGeneratedTypeRoots(IReadOnlyList providers, HashSet generatedTypeNames) + { + var roots = new HashSet(StringComparer.Ordinal); + foreach (var customCodeView in GetCustomCodeViews(providers)) + { + if (!customCodeView.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) + { + continue; + } + + AddCustomCodeViewGeneratedTypeRoot(roots, customCodeView, generatedTypeNames); + AddCustomCodeViewRoots(roots, customCodeView, generatedTypeNames, publicOnly: true); + } + + return roots; + } + + private static IEnumerable GetCustomCodeViews(IReadOnlyList providers) + { + var visited = new HashSet(StringComparer.Ordinal); + var modelFactoryCustomCodeView = CodeModelGenerator.Instance.OutputLibrary.ModelFactory.Value.CustomCodeView; + if (modelFactoryCustomCodeView != null && visited.Add(GetCustomCodeViewIdentity(modelFactoryCustomCodeView))) + { + yield return modelFactoryCustomCodeView; + } + + foreach (var provider in providers) + { + var customCodeView = provider.CustomCodeView; + if (customCodeView == null || !visited.Add(GetCustomCodeViewIdentity(customCodeView))) + { + continue; + } + + yield return customCodeView; + } + + foreach (var customTypeProvider in CodeModelGenerator.Instance.SourceInputModel.GetCustomizationTypeProviders()) + { + if (visited.Add(GetCustomCodeViewIdentity(customTypeProvider))) + { + yield return customTypeProvider; + } + } + } + + private static string GetCustomCodeViewIdentity(TypeProvider customCodeView) => + customCodeView is NamedTypeSymbolProvider namedTypeSymbolProvider + ? namedTypeSymbolProvider.MetadataName + : GetProviderTypeName(customCodeView.Type); + + private static void AddCustomRequestHeaderExtensionsRoot(HashSet roots, IReadOnlyList providers, HashSet nodes) + { + if (!HasCustomRequestHeaderExtensionsReference(providers)) + { + return; + } + + AddMatchingNamesWithSimpleNameSuffix(roots, "RequestHeaderExtensions", nodes); + AddMatchingNamesWithSimpleNameSuffix(roots, "RequestHeadersExtensions", nodes); + } + + private static void AddCustomCodeExtensionRoots(HashSet roots, IReadOnlyList providers, HashSet nodes) + { + foreach (var customCodeView in GetCustomCodeViews(providers)) + { + AddMatchingName(roots, $"{GetCustomCodeViewSimpleName(customCodeView)}Extensions", nodes); + } + } + + private static string GetCustomCodeViewSimpleName(TypeProvider customCodeView) => + customCodeView is NamedTypeSymbolProvider namedTypeSymbolProvider + ? namedTypeSymbolProvider.MetadataSimpleName + : customCodeView.Type.Name; + + private static void AddCustomCodeViewGeneratedTypeRoot(HashSet roots, TypeProvider customCodeView, HashSet generatedTypeNames) + { + if (customCodeView is NamedTypeSymbolProvider namedTypeSymbolProvider) + { + AddExactMetadataNameMatch(roots, namedTypeSymbolProvider.MetadataName, generatedTypeNames); + return; + } + + AddTypeReference(roots, customCodeView.Type, generatedTypeNames); + } + + private static void AddCustomizationBackedExtensionRoots(HashSet roots, HashSet nodes) + { + foreach (var node in nodes) + { + var simpleName = GetSimpleName(node); + if (!simpleName.EndsWith("Extensions", StringComparison.Ordinal)) + { + continue; + } + + var namespaceName = GetNamespaceName(node); + if (namespaceName == null) + { + continue; + } + + var customTypeName = simpleName.Substring(0, simpleName.Length - "Extensions".Length); + if (CodeModelGenerator.Instance.SourceInputModel.FindForTypeInCustomization(namespaceName, customTypeName) != null) + { + roots.Add(node); + } + } + } + + private static void AddCustomCodeViewRoots(HashSet roots, TypeProvider customCodeView, HashSet generatedTypeNames, bool publicOnly) + { + AddTypeReference(roots, customCodeView.BaseType, generatedTypeNames); + AddProviderBodyDependencyTypes(roots, customCodeView.SignatureDependencyTypes, generatedTypeNames, includeSimpleNameReferences: true, includeUnqualifiedSimpleNameReferences: true, includeExtensionReferences: !publicOnly); + if (!publicOnly) + { + AddProviderBodyDependencyTypes(roots, customCodeView.BodyDependencyTypes, generatedTypeNames); + AddCustomCodeViewNamespaceBodyDependencyTypes(roots, customCodeView, generatedTypeNames); + AddAttributes(roots, customCodeView.Attributes, generatedTypeNames, serializationProviderNamesByType: null, includeArguments: true); + AddMatchingName(roots, $"{GetCustomCodeViewSimpleName(customCodeView)}Extensions", generatedTypeNames); + } + + foreach (var implementedType in customCodeView.Implements) + { + AddTypeReference(roots, implementedType, generatedTypeNames); + } + + foreach (var constructor in customCodeView.Constructors) + { + if (publicOnly && !IsPublic(constructor.Signature.Modifiers)) + { + continue; + } + + AddSignatureReferences(roots, constructor.Signature, generatedTypeNames, serializationProviderNamesByType: null, includeAttributes: !publicOnly); + } + + foreach (var method in customCodeView.Methods) + { + if (publicOnly && !IsPublic(method.Signature.Modifiers)) + { + continue; + } + + AddSignatureReferences(roots, method.Signature, generatedTypeNames, serializationProviderNamesByType: null, includeAttributes: !publicOnly); + } + + foreach (var property in customCodeView.Properties) + { + if (publicOnly && !IsPublic(property.Modifiers)) + { + continue; + } + + AddTypeReference(roots, property.Type, generatedTypeNames); + AddTypeReference(roots, property.ExplicitInterface, generatedTypeNames); + if (!publicOnly) + { + AddAttributes(roots, property.Attributes, generatedTypeNames, serializationProviderNamesByType: null, includeArguments: true); + } + } + + foreach (var field in customCodeView.Fields) + { + if (publicOnly && !IsPublic(field.Modifiers)) + { + continue; + } + + AddTypeReference(roots, field.Type, generatedTypeNames); + if (!publicOnly) + { + AddAttributes(roots, field.Attributes, generatedTypeNames, serializationProviderNamesByType: null, includeArguments: true); + } + } + } + + private static void AddCustomCodeViewNamespaceBodyDependencyTypes(HashSet roots, TypeProvider customCodeView, HashSet generatedTypeNames) + { + var customNamespace = customCodeView.Type.Namespace; + if (string.IsNullOrEmpty(customNamespace)) + { + return; + } + + var customMemberNames = GetCustomCodeViewMemberNames(customCodeView); + foreach (var dependency in customCodeView.BodyDependencyTypes) + { + AddNamespaceBodyDependencyType(roots, dependency, customNamespace, customMemberNames, generatedTypeNames); + } + } + + private static void AddNamespaceBodyDependencyType(HashSet roots, CSharpType? dependency, string customNamespace, HashSet customMemberNames, HashSet generatedTypeNames) + { + if (dependency == null) + { + return; + } + + if (string.IsNullOrEmpty(dependency.Namespace) && + dependency.Arguments.Count == 0 && + !customMemberNames.Contains(dependency.Name)) + { + AddExactMetadataNameMatch(roots, $"{customNamespace}.{dependency.Name}", generatedTypeNames); + } + + foreach (var argument in dependency.Arguments) + { + AddNamespaceBodyDependencyType(roots, argument, customNamespace, customMemberNames, generatedTypeNames); + } + } + + private static HashSet GetCustomCodeViewMemberNames(TypeProvider customCodeView) + { + var memberNames = new HashSet(StringComparer.Ordinal); + foreach (var property in customCodeView.Properties) + { + memberNames.Add(property.Name); + } + + foreach (var field in customCodeView.Fields) + { + memberNames.Add(field.Name); + } + + foreach (var method in customCodeView.Methods) + { + memberNames.Add(method.Signature.Name); + } + + return memberNames; + } + + private static HashSet GetApiBaselineGeneratedTypeRoots(HashSet generatedTypeNames) + { + var roots = new HashSet(StringComparer.Ordinal); + var projectDirectory = CodeModelGenerator.Instance.Configuration.ProjectDirectory; + if (string.IsNullOrEmpty(projectDirectory)) + { + return roots; + } + + var apiDirectory = Path.GetFullPath(Path.Combine(projectDirectory, "..", "api")); + if (!Directory.Exists(apiDirectory)) + { + return roots; + } + + var apiText = string.Join("\n", Directory.GetFiles(apiDirectory, "*.cs", SearchOption.AllDirectories).Select(File.ReadAllText)); + var apiDeclaredTypeNames = GetApiDeclaredTypeNames(apiText); + foreach (var fullName in generatedTypeNames) + { + var simpleName = StripGenericArity(GetSimpleName(fullName)); + var normalizedFullName = StripGenericArity(fullName); + if (!ContainsApiTypeReference(apiText, apiDeclaredTypeNames, normalizedFullName, simpleName)) + { + continue; + } + + roots.Add(fullName); + } + + return roots; + } + + private static HashSet GetApiDeclaredTypeNames(string apiText) + { + var declaredTypeNames = new HashSet(StringComparer.Ordinal); + string? currentNamespace = null; + foreach (var line in apiText.Split('\n')) + { + var namespaceMatch = Regex.Match(line, @"^namespace\s+([\w.]+)\s*\{?\s*$"); + if (namespaceMatch.Success) + { + currentNamespace = namespaceMatch.Groups[1].Value; + continue; + } + + if (currentNamespace == null) + { + continue; + } + + var declarationMatch = Regex.Match(line, @"^ \S.*?\b(class|struct|interface|enum)\s+([A-Za-z_][A-Za-z0-9_]*)(?!\s*<)(?!\w)"); + if (declarationMatch.Success) + { + declaredTypeNames.Add($"{currentNamespace}.{declarationMatch.Groups[2].Value}"); + } + } + + return declaredTypeNames; + } + + private static bool ContainsApiTypeReference(string apiText, HashSet apiDeclaredTypeNames, string fullName, string simpleName) + { + var fullNamePattern = $@"(? GetCustomCodeInternalGeneratedTypeDeclarations(IReadOnlyList providers, HashSet generatedTypeNames) + { + var declarations = new HashSet(StringComparer.Ordinal); + foreach (var customCodeView in GetCustomCodeViews(providers)) + { + if (!customCodeView.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)) + { + continue; + } + + if (customCodeView is NamedTypeSymbolProvider namedTypeSymbolProvider) + { + AddExactMetadataNameMatch(declarations, namedTypeSymbolProvider.MetadataName, generatedTypeNames); + } + else + { + AddTypeReference(declarations, customCodeView.Type, generatedTypeNames); + } + } + + return declarations; + } + + private static void AddExactMetadataNameMatch(HashSet target, string metadataName, HashSet generatedTypeNames) + { + var normalizedName = NormalizeMetadataTypeName(metadataName); + if (!string.IsNullOrEmpty(normalizedName) && generatedTypeNames.Contains(normalizedName)) + { + target.Add(normalizedName); + } + } + + private static string NormalizeMetadataTypeName(string metadataName) + { + var arrayIndex = metadataName.IndexOf('[', StringComparison.Ordinal); + if (arrayIndex > 0) + { + metadataName = metadataName.Substring(0, arrayIndex); + } + + return metadataName; + } + + private static HashSet GetGeneratedPersistableModelProxyTypeNames(IReadOnlyList providers, HashSet generatedTypeNames) + { + var proxyTypes = new HashSet(StringComparer.Ordinal); + foreach (var provider in GetGeneratedProviders(providers)) + { + if (provider.Attributes.Any(static attribute => IsAttributeNamed(attribute, "PersistableModelProxy"))) + { + AddTypeReference(proxyTypes, provider.Type, generatedTypeNames); + } + } + + return proxyTypes; + } + + private static HashSet GetGeneratedInternalTypeDeclarations(IReadOnlyList providers, HashSet generatedTypeNames) + { + var declarations = GetGeneratedTypeDeclarationsByLastContractAccessibility(providers, generatedTypeNames, TypeSignatureModifiers.Internal); + declarations.UnionWith(GetGeneratedTypeDeclarationsByExistingSourceAccessibility(providers, generatedTypeNames, TypeSignatureModifiers.Internal)); + return declarations; + } + + private static HashSet GetGeneratedPublicTypeDeclarations(IReadOnlyList providers, HashSet generatedTypeNames) + => GetGeneratedTypeDeclarationsByLastContractAccessibility(providers, generatedTypeNames, TypeSignatureModifiers.Public); + + private static HashSet GetGeneratedPublicTypeDeclarationsFromLastContract(IReadOnlyList providers, HashSet generatedTypeNames) + => HasApiBaselineDirectory() + ? new HashSet(StringComparer.Ordinal) + : GetGeneratedPublicTypeDeclarations(providers, generatedTypeNames); + + private static HashSet GetGeneratedTypeDeclarationsByLastContractAccessibility( + IReadOnlyList providers, + HashSet generatedTypeNames, + TypeSignatureModifiers accessibility) + { + var declarations = new HashSet(StringComparer.Ordinal); + foreach (var provider in GetGeneratedProviders(providers)) + { + var lastContractView = CodeModelGenerator.Instance.SourceInputModel.FindForTypeInLastContractIncludingInternal( + provider.Type.Namespace, + provider.Type.Name, + provider.DeclaringTypeProvider?.Type.Name); + if (lastContractView?.DeclarationModifiers.HasFlag(accessibility) != true) + { + continue; + } + + AddTypeReference(declarations, provider.Type, generatedTypeNames); + } + + return declarations; + } + + private static HashSet GetGeneratedTypeDeclarationsByExistingSourceAccessibility( + IReadOnlyList providers, + HashSet generatedTypeNames, + TypeSignatureModifiers accessibility) + { + var projectDirectory = CodeModelGenerator.Instance.Configuration.ProjectDirectory; + if (string.IsNullOrEmpty(projectDirectory)) + { + return []; + } + + var declarations = new HashSet(StringComparer.Ordinal); + foreach (var provider in GetGeneratedProviders(providers)) + { + if (!generatedTypeNames.Contains(GetProviderTypeName(provider.Type)) || + !ExistingSourceHasAccessibility(projectDirectory, provider, accessibility)) + { + continue; + } + + AddTypeReference(declarations, provider.Type, generatedTypeNames); + } + + return declarations; + } + + private static bool ExistingSourceHasAccessibility(string projectDirectory, TypeProvider provider, TypeSignatureModifiers accessibility) + { + var relativeFilePath = provider.DeclaringTypeProvider?.RelativeFilePath ?? provider.RelativeFilePath; + var path = Path.GetFullPath(Path.Combine(projectDirectory, relativeFilePath)); + if (!File.Exists(path)) + { + return false; + } + + var root = CSharpSyntaxTree.ParseText(File.ReadAllText(path)).GetCompilationUnitRoot(); + foreach (var declaration in root.DescendantNodes().OfType()) + { + if (!string.Equals(declaration.Identifier.ValueText, provider.Type.Name, StringComparison.Ordinal) || + !IsDeclaringTypeMatch(declaration, provider.DeclaringTypeProvider)) + { + continue; + } + + return HasAccessibility(declaration.Modifiers, accessibility); + } + + return false; + } + + private static bool IsDeclaringTypeMatch(BaseTypeDeclarationSyntax declaration, TypeProvider? declaringTypeProvider) + { + var declaringType = declaration.Parent as BaseTypeDeclarationSyntax; + if (declaringTypeProvider == null) + { + return declaringType == null; + } + + return declaringType != null && + string.Equals(declaringType.Identifier.ValueText, declaringTypeProvider.Type.Name, StringComparison.Ordinal); + } + + private static bool HasAccessibility(SyntaxTokenList modifiers, TypeSignatureModifiers accessibility) => + accessibility switch + { + TypeSignatureModifiers.Internal => modifiers.Any(SyntaxKind.InternalKeyword), + TypeSignatureModifiers.Public => modifiers.Any(SyntaxKind.PublicKeyword), + _ => false + }; + + private static HashSet GetGeneratedImplementationInternalTypeDeclarations( + IReadOnlyList providers, + HashSet generatedInternalDeclarations) + { + var implementationDeclarations = new HashSet(StringComparer.Ordinal); + foreach (var provider in GetGeneratedProviders(providers)) + { + if (!provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Static)) + { + continue; + } + + AddTypeReference(implementationDeclarations, provider.Type, generatedInternalDeclarations); + } + + foreach (var name in generatedInternalDeclarations) + { + if (GetSimpleName(name).StartsWith("Internal", StringComparison.Ordinal)) + { + implementationDeclarations.Add(name); + } + } + + return implementationDeclarations; + } + + private static bool IsGeneratedInternalImplementation(TypeProvider provider) + => provider.RelativeFilePath.Contains( + $"{Path.DirectorySeparatorChar}Generated{Path.DirectorySeparatorChar}Internal{Path.DirectorySeparatorChar}", + StringComparison.Ordinal); + + private static HashSet GetSimpleNames(HashSet names) + { + var simpleNames = new HashSet(StringComparer.Ordinal); + foreach (var name in names) + { + simpleNames.Add(GetSimpleName(name)); + } + + return simpleNames; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.GraphBuilder.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.GraphBuilder.cs new file mode 100644 index 00000000000..0d2536e18c1 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.GraphBuilder.cs @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; + +namespace Microsoft.TypeSpec.Generator +{ + internal static partial class ProviderReferenceMapAnalyzer + { + private static ProviderReferenceGraph BuildGraph(IReadOnlyList generatedProviders, bool publicOnly = false) + { + // Each generated provider becomes a node, and provider metadata supplies the edges: + // inheritance, signatures, properties, fields, nested/serialization providers, attributes, + // and selected implementation dependencies. This avoids parsing generated C# just to + // rediscover generated-to-generated references. + var serializationProviderNamesByType = GetSerializationProviderNamesByType(generatedProviders); + IReadOnlyDictionary? serializationReferenceNamesByType = publicOnly ? null : serializationProviderNamesByType; + var nodes = new HashSet(StringComparer.Ordinal); + var references = new Dictionary>(StringComparer.Ordinal); + foreach (var provider in generatedProviders) + { + var providerName = GetProviderTypeName(provider.Type); + if (nodes.Add(providerName)) + { + references.Add(providerName, new HashSet(StringComparer.Ordinal)); + } + } + + foreach (var provider in generatedProviders) + { + var current = GetProviderTypeName(provider.Type); + AddTypeReference(references[current], provider.Type, nodes, serializationReferenceNamesByType); + AddTypeReference(references[current], provider.BaseType, nodes, serializationReferenceNamesByType); + AddTypeReference(references[current], provider.DeclaringTypeProvider?.Type, nodes, serializationReferenceNamesByType); + + if (publicOnly && !provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) + { + continue; + } + + if (!publicOnly && IsKept(provider.Type, CodeModelGenerator.Instance.NonRootTypes, nodes)) + { + continue; + } + + // Model factory signatures mention many models. The existing Roslyn post-processor + // removes factory methods for unreachable models, so model factory should only + // contribute helper dependencies, not model reachability edges. + if (IsModelFactoryProvider(provider)) + { + continue; + } + + foreach (var implementedType in provider.Implements) + { + AddTypeReference(references[current], implementedType, nodes, serializationReferenceNamesByType); + } + + if (!publicOnly) + { + foreach (var nestedType in provider.NestedTypes) + { + AddTypeReference(references[current], nestedType.Type, nodes, serializationReferenceNamesByType); + } + } + + if (!publicOnly) + { + foreach (var serializationProvider in provider.SerializationProviders) + { + AddTypeReference(references[current], serializationProvider.Type, nodes, serializationReferenceNamesByType); + } + } + + foreach (var property in provider.Properties) + { + if (publicOnly && !IsPublic(property.Modifiers)) + { + continue; + } + + AddTypeReference(references[current], property.Type, nodes, serializationReferenceNamesByType); + AddTypeReference(references[current], property.ExplicitInterface, nodes, serializationReferenceNamesByType); + if (!publicOnly) + { + AddAttributes(references[current], property.Attributes, nodes, serializationReferenceNamesByType, includeArguments: false); + } + } + + foreach (var field in provider.Fields) + { + if (publicOnly && !field.Modifiers.HasFlag(FieldModifiers.Public)) + { + continue; + } + + AddTypeReference(references[current], field.Type, nodes, serializationReferenceNamesByType); + if (!publicOnly) + { + AddAttributes(references[current], field.Attributes, nodes, serializationReferenceNamesByType, includeArguments: false); + } + } + + foreach (var constructor in provider.Constructors) + { + if (publicOnly && !IsPublic(constructor.Signature.Modifiers)) + { + continue; + } + + AddSignatureReferences(references[current], constructor.Signature, nodes, serializationReferenceNamesByType, includeAttributes: !publicOnly, includeAttributeArguments: false); + } + + foreach (var method in provider.Methods) + { + if (method.IsMethodSuppressed()) + { + continue; + } + + if (publicOnly && !IsPublic(method.Signature.Modifiers)) + { + continue; + } + + AddSignatureReferences(references[current], method.Signature, nodes, serializationReferenceNamesByType, includeAttributes: !publicOnly, includeAttributeArguments: false); + if (!publicOnly) + { + AddTypeReference(references[current], GetCollectionDefinitionType(method), nodes, serializationReferenceNamesByType); + } + } + } + + return new ProviderReferenceGraph(nodes, references); + } + + private static Dictionary GetSerializationProviderNamesByType(IReadOnlyList generatedProviders) + { + var namesByType = new Dictionary>(StringComparer.Ordinal); + foreach (var provider in generatedProviders) + { + if (provider.SerializationProviders.Count == 0) + { + continue; + } + + var providerName = GetProviderTypeName(provider.Type); + if (!namesByType.TryGetValue(providerName, out var serializationProviderNames)) + { + serializationProviderNames = new HashSet(StringComparer.Ordinal); + namesByType.Add(providerName, serializationProviderNames); + } + + foreach (var serializationProvider in provider.SerializationProviders) + { + serializationProviderNames.Add(GetProviderTypeName(serializationProvider.Type)); + } + } + + var result = new Dictionary(StringComparer.Ordinal); + foreach (var (providerName, serializationProviderNames) in namesByType) + { + result.Add(providerName, [.. serializationProviderNames]); + } + + return result; + } + + private static CSharpType? GetCollectionDefinitionType(MethodProvider method) + { + var property = method.GetType().GetProperty("CollectionDefinition"); + return property?.GetValue(method) is TypeProvider collectionDefinition + ? collectionDefinition.Type + : null; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.Helpers.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.Helpers.cs new file mode 100644 index 00000000000..e07ee8960a2 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.Helpers.cs @@ -0,0 +1,617 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; + +namespace Microsoft.TypeSpec.Generator +{ + internal static partial class ProviderReferenceMapAnalyzer + { + private static bool IsKept(CSharpType type, HashSet roots, HashSet nodes) + { + var providerName = GetProviderTypeName(type); + return IsKeptName(providerName, type.Name, roots, nodes); + } + + private static bool IsKeptName(string providerName, HashSet roots, HashSet nodes) + { + return IsKeptName(providerName, StripGenericArity(GetSimpleName(providerName)), roots, nodes); + } + + private static bool IsKeptName(string providerName, string simpleName, HashSet roots, HashSet nodes) + { + if (roots.Contains(providerName) && nodes.Contains(providerName)) + { + return true; + } + + if (!roots.Contains(simpleName)) + { + return false; + } + + var simpleNameLookup = _simpleNameLookupCache.GetValue(nodes, BuildSimpleNameLookup); + return simpleNameLookup.TryGetValue(simpleName, out var matches) && + matches.Length == 1 && + string.Equals(matches[0], providerName, StringComparison.Ordinal); + } + + private static bool IsClientProviderRoot(TypeProvider provider, bool publicOnly) => + IsClientProvider(provider) && + (!publicOnly || !HasApiBaselineDirectory() && provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + + private static bool IsClientProvider(TypeProvider provider) + { + for (var type = provider.GetType(); type != null && type != typeof(TypeProvider); type = type.BaseType) + { + if (string.Equals(type.Name, "ClientProvider", StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + + private static bool IsAdditionalRootProvider(TypeProvider provider, HashSet roots, HashSet nodes) + { + if (provider.DeclaringTypeProvider != null || !IsKept(provider.Type, roots, nodes)) + { + return false; + } + + return provider is not ModelProvider && provider is not EnumProvider; + } + + private static bool HasApiBaselineDirectory() + { + var projectDirectory = CodeModelGenerator.Instance.Configuration.ProjectDirectory; + if (string.IsNullOrEmpty(projectDirectory)) + { + return false; + } + + var directory = new DirectoryInfo(Path.GetFullPath(projectDirectory)); + while (directory != null) + { + if (Directory.Exists(Path.Combine(directory.FullName, "api")) || + directory.Parent != null && Directory.Exists(Path.Combine(directory.Parent.FullName, "api"))) + { + return true; + } + + directory = directory.Parent; + } + + return false; + } + + private static bool IsModelFactoryProvider(TypeProvider provider) + => provider is ModelFactoryProvider; + + private static IEnumerable GetExistingGeneratedHelperRoots( + IReadOnlyList generatedProviders, + HashSet generatedInternalDeclarations) + { + var helperNames = new HashSet(StringComparer.Ordinal); + foreach (var provider in GetGeneratedProviders(generatedProviders)) + { + if (!IsGeneratedInternalHelperDeclaration(provider)) + { + continue; + } + + var name = GetProviderTypeName(provider.Type); + if (generatedInternalDeclarations.Contains(name)) + { + helperNames.Add(name); + } + } + + AddSiblingExtensionRoot(helperNames, generatedInternalDeclarations, "CancellationTokenExtensions", "RequestContextExtensions"); + + return helperNames; + } + + private static bool IsGeneratedInternalHelperDeclaration(TypeProvider provider) + { + if (provider is ModelProvider || + provider is EnumProvider || + IsModelFactoryProvider(provider) || + provider.DeclaringTypeProvider != null || + provider.SerializationProviders.Count > 0) + { + return false; + } + + return !string.Equals(provider.RelativeFilePath, Path.Combine("src", "Generated", "Models", $"{provider.Name}.cs"), StringComparison.Ordinal); + } + + private static HashSet GetHelperRootNames( + IReadOnlyList providers, + HashSet nodes, + HashSet reachableTypes, + IReadOnlyDictionary>? references = null) + { + var roots = new HashSet(StringComparer.Ordinal); + foreach (var provider in GetGeneratedProviders(providers)) + { + var providerName = GetProviderTypeName(provider.Type); + var isModelFactory = IsModelFactoryProvider(provider); + if (!reachableTypes.Contains(providerName) && !isModelFactory) + { + continue; + } + + AddHelperDependencies(roots, provider.HelperDependencyTypes, nodes, references == null ? null : references[providerName]); + + foreach (var property in provider.Properties) + { + AddInitializationHelperRoot(roots, property.Type, nodes); + AddParameterValidationHelperRoot(roots, property.AsParameter, nodes); + } + + foreach (var field in provider.Fields) + { + AddParameterValidationHelperRoot(roots, field.AsParameter, nodes); + } + + foreach (var constructor in provider.Constructors) + { + foreach (var parameter in constructor.Signature.Parameters) + { + AddParameterValidationHelperRoot(roots, parameter, nodes); + } + } + + foreach (var method in provider.Methods) + { + // Only factory methods for reachable models can instantiate collection helpers. + if (isModelFactory && + (method.Signature.ReturnType == null || !reachableTypes.Contains(GetProviderTypeName(method.Signature.ReturnType)))) + { + continue; + } + + foreach (var parameter in method.Signature.Parameters) + { + AddParameterValidationHelperRoot(roots, parameter, nodes); + if (isModelFactory) + { + AddModelFactoryCollectionInitializationHelperRoot(roots, parameter.Type, nodes); + } + } + } + } + + AddSiblingExtensionRoot(roots, nodes, "CancellationTokenExtensions", "RequestContextExtensions"); + return roots; + } + + private static void AddSiblingExtensionRoot(HashSet roots, HashSet nodes, string sourceName, string siblingName) + { + var siblingRoots = new List(); + foreach (var root in roots) + { + if (!string.Equals(GetSimpleName(root), sourceName, StringComparison.Ordinal)) + { + continue; + } + + var namespaceName = GetNamespaceName(root); + if (namespaceName == null) + { + continue; + } + + var sibling = $"{namespaceName}.{siblingName}"; + if (nodes.Contains(sibling)) + { + siblingRoots.Add(sibling); + } + } + + roots.UnionWith(siblingRoots); + } + + private static void AddParameterValidationHelperRoot(HashSet roots, ParameterProvider parameter, HashSet nodes) + { + if (parameter.Validation != ParameterValidationType.None) + { + AddMatchingName(roots, "Argument", nodes); + } + } + + private static void AddHelperDependencies( + HashSet roots, + IReadOnlyList dependencies, + HashSet nodes, + HashSet? referencedNames) + { + foreach (var dependency in dependencies) + { + if (referencedNames == null) + { + AddTypeReference(roots, dependency, nodes); + continue; + } + + var matches = new HashSet(StringComparer.Ordinal); + AddTypeReference(matches, dependency, nodes); + foreach (var match in matches) + { + if (referencedNames.Contains(match)) + { + roots.Add(match); + } + } + } + } + + private static void RemoveUnusedRequestHeaderExtensionsRoot( + HashSet roots, + IReadOnlyDictionary> references, + IReadOnlyList providers) + { + var hasCustomReference = HasCustomRequestHeaderExtensionsReference(providers); + if (hasCustomReference) + { + return; + } + + var unusedRequestHeaderExtensions = new List(); + foreach (var root in roots) + { + if (IsRequestHeadersExtensionsRoot(root) && + !HasExternalReference(root, references)) + { + unusedRequestHeaderExtensions.Add(root); + } + } + + roots.ExceptWith(unusedRequestHeaderExtensions); + } + + private static bool HasExternalReference(string root, IReadOnlyDictionary> references) + { + foreach (var (source, sourceReferences) in references) + { + if (!string.Equals(source, root, StringComparison.Ordinal) && + sourceReferences.Contains(root)) + { + return true; + } + } + + return false; + } + + private static bool IsRequestHeadersExtensionsRoot(string root) => + root.EndsWith(".RequestHeaderExtensions", StringComparison.Ordinal) || + root.EndsWith(".RequestHeadersExtensions", StringComparison.Ordinal); + + private static bool HasCustomRequestHeaderExtensionsReference(IReadOnlyList providers) + { + foreach (var customCodeView in GetCustomCodeViews(providers)) + { + if (customCodeView is NamedTypeSymbolProvider) + { + if (HasRequestHeaderExtensionsDependency(customCodeView.HelperDependencyTypes) || + HasRequestHeaderExtensionsDependency(customCodeView.BodyDependencyTypes) || + HasRequestHeaderExtensionsDependency(customCodeView.SignatureDependencyTypes)) + { + return true; + } + + continue; + } + + if (HasRequestHeaderExtensionsDependency(customCodeView.HelperDependencyTypes) || + HasRequestHeaderExtensionsDependency(customCodeView.BodyDependencyTypes) || + HasRequestHeaderExtensionsMethodDependency(customCodeView.Methods) || + HasRequestHeaderExtensionsPropertyDependency(customCodeView.Properties) || + HasRequestHeaderExtensionsFieldDependency(customCodeView.Fields)) + { + return true; + } + } + + return false; + } + + private static bool HasRequestHeaderExtensionsDependency(IEnumerable dependencies) + { + foreach (var dependency in dependencies) + { + if (IsRequestHeaderExtensionsDependency(dependency)) + { + return true; + } + } + + return false; + } + + private static bool HasRequestHeaderExtensionsMethodDependency(IReadOnlyList methods) + { + foreach (var method in methods) + { + if (IsRequestHeaderExtensionsDependency(method.Signature.ReturnType)) + { + return true; + } + + foreach (var parameter in method.Signature.Parameters) + { + if (IsRequestHeaderExtensionsDependency(parameter.Type)) + { + return true; + } + } + } + + return false; + } + + private static bool HasRequestHeaderExtensionsPropertyDependency(IReadOnlyList properties) + { + foreach (var property in properties) + { + if (IsRequestHeaderExtensionsDependency(property.Type)) + { + return true; + } + } + + return false; + } + + private static bool HasRequestHeaderExtensionsFieldDependency(IReadOnlyList fields) + { + foreach (var field in fields) + { + if (IsRequestHeaderExtensionsDependency(field.Type)) + { + return true; + } + } + + return false; + } + + private static bool IsRequestHeaderExtensionsDependency(string name) + => string.Equals(name, "RequestHeaderExtensions", StringComparison.Ordinal) || + string.Equals(name, "SetDelimited", StringComparison.Ordinal); + + private static bool IsRequestHeaderExtensionsDependency(CSharpType? type) + { + if (type == null) + { + return false; + } + + if (IsRequestHeaderExtensionsDependency(type.Name)) + { + return true; + } + + foreach (var argument in type.Arguments) + { + if (IsRequestHeaderExtensionsDependency(argument)) + { + return true; + } + } + + return false; + } + + private static void AddInitializationHelperRoot(HashSet roots, CSharpType? type, HashSet nodes) + { + if (type == null) + { + return; + } + + var initializationType = type.PropertyInitializationType; + if (!string.Equals(initializationType.FullyQualifiedName, type.FullyQualifiedName, StringComparison.Ordinal)) + { + AddMatchingName(roots, initializationType.Name, nodes); + } + + if (type is { IsList: true, IsReadOnlyMemory: false }) + { + AddTypeReference(roots, CodeModelGenerator.Instance.TypeFactory.ListInitializationType, nodes); + } + + if (type.IsDictionary) + { + AddTypeReference(roots, CodeModelGenerator.Instance.TypeFactory.DictionaryInitializationType, nodes); + } + + foreach (var argument in type.Arguments) + { + AddInitializationHelperRoot(roots, argument, nodes); + } + } + + private static void AddModelFactoryCollectionInitializationHelperRoot(HashSet roots, CSharpType? type, HashSet nodes) + { + if (type == null) + { + return; + } + + if (type is { IsList: true, IsReadOnlyMemory: false }) + { + AddTypeReference(roots, CodeModelGenerator.Instance.TypeFactory.ListInitializationType, nodes); + } + + if (type.IsDictionary) + { + AddTypeReference(roots, CodeModelGenerator.Instance.TypeFactory.DictionaryInitializationType, nodes); + } + + foreach (var argument in type.Arguments) + { + AddModelFactoryCollectionInitializationHelperRoot(roots, argument, nodes); + } + } + + private static void AddMatchingName(HashSet target, string name, HashSet nodes) + { + if (nodes.Contains(name)) + { + target.Add(name); + return; + } + + var simpleNameLookup = _simpleNameLookupCache.GetValue(nodes, BuildSimpleNameLookup); + if (!simpleNameLookup.TryGetValue(name, out var matches)) + { + return; + } + + foreach (var match in matches) + { + target.Add(match); + } + } + + private static void AddUnambiguousMatchingName(HashSet target, string name, HashSet nodes) + { + if (nodes.Contains(name)) + { + target.Add(name); + return; + } + + var simpleNameLookup = _simpleNameLookupCache.GetValue(nodes, BuildSimpleNameLookup); + if (simpleNameLookup.TryGetValue(name, out var matches) && matches.Length == 1) + { + target.Add(matches[0]); + } + } + + private static void AddMatchingNamesWithSimpleNameSuffix(HashSet target, string suffix, HashSet nodes) + { + foreach (var node in nodes) + { + if (GetSimpleName(node).EndsWith(suffix, StringComparison.Ordinal)) + { + target.Add(node); + } + } + } + + private static Dictionary BuildSimpleNameLookup(HashSet nodes) + { + var lookup = new Dictionary>(StringComparer.Ordinal); + foreach (var node in nodes) + { + if (IsNestedNode(node, nodes)) + { + continue; + } + + var simpleName = StripGenericArity(GetSimpleName(node)); + if (!lookup.TryGetValue(simpleName, out var matchingNodes)) + { + matchingNodes = []; + lookup.Add(simpleName, matchingNodes); + } + + matchingNodes.Add(node); + } + + var result = new Dictionary(StringComparer.Ordinal); + foreach (var (simpleName, matchingNodes) in lookup) + { + result.Add(simpleName, [.. matchingNodes]); + } + + return result; + } + + private static bool IsNestedNode(string node, HashSet nodes) + { + var dotIndex = node.IndexOf('.'); + while (dotIndex >= 0) + { + if (nodes.Contains(node.Substring(0, dotIndex))) + { + return true; + } + + dotIndex = node.IndexOf('.', dotIndex + 1); + } + + return false; + } + + private static HashSet GetReachableTypes(HashSet roots, IReadOnlyDictionary> references) + { + return GetReachableTypes(roots, references, expandableNodes: null); + } + + private static HashSet GetReachableTypes( + HashSet roots, + IReadOnlyDictionary> references, + HashSet? expandableNodes) + { + var reachable = new HashSet(StringComparer.Ordinal); + var queue = new Queue(roots); + while (queue.Count > 0) + { + var current = queue.Dequeue(); + if (!reachable.Add(current)) + { + continue; + } + + if (expandableNodes != null && !expandableNodes.Contains(current)) + { + continue; + } + + if (!references.TryGetValue(current, out var children)) + { + continue; + } + + foreach (var child in children) + { + queue.Enqueue(child); + } + } + + return reachable; + } + + private static bool HasPublicApiPredecessor( + string name, + IReadOnlyDictionary> references, + HashSet publicizeReachable, + HashSet generatedImplementationInternalDeclarations) + { + foreach (var (owner, children) in references) + { + if (!publicizeReachable.Contains(owner) || + string.Equals(owner, name, StringComparison.Ordinal) || + generatedImplementationInternalDeclarations.Contains(owner) || + !children.Contains(name)) + { + continue; + } + + return true; + } + + return false; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.ReferenceTraversal.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.ReferenceTraversal.cs new file mode 100644 index 00000000000..5674657f48d --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.ReferenceTraversal.cs @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; + +namespace Microsoft.TypeSpec.Generator +{ + internal static partial class ProviderReferenceMapAnalyzer + { + private static Dictionary> CloneReferences(IReadOnlyDictionary> references) + { + var clone = new Dictionary>(StringComparer.Ordinal); + foreach (var (name, referencedNames) in references) + { + clone.Add(name, new HashSet(referencedNames, StringComparer.Ordinal)); + } + + return clone; + } + + private static void AddDerivedModelReferences( + IReadOnlyList providers, + HashSet nodes, + Dictionary> references, + HashSet publicBaseModels, + HashSet generatedDiscriminatorBaseNames) + { + var modelProviders = new List(); + var discriminatorProviders = new List(); + var discriminatorBaseNames = new HashSet(StringComparer.Ordinal); + foreach (var provider in providers) + { + if (provider is not ModelProvider modelProvider || + !modelProvider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) + { + continue; + } + + modelProviders.Add(modelProvider); + + if (modelProvider.DiscriminatorProperty != null) + { + discriminatorBaseNames.Add(GetProviderTypeName(modelProvider.Type)); + } + + if (!modelProvider.IsUnknownDiscriminatorModel && + (modelProvider.DiscriminatorProperty != null || modelProvider.DiscriminatorValue != null)) + { + discriminatorProviders.Add(modelProvider); + } + } + + discriminatorBaseNames.UnionWith(generatedDiscriminatorBaseNames); + var addedReference = true; + while (addedReference) + { + addedReference = false; + foreach (var provider in discriminatorProviders) + { + var providerName = GetProviderTypeName(provider.Type); + if (!nodes.Contains(providerName) || + !publicBaseModels.Contains(providerName)) + { + continue; + } + + foreach (var derivedModel in provider.DerivedModels) + { + if (derivedModel.IsUnknownDiscriminatorModel || + !derivedModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) + { + continue; + } + + var before = references[providerName].Count; + AddTypeReference(references[providerName], derivedModel.Type, nodes); + var derivedName = GetProviderTypeName(derivedModel.Type); + if (nodes.Contains(derivedName) && publicBaseModels.Add(derivedName) || references[providerName].Count != before) + { + addedReference = true; + } + } + } + + foreach (var provider in modelProviders) + { + if (provider.IsUnknownDiscriminatorModel || + !provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) + { + continue; + } + + var providerName = GetProviderTypeName(provider.Type); + if (!nodes.Contains(providerName)) + { + continue; + } + + var baseTypeName = provider.BaseType == null ? null : GetProviderTypeName(provider.BaseType); + if (baseTypeName == null || + !discriminatorBaseNames.Contains(baseTypeName) || + !nodes.Contains(baseTypeName) || + !publicBaseModels.Contains(baseTypeName)) + { + continue; + } + + var before = references[baseTypeName].Count; + references[baseTypeName].Add(providerName); + if (publicBaseModels.Add(providerName) || references[baseTypeName].Count != before) + { + addedReference = true; + } + } + } + } + + private static void AddBasePreservedReferences( + IReadOnlyList providers, + HashSet nodes, + IReadOnlyDictionary> references, + HashSet reachableTypes) + { + var basePreservedRoots = new HashSet(StringComparer.Ordinal); + var addedRoot = true; + while (addedRoot) + { + addedRoot = false; + foreach (var provider in GetGeneratedProviders(providers)) + { + var providerName = GetProviderTypeName(provider.Type); + if (!nodes.Contains(providerName) || reachableTypes.Contains(providerName) || basePreservedRoots.Contains(providerName)) + { + continue; + } + + var baseTypeName = provider.BaseType == null ? null : GetProviderTypeName(provider.BaseType); + if (baseTypeName == null || !reachableTypes.Contains(baseTypeName)) + { + continue; + } + + if (basePreservedRoots.Add(providerName)) + { + addedRoot = true; + } + } + + if (addedRoot) + { + reachableTypes.UnionWith(GetReachableTypes(basePreservedRoots, references)); + } + } + } + + private static IReadOnlyList GetGeneratedProviders(IReadOnlyList providers) + { + var generatedProviders = new List(); + foreach (var provider in providers) + { + AddGeneratedProvider(generatedProviders, provider); + } + + return generatedProviders; + } + + private static void AddGeneratedProvider(List generatedProviders, TypeProvider provider) + { + generatedProviders.Add(provider); + foreach (var nestedType in provider.NestedTypes) + { + AddGeneratedProvider(generatedProviders, nestedType); + } + + foreach (var serializationProvider in provider.SerializationProviders) + { + AddGeneratedProvider(generatedProviders, serializationProvider); + } + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.RootSelection.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.RootSelection.cs new file mode 100644 index 00000000000..e56982cf089 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.RootSelection.cs @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; + +namespace Microsoft.TypeSpec.Generator +{ + internal static partial class ProviderReferenceMapAnalyzer + { + private static HashSet GetRootNames( + IReadOnlyList providers, + HashSet nodes, + HashSet helperRoots, + bool includeModelFactory, + bool includeAdditionalRoots, + bool includeUnionVariantRoots, + bool includeModelFactorySignatureRoots, + bool publicClientRootsOnly) + { + var generator = CodeModelGenerator.Instance; + var roots = new HashSet(StringComparer.Ordinal); + var modelFactoryName = GetProviderTypeName(generator.OutputLibrary.ModelFactory.Value.Type); + + foreach (var provider in providers) + { + var name = GetProviderTypeName(provider.Type); + if (IsClientProviderRoot(provider, publicClientRootsOnly) || + includeAdditionalRoots && IsAdditionalRootProvider(provider, generator.AdditionalRootTypes, nodes) || + includeModelFactory && string.Equals(name, modelFactoryName, StringComparison.Ordinal) || + includeModelFactory && helperRoots.Contains(name)) + { + roots.Add(name); + } + } + + if (includeModelFactorySignatureRoots) + { + AddLastContractModelFactorySignatureRoots(providers, roots, nodes); + } + + if (!includeUnionVariantRoots) + { + return roots; + } + + AddUnionVariantRoots(roots, providers, nodes); + + return roots; + } + + private static void AddLastContractModelFactorySignatureRoots(IReadOnlyList providers, HashSet roots, HashSet nodes) + { + foreach (var provider in providers) + { + if (!IsModelFactoryProvider(provider)) + { + continue; + } + + foreach (var method in provider.LastContractView?.Methods ?? []) + { + if (!method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public) || + IsImplementationOnlyModelFactoryMethod(method)) + { + continue; + } + + AddTypeReference(roots, method.Signature.ReturnType, nodes); + foreach (var parameter in method.Signature.Parameters) + { + AddTypeReference(roots, parameter.Type, nodes); + } + } + } + } + + private static void AddUnionVariantRoots(HashSet roots, IReadOnlyList providers, HashSet nodes) + { + var unionVariantTypesToKeep = CodeModelGenerator.Instance.TypeFactory.UnionVariantTypesToKeep; + foreach (var provider in GetGeneratedProviders(providers)) + { + if (provider is not ModelProvider || + !unionVariantTypesToKeep.Contains(provider.Type.Name) || + string.Equals(provider.Type.Namespace, "TypeSpec.Http", StringComparison.Ordinal)) + { + continue; + } + + AddMatchingName(roots, GetProviderTypeName(provider.Type), nodes); + } + } + + private static bool ShouldUseUnionVariantFallbackRoots() => + !HasApiBaselineDirectory() && + CodeModelGenerator.Instance.SourceInputModel.LastContract == null; + + private static bool IsImplementationOnlyModelFactoryMethod(MethodProvider method) + { + var returnType = method.Signature.ReturnType; + if (returnType == null) + { + return true; + } + + var returnTypeName = GetSimpleName(GetProviderTypeName(returnType)); + return returnTypeName.StartsWith("Paged", StringComparison.Ordinal) || + returnTypeName.EndsWith("Request", StringComparison.Ordinal); + } + + private static void RemoveMethodsFromModelFactory(HashSet namesToRemove) + { + if (namesToRemove.Count == 0) + { + return; + } + + var modelFactory = CodeModelGenerator.Instance.OutputLibrary.ModelFactory.Value; + _preWriteModelFactory = modelFactory; + _preWriteModelFactoryMethods ??= [.. modelFactory.Methods]; + var removedLeadingMethod = false; + var methodsToKeep = new List(); + for (int i = 0; i < modelFactory.Methods.Count; i++) + { + var method = modelFactory.Methods[i]; + if (!namesToRemove.Contains(method.Signature.Name)) + { + methodsToKeep.Add(method); + continue; + } + + if (i == 0) + { + removedLeadingMethod = true; + } + } + + if (removedLeadingMethod && methodsToKeep.Count > 0) + { + modelFactory.PreserveLeadingMethodSeparatorForBackCompat(); + } + + modelFactory.Update(methods: methodsToKeep); + } + + private static HashSet GetPostProcessorDeclaredNodes(IReadOnlyList providers, HashSet nodes, bool publicOnly) + { + var generator = CodeModelGenerator.Instance; + var declaredNodes = new HashSet(StringComparer.Ordinal); + foreach (var provider in GetGeneratedProviders(providers)) + { + if (IsModelFactoryProvider(provider)) + { + continue; + } + + if (publicOnly && !provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) + { + continue; + } + + var name = GetProviderTypeName(provider.Type); + if (!nodes.Contains(name)) + { + continue; + } + + declaredNodes.Add(name); + } + + return declaredNodes; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.TypeReferenceCollector.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.TypeReferenceCollector.cs new file mode 100644 index 00000000000..3833ab0350e --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.TypeReferenceCollector.cs @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Microsoft.TypeSpec.Generator.Expressions; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; +using Microsoft.TypeSpec.Generator.Statements; + +namespace Microsoft.TypeSpec.Generator +{ + internal static partial class ProviderReferenceMapAnalyzer + { + private static void AddSignatureReferences( + HashSet references, + MethodSignatureBase signature, + HashSet nodes, + IReadOnlyDictionary? serializationProviderNamesByType, + bool includeAttributes = true, + bool includeAttributeArguments = true) + { + AddTypeReference(references, signature.ReturnType, nodes, serializationProviderNamesByType); + if (includeAttributes) + { + AddAttributes(references, signature.Attributes, nodes, serializationProviderNamesByType, includeAttributeArguments); + } + + foreach (var parameter in signature.Parameters) + { + AddTypeReference(references, parameter.Type, nodes, serializationProviderNamesByType); + if (includeAttributes) + { + AddAttributes(references, parameter.Attributes, nodes, serializationProviderNamesByType, includeAttributeArguments); + } + } + + if (signature is MethodSignature methodSignature) + { + AddTypeReference(references, methodSignature.ExplicitInterface, nodes, serializationProviderNamesByType); + if (methodSignature.GenericArguments != null) + { + foreach (var genericArgument in methodSignature.GenericArguments) + { + AddTypeReference(references, genericArgument, nodes, serializationProviderNamesByType); + } + } + + if (methodSignature.GenericParameterConstraints != null) + { + foreach (var constraint in methodSignature.GenericParameterConstraints) + { + AddTypeReference(references, constraint.Type, nodes, serializationProviderNamesByType); + } + } + } + + if (signature is ConstructorSignature constructorSignature) + { + AddTypeReference(references, constructorSignature.Type, nodes, serializationProviderNamesByType); + } + } + + private static void AddAttributes( + HashSet references, + IReadOnlyList attributes, + HashSet nodes, + IReadOnlyDictionary? serializationProviderNamesByType, + bool includeArguments) + { + foreach (var attribute in attributes) + { + AddTypeReference(references, attribute.Type, nodes, serializationProviderNamesByType); + if (!includeArguments) + { + continue; + } + + foreach (var argument in attribute.Arguments) + { + AddAttributeArgumentReference(references, argument, nodes, serializationProviderNamesByType); + } + + foreach (var (_, argument) in attribute.PositionalArguments) + { + AddAttributeArgumentReference(references, argument, nodes, serializationProviderNamesByType); + } + } + } + + private static bool IsAttributeNamed(AttributeStatement attribute, string name) + => string.Equals(attribute.Type.Name, name, StringComparison.Ordinal) || + string.Equals(attribute.Type.Name, $"{name}Attribute", StringComparison.Ordinal); + + private static void AddAttributeArgumentReference( + HashSet references, + ValueExpression argument, + HashSet nodes, + IReadOnlyDictionary? serializationProviderNamesByType) + { + if (argument is TypeOfExpression typeOf) + { + AddTypeReference(references, typeOf.Type, nodes, serializationProviderNamesByType); + AddMatchingName(references, typeOf.Type.Name, nodes); + } + } + + private static void AddTypeReference( + HashSet references, + CSharpType? type, + HashSet nodes, + IReadOnlyDictionary? serializationProviderNamesByType = null) + { + if (type == null) + { + return; + } + + if (type.IsArray) + { + AddTypeReference(references, type.ElementType, nodes, serializationProviderNamesByType); + return; + } + + var providerTypeName = GetProviderTypeName(type); + if (nodes.Contains(providerTypeName)) + { + references.Add(providerTypeName); + if (serializationProviderNamesByType != null && serializationProviderNamesByType.TryGetValue(providerTypeName, out var serializationProviderNames)) + { + foreach (var serializationProviderName in serializationProviderNames) + { + references.Add(serializationProviderName); + } + } + } + + AddTypeReference(references, type.BaseType, nodes, serializationProviderNamesByType); + AddTypeReference(references, type.DeclaringType, nodes, serializationProviderNamesByType); + foreach (var argument in type.Arguments) + { + AddTypeReference(references, argument, nodes, serializationProviderNamesByType); + } + } + + private static string GetSimpleName(string fullyQualifiedName) + { + var lastDot = fullyQualifiedName.LastIndexOf('.'); + return lastDot < 0 ? fullyQualifiedName : fullyQualifiedName.Substring(lastDot + 1); + } + + private static string? GetNamespaceName(string fullyQualifiedName) + { + var lastDot = fullyQualifiedName.LastIndexOf('.'); + return lastDot < 0 ? null : fullyQualifiedName.Substring(0, lastDot); + } + + private static string GetProviderTypeName(CSharpType type) + { + var name = type.Arguments.Count > 0 && !type.Name.Contains('`', StringComparison.Ordinal) + ? $"{type.Name}`{type.Arguments.Count}" + : type.Name; + if (type.DeclaringType is { } declaringType) + { + return $"{GetProviderTypeName(declaringType)}.{name}"; + } + + return string.IsNullOrEmpty(type.Namespace) ? name : $"{type.Namespace}.{name}"; + } + + private static string StripGenericArity(string name) + { + var tick = name.IndexOf('`'); + return tick < 0 ? name : name.Substring(0, tick); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.cs new file mode 100644 index 00000000000..5a96b69a868 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.cs @@ -0,0 +1,268 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using Microsoft.TypeSpec.Generator.Expressions; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; +using Microsoft.TypeSpec.Generator.Statements; + +namespace Microsoft.TypeSpec.Generator +{ + internal static partial class ProviderReferenceMapAnalyzer + { + private static ProviderReferenceMapResult? _latestResult; + private static readonly ConditionalWeakTable, Dictionary> _simpleNameLookupCache = new(); + private static TypeProvider? _preWriteModelFactory; + private static MethodProvider[]? _preWriteModelFactoryMethods; + + public static ProviderReferenceMapResult? LatestResult => _latestResult; + public static bool PreWriteAccessibilityApplied { get; private set; } + + public static bool ShouldWriteProvider(TypeProvider provider) => + _latestResult?.RemoveCandidates.Contains(GetProviderTypeName(provider.Type)) != true; + + public static void ResetPreWriteAccessibility() + { + RestorePreWriteModelFactoryMethods(); + _latestResult = null; + PreWriteAccessibilityApplied = false; + } + + public static void ApplyPreWriteAccessibility(IReadOnlyList providers) + { + PreWriteAccessibilityApplied = false; + if (Configuration.UnreferencedTypesHandling == Configuration.UnreferencedTypesHandlingOption.KeepAll) + { + return; + } + + // Accessibility has to be adjusted before files are written. Roslyn can remove files + // later, but it cannot safely change provider declarations or model factory signatures. + var (internalizeCandidates, publicizeCandidates) = GetPreWriteAccessibilityCandidates(providers); + foreach (var provider in GetGeneratedProviders(providers)) + { + var providerName = GetProviderTypeName(provider.Type); + if (internalizeCandidates.Contains(providerName)) + { + if (provider.DeclaringTypeProvider is null) + { + provider.PreserveXmlDocs(); + } + provider.Update(modifiers: MakeInternal(provider.DeclarationModifiers)); + } + else if (publicizeCandidates.Contains(providerName) && !IsGeneratedInternalImplementation(provider)) + { + provider.Update(modifiers: MakePublic(provider.DeclarationModifiers)); + } + } + + RemoveMethodsFromModelFactory(GetSimpleNames(internalizeCandidates)); + PreWriteAccessibilityApplied = true; + } + + public static void RestorePreWriteModelFactoryMethods() + { + if (_preWriteModelFactory == null || _preWriteModelFactoryMethods == null) + { + return; + } + + _preWriteModelFactory.Update(methods: _preWriteModelFactoryMethods); + _preWriteModelFactory = null; + _preWriteModelFactoryMethods = null; + } + + public static void Analyze(IReadOnlyList providers) + { + var generatedProviders = GetGeneratedProviders(providers); + + // Build two graphs from provider metadata: the full implementation graph for removal, + // and the public-surface graph for accessibility decisions. + var graph = BuildGraph(generatedProviders); + var publicGraph = BuildGraph(generatedProviders, publicOnly: true); + + var customPublicRoots = GetCustomCodePublicGeneratedTypeRoots(generatedProviders, graph.Nodes); + var apiBaselineGeneratedTypeRoots = GetApiBaselineGeneratedTypeRoots(graph.Nodes); + customPublicRoots.UnionWith(apiBaselineGeneratedTypeRoots); + var generatedPublicDeclarations = GetGeneratedPublicTypeDeclarationsFromLastContract(generatedProviders, graph.Nodes); + customPublicRoots.UnionWith(generatedPublicDeclarations); + var customCodeRemovalRoots = GetCustomCodeGeneratedTypeRoots(generatedProviders, graph.Nodes); + var customRemovalRoots = new HashSet(customCodeRemovalRoots, StringComparer.Ordinal); + customRemovalRoots.UnionWith(apiBaselineGeneratedTypeRoots); + customRemovalRoots.UnionWith(generatedPublicDeclarations); + var customInternalDeclarations = GetCustomCodeInternalGeneratedTypeDeclarations(generatedProviders, graph.Nodes); + var generatedInternalDeclarations = GetGeneratedInternalTypeDeclarations(generatedProviders, graph.Nodes); + customRemovalRoots.UnionWith(GetExistingGeneratedHelperRoots(generatedProviders, generatedInternalDeclarations)); + + // Helper types are rooted after an initial reachability pass so unused infrastructure + // such as change-tracking dictionaries can still be removed when no reachable type needs them. + var generatedDiscriminatorBaseNames = GetGeneratedPersistableModelProxyTypeNames(generatedProviders, publicGraph.Nodes); + var (internalizeCandidates, publicizeCandidates, _) = GetAccessibilityCandidates( + providers, + generatedProviders, + graph, + publicGraph, + customPublicRoots, + customInternalDeclarations, + generatedInternalDeclarations, + generatedDiscriminatorBaseNames); + + var removeCandidates = new HashSet(StringComparer.Ordinal); + if (Configuration.UnreferencedTypesHandling == Configuration.UnreferencedTypesHandlingOption.RemoveOrInternalize) + { + // Body-only generated dependencies are needed to avoid deleting helper files, but they do + // not contribute to public API reachability for internalization. + AddGeneratedBodyReferences(providers, graph); + removeCandidates = GetRemovalCandidates( + providers, + generatedProviders, + graph, + customRemovalRoots, + generatedDiscriminatorBaseNames); + } + + _latestResult = new ProviderReferenceMapResult( + internalizeCandidates, + publicizeCandidates, + removeCandidates); + if (Configuration.UnreferencedTypesHandling == Configuration.UnreferencedTypesHandlingOption.RemoveOrInternalize) + { + RemoveMethodsFromModelFactory(GetSimpleNames(removeCandidates)); + } + } + + private static (HashSet InternalizeCandidates, HashSet PublicizeCandidates) GetPreWriteAccessibilityCandidates(IReadOnlyList providers) + { + var generatedProviders = GetGeneratedProviders(providers); + var graph = BuildGraph(generatedProviders); + var publicGraph = BuildGraph(generatedProviders, publicOnly: true); + var customPublicRoots = GetCustomCodePublicGeneratedTypeRoots(generatedProviders, graph.Nodes); + var apiBaselineGeneratedTypeRoots = GetApiBaselineGeneratedTypeRoots(graph.Nodes); + customPublicRoots.UnionWith(apiBaselineGeneratedTypeRoots); + var generatedPublicDeclarations = GetGeneratedPublicTypeDeclarationsFromLastContract(generatedProviders, graph.Nodes); + customPublicRoots.UnionWith(generatedPublicDeclarations); + var customInternalDeclarations = GetCustomCodeInternalGeneratedTypeDeclarations(generatedProviders, graph.Nodes); + var generatedInternalDeclarations = GetGeneratedInternalTypeDeclarations(generatedProviders, graph.Nodes); + var generatedDiscriminatorBaseNames = new HashSet(StringComparer.Ordinal); + + var (internalizeCandidates, publicizeCandidates, _) = GetAccessibilityCandidates( + providers, + generatedProviders, + graph, + publicGraph, + customPublicRoots, + customInternalDeclarations, + generatedInternalDeclarations, + generatedDiscriminatorBaseNames); + + return (internalizeCandidates, publicizeCandidates); + } + + private static (HashSet InternalizeCandidates, HashSet PublicizeCandidates, HashSet InternalizeHelperRoots) GetAccessibilityCandidates( + IReadOnlyList providers, + IReadOnlyList generatedProviders, + ProviderReferenceGraph graph, + ProviderReferenceGraph publicGraph, + HashSet customPublicRoots, + HashSet customInternalDeclarations, + HashSet generatedInternalDeclarations, + HashSet generatedDiscriminatorBaseNames) + { + var internalizeReferences = CloneReferences(publicGraph.References); + + // Start from public client and custom/public API roots. Anything public-reachable can + // stay public unless it crosses a custom/internal boundary. + var internalizeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: false, includeAdditionalRoots: true, includeUnionVariantRoots: false, includeModelFactorySignatureRoots: true, publicClientRootsOnly: true); + if (ShouldUseUnionVariantFallbackRoots()) + { + AddUnionVariantRoots(internalizeRoots, providers, graph.Nodes); + } + + var generatedPublicReachable = GetReachableTypes(internalizeRoots, internalizeReferences); + AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences, generatedPublicReachable, generatedDiscriminatorBaseNames); + internalizeRoots.UnionWith(customPublicRoots); + var internalizeReachableWithoutHelpers = GetReachableTypes(internalizeRoots, internalizeReferences); + AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences, internalizeReachableWithoutHelpers, generatedDiscriminatorBaseNames); + internalizeReachableWithoutHelpers = GetReachableTypes(internalizeRoots, internalizeReferences); + var publicizeRoots = new HashSet(internalizeRoots, StringComparer.Ordinal); + var publicApiReferences = CloneReferences(publicGraph.References); + var internalizeHelperRoots = GetHelperRootNames(generatedProviders, graph.Nodes, internalizeReachableWithoutHelpers, graph.References); + internalizeRoots.UnionWith(internalizeHelperRoots); + var internalizeDeclaredNodes = GetPostProcessorDeclaredNodes(generatedProviders, graph.Nodes, publicOnly: true); + var customInternalBoundaryNodes = GetCustomInternalBoundaryNodes(publicGraph, customInternalDeclarations); + var publicizeDeclaredNodes = GetPublicizeDeclaredNodes(generatedProviders, graph.Nodes, internalizeDeclaredNodes); + var generatedImplementationInternalDeclarations = GetGeneratedImplementationInternalTypeDeclarations(generatedProviders, generatedInternalDeclarations); + var publicApiTraversalNodes = GetPublicApiTraversalNodes( + internalizeDeclaredNodes, + publicizeDeclaredNodes, + generatedInternalDeclarations, + generatedImplementationInternalDeclarations); + var publicizeReachable = GetReachableTypes(publicizeRoots, internalizeReferences, publicApiTraversalNodes); + var internalizeCandidates = GetInternalizeCandidates( + internalizeDeclaredNodes, + publicizeReachable, + customInternalDeclarations, + generatedInternalDeclarations, + customInternalBoundaryNodes, + customPublicRoots, + publicizeRoots, + graph.Nodes, + internalizeReferences); + AddNestedInternalizeCandidates(generatedProviders, internalizeCandidates, graph.Nodes); + AddInternalOnlyDependencyCandidates( + internalizeDeclaredNodes, + internalizeCandidates, + customInternalDeclarations, + generatedInternalDeclarations, + customPublicRoots, + internalizeReferences, + generatedImplementationInternalDeclarations); + AddNestedInternalizeCandidates(generatedProviders, internalizeCandidates, graph.Nodes); + publicizeRoots.ExceptWith(internalizeCandidates); + publicizeReachable = GetReachableTypes(publicizeRoots, internalizeReferences, publicApiTraversalNodes); + var publicizeRootExclusions = GetRootNames( + providers, + graph.Nodes, + helperRoots: [], + includeModelFactory: true, + includeAdditionalRoots: true, + includeUnionVariantRoots: true, + includeModelFactorySignatureRoots: false, + publicClientRootsOnly: true); + var publicizeCandidates = GetPublicizeCandidates( + publicizeDeclaredNodes, + publicizeReachable, + customInternalDeclarations, + customInternalBoundaryNodes, + internalizeHelperRoots, + publicizeRootExclusions, + generatedInternalDeclarations, + publicizeRoots, + publicApiReferences, + internalizeReferences, + generatedImplementationInternalDeclarations); + return (internalizeCandidates, publicizeCandidates, internalizeHelperRoots); + } + + private static bool IsPublic(MethodSignatureModifiers modifiers) => modifiers.HasFlag(MethodSignatureModifiers.Public); + private static bool IsPublic(FieldModifiers modifiers) => modifiers.HasFlag(FieldModifiers.Public); + + private static TypeSignatureModifiers MakeInternal(TypeSignatureModifiers modifiers) + => (modifiers & ~(TypeSignatureModifiers.Public | TypeSignatureModifiers.Private | TypeSignatureModifiers.Protected)) | TypeSignatureModifiers.Internal; + + private static TypeSignatureModifiers MakePublic(TypeSignatureModifiers modifiers) + => (modifiers & ~(TypeSignatureModifiers.Internal | TypeSignatureModifiers.Private | TypeSignatureModifiers.Protected)) | TypeSignatureModifiers.Public; + + private sealed record ProviderReferenceGraph( + HashSet Nodes, + Dictionary> References); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapResult.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapResult.cs similarity index 100% rename from packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapResult.cs rename to packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapResult.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs index 206379b1b4e..17433758fe5 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs @@ -22,6 +22,7 @@ public class SourceInputModel public ApiCompatBaseline ApiCompatBaseline { get; } private readonly Lazy> _nameMap; + private readonly Lazy> _customizationTypeProviders; public SourceInputModel(Compilation? customization, Compilation? lastContract) : this(customization, lastContract, ApiCompatBaseline.Empty) @@ -35,6 +36,7 @@ public SourceInputModel(Compilation? customization, Compilation? lastContract, A ApiCompatBaseline = apiCompatBaseline ?? ApiCompatBaseline.Empty; _nameMap = new(PopulateNameMap); + _customizationTypeProviders = new(PopulateCustomizationTypeProviders); } private IReadOnlyDictionary PopulateNameMap() @@ -70,11 +72,17 @@ private IReadOnlyDictionary PopulateNameMap() return FindTypeInCompilation(LastContract, ns, name, true, declaringTypeName, includeInternal: false); } - internal IEnumerable GetCustomizationTypeProviders() + internal TypeProvider? FindForTypeInLastContractIncludingInternal(string ns, string name, string? declaringTypeName = null) { + return FindTypeInCompilation(LastContract, ns, name, true, declaringTypeName); + } + + private IReadOnlyList PopulateCustomizationTypeProviders() + { + var providers = new List(); if (Customization == null) { - yield break; + return providers; } foreach (IModuleSymbol module in Customization.Assembly.Modules) @@ -83,12 +91,16 @@ internal IEnumerable GetCustomizationTypeProviders() { if (type is INamedTypeSymbol namedTypeSymbol) { - yield return new NamedTypeSymbolProvider(namedTypeSymbol, Customization); + providers.Add(new NamedTypeSymbolProvider(namedTypeSymbol, Customization)); } } } + + return providers; } + internal IReadOnlyList GetCustomizationTypeProviders() => _customizationTypeProviders.Value; + private TypeProvider? FindTypeInCompilation( Compilation? compilation, string ns, diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/NamedTypeSymbolProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/NamedTypeSymbolProviderTests.cs index 9ceee883bd4..4715686fce7 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/NamedTypeSymbolProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/NamedTypeSymbolProviderTests.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.TypeSpec.Generator.Expressions; using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Providers; @@ -370,6 +371,54 @@ public async Task ValidatePartialMethodWithBodyIsNotDetectedAsPartialDeclaration Assert.IsFalse(doIt.IsPartialMethod, "Partial methods with bodies should not be treated as customization signals."); } + [Test] + public void BodyDependenciesIncludeUsingNamespaceCandidatesForUnresolvedTypeSyntax() + { + var tree = CSharpSyntaxTree.ParseText(""" + using Sample.Models; + + namespace Sample + { + public partial class CustomClient + { + public void Test(object response) + { + ReferencedModel result = (ReferencedModel)response; + } + } + } + """); + var compilation = CSharpCompilation.Create( + "TestAssembly", + [tree], + [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]); + var symbol = CompilationHelper.GetSymbol(compilation.Assembly.Modules.First().GlobalNamespace, "CustomClient")!; + var provider = new NamedTypeSymbolProvider(symbol, compilation); + + Assert.IsTrue(provider.BodyDependencyTypes.Any(type => type.FullyQualifiedName == "Sample.Models.ReferencedModel")); + } + + [Test] + public void MetadataNamePreservesGenericArity() + { + var tree = CSharpSyntaxTree.ParseText(""" + namespace Sample.Models + { + public partial class CustomModel + { + } + } + """); + var compilation = CSharpCompilation.Create( + "TestAssembly", + [tree], + [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]); + var symbol = CompilationHelper.GetSymbol(compilation.Assembly.Modules.First().GlobalNamespace, "CustomModel`1")!; + var provider = new NamedTypeSymbolProvider(symbol, compilation); + + Assert.AreEqual("Sample.Models.CustomModel`1", provider.MetadataName); + } + // Operator signatures parsed from a customization partial must compare equal to the corresponding generated signatures. [Test] public async Task ValidateOperatorSignaturesMatchGenerated()