Skip to content

Commit

Permalink
Merge pull request #15 from poizan42/inject-parent-commands
Browse files Browse the repository at this point in the history
Proposal for fix for #14 by adding support for injecting parent commands as properties.
  • Loading branch information
calacayir authored Mar 5, 2024
2 parents e3f0587 + c7d0b80 commit 5e6b904
Show file tree
Hide file tree
Showing 86 changed files with 1,691 additions and 547 deletions.
44 changes: 38 additions & 6 deletions src/DotMake.CommandLine.SourceGeneration/CliCommandInfo.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
Expand Down Expand Up @@ -64,13 +65,19 @@ public CliCommandInfo(ISymbol symbol, SyntaxNode syntaxNode, AttributeData attri

var visitedProperties = new Dictionary<string, ISymbol>(StringComparer.Ordinal);
var addedProperties = new HashSet<string>(StringComparer.Ordinal);
Dictionary<ITypeSymbol, (int Index, CliCommandSettings Settings)> ancestorsByType = Settings
.GetParentTree()
.Select((s, i) => (i, Setings: s))
.ToDictionary(x => x.Setings.Symbol, (IEqualityComparer<ITypeSymbol>)SymbolEqualityComparer.Default);

foreach (var member in Symbol.GetAllMembers())
{
if (member is IPropertySymbol)
if (member is IPropertySymbol property)
{
if (addedProperties.Contains(member.Name))
continue;

bool added = false;
foreach (var memberAttributeData in member.GetAttributes())
{
var attributeFullName = memberAttributeData.AttributeClass?.ToCompareString();
Expand All @@ -81,12 +88,19 @@ public CliCommandInfo(ISymbol symbol, SyntaxNode syntaxNode, AttributeData attri
{
childOptions.Add(new CliOptionInfo(visitedMember ?? member, null, memberAttributeData, SemanticModel, this));
addedProperties.Add(member.Name);
added = true;
} else if (attributeFullName == CliArgumentInfo.AttributeFullName)
{
childArguments.Add(new CliArgumentInfo(visitedMember ?? member, null, memberAttributeData, SemanticModel, this));
addedProperties.Add(member.Name);
added = true;
}
}
if (!added && ancestorsByType.TryGetValue(property.Type, out var ancestorInfo))
{
childParentCommandRefs.Add(new CliParentCommandRefInfo(property, null, SemanticModel, ancestorInfo.Index, ancestorInfo.Settings));
addedProperties.Add(member.Name);
}

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

public IReadOnlyList<CliParentCommandRefInfo> ChildParentCommandRefs => childParentCommandRefs;
private readonly List<CliParentCommandRefInfo> childParentCommandRefs = new List<CliParentCommandRefInfo>();

private void Analyze()
{
if ((Symbol.DeclaredAccessibility != Accessibility.Public && Symbol.DeclaredAccessibility != Accessibility.Internal)
Expand Down Expand Up @@ -206,16 +223,21 @@ public override void ReportDiagnostics(SourceProductionContext sourceProductionC

foreach (var child in ChildCommands)
child.ReportDiagnostics(sourceProductionContext);

foreach (var child in ChildParentCommandRefs)
child.ReportDiagnostics(sourceProductionContext);
}

public void AppendCSharpDefineString(CodeStringBuilder sb, bool addNamespaceBlock)
{
var childOptionsWithoutProblem = ChildOptions.Where(c => !c.HasProblem).ToArray();
var childArgumentsWithoutProblem = ChildArguments.Where(c => !c.HasProblem).ToArray();
var childCommandsWithoutProblem = ChildCommands.Where(c => !c.HasProblem).ToArray();
var childParentCommandRefsWithoutProblem = ChildParentCommandRefs.Where(c => !c.HasProblem).ToArray();
var handlerWithoutProblem = (Handler != null && !Handler.HasProblem) ? Handler : null;
var memberHasRequiredModifier = childOptionsWithoutProblem.Any(o => o.Symbol.IsRequired)
|| childArgumentsWithoutProblem.Any(a => a.Symbol.IsRequired);
|| childArgumentsWithoutProblem.Any(a => a.Symbol.IsRequired)
|| childParentCommandRefsWithoutProblem.Any(r => r.Symbol.IsRequired);

if (string.IsNullOrEmpty(GeneratedClassNamespace))
addNamespaceBlock = false;
Expand Down Expand Up @@ -308,7 +330,7 @@ public void AppendCSharpDefineString(CodeStringBuilder sb, bool addNamespaceBloc
}

sb.AppendLine();
using (sb.AppendBlockStart($"BindFunc = (parseResult) =>", ";"))
using (sb.AppendBlockStart($"BindFunc = (cliBindContext) =>", ";"))
{
var varTargetClass = "targetClass";

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

sb.AppendLine();
Expand All @@ -329,7 +351,15 @@ public void AppendCSharpDefineString(CodeStringBuilder sb, bool addNamespaceBloc
{
var cliArgumentInfo = childArgumentsWithoutProblem[index];
var varArgument = $"argument{index}";
sb.AppendLine($"{varTargetClass}.{cliArgumentInfo.Symbol.Name} = GetValueForArgument(parseResult, {varArgument});");
sb.AppendLine($"{varTargetClass}.{cliArgumentInfo.Symbol.Name} = GetValueForArgument(cliBindContext.ParseResult, {varArgument});");
}

sb.AppendLine();
sb.AppendLine("// Set the values for the parent command references");
for (var index = 0; index < childParentCommandRefsWithoutProblem.Length; index++)
{
var cliParentCommandRefInfo = childParentCommandRefsWithoutProblem[index];
sb.AppendLine($"{varTargetClass}.{cliParentCommandRefInfo.Symbol.Name} = cliBindContext.BindOrGetBindResult<{cliParentCommandRefInfo.Symbol.Type.ToReferenceString()}>();");
}

sb.AppendLine();
Expand All @@ -339,6 +369,7 @@ public void AppendCSharpDefineString(CodeStringBuilder sb, bool addNamespaceBloc
sb.AppendLine();
var varParseResult = "parseResult";
var varCancellationToken = "cancellationToken";
var varCliBindContext = "cliBindContext";
var varCliContext = "cliContext";
var isAsync = (handlerWithoutProblem != null && handlerWithoutProblem.IsAsync);
using (sb.AppendBlockStart(isAsync
Expand All @@ -348,7 +379,8 @@ public void AppendCSharpDefineString(CodeStringBuilder sb, bool addNamespaceBloc
{
var varTargetClass = "targetClass";

sb.AppendLine($"var {varTargetClass} = ({definitionClass}) BindFunc({varParseResult});");
sb.AppendLine($"var {varCliBindContext} = new DotMake.CommandLine.CliBindContext({varParseResult});");
sb.AppendLine($"var {varTargetClass} = ({definitionClass}) BindFunc({varCliBindContext});");
sb.AppendLine();

sb.AppendLine("// Call the command handler");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System;
using Microsoft.CodeAnalysis;

namespace DotMake.CommandLine.SourceGeneration
{
public class CliParentCommandRefInfo : CliSymbolInfo, IEquatable<CliParentCommandRefInfo>
{
public const string DiagnosticName = "CLI parent command reference";

public CliParentCommandRefInfo(IPropertySymbol symbol, SyntaxNode syntaxNode, SemanticModel semanticModel,
int parentTreeIndex, CliCommandSettings parentCommandSettings) : base(symbol, syntaxNode, semanticModel)
{
ParentTreeIndex = parentTreeIndex;
ParentCommandSettings = parentCommandSettings;

Analyze();

if (HasProblem)
return;
}

public int ParentTreeIndex { get; }
public CliCommandSettings ParentCommandSettings { get; }
public new IPropertySymbol Symbol => (IPropertySymbol)base.Symbol;

private void Analyze()
{
if ((Symbol.DeclaredAccessibility != Accessibility.Public && Symbol.DeclaredAccessibility != Accessibility.Internal)
|| Symbol.IsStatic)
AddDiagnostic(DiagnosticDescriptors.WarningPropertyNotPublicNonStatic, DiagnosticName);
else
{
if (Symbol.GetMethod == null
|| (Symbol.GetMethod.DeclaredAccessibility != Accessibility.Public && Symbol.GetMethod.DeclaredAccessibility != Accessibility.Internal))
AddDiagnostic(DiagnosticDescriptors.ErrorPropertyHasNotPublicGetter, DiagnosticName);

if (Symbol.SetMethod == null
|| (Symbol.SetMethod.DeclaredAccessibility != Accessibility.Public && Symbol.SetMethod.DeclaredAccessibility != Accessibility.Internal))
AddDiagnostic(DiagnosticDescriptors.ErrorPropertyHasNotPublicSetter, DiagnosticName);
}
}

public bool Equals(CliParentCommandRefInfo other)
{
return base.Equals(other);
}
}
}
52 changes: 52 additions & 0 deletions src/DotMake.CommandLine/CliBindContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.CommandLine;

namespace DotMake.CommandLine
{
/// <summary>
/// Context used during binding of commands
/// </summary>
public class CliBindContext
{
/// <summary>
/// Initializes a new instance of the <see cref="CliBindContext" /> class.
/// </summary>
/// <param name="parseResult">A parse result describing the outcome of the parse operation.</param>
public CliBindContext(ParseResult parseResult)
{
ParseResult = parseResult;
}

/// <summary>A parse result describing the outcome of the parse operation.</summary>
public ParseResult ParseResult { get; }

/// <summary>
/// Creates a new instance of the definition class and binds/populates the properties from the parse result,
/// or returns a cached instance of the definition class earlier returned from either BindOrGetBindResult() overload.
/// </summary>
/// <typeparam name="TDefinition">The definition class.</typeparam>
/// <returns></returns>
public TDefinition BindOrGetBindResult<TDefinition>() => (TDefinition)BindOrGetBindResult(typeof(TDefinition));

/// <summary>
/// Creates a new instance of the definition class and binds/populates the properties from the parse result,
/// or returns a cached instance of the definition class earlier returned from either BindOrGetBindResult() overload.
/// </summary>
/// <param name="commandDefinitionType">The type of the definition class.</param>
/// <returns>An instance of the definition class whose properties were bound/populated from the parse result.</returns>
public object BindOrGetBindResult(Type commandDefinitionType)
{
if (bindResults.TryGetValue(commandDefinitionType, out object bindResult))
{
return bindResult;
}
var commandBuilder = CliCommandBuilder.Get(commandDefinitionType);
object commandObj = commandBuilder.Bind(this);
bindResults[commandDefinitionType] = commandObj;
return commandObj;
}

private readonly Dictionary<Type, object> bindResults = new();
}
}
16 changes: 13 additions & 3 deletions src/DotMake.CommandLine/CliCommandBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ namespace DotMake.CommandLine
public abstract class CliCommandBuilder
{
/// <summary>
/// A delegate which is set by the source generator to be called from <see cref="Bind"/> method.
/// A delegate which is set by the source generator to be called from <see cref="Bind(CliBindContext)"/> method.
/// </summary>
protected Func<ParseResult, object> BindFunc;
protected Func<CliBindContext, object> BindFunc;

/// <summary>
/// Initializes a new instance of the <see cref="CliCommandBuilder" /> class.
Expand Down Expand Up @@ -72,11 +72,21 @@ protected CliCommandBuilder()
/// <param name="parseResult">A parse result describing the outcome of the parse operation.</param>
/// <returns>An instance of the definition class whose properties were bound/populated from the parse result.</returns>
public object Bind(ParseResult parseResult)
{
return Bind(new CliBindContext(parseResult));
}

/// <summary>
/// Creates a new instance of the definition class and binds/populates the properties from the parse result.
/// </summary>
/// <param name="cliBindContext">A <see cref="CliBindContext"/> instance to use for the binding operation.</param>
/// <returns>An instance of the definition class whose properties were bound/populated from the parse result.</returns>
public object Bind(CliBindContext cliBindContext)
{
if (BindFunc == null)
throw new Exception("Ensure Build method is called first.");

return BindFunc(parseResult);
return BindFunc(cliBindContext);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#pragma warning disable CS1591
#pragma warning disable CA1822 // Mark members as static
using System;
using DotMake.CommandLine;

namespace TestApp.Commands
{
#region RootWithNestedChildrenReferencingRootCliCommand

// Sub-commands can get a reference to the parent command by adding a property of the parent command type.

[CliCommand(Description = "A root cli command with nested children")]
public class RootWithNestedChildrenReferencingRootCliCommand
{
[CliOption(Description = "This is a global option (Recursive option on the root command), it can appear anywhere on the command line",
Recursive = true)]
public string GlobalOption1 { get; set; } = "DefaultForGlobalOption1";

[CliArgument(Description = "Description for RootArgument1")]
public string RootArgument1 { get; set; }

public void Run(CliContext context)
{
context.ShowValues();
}

[CliCommand(Description = "A nested level 1 sub-command which accesses the root command")]
public class Level1SubCliCommand
{
[CliOption(Description = "This is global for all sub commands (it can appear anywhere after the level-1 verb)",
Recursive = true)]
public string Level1RecursiveOption1 { get; set; } = "DefaultForLevel1RecusiveOption1";

[CliArgument(Description = "Description for Argument1")]
public string Argument1 { get; set; }

// The parent command gets automatically injected
public RootWithNestedChildrenReferencingRootCliCommand RootCommand { get; set; }

public void Run(CliContext context)
{
context.ShowValues();
}

[CliCommand(Description = "A nested level 2 sub-command which accesses its parent commands")]
public class Level2SubCliCommand
{
[CliOption(Description = "Description for Option1")]
public string Option1 { get; set; } = "DefaultForOption1";

[CliArgument(Description = "Description for Argument1")]
public string Argument1 { get; set; }

// All ancestor commands gets injected
public RootWithNestedChildrenReferencingRootCliCommand RootCommand { get; set; }
public Level1SubCliCommand ParentCommand { get; set; }

public void Run(CliContext context)
{
context.ShowValues();
Console.WriteLine($"Level1RecursiveOption1 = {ParentCommand.Level1RecursiveOption1}");
Console.WriteLine($"parent Argument1 = {ParentCommand.Argument1}");
Console.WriteLine($"GlobalOption1 = {RootCommand.GlobalOption1}");
Console.WriteLine($"RootArgument1 = {RootCommand.RootArgument1}");
}
}
}
}

#endregion
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
// Generated by DotMake.CommandLine.SourceGeneration v1.8.5.0
// Roslyn (Microsoft.CodeAnalysis) v4.900.24.8111
// Roslyn (Microsoft.CodeAnalysis) v4.900.24.12101
// Generation: 1

#if !NET5_0_OR_GREATER
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
// Generated by DotMake.CommandLine.SourceGeneration v1.8.5.0
// Roslyn (Microsoft.CodeAnalysis) v4.900.24.8111
// Roslyn (Microsoft.CodeAnalysis) v4.900.24.12101
// Generation: 1

// Licensed to the .NET Foundation under one or more agreements.
Expand Down
Loading

0 comments on commit 5e6b904

Please sign in to comment.