Skip to content

Commit

Permalink
Merge pull request #15 from earloc/feature/typed-parameters
Browse files Browse the repository at this point in the history
typed parameters
  • Loading branch information
earloc authored Nov 12, 2022
2 parents 6b0971a + 9da19f9 commit f95ac2f
Show file tree
Hide file tree
Showing 16 changed files with 428 additions and 117 deletions.
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

0 comments on commit f95ac2f

Please sign in to comment.