Skip to content

Commit 5e6b904

Browse files
authored
Merge pull request #15 from poizan42/inject-parent-commands
Proposal for fix for #14 by adding support for injecting parent commands as properties.
2 parents e3f0587 + c7d0b80 commit 5e6b904

File tree

86 files changed

+1691
-547
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

86 files changed

+1691
-547
lines changed

src/DotMake.CommandLine.SourceGeneration/CliCommandInfo.cs

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4+
using System.Text;
45
using Microsoft.CodeAnalysis;
56
using Microsoft.CodeAnalysis.CSharp;
67
using Microsoft.CodeAnalysis.CSharp.Syntax;
@@ -64,13 +65,19 @@ public CliCommandInfo(ISymbol symbol, SyntaxNode syntaxNode, AttributeData attri
6465

6566
var visitedProperties = new Dictionary<string, ISymbol>(StringComparer.Ordinal);
6667
var addedProperties = new HashSet<string>(StringComparer.Ordinal);
68+
Dictionary<ITypeSymbol, (int Index, CliCommandSettings Settings)> ancestorsByType = Settings
69+
.GetParentTree()
70+
.Select((s, i) => (i, Setings: s))
71+
.ToDictionary(x => x.Setings.Symbol, (IEqualityComparer<ITypeSymbol>)SymbolEqualityComparer.Default);
72+
6773
foreach (var member in Symbol.GetAllMembers())
6874
{
69-
if (member is IPropertySymbol)
75+
if (member is IPropertySymbol property)
7076
{
7177
if (addedProperties.Contains(member.Name))
7278
continue;
7379

80+
bool added = false;
7481
foreach (var memberAttributeData in member.GetAttributes())
7582
{
7683
var attributeFullName = memberAttributeData.AttributeClass?.ToCompareString();
@@ -81,12 +88,19 @@ public CliCommandInfo(ISymbol symbol, SyntaxNode syntaxNode, AttributeData attri
8188
{
8289
childOptions.Add(new CliOptionInfo(visitedMember ?? member, null, memberAttributeData, SemanticModel, this));
8390
addedProperties.Add(member.Name);
91+
added = true;
8492
} else if (attributeFullName == CliArgumentInfo.AttributeFullName)
8593
{
8694
childArguments.Add(new CliArgumentInfo(visitedMember ?? member, null, memberAttributeData, SemanticModel, this));
8795
addedProperties.Add(member.Name);
96+
added = true;
8897
}
8998
}
99+
if (!added && ancestorsByType.TryGetValue(property.Type, out var ancestorInfo))
100+
{
101+
childParentCommandRefs.Add(new CliParentCommandRefInfo(property, null, SemanticModel, ancestorInfo.Index, ancestorInfo.Settings));
102+
addedProperties.Add(member.Name);
103+
}
90104

91105
if (!visitedProperties.ContainsKey(member.Name))
92106
visitedProperties.Add(member.Name, member);
@@ -164,6 +178,9 @@ public static CliCommandInfo From(GeneratorAttributeSyntaxContext attributeSynta
164178
public IReadOnlyList<CliCommandInfo> ChildCommands => childCommands;
165179
private readonly List<CliCommandInfo> childCommands = new List<CliCommandInfo>();
166180

181+
public IReadOnlyList<CliParentCommandRefInfo> ChildParentCommandRefs => childParentCommandRefs;
182+
private readonly List<CliParentCommandRefInfo> childParentCommandRefs = new List<CliParentCommandRefInfo>();
183+
167184
private void Analyze()
168185
{
169186
if ((Symbol.DeclaredAccessibility != Accessibility.Public && Symbol.DeclaredAccessibility != Accessibility.Internal)
@@ -206,16 +223,21 @@ public override void ReportDiagnostics(SourceProductionContext sourceProductionC
206223

207224
foreach (var child in ChildCommands)
208225
child.ReportDiagnostics(sourceProductionContext);
226+
227+
foreach (var child in ChildParentCommandRefs)
228+
child.ReportDiagnostics(sourceProductionContext);
209229
}
210230

211231
public void AppendCSharpDefineString(CodeStringBuilder sb, bool addNamespaceBlock)
212232
{
213233
var childOptionsWithoutProblem = ChildOptions.Where(c => !c.HasProblem).ToArray();
214234
var childArgumentsWithoutProblem = ChildArguments.Where(c => !c.HasProblem).ToArray();
215235
var childCommandsWithoutProblem = ChildCommands.Where(c => !c.HasProblem).ToArray();
236+
var childParentCommandRefsWithoutProblem = ChildParentCommandRefs.Where(c => !c.HasProblem).ToArray();
216237
var handlerWithoutProblem = (Handler != null && !Handler.HasProblem) ? Handler : null;
217238
var memberHasRequiredModifier = childOptionsWithoutProblem.Any(o => o.Symbol.IsRequired)
218-
|| childArgumentsWithoutProblem.Any(a => a.Symbol.IsRequired);
239+
|| childArgumentsWithoutProblem.Any(a => a.Symbol.IsRequired)
240+
|| childParentCommandRefsWithoutProblem.Any(r => r.Symbol.IsRequired);
219241

220242
if (string.IsNullOrEmpty(GeneratedClassNamespace))
221243
addNamespaceBlock = false;
@@ -308,7 +330,7 @@ public void AppendCSharpDefineString(CodeStringBuilder sb, bool addNamespaceBloc
308330
}
309331

310332
sb.AppendLine();
311-
using (sb.AppendBlockStart($"BindFunc = (parseResult) =>", ";"))
333+
using (sb.AppendBlockStart($"BindFunc = (cliBindContext) =>", ";"))
312334
{
313335
var varTargetClass = "targetClass";
314336

@@ -320,7 +342,7 @@ public void AppendCSharpDefineString(CodeStringBuilder sb, bool addNamespaceBloc
320342
{
321343
var cliOptionInfo = childOptionsWithoutProblem[index];
322344
var varOption = $"option{index}";
323-
sb.AppendLine($"{varTargetClass}.{cliOptionInfo.Symbol.Name} = GetValueForOption(parseResult, {varOption});");
345+
sb.AppendLine($"{varTargetClass}.{cliOptionInfo.Symbol.Name} = GetValueForOption(cliBindContext.ParseResult, {varOption});");
324346
}
325347

326348
sb.AppendLine();
@@ -329,7 +351,15 @@ public void AppendCSharpDefineString(CodeStringBuilder sb, bool addNamespaceBloc
329351
{
330352
var cliArgumentInfo = childArgumentsWithoutProblem[index];
331353
var varArgument = $"argument{index}";
332-
sb.AppendLine($"{varTargetClass}.{cliArgumentInfo.Symbol.Name} = GetValueForArgument(parseResult, {varArgument});");
354+
sb.AppendLine($"{varTargetClass}.{cliArgumentInfo.Symbol.Name} = GetValueForArgument(cliBindContext.ParseResult, {varArgument});");
355+
}
356+
357+
sb.AppendLine();
358+
sb.AppendLine("// Set the values for the parent command references");
359+
for (var index = 0; index < childParentCommandRefsWithoutProblem.Length; index++)
360+
{
361+
var cliParentCommandRefInfo = childParentCommandRefsWithoutProblem[index];
362+
sb.AppendLine($"{varTargetClass}.{cliParentCommandRefInfo.Symbol.Name} = cliBindContext.BindOrGetBindResult<{cliParentCommandRefInfo.Symbol.Type.ToReferenceString()}>();");
333363
}
334364

335365
sb.AppendLine();
@@ -339,6 +369,7 @@ public void AppendCSharpDefineString(CodeStringBuilder sb, bool addNamespaceBloc
339369
sb.AppendLine();
340370
var varParseResult = "parseResult";
341371
var varCancellationToken = "cancellationToken";
372+
var varCliBindContext = "cliBindContext";
342373
var varCliContext = "cliContext";
343374
var isAsync = (handlerWithoutProblem != null && handlerWithoutProblem.IsAsync);
344375
using (sb.AppendBlockStart(isAsync
@@ -348,7 +379,8 @@ public void AppendCSharpDefineString(CodeStringBuilder sb, bool addNamespaceBloc
348379
{
349380
var varTargetClass = "targetClass";
350381

351-
sb.AppendLine($"var {varTargetClass} = ({definitionClass}) BindFunc({varParseResult});");
382+
sb.AppendLine($"var {varCliBindContext} = new DotMake.CommandLine.CliBindContext({varParseResult});");
383+
sb.AppendLine($"var {varTargetClass} = ({definitionClass}) BindFunc({varCliBindContext});");
352384
sb.AppendLine();
353385

354386
sb.AppendLine("// Call the command handler");
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System;
2+
using Microsoft.CodeAnalysis;
3+
4+
namespace DotMake.CommandLine.SourceGeneration
5+
{
6+
public class CliParentCommandRefInfo : CliSymbolInfo, IEquatable<CliParentCommandRefInfo>
7+
{
8+
public const string DiagnosticName = "CLI parent command reference";
9+
10+
public CliParentCommandRefInfo(IPropertySymbol symbol, SyntaxNode syntaxNode, SemanticModel semanticModel,
11+
int parentTreeIndex, CliCommandSettings parentCommandSettings) : base(symbol, syntaxNode, semanticModel)
12+
{
13+
ParentTreeIndex = parentTreeIndex;
14+
ParentCommandSettings = parentCommandSettings;
15+
16+
Analyze();
17+
18+
if (HasProblem)
19+
return;
20+
}
21+
22+
public int ParentTreeIndex { get; }
23+
public CliCommandSettings ParentCommandSettings { get; }
24+
public new IPropertySymbol Symbol => (IPropertySymbol)base.Symbol;
25+
26+
private void Analyze()
27+
{
28+
if ((Symbol.DeclaredAccessibility != Accessibility.Public && Symbol.DeclaredAccessibility != Accessibility.Internal)
29+
|| Symbol.IsStatic)
30+
AddDiagnostic(DiagnosticDescriptors.WarningPropertyNotPublicNonStatic, DiagnosticName);
31+
else
32+
{
33+
if (Symbol.GetMethod == null
34+
|| (Symbol.GetMethod.DeclaredAccessibility != Accessibility.Public && Symbol.GetMethod.DeclaredAccessibility != Accessibility.Internal))
35+
AddDiagnostic(DiagnosticDescriptors.ErrorPropertyHasNotPublicGetter, DiagnosticName);
36+
37+
if (Symbol.SetMethod == null
38+
|| (Symbol.SetMethod.DeclaredAccessibility != Accessibility.Public && Symbol.SetMethod.DeclaredAccessibility != Accessibility.Internal))
39+
AddDiagnostic(DiagnosticDescriptors.ErrorPropertyHasNotPublicSetter, DiagnosticName);
40+
}
41+
}
42+
43+
public bool Equals(CliParentCommandRefInfo other)
44+
{
45+
return base.Equals(other);
46+
}
47+
}
48+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.CommandLine;
4+
5+
namespace DotMake.CommandLine
6+
{
7+
/// <summary>
8+
/// Context used during binding of commands
9+
/// </summary>
10+
public class CliBindContext
11+
{
12+
/// <summary>
13+
/// Initializes a new instance of the <see cref="CliBindContext" /> class.
14+
/// </summary>
15+
/// <param name="parseResult">A parse result describing the outcome of the parse operation.</param>
16+
public CliBindContext(ParseResult parseResult)
17+
{
18+
ParseResult = parseResult;
19+
}
20+
21+
/// <summary>A parse result describing the outcome of the parse operation.</summary>
22+
public ParseResult ParseResult { get; }
23+
24+
/// <summary>
25+
/// Creates a new instance of the definition class and binds/populates the properties from the parse result,
26+
/// or returns a cached instance of the definition class earlier returned from either BindOrGetBindResult() overload.
27+
/// </summary>
28+
/// <typeparam name="TDefinition">The definition class.</typeparam>
29+
/// <returns></returns>
30+
public TDefinition BindOrGetBindResult<TDefinition>() => (TDefinition)BindOrGetBindResult(typeof(TDefinition));
31+
32+
/// <summary>
33+
/// Creates a new instance of the definition class and binds/populates the properties from the parse result,
34+
/// or returns a cached instance of the definition class earlier returned from either BindOrGetBindResult() overload.
35+
/// </summary>
36+
/// <param name="commandDefinitionType">The type of the definition class.</param>
37+
/// <returns>An instance of the definition class whose properties were bound/populated from the parse result.</returns>
38+
public object BindOrGetBindResult(Type commandDefinitionType)
39+
{
40+
if (bindResults.TryGetValue(commandDefinitionType, out object bindResult))
41+
{
42+
return bindResult;
43+
}
44+
var commandBuilder = CliCommandBuilder.Get(commandDefinitionType);
45+
object commandObj = commandBuilder.Bind(this);
46+
bindResults[commandDefinitionType] = commandObj;
47+
return commandObj;
48+
}
49+
50+
private readonly Dictionary<Type, object> bindResults = new();
51+
}
52+
}

src/DotMake.CommandLine/CliCommandBuilder.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ namespace DotMake.CommandLine
1414
public abstract class CliCommandBuilder
1515
{
1616
/// <summary>
17-
/// A delegate which is set by the source generator to be called from <see cref="Bind"/> method.
17+
/// A delegate which is set by the source generator to be called from <see cref="Bind(CliBindContext)"/> method.
1818
/// </summary>
19-
protected Func<ParseResult, object> BindFunc;
19+
protected Func<CliBindContext, object> BindFunc;
2020

2121
/// <summary>
2222
/// Initializes a new instance of the <see cref="CliCommandBuilder" /> class.
@@ -72,11 +72,21 @@ protected CliCommandBuilder()
7272
/// <param name="parseResult">A parse result describing the outcome of the parse operation.</param>
7373
/// <returns>An instance of the definition class whose properties were bound/populated from the parse result.</returns>
7474
public object Bind(ParseResult parseResult)
75+
{
76+
return Bind(new CliBindContext(parseResult));
77+
}
78+
79+
/// <summary>
80+
/// Creates a new instance of the definition class and binds/populates the properties from the parse result.
81+
/// </summary>
82+
/// <param name="cliBindContext">A <see cref="CliBindContext"/> instance to use for the binding operation.</param>
83+
/// <returns>An instance of the definition class whose properties were bound/populated from the parse result.</returns>
84+
public object Bind(CliBindContext cliBindContext)
7585
{
7686
if (BindFunc == null)
7787
throw new Exception("Ensure Build method is called first.");
7888

79-
return BindFunc(parseResult);
89+
return BindFunc(cliBindContext);
8090
}
8191

8292
/// <summary>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#pragma warning disable CS1591
2+
#pragma warning disable CA1822 // Mark members as static
3+
using System;
4+
using DotMake.CommandLine;
5+
6+
namespace TestApp.Commands
7+
{
8+
#region RootWithNestedChildrenReferencingRootCliCommand
9+
10+
// Sub-commands can get a reference to the parent command by adding a property of the parent command type.
11+
12+
[CliCommand(Description = "A root cli command with nested children")]
13+
public class RootWithNestedChildrenReferencingRootCliCommand
14+
{
15+
[CliOption(Description = "This is a global option (Recursive option on the root command), it can appear anywhere on the command line",
16+
Recursive = true)]
17+
public string GlobalOption1 { get; set; } = "DefaultForGlobalOption1";
18+
19+
[CliArgument(Description = "Description for RootArgument1")]
20+
public string RootArgument1 { get; set; }
21+
22+
public void Run(CliContext context)
23+
{
24+
context.ShowValues();
25+
}
26+
27+
[CliCommand(Description = "A nested level 1 sub-command which accesses the root command")]
28+
public class Level1SubCliCommand
29+
{
30+
[CliOption(Description = "This is global for all sub commands (it can appear anywhere after the level-1 verb)",
31+
Recursive = true)]
32+
public string Level1RecursiveOption1 { get; set; } = "DefaultForLevel1RecusiveOption1";
33+
34+
[CliArgument(Description = "Description for Argument1")]
35+
public string Argument1 { get; set; }
36+
37+
// The parent command gets automatically injected
38+
public RootWithNestedChildrenReferencingRootCliCommand RootCommand { get; set; }
39+
40+
public void Run(CliContext context)
41+
{
42+
context.ShowValues();
43+
}
44+
45+
[CliCommand(Description = "A nested level 2 sub-command which accesses its parent commands")]
46+
public class Level2SubCliCommand
47+
{
48+
[CliOption(Description = "Description for Option1")]
49+
public string Option1 { get; set; } = "DefaultForOption1";
50+
51+
[CliArgument(Description = "Description for Argument1")]
52+
public string Argument1 { get; set; }
53+
54+
// All ancestor commands gets injected
55+
public RootWithNestedChildrenReferencingRootCliCommand RootCommand { get; set; }
56+
public Level1SubCliCommand ParentCommand { get; set; }
57+
58+
public void Run(CliContext context)
59+
{
60+
context.ShowValues();
61+
Console.WriteLine($"Level1RecursiveOption1 = {ParentCommand.Level1RecursiveOption1}");
62+
Console.WriteLine($"parent Argument1 = {ParentCommand.Argument1}");
63+
Console.WriteLine($"GlobalOption1 = {RootCommand.GlobalOption1}");
64+
Console.WriteLine($"RootArgument1 = {RootCommand.RootArgument1}");
65+
}
66+
}
67+
}
68+
}
69+
70+
#endregion
71+
}

src/TestApp/GeneratedFiles/net472/DotMake.CommandLine.SourceGeneration/DotMake.CommandLine.SourceGeneration.CliCommandGenerator/(ModuleInitializerAttribute).g.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
// <auto-generated />
1+
// <auto-generated />
22
// Generated by DotMake.CommandLine.SourceGeneration v1.8.5.0
3-
// Roslyn (Microsoft.CodeAnalysis) v4.900.24.8111
3+
// Roslyn (Microsoft.CodeAnalysis) v4.900.24.12101
44
// Generation: 1
55

66
#if !NET5_0_OR_GREATER

src/TestApp/GeneratedFiles/net472/DotMake.CommandLine.SourceGeneration/DotMake.CommandLine.SourceGeneration.CliCommandGenerator/(RequiredMemberAttribute).g.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
// <auto-generated />
1+
// <auto-generated />
22
// Generated by DotMake.CommandLine.SourceGeneration v1.8.5.0
3-
// Roslyn (Microsoft.CodeAnalysis) v4.900.24.8111
3+
// Roslyn (Microsoft.CodeAnalysis) v4.900.24.12101
44
// Generation: 1
55

66
// Licensed to the .NET Foundation under one or more agreements.

0 commit comments

Comments
 (0)