Skip to content

Add code analysis for attributed model #14

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

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
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
2 changes: 2 additions & 0 deletions Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@

<!-- Package versions for package references across all projects -->
<ItemGroup>
<PackageReference Update="coverlet.collector" Version="3.0.2" />
<PackageReference Update="coverlet.msbuild" Version="3.0.2" />
<PackageReference Update="Microsoft.CodeAnalysis" Version="3.9.0" />
<PackageReference Update="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" />
<PackageReference Update="Microsoft.Extensions.DependencyInjection" Version="5.0.0" />
<PackageReference Update="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
Expand Down
36 changes: 36 additions & 0 deletions Finite.Commands.sln
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Parsing", "Parsing", "{4B61
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Finite.Commands.Parsing.Positional.SourceGenerator", "tools\SourceGenerators\Parsing\Positional\Finite.Commands.Parsing.Positional.SourceGenerator.csproj", "{76C919E7-93B1-4319-900C-08B15312F5B9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SourceGenerators", "SourceGenerators", "{0DB848EE-2A0B-47C8-9CE6-7F159F04979A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestingHelpers", "tests\SourceGenerators\TestingHelpers\TestingHelpers.csproj", "{E374AC1C-D3E1-4DF0-9AA7-CD6F52BFDFFE}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Models", "Models", "{2C8876C9-5710-40EA-B818-29669B31C4F0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Finite.Commands.Models.AttributedModel.SourceGenerator.UnitTests", "tests\SourceGenerators\Models\AttributedModel\Finite.Commands.Models.AttributedModel.SourceGenerator.UnitTests.csproj", "{1E81186D-DBB8-44FF-8168-B7473A298782}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -162,6 +170,30 @@ Global
{76C919E7-93B1-4319-900C-08B15312F5B9}.Release|x64.Build.0 = Release|Any CPU
{76C919E7-93B1-4319-900C-08B15312F5B9}.Release|x86.ActiveCfg = Release|Any CPU
{76C919E7-93B1-4319-900C-08B15312F5B9}.Release|x86.Build.0 = Release|Any CPU
{E374AC1C-D3E1-4DF0-9AA7-CD6F52BFDFFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E374AC1C-D3E1-4DF0-9AA7-CD6F52BFDFFE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E374AC1C-D3E1-4DF0-9AA7-CD6F52BFDFFE}.Debug|x64.ActiveCfg = Debug|Any CPU
{E374AC1C-D3E1-4DF0-9AA7-CD6F52BFDFFE}.Debug|x64.Build.0 = Debug|Any CPU
{E374AC1C-D3E1-4DF0-9AA7-CD6F52BFDFFE}.Debug|x86.ActiveCfg = Debug|Any CPU
{E374AC1C-D3E1-4DF0-9AA7-CD6F52BFDFFE}.Debug|x86.Build.0 = Debug|Any CPU
{E374AC1C-D3E1-4DF0-9AA7-CD6F52BFDFFE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E374AC1C-D3E1-4DF0-9AA7-CD6F52BFDFFE}.Release|Any CPU.Build.0 = Release|Any CPU
{E374AC1C-D3E1-4DF0-9AA7-CD6F52BFDFFE}.Release|x64.ActiveCfg = Release|Any CPU
{E374AC1C-D3E1-4DF0-9AA7-CD6F52BFDFFE}.Release|x64.Build.0 = Release|Any CPU
{E374AC1C-D3E1-4DF0-9AA7-CD6F52BFDFFE}.Release|x86.ActiveCfg = Release|Any CPU
{E374AC1C-D3E1-4DF0-9AA7-CD6F52BFDFFE}.Release|x86.Build.0 = Release|Any CPU
{1E81186D-DBB8-44FF-8168-B7473A298782}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1E81186D-DBB8-44FF-8168-B7473A298782}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1E81186D-DBB8-44FF-8168-B7473A298782}.Debug|x64.ActiveCfg = Debug|Any CPU
{1E81186D-DBB8-44FF-8168-B7473A298782}.Debug|x64.Build.0 = Debug|Any CPU
{1E81186D-DBB8-44FF-8168-B7473A298782}.Debug|x86.ActiveCfg = Debug|Any CPU
{1E81186D-DBB8-44FF-8168-B7473A298782}.Debug|x86.Build.0 = Debug|Any CPU
{1E81186D-DBB8-44FF-8168-B7473A298782}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1E81186D-DBB8-44FF-8168-B7473A298782}.Release|Any CPU.Build.0 = Release|Any CPU
{1E81186D-DBB8-44FF-8168-B7473A298782}.Release|x64.ActiveCfg = Release|Any CPU
{1E81186D-DBB8-44FF-8168-B7473A298782}.Release|x64.Build.0 = Release|Any CPU
{1E81186D-DBB8-44FF-8168-B7473A298782}.Release|x86.ActiveCfg = Release|Any CPU
{1E81186D-DBB8-44FF-8168-B7473A298782}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{545A3778-CEE6-4439-95DF-73BE895CDB1B} = {1912D85B-006D-44DE-B046-164AFB717064}
Expand All @@ -179,5 +211,9 @@ Global
{E7B03E1B-71DA-428C-BCFE-E7CA338CE868} = {AF3EE126-32C3-4DB4-8CE9-9CF1CFBA0627}
{4B6105E8-FD91-4AC8-A2C1-BE9448DD52B1} = {6263DEF7-8D9E-4F0B-B608-0C3C9BEC710D}
{76C919E7-93B1-4319-900C-08B15312F5B9} = {4B6105E8-FD91-4AC8-A2C1-BE9448DD52B1}
{0DB848EE-2A0B-47C8-9CE6-7F159F04979A} = {4FEFE436-21B1-48D9-9F3E-7AF2F56842FD}
{E374AC1C-D3E1-4DF0-9AA7-CD6F52BFDFFE} = {0DB848EE-2A0B-47C8-9CE6-7F159F04979A}
{2C8876C9-5710-40EA-B818-29669B31C4F0} = {0DB848EE-2A0B-47C8-9CE6-7F159F04979A}
{1E81186D-DBB8-44FF-8168-B7473A298782} = {2C8876C9-5710-40EA-B818-29669B31C4F0}
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,19 @@ public ValueTask<ICommandResult> HelloWorldCommand(

return new ValueTask<ICommandResult>(new NoContentCommandResult());
}

[Command("other")]
public ValueTask<ICommandResult> OtherCommand(
[Remainder]int coolParameter, string coolerParameter)
{
_logger.LogInformation("Hello world from HelloModule!");

_logger.LogInformation(
"The int is {int} and the string is {string}",
coolParameter,
coolerParameter);

return new ValueTask<ICommandResult>(new NoContentCommandResult());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System;
using NUnit.Framework;
using Finite.Commands;
using Finite.Commands.UnitTests;
using Finite.Commands.AttributedModel.SourceGenerator;
using Finite.Commands.AttributedModel;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.CSharp;

namespace Finite.Commands.Models.AttributedModel.SourceGenerator.UnitTests
{
/// <summary>
/// Unit tests for the attributed model source generator.
/// </summary>
public class AttributedModelSourceGeneratorTests
{
/// <summary>
/// Ensures that the attributed model source generator only generates
/// the data provider type when no commands are defined.
/// </summary>
[Test]
public void NoAttributedModelCommandsGeneratesOnlyDataProvider()
{
var helper = new GeneratorTestHelper<AttributedModelSourceGenerator>(
typeof(System.Reflection.Binder).Assembly,
typeof(ICommandResult).Assembly,
typeof(Module).Assembly
);

var result = helper.Run(
@"using System;

namespace UnitTestCode
{
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine(""No commands"");
}
}
}");

Assert.IsEmpty(result.Diagnostics);
Assert.AreEqual(result.GeneratedTrees.Length, 1);

var text = result.GeneratedTrees[0];
var expected = CSharpSyntaxTree.ParseText(
@"using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;

namespace Finite.Commands.AttributedModel.Internal.Commands
{
internal static class DataProvider
{
private static readonly IAdditionalDataProviderFactory[] Factories
= new IAdditionalDataProviderFactory[]
{

};

public static IEnumerable<KeyValuePair<object, object?>> GetData(
MethodInfo method)
{
foreach (var factory in Factories)
foreach (var provider in factory.GetDataProvider(method))
foreach (var kvp in provider.GetData())
yield return kvp;
}

public static IEnumerable<KeyValuePair<object, object?>> GetData(
ParameterInfo parameter)
{
foreach (var factory in Factories)
foreach (var provider in factory.GetDataProvider(parameter))
foreach (var kvp in provider.GetData())
yield return kvp;
}
}
}");

Assert.IsTrue(text.IsEquivalentTo(expected));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="../../../../src/Models/AttributedModel/Finite.Commands.Models.AttributedModel.csproj" />
<ProjectReference Include="../../../../tools/SourceGenerators/Models/AttributedModel/Finite.Commands.Models.AttributedModel.SourceGenerator.csproj" />
<ProjectReference Include="../../TestingHelpers/TestingHelpers.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis" />
<PackageReference Include="coverlet.collector">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

</Project>
58 changes: 58 additions & 0 deletions tests/SourceGenerators/TestingHelpers/GeneratorTestHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using System.Linq;
using System.Reflection;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

namespace Finite.Commands.UnitTests
{
/// <summary>
/// Defines a class which can be used to run source generator tests.
/// </summary>
public class GeneratorTestHelper<TGenerator>
where TGenerator : ISourceGenerator, new()
{
private readonly GeneratorDriver _driver;
private readonly TGenerator _generator;
private readonly Assembly[] _references;

/// <summary>
/// Gets the source generator instance used.
/// </summary>
public TGenerator Generator => _generator;

/// <summary>
/// Creates a new instance of the <see cref="GeneratorTestHelper{T}"/>
/// class.
/// </summary>
public GeneratorTestHelper(params Assembly[] references)
{
_generator = new TGenerator();
_driver = CSharpGeneratorDriver.Create(_generator);
_references = references;
}

/// <summary>
/// Runs the generator with the given source code.
/// </summary>
/// <param name="sourceCode">
/// The source code to pass to the generator.
/// </param>
/// <returns>
/// Returns a <see cref="GeneratorDriverRunResult"/> containing the
/// result of running the generator with the given source code.
/// </returns>
public GeneratorDriverRunResult Run(string sourceCode)
{
var compilation = CSharpCompilation.Create("UnitTest",
syntaxTrees: new[] { CSharpSyntaxTree.ParseText(sourceCode) },
references: _references
.Select(x => MetadataReference.CreateFromFile(x.Location)),
options: new CSharpCompilationOptions(
OutputKind.DynamicallyLinkedLibrary));

var resultDriver = _driver.RunGenerators(compilation);
return resultDriver.GetRunResult();
}
}
}
11 changes: 11 additions & 0 deletions tests/SourceGenerators/TestingHelpers/TestingHelpers.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis" />
</ItemGroup>

</Project>
5 changes: 3 additions & 2 deletions tools/SourceGenerators/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@
<PropertyGroup>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<NoPackageAnalysis>true</NoPackageAnalysis>
<!-- Disable release tracking analyzers due to weird behaviour with OmniSharp -->
<NoWarn>$(NoWarn);RS2000;RS2001;RS2002;RS2003;RS2004;RS2005;RS2006;RS2007;RS2008</NoWarn>
<!-- Disable release tracking analyzers due to weird behaviour with OmniSharp -->
<!--<NoWarn>$(NoWarn);RS2000;RS2001;RS2002;RS2003;RS2004;RS2005;RS2006;RS2007;RS2008</NoWarn>-->
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" IsImplicitlyDefined="true" PrivateAssets="all" Version="3.3.2" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" IsImplicitlyDefined="true" PrivateAssets="all" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
### New Rules
Rule ID | Category | Severity | Notes
--------|----------|----------|-------
FCAM0001 | FiniteCommandsCorrectness | Error | AttributedModelSourceGenerator
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;
using Microsoft.CodeAnalysis;

namespace Finite.Commands.AttributedModel.SourceGenerator
{
public partial class AttributedModelSourceGenerator
{
private static readonly DiagnosticDescriptor InvalidCommandReturnTypeRule
= new(
id: "FCAM0001",
title: "Command has invalid return type",
messageFormat: "Command '{0}' has invalid " +
"return type '{1}'",
category: "FiniteCommandsCorrectness",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Commands should return ICommandResult, or an " +
"awaitable type returning ICommandResult."
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Text;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Finite.Commands.AttributedModel.SourceGenerator
Expand Down Expand Up @@ -36,13 +37,17 @@ public void Execute(GeneratorExecutionContext context)
{
var receiver = (SyntaxReceiver)context.SyntaxContextReceiver!;

var commandResultSymbol = context.Compilation
.GetTypeByMetadataName(
"Finite.Commands.ICommandResult");
var groupAttributeSymbol = context.Compilation
.GetTypeByMetadataName(
"Finite.Commands.AttributedModel.GroupAttribute");
var commandAttributeSymbol = context.Compilation
.GetTypeByMetadataName(
"Finite.Commands.AttributedModel.CommandAttribute");

Debug.Assert(commandResultSymbol != null);
Debug.Assert(groupAttributeSymbol != null);
Debug.Assert(commandAttributeSymbol != null);

Expand Down Expand Up @@ -71,6 +76,23 @@ public void Execute(GeneratorExecutionContext context)
"Could not find method symbol for " +
$"{method.Identifier}");

if (
!IsValidSyncReturnType(methodSymbol.ReturnType,
commandResultSymbol!)
&& !IsValidAsyncReturnType(semanticModel,
methodSymbol.ReturnType, commandResultSymbol!))
{
context.ReportDiagnostic(
Diagnostic.Create(
InvalidCommandReturnTypeRule,
method.GetLocation(),
methodSymbol.ToDisplayString(),
methodSymbol.ReturnType.ToDisplayString()));

continue;
}


foreach (var parameterSymbol in methodSymbol.Parameters)
{
context.AddSource(
Expand All @@ -85,6 +107,35 @@ public void Execute(GeneratorExecutionContext context)
groupAttributeSymbol!, commandAttributeSymbol!));
}
}

static bool IsValidAsyncReturnType(SemanticModel model,
ITypeSymbol returnType, INamedTypeSymbol commandResultType)
{
return returnType.GetMembers()
.OfType<IMethodSymbol>()
.FirstOrDefault(x => x.Name == "GetAwaiter")
is IMethodSymbol getAwaiterMethod

&& getAwaiterMethod.ReturnType.GetMembers()
.OfType<IMethodSymbol>()
.FirstOrDefault(x => x.Name == "GetResult")
is IMethodSymbol getResultMethod

&& (
SymbolEqualityComparer.Default.Equals(
getResultMethod.ReturnType, commandResultType)
|| getResultMethod.ReturnType.AllInterfaces
.Contains(commandResultType,
SymbolEqualityComparer.Default));
}

static bool IsValidSyncReturnType(ITypeSymbol returnType,
INamedTypeSymbol commandResultType)
{
return returnType.AllInterfaces
.Contains(commandResultType,
SymbolEqualityComparer.Default);
}
}

public void Initialize(GeneratorInitializationContext context)
Expand Down
Empty file.
Loading