Skip to content

Commit 76ccf9c

Browse files
committed
Added Nullability support
* Added NamespaceQualifiedType to capture nullability of types. - Previously only the name of a type was captured. - It now captures the type name OR the underlying type name for nullable value types AND the nullability annotation * Added default constructor for `NamespaceQualifiedName` * Added `PropertyInfo` that accepts a `IPropertySymbol`
1 parent 90aebad commit 76ccf9c

20 files changed

Lines changed: 451 additions & 68 deletions

File tree

Invoke-Tests.ps1

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ try
3838
Push-Location $BuildInfo['SrcRootPath']
3939
try
4040
{
41-
dir (Join-Path $BuildInfo['BuildOutputPath'] 'bin' 'Ubiquity.NET.CommandLine.SrcGen.UT' "$Configuration" 'net10.0' 'Ubiquity.NET.CommandLine.*')
4241
Invoke-External dotnet test Ubiquity.NET.Utils.slnx '-c' $Configuration '-tl:off' '--logger:trx' '--no-build' '-s' '.\.runsettings'
4342
}
4443
finally

src/DemoCommandLineSrcGen/Program.cs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ public static async Task<int> Main( string[] args )
3131
.ParseAndInvokeResultAsync( reporter, cts.Token, args );
3232
}
3333

34+
/// <summary>Asynchronous main entry point</summary>
35+
/// <param name="options">Parsed command line options</param>
36+
/// <param name="reporter">Reporter to use for reporting diagnostics for this app</param>
37+
/// <param name="ct">Cancellation token to indicate cancellation of the entire app</param>
38+
/// <returns>Exit code for the app; 0=NOERROR; any other value MAY indicate an error (App defined)</returns>
3439
private static async Task<int> AppMainAsync(
3540
TestOptions options,
3641
ColoredConsoleReporter reporter,
@@ -43,13 +48,17 @@ CancellationToken ct
4348
reporter = new ColoredConsoleReporter( options.Verbosity );
4449
}
4550

46-
reporter.Verbose( "AppMainAsync" );
47-
48-
// Core application code here...
49-
5051
// Use the cancellation token to indicate cancellation
5152
// This is set when CTRL-C is pressed in Main() above.
5253
ct.ThrowIfCancellationRequested();
54+
55+
// For demo, Report the full parsed args to the "Verbose" channel
56+
// in a normal app this isn't done or would use some sort of logging
57+
// and not a UX reporter, but it is useful for validation scripts
58+
reporter.Verbose( $"TestOptions:\n{options}" );
59+
60+
// Core application code here...
61+
// app should use/test ct for cancellation of long operations.
5362
return 0;
5463
}
5564
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Demo/Test command line source generator
2+
This project demonstrates use of the command line source generator AND also tests the final
3+
NuGet Packaging. It lists the command line library package. This package should also bring
4+
in the analyzer and generator.
5+
6+
>[!IMPORTANT]
7+
> To test/leverage the final package it MUST be built already AND can only reference the
8+
> dependent packages that also exist. (Including the correct versions). This is normally not
9+
> plausible from an IDE build. The version number for such builds is based on the timestamp
10+
> of the build and is determined for each project independently. However, the command line
11+
> will build ALL of the projects with the same version number.
12+
> `Build-Source.ps1 -FullInit -ForceClean` will ensure the same version is built into all of
13+
> the packages. It will then build this project. This breaks the dependency cycles and
14+
> eliminates the problem of expressing the dependency on a set of generated packages but NOT
15+
> the projects that create them. (Sadly, this turns out to be a more complex problem than
16+
> one would think)
17+
18+
To support the dependency and testing this is NOT part of the same solution as the other
19+
components. Within this repository, it does depend on those being built first.
20+
21+
## Purpose
22+
Aside from serving as a general demo for use in documentation etc... this project serves to
23+
validate the proper packaging of the generator with the library it is generating source for.
24+
The formal tests will validate the behavior of the generator/analyzer itself but don't use
25+
NuGet packaging (They use project references). The meta package that includes a reference to
26+
both the library and the generator is built independent of those assemblies. This is done to
27+
break the dependency problem where the meta package references but does not need them to
28+
build. Thus, this package validates the NuGet Packaging is functioning as intended. It will
29+
fail to build if there is a problem. So it is built after all the other projects as part of
30+
the `Build-Source.ps1` script. As long as it builds without error it worked for what is
31+
tested. (The formal tests validate the behavior more directly)
32+
33+
## Differences from Real world use
34+
The only thing that is different from this demo and real world use is the `VersionOverride`
35+
attribute on the `PackageReference` for `Ubiquity.NET.CommandLine`. This is simply present
36+
for the use in this repo as the [Nuget.Config](../../NuGet.Config) uses source mapping to
37+
constrain resolving packages generated by this rep to this repo. This constrains NuGet to
38+
resolve these dependencies to what was built in this repository only. This ensures that what
39+
is consumed and tested is what is built. This is a necessity of building or testing this
40+
repository. This specific demo project, only has the `VersionOverride` to fit into the repo
41+
build and verify the package built before accepting/releasing it.

src/DemoCommandLineSrcGen/TestOptions.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111

1212
namespace TestNamespace
1313
{
14+
// It is important to understand how "required" is handled in the underlying command line handler
15+
// in System.CommandLine. The semantics are:
16+
// When the command is invoked, the value may not be null.
17+
// That is, it is NOT evaluated during parse, ONLY during invocation.
1418
[RootCommand( Description = "Root command for tests" )]
1519
internal partial class TestOptions
1620
{
@@ -25,14 +29,26 @@ internal partial class TestOptions
2529
[FolderValidation( FolderValidation.ExistingOnly )]
2630
public required DirectoryInfo SomeExistingPath { get; init; }
2731

28-
[Option( "--thing1", Aliases = [ "-t" ], Required = true, Description = "Test Thing1", HelpName = "Help name for thing1" )]
29-
public bool Thing1 { get; init; }
32+
[Option( "--thing1", Aliases = [ "-t" ], Description = "Test Thing1", HelpName = "Help name for thing1" )]
33+
public bool? Thing1 { get; init; }
3034

3135
// This should be ignored by generator
3236
public string? NotAnOption { get; set; }
3337

3438
[Option( "-a", Hidden = true, Required = false, ArityMin = 0, ArityMax = 3, Description = "Test SomeOtherPath" )]
3539
[FileValidation( FileValidation.ExistingOnly )]
3640
public required FileInfo SomeOtherPath { get; init; }
41+
42+
public override string ToString( )
43+
{
44+
return $"""
45+
SomePath = '{SomePath?.FullName ?? "<null>"}'
46+
Verbosity = {Verbosity}
47+
SomeExistingPath = '{SomeExistingPath?.FullName ?? "<null>"}'
48+
Thing1 = {Thing1}
49+
NotAnOption = {NotAnOption ?? "<null>"}
50+
SomeOtherPath = '{SomeOtherPath?.FullName ?? "<null>"}'
51+
""";
52+
}
3753
}
3854
}

src/Ubiquity.NET.CodeAnalysis.Utils/AttributeDataExtensions.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,12 @@ public static bool IsFullNameMatch( this AttributeData self, NamespaceQualifiedN
1818
.SequenceEqual( fullName.NamespaceNames );
1919
}
2020

21-
/// <summary>Gets the <see cref="NamespaceQualifiedName"/> for the attribute</summary>
21+
/// <summary>Gets the <see cref="NamespaceQualifiedTypeName"/> for the attribute</summary>
2222
/// <param name="self">self</param>
23-
/// <returns><see cref="NamespaceQualifiedName"/> for the attribute</returns>
24-
public static NamespaceQualifiedName GetNamespaceQualifiedName( this AttributeData self )
23+
/// <returns><see cref="NamespaceQualifiedTypeName"/> for the attribute</returns>
24+
public static NamespaceQualifiedTypeName GetNamespaceQualifiedName( this AttributeData self )
2525
{
26-
return self.AttributeClass is null
27-
? new( [], string.Empty )
28-
: self.AttributeClass.GetNamespaceQualifiedName();
26+
return self.AttributeClass?.GetNamespaceQualifiedName() ?? new();
2927
}
3028

3129
/// <summary>Gets the location from the <see cref="AttributeData.ApplicationSyntaxReference"/> if available</summary>

src/Ubiquity.NET.CodeAnalysis.Utils/EquatableAttributeData.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ public EquatableAttributeData( AttributeData data )
3333
NamedArguments = namedArgs.ToImmutableDictionary();
3434
}
3535

36-
/// <summary>Gets the full namespace qualified name for this attribute</summary>
37-
public NamespaceQualifiedName Name { get; }
36+
/// <summary>Gets the full namespace qualified name for the type of this attribute</summary>
37+
public NamespaceQualifiedTypeName Name { get; }
3838

3939
/// <summary>Gets the unnamed constructor arguments for this attribute</summary>
4040
public ImmutableArray<StructurallyEquatableTypedConstant> ConstructorArguments { get; }

src/Ubiquity.NET.CodeAnalysis.Utils/NamespaceQualifiedName.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,23 @@ public class NamespaceQualifiedName
1919
, IEquatable<Type>
2020
, IFormattable
2121
{
22+
/// <summary>Initializes a new instance of the <see cref="NamespaceQualifiedName"/> class.</summary>
23+
public NamespaceQualifiedName()
24+
{
25+
NamespaceNames = [];
26+
SimpleName = string.Empty;
27+
}
28+
2229
/// <summary>Initializes a new instance of the <see cref="NamespaceQualifiedName"/> class.</summary>
2330
/// <param name="namespaceNames">sequence of namespace names (outermost to innermost)</param>
2431
/// <param name="simpleName">Unqualified name of the symbol</param>
25-
public NamespaceQualifiedName(IEnumerable<string> namespaceNames, string simpleName )
32+
public NamespaceQualifiedName( IEnumerable<string> namespaceNames, string simpleName )
2633
{
2734
PolyFillExceptionValidators.ThrowIfNull(namespaceNames);
2835
PolyFillExceptionValidators.ThrowIfNullOrWhiteSpace(simpleName);
2936

3037
SimpleName = simpleName;
3138
NamespaceNames = [ .. namespaceNames.Select( s => ValidateNamespacePart(s) ) ];
32-
3339
static string ValidateNamespacePart( string s, [CallerArgumentExpression(nameof(s))] string? exp = null )
3440
{
3541
PolyFillExceptionValidators.ThrowIfNullOrWhiteSpace( s, exp );
@@ -69,6 +75,8 @@ public IEnumerable<string> FullNameParts
6975
}
7076
}
7177

78+
#region Equality
79+
7280
/// <inheritdoc/>
7381
public bool Equals( NamespaceQualifiedName other )
7482
{
@@ -124,6 +132,7 @@ public bool Equals( Type other )
124132
{
125133
return ToString() == other.FullName;
126134
}
135+
#endregion
127136

128137
/// <summary>Gets the string representation of the full namespace using '.' as the delimiter</summary>
129138
/// <returns>Full namespace qualified name as a string (with a global prefix or an alias if available)</returns>
@@ -155,7 +164,7 @@ public override string ToString( )
155164
/// </remarks>
156165
/// <exception cref="NotSupportedException"><paramref name="format"/> is not supported</exception>
157166
[SuppressMessage( "Style", "IDE0046:Convert to conditional expression", Justification = "Result is anything but simpler" )]
158-
public string ToString( string format, IFormatProvider? formatProvider )
167+
public virtual string ToString( string format, IFormatProvider? formatProvider )
159168
{
160169
// default to the C# formatter unless specified.
161170
formatProvider ??= NamespaceQualifiedNameFormatter.CSharp;

src/Ubiquity.NET.CodeAnalysis.Utils/NamespaceQualifiedNameFormatter.cs

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,19 @@
33

44
namespace Ubiquity.NET.CodeAnalysis.Utils
55
{
6-
/// <summary>Custom formatter for <see cref="NamespaceQualifiedName"/></summary>
6+
/// <summary>Custom formatter for <see cref="NamespaceQualifiedName"/> and <see cref="NamespaceQualifiedTypeName"/></summary>
77
public class NamespaceQualifiedNameFormatter
88
: IFormatProvider
99
, ICustomFormatter<NamespaceQualifiedName>
10+
, ICustomFormatter<NamespaceQualifiedTypeName>
1011
{
1112
/// <summary>Initializes a new instance of the <see cref="NamespaceQualifiedNameFormatter"/> class.</summary>
1213
/// <param name="globalPrefix">Global prefix for the language this formatter will produce</param>
1314
/// <param name="aliasMap">Map of aliases for the language (Key is the full namespace name of a type, the values are the alias)</param>
1415
public NamespaceQualifiedNameFormatter( string globalPrefix, IReadOnlyDictionary<string, string> aliasMap )
1516
{
1617
GlobalPrefix = globalPrefix ?? string.Empty;
17-
AliasMap = aliasMap ?? throw new ArgumentNullException(nameof(aliasMap));
18+
AliasMap = aliasMap ?? throw new ArgumentNullException( nameof( aliasMap ) );
1819
}
1920

2021
/// <summary>Gets the global prefix for the language formatting (example: "global::")</summary>
@@ -30,12 +31,24 @@ public NamespaceQualifiedNameFormatter( string globalPrefix, IReadOnlyDictionary
3031
/// <inheritdoc/>
3132
public string Format( string format, object arg, IFormatProvider? formatProvider )
3233
{
33-
return arg is not NamespaceQualifiedName self
34-
? string.Empty
35-
: Format(format, self, formatProvider);
34+
if(arg is NamespaceQualifiedName name)
35+
{
36+
return Format( format, name, formatProvider );
37+
}
38+
39+
if(arg is NamespaceQualifiedTypeName typeName)
40+
{
41+
return Format( format, typeName, formatProvider );
42+
}
43+
44+
// Not a type supported; use the type to do it, if it's formattable itself use that
45+
// otherwise fall back to simple Object.ToString().
46+
return arg is IFormattable formattable
47+
? formattable.ToString( format, formatProvider )
48+
: arg.ToString();
3649
}
3750

38-
/// <summary>Formats this instance according to the args</summary>
51+
/// <summary>Formats <paramref name="arg"/> according to <paramref name="format"/></summary>
3952
/// <param name="format">Format string for this instance (see remarks)</param>
4053
/// <param name="arg">The value to format</param>
4154
/// <param name="formatProvider">[ignored]</param>
@@ -76,10 +89,39 @@ public string Format( string format, NamespaceQualifiedName arg, IFormatProvider
7689
: rawName;
7790
}
7891

92+
/// <summary>Formats <paramref name="arg"/> according to <paramref name="format"/></summary>
93+
/// <param name="format">Format string for this instance (see remarks)</param>
94+
/// <param name="arg">The value to format</param>
95+
/// <param name="formatProvider">[ignored]</param>
96+
/// <returns>Formatted string representation of this instance</returns>
97+
/// <remarks>
98+
/// The supported values for <paramref name="format"/> are:
99+
/// <list type="table">
100+
/// <listheader><term>Value</term><description>Description</description></listheader>
101+
/// <item><term>A</term><description>Format as a language specific alias if possible</description></item>
102+
/// <item><term>G</term><description>Format with a language specific global prefix.</description></item>
103+
/// <item><term>AG</term><description>Format with a language specific alias if possible, otherwise include a global prefix.</description></item>
104+
/// <item><term>R</term><description>The raw full name without any qualifications</description></item>
105+
/// </list>
106+
/// <note type="important">
107+
/// This will always append a <c>?</c> to the end of the formatted value IF the <see cref="NamespaceQualifiedTypeName.NullableAnnotation"/>
108+
/// indicates the value is nullable.
109+
/// </note>
110+
/// </remarks>
111+
/// <exception cref="NotSupportedException"><paramref name="format"/> is not supported</exception>
112+
public string Format( string format, NamespaceQualifiedTypeName arg, IFormatProvider? formatProvider )
113+
{
114+
string formattedString = Format(format, (NamespaceQualifiedName)arg, formatProvider);
115+
return arg.NullableAnnotation == NullableAnnotation.Annotated
116+
? $"{formattedString}?"
117+
: formattedString;
118+
}
119+
79120
/// <inheritdoc/>
80121
public object? GetFormat( Type formatType )
81122
{
82123
return formatType == typeof( ICustomFormatter<NamespaceQualifiedName> )
124+
|| formatType == typeof( ICustomFormatter<NamespaceQualifiedTypeName> )
83125
? this
84126
: null;
85127
}

0 commit comments

Comments
 (0)