From ce12edf2fe3cd2e437d3b801b034c9947a313be2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Jun 2026 18:08:35 +0000 Subject: [PATCH 01/12] Initial plan From 3f6c86a7559feac0e94f47fb97b1231061b29abf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Jun 2026 18:13:14 +0000 Subject: [PATCH 02/12] Remove unused System.Diagnostics.CodeAnalysis using after internalizing Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/PostProcessing/PostProcessor.cs | 77 +++++++++++++++++++ .../test/PostProcessing/PostProcessorTests.cs | 37 ++++++++- .../UnreferencedStillUsingCodeAnalysis.cs | 14 ++++ 3 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/UnreferencedStillUsingCodeAnalysis.cs 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 738174e7628..7e64ba7ac0e 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 @@ -162,6 +162,11 @@ public async Task InternalizeAsync(Project project) { CodeModelGenerator.Instance.Emitter.Info( $"Internalized {nodesToInternalize.Count} unreferenced public type(s)."); + + // Removing the [Experimental] attribute while internalizing can leave the + // System.Diagnostics.CodeAnalysis using directive unused, so clean it up. + var internalizedDocumentIds = nodesToInternalize.Values.ToHashSet(); + project = await RemoveUnusedCodeAnalysisUsingsAsync(project, internalizedDocumentIds); } var modelNamesToRemove = @@ -518,6 +523,78 @@ private async Task RemoveInvalidRefs(Project project) return solution.GetProject(project.Id)!; } + private const string CodeAnalysisNamespace = "System.Diagnostics.CodeAnalysis"; + + /// + /// Removes the using System.Diagnostics.CodeAnalysis; directive from the given documents when it is no + /// longer referenced. Internalizing a type strips its [Experimental] attribute, which can leave this + /// using directive unused. + /// + private async Task RemoveUnusedCodeAnalysisUsingsAsync(Project project, IEnumerable documentIds) + { + var solution = project.Solution; + foreach (var documentId in documentIds) + { + solution = await RemoveUnusedCodeAnalysisUsing(solution, documentId); + } + + return solution.GetProject(project.Id)!; + } + + private async Task RemoveUnusedCodeAnalysisUsing(Solution solution, DocumentId documentId) + { + var document = solution.GetDocument(documentId)!; + var root = await document.GetSyntaxRootAsync(); + var model = await document.GetSemanticModelAsync(); + + if (root is not CompilationUnitSyntax cu || model == null) + return solution; + + var unusedUsings = cu.Usings + .Where(u => u.Alias == null + && u.StaticKeyword.IsKind(SyntaxKind.None) + && u.Name?.ToString() == CodeAnalysisNamespace) + .ToList(); + + if (unusedUsings.Count == 0 || IsNamespaceReferenced(cu, model, CodeAnalysisNamespace)) + return solution; + + cu = cu.RemoveNodes(unusedUsings, SyntaxRemoveOptions.KeepNoTrivia)!; + solution = solution.WithDocumentSyntaxRoot(documentId, cu); + + return solution; + } + + /// + /// Determines whether any symbol declared in is referenced from a name syntax + /// in , ignoring the using directives themselves. + /// + private static bool IsNamespaceReferenced(CompilationUnitSyntax cu, SemanticModel model, string namespaceName) + { + foreach (var name in cu.DescendantNodes().OfType()) + { + // Skip names that are part of a using directive (e.g. the using being evaluated). + if (name.Ancestors().OfType().Any()) + continue; + + var symbol = model.GetSymbolInfo(name).Symbol; + if (symbol == null) + continue; + + var containingNamespace = symbol switch + { + INamespaceSymbol namespaceSymbol => namespaceSymbol.ToDisplayString(), + ITypeSymbol typeSymbol => typeSymbol.ContainingNamespace?.ToDisplayString(), + _ => (symbol.ContainingType?.ContainingNamespace ?? symbol.ContainingNamespace)?.ToDisplayString() + }; + + if (containingNamespace == namespaceName) + return true; + } + + return false; + } + private async Task RemoveInvalidUsings(Solution solution, DocumentId documentId) { var document = solution.GetDocument(documentId)!; 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 ebd37a0c146..cbac4c87bee 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 @@ -316,7 +316,8 @@ public async Task RemovesExperimentalAttributeWhenInternalizing() "ReferencedModel.cs", "UnreferencedModel.cs", "UnreferencedWithOtherAttribute.cs", - "UnreferencedWithCombinedAttributes.cs" + "UnreferencedWithCombinedAttributes.cs", + "UnreferencedStillUsingCodeAnalysis.cs" ]; foreach (var fileName in modelFileNames) { @@ -362,6 +363,40 @@ public async Task RemovesExperimentalAttributeWhenInternalizing() Assert.IsTrue(unreferencedWithCombined.Modifiers.Any(m => m.IsKind(SyntaxKind.InternalKeyword))); Assert.IsFalse(HasExperimentalAttribute(unreferencedWithCombined), "Internalized model should not keep [Experimental]."); Assert.IsTrue(HasAttribute(unreferencedWithCombined, "Serializable"), "Other attributes in the same list should be preserved."); + + // The referenced model keeps its [Experimental] attribute and therefore keeps the using directive. + Assert.IsTrue( + await HasCodeAnalysisUsingAsync(resultProject, "ReferencedModel.cs"), + "Referenced model should keep the System.Diagnostics.CodeAnalysis using."); + + // Internalizing strips [Experimental], leaving the System.Diagnostics.CodeAnalysis using unused, so it is removed. + Assert.IsFalse( + await HasCodeAnalysisUsingAsync(resultProject, "UnreferencedModel.cs"), + "Unused System.Diagnostics.CodeAnalysis using should be removed."); + Assert.IsFalse( + await HasCodeAnalysisUsingAsync(resultProject, "UnreferencedWithOtherAttribute.cs"), + "Unused System.Diagnostics.CodeAnalysis using should be removed when another attribute is preserved."); + Assert.IsFalse( + await HasCodeAnalysisUsingAsync(resultProject, "UnreferencedWithCombinedAttributes.cs"), + "Unused System.Diagnostics.CodeAnalysis using should be removed when attributes are combined in one list."); + + // When the namespace is still used by another attribute, the using directive must be preserved. + var unreferencedStillUsing = await GetSingleClassAsync(resultProject, "UnreferencedStillUsingCodeAnalysis.cs", "UnreferencedStillUsingCodeAnalysis"); + Assert.IsTrue(unreferencedStillUsing.Modifiers.Any(m => m.IsKind(SyntaxKind.InternalKeyword))); + Assert.IsFalse(HasExperimentalAttribute(unreferencedStillUsing), "Internalized model should not keep [Experimental]."); + Assert.IsTrue(HasAttribute(unreferencedStillUsing, "SuppressMessage"), "Other CodeAnalysis attributes should be preserved."); + Assert.IsTrue( + await HasCodeAnalysisUsingAsync(resultProject, "UnreferencedStillUsingCodeAnalysis.cs"), + "System.Diagnostics.CodeAnalysis using should be preserved when still referenced by another attribute."); + } + + private static async Task HasCodeAnalysisUsingAsync(Project project, string fileName) + { + var doc = project.Documents.Single(d => d.Name == fileName); + var root = await doc.GetSyntaxRootAsync(); + return ((CompilationUnitSyntax)root!) + .Usings + .Any(u => u.Name?.ToString() == "System.Diagnostics.CodeAnalysis"); } private static async Task GetSingleClassAsync(Project project, string fileName, string className) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/UnreferencedStillUsingCodeAnalysis.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/UnreferencedStillUsingCodeAnalysis.cs new file mode 100644 index 00000000000..0384590315e --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/UnreferencedStillUsingCodeAnalysis.cs @@ -0,0 +1,14 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Sample +{ + /// + /// An unreferenced model that still uses the System.Diagnostics.CodeAnalysis namespace through + /// another attribute, so the using directive must be preserved after internalizing. + /// + [SuppressMessage("Category", "Rule")] + [Experimental("EXP001")] + public class UnreferencedStillUsingCodeAnalysis + { + } +} From ac280deef64c53b2c92c47ea74978b33a72492e2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Jun 2026 18:21:06 +0000 Subject: [PATCH 03/12] Address review: clarify namespace fallback comment and method naming Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/PostProcessing/PostProcessor.cs | 6 ++++-- 1 file changed, 4 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 7e64ba7ac0e..d71ea488961 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 @@ -535,13 +535,13 @@ private async Task RemoveUnusedCodeAnalysisUsingsAsync(Project project, var solution = project.Solution; foreach (var documentId in documentIds) { - solution = await RemoveUnusedCodeAnalysisUsing(solution, documentId); + solution = await RemoveUnusedCodeAnalysisUsings(solution, documentId); } return solution.GetProject(project.Id)!; } - private async Task RemoveUnusedCodeAnalysisUsing(Solution solution, DocumentId documentId) + private async Task RemoveUnusedCodeAnalysisUsings(Solution solution, DocumentId documentId) { var document = solution.GetDocument(documentId)!; var root = await document.GetSyntaxRootAsync(); @@ -585,6 +585,8 @@ private static bool IsNamespaceReferenced(CompilationUnitSyntax cu, SemanticMode { INamespaceSymbol namespaceSymbol => namespaceSymbol.ToDisplayString(), ITypeSymbol typeSymbol => typeSymbol.ContainingNamespace?.ToDisplayString(), + // For members (methods, properties, etc.) prefer the namespace of the declaring type, + // falling back to the symbol's own containing namespace. _ => (symbol.ContainingType?.ContainingNamespace ?? symbol.ContainingNamespace)?.ToDisplayString() }; From 75dc9d611f4a28f8aba4eafe744401ca8a104794 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Jun 2026 19:27:07 +0000 Subject: [PATCH 04/12] Changes before error encountered Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/fd547137-3f63-4177-82be-da0c260abc81 Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/PostProcessing/PostProcessor.cs | 61 +++++-------------- 1 file changed, 16 insertions(+), 45 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 d71ea488961..7414a5f14d4 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 @@ -22,6 +22,9 @@ internal class PostProcessor private static readonly string[] _experimentalAttributeNames = ["Experimental", "ExperimentalAttribute"]; + // CS8019: Unnecessary using directive. + private const string UnnecessaryUsingDirectiveDiagnosticId = "CS8019"; + public PostProcessor( HashSet typesToKeep, string? modelFactoryFullName = null, @@ -523,25 +526,24 @@ private async Task RemoveInvalidRefs(Project project) return solution.GetProject(project.Id)!; } - private const string CodeAnalysisNamespace = "System.Diagnostics.CodeAnalysis"; - /// - /// Removes the using System.Diagnostics.CodeAnalysis; directive from the given documents when it is no - /// longer referenced. Internalizing a type strips its [Experimental] attribute, which can leave this - /// using directive unused. + /// Removes unnecessary using directives from the given documents. Internalizing a type strips its + /// [Experimental] attribute, which can leave a using directive (such as + /// System.Diagnostics.CodeAnalysis) unused. The C# compiler reports such directives via CS8019, so + /// this pass removes any using directive flagged by that diagnostic. /// - private async Task RemoveUnusedCodeAnalysisUsingsAsync(Project project, IEnumerable documentIds) + private async Task RemoveUnusedUsingsAsync(Project project, IEnumerable documentIds) { var solution = project.Solution; foreach (var documentId in documentIds) { - solution = await RemoveUnusedCodeAnalysisUsings(solution, documentId); + solution = await RemoveUnusedUsings(solution, documentId); } return solution.GetProject(project.Id)!; } - private async Task RemoveUnusedCodeAnalysisUsings(Solution solution, DocumentId documentId) + private async Task RemoveUnusedUsings(Solution solution, DocumentId documentId) { var document = solution.GetDocument(documentId)!; var root = await document.GetSyntaxRootAsync(); @@ -550,13 +552,14 @@ private async Task RemoveUnusedCodeAnalysisUsings(Solution solution, D if (root is not CompilationUnitSyntax cu || model == null) return solution; - var unusedUsings = cu.Usings - .Where(u => u.Alias == null - && u.StaticKeyword.IsKind(SyntaxKind.None) - && u.Name?.ToString() == CodeAnalysisNamespace) + // CS8019: Unnecessary using directive. + var unusedUsings = model.GetDiagnostics() + .Where(d => d.Id == UnnecessaryUsingDirectiveDiagnosticId) + .Select(d => cu.FindNode(d.Location.SourceSpan).FirstAncestorOrSelf()) + .OfType() .ToList(); - if (unusedUsings.Count == 0 || IsNamespaceReferenced(cu, model, CodeAnalysisNamespace)) + if (unusedUsings.Count == 0) return solution; cu = cu.RemoveNodes(unusedUsings, SyntaxRemoveOptions.KeepNoTrivia)!; @@ -565,38 +568,6 @@ private async Task RemoveUnusedCodeAnalysisUsings(Solution solution, D return solution; } - /// - /// Determines whether any symbol declared in is referenced from a name syntax - /// in , ignoring the using directives themselves. - /// - private static bool IsNamespaceReferenced(CompilationUnitSyntax cu, SemanticModel model, string namespaceName) - { - foreach (var name in cu.DescendantNodes().OfType()) - { - // Skip names that are part of a using directive (e.g. the using being evaluated). - if (name.Ancestors().OfType().Any()) - continue; - - var symbol = model.GetSymbolInfo(name).Symbol; - if (symbol == null) - continue; - - var containingNamespace = symbol switch - { - INamespaceSymbol namespaceSymbol => namespaceSymbol.ToDisplayString(), - ITypeSymbol typeSymbol => typeSymbol.ContainingNamespace?.ToDisplayString(), - // For members (methods, properties, etc.) prefer the namespace of the declaring type, - // falling back to the symbol's own containing namespace. - _ => (symbol.ContainingType?.ContainingNamespace ?? symbol.ContainingNamespace)?.ToDisplayString() - }; - - if (containingNamespace == namespaceName) - return true; - } - - return false; - } - private async Task RemoveInvalidUsings(Solution solution, DocumentId documentId) { var document = solution.GetDocument(documentId)!; From f11ecdd74f5c9b35675f8b0f3be4f28221cf185e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Jun 2026 19:37:51 +0000 Subject: [PATCH 05/12] Use generic CS8019 unused-using pass; remove comments; add TestData-validated tests Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/PostProcessing/PostProcessor.cs | 10 +-------- .../test/PostProcessing/PostProcessorTests.cs | 22 ++++++++++++++++++- ...ing(UnreferencedStillUsingCodeAnalysis).cs | 13 +++++++++++ ...ernalizing(UnreferencedWithUnusedUsing).cs | 11 ++++++++++ .../UnreferencedWithUnusedUsing.cs | 14 ++++++++++++ 5 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing(UnreferencedStillUsingCodeAnalysis).cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing(UnreferencedWithUnusedUsing).cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/UnreferencedWithUnusedUsing.cs 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 7414a5f14d4..bb74d93745a 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 @@ -166,10 +166,8 @@ public async Task InternalizeAsync(Project project) CodeModelGenerator.Instance.Emitter.Info( $"Internalized {nodesToInternalize.Count} unreferenced public type(s)."); - // Removing the [Experimental] attribute while internalizing can leave the - // System.Diagnostics.CodeAnalysis using directive unused, so clean it up. var internalizedDocumentIds = nodesToInternalize.Values.ToHashSet(); - project = await RemoveUnusedCodeAnalysisUsingsAsync(project, internalizedDocumentIds); + project = await RemoveUnusedUsingsAsync(project, internalizedDocumentIds); } var modelNamesToRemove = @@ -526,12 +524,6 @@ private async Task RemoveInvalidRefs(Project project) return solution.GetProject(project.Id)!; } - /// - /// Removes unnecessary using directives from the given documents. Internalizing a type strips its - /// [Experimental] attribute, which can leave a using directive (such as - /// System.Diagnostics.CodeAnalysis) unused. The C# compiler reports such directives via CS8019, so - /// this pass removes any using directive flagged by that diagnostic. - /// private async Task RemoveUnusedUsingsAsync(Project project, IEnumerable documentIds) { var solution = project.Solution; 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 cbac4c87bee..4d7a7b348ef 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 @@ -317,7 +317,8 @@ public async Task RemovesExperimentalAttributeWhenInternalizing() "UnreferencedModel.cs", "UnreferencedWithOtherAttribute.cs", "UnreferencedWithCombinedAttributes.cs", - "UnreferencedStillUsingCodeAnalysis.cs" + "UnreferencedStillUsingCodeAnalysis.cs", + "UnreferencedWithUnusedUsing.cs" ]; foreach (var fileName in modelFileNames) { @@ -388,6 +389,25 @@ await HasCodeAnalysisUsingAsync(resultProject, "UnreferencedWithCombinedAttribut Assert.IsTrue( await HasCodeAnalysisUsingAsync(resultProject, "UnreferencedStillUsingCodeAnalysis.cs"), "System.Diagnostics.CodeAnalysis using should be preserved when still referenced by another attribute."); + + // Validate the full generated output against the expected TestData files. The preserved-using case keeps + // the System.Diagnostics.CodeAnalysis directive, while the other case has all of its now-unused usings + // (including a non-CodeAnalysis one) removed. + Assert.AreEqual( + Helpers.GetExpectedFromFile("UnreferencedStillUsingCodeAnalysis").TrimEnd(), + (await GetDocumentTextAsync(resultProject, "UnreferencedStillUsingCodeAnalysis.cs")).TrimEnd(), + "The generated output should match the expected content."); + Assert.AreEqual( + Helpers.GetExpectedFromFile("UnreferencedWithUnusedUsing").TrimEnd(), + (await GetDocumentTextAsync(resultProject, "UnreferencedWithUnusedUsing.cs")).TrimEnd(), + "The generated output should match the expected content."); + } + + private static async Task GetDocumentTextAsync(Project project, string fileName) + { + var doc = project.Documents.Single(d => d.Name == fileName); + var root = await doc.GetSyntaxRootAsync(); + return root!.ToFullString(); } private static async Task HasCodeAnalysisUsingAsync(Project project, string fileName) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing(UnreferencedStillUsingCodeAnalysis).cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing(UnreferencedStillUsingCodeAnalysis).cs new file mode 100644 index 00000000000..76869274ade --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing(UnreferencedStillUsingCodeAnalysis).cs @@ -0,0 +1,13 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Sample +{ + /// + /// An unreferenced model that still uses the System.Diagnostics.CodeAnalysis namespace through + /// another attribute, so the using directive must be preserved after internalizing. + /// + [SuppressMessage("Category", "Rule")] + internal class UnreferencedStillUsingCodeAnalysis + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing(UnreferencedWithUnusedUsing).cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing(UnreferencedWithUnusedUsing).cs new file mode 100644 index 00000000000..72536a1c6f8 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing(UnreferencedWithUnusedUsing).cs @@ -0,0 +1,11 @@ + +namespace Sample +{ + /// + /// An unreferenced model whose only usings (System.Diagnostics.CodeAnalysis and System.Text) become + /// unused after internalizing, so both using directives must be removed. + /// + internal class UnreferencedWithUnusedUsing + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/UnreferencedWithUnusedUsing.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/UnreferencedWithUnusedUsing.cs new file mode 100644 index 00000000000..8d10c7f45c3 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/UnreferencedWithUnusedUsing.cs @@ -0,0 +1,14 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace Sample +{ + /// + /// An unreferenced model whose only usings (System.Diagnostics.CodeAnalysis and System.Text) become + /// unused after internalizing, so both using directives must be removed. + /// + [Experimental("EXP001")] + public class UnreferencedWithUnusedUsing + { + } +} From fc528d86e111012f16f7bdb71d12d679cfd7d483 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Jun 2026 20:19:57 +0000 Subject: [PATCH 06/12] Run unused-using removal after model factory pass; reduce names first to avoid stripping needed usings Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/PostProcessing/PostProcessor.cs | 32 +++++++++++++++++-- 1 file changed, 29 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 bb74d93745a..d7a0a2f5c34 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 @@ -165,15 +165,18 @@ public async Task InternalizeAsync(Project project) { CodeModelGenerator.Instance.Emitter.Info( $"Internalized {nodesToInternalize.Count} unreferenced public type(s)."); - - var internalizedDocumentIds = nodesToInternalize.Values.ToHashSet(); - project = await RemoveUnusedUsingsAsync(project, internalizedDocumentIds); } var modelNamesToRemove = nodesToInternalize.Keys.Select(item => item.Identifier.Text); project = await RemoveMethodsFromModelFactoryAsync(project, definitions, modelNamesToRemove.ToHashSet()); + if (nodesToInternalize.Count > 0) + { + var internalizedDocumentIds = nodesToInternalize.Values.ToHashSet(); + project = await RemoveUnusedUsingsAsync(project, internalizedDocumentIds); + } + return project; } @@ -538,6 +541,14 @@ private async Task RemoveUnusedUsingsAsync(Project project, IEnumerable private async Task RemoveUnusedUsings(Solution solution, DocumentId documentId) { var document = solution.GetDocument(documentId)!; + + // The post-processor runs before the simplification pass, so type references in the document are + // still fully qualified (annotated for later reduction). Reduce them first so that the CS8019 + // diagnostic only flags using directives that are genuinely unused (such as the + // System.Diagnostics.CodeAnalysis directive left over from a stripped [Experimental] attribute), + // rather than directives that are still needed once names are simplified. + document = await Simplifier.ReduceAsync(document); + var root = await document.GetSyntaxRootAsync(); var model = await document.GetSemanticModelAsync(); @@ -554,7 +565,22 @@ private async Task RemoveUnusedUsings(Solution solution, DocumentId do if (unusedUsings.Count == 0) return solution; + // Preserve any leading trivia on the first using directive (such as the file header and the + // #nullable directive) when that directive is removed, by carrying it over to the node that + // follows the removed directives. + var firstUsing = cu.Usings.FirstOrDefault(); + var leadingTrivia = firstUsing is not null && unusedUsings.Contains(firstUsing) + ? firstUsing.GetLeadingTrivia() + : default; + cu = cu.RemoveNodes(unusedUsings, SyntaxRemoveOptions.KeepNoTrivia)!; + + if (leadingTrivia.Count > 0) + { + var firstToken = cu.GetFirstToken(); + cu = cu.ReplaceToken(firstToken, firstToken.WithLeadingTrivia(leadingTrivia.AddRange(firstToken.LeadingTrivia))); + } + solution = solution.WithDocumentSyntaxRoot(documentId, cu); return solution; From 9c4d1df9861b248455dbde8d404986e7a0941475 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Jun 2026 20:55:30 +0000 Subject: [PATCH 07/12] Clean unused usings in model factory on internalize; address review feedback Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/PostProcessing/PostProcessor.cs | 31 ++++--- .../test/PostProcessing/PostProcessorTests.cs | 81 ++++++++++++++++++- ...dUsingFromModelFactoryWhenInternalizing.cs | 8 ++ .../KeptModel.cs | 9 +++ .../ModelFactoryRoot.cs | 7 ++ .../OtherNamespaceModel.cs | 10 +++ .../SampleModelFactory.cs | 11 +++ .../Generated/ParametersBasicModelFactory.cs | 1 - .../Generated/ParametersSpreadModelFactory.cs | 2 - .../Generated/PayloadMultiPartModelFactory.cs | 1 - .../Generated/PayloadPageableModelFactory.cs | 5 -- .../src/Generated/SpecialWordsModelFactory.cs | 2 - 12 files changed, 145 insertions(+), 23 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesUnusedUsingFromModelFactoryWhenInternalizing.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesUnusedUsingFromModelFactoryWhenInternalizing/KeptModel.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesUnusedUsingFromModelFactoryWhenInternalizing/ModelFactoryRoot.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesUnusedUsingFromModelFactoryWhenInternalizing/OtherNamespaceModel.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesUnusedUsingFromModelFactoryWhenInternalizing/SampleModelFactory.cs 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 d7a0a2f5c34..eec2e5c1cd3 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 @@ -169,24 +169,31 @@ public async Task InternalizeAsync(Project project) var modelNamesToRemove = nodesToInternalize.Keys.Select(item => item.Identifier.Text); - project = await RemoveMethodsFromModelFactoryAsync(project, definitions, modelNamesToRemove.ToHashSet()); + DocumentId? modelFactoryDocumentId; + (project, modelFactoryDocumentId) = await RemoveMethodsFromModelFactoryAsync(project, definitions, modelNamesToRemove.ToHashSet()); if (nodesToInternalize.Count > 0) { - var internalizedDocumentIds = nodesToInternalize.Values.ToHashSet(); - project = await RemoveUnusedUsingsAsync(project, internalizedDocumentIds); + var documentsToClean = nodesToInternalize.Values.ToHashSet(); + // Removing methods from the model factory can leave a using directive (for a model in a + // different namespace) unused, so include the model factory document in the cleanup pass. + if (modelFactoryDocumentId != null) + { + documentsToClean.Add(modelFactoryDocumentId); + } + project = await RemoveUnusedUsingsAsync(project, documentsToClean); } return project; } - private async Task RemoveMethodsFromModelFactoryAsync(Project project, + private async Task<(Project Project, DocumentId? ModelFactoryDocumentId)> RemoveMethodsFromModelFactoryAsync(Project project, TypeSymbols definitions, HashSet namesToRemove) { var modelFactorySymbol = definitions.ModelFactorySymbol; if (modelFactorySymbol == null) - return project; + return (project, null); var nodesToRemove = new List(); @@ -220,7 +227,7 @@ private async Task RemoveMethodsFromModelFactoryAsync(Project project, // maybe this is possible, for instance, we could be adding the customization all entries previously inside the generated model factory so that the generated model factory is empty and removed. if (modelFactoryGeneratedDocument == null) - return project; + return (project, null); var root = await modelFactoryGeneratedDocument.GetSyntaxRootAsync(); Debug.Assert(root is not null); @@ -231,10 +238,10 @@ private async Task RemoveMethodsFromModelFactoryAsync(Project project, var methods = root.DescendantNodes().OfType(); if (!methods.Any()) { - return project.RemoveDocument(modelFactoryGeneratedDocument.Id); + return (project.RemoveDocument(modelFactoryGeneratedDocument.Id), null); } - return modelFactoryGeneratedDocument.Project; + return (modelFactoryGeneratedDocument.Project, modelFactoryGeneratedDocument.Id); } /// @@ -553,7 +560,9 @@ private async Task RemoveUnusedUsings(Solution solution, DocumentId do var model = await document.GetSemanticModelAsync(); if (root is not CompilationUnitSyntax cu || model == null) + { return solution; + } // CS8019: Unnecessary using directive. var unusedUsings = model.GetDiagnostics() @@ -563,7 +572,9 @@ private async Task RemoveUnusedUsings(Solution solution, DocumentId do .ToList(); if (unusedUsings.Count == 0) + { return solution; + } // Preserve any leading trivia on the first using directive (such as the file header and the // #nullable directive) when that directive is removed, by carrying it over to the node that @@ -581,9 +592,7 @@ private async Task RemoveUnusedUsings(Solution solution, DocumentId do cu = cu.ReplaceToken(firstToken, firstToken.WithLeadingTrivia(leadingTrivia.AddRange(firstToken.LeadingTrivia))); } - solution = solution.WithDocumentSyntaxRoot(documentId, cu); - - return solution; + return solution.WithDocumentSyntaxRoot(documentId, cu); } private async Task RemoveInvalidUsings(Solution solution, DocumentId documentId) 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 4d7a7b348ef..3b3204542b9 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 @@ -403,6 +403,76 @@ await HasCodeAnalysisUsingAsync(resultProject, "UnreferencedStillUsingCodeAnalys "The generated output should match the expected content."); } + [Test] + public async Task RemovesUnusedUsingFromModelFactoryWhenInternalizing() + { + 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); + var folder = Helpers.GetAssetFileOrDirectoryPath(false); + const string rootFileName = "ModelFactoryRoot.cs"; + string[] modelFileNames = + [ + "KeptModel.cs", + "OtherNamespaceModel.cs" + ]; + foreach (var fileName in modelFileNames) + { + project = project.AddDocument( + fileName, + File.ReadAllText(Path.Join(folder, fileName))).Project; + } + // The model factory lives in a generated document so the post-processor can rewrite it. + const string modelFactoryFileName = "SampleModelFactory.cs"; + project = project.AddDocument( + modelFactoryFileName, + File.ReadAllText(Path.Join(folder, modelFactoryFileName)), + folders: ["Generated"]).Project; + project = project.AddDocument( + rootFileName, + File.ReadAllText(Path.Join(folder, rootFileName))).Project; + var postProcessor = new TestPostProcessor(rootFileName, modelFactoryFullName: "Sample.SampleModelFactory"); + + var resultProject = await postProcessor.InternalizeAsync(project); + + // The model in the other namespace is unreferenced and is internalized. + var otherNamespaceModel = await GetSingleClassAsync(resultProject, "OtherNamespaceModel.cs", "OtherNamespaceModel"); + Assert.IsTrue(otherNamespaceModel.Modifiers.Any(m => m.IsKind(SyntaxKind.InternalKeyword))); + + // The referenced model stays public and keeps its model factory method. + var keptModel = await GetSingleClassAsync(resultProject, "KeptModel.cs", "KeptModel"); + Assert.IsTrue(keptModel.Modifiers.Any(m => m.IsKind(SyntaxKind.PublicKeyword))); + + var modelFactory = await GetSingleClassAsync(resultProject, modelFactoryFileName, "SampleModelFactory"); + var methodNames = modelFactory.Members + .OfType() + .Select(m => m.Identifier.Text) + .ToList(); + Assert.IsTrue(methodNames.Contains("KeptModel"), "The model factory method for the referenced model should be preserved."); + Assert.IsFalse(methodNames.Contains("OtherNamespaceModel"), "The model factory method for the internalized model should be removed."); + + // Removing the model factory method for the internalized model leaves the using for its namespace unused, so it is removed. + Assert.IsFalse( + await HasUsingAsync(resultProject, modelFactoryFileName, "Sample.Models"), + "Unused using for the internalized model's namespace should be removed from the model factory."); + + Assert.AreEqual( + Helpers.GetExpectedFromFile().TrimEnd(), + (await GetDocumentTextAsync(resultProject, modelFactoryFileName)).TrimEnd(), + "The generated model factory output should match the expected content."); + } + private static async Task GetDocumentTextAsync(Project project, string fileName) { var doc = project.Documents.Single(d => d.Name == fileName); @@ -410,6 +480,15 @@ private static async Task GetDocumentTextAsync(Project project, string f return root!.ToFullString(); } + private static async Task HasUsingAsync(Project project, string fileName, string usingName) + { + var doc = project.Documents.Single(d => d.Name == fileName); + var root = await doc.GetSyntaxRootAsync(); + return ((CompilationUnitSyntax)root!) + .Usings + .Any(u => u.Name?.ToString() == usingName); + } + private static async Task HasCodeAnalysisUsingAsync(Project project, string fileName) { var doc = project.Documents.Single(d => d.Name == fileName); @@ -444,7 +523,7 @@ private class TestPostProcessor : PostProcessor { private readonly string _rootFile; - public TestPostProcessor(string rootFile, IEnumerable? nonRootTypes = null) : base([], additionalNonRootTypeNames: nonRootTypes) + public TestPostProcessor(string rootFile, IEnumerable? nonRootTypes = null, string? modelFactoryFullName = null) : base([], modelFactoryFullName: modelFactoryFullName, additionalNonRootTypeNames: nonRootTypes) { _rootFile = rootFile; } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesUnusedUsingFromModelFactoryWhenInternalizing.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesUnusedUsingFromModelFactoryWhenInternalizing.cs new file mode 100644 index 00000000000..92a6592b018 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesUnusedUsingFromModelFactoryWhenInternalizing.cs @@ -0,0 +1,8 @@ + +namespace Sample +{ + public static partial class SampleModelFactory + { + public static KeptModel KeptModel() => null; + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesUnusedUsingFromModelFactoryWhenInternalizing/KeptModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesUnusedUsingFromModelFactoryWhenInternalizing/KeptModel.cs new file mode 100644 index 00000000000..bb8a56d4689 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesUnusedUsingFromModelFactoryWhenInternalizing/KeptModel.cs @@ -0,0 +1,9 @@ +namespace Sample +{ + /// + /// A model referenced from the root type, so it stays public and keeps its model factory method. + /// + public class KeptModel + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesUnusedUsingFromModelFactoryWhenInternalizing/ModelFactoryRoot.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesUnusedUsingFromModelFactoryWhenInternalizing/ModelFactoryRoot.cs new file mode 100644 index 00000000000..0253c599a7e --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesUnusedUsingFromModelFactoryWhenInternalizing/ModelFactoryRoot.cs @@ -0,0 +1,7 @@ +namespace Sample +{ + public class ModelFactoryRoot + { + public KeptModel Model { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesUnusedUsingFromModelFactoryWhenInternalizing/OtherNamespaceModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesUnusedUsingFromModelFactoryWhenInternalizing/OtherNamespaceModel.cs new file mode 100644 index 00000000000..24f19ea2441 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesUnusedUsingFromModelFactoryWhenInternalizing/OtherNamespaceModel.cs @@ -0,0 +1,10 @@ +namespace Sample.Models +{ + /// + /// A model in a different namespace that is not referenced from any root type and is internalized, + /// so its model factory method (and the using directive for this namespace) is removed. + /// + public class OtherNamespaceModel + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesUnusedUsingFromModelFactoryWhenInternalizing/SampleModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesUnusedUsingFromModelFactoryWhenInternalizing/SampleModelFactory.cs new file mode 100644 index 00000000000..8f852f65b95 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesUnusedUsingFromModelFactoryWhenInternalizing/SampleModelFactory.cs @@ -0,0 +1,11 @@ +using Sample.Models; + +namespace Sample +{ + public static partial class SampleModelFactory + { + public static KeptModel KeptModel() => null; + + public static OtherNamespaceModel OtherNamespaceModel() => 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..9f8c3ce6d6d 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,12 +2,7 @@ #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 { 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 7a39b7329f3a08049bb5b425157ea5ce0d66f839 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Jun 2026 21:07:50 +0000 Subject: [PATCH 08/12] Add null guards and remove comment in RemoveUnusedUsings per review Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/PostProcessing/PostProcessor.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 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 eec2e5c1cd3..1542b413afc 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 @@ -542,18 +542,17 @@ private async Task RemoveUnusedUsingsAsync(Project project, IEnumerable solution = await RemoveUnusedUsings(solution, documentId); } - return solution.GetProject(project.Id)!; + return solution.GetProject(project.Id) ?? project; } private async Task RemoveUnusedUsings(Solution solution, DocumentId documentId) { - var document = solution.GetDocument(documentId)!; + var document = solution.GetDocument(documentId); + if (document == null) + { + return solution; + } - // The post-processor runs before the simplification pass, so type references in the document are - // still fully qualified (annotated for later reduction). Reduce them first so that the CS8019 - // diagnostic only flags using directives that are genuinely unused (such as the - // System.Diagnostics.CodeAnalysis directive left over from a stripped [Experimental] attribute), - // rather than directives that are still needed once names are simplified. document = await Simplifier.ReduceAsync(document); var root = await document.GetSyntaxRootAsync(); From 9d2d1f3a3b890c4bcd43cee003195f9bf01e89a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Jun 2026 21:16:38 +0000 Subject: [PATCH 09/12] Replace null-forgiving operator with explicit null check in RemoveUnusedUsings Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/PostProcessing/PostProcessor.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 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 1542b413afc..a7d88be1ea1 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 @@ -583,15 +583,19 @@ private async Task RemoveUnusedUsings(Solution solution, DocumentId do ? firstUsing.GetLeadingTrivia() : default; - cu = cu.RemoveNodes(unusedUsings, SyntaxRemoveOptions.KeepNoTrivia)!; + var updatedRoot = cu.RemoveNodes(unusedUsings, SyntaxRemoveOptions.KeepNoTrivia); + if (updatedRoot == null) + { + return solution; + } if (leadingTrivia.Count > 0) { - var firstToken = cu.GetFirstToken(); - cu = cu.ReplaceToken(firstToken, firstToken.WithLeadingTrivia(leadingTrivia.AddRange(firstToken.LeadingTrivia))); + var firstToken = updatedRoot.GetFirstToken(); + updatedRoot = updatedRoot.ReplaceToken(firstToken, firstToken.WithLeadingTrivia(leadingTrivia.AddRange(firstToken.LeadingTrivia))); } - return solution.WithDocumentSyntaxRoot(documentId, cu); + return solution.WithDocumentSyntaxRoot(documentId, updatedRoot); } private async Task RemoveInvalidUsings(Solution solution, DocumentId documentId) From 63bf1c8fed88129375d1fca198779524fbd62ea6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Jun 2026 23:19:02 +0000 Subject: [PATCH 10/12] Remove reverted [Experimental]-stripping logic; keep generic unused-using cleanup Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- ...-usings-on-internalize-2026-5-11-23-0-0.md | 7 + .../src/PostProcessing/PostProcessor.cs | 77 ---------- .../test/PostProcessing/PostProcessorTests.cs | 133 ------------------ ...ing(UnreferencedStillUsingCodeAnalysis).cs | 13 -- ...ernalizing(UnreferencedWithUnusedUsing).cs | 11 -- .../ExperimentalInternalizeRoot.cs | 7 - .../ReferencedModel.cs | 12 -- .../UnreferencedModel.cs | 12 -- .../UnreferencedStillUsingCodeAnalysis.cs | 14 -- .../UnreferencedWithCombinedAttributes.cs | 13 -- .../UnreferencedWithOtherAttribute.cs | 14 -- .../UnreferencedWithUnusedUsing.cs | 14 -- 12 files changed, 7 insertions(+), 320 deletions(-) create mode 100644 .chronus/changes/remove-unused-usings-on-internalize-2026-5-11-23-0-0.md delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing(UnreferencedStillUsingCodeAnalysis).cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing(UnreferencedWithUnusedUsing).cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/ExperimentalInternalizeRoot.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/ReferencedModel.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/UnreferencedModel.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/UnreferencedStillUsingCodeAnalysis.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/UnreferencedWithCombinedAttributes.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/UnreferencedWithOtherAttribute.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/UnreferencedWithUnusedUsing.cs diff --git a/.chronus/changes/remove-unused-usings-on-internalize-2026-5-11-23-0-0.md b/.chronus/changes/remove-unused-usings-on-internalize-2026-5-11-23-0-0.md new file mode 100644 index 00000000000..5c192deb948 --- /dev/null +++ b/.chronus/changes/remove-unused-usings-on-internalize-2026-5-11-23-0-0.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/http-client-csharp" +--- + +Remove now-unused `using` directives during post-processing. When internalizing unreferenced public types and pruning the corresponding model factory methods, any `using` directive left unused (flagged by the C# compiler's `CS8019` diagnostic) is now removed from the affected documents. 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 a7d88be1ea1..e16c4087e99 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 readonly string[] _experimentalAttributeNames = ["Experimental", "ExperimentalAttribute"]; - // CS8019: Unnecessary using directive. private const string UnnecessaryUsingDirectiveDiagnosticId = "CS8019"; @@ -364,9 +362,6 @@ private Project MarkInternal(Project project, BaseTypeDeclarationSyntax declarat $"Internalizing unreferenced public type '{declarationNode.Identifier.Text}'."); var newNode = ChangeModifier(declarationNode, SyntaxKind.PublicKeyword, SyntaxKind.InternalKeyword); - // The [Experimental] attribute is a public-API stability signal that is meaningless on a type that is - // being internalized, so strip it to avoid emitting it (and its use-site diagnostics) on internal types. - newNode = RemoveExperimentalAttribute(newNode, declarationNode.Identifier.Text); var tree = declarationNode.SyntaxTree; var document = project.GetDocument(documentId)!; var newRoot = tree.GetRoot().ReplaceNode(declarationNode, newNode) @@ -375,78 +370,6 @@ private Project MarkInternal(Project project, BaseTypeDeclarationSyntax declarat return document.Project; } - /// - /// Removes any [Experimental] () - /// attribute from . The declaration's leading trivia (such as documentation - /// comments and indentation) is re-attached to the resulting first token so that the surrounding formatting and - /// any documentation comment are preserved even when the attribute that carried them is removed. - /// - private static BaseTypeDeclarationSyntax RemoveExperimentalAttribute( - BaseTypeDeclarationSyntax declarationNode, - string typeName) - { - if (declarationNode.AttributeLists.Count == 0) - { - return declarationNode; - } - - var newAttributeLists = new List(); - bool removed = false; - - foreach (var attributeList in declarationNode.AttributeLists) - { - var keptAttributes = attributeList.Attributes - .Where(attribute => !IsExperimentalAttribute(attribute)) - .ToList(); - - if (keptAttributes.Count == attributeList.Attributes.Count) - { - // Nothing removed from this list. - newAttributeLists.Add(attributeList); - } - else if (keptAttributes.Count > 0) - { - // Keep the remaining (non-experimental) attributes in this list. - removed = true; - newAttributeLists.Add(attributeList.WithAttributes(SyntaxFactory.SeparatedList(keptAttributes))); - } - else - { - // The entire list only contained experimental attributes and is dropped. - removed = true; - } - } - - if (!removed) - { - return declarationNode; - } - - CodeModelGenerator.Instance.Emitter.Debug( - $"Removed [Experimental] attribute from '{typeName}' while internalizing it."); - - // Preserve the original leading trivia (e.g. documentation comments and indentation) by re-attaching it to - // whatever token now leads the declaration. This keeps the doc comment even when it was attached to the - // attribute list that was removed. - var originalLeadingTrivia = declarationNode.GetLeadingTrivia(); - var newNode = declarationNode.WithAttributeLists(SyntaxFactory.List(newAttributeLists)); - var firstToken = newNode.GetFirstToken(); - - return newNode.ReplaceToken(firstToken, firstToken.WithLeadingTrivia(originalLeadingTrivia)); - } - - private static bool IsExperimentalAttribute(AttributeSyntax attribute) - { - var name = attribute.Name switch - { - QualifiedNameSyntax qualified => qualified.Right.Identifier.Text, - IdentifierNameSyntax identifier => identifier.Identifier.Text, - _ => attribute.Name.ToString() - }; - - return Array.IndexOf(_experimentalAttributeNames, name) >= 0; - } - private async Task RemoveModelsAsync(Project project, IEnumerable unusedModels) { 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 3b3204542b9..958d3f1f1ec 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 @@ -3,7 +3,6 @@ using System.ClientModel.Primitives; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -291,118 +290,6 @@ public async Task DoesNotRemoveValidAttributes() Assert.AreEqual(Helpers.GetExpectedFromFile().TrimEnd(), output, "The output should match the expected content."); } - [Test] - public async Task RemovesExperimentalAttributeWhenInternalizing() - { - 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), - MetadataReference.CreateFromFile(typeof(ExperimentalAttribute).Assembly.Location) - }); - - var project = workspace.AddProject(projectInfo); - var folder = Helpers.GetAssetFileOrDirectoryPath(false); - const string rootFileName = "ExperimentalInternalizeRoot.cs"; - string[] modelFileNames = - [ - "ReferencedModel.cs", - "UnreferencedModel.cs", - "UnreferencedWithOtherAttribute.cs", - "UnreferencedWithCombinedAttributes.cs", - "UnreferencedStillUsingCodeAnalysis.cs", - "UnreferencedWithUnusedUsing.cs" - ]; - foreach (var fileName in modelFileNames) - { - project = project.AddDocument( - fileName, - File.ReadAllText(Path.Join(folder, fileName))).Project; - } - project = project.AddDocument( - rootFileName, - File.ReadAllText(Path.Join(folder, rootFileName))).Project; - var postProcessor = new TestPostProcessor(rootFileName); - - var resultProject = await postProcessor.InternalizeAsync(project); - - var referencedModel = await GetSingleClassAsync(resultProject, "ReferencedModel.cs", "ReferencedModel"); - var unreferencedModel = await GetSingleClassAsync(resultProject, "UnreferencedModel.cs", "UnreferencedModel"); - var unreferencedWithOther = await GetSingleClassAsync(resultProject, "UnreferencedWithOtherAttribute.cs", "UnreferencedWithOtherAttribute"); - var unreferencedWithCombined = await GetSingleClassAsync(resultProject, "UnreferencedWithCombinedAttributes.cs", "UnreferencedWithCombinedAttributes"); - - // The referenced model stays public and keeps its [Experimental] attribute. - Assert.IsTrue(referencedModel.Modifiers.Any(m => m.IsKind(SyntaxKind.PublicKeyword))); - Assert.IsTrue(HasExperimentalAttribute(referencedModel), "Referenced (public) model should keep [Experimental]."); - - // The unreferenced model is internalized and loses its [Experimental] attribute. - Assert.IsTrue(unreferencedModel.Modifiers.Any(m => m.IsKind(SyntaxKind.InternalKeyword))); - Assert.IsFalse(unreferencedModel.Modifiers.Any(m => m.IsKind(SyntaxKind.PublicKeyword))); - Assert.IsFalse(HasExperimentalAttribute(unreferencedModel), "Internalized model should not keep [Experimental]."); - - // The documentation comment on the internalized type is preserved. - Assert.IsTrue( - unreferencedModel.GetLeadingTrivia().ToFullString().Contains("not referenced"), - "Doc comment of the internalized type should be preserved."); - - // An internalized type with another attribute in a separate list keeps the other attribute. - Assert.IsTrue(unreferencedWithOther.Modifiers.Any(m => m.IsKind(SyntaxKind.InternalKeyword))); - Assert.IsFalse(HasExperimentalAttribute(unreferencedWithOther), "Internalized model should not keep [Experimental]."); - Assert.IsTrue(HasAttribute(unreferencedWithOther, "Serializable"), "Other attributes should be preserved."); - Assert.IsTrue( - unreferencedWithOther.GetLeadingTrivia().ToFullString().Contains("must be preserved"), - "Doc comment should be preserved when only one of several attribute lists is removed."); - - // An internalized type with the experimental attribute combined in a single list keeps the others. - Assert.IsTrue(unreferencedWithCombined.Modifiers.Any(m => m.IsKind(SyntaxKind.InternalKeyword))); - Assert.IsFalse(HasExperimentalAttribute(unreferencedWithCombined), "Internalized model should not keep [Experimental]."); - Assert.IsTrue(HasAttribute(unreferencedWithCombined, "Serializable"), "Other attributes in the same list should be preserved."); - - // The referenced model keeps its [Experimental] attribute and therefore keeps the using directive. - Assert.IsTrue( - await HasCodeAnalysisUsingAsync(resultProject, "ReferencedModel.cs"), - "Referenced model should keep the System.Diagnostics.CodeAnalysis using."); - - // Internalizing strips [Experimental], leaving the System.Diagnostics.CodeAnalysis using unused, so it is removed. - Assert.IsFalse( - await HasCodeAnalysisUsingAsync(resultProject, "UnreferencedModel.cs"), - "Unused System.Diagnostics.CodeAnalysis using should be removed."); - Assert.IsFalse( - await HasCodeAnalysisUsingAsync(resultProject, "UnreferencedWithOtherAttribute.cs"), - "Unused System.Diagnostics.CodeAnalysis using should be removed when another attribute is preserved."); - Assert.IsFalse( - await HasCodeAnalysisUsingAsync(resultProject, "UnreferencedWithCombinedAttributes.cs"), - "Unused System.Diagnostics.CodeAnalysis using should be removed when attributes are combined in one list."); - - // When the namespace is still used by another attribute, the using directive must be preserved. - var unreferencedStillUsing = await GetSingleClassAsync(resultProject, "UnreferencedStillUsingCodeAnalysis.cs", "UnreferencedStillUsingCodeAnalysis"); - Assert.IsTrue(unreferencedStillUsing.Modifiers.Any(m => m.IsKind(SyntaxKind.InternalKeyword))); - Assert.IsFalse(HasExperimentalAttribute(unreferencedStillUsing), "Internalized model should not keep [Experimental]."); - Assert.IsTrue(HasAttribute(unreferencedStillUsing, "SuppressMessage"), "Other CodeAnalysis attributes should be preserved."); - Assert.IsTrue( - await HasCodeAnalysisUsingAsync(resultProject, "UnreferencedStillUsingCodeAnalysis.cs"), - "System.Diagnostics.CodeAnalysis using should be preserved when still referenced by another attribute."); - - // Validate the full generated output against the expected TestData files. The preserved-using case keeps - // the System.Diagnostics.CodeAnalysis directive, while the other case has all of its now-unused usings - // (including a non-CodeAnalysis one) removed. - Assert.AreEqual( - Helpers.GetExpectedFromFile("UnreferencedStillUsingCodeAnalysis").TrimEnd(), - (await GetDocumentTextAsync(resultProject, "UnreferencedStillUsingCodeAnalysis.cs")).TrimEnd(), - "The generated output should match the expected content."); - Assert.AreEqual( - Helpers.GetExpectedFromFile("UnreferencedWithUnusedUsing").TrimEnd(), - (await GetDocumentTextAsync(resultProject, "UnreferencedWithUnusedUsing.cs")).TrimEnd(), - "The generated output should match the expected content."); - } - [Test] public async Task RemovesUnusedUsingFromModelFactoryWhenInternalizing() { @@ -489,15 +376,6 @@ private static async Task HasUsingAsync(Project project, string fileName, .Any(u => u.Name?.ToString() == usingName); } - private static async Task HasCodeAnalysisUsingAsync(Project project, string fileName) - { - var doc = project.Documents.Single(d => d.Name == fileName); - var root = await doc.GetSyntaxRootAsync(); - return ((CompilationUnitSyntax)root!) - .Usings - .Any(u => u.Name?.ToString() == "System.Diagnostics.CodeAnalysis"); - } - private static async Task GetSingleClassAsync(Project project, string fileName, string className) { var doc = project.Documents.Single(d => d.Name == fileName); @@ -508,17 +386,6 @@ private static async Task GetSingleClassAsync(Project pr .Single(t => t.Identifier.Text == className); } - private static bool HasAttribute(BaseTypeDeclarationSyntax type, string attributeName) - => type.AttributeLists - .SelectMany(list => list.Attributes) - .Any(attr => attr.Name.ToString() == attributeName); - - - private static bool HasExperimentalAttribute(BaseTypeDeclarationSyntax type) - => type.AttributeLists - .SelectMany(list => list.Attributes) - .Any(attr => attr.Name.ToString() is "Experimental" or "ExperimentalAttribute"); - private class TestPostProcessor : PostProcessor { private readonly string _rootFile; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing(UnreferencedStillUsingCodeAnalysis).cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing(UnreferencedStillUsingCodeAnalysis).cs deleted file mode 100644 index 76869274ade..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing(UnreferencedStillUsingCodeAnalysis).cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Sample -{ - /// - /// An unreferenced model that still uses the System.Diagnostics.CodeAnalysis namespace through - /// another attribute, so the using directive must be preserved after internalizing. - /// - [SuppressMessage("Category", "Rule")] - internal class UnreferencedStillUsingCodeAnalysis - { - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing(UnreferencedWithUnusedUsing).cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing(UnreferencedWithUnusedUsing).cs deleted file mode 100644 index 72536a1c6f8..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing(UnreferencedWithUnusedUsing).cs +++ /dev/null @@ -1,11 +0,0 @@ - -namespace Sample -{ - /// - /// An unreferenced model whose only usings (System.Diagnostics.CodeAnalysis and System.Text) become - /// unused after internalizing, so both using directives must be removed. - /// - internal class UnreferencedWithUnusedUsing - { - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/ExperimentalInternalizeRoot.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/ExperimentalInternalizeRoot.cs deleted file mode 100644 index 350fb5fdbba..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/ExperimentalInternalizeRoot.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Sample -{ - public class ExperimentalInternalizeRoot - { - public ReferencedModel Model { get; set; } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/ReferencedModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/ReferencedModel.cs deleted file mode 100644 index 877443f345a..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/ReferencedModel.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Sample -{ - /// - /// A model that is referenced from a root type and therefore stays public. - /// - [Experimental("EXP001")] - public class ReferencedModel - { - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/UnreferencedModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/UnreferencedModel.cs deleted file mode 100644 index 727b2c097d4..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/UnreferencedModel.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Sample -{ - /// - /// A model that is not referenced from any root type and is internalized. - /// - [Experimental("EXP001")] - public class UnreferencedModel - { - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/UnreferencedStillUsingCodeAnalysis.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/UnreferencedStillUsingCodeAnalysis.cs deleted file mode 100644 index 0384590315e..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/UnreferencedStillUsingCodeAnalysis.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Sample -{ - /// - /// An unreferenced model that still uses the System.Diagnostics.CodeAnalysis namespace through - /// another attribute, so the using directive must be preserved after internalizing. - /// - [SuppressMessage("Category", "Rule")] - [Experimental("EXP001")] - public class UnreferencedStillUsingCodeAnalysis - { - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/UnreferencedWithCombinedAttributes.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/UnreferencedWithCombinedAttributes.cs deleted file mode 100644 index 5a2afab2013..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/UnreferencedWithCombinedAttributes.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; - -namespace Sample -{ - /// - /// An unreferenced model with the experimental attribute combined in a single list. - /// - [Serializable, Experimental("EXP001")] - public class UnreferencedWithCombinedAttributes - { - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/UnreferencedWithOtherAttribute.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/UnreferencedWithOtherAttribute.cs deleted file mode 100644 index 5d970ad6d99..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/UnreferencedWithOtherAttribute.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; - -namespace Sample -{ - /// - /// An unreferenced model that carries another attribute which must be preserved. - /// - [Serializable] - [Experimental("EXP001")] - public class UnreferencedWithOtherAttribute - { - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/UnreferencedWithUnusedUsing.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/UnreferencedWithUnusedUsing.cs deleted file mode 100644 index 8d10c7f45c3..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesExperimentalAttributeWhenInternalizing/UnreferencedWithUnusedUsing.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Text; - -namespace Sample -{ - /// - /// An unreferenced model whose only usings (System.Diagnostics.CodeAnalysis and System.Text) become - /// unused after internalizing, so both using directives must be removed. - /// - [Experimental("EXP001")] - public class UnreferencedWithUnusedUsing - { - } -} From 5c7daed5c238630f7067ca6517a458174fd94481 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Jun 2026 23:43:13 +0000 Subject: [PATCH 11/12] Skip unused-using removal when references are unresolved (avoids dropping needed usings) Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/PostProcessing/PostProcessor.cs | 25 +++++++++- .../test/PostProcessing/PostProcessorTests.cs | 49 +++++++++++++++++++ ...sedUsingWhenTypeReferencesAreUnresolved.cs | 18 +++++++ .../Root.cs | 7 +++ .../UnreferencedModel.Serialization.cs | 18 +++++++ .../UnreferencedModel.cs | 7 +++ 6 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/KeepsUsedUsingWhenTypeReferencesAreUnresolved.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/KeepsUsedUsingWhenTypeReferencesAreUnresolved/Root.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/KeepsUsedUsingWhenTypeReferencesAreUnresolved/UnreferencedModel.Serialization.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/KeepsUsedUsingWhenTypeReferencesAreUnresolved/UnreferencedModel.cs 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 e16c4087e99..41919b8d249 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 @@ -23,6 +23,18 @@ internal class PostProcessor // CS8019: Unnecessary using directive. private const string UnnecessaryUsingDirectiveDiagnosticId = "CS8019"; + // Diagnostics that indicate a type, namespace, or name could not be resolved. When any of these are + // present in a document the compiler cannot reliably determine whether a using directive is used, so + // the CS8019 ("unnecessary using") diagnostic may be a false positive and must not be trusted. + private static readonly HashSet UnresolvedReferenceDiagnosticIds = + [ + "CS0103", // The name does not exist in the current context. + "CS0234", // The type or namespace name does not exist in the namespace (missing assembly reference?). + "CS0246", // The type or namespace name could not be found (missing using or assembly reference?). + "CS0305", // Using the generic type requires type arguments. + "CS0308", // The non-generic type cannot be used with type arguments. + ]; + public PostProcessor( HashSet typesToKeep, string? modelFactoryFullName = null, @@ -487,7 +499,18 @@ private async Task RemoveUnusedUsings(Solution solution, DocumentId do } // CS8019: Unnecessary using directive. - var unusedUsings = model.GetDiagnostics() + var diagnostics = model.GetDiagnostics(); + + // If the document has any unresolved type/namespace/name references, the compiler cannot reliably + // determine which using directives are actually used, so CS8019 may flag a using that is genuinely + // needed (for example, "using System.ClientModel;" in a serialization file when that assembly is not + // referenced in the post-processing compilation). In that case skip removal to avoid breaking the build. + if (diagnostics.Any(d => UnresolvedReferenceDiagnosticIds.Contains(d.Id))) + { + return solution; + } + + var unusedUsings = diagnostics .Where(d => d.Id == UnnecessaryUsingDirectiveDiagnosticId) .Select(d => cu.FindNode(d.Location.SourceSpan).FirstAncestorOrSelf()) .OfType() 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 958d3f1f1ec..33105cb4328 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 @@ -360,6 +360,55 @@ await HasUsingAsync(resultProject, modelFactoryFileName, "Sample.Models"), "The generated model factory output should match the expected content."); } + [Test] + public async Task KeepsUsedUsingWhenTypeReferencesAreUnresolved() + { + MockHelpers.LoadMockGenerator(); + var workspace = new AdhocWorkspace(); + // Intentionally omit the System.ClientModel reference so that the BinaryContent/ClientResult + // type references in the serialization document cannot be resolved. This mirrors the post-processing + // compilation in real generation, where not every external assembly is referenced. + var projectInfo = ProjectInfo.Create( + ProjectId.CreateNewId(), + VersionStamp.Create(), + name: "TestProj", + assemblyName: "TestProj", + language: LanguageNames.CSharp) + .WithMetadataReferences(new[] + { + MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.BinaryData).Assembly.Location) + }); + + var project = workspace.AddProject(projectInfo); + var folder = Helpers.GetAssetFileOrDirectoryPath(false); + const string rootFileName = "Root.cs"; + const string serializationFileName = "UnreferencedModel.Serialization.cs"; + foreach (var fileName in new[] { "UnreferencedModel.cs", serializationFileName }) + { + project = project.AddDocument(fileName, File.ReadAllText(Path.Join(folder, fileName)), folders: ["Generated"]).Project; + } + project = project.AddDocument(rootFileName, File.ReadAllText(Path.Join(folder, rootFileName))).Project; + var postProcessor = new TestPostProcessor(rootFileName); + + var resultProject = await postProcessor.InternalizeAsync(project); + + // The unreferenced model is internalized. + var model = await GetSingleClassAsync(resultProject, "UnreferencedModel.cs", "UnreferencedModel"); + Assert.IsTrue(model.Modifiers.Any(m => m.IsKind(SyntaxKind.InternalKeyword))); + + // The using is still referenced by the conversion operators, but the type references cannot be + // resolved in this compilation. The using must be preserved rather than removed by the CS8019 pass. + Assert.IsTrue( + await HasUsingAsync(resultProject, serializationFileName, "System.ClientModel"), + "A used using directive must not be removed when its type references cannot be resolved."); + + Assert.AreEqual( + Helpers.GetExpectedFromFile().TrimEnd(), + (await GetDocumentTextAsync(resultProject, serializationFileName)).TrimEnd(), + "The serialization document should be left untouched when references are unresolved."); + } + private static async Task GetDocumentTextAsync(Project project, string fileName) { var doc = project.Documents.Single(d => d.Name == fileName); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/KeepsUsedUsingWhenTypeReferencesAreUnresolved.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/KeepsUsedUsingWhenTypeReferencesAreUnresolved.cs new file mode 100644 index 00000000000..10ef9e57286 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/KeepsUsedUsingWhenTypeReferencesAreUnresolved.cs @@ -0,0 +1,18 @@ +using System.ClientModel; + +namespace Sample.Models +{ + internal partial class UnreferencedModel + { + public static implicit operator BinaryContent(UnreferencedModel model) + { + return BinaryContent.Create(model); + } + + public static explicit operator UnreferencedModel(ClientResult result) + { + PipelineResponse response = result.GetRawResponse(); + return new UnreferencedModel(); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/KeepsUsedUsingWhenTypeReferencesAreUnresolved/Root.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/KeepsUsedUsingWhenTypeReferencesAreUnresolved/Root.cs new file mode 100644 index 00000000000..808b8cae401 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/KeepsUsedUsingWhenTypeReferencesAreUnresolved/Root.cs @@ -0,0 +1,7 @@ +namespace Sample +{ + public class Root + { + public int Value { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/KeepsUsedUsingWhenTypeReferencesAreUnresolved/UnreferencedModel.Serialization.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/KeepsUsedUsingWhenTypeReferencesAreUnresolved/UnreferencedModel.Serialization.cs new file mode 100644 index 00000000000..4ad04710c49 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/KeepsUsedUsingWhenTypeReferencesAreUnresolved/UnreferencedModel.Serialization.cs @@ -0,0 +1,18 @@ +using System.ClientModel; + +namespace Sample.Models +{ + public partial class UnreferencedModel + { + public static implicit operator BinaryContent(UnreferencedModel model) + { + return BinaryContent.Create(model); + } + + public static explicit operator UnreferencedModel(ClientResult result) + { + PipelineResponse response = result.GetRawResponse(); + return new UnreferencedModel(); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/KeepsUsedUsingWhenTypeReferencesAreUnresolved/UnreferencedModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/KeepsUsedUsingWhenTypeReferencesAreUnresolved/UnreferencedModel.cs new file mode 100644 index 00000000000..de228af0eb9 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/KeepsUsedUsingWhenTypeReferencesAreUnresolved/UnreferencedModel.cs @@ -0,0 +1,7 @@ +namespace Sample.Models +{ + public partial class UnreferencedModel + { + public int X { get; set; } + } +} From 2eafa9d6a626610a17edc17d4ecaacb4c8eb75d6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Jun 2026 15:26:58 +0000 Subject: [PATCH 12/12] Remove inline comments and changelog file per review feedback Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- ...remove-unused-usings-on-internalize-2026-5-11-23-0-0.md | 7 ------- .../src/PostProcessing/PostProcessor.cs | 5 ----- 2 files changed, 12 deletions(-) delete mode 100644 .chronus/changes/remove-unused-usings-on-internalize-2026-5-11-23-0-0.md diff --git a/.chronus/changes/remove-unused-usings-on-internalize-2026-5-11-23-0-0.md b/.chronus/changes/remove-unused-usings-on-internalize-2026-5-11-23-0-0.md deleted file mode 100644 index 5c192deb948..00000000000 --- a/.chronus/changes/remove-unused-usings-on-internalize-2026-5-11-23-0-0.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -changeKind: fix -packages: - - "@typespec/http-client-csharp" ---- - -Remove now-unused `using` directives during post-processing. When internalizing unreferenced public types and pruning the corresponding model factory methods, any `using` directive left unused (flagged by the C# compiler's `CS8019` diagnostic) is now removed from the affected documents. 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 41919b8d249..2800967b4a9 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 @@ -498,13 +498,8 @@ private async Task RemoveUnusedUsings(Solution solution, DocumentId do return solution; } - // CS8019: Unnecessary using directive. var diagnostics = model.GetDiagnostics(); - // If the document has any unresolved type/namespace/name references, the compiler cannot reliably - // determine which using directives are actually used, so CS8019 may flag a using that is genuinely - // needed (for example, "using System.ClientModel;" in a serialization file when that assembly is not - // referenced in the post-processing compilation). In that case skip removal to avoid breaking the build. if (diagnostics.Any(d => UnresolvedReferenceDiagnosticIds.Contains(d.Id))) { return solution;