Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add InjectAttributeCodeFixProvider #89

Merged
merged 4 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,8 @@ csharp_preserve_single_line_statements = true
# IDE0060: Remove unused parameter
dotnet_diagnostic.IDE0060.severity = warning

resharper_foreach_can_be_converted_to_query_using_another_get_enumerator_highlighting = none

[src/{Compilers,ExpressionEvaluator,Scripting}/**Test**/*.{cs,vb}]

# IDE0060: Remove unused parameter
Expand Down Expand Up @@ -279,6 +281,7 @@ dotnet_diagnostic.IDE2005.severity = warning
# csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental
dotnet_diagnostic.IDE2006.severity = warning


[src/{VisualStudio}/**/*.{cs,vb}]
# CA1822: Make member static
# There is a risk of accidentally breaking an internal API that partners rely on though IVT.
Expand Down
2 changes: 1 addition & 1 deletion VContainerAnalyzer.Test/FieldAnalyzerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public async Task EmptySourceCode_NoDiagnosticReport()
[Test]
public async ValueTask Analyze_FieldInjection_ReportDiagnostic()
{
var source = Helper.ReadCodes("FieldInjectionClass.cs", "EmptyClassStub.cs");
var source = Helper.GetFileContentTexts("FieldInjectionClass.cs", "EmptyClassStub.cs");
var analyzer = new FieldAnalyzer();
var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source);

Expand Down
36 changes: 30 additions & 6 deletions VContainerAnalyzer.Test/Helper.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
// Copyright (c) 2020-2024 VeyronSakai.
// This software is released under the MIT License.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace VContainerAnalyzer.Test;

public static class Helper
internal static class Helper
{
private const string VContainerDirectory = "VContainer";
private const string TestDataDirPath = "../../../TestData";
private const string VContainerDirectoryName = "VContainer";

private static IEnumerable<string> VContainerSourcePaths { get; } =
GetVContainerFiles.Select(file => $"{Path.Combine(VContainerDirectory, file)}").ToArray();
GetVContainerFiles.Select(file => $"{Path.Combine(VContainerDirectoryName, file)}").ToArray();

private static IEnumerable<string> GetVContainerFiles =>
[
Expand All @@ -25,11 +30,30 @@ public static class Helper
"InjectAttribute.cs",
];

public static string[] ReadCodes(params string[] sources)
internal static string[] GetFileContentTexts(params string[] sourcePaths)
{
const string TestDataDirPath = "../../../TestData";
return sources
return sourcePaths
.Concat(VContainerSourcePaths)
.Select(file => File.ReadAllText($"{TestDataDirPath}/{file}", Encoding.UTF8)).ToArray();
}

internal static string GetJoinedFilesContentText(params string[] sources)
{
var fileBodyBuilder = new StringBuilder();
var usingStatementsBuilder = new StringBuilder();

foreach (var filePath in sources.Concat(VContainerSourcePaths))
{
var fileContent = File.ReadAllText($"{TestDataDirPath}/{filePath}", Encoding.UTF8);
var tree = CSharpSyntaxTree.ParseText(fileContent);
var root = tree.GetRoot();
var usingDirectives = root.DescendantNodes().OfType<UsingDirectiveSyntax>().ToList();
var newRoot = root.RemoveNodes(usingDirectives, SyntaxRemoveOptions.KeepNoTrivia);
var usingStatements = string.Join(Environment.NewLine, usingDirectives.Select(u => u.ToFullString()));
fileBodyBuilder.Append(newRoot?.ToFullString());
usingStatementsBuilder.Append(usingStatements);
}

return usingStatementsBuilder.Append(fileBodyBuilder).ToString();
}
}
27 changes: 27 additions & 0 deletions VContainerAnalyzer.Test/InjectAttributeCodeFixProviderTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) 2020-2024 VeyronSakai.
// This software is released under the MIT License.

using System.Threading.Tasks;
using NUnit.Framework;
using Verify =
Microsoft.CodeAnalysis.CSharp.Testing.NUnit.CodeFixVerifier<VContainerAnalyzer.Analyzers.FieldAnalyzer,
VContainerAnalyzer.CodeFixProviders.InjectAttributeCodeFixProvider>;

namespace VContainerAnalyzer.Test;

[TestFixture]
public class InjectAttributeCodeFixProviderTest
{
[Test]
public async Task TypeNameContainingLowercase_CodeFixed()
{
var source = Helper.GetJoinedFilesContentText("FieldInjectionClass.cs", "EmptyClassStub.cs");
var fixedSource = Helper.GetJoinedFilesContentText("FieldInjectionClassFixed.txt", "EmptyClassStub.cs");

var expected = Verify.Diagnostic()
.WithSpan(22, 10, 22, 16)
.WithArguments("_field1");

await Verify.VerifyCodeFixAsync(source, expected, fixedSource);
}
}
2 changes: 1 addition & 1 deletion VContainerAnalyzer.Test/PropertyAnalyzerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public async Task EmptySourceCode_NoDiagnosticReport()
[Test]
public async ValueTask Analyze_PropertyInjection_ReportDiagnostic()
{
var source = Helper.ReadCodes("PropertyInjectionClass.cs", "EmptyClassStub.cs");
var source = Helper.GetFileContentTexts("PropertyInjectionClass.cs", "EmptyClassStub.cs");
var analyzer = new PropertyAnalyzer();
var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source);

Expand Down
13 changes: 13 additions & 0 deletions VContainerAnalyzer.Test/TestData/FieldInjectionClassFixed.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) 2020-2024 VeyronSakai.
// This software is released under the MIT License.

using VContainer;

namespace VContainerAnalyzer.Test.TestData
{
public class FieldInjectionClass
{
private EmptyClassStub _field1;
private EmptyClassStub _field2;
}
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright (c) 2020-2024 VeyronSakai.
// This software is released under the MIT License.

using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
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.Syntax;
using SyntaxFactory = Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

namespace VContainerAnalyzer.CodeFixProviders;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(InjectAttributeCodeFixProvider)), Shared]
public sealed class InjectAttributeCodeFixProvider : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(Rules.Rule0002.Id);

public override FixAllProvider GetFixAllProvider()
{
return WellKnownFixAllProviders.BatchFixer;
}

public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context
.Document
.GetSyntaxRootAsync(context.CancellationToken)
.ConfigureAwait(false);

var diagnostic = context.Diagnostics.First();
var diagnosticSpan = diagnostic.Location.SourceSpan;
var declaration = root?
.FindToken(diagnosticSpan.Start)
.Parent?
.AncestorsAndSelf()
.OfType<MemberDeclarationSyntax>()
.FirstOrDefault();

if (declaration == null)
{
return;
}

if (declaration is not FieldDeclarationSyntax && declaration is not PropertyDeclarationSyntax)
{
return;
}

context.RegisterCodeFix(
CodeAction.Create(
"Remove InjectAttribute",
cancellationToken =>
RemoveInjectAttribute(context.Document, declaration, cancellationToken),
FixableDiagnosticIds.Single()),
context.Diagnostics);
}

private static async Task<Document> RemoveInjectAttribute(Document document, MemberDeclarationSyntax declaration,
CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var model = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
var newAttributeLists = new List<AttributeListSyntax>();

foreach (var attributeList in declaration.AttributeLists)
{
var nodesToRemove = new List<AttributeSyntax>();

foreach (var attribute in attributeList.Attributes)
{
var attributeType = model?.GetTypeInfo(attribute).Type;
if (attributeType != null && attributeType.IsVContainerInjectAttribute())
{
nodesToRemove.Add(attribute);
}
}

var newAttributes = attributeList.RemoveNodes(nodesToRemove, SyntaxRemoveOptions.KeepNoTrivia);
if (newAttributes.Attributes.Any())
{
newAttributeLists.Add(newAttributes);
}
}

var newDeclaration = declaration
.WithAttributeLists(SyntaxFactory.List(newAttributeLists))
.WithLeadingTrivia(declaration.GetLeadingTrivia());

var newRoot = root?.ReplaceNode(declaration, newDeclaration);
return newRoot == null ? document : document.WithSyntaxRoot(newRoot);
}
}