diff --git a/src/Typely.Generators/Analysers/TypelySpecificationAnalyser.cs b/src/Typely.Generators/Analysers/TypelySpecificationAnalyser.cs new file mode 100644 index 0000000..5a7079d --- /dev/null +++ b/src/Typely.Generators/Analysers/TypelySpecificationAnalyser.cs @@ -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 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() + .FirstOrDefault(x => x.Identifier.Text == SymbolNames.CreateMethod); + + if (createMethodDeclaration is null) + { + return; + } + + var localDeclarations = createMethodDeclaration.DescendantNodes().OfType(); + 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; } + } +} \ No newline at end of file diff --git a/src/Typely.Generators/AnalyzerReleases.Unshipped.md b/src/Typely.Generators/AnalyzerReleases.Unshipped.md deleted file mode 100644 index f197394..0000000 --- a/src/Typely.Generators/AnalyzerReleases.Unshipped.md +++ /dev/null @@ -1,5 +0,0 @@ -### New Rules - -Rule ID | Category | Severity | Notes ---------|----------|----------|------------------------------ -TYP0001 | Design | Error | TYP0001_UnsupportedExpression \ No newline at end of file diff --git a/src/Typely.Generators/Infrastructure/SymbolExtensions.cs b/src/Typely.Generators/Infrastructure/SymbolExtensions.cs new file mode 100644 index 0000000..ab7caa0 --- /dev/null +++ b/src/Typely.Generators/Infrastructure/SymbolExtensions.cs @@ -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; + } +} \ No newline at end of file diff --git a/src/Typely.Generators/Typely.Generators.csproj b/src/Typely.Generators/Typely.Generators.csproj index 75cbeca..8ceab1b 100644 --- a/src/Typely.Generators/Typely.Generators.csproj +++ b/src/Typely.Generators/Typely.Generators.csproj @@ -46,8 +46,4 @@ Emitter.cs - - - - diff --git a/src/Typely.Generators/Typely/DiagnosticDescriptors.cs b/src/Typely.Generators/Typely/DiagnosticDescriptors.cs index 84a4c6a..3298b78 100644 --- a/src/Typely.Generators/Typely/DiagnosticDescriptors.cs +++ b/src/Typely.Generators/Typely/DiagnosticDescriptors.cs @@ -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); diff --git a/src/Typely.Generators/Typely/Parsing/Parser.cs b/src/Typely.Generators/Typely/Parsing/Parser.cs index 471ebc0..06b2b84 100644 --- a/src/Typely.Generators/Typely/Parsing/Parser.cs +++ b/src/Typely.Generators/Typely/Parsing/Parser.cs @@ -20,8 +20,8 @@ internal static class Parser /// /// Filter classes having an interface name "ITypelySpecification". /// - private static bool IsTypelySpecificationClass(ClassDeclarationSyntax syntax) => - syntax.HasInterface(TypelySpecification.InterfaceName); + internal static bool IsTypelySpecificationClass(ClassDeclarationSyntax syntax) => + syntax.HasInterface(SymbolNames.ITypelySpecificationName); /// /// Filter classes having an interface full name "Typely.Core.ITypelySpecification". @@ -29,7 +29,7 @@ internal static class Parser private static bool IsTypelySpecificationClass(SemanticModel model, ClassDeclarationSyntax classDeclarationSyntax) { var classSymbol = model.GetDeclaredSymbol(classDeclarationSyntax)!; - return classSymbol.AllInterfaces.Any(x => x.ToString() == TypelySpecification.FullInterfaceName); + return classSymbol.AllInterfaces.Any(x => x.ToString() == SymbolNames.ITypelySpecification); } /// @@ -83,7 +83,7 @@ private static bool IsTypelySpecificationClass(SemanticModel model, ClassDeclara /// Filter methods having a name that matches . /// private static bool IsCreateMethod(SyntaxNode syntaxNode) => - syntaxNode is MethodDeclarationSyntax { Identifier.Text: TypelySpecification.MethodName }; + syntaxNode is MethodDeclarationSyntax { Identifier.Text: SymbolNames.CreateMethod }; /// /// Parse a and generate a list of . @@ -264,9 +264,11 @@ private static string GetNamespace(SyntaxNode classSyntax) } else { - var diagnostic = Diagnostic.Create(DiagnosticDescriptors.UnsupportedExpression, - invocationExpressionSyntax.GetLocation(), invocationExpressionSyntax.Expression); - diagnostics.Add(diagnostic); + diagnostics.Add(Diagnostic.Create( + DiagnosticDescriptors.TYP0001_UnsupportedExpression, + invocationExpressionSyntax.GetLocation(), + invocationExpressionSyntax.Expression)); + return false; } } @@ -277,9 +279,11 @@ private static string GetNamespace(SyntaxNode classSyntax) } else { - var diagnostic = Diagnostic.Create(DiagnosticDescriptors.UnsupportedExpression, syntaxNode.GetLocation(), - syntaxNode); - diagnostics.Add(diagnostic); + diagnostics.Add(Diagnostic.Create( + DiagnosticDescriptors.TYP0001_UnsupportedExpression, + syntaxNode.GetLocation(), + syntaxNode)); + return false; } @@ -290,21 +294,23 @@ private static string GetNamespace(SyntaxNode classSyntax) { if (!SupportedMembers.All.Contains(memberName)) { - var diagnostic = Diagnostic.Create(DiagnosticDescriptors.UnsupportedExpression, + diagnostics.Add(Diagnostic.Create( + DiagnosticDescriptors.TYP0001_UnsupportedExpression, memberAccessExpressionSyntax.GetLocation(), - memberAccessExpressionSyntax.Name.GetText()); - diagnostics.Add(diagnostic); + memberAccessExpressionSyntax.Name.GetText())); + return false; } if (argumentList.Arguments.Any() && argumentList.Arguments.First().Expression is IdentifierNameSyntax identifierNameSyntax) { - var diagnostic = Diagnostic.Create(DiagnosticDescriptors.UnsupportedParameter, + diagnostics.Add(Diagnostic.Create( + DiagnosticDescriptors.TYP0002_UnsupportedParameter, memberAccessExpressionSyntax.GetLocation(), identifierNameSyntax.Identifier.Text, - memberAccessExpressionSyntax.Name.GetText()); - diagnostics.Add(diagnostic); + memberAccessExpressionSyntax.Name.GetText())); + return false; } diff --git a/src/Typely.Generators/Typely/SymbolNames.cs b/src/Typely.Generators/Typely/SymbolNames.cs new file mode 100644 index 0000000..9755c3f --- /dev/null +++ b/src/Typely.Generators/Typely/SymbolNames.cs @@ -0,0 +1,71 @@ +namespace Typely.Generators.Typely; + +public static class SymbolNames +{ + public const string CreateMethod = "Create"; + public const string ITypelySpecificationName = "ITypelySpecification"; + public const string ITypelySpecification = "Typely.Core.ITypelySpecification"; + + public const string ITypelyBuilderOfBool = "Typely.Core.Builders.ITypelyBuilderOfBool"; + public const string ITypelyBuilderOfByte = "Typely.Core.Builders.ITypelyBuilderOfByte"; + public const string ITypelyBuilderOfChar = "Typely.Core.Builders.ITypelyBuilderOfChar"; + public const string ITypelyBuilderOfDateOnly = "Typely.Core.Builders.ITypelyBuilderOfDateOnly"; + public const string ITypelyBuilderOfDateTime = "Typely.Core.Builders.ITypelyBuilderOfDateTime"; + public const string ITypelyBuilderOfDateTimeOffset = "Typely.Core.Builders.ITypelyBuilderOfDateTimeOffset"; + public const string ITypelyBuilderOfInt = "Typely.Core.Builders.ITypelyBuilderOfInt"; + public const string ITypelyBuilderOfDecimal = "Typely.Core.Builders.ITypelyBuilderOfDecimal"; + public const string ITypelyBuilderOfFloat = "Typely.Core.Builders.ITypelyBuilderOfFloat"; + public const string ITypelyBuilderOfGuid = "Typely.Core.Builders.ITypelyBuilderOfGuid"; + public const string ITypelyBuilderOfDouble = "Typely.Core.Builders.ITypelyBuilderOfDouble"; + public const string ITypelyBuilderOfLong = "Typely.Core.Builders.ITypelyBuilderOfLong"; + public const string ITypelyBuilderOfSByte = "Typely.Core.Builders.ITypelyBuilderOfSByte"; + public const string ITypelyBuilderOfShort = "Typely.Core.Builders.ITypelyBuilderOfShort"; + public const string ITypelyBuilderOfString = "Typely.Core.Builders.ITypelyBuilderOfString"; + public const string ITypelyBuilderOfTimeOnly = "Typely.Core.Builders.ITypelyBuilderOfTimeOnly"; + public const string ITypelyBuilderOfTimeSpan = "Typely.Core.Builders.ITypelyBuilderOfTimeSpan"; + public const string ITypelyBuilderOfUInt = "Typely.Core.Builders.ITypelyBuilderOfUInt"; + public const string ITypelyBuilderOfULong = "Typely.Core.Builders.ITypelyBuilderOfULong"; + public const string ITypelyBuilderOfUShort = "Typely.Core.Builders.ITypelyBuilderOfUShort"; + + public const string IRuleBuilderOfBool = "Typely.Core.Builders.IRuleBuilderOfBool"; + public const string IRuleBuilderOfByte = "Typely.Core.Builders.IRuleBuilderOfByte"; + public const string IRuleBuilderOfChar = "Typely.Core.Builders.IRuleBuilderOfChar"; + public const string IRuleBuilderOfDateOnly = "Typely.Core.Builders.IRuleBuilderOfDateOnly"; + public const string IRuleBuilderOfDateTime = "Typely.Core.Builders.IRuleBuilderOfDateTime"; + public const string IRuleBuilderOfDateTimeOffset = "Typely.Core.Builders.IRuleBuilderOfDateTimeOffset"; + public const string IRuleBuilderOfInt = "Typely.Core.Builders.IRuleBuilderOfInt"; + public const string IRuleBuilderOfDecimal = "Typely.Core.Builders.IRuleBuilderOfDecimal"; + public const string IRuleBuilderOfFloat = "Typely.Core.Builders.IRuleBuilderOfFloat"; + public const string IRuleBuilderOfGuid = "Typely.Core.Builders.IRuleBuilderOfGuid"; + public const string IRuleBuilderOfDouble = "Typely.Core.Builders.IRuleBuilderOfDouble"; + public const string IRuleBuilderOfLong = "Typely.Core.Builders.IRuleBuilderOfLong"; + public const string IRuleBuilderOfSByte = "Typely.Core.Builders.IRuleBuilderOfSByte"; + public const string IRuleBuilderOfShort = "Typely.Core.Builders.IRuleBuilderOfShort"; + public const string IRuleBuilderOfString = "Typely.Core.Builders.IRuleBuilderOfString"; + public const string IRuleBuilderOfTimeOnly = "Typely.Core.Builders.IRuleBuilderOfTimeOnly"; + public const string IRuleBuilderOfTimeSpan = "Typely.Core.Builders.IRuleBuilderOfTimeSpan"; + public const string IRuleBuilderOfUInt = "Typely.Core.Builders.IRuleBuilderOfUInt"; + public const string IRuleBuilderOfULong = "Typely.Core.Builders.IRuleBuilderOfULong"; + public const string IRuleBuilderOfUShort = "Typely.Core.Builders.IRuleBuilderOfUShort"; + + public const string IFactoryOfBool = "Typely.Core.Builders.IFactoryOfBool"; + public const string IFactoryOfByte = "Typely.Core.Builders.IFactoryOfByte"; + public const string IFactoryOfChar = "Typely.Core.Builders.IFactoryOfChar"; + public const string IFactoryOfDateOnly = "Typely.Core.Builders.IFactoryOfDateOnly"; + public const string IFactoryOfDateTime = "Typely.Core.Builders.IFactoryOfDateTime"; + public const string IFactoryOfDateTimeOffset = "Typely.Core.Builders.IFactoryOfDateTimeOffset"; + public const string IFactoryOfInt = "Typely.Core.Builders.IFactoryOfInt"; + public const string IFactoryOfDecimal = "Typely.Core.Builders.IFactoryOfDecimal"; + public const string IFactoryOfFloat = "Typely.Core.Builders.IFactoryOfFloat"; + public const string IFactoryOfGuid = "Typely.Core.Builders.IFactoryOfGuid"; + public const string IFactoryOfDouble = "Typely.Core.Builders.IFactoryOfDouble"; + public const string IFactoryOfLong = "Typely.Core.Builders.IFactoryOfLong"; + public const string IFactoryOfSByte = "Typely.Core.Builders.IFactoryOfSByte"; + public const string IFactoryOfShort = "Typely.Core.Builders.IFactoryOfShort"; + public const string IFactoryOfString = "Typely.Core.Builders.IFactoryOfString"; + public const string IFactoryOfTimeOnly = "Typely.Core.Builders.IFactoryOfTimeOnly"; + public const string IFactoryOfTimeSpan = "Typely.Core.Builders.IFactoryOfTimeSpan"; + public const string IFactoryOfUInt = "Typely.Core.Builders.IFactoryOfUInt"; + public const string IFactoryOfULong = "Typely.Core.Builders.IFactoryOfULong"; + public const string IFactoryOfUShort = "Typely.Core.Builders.IFactoryOfUShort"; +} \ No newline at end of file diff --git a/src/Typely.Generators/Typely/TypelySpecification.cs b/src/Typely.Generators/Typely/TypelySpecification.cs deleted file mode 100644 index b13d501..0000000 --- a/src/Typely.Generators/Typely/TypelySpecification.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Typely.Generators.Typely; - -public static class TypelySpecification -{ - public const string InterfaceName = "ITypelySpecification"; - public const string FullInterfaceName = "Typely.Core.ITypelySpecification"; - public const string MethodName = "Create"; -} \ No newline at end of file diff --git a/tests/Typely.Generators.Tests/Analysers/TypelySpecificationAnalyserFixture.cs b/tests/Typely.Generators.Tests/Analysers/TypelySpecificationAnalyserFixture.cs new file mode 100644 index 0000000..850139c --- /dev/null +++ b/tests/Typely.Generators.Tests/Analysers/TypelySpecificationAnalyserFixture.cs @@ -0,0 +1,30 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Immutable; +// ReSharper disable InconsistentNaming + +namespace Typely.Generators.Tests.Analysers; + +internal static class AnalyserRunner +{ + private const string CS5001_ProgramDoesNotContainValidEntryPointId = "CS5001"; + private const string CS0012_TypeIsDefinedInAnAssemblyThatIsNotReferenced = "CS0012"; + + private static readonly string[] DisabledDiagnostics = { + CS5001_ProgramDoesNotContainValidEntryPointId, + CS0012_TypeIsDefinedInAnAssemblyThatIsNotReferenced + }; + + public static async Task> GetDiagnostics() + where TAnalyser : DiagnosticAnalyzer, new() + { + var compilation = + new CompilationWithAnalysersFixture() + .WithSpecification() + .WithAnalyser() + .Create(); + + var diagnostics = await compilation.GetAllDiagnosticsAsync(); + return diagnostics.Where(x => !DisabledDiagnostics.Contains(x.Id)).ToImmutableArray(); + } +} \ No newline at end of file diff --git a/tests/Typely.Generators.Tests/Analysers/TypelySpecificationAnalyserTests.cs b/tests/Typely.Generators.Tests/Analysers/TypelySpecificationAnalyserTests.cs new file mode 100644 index 0000000..33da28b --- /dev/null +++ b/tests/Typely.Generators.Tests/Analysers/TypelySpecificationAnalyserTests.cs @@ -0,0 +1,53 @@ +using Microsoft.CodeAnalysis; +using System.Collections.Immutable; +using Typely.Generators.Analysers; +using Typely.Generators.Tests.Typely.Specifications.Diagnostics; +using Typely.Generators.Typely; + +namespace Typely.Generators.Tests.Analysers; + +public class TypelySpecificationAnalyserTests +{ + [Fact] + public async Task DetectUnsupportedMethods() + { + var diagnostics = await GetDiagnostics(); + + diagnostics.ShouldContainExactlyNDiagnosticsWithId(1, DiagnosticDescriptors.TYP0003_UnsupportedMethod.Id); + } + + [Fact] + public async Task DetectUnsupportedProperties() + { + var diagnostics = await GetDiagnostics(); + + diagnostics.ShouldContainExactlyNDiagnosticsWithId(1, DiagnosticDescriptors.TYP0005_UnsupportedProperty.Id); + } + + [Fact] + public async Task DetectUnsupportedFields() + { + var diagnostics = await GetDiagnostics(); + + diagnostics.ShouldContainExactlyNDiagnosticsWithId(2, DiagnosticDescriptors.TYP0004_UnsupportedField.Id); + } + + [Fact] + public async Task DetectUnsupportedTypes() + { + var diagnostics = await GetDiagnostics(); + + diagnostics.ShouldContainExactlyNDiagnosticsWithId(4, DiagnosticDescriptors.TYP0006_UnsupportedType.Id); + } + + [Fact] + public async Task DetectUnsupportedVariables() + { + var diagnostics = await GetDiagnostics(); + + diagnostics.ShouldContainExactlyNDiagnosticsWithId(3, DiagnosticDescriptors.TYP0007_UnsupportedVariable.Id); + } + + private async Task> GetDiagnostics() => + await AnalyserRunner.GetDiagnostics(); +} \ No newline at end of file diff --git a/tests/Typely.Generators.Tests/CompilationFixture.cs b/tests/Typely.Generators.Tests/CompilationFixture.cs new file mode 100644 index 0000000..777ce97 --- /dev/null +++ b/tests/Typely.Generators.Tests/CompilationFixture.cs @@ -0,0 +1,81 @@ +using AutoFixture; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using System.Text.RegularExpressions; +using Typely.Core; + +namespace Typely.Generators.Tests; + +internal class CompilationFixture : CompilationFixture + where TFixture : CompilationFixture +{ +} + +internal class CompilationFixture : BaseFixture where TFixture : CompilationFixture +{ + private IEnumerable _syntaxTrees = new List(); + + public CompilationFixture() + { + Fixture.Register(CreateCompilation); + } + + public TFixture WithSpecification() + { + _syntaxTrees = new[] { CreateSyntaxTree(typeof(TSpecification)) }; + return (TFixture)this; + } + + public TFixture WithSpecifications(params Type[] specClasses) + { + _syntaxTrees = specClasses.Select(CreateSyntaxTree); + return (TFixture)this; + } + + public TFixture WithoutSpecification() + { + _syntaxTrees = new List { CSharpSyntaxTree.ParseText("public class EmptyClass {}") }; + return (TFixture)this; + } + + private static SyntaxTree CreateSyntaxTree(string filePath) + { + var source = File.ReadAllText(filePath); + return CSharpSyntaxTree.ParseText(source, path: filePath); + } + + public static SyntaxTree CreateSyntaxTree(Type configClass) + { + string sourceFilePath = GetFilePath(configClass); + return CreateSyntaxTree(sourceFilePath); + } + + private static string GetFilePath(Type configClass) + { + var pathFromNamespace = configClass.FullName!.Replace("Typely.Generators.Tests", "").Replace(".", "/"); + + //Remove nested class path + pathFromNamespace = Regex.Replace(pathFromNamespace, @"(.+)\/(.+)\+(.+)", "$1/$3"); + + //Add base path for class without namespace + if (!pathFromNamespace.Contains("/Typely/Specifications/")) + { + pathFromNamespace = $"Typely/Specifications/{pathFromNamespace}"; + } + + return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"../../../{pathFromNamespace}.cs"); + } + + protected Compilation CreateCompilation() => + CSharpCompilation.Create( + assemblyName: "tests", + syntaxTrees: _syntaxTrees, + references: new[] + { + MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location), + MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.Linq.Expressions.Expression).Assembly.Location), + MetadataReference.CreateFromFile(typeof(ITypelySpecification).Assembly.Location), + }); +} \ No newline at end of file diff --git a/tests/Typely.Generators.Tests/CompilationWithAnalysersFixture.cs b/tests/Typely.Generators.Tests/CompilationWithAnalysersFixture.cs new file mode 100644 index 0000000..0d769ef --- /dev/null +++ b/tests/Typely.Generators.Tests/CompilationWithAnalysersFixture.cs @@ -0,0 +1,27 @@ +using AutoFixture; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Immutable; + +namespace Typely.Generators.Tests; + +internal class CompilationWithAnalysersFixture : CompilationFixture +{ + private ImmutableArray _diagnosticAnalyzers; + + public CompilationWithAnalysersFixture() + { + Fixture.Register(() => CreateCompilation().WithAnalyzers(_diagnosticAnalyzers)); + } + + public CompilationWithAnalysersFixture WithAnalysers(ImmutableArray diagnosticAnalyzers) + { + _diagnosticAnalyzers = diagnosticAnalyzers; + return this; + } + + public CompilationWithAnalysersFixture WithAnalyser() where TAnalyser : DiagnosticAnalyzer, new() + { + _diagnosticAnalyzers = ImmutableArray.Create(new TAnalyser()); + return this; + } +} \ No newline at end of file diff --git a/tests/Typely.Generators.Tests/TestExtensions.cs b/tests/Typely.Generators.Tests/TestExtensions.cs new file mode 100644 index 0000000..16f11ca --- /dev/null +++ b/tests/Typely.Generators.Tests/TestExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.CodeAnalysis; +using System.Collections.Immutable; + +namespace Typely.Generators.Tests; + +public static class TestExtensions +{ + internal static void ShouldContainExactlyNDiagnosticsWithId(this ImmutableArray diagnostics, int n, string diagnosticId) + { + Assert.Equal(n, diagnostics.Count(x => x.Id == diagnosticId)); + } +} \ No newline at end of file diff --git a/tests/Typely.Generators.Tests/Typely.Generators.Tests.csproj b/tests/Typely.Generators.Tests/Typely.Generators.Tests.csproj index 2118e2a..1db603c 100644 --- a/tests/Typely.Generators.Tests/Typely.Generators.Tests.csproj +++ b/tests/Typely.Generators.Tests/Typely.Generators.Tests.csproj @@ -17,8 +17,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/tests/Typely.Generators.Tests/Typely/Specifications/Diagnostics/UnsupportedFieldsSpecification.cs b/tests/Typely.Generators.Tests/Typely/Specifications/Diagnostics/UnsupportedFieldsSpecification.cs new file mode 100644 index 0000000..cccaaa3 --- /dev/null +++ b/tests/Typely.Generators.Tests/Typely/Specifications/Diagnostics/UnsupportedFieldsSpecification.cs @@ -0,0 +1,16 @@ +using Typely.Core; +using Typely.Core.Builders; + +namespace Typely.Generators.Tests.Typely.Specifications.Diagnostics; + +public class UnsupportedFieldsSpecification: ITypelySpecification +{ + private const string Field1 = "Field1"; + private string Field2 = "Field2"; + + public void Create(ITypelyBuilder builder) + { + builder.OfString().For(Field1); + builder.OfString().For(Field2); + } +} \ No newline at end of file diff --git a/tests/Typely.Generators.Tests/Typely/Specifications/Diagnostics/UnsupportedMethodsSpecification.cs b/tests/Typely.Generators.Tests/Typely/Specifications/Diagnostics/UnsupportedMethodsSpecification.cs new file mode 100644 index 0000000..10fd923 --- /dev/null +++ b/tests/Typely.Generators.Tests/Typely/Specifications/Diagnostics/UnsupportedMethodsSpecification.cs @@ -0,0 +1,26 @@ +using Typely.Core; +using Typely.Core.Builders; + +namespace Typely.Generators.Tests.Typely.Specifications.Diagnostics; + +public class UnsupportedMethodsSpecification: ITypelySpecification +{ + public void Create(ITypelyBuilder builder) + { + builder.OfString().For("Unsupported").UnsupportedExtension(); + + CreateTypeFromUnsupportedCall(builder); + + var unsupported = CreateTypeFromUnsupportedCall(builder); + unsupported.NotEmpty(); + } + + public IRuleBuilderOfInt CreateTypeFromUnsupportedCall(ITypelyBuilder builder) => + builder.OfInt().For("AddressId").GreaterThan(0); +} + +internal static class TypelyBuilderExtensions +{ + public static void UnsupportedExtension(this ITypelyBuilderOfString builder) => + builder.MinLength(3).MaxLength(20).Normalize(x => x.ToUpper()); +} \ No newline at end of file diff --git a/tests/Typely.Generators.Tests/Typely/Specifications/Diagnostics/UnsupportedPropertiesSpecification.cs b/tests/Typely.Generators.Tests/Typely/Specifications/Diagnostics/UnsupportedPropertiesSpecification.cs new file mode 100644 index 0000000..6510923 --- /dev/null +++ b/tests/Typely.Generators.Tests/Typely/Specifications/Diagnostics/UnsupportedPropertiesSpecification.cs @@ -0,0 +1,14 @@ +using Typely.Core; +using Typely.Core.Builders; + +namespace Typely.Generators.Tests.Typely.Specifications.Diagnostics; + +public class UnsupportedPropertiesSpecification : ITypelySpecification +{ + private string TypeName => "MyType"; + + public void Create(ITypelyBuilder builder) + { + builder.OfString().For(TypeName); + } +} diff --git a/tests/Typely.Generators.Tests/Typely/Specifications/Diagnostics/UnsupportedTypesSpecification.cs b/tests/Typely.Generators.Tests/Typely/Specifications/Diagnostics/UnsupportedTypesSpecification.cs new file mode 100644 index 0000000..025ac9b --- /dev/null +++ b/tests/Typely.Generators.Tests/Typely/Specifications/Diagnostics/UnsupportedTypesSpecification.cs @@ -0,0 +1,25 @@ +using Typely.Core; +using Typely.Core.Builders; + +namespace Typely.Generators.Tests.Typely.Specifications.Diagnostics; + +public class UnsupportedTypesSpecification : ITypelySpecification +{ + public class A + { + } + + public record B + { + } + + private struct C + { + } + + protected enum D { } + + public void Create(ITypelyBuilder builder) + { + } +} \ No newline at end of file diff --git a/tests/Typely.Generators.Tests/Typely/Specifications/Diagnostics/UnsupportedVariablesSpecification.cs b/tests/Typely.Generators.Tests/Typely/Specifications/Diagnostics/UnsupportedVariablesSpecification.cs new file mode 100644 index 0000000..7077299 --- /dev/null +++ b/tests/Typely.Generators.Tests/Typely/Specifications/Diagnostics/UnsupportedVariablesSpecification.cs @@ -0,0 +1,24 @@ +using Typely.Core; +using Typely.Core.Builders; + +namespace Typely.Generators.Tests.Typely.Specifications.Diagnostics; + +public class UnsupportedVariablesSpecification : ITypelySpecification +{ + public void Create(ITypelyBuilder builder) + { + var factory = builder.OfString().AsFactory(); + factory.For("BasicType"); + + const string constVar1 = "constVar1"; + string var2 = "var2"; + int i = 0; + + builder.OfString().For(constVar1); + builder.OfString().For(var2); + builder.OfString().For(i.ToString()); + + var number = builder.OfInt().LessThan(100); + number.For("A"); + } +} \ No newline at end of file diff --git a/tests/Typely.Tests/CompleteTypelySpecification.cs b/tests/Typely.Tests/CompleteTypelySpecification.cs index 7a6ccd6..21a5647 100644 --- a/tests/Typely.Tests/CompleteTypelySpecification.cs +++ b/tests/Typely.Tests/CompleteTypelySpecification.cs @@ -21,6 +21,6 @@ public void Create(ITypelyBuilder builder) .WithName("Owner identifier") .AsStruct() .NotEmpty().WithMessage("'Name' cannot be empty.").WithErrorCode("ERR001") - .NotEqual("1"); + .NotEqual("1"); } } \ No newline at end of file diff --git a/tests/Typely.Tests/Typely.Tests.csproj b/tests/Typely.Tests/Typely.Tests.csproj index 5c7b26f..56eb8b0 100644 --- a/tests/Typely.Tests/Typely.Tests.csproj +++ b/tests/Typely.Tests/Typely.Tests.csproj @@ -8,7 +8,6 @@ -