Skip to content

Commit 5144c25

Browse files
committed
Add support for nested IDs
1 parent 402e361 commit 5144c25

20 files changed

+848
-56
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ Version 0.x of this library used the helper library [CodeGeneration.Roslyn](http
4444

4545
### Bug Fixes
4646

47-
* Some converters had incorrect implementations, such as in ([#26](https://github.com/andrewlock/StronglyTypedId/issues/24)). These have been addressed in version 1.x.
47+
* Some converters had incorrect implementations, such as in ([#24](https://github.com/andrewlock/StronglyTypedId/issues/24)). These have been addressed in version 1.x.
4848
* Better null handling has been added for the `String` backing type, handling issues such as [#32](https://github.com/andrewlock/StronglyTypedId/issues/32).
4949
* The code is marked as auto generated, to avoid errors such as #CS1591 as described in [#27](https://github.com/andrewlock/StronglyTypedId/issues/27)
5050
* An error deserializing nullable StronglyTypedIds with Newtonsoft.Json [#36](https://github.com/andrewlock/StronglyTypedId/issues/36)
@@ -179,7 +179,7 @@ The StronglyTypedId NuGet package is a .NET Standard 2.0 package.
179179

180180
You must be using the .NET 6+ SDK (though you can compile for other target frameworks like .NET Core 2.1 and .NET Framework 4.8)
181181

182-
The `struct`s you decorate with the `StronglyTypedId` attribute must be marked `partial`, and cannot be nested inside another class.
182+
The `struct`s you decorate with the `StronglyTypedId` attribute must be marked `partial`.
183183

184184
## Credits
185185
[Credits]: #credits

build/Build.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,12 @@ class Build : NukeBuild
119119
TestsDirectory / "StronglyTypedIds.Nuget.Attributes.IntegrationTests",
120120
};
121121

122+
if (!string.IsNullOrEmpty(PackagesDirectory))
123+
{
124+
DeleteDirectory(PackagesDirectory / "stronglytypedid");
125+
DeleteDirectory(PackagesDirectory / "stronglytypedid.attributes");
126+
}
127+
122128
DotNetRestore(s => s
123129
.When(!string.IsNullOrEmpty(PackagesDirectory), x => x.SetPackageDirectory(PackagesDirectory))
124130
.SetConfigFile(RootDirectory / "NuGet.integration-tests.config")

src/StronglyTypedIds/Diagnostics/NestedTypeDiagnostic.cs

Lines changed: 0 additions & 17 deletions
This file was deleted.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace StronglyTypedIds;
2+
3+
internal class ParentClass
4+
{
5+
public ParentClass(string keyword, string name, string constraints, ParentClass? child)
6+
{
7+
Keyword = keyword;
8+
Name = name;
9+
Constraints = constraints;
10+
Child = child;
11+
}
12+
13+
public ParentClass? Child { get; }
14+
public string Keyword { get; }
15+
public string Name { get; }
16+
public string Constraints { get; }
17+
}

src/StronglyTypedIds/Parser.cs

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,13 @@ public static bool IsAttributeTargetForGeneration(SyntaxNode node)
8484
return null;
8585
}
8686

87-
public static List<(string Name, string NameSpace, StronglyTypedIdConfiguration Config)> GetTypesToGenerate(
87+
public static List<(string Name, string NameSpace, StronglyTypedIdConfiguration Config, ParentClass? Parent)> GetTypesToGenerate(
8888
Compilation compilation,
8989
ImmutableArray<StructDeclarationSyntax> targets,
9090
Action<Diagnostic> reportDiagnostic,
9191
CancellationToken ct)
9292
{
93-
var idsToGenerate = new List<(string Name, string NameSpace, StronglyTypedIdConfiguration Config)>();
93+
var idsToGenerate = new List<(string Name, string NameSpace, StronglyTypedIdConfiguration Config, ParentClass? Parent)>();
9494
INamedTypeSymbol? idAttribute = compilation.GetTypeByMetadataName(StronglyTypedIdAttribute);
9595
if (idAttribute == null)
9696
{
@@ -226,15 +226,11 @@ public static bool IsAttributeTargetForGeneration(SyntaxNode node)
226226
reportDiagnostic(NotPartialDiagnostic.Create(structDeclarationSyntax));
227227
}
228228

229-
if (structSymbol.ContainingType is not null)
230-
{
231-
reportDiagnostic(NestedTypeDiagnostic.Create(structDeclarationSyntax));
232-
}
233-
234-
string nameSpace = structSymbol.ContainingNamespace.IsGlobalNamespace ? string.Empty : structSymbol.ContainingNamespace.ToString();
229+
string nameSpace = GetNameSpace(structDeclarationSyntax);
230+
var parentClass = GetParentClasses(structDeclarationSyntax);
235231
var name = structSymbol.Name;
236232

237-
idsToGenerate.Add((Name: name, NameSpace: nameSpace, Config: config.Value));
233+
idsToGenerate.Add((Name: name, NameSpace: nameSpace, Config: config.Value, Parent: parentClass));
238234
}
239235

240236
return idsToGenerate;
@@ -370,4 +366,58 @@ public static bool IsAttributeTargetForGeneration(SyntaxNode node)
370366

371367
return null;
372368
}
369+
370+
private static string GetNameSpace(StructDeclarationSyntax structSymbol)
371+
{
372+
// determine the namespace the struct is declared in, if any
373+
SyntaxNode? potentialNamespaceParent = structSymbol.Parent;
374+
while (potentialNamespaceParent != null &&
375+
potentialNamespaceParent is not NamespaceDeclarationSyntax
376+
&& potentialNamespaceParent is not FileScopedNamespaceDeclarationSyntax)
377+
{
378+
potentialNamespaceParent = potentialNamespaceParent.Parent;
379+
}
380+
381+
if (potentialNamespaceParent is BaseNamespaceDeclarationSyntax namespaceParent)
382+
{
383+
string nameSpace = namespaceParent.Name.ToString();
384+
while (true)
385+
{
386+
if(namespaceParent.Parent is not NamespaceDeclarationSyntax namespaceParentParent)
387+
{
388+
break;
389+
}
390+
391+
namespaceParent = namespaceParentParent;
392+
nameSpace = $"{namespaceParent.Name}.{nameSpace}";
393+
}
394+
395+
return nameSpace;
396+
}
397+
return string.Empty;
398+
}
399+
400+
private static ParentClass? GetParentClasses(StructDeclarationSyntax structSymbol)
401+
{
402+
TypeDeclarationSyntax? parentIdClass = structSymbol.Parent as TypeDeclarationSyntax;
403+
ParentClass? parentClass = null;
404+
405+
while (parentIdClass != null && IsAllowedKind(parentIdClass.Kind()))
406+
{
407+
parentClass = new ParentClass(
408+
keyword: parentIdClass.Keyword.ValueText,
409+
name: parentIdClass.Identifier.ToString() + parentIdClass.TypeParameterList,
410+
constraints: parentIdClass.ConstraintClauses.ToString(),
411+
child: parentClass);
412+
413+
parentIdClass = (parentIdClass.Parent as TypeDeclarationSyntax);
414+
}
415+
416+
return parentClass;
417+
418+
static bool IsAllowedKind(SyntaxKind kind) =>
419+
kind == SyntaxKind.ClassDeclaration ||
420+
kind == SyntaxKind.StructDeclaration ||
421+
kind == SyntaxKind.RecordDeclaration;
422+
}
373423
}

src/StronglyTypedIds/SourceGenerationHelper.cs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,16 @@ internal static class SourceGenerationHelper
1010
public static string CreateId(
1111
string idNamespace,
1212
string idName,
13+
ParentClass? parentClass,
1314
StronglyTypedIdConverter converters,
1415
StronglyTypedIdBackingType backingType,
1516
StronglyTypedIdImplementations implementations)
16-
=> CreateId(idNamespace, idName, converters, backingType, implementations, null);
17+
=> CreateId(idNamespace, idName, parentClass, converters, backingType, implementations, null);
1718

1819
public static string CreateId(
1920
string idNamespace,
2021
string idName,
22+
ParentClass? parentClass,
2123
StronglyTypedIdConverter converters,
2224
StronglyTypedIdBackingType backingType,
2325
StronglyTypedIdImplementations implementations,
@@ -33,12 +35,13 @@ public static string CreateId(
3335
_ => throw new ArgumentException("Unknown backing type: " + backingType, nameof(backingType)),
3436
};
3537

36-
return CreateId(idNamespace, idName, converters, implementations, resources, sb);
38+
return CreateId(idNamespace, idName, parentClass, converters, implementations, resources, sb);
3739
}
3840

3941
static string CreateId(
4042
string idNamespace,
4143
string idName,
44+
ParentClass? parentClass,
4245
StronglyTypedIdConverter converters,
4346
StronglyTypedIdImplementations implementations,
4447
EmbeddedSources.ResourceCollection resources,
@@ -70,6 +73,8 @@ static string CreateId(
7073
var useIEquatable = implementations.IsSet(StronglyTypedIdImplementations.IEquatable);
7174
var useIComparable = implementations.IsSet(StronglyTypedIdImplementations.IComparable);
7275

76+
var parentsCount = 0;
77+
7378
sb ??= new StringBuilder();
7479
sb.Append(resources.Header);
7580

@@ -87,6 +92,21 @@ static string CreateId(
8792
{");
8893
}
8994

95+
while (parentClass is not null)
96+
{
97+
sb
98+
.Append(" partial ")
99+
.Append(parentClass.Keyword)
100+
.Append(' ')
101+
.Append(parentClass.Name)
102+
.Append(' ')
103+
.Append(parentClass.Constraints)
104+
.AppendLine(@"
105+
{");
106+
parentsCount++;
107+
parentClass = parentClass.Child;
108+
}
109+
90110
if (useNewtonsoftJson)
91111
{
92112
sb.AppendLine(EmbeddedSources.NewtonsoftJsonAttributeSource);
@@ -139,6 +159,12 @@ static string CreateId(
139159

140160
sb.Replace("TESTID", idName);
141161
sb.AppendLine(@" }");
162+
163+
for (int i = 0; i < parentsCount; i++)
164+
{
165+
sb.AppendLine(@" }");
166+
}
167+
142168
if (hasNamespace)
143169
{
144170
sb.Append('}').AppendLine();

src/StronglyTypedIds/StronglyTypedIdGenerator.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ static void Execute(
5858
return;
5959
}
6060

61-
List<(string Name, string NameSpace, StronglyTypedIdConfiguration Config)> idsToGenerate =
61+
List<(string Name, string NameSpace, StronglyTypedIdConfiguration Config, ParentClass? Parent)> idsToGenerate =
6262
Parser.GetTypesToGenerate(compilation, structs, context.ReportDiagnostic, context.CancellationToken);
6363

6464
if (idsToGenerate.Count > 0)
@@ -72,6 +72,7 @@ static void Execute(
7272
var result = SourceGenerationHelper.CreateId(
7373
idToGenerate.NameSpace,
7474
idToGenerate.Name,
75+
idToGenerate.Parent,
7576
values.Converters,
7677
values.BackingType,
7778
values.Implementations,
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using StronglyTypedIds.IntegrationTests.Types;
2+
using Xunit;
3+
4+
namespace StronglyTypedIds.IntegrationTests;
5+
6+
public class NestedIdTests
7+
{
8+
[Fact]
9+
public void CanCreateNestedId()
10+
{
11+
var id = SomeType<object>.NestedType<string, int>.MoreNesting.VeryNestedId.New();
12+
}
13+
}

test/StronglyTypedIds.IntegrationTests/Types/DefaultId.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,16 @@ public partial struct BothJsonDefaultId { }
2626

2727
[StronglyTypedId(converters: StronglyTypedIdConverter.EfCoreValueConverter)]
2828
public partial struct EfCoreDefaultId { }
29+
30+
public partial class SomeType<T> where T : new()
31+
{
32+
public partial record NestedType<TKey, TValue>
33+
{
34+
public partial struct MoreNesting
35+
{
36+
[StronglyTypedId]
37+
public partial struct VeryNestedId {}
38+
}
39+
}
40+
}
2941
}

test/StronglyTypedIds.Tests/Diagnostics/NestedTypeDiagnosticTests.cs

Lines changed: 0 additions & 25 deletions
This file was deleted.

0 commit comments

Comments
 (0)