Skip to content

Commit

Permalink
Restrict the use of TypelySpecification with an analyser (#37)
Browse files Browse the repository at this point in the history
Co-authored-by: Adam Paquette <nfs12@gmail.com>
  • Loading branch information
adampaquette and Adam Paquette committed Oct 9, 2023
1 parent 966f5f2 commit 7efd886
Show file tree
Hide file tree
Showing 21 changed files with 710 additions and 42 deletions.
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

0 comments on commit 7efd886

Please sign in to comment.