Skip to content

Commit

Permalink
DefaultValue attribute roslyn analyzers (#1040)
Browse files Browse the repository at this point in the history
* setup tests for code fixx

* refactor and make roslyn code fix to include protobuf packages

* prepare test infra for validating code-fix-provider

* more tests to understand the behavior

* implement ShouldDeclareDefault_CodeFixProvider !

* remove decimal from`ShouldDeclareDefault` diagnostic

* refactor the diagnostic part on different scenarios

* make previous tests pass

* split up tests

* more tests and fixes

* add the constructor parameter equality checks

* refactor to use semantic model value

* simplify and split up tests

* and implement IsRequired

* merge main & resolve conflicts

* remove warnings

* lightxunitverifier to fix glitch

* Revert "lightxunitverifier to fix glitch"

This reverts commit 4143f21.

* try use SemanticStructure validation only

* validate using addition in a separate test + fix glitch

* and fix last glitching test

* add tests for long syntax

* try to parse in the way it is done by DefaultValue itself

* support long syntax + short syntax separately

* make `decimal` work with long syntax for both short and long syntax

* use long syntax for other types as well (tests not working)

* fix the converting issues in all flows + transfer `nint` and `nuint` to `IsRequired` scenario

* reduce warnings

* use cast in all short-syntax default value code fixes

* more warning fixes

* disable warning on usages

* suppress RS1022 warning on class directly
  • Loading branch information
DeagleGross committed May 31, 2023
1 parent 9618de9 commit 1cab873
Show file tree
Hide file tree
Showing 28 changed files with 1,979 additions and 211 deletions.
14 changes: 14 additions & 0 deletions src/BuildToolsUnitTests/Abstractions/RoslynAnalysisTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace BuildToolsUnitTests.Abstractions
{
public abstract class RoslynAnalysisTests
{
protected CompilationUnitSyntax BuildCompilationUnitSyntax(string programText)
{
var tree = CSharpSyntaxTree.ParseText(programText);
return tree.GetCompilationUnitRoot();
}
}
}
7 changes: 2 additions & 5 deletions src/BuildToolsUnitTests/AnalyzerTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,8 @@ protected async Task<(Project Project, Compilation Compilation)> ObtainProjectAn
project = SetupProject(project);

if (ReferenceProtoBuf)
{
project = project
.AddMetadataReference(MetadataReference.CreateFromFile(Assembly.Load("netstandard, Version=2.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51").Location))
.AddMetadataReference(MetadataReference.CreateFromFile(Assembly.Load("System.Runtime").Location))
.AddMetadataReference(MetadataReference.CreateFromFile(typeof(TypeModel).Assembly.Location));
{
project = project.ReferenceMetadataReferences(MetadataReferenceHelpers.ProtoBufReferences);
}

project = projectModifier?.Invoke(project) ?? project;
Expand Down
10 changes: 10 additions & 0 deletions src/BuildToolsUnitTests/BuildToolsUnitTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
<ItemGroup>
<!--<PackageReference Include="Microsoft.Build" />-->
<!--<PackageReference Include="Microsoft.Build.Utilities.Core" />-->
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.XUnit" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeRefactoring.Testing.XUnit" />

<PackageReference Include="System.Collections.Immutable" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
Expand All @@ -20,6 +24,12 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" />
</ItemGroup>

<ItemGroup>
<Compile Update="DataContractAnalyzerTests.DefaultValueAttribute.cs">
<DependentUpon>DataContractAnalyzerTests.cs</DependentUpon>
</Compile>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="../protobuf-net.BuildTools/protobuf-net.BuildTools.csproj" ReferenceOutputAssembly="true" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Testing.Verifiers;
using Microsoft.CodeAnalysis.Testing;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using System.Reflection;
using System.Linq;
using System.Runtime.Versioning;

namespace BuildToolsUnitTests.CodeFixes.Abstractions
{
public abstract class CodeFixProviderTestsBase<TCodeFixProvider>
where TCodeFixProvider : CodeFixProvider, new()
{
static TargetFrameworkAttribute CurrentRunningAssemblyTargetFramework
=> (TargetFrameworkAttribute)Assembly.GetExecutingAssembly()
.GetCustomAttributes(typeof(TargetFrameworkAttribute), false)
.Single();

protected async Task RunCodeFixTestAsync<TDiagnosticAnalyzer>(
string sourceCode,
string expectedCode,
DiagnosticResult? diagnosticResult = null,
string? targetFramework = null,
params DiagnosticResult[] standardExpectedDiagnostics)
where TDiagnosticAnalyzer : DiagnosticAnalyzer, new()
{
var codeFixTest = BuildCSharpCodeFixTest<TDiagnosticAnalyzer>(sourceCode, expectedCode, targetFramework);

if (diagnosticResult is not null)
{
// expect a diagnostic in sourceCode
codeFixTest.TestState.ExpectedDiagnostics.Add(diagnosticResult.Value);
}

// we expect some standard diagnostics in both of code compilations
AddStandardDiagnostics(codeFixTest.TestState, standardExpectedDiagnostics);
AddStandardDiagnostics(codeFixTest.FixedState, standardExpectedDiagnostics);

await codeFixTest.RunAsync();
}

CSharpCodeFixTest<TDiagnosticAnalyzer, TCodeFixProvider, XUnitVerifier> BuildCSharpCodeFixTest<TDiagnosticAnalyzer>(
string sourceCode, string expectedCode, string? targetFramework = null)
where TDiagnosticAnalyzer : DiagnosticAnalyzer, new()
{
if (string.IsNullOrEmpty(targetFramework))
{
targetFramework = CurrentRunningAssemblyTargetFramework.FrameworkDisplayName!;
}

var codeFixTest = new CSharpCodeFixTest<TDiagnosticAnalyzer, TCodeFixProvider, XUnitVerifier>
{
ReferenceAssemblies = new ReferenceAssemblies(targetFramework),
TestState = { Sources = { sourceCode }, OutputKind = OutputKind.DynamicallyLinkedLibrary },
FixedState = { Sources = { expectedCode }, OutputKind = OutputKind.DynamicallyLinkedLibrary }
};

codeFixTest.CodeActionValidationMode = CodeActionValidationMode.SemanticStructure;

AddAdditionalReferences(codeFixTest.TestState);
AddAdditionalReferences(codeFixTest.FixedState);

return codeFixTest;
}

private static void AddAdditionalReferences(SolutionState solutionState)
{
solutionState.AdditionalReferences.AddRange(MetadataReferenceHelpers.ProtoBufReferences);
solutionState.AdditionalReferences.AddRange(MetadataReferenceHelpers.WellKnownReferences);
}

private static void AddStandardDiagnostics(SolutionState solutionState, params DiagnosticResult[] diagnosticResults)
{
if (diagnosticResults.Length <= 0) return;
foreach (var diagnosticResult in diagnosticResults)
{
solutionState.ExpectedDiagnostics.Add(diagnosticResult);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
using BuildToolsUnitTests.CodeFixes.Abstractions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Testing;
using ProtoBuf.BuildTools.Analyzers;
using System.Threading.Tasks;
using ProtoBuf.CodeFixes.DefaultValue;
using Xunit;

namespace BuildToolsUnitTests.CodeFixes
{
public class ShouldDeclareDefaultCodeFixProviderTests : CodeFixProviderTestsBase<ShouldDeclareDefaultCodeFixProvider>
{
private readonly DiagnosticResult[] _standardExpectedDiagnostics = new[] {
new DiagnosticResult(DataContractAnalyzer.MissingCompatibilityLevel)
};

[Theory]
[InlineData("int", "-2")]
public async Task CodeFixValidate_ShouldDeclareDefault_NonProtoMemberAttributeExists(
string propertyType, string propertyDefaultValue)
{
var sourceCode = $@"
using ProtoBuf;
using System;
[ProtoContract]
public class Foo
{{
public {propertyType} Bar {{ get; set; }} = {propertyDefaultValue};
}}
";

var expectedCode = $@"
using ProtoBuf;
using System;
[ProtoContract]
public class Foo
{{
public {propertyType} Bar {{ get; set; }} = {propertyDefaultValue};
}}
";

await RunCodeFixTestAsync<DataContractAnalyzer>(
sourceCode,
expectedCode,
diagnosticResult: null, // no diagnostic expected!
standardExpectedDiagnostics: _standardExpectedDiagnostics);
}

[Theory]
[InlineData("int", "-2")]
public async Task CodeFixValidate_ShouldDeclareDefault_AnotherCustomAttributeExists(
string propertyType, string propertyDefaultValue)
{
var sourceCode = $@"
using ProtoBuf;
using System;
using System.ComponentModel;
[ProtoContract]
public class Foo
{{
[ProtoMember(1)]
[Custom]
public {propertyType} Bar {{ get; set; }} = {propertyDefaultValue};
}}
public class CustomAttribute : Attribute {{ }}";

// System.ComponentModel is added as part of code-fix
var expectedCode = $@"
using ProtoBuf;
using System;
using System.ComponentModel;
[ProtoContract]
public class Foo
{{
[ProtoMember(1), DefaultValue(({propertyType}){propertyDefaultValue})]
[Custom]
public {propertyType} Bar {{ get; set; }} = {propertyDefaultValue};
}}
public class CustomAttribute : Attribute {{ }}";

var diagnosticResult = PrepareDiagnosticResult(
DataContractAnalyzer.ShouldDeclareDefault,
9, 6, 9, 20,
propertyDefaultValue);

await RunCodeFixTestAsync<DataContractAnalyzer>(
sourceCode,
expectedCode,
diagnosticResult,
standardExpectedDiagnostics: _standardExpectedDiagnostics);
}

[Theory]
[InlineData("int", "-2")]
public async Task CodeFixValidate_ShouldDeclareDefault_UsingDirectiveAlreadyExists(
string propertyType, string propertyDefaultValue)
{
var sourceCode = $@"
using ProtoBuf;
using System;
using System.ComponentModel;
[ProtoContract]
public class Foo
{{
[ProtoMember(1)]
public {propertyType} Bar {{ get; set; }} = {propertyDefaultValue};
}}";

// System.ComponentModel is added as part of code-fix
var expectedCode = $@"
using ProtoBuf;
using System;
using System.ComponentModel;
[ProtoContract]
public class Foo
{{
[ProtoMember(1), DefaultValue(({propertyType}){propertyDefaultValue})]
public {propertyType} Bar {{ get; set; }} = {propertyDefaultValue};
}}";

var diagnosticResult = PrepareDiagnosticResult(
DataContractAnalyzer.ShouldDeclareDefault,
9, 6, 9, 20,
propertyDefaultValue);

await RunCodeFixTestAsync<DataContractAnalyzer>(
sourceCode,
expectedCode,
diagnosticResult,
standardExpectedDiagnostics: _standardExpectedDiagnostics);
}

[Theory]
[InlineData("decimal", "2.1", "2.1m")]
[InlineData("sbyte", "1", "1")]
[InlineData("uint", "6", "6u")]
[InlineData("ulong", "6758493021", "6758493021UL")]
[InlineData("ushort", "4", "4")]
public async Task CodeFixValidate_ShouldDeclareDefault_ReportsDiagnostic_LongSyntax(string propertyType, string attributeValue, string propertyDefaultValue)
{
var sourceCode = $@"
using ProtoBuf;
using System;
using System.ComponentModel;
[ProtoContract]
public class Foo
{{
[ProtoMember(1)]
public {propertyType} Bar {{ get; set; }} = {propertyDefaultValue};
}}";

// note: System.ComponentModel is added as part of code-fix
// and this behavior is tested in separate class, since "usingDirective" addition produces
// wrong line endings, which fail in roslyn codeFix test
// https://github.com/dotnet/roslyn/issues/62976
var expectedCode = $@"
using ProtoBuf;
using System;
using System.ComponentModel;
[ProtoContract]
public class Foo
{{
[ProtoMember(1), DefaultValue(typeof({propertyType}), ""{attributeValue}"")]
public {propertyType} Bar {{ get; set; }} = {propertyDefaultValue};
}}";

var diagnosticResult = PrepareDiagnosticResult(
DataContractAnalyzer.ShouldDeclareDefault,
9, 6, 9, 20,
propertyDefaultValue);

await RunCodeFixTestAsync<DataContractAnalyzer>(
sourceCode,
expectedCode,
diagnosticResult,
standardExpectedDiagnostics: _standardExpectedDiagnostics);
}

[Theory]
[InlineData("bool", "true")]
[InlineData("DayOfWeek", "DayOfWeek.Monday", false)]
[InlineData("char", "'x'")]
[InlineData("byte", "0x2")]
[InlineData("short", "0b0000_0011")]
[InlineData("int", "-2")]
[InlineData("long", "1234567890123456789L")]
[InlineData("float", "2.71828f")]
[InlineData("double", "3.14159265")]
public async Task CodeFixValidate_ShouldDeclareDefault_ReportsDiagnostic_ShortSyntax(
string propertyType, string propertyDefaultValue, bool isCasted = true)
{
var sourceCode = $@"
using ProtoBuf;
using System;
using System.ComponentModel;
[ProtoContract]
public class Foo
{{
[ProtoMember(1)]
public {propertyType} Bar {{ get; set; }} = {propertyDefaultValue};
}}";

// note: System.ComponentModel is added as part of code-fix
// and this behavior is tested in separate class, since "usingDirective" addition produces
// wrong line endings, which fail in roslyn codeFix test
// https://github.com/dotnet/roslyn/issues/62976

var castExpression = isCasted ? $"({propertyType})" : string.Empty;

var expectedCode = $@"
using ProtoBuf;
using System;
using System.ComponentModel;
[ProtoContract]
public class Foo
{{
[ProtoMember(1), DefaultValue({castExpression}{propertyDefaultValue})]
public {propertyType} Bar {{ get; set; }} = {propertyDefaultValue};
}}";

var diagnosticResult = PrepareDiagnosticResult(
DataContractAnalyzer.ShouldDeclareDefault,
9, 6, 9, 20,
propertyDefaultValue);

await RunCodeFixTestAsync<DataContractAnalyzer>(
sourceCode,
expectedCode,
diagnosticResult,
standardExpectedDiagnostics: _standardExpectedDiagnostics);
}

static DiagnosticResult PrepareDiagnosticResult(
DiagnosticDescriptor diagnosticDescriptor,
int startLine, int startColumn, int endLine, int endColumn,
string propertyDefaultValue)
{
return new DiagnosticResult(diagnosticDescriptor)
.WithSpan(startLine, startColumn, endLine, endColumn)
.WithArguments("Bar", propertyDefaultValue);
}
}
}
Loading

0 comments on commit 1cab873

Please sign in to comment.