|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.ImplementInterface;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Diagnostics;
using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.Test.Utilities;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.ImplementInterface;
[Trait(Traits.Feature, Traits.Features.CodeActionsImplementInterface)]
public sealed class ImplementInterfaceCodeFixTests_Razor(ITestOutputHelper logger)
: AbstractCSharpDiagnosticProviderBasedUserDiagnosticTest_NoEditor(logger)
{
private const string CS0535 = nameof(CS0535);
private const string RazorSourceGeneratorTypeName = "Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator";
private sealed record GeneratedDocumentData(string HintName, string GeneratedCode, bool IsRazor);
internal override (DiagnosticAnalyzer?, CodeFixProvider) CreateDiagnosticProviderAndFixer(Workspace workspace)
=> (null, new CSharpImplementInterfaceCodeFixProvider());
protected override void InitializeWorkspace(TestWorkspace workspace, TestParameters parameters)
{
if (parameters.fixProviderData is not GeneratedDocumentData generatedDocument)
{
return;
}
var analyzerReference = generatedDocument.IsRazor
? new TestGeneratorReference(
new Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator(
context => context.AddSource(generatedDocument.HintName, generatedDocument.GeneratedCode)))
: new TestGeneratorReference(new TestSourceGenerator(generatedDocument.HintName, generatedDocument.GeneratedCode));
var project = workspace.CurrentSolution.Projects.Single();
var updatedProject = project.AddAnalyzerReference(analyzerReference);
Assert.True(workspace.TryApplyChanges(updatedProject.Solution));
}
private static TestParameters WithRazorGeneratedDocument(string generatedCode, string hintName = "Component.razor.g.cs")
=> new(fixProviderData: new GeneratedDocumentData(hintName, generatedCode, IsRazor: true));
private static TestParameters WithGeneratedDocument(string generatedCode, string hintName = "Component.g.cs")
=> new(fixProviderData: new GeneratedDocumentData(hintName, generatedCode, IsRazor: false));
[Fact]
public Task TestImplementInterfaceInRazorSourceGeneratedDocument()
=> TestImplementingInterfaceInGeneratedDocumentAsync(
workspaceMarkup: """
<Workspace>
<Project Language="C#" AssemblyName="ClassLibrary1" CommonReferences="true">
<Document>
interface IMyInterface
{
void Method1();
}
</Document>
</Project>
</Workspace>
""",
generatedCode: """
// <auto-generated />
public partial class Component : IMyInterface
{
}
""",
expectedGeneratedCode: """
// <auto-generated />
public partial class Component : IMyInterface
{
public void Method1()
{
throw new System.NotImplementedException();
}
}
""",
actionTitle: CodeFixesResources.Implement_interface,
isRazorSourceGeneratedDocument: true);
[Fact]
public Task TestImplementInterfaceInHiddenRazorSourceGeneratedDocument()
=> TestImplementingInterfaceInGeneratedDocumentAsync(
workspaceMarkup: """
<Workspace>
<Project Language="C#" AssemblyName="ClassLibrary1" CommonReferences="true">
<Document>
interface IMyInterface
{
void Method1();
}
</Document>
</Project>
</Workspace>
""",
generatedCode: """
// <auto-generated />
#line hidden
public partial class Component : IMyInterface
{
}
#line default
""",
expectedGeneratedCode: """
// <auto-generated />
#line hidden
public partial class Component : IMyInterface
{
public void Method1()
{
throw new System.NotImplementedException();
}
}
#line default
""",
actionTitle: CodeFixesResources.Implement_interface,
isRazorSourceGeneratedDocument: true);
[Fact]
public Task TestImplementInterfaceExplicitlyInRazorSourceGeneratedDocument()
=> TestImplementingInterfaceInGeneratedDocumentAsync(
workspaceMarkup: """
<Workspace>
<Project Language="C#" AssemblyName="ClassLibrary1" CommonReferences="true">
<Document>
interface IMyInterface
{
void Method1();
}
</Document>
</Project>
</Workspace>
""",
generatedCode: """
// <auto-generated />
public partial class Component : IMyInterface
{
}
""",
expectedGeneratedCode: """
// <auto-generated />
public partial class Component : IMyInterface
{
void IMyInterface.Method1()
{
throw new System.NotImplementedException();
}
}
""",
actionTitle: CodeFixesResources.Implement_all_members_explicitly,
isRazorSourceGeneratedDocument: true);
[Fact]
public async Task TestDoesNotImplementInterfaceInNonRazorSourceGeneratedDocument()
{
using var workspace = CreateWorkspaceFromOptions(
"""
<Workspace>
<Project Language="C#" AssemblyName="ClassLibrary1" CommonReferences="true">
<Document>
interface IMyInterface
{
void Method1();
}
</Document>
</Project>
</Workspace>
""",
WithGeneratedDocument(
"""
// <auto-generated />
public partial class Component : IMyInterface
{
}
"""));
var project = workspace.CurrentSolution.Projects.Single();
var sourceGeneratedDocument = Assert.Single(await project.GetSourceGeneratedDocumentsAsync(CancellationToken.None));
Assert.DoesNotContain(RazorSourceGeneratorTypeName, sourceGeneratedDocument.FilePath);
var codeActions = await GetCodeActionsAsync(project, sourceGeneratedDocument, CancellationToken.None);
Assert.Empty(codeActions);
}
private async Task TestImplementingInterfaceInGeneratedDocumentAsync(
string workspaceMarkup,
string generatedCode,
string expectedGeneratedCode,
string actionTitle,
bool isRazorSourceGeneratedDocument)
{
using var workspace = CreateWorkspaceFromOptions(
workspaceMarkup,
isRazorSourceGeneratedDocument ? WithRazorGeneratedDocument(generatedCode) : WithGeneratedDocument(generatedCode));
var project = workspace.CurrentSolution.Projects.Single();
var sourceGeneratedDocument = Assert.Single(await project.GetSourceGeneratedDocumentsAsync(CancellationToken.None));
if (isRazorSourceGeneratedDocument)
{
Assert.Contains(RazorSourceGeneratorTypeName, sourceGeneratedDocument.FilePath);
}
else
{
Assert.DoesNotContain(RazorSourceGeneratorTypeName, sourceGeneratedDocument.FilePath);
}
Assert.StartsWith(
"// <auto-generated />",
(await sourceGeneratedDocument.GetTextAsync(CancellationToken.None)).ToString());
var codeActions = await GetCodeActionsAsync(project, sourceGeneratedDocument, CancellationToken.None);
var codeAction = Assert.Single(codeActions.Where(action => action.Title == actionTitle));
var operations = await codeAction.GetOperationsAsync(CancellationToken.None);
var changedSolutions = await ApplyOperationsAndGetSolutionAsync(workspace, operations);
var newSourceGeneratedDocument = await changedSolutions.Item2.GetSourceGeneratedDocumentAsync(sourceGeneratedDocument.Id, CancellationToken.None);
Assert.NotNull(newSourceGeneratedDocument);
AssertEx.EqualOrDiff(
expectedGeneratedCode,
(await newSourceGeneratedDocument!.GetTextAsync(CancellationToken.None)).ToString());
}
private static async Task<ImmutableArray<CodeAction>> GetCodeActionsAsync(Project project, Document requestDocument, CancellationToken cancellationToken)
{
var requestTree = await requestDocument.GetSyntaxTreeAsync(cancellationToken);
var compilation = await project.GetCompilationAsync(cancellationToken);
Assert.NotNull(requestTree);
Assert.NotNull(compilation);
var diagnostic = Assert.Single(
compilation!.GetDiagnostics(cancellationToken).Where(
diagnostic => diagnostic.Id == CS0535 &&
diagnostic.Severity == DiagnosticSeverity.Error &&
diagnostic.Location.SourceTree == requestTree));
var codeActions = new List<CodeAction>();
var context = new CodeFixContext(
requestDocument,
diagnostic.Location.SourceSpan,
[diagnostic],
(action, _) => codeActions.Add(action),
cancellationToken);
var provider = new CSharpImplementInterfaceCodeFixProvider();
await provider.RegisterCodeFixesAsync(context);
return FlattenActions([.. codeActions]);
}
#pragma warning disable RS1042 // Do not implement
private sealed class TestSourceGenerator(string hintName, string generatedCode) : ISourceGenerator
#pragma warning restore RS1042 // Do not implement
{
public void Initialize(GeneratorInitializationContext context)
{
}
public void Execute(GeneratorExecutionContext context)
=> context.AddSource(hintName, generatedCode);
}
}
|