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

Restrict the use of TypelySpecification with an analyser #37

Merged
merged 2 commits into from
Oct 9, 2023
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
240 changes: 240 additions & 0 deletions src/Typely.Generators/Analysers/TypelySpecificationAnalyser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;
using Typely.Generators.Infrastructure;
using Typely.Generators.Typely;
using Typely.Generators.Typely.Parsing;

namespace Typely.Generators.Analysers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class TypelySpecificationAnalyser : DiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
ImmutableArray.Create(
DiagnosticDescriptors.TYP0001_UnsupportedExpression,
DiagnosticDescriptors.TYP0002_UnsupportedParameter,
DiagnosticDescriptors.TYP0003_UnsupportedMethod,
DiagnosticDescriptors.TYP0004_UnsupportedField,
DiagnosticDescriptors.TYP0005_UnsupportedProperty,
DiagnosticDescriptors.TYP0006_UnsupportedType,
DiagnosticDescriptors.TYP0007_UnsupportedVariable);

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze |
GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.RegisterCompilationStartAction(OnCompilationStart);
}

private static void OnCompilationStart(CompilationStartAnalysisContext context)
{
var typeCache = new TypeCache(context.Compilation);
if (typeCache.ITypelySpecification == null)
{
return;
}

context.RegisterSymbolAction(ctx => AnalyseSymbol(ctx, typeCache),
SymbolKind.NamedType);

context.RegisterSyntaxNodeAction(ctx => AnalyzeSyntaxNode(ctx, typeCache),
SyntaxKind.ClassDeclaration);
}

private static void AnalyseSymbol(SymbolAnalysisContext context, TypeCache typeCache)
{
var namedTypeSymbol = (INamedTypeSymbol)context.Symbol;
if (!IsTypelySpecificationClass(namedTypeSymbol, typeCache.ITypelySpecification!))
{
return;
}

foreach (var member in namedTypeSymbol.GetMembers())
{
switch (member)
{
case IFieldSymbol symbol:
AnalyseFieldSymbol(context, symbol);
break;
case IMethodSymbol symbol:
AnalyseMethodSymbol(context, symbol);
break;
case INamedTypeSymbol symbol:
AnalyseNamedTypeSymbol(context, symbol);
break;
case IPropertySymbol symbol:
AnalysePropertySymbol(context, symbol);
break;
}
}
}

private static void AnalysePropertySymbol(SymbolAnalysisContext context, IPropertySymbol property)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.TYP0005_UnsupportedProperty,
property.Locations.FirstOrDefault(),
property.Name));
}

private static void AnalyseNamedTypeSymbol(SymbolAnalysisContext context, INamedTypeSymbol namedType)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.TYP0006_UnsupportedType,
namedType.Locations.FirstOrDefault(),
namedType.Name));
}

private static void AnalyseMethodSymbol(SymbolAnalysisContext context, IMethodSymbol method)
{
if (!IsAllowedMethod(method))
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.TYP0003_UnsupportedMethod,
method.Locations.FirstOrDefault(),
method.Name));
}

bool IsAllowedMethod(IMethodSymbol method) =>
method.MethodKind is MethodKind.Constructor or MethodKind.PropertyGet or MethodKind.PropertySet ||
IsCreateMethod();

bool IsCreateMethod() =>
method is { MethodKind: MethodKind.Ordinary, Name: SymbolNames.CreateMethod };
}

private static void AnalyseFieldSymbol(SymbolAnalysisContext context, IFieldSymbol field)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.TYP0004_UnsupportedField,
field.Locations.FirstOrDefault(),
field.Name));
}

private static void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context, TypeCache typeCache)
{
var classDeclaration = (ClassDeclarationSyntax)context.Node;

if (!Parser.IsTypelySpecificationClass(classDeclaration))
{
return;
}

var createMethodDeclaration = classDeclaration.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.FirstOrDefault(x => x.Identifier.Text == SymbolNames.CreateMethod);

if (createMethodDeclaration is null)
{
return;
}

var localDeclarations = createMethodDeclaration.DescendantNodes().OfType<LocalDeclarationStatementSyntax>();
var semanticModel = context.SemanticModel;

foreach (var localDeclaration in localDeclarations)
{
foreach (var variable in localDeclaration.Declaration.Variables)
{
if (semanticModel.GetDeclaredSymbol(variable) is not ILocalSymbol localSymbol)
{
continue;
}

if (!typeCache.SupportedVariablesTypes.Contains(localSymbol.Type, SymbolEqualityComparer.Default))
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.TYP0007_UnsupportedVariable,
localSymbol.Locations.FirstOrDefault(),
localSymbol.Type,
localSymbol.Name));
}
}
}
}

private static bool IsTypelySpecificationClass(INamedTypeSymbol type, INamedTypeSymbol itypelySpecification)
{
return type.TypeKind == TypeKind.Class &&
!type.IsStatic &&
type.Implements(itypelySpecification);
}

private sealed class TypeCache
{
public TypeCache(Compilation compilation)
{
ITypelySpecification = compilation.GetTypeByMetadataName(SymbolNames.ITypelySpecification);
SupportedVariablesTypes = new[]
{
compilation.GetTypeByMetadataName(SymbolNames.ITypelyBuilderOfBool),
compilation.GetTypeByMetadataName(SymbolNames.ITypelyBuilderOfByte),
compilation.GetTypeByMetadataName(SymbolNames.ITypelyBuilderOfChar),
compilation.GetTypeByMetadataName(SymbolNames.ITypelyBuilderOfDateOnly),
compilation.GetTypeByMetadataName(SymbolNames.ITypelyBuilderOfDateTime),
compilation.GetTypeByMetadataName(SymbolNames.ITypelyBuilderOfDateTimeOffset),
compilation.GetTypeByMetadataName(SymbolNames.ITypelyBuilderOfInt),
compilation.GetTypeByMetadataName(SymbolNames.ITypelyBuilderOfDecimal),
compilation.GetTypeByMetadataName(SymbolNames.ITypelyBuilderOfFloat),
compilation.GetTypeByMetadataName(SymbolNames.ITypelyBuilderOfGuid),
compilation.GetTypeByMetadataName(SymbolNames.ITypelyBuilderOfDouble),
compilation.GetTypeByMetadataName(SymbolNames.ITypelyBuilderOfLong),
compilation.GetTypeByMetadataName(SymbolNames.ITypelyBuilderOfSByte),
compilation.GetTypeByMetadataName(SymbolNames.ITypelyBuilderOfShort),
compilation.GetTypeByMetadataName(SymbolNames.ITypelyBuilderOfString),
compilation.GetTypeByMetadataName(SymbolNames.ITypelyBuilderOfTimeOnly),
compilation.GetTypeByMetadataName(SymbolNames.ITypelyBuilderOfTimeSpan),
compilation.GetTypeByMetadataName(SymbolNames.ITypelyBuilderOfUInt),
compilation.GetTypeByMetadataName(SymbolNames.ITypelyBuilderOfULong),
compilation.GetTypeByMetadataName(SymbolNames.ITypelyBuilderOfUShort),
compilation.GetTypeByMetadataName(SymbolNames.IRuleBuilderOfBool),
compilation.GetTypeByMetadataName(SymbolNames.IRuleBuilderOfByte),
compilation.GetTypeByMetadataName(SymbolNames.IRuleBuilderOfChar),
compilation.GetTypeByMetadataName(SymbolNames.IRuleBuilderOfDateOnly),
compilation.GetTypeByMetadataName(SymbolNames.IRuleBuilderOfDateTime),
compilation.GetTypeByMetadataName(SymbolNames.IRuleBuilderOfDateTimeOffset),
compilation.GetTypeByMetadataName(SymbolNames.IRuleBuilderOfInt),
compilation.GetTypeByMetadataName(SymbolNames.IRuleBuilderOfDecimal),
compilation.GetTypeByMetadataName(SymbolNames.IRuleBuilderOfFloat),
compilation.GetTypeByMetadataName(SymbolNames.IRuleBuilderOfGuid),
compilation.GetTypeByMetadataName(SymbolNames.IRuleBuilderOfDouble),
compilation.GetTypeByMetadataName(SymbolNames.IRuleBuilderOfLong),
compilation.GetTypeByMetadataName(SymbolNames.IRuleBuilderOfSByte),
compilation.GetTypeByMetadataName(SymbolNames.IRuleBuilderOfShort),
compilation.GetTypeByMetadataName(SymbolNames.IRuleBuilderOfString),
compilation.GetTypeByMetadataName(SymbolNames.IRuleBuilderOfTimeOnly),
compilation.GetTypeByMetadataName(SymbolNames.IRuleBuilderOfTimeSpan),
compilation.GetTypeByMetadataName(SymbolNames.IRuleBuilderOfUInt),
compilation.GetTypeByMetadataName(SymbolNames.IRuleBuilderOfULong),
compilation.GetTypeByMetadataName(SymbolNames.IRuleBuilderOfUShort),
compilation.GetTypeByMetadataName(SymbolNames.IFactoryOfBool),
compilation.GetTypeByMetadataName(SymbolNames.IFactoryOfByte),
compilation.GetTypeByMetadataName(SymbolNames.IFactoryOfChar),
compilation.GetTypeByMetadataName(SymbolNames.IFactoryOfDateOnly),
compilation.GetTypeByMetadataName(SymbolNames.IFactoryOfDateTime),
compilation.GetTypeByMetadataName(SymbolNames.IFactoryOfDateTimeOffset),
compilation.GetTypeByMetadataName(SymbolNames.IFactoryOfInt),
compilation.GetTypeByMetadataName(SymbolNames.IFactoryOfDecimal),
compilation.GetTypeByMetadataName(SymbolNames.IFactoryOfFloat),
compilation.GetTypeByMetadataName(SymbolNames.IFactoryOfGuid),
compilation.GetTypeByMetadataName(SymbolNames.IFactoryOfDouble),
compilation.GetTypeByMetadataName(SymbolNames.IFactoryOfLong),
compilation.GetTypeByMetadataName(SymbolNames.IFactoryOfSByte),
compilation.GetTypeByMetadataName(SymbolNames.IFactoryOfShort),
compilation.GetTypeByMetadataName(SymbolNames.IFactoryOfString),
compilation.GetTypeByMetadataName(SymbolNames.IFactoryOfTimeOnly),
compilation.GetTypeByMetadataName(SymbolNames.IFactoryOfTimeSpan),
compilation.GetTypeByMetadataName(SymbolNames.IFactoryOfUInt),
compilation.GetTypeByMetadataName(SymbolNames.IFactoryOfULong),
compilation.GetTypeByMetadataName(SymbolNames.IFactoryOfUShort)
};
}

public INamedTypeSymbol? ITypelySpecification { get; }
public INamedTypeSymbol?[] SupportedVariablesTypes { get; }
}
}
5 changes: 0 additions & 5 deletions src/Typely.Generators/AnalyzerReleases.Unshipped.md

This file was deleted.

18 changes: 18 additions & 0 deletions src/Typely.Generators/Infrastructure/SymbolExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Microsoft.CodeAnalysis;

namespace Typely.Generators.Infrastructure;

public static class SymbolExtensions
{
public static bool Implements(this ITypeSymbol type, ITypeSymbol interfaceType)
{
foreach (var t in type.AllInterfaces)
{
if (SymbolEqualityComparer.Default.Equals(t, interfaceType))
{
return true;
}
}
return false;
}
}
4 changes: 0 additions & 4 deletions src/Typely.Generators/Typely.Generators.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,4 @@
<DependentUpon>Emitter.cs</DependentUpon>
</Compile>
</ItemGroup>

<ItemGroup>
<AdditionalFiles Include="AnalyzerReleases.Unshipped.md" />
</ItemGroup>
</Project>
53 changes: 48 additions & 5 deletions src/Typely.Generators/Typely/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,64 @@
using Microsoft.CodeAnalysis;

// ReSharper disable InconsistentNaming

namespace Typely.Generators.Typely;

public static class DiagnosticDescriptors
{
public static DiagnosticDescriptor UnsupportedExpression { get; } = new(
public static DiagnosticDescriptor TYP0001_UnsupportedExpression { get; } = new(
id: "TYP0001",
title: "Unsupported expression",
messageFormat: "The use of '{0}' is not allowed in a TypelySpecification.",
messageFormat: "The use of '{0}' is not supported in a TypelySpecification.",
category: "Design",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
public static DiagnosticDescriptor UnsupportedParameter { get; } = new(

public static DiagnosticDescriptor TYP0002_UnsupportedParameter { get; } = new(
id: "TYP0002",
title: "Unsupported parameter",
messageFormat: "'{0}' is not allowed as a parameter of '{1}' in the TypelySpecification. Instead use a string constant.",
messageFormat:
"'{0}' is not supported as a parameter of '{1}' in the TypelySpecification. Instead use a string constant.",
category: "Design",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static DiagnosticDescriptor TYP0003_UnsupportedMethod { get; } = new(
id: "TYP0003",
title: "Unsupported method",
messageFormat: "Custom methods are not supported in a TypelySpecification. Remove '{0}'.",
category: "Design",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static DiagnosticDescriptor TYP0004_UnsupportedField { get; } = new(
id: "TYP0004",
title: "Unsupported field",
messageFormat: "Custom fields are not supported in a TypelySpecification. Remove '{0}'.",
category: "Design",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static DiagnosticDescriptor TYP0005_UnsupportedProperty { get; } = new(
id: "TYP0005",
title: "Unsupported property",
messageFormat: "Custom properties are not supported in a TypelySpecification. Remove '{0}'.",
category: "Design",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static DiagnosticDescriptor TYP0006_UnsupportedType { get; } = new(
id: "TYP0006",
title: "Unsupported type",
messageFormat: "Custom types are not supported in a TypelySpecification. Remove '{0}'.",
category: "Design",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static DiagnosticDescriptor TYP0007_UnsupportedVariable { get; } = new(
id: "TYP0007",
title: "Unsupported variable",
messageFormat: "Variable type '{0}' is not supported for '{1}' in a TypelySpecification.",
category: "Design",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
Expand Down
Loading
Loading