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

typed parameters #15

Merged
merged 17 commits into from
Nov 12, 2022
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# TYPEALIZR000010: TargetProjectRootDirectoryNotFound
# TR0001: TargetProjectRootDirectoryNotFound

## Cause
The code generator could not determine the projects root-directory
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# TYPEALIZR001010: AmbigiousRessourceKey
# TR0002: AmbigiousRessourceKey

## Cause
The processed Resx-file contains keys that end up in possible duplicate extension-method names.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# TYPEALIZR001011: UnnamedGenericParameter
# TR0003: UnnamedGenericParameter

## Cause
The processed Resx-file contains a key that uses a generic format-parameter like '{0}', '{1}', etc.
Expand Down
84 changes: 84 additions & 0 deletions docs/reference/TR0004_UnrecognizedParameterType.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# TR0004: UnrecognizedParameterType

## Cause
The processed Resx-file contains a key that uses a parameter with an unrecognized type-annotation

## Rule description

When extracting parameters for a method-signature, the source-generator tries to infer a better suitable parameter-type to be use when constructing the method-signature.
This is done by inspecting the parameter-token after the colon `:`, if present.

> e.g. `xyz` as �n `hello {world:xyz}`

If no parameter-annotation was found, the generator just assumes to use `object`.

When a value is found, it is matched against the following built-in data-types, in order to generate a stricter typed method-signature:

|alias | type | example usage (alias) | example usage (type) |
|-------|-----------------|-----------------------|-----------------------|
| i | int | {count:i} | {count:int} |
| s | string | {name:s} | {name:string} |
| dt | DateTime | {date:dt} | {date:DateTime} |
| dto | DateTimeOffset | {date:dto} | {date:DateTimeOffset} |
| d | DateOnly | {date:d} | {date:DateOnly} |
| t | TimeOnly | {time:t} | {time:TimeOnly} |

However, when the provided parameter-annotation could not be matched, the generator still falls back to using `object` as the parameter-type within the generated method-signature and emits this warning, additionaly.

## How to fix violations
Remove the type-annotation or use a valid one as shown in above table.

## When to suppress warnings
If you don�t care that your code-base has an easy-to-fix issue.

## Example of a violation

### Description
The following content of a Resx-file demonstrate, what keys produce this warning.
### Code

```xml
<root>
<data name="Hello {name:xyz}." xml:space="preserve">
<value>Hello, {0}.</value>
</data>
</root>
```

## Example of how to fix

### Description
In order to fix this particular situation, just remove the type-annotation, or use one of the built-in supported ones.
### Code

```xml
<root>
<data name="Hello {name}." xml:space="preserve">
<value>Hello, {0}.</value>
</data>
</root>
```

or

```xml
<root>
<data name="Hello {name:s}." xml:space="preserve">
<value>Hello, {0}.</value>
</data>
</root>
```

or

```xml
<root>
<data name="Hello {name:int}." xml:space="preserve">
<value>Hello, {0}.</value>
</data>
</root>
```

or similar ;)

## Related rules
9 changes: 9 additions & 0 deletions src/TypealizR.SourceGenerators.Playground.Console/App.resx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,15 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="?asd {:::??34}" xml:space="preserve">
<value>?asd {0}</value>
</data>
<data name="Broken {xy:z}" xml:space="preserve">
<value>Broken</value>
</data>
<data name="Hello {name:s}, today is {now:d}" xml:space="preserve">
<value>Hello {0}, today is {1}</value>
</data>
<data name="Hello, World!" xml:space="preserve">
<value>Hello, World!</value>
</data>
Expand Down
8 changes: 5 additions & 3 deletions src/TypealizR.SourceGenerators.Playground.Console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
Console.WriteLine(localize.Hello_World());
Console.WriteLine(localize.Hello_World1());
Console.WriteLine(localize.Hello_World2());
Console.WriteLine(localize.Hello_World2());

Console.WriteLine(localize.Hello__0(_0: "Arthur"));
Console.WriteLine(localize.Hello__UserName(UserName: "Arthur"));
Console.WriteLine(localize.Hello__0("Arthur"));
Console.WriteLine(localize.Hello__UserName(UserName: "Arthur"));
var today = DateOnly.FromDateTime(DateTime.Now);

Console.WriteLine(localize.Hello__name__today_is__now(name: "Arthur", now: today));
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
using FluentAssertions;
using TypealizR.SourceGenerators.StringLocalizer;

namespace TypealizR.SourceGenerators.Tests;

public class ExtensionMethodParameterInfo_Tests
{
[Theory]
[InlineData("{0}", "object", false)]
[InlineData("{1}", "object", false)]
[InlineData("{userName}", "object", false)]
[InlineData("{count:int}", "int", false)]
[InlineData("{count:i}", "int", false)]
[InlineData("{userName:string}", "string", false)]
[InlineData("{userName:s}", "string", false)]
[InlineData("{now:DateTime}", "DateTime", false)]
[InlineData("{now:dt}", "DateTime", false)]
[InlineData("{now:DateTimeOffset}", "DateTimeOffset", false)]
[InlineData("{now:dto}", "DateTimeOffset", false)]
[InlineData("{today:DateOnly}", "DateOnly", false)]
[InlineData("{today:d}", "DateOnly", false)]
[InlineData("{now:TimeOnly}", "TimeOnly", false)]
[InlineData("{now:t}", "TimeOnly", false)]
[InlineData("{now:wtf}", "object", true)]
public void Parameter_Gets_Typed_As(string token, string expected, bool expectInvalidTypeExpression)
{
var match = StringLocalizerExtensionMethodBuilder.parameterExpression.Match(token);
var name = match.Groups["name"].Value;
var expression = match.Groups["expression"].Value;

var sut = new ExtensionMethodParameterInfo(token, name, expression);

var actual = sut.Type;

actual.Should().Be(expected);

if (expectInvalidTypeExpression)
{
sut.InvalidTypeAnnotation.Should().NotBeNullOrEmpty();
}

}
}

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
using FluentAssertions;

namespace TypealizR.SourceGenerators.Tests;
public class RessourceFileTests
public class RessourceFile_Tests
{

[Theory]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,10 @@
using TypealizR.SourceGenerators.StringLocalizer;

namespace TypealizR.SourceGenerators.Tests;
public class StringLocalizerExtensionClassBuilderTests
public class StringLocalizerExtensionClassBuilder_Tests
{

private const string SomeFileName = "Ressource1.resx";


[Fact]
public void Simple_Method_Can_Be_Generated ()
{
Expand Down Expand Up @@ -65,7 +63,7 @@ public void Keys_Ending_Up_To_Produce_Duplicate_MethodNames_Produce_Diagnostics(

var expected = new Diagnostic[]
{
ErrorCodes.AmbigiousRessourceKey_001010(SomeFileName, 20, duplicateKey, $"{firstMethod.Name}1"),
ErrorCodes.AmbigiousRessourceKey_0002(SomeFileName, duplicateKey, 20, $"{firstMethod.Name}1"),
}
.Select(x => x.ToString());

Expand All @@ -77,11 +75,11 @@ public void Keys_Ending_Up_To_Produce_Duplicate_MethodNames_Produce_Diagnostics(

[Theory]
[InlineData("Hello {0}",
"Ressource-key 'Hello {0}' uses the generic format-parameter '{0}'. Consider to to use a more meaningful name, instead"
"TR0003"
)]
[InlineData("Hello {0}, today is {1}",
"Ressource-key 'Hello {0}, today is {1}' uses the generic format-parameter '{0}'. Consider to to use a more meaningful name, instead",
"Ressource-key 'Hello {0}, today is {1}' uses the generic format-parameter '{1}'. Consider to to use a more meaningful name, instead"
"TR0003",
"TR0003"
)]
public void Emits_Warning_For_Generic_Parameter_Names(string input, params string[] expectedWarnings)
{
Expand All @@ -91,7 +89,28 @@ public void Emits_Warning_For_Generic_Parameter_Names(string input, params strin

var extensionClass = sut.Build(new("Name.Space", "TypeName"));

var actual = extensionClass.Warnings.Select(x => x.GetMessage());
var actual = extensionClass.Warnings.Select(x => x.Id);

actual.Should().BeEquivalentTo(expectedWarnings);
}

[Theory]
[InlineData("Hello {name:xyz}",
"TR0004"
)]
[InlineData("Hello {0:xyz}",
"TR0003",
"TR0004"
)]
public void Emits_Warning_For_Unrecognized_Parameter_Type(string input, params string[] expectedWarnings)
{
var sut = new StringLocalizerExtensionClassBuilder(SomeFileName);

sut.WithMethodFor(input, "some value", 30);

var extensionClass = sut.Build(new("Name.Space", "TypeName"));

var actual = extensionClass.Warnings.Select(x => x.Id);

actual.Should().BeEquivalentTo(expectedWarnings);
}
Expand Down
Loading