Skip to content

Commit

Permalink
Add report diagnostics + tests coverage (#28)
Browse files Browse the repository at this point in the history
  • Loading branch information
adampaquette committed May 2, 2023
1 parent e02c9fe commit 81728bf
Show file tree
Hide file tree
Showing 20 changed files with 666 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public void Configure(ITypelyBuilder builder)
{
//Contact
builder.OfInt().For("ContactId").GreaterThan(0);

builder.OfString()
.For("Phone")
.MaxLength(12)
Expand Down
16 changes: 8 additions & 8 deletions src/Typely.Core/ValidationErrorFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ public static class ValidationErrorFactory
placeholderValues.Add(ValidationPlaceholders.ActualLength, actualLength);
}

if (TypelyOptions.Instance.IsSensitiveDataLoggingEnabled)
{
if (!placeholderValues.ContainsKey(ValidationPlaceholders.Value))
{
placeholderValues.Add(ValidationPlaceholders.Value, value);
}
attemptedValue = value;
}
// if (TypelyOptions.Instance.IsSensitiveDataLoggingEnabled)
// {
// if (!placeholderValues.ContainsKey(ValidationPlaceholders.Value))
// {
// placeholderValues.Add(ValidationPlaceholders.Value, value);
// }
// attemptedValue = value;
// }

return new ValidationError(errorCode, errorMessageWithPlaceholders, attemptedValue, typeName, placeholderValues);
}
Expand Down
26 changes: 18 additions & 8 deletions src/Typely.Generators/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
namespace Typely.Generators;
using Microsoft.CodeAnalysis;

namespace Typely.Generators;

internal static class DiagnosticDescriptors
{
//public static DiagnosticDescriptor InvalidLoggingMethodName { get; } = new DiagnosticDescriptor(
//id: "SYSLIB1001",
//title: new LocalizableResourceString(nameof(SR.InvalidLoggingMethodNameMessage), SR.ResourceManager, typeof(FxResources.Microsoft.Extensions.Logging.Generators.SR)),
//messageFormat: new LocalizableResourceString(nameof(SR.InvalidLoggingMethodNameMessage), SR.ResourceManager, typeof(FxResources.Microsoft.Extensions.Logging.Generators.SR)),
//category: "LoggingGenerator",
//DiagnosticSeverity.Error,
//isEnabledByDefault: true);
public static DiagnosticDescriptor TypeNameMissing { get; } = new(
id: "TYPLY0001",
title: "Type name missing.",
messageFormat: "A type is declared without a name in the namespace '{0}'.",
category: "TypelyGenerator",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static DiagnosticDescriptor NameMissing { get; } = new(
id: "TYPLY0002",
title: "Name missing.",
messageFormat: "A type is declared without a user friendly name in the namespace '{0}'.",
category: "TypelyGenerator",
DiagnosticSeverity.Error,
isEnabledByDefault: true);
}
2 changes: 1 addition & 1 deletion src/Typely.Generators/Typely.Generators.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFramework>netstandard2.0</TargetFramework>
<IsRoslynComponent>true</IsRoslynComponent>
<Description>Typely lets you create types easily with a fluent API to embrace Domain-driven design and value objects. This package contains type generators.</Description>
<Version>1.4.5-alpha</Version>
<Version>1.4.6-alpha</Version>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>

Expand Down
84 changes: 43 additions & 41 deletions src/Typely.Generators/Typely/Emitting/Emitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,18 @@ public Emitter(Action<Diagnostic> reportDiagnostic, CancellationToken cancellati
_cancellationToken = cancellationToken;
}

public string Emit(EmittableType t)
public string? Emit(EmittableType t)
{
if (t.Name == null)
{
return string.Empty;
_reportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.NameMissing, Location.None, t.Namespace));
return null;
}

if (t.TypeName == null)
{
return string.Empty;
_reportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.TypeNameMissing, Location.None, t.Namespace));
return null;
}

var typeName = t.TypeName;
Expand All @@ -41,10 +43,10 @@ public string Emit(EmittableType t)
var safeNormalize = GenerateSafeNormalize(t);
var maxLengthInterface = GenerateMaxLengthInterface(t);
var properties = GenerateProperties(t);
var interrogationPoint = t.IsValueType ? string.Empty: "?";
var interrogationPoint = t.IsValueType ? string.Empty : "?";
var falseCoalescing = t.IsValueType ? string.Empty : " ?? false";
var oneCoalescing = t.IsValueType ? string.Empty : " ?? 1";

return $$"""
// <auto-generated>This file was generated by Typely.</auto-generated>
{{namespaces}}
Expand Down Expand Up @@ -104,8 +106,8 @@ public static bool TryFrom({{underlyingType}} value, out {{typeName}} typelyType
}

private string GenerateProperties(EmittableType emittableType) =>
emittableType.Properties.ContainsMaxLength()
? $"\n public static int MaxLength => {emittableType.Properties.GetMaxLength()};\n"
emittableType.Properties.ContainsMaxLength()
? $"\n public static int MaxLength => {emittableType.Properties.GetMaxLength()};\n"
: String.Empty;

private string GenerateMaxLengthInterface(EmittableType emittableType) =>
Expand Down Expand Up @@ -182,57 +184,57 @@ private string GenerateSafeNormalize(EmittableType emittableType)
}

private string GenerateValidations(EmittableType emittableType)
{
if (!emittableType.Rules.Any() && emittableType.IsValueType)
{
if (!emittableType.Rules.Any() && emittableType.IsValueType)
{
return " => null;";
}
return " => null;";
}

var builder = new StringBuilder("\n");
builder.AppendLine(" {");
var builder = new StringBuilder("\n");
builder.AppendLine(" {");

foreach (var rule in emittableType.Rules)
{
_cancellationToken.ThrowIfCancellationRequested();
var errorCode = rule.ErrorCode;
var placeholders = GenerateValidationPlaceholders(rule.PlaceholderValues);
foreach (var rule in emittableType.Rules)
{
_cancellationToken.ThrowIfCancellationRequested();
var errorCode = rule.ErrorCode;
var placeholders = GenerateValidationPlaceholders(rule.PlaceholderValues);

builder.AppendLine($$"""
builder.AppendLine($$"""
if ({{rule.Rule}})
{
return ValidationErrorFactory.Create(value, "{{errorCode}}", {{rule.Message}}, {{emittableType.Name}}{{placeholders}}
}
""")
.AppendLine();
}
.AppendLine();
}

builder
.AppendLine(" return null;")
.Append(" }");
builder
.AppendLine(" return null;")
.Append(" }");

return builder.ToString();
}
return builder.ToString();
}

private static string GenerateValidationPlaceholders(Dictionary<string, object?> placeholders)
private static string GenerateValidationPlaceholders(Dictionary<string, object?> placeholders)
{
if (!placeholders.Any())
{
if (!placeholders.Any())
{
return ");";
}
return ");";
}

var builder = new StringBuilder("""
var builder = new StringBuilder("""
,
new Dictionary<string, object?>
{
""")
.AppendLine();
.AppendLine();

foreach (var placeholder in placeholders)
{
builder.AppendLine($$""" { "{{placeholder.Key}}", {{placeholder.Value}} },""");
}

return builder.Append(" });")
.ToString();
foreach (var placeholder in placeholders)
{
builder.AppendLine($$""" { "{{placeholder.Key}}", {{placeholder.Value}} },""");
}
}

return builder.Append(" });")
.ToString();
}
}
2 changes: 1 addition & 1 deletion src/Typely.Generators/Typely/Parsing/EmittableType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ internal class EmittableType
/// <summary>
/// A set of dynamic properties. For example: MaxLength on string type.
/// </summary>
public TypeProperties Properties { get; set; } = new();
public TypeProperties Properties { get; } = new();

public EmittableType(string underlyingType, bool isValueType, string configurationNamespace)
{
Expand Down
21 changes: 5 additions & 16 deletions src/Typely.Generators/Typely/Parsing/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ public Parser(Compilation compilation, Action<Diagnostic> reportDiagnostic, Canc
}

/// <summary>
/// Filter classes having an interface name <see cref="ITypelyConfiguration"/>.
/// Filter classes having an interface name "ITypelyConfiguration".
/// </summary>
internal static bool IsTypelyConfigurationClass(SyntaxNode syntaxNode) =>
syntaxNode is ClassDeclarationSyntax c && IsTypelyConfigurationClass(c);

/// <summary>
/// Filter classes having an interface name <see cref="ITypelyConfiguration"/>.
/// Filter classes having an interface name "ITypelyConfiguration".
/// </summary>
private static bool IsTypelyConfigurationClass(ClassDeclarationSyntax syntax) =>
syntax.HasInterface(TypelyConfiguration.InterfaceName);
Expand All @@ -38,7 +38,7 @@ public Parser(Compilation compilation, Action<Diagnostic> reportDiagnostic, Canc
syntaxNode is MethodDeclarationSyntax c && c.Identifier.Text == TypelyConfiguration.ConfigureMethodName;

/// <summary>
/// Filter classes having an interface <see cref="ITypelyConfiguration"/> that matches the
/// Filter classes having an interface "ITypelyConfiguration" that matches the
/// namespace and returns the <see cref="ClassDeclarationSyntax"/>.
/// </summary>
internal static ClassDeclarationSyntax? GetSemanticTargetForGeneration(GeneratorSyntaxContext context)
Expand All @@ -52,18 +52,7 @@ public Parser(Compilation compilation, Action<Diagnostic> reportDiagnostic, Canc
}

/// <summary>
/// Execute the different <see cref="ITypelyConfiguration"/> classes founds and generate models of the desired user types.
/// </summary>
/// <param name="classes">Classes to parse.</param>
/// <returns>A list of representation of desired user types.</returns>
public IReadOnlyList<EmittableType> GetEmittableTypes(IEnumerable<ClassDeclarationSyntax> classes)
{
// We enumerate by syntax tree, to minimize impact on performance
return classes.GroupBy(x => x.SyntaxTree).SelectMany(x => GetEmittableTypes(x.Key)).ToList().AsReadOnly();
}

/// <summary>
/// Execute the different <see cref="ITypelyConfiguration"/> classes and generate models of the desired user types.
/// Execute the different "ITypelyConfiguration" classes and generate models of the desired user types.
/// </summary>
/// <param name="syntaxTree">SyntaxTree to parse</param>
/// <returns>A list of representation of desired user types.</returns>
Expand Down Expand Up @@ -114,7 +103,7 @@ private IEnumerable<EmittableType> ParseClass(ClassDeclarationSyntax classSyntax
/// Parse each line of code of a method.
/// </summary>
/// <param name="methodDeclarationSyntax">The <see cref="MethodDeclarationSyntax"/>.</param>
/// <param name="typelyBuilderParameterName">The builder parameter name used in <see cref="ITypelyConfiguration.Configure"/>.</param>
/// <param name="typelyBuilderParameterName">The builder parameter name used in "ITypelyConfiguration.Configure".</param>
/// <param name="model">The <see cref="SemanticModel"/>.</param>
/// <returns>Return a list of <see cref="ParseDeclarationStatement"/>.</returns>
private static List<ParsedStatement> ParseStatements(MethodDeclarationSyntax methodDeclarationSyntax,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ internal static class EmittableTypeBuilderFactory
return new EmittableTypeBuilderOfULong(defaultNamespace, invocations, model);
case TypelyBuilder.OfUShort:
return new EmittableTypeBuilderOfUShort(defaultNamespace, invocations, model);
default: throw new InvalidOperationException($"Unknown builder type: {builderType}");
default: throw new NotSupportedException($"Unknown builder type: {builderType}");
}
}
}
5 changes: 5 additions & 0 deletions src/Typely.Generators/Typely/TypelyGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ private static void Execute(ClassDeclarationSyntax classSyntax, Compilation comp
context.CancellationToken.ThrowIfCancellationRequested();

var source = emitter.Emit(emittableType);
if(source is null)
{
continue;
}

context.AddSource($"{emittableType.Namespace}.{emittableType.TypeName}.g.cs", SourceText.From(source, Encoding.UTF8));
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Typely.Core;
using Typely.Core.Builders;

namespace Typely.Generators.Tests.Typely.Configurations;

public class MissingTypeNameConfiguration : ITypelyConfiguration
{
public void Configure(ITypelyBuilder builder)
{
builder.OfBool();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ public void Configure(ITypelyBuilder builder)
{
builder.OfString().For("Code")
.AsClass()
.Length(4)
.WithName(() => "Code")
.Length(21)
.Length(4, 20)
.NotEqual("0000")
.Matches(new Regex(".+"))
.GreaterThan("A")
.GreaterThanOrEqualTo("A")
.LessThan("A")
.MinLength(2)
.LessThanOrEqualTo("A")
.Normalize(x => x.Trim().ToLower());

Expand Down
29 changes: 29 additions & 0 deletions tests/Typely.Generators.Tests/Typely/Emitting/EmitterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Microsoft.CodeAnalysis;
using Typely.Generators.Typely.Emitting;
using Typely.Generators.Typely.Parsing;

namespace Typely.Generators.Tests.Typely.Emitting;

public class EmitterTests
{
[Fact]
public void Emitter_Should_OutputDiagnostics_When_TypeNameIsNull()
{
var diagnostics = new List<Diagnostic>();
var emitter = new Emitter(diagnostic => diagnostics.Add(diagnostic), CancellationToken.None);
var emittableType = new EmittableType("int", true, "namespace");
emittableType.SetName("aa");
emitter.Emit(emittableType);
Assert.NotEmpty(diagnostics);
}

[Fact]
public void Emitter_Should_OutputDiagnostics_When_NameIsNull()
{
var diagnostics = new List<Diagnostic>();
var emitter = new Emitter(diagnostic => diagnostics.Add(diagnostic), CancellationToken.None);
var emittableType = new EmittableType("int", true, "namespace");
emitter.Emit(emittableType);
Assert.NotEmpty(diagnostics);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Typely.Generators.Typely.Parsing;
using Typely.Generators.Typely.Parsing.TypeBuilders;

namespace Typely.Generators.Tests.Typely.Parsing;

public class EmittableTypeBuilderFactoryTests
{
[Fact]
public void UnsupportedType_Should_Throw()
{
var statement = new ParsedStatement(null!) { Root = "builder", };
statement.Invocations.Add(new ParsedInvocation(null!, "OfUnsupportedType"));

Assert.Throws<NotSupportedException>(() => EmittableTypeBuilderFactory.Create("namespace", statement));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ public ParserFixture WithConfigurations(params Type[] configClasses)
_syntaxTrees = configClasses.Select(CreateSyntaxTree);
return this;
}

public ParserFixture WithSyntaxTrees(params SyntaxTree[] syntaxTrees)
{
_syntaxTrees = syntaxTrees;
return this;
}

public static SyntaxTree CreateSyntaxTree(Type configClass)
{
Expand All @@ -52,6 +58,5 @@ private static string GetFilePath(Type configClass)
references: new[]
{
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(typeof(ITypelyConfiguration).Assembly.Location),
});
}
Loading

0 comments on commit 81728bf

Please sign in to comment.