Skip to content

Commit 8320e50

Browse files
vpkopylovnatemcmaster
authored andcommitted
Add check for subcommand cycle (#239)
1 parent 08c6764 commit 8320e50

File tree

3 files changed

+78
-0
lines changed

3 files changed

+78
-0
lines changed

src/CommandLineUtils/Conventions/SubcommandAttributeConvention.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) Nate McMaster.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using McMaster.Extensions.CommandLineUtils.Abstractions;
5+
using McMaster.Extensions.CommandLineUtils.Errors;
46
using System;
57
using System.Linq;
68
using System.Reflection;
@@ -28,6 +30,8 @@ public virtual void Apply(ConventionContext context)
2830
var contextArgs = new object[] { context, attribute };
2931
foreach (var type in attribute.Types)
3032
{
33+
AssertSubcommandIsNotCycled(type, context.Application);
34+
3135
var impl = s_addSubcommandMethod.MakeGenericMethod(type);
3236
try
3337
{
@@ -42,6 +46,19 @@ public virtual void Apply(ConventionContext context)
4246
}
4347
}
4448

49+
private void AssertSubcommandIsNotCycled(Type modelType, CommandLineApplication parentCommand)
50+
{
51+
while (parentCommand != null)
52+
{
53+
if (parentCommand is IModelAccessor parentCommandAccessor
54+
&& parentCommandAccessor.GetModelType() == modelType)
55+
{
56+
throw new SubcommandCycleException(modelType);
57+
}
58+
parentCommand = parentCommand.Parent;
59+
}
60+
}
61+
4562
private static readonly MethodInfo s_addSubcommandMethod
4663
= typeof(SubcommandAttributeConvention).GetRuntimeMethods()
4764
.Single(m => m.Name == nameof(AddSubcommandImpl));
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) Nate McMaster.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
6+
namespace McMaster.Extensions.CommandLineUtils.Errors
7+
{
8+
/// <summary>
9+
/// The exception that is thrown when a subcommand cycle is detected
10+
/// </summary>
11+
public class SubcommandCycleException : Exception
12+
{
13+
/// <summary>
14+
/// Initializes an instance of <see cref="SubcommandCycleException"/>.
15+
/// </summary>
16+
/// <param name="modelType">The type of the cycled command model</param>
17+
public SubcommandCycleException(Type modelType)
18+
: base($"Subcommand cycle detected: trying to add command of model {modelType} as its own direct or indirect subcommand")
19+
{
20+
ModelType = modelType;
21+
}
22+
23+
/// <summary>
24+
/// The type of the cycled command model
25+
/// </summary>
26+
public Type ModelType { get; }
27+
}
28+
}

test/CommandLineUtils.Tests/SubcommandAttributeTests.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Nate McMaster.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using McMaster.Extensions.CommandLineUtils.Errors;
45
using System;
56
using System.IO;
67
using System.Linq;
@@ -198,5 +199,37 @@ public void CommandNamesCannotDifferByCaseOnly()
198199
() => CommandLineApplication.Execute<DuplicateSubCommands>(new TestConsole(_output)));
199200
Assert.Equal(Strings.DuplicateSubcommandName("level1"), ex.Message);
200201
}
202+
203+
[Command, Subcommand(typeof(CycledCommand2))]
204+
private class CycledCommand1
205+
{
206+
}
207+
208+
[Command, Subcommand(typeof(CycledCommand1))]
209+
private class CycledCommand2
210+
{
211+
}
212+
213+
[Fact]
214+
public void ThrowsForCycledSubCommand()
215+
{
216+
var ex = Assert.Throws<SubcommandCycleException>(
217+
() => CommandLineApplication.Execute<CycledCommand1>(new TestConsole(_output)));
218+
Assert.Equal(typeof(CycledCommand1), ex.ModelType);
219+
}
220+
221+
[Command, Subcommand(typeof(SelfCycledCommand))]
222+
private class SelfCycledCommand
223+
{
224+
225+
}
226+
227+
[Fact]
228+
public void ThrowsForSelfCycledCommand()
229+
{
230+
var ex = Assert.Throws<SubcommandCycleException>(
231+
() => CommandLineApplication.Execute<SelfCycledCommand>(new TestConsole(_output)));
232+
Assert.Equal(typeof(SelfCycledCommand), ex.ModelType);
233+
}
201234
}
202235
}

0 commit comments

Comments
 (0)