Refactor (#94)

This commit is contained in:
Alexey Golub
2021-03-21 09:54:00 +02:00
committed by GitHub
parent 58df63a7ad
commit 7d3d79b861
228 changed files with 10480 additions and 8968 deletions

View File

@@ -1,43 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
namespace CliFx.Analyzers.Tests
{
public class AnalyzerTestCase
{
public string Name { get; }
public IReadOnlyList<DiagnosticDescriptor> TestedDiagnostics { get; }
public IReadOnlyList<string> SourceCodes { get; }
public AnalyzerTestCase(
string name,
IReadOnlyList<DiagnosticDescriptor> testedDiagnostics,
IReadOnlyList<string> sourceCodes)
{
Name = name;
TestedDiagnostics = testedDiagnostics;
SourceCodes = sourceCodes;
}
public AnalyzerTestCase(
string name,
IReadOnlyList<DiagnosticDescriptor> testedDiagnostics,
string sourceCode)
: this(name, testedDiagnostics, new[] {sourceCode})
{
}
public AnalyzerTestCase(
string name,
DiagnosticDescriptor testedDiagnostic,
string sourceCode)
: this(name, new[] {testedDiagnostic}, sourceCode)
{
}
public override string ToString() => $"{Name} [{string.Join(", ", TestedDiagnostics.Select(d => d.Id))}]";
}
}

View File

@@ -9,14 +9,17 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Gu.Roslyn.Asserts" Version="3.3.1" />
<PackageReference Include="GitHubActionsTestLogger" Version="1.1.2" />
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="GitHubActionsTestLogger" Version="1.2.0" />
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.4.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.4.0" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" PrivateAssets="all" />
<PackageReference Include="coverlet.msbuild" Version="2.9.0" PrivateAssets="all" />
<PackageReference Include="coverlet.msbuild" Version="3.0.3" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,72 @@
using CliFx.Analyzers.Tests.Utils;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace CliFx.Analyzers.Tests
{
public class CommandMustBeAnnotatedAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new CommandMustBeAnnotatedAnalyzer();
[Fact]
public void Analyzer_reports_an_error_if_a_command_is_not_annotated_with_the_command_attribute()
{
// Arrange
// language=cs
const string code = @"
public class MyCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_a_command_is_annotated_with_the_command_attribute()
{
// Arrange
// language=cs
const string code = @"
[Command]
public abstract class MyCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_a_command_is_implemented_as_an_abstract_class()
{
// Arrange
// language=cs
const string code = @"
public abstract class MyCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_on_a_class_that_is_not_a_command()
{
// Arrange
// language=cs
const string code = @"
public class Foo
{
public int Bar { get; set; } = 5;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
}
}

View File

@@ -0,0 +1,58 @@
using CliFx.Analyzers.Tests.Utils;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace CliFx.Analyzers.Tests
{
public class CommandMustImplementInterfaceAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new CommandMustImplementInterfaceAnalyzer();
[Fact]
public void Analyzer_reports_an_error_if_a_command_does_not_implement_ICommand_interface()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_a_command_implements_ICommand_interface()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_on_a_class_that_is_not_a_command()
{
// Arrange
// language=cs
const string code = @"
public class Foo
{
public int Bar { get; set; } = 5;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
}
}

View File

@@ -1,719 +0,0 @@
using System.Collections.Generic;
using CliFx.Analyzers.Tests.Internal;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace CliFx.Analyzers.Tests
{
public class CommandSchemaAnalyzerTests
{
private static DiagnosticAnalyzer Analyzer { get; } = new CommandSchemaAnalyzer();
public static IEnumerable<object[]> GetValidCases()
{
yield return new object[]
{
new AnalyzerTestCase(
"Non-command type",
Analyzer.SupportedDiagnostics,
// language=cs
@"
public class Foo
{
public int Bar { get; set; } = 5;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Command implements interface and has attribute",
Analyzer.SupportedDiagnostics,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Command doesn't have an attribute but is an abstract type",
Analyzer.SupportedDiagnostics,
// language=cs
@"
public abstract class MyCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Parameters with unique order",
Analyzer.SupportedDiagnostics,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(13)]
public string ParamA { get; set; }
[CommandParameter(15)]
public string ParamB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Parameters with unique names",
Analyzer.SupportedDiagnostics,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(13, Name = ""foo"")]
public string ParamA { get; set; }
[CommandParameter(15, Name = ""bar"")]
public string ParamB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Single non-scalar parameter",
Analyzer.SupportedDiagnostics,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(1)]
public string ParamA { get; set; }
[CommandParameter(2)]
public HashSet<string> ParamB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Non-scalar parameter is last in order",
Analyzer.SupportedDiagnostics,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(1)]
public string ParamA { get; set; }
[CommandParameter(2)]
public IReadOnlyList<string> ParamB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Parameter with valid converter",
Analyzer.SupportedDiagnostics,
// language=cs
@"
public class MyConverter : ArgumentValueConverter<string>
{
public string ConvertFrom(string value) => value;
}
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0, Converter = typeof(MyConverter))]
public string Param { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Parameter with valid validator",
Analyzer.SupportedDiagnostics,
// language=cs
@"
public class MyValidator : ArgumentValueValidator<string>
{
public ValidationResult Validate(string value) => ValidationResult.Ok();
}
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0, Validators = new[] {typeof(MyValidator)})]
public string Param { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Option with a proper name",
Analyzer.SupportedDiagnostics,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"")]
public string Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Option with a proper name and short name",
Analyzer.SupportedDiagnostics,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"", 'f')]
public string Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Options with unique names",
Analyzer.SupportedDiagnostics,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"")]
public string OptionA { get; set; }
[CommandOption(""bar"")]
public string OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Options with unique short names",
Analyzer.SupportedDiagnostics,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandOption('f')]
public string OptionA { get; set; }
[CommandOption('x')]
public string OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Options with unique environment variable names",
Analyzer.SupportedDiagnostics,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandOption('a', EnvironmentVariableName = ""env_var_a"")]
public string OptionA { get; set; }
[CommandOption('b', EnvironmentVariableName = ""env_var_b"")]
public string OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Option with valid converter",
Analyzer.SupportedDiagnostics,
// language=cs
@"
public class MyConverter : ArgumentValueConverter<string>
{
public string ConvertFrom(string value) => value;
}
[Command]
public class MyCommand : ICommand
{
[CommandOption('o', Converter = typeof(MyConverter))]
public string Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Option with valid validator",
Analyzer.SupportedDiagnostics,
// language=cs
@"
public class MyValidator : ArgumentValueValidator<string>
{
public ValidationResult Validate(string value) => ValidationResult.Ok();
}
[Command]
public class MyCommand : ICommand
{
[CommandOption('o', Validators = new[] {typeof(MyValidator)})]
public string Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
}
public static IEnumerable<object[]> GetInvalidCases()
{
yield return new object[]
{
new AnalyzerTestCase(
"Command is missing the attribute",
DiagnosticDescriptors.CliFx0002,
// language=cs
@"
public class MyCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Command doesn't implement the interface",
DiagnosticDescriptors.CliFx0001,
// language=cs
@"
[Command]
public class MyCommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Parameters with duplicate order",
DiagnosticDescriptors.CliFx0021,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(13)]
public string ParamA { get; set; }
[CommandParameter(13)]
public string ParamB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Parameters with duplicate names",
DiagnosticDescriptors.CliFx0022,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(13, Name = ""foo"")]
public string ParamA { get; set; }
[CommandParameter(15, Name = ""foo"")]
public string ParamB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Multiple non-scalar parameters",
DiagnosticDescriptors.CliFx0023,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(1)]
public IReadOnlyList<string> ParamA { get; set; }
[CommandParameter(2)]
public HashSet<string> ParamB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Non-last non-scalar parameter",
DiagnosticDescriptors.CliFx0024,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(1)]
public IReadOnlyList<string> ParamA { get; set; }
[CommandParameter(2)]
public string ParamB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Parameter with invalid converter",
DiagnosticDescriptors.CliFx0025,
// language=cs
@"
public class MyConverter
{
public object ConvertFrom(string value) => value;
}
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0, Converter = typeof(MyConverter))]
public string Param { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Parameter with invalid validator",
DiagnosticDescriptors.CliFx0026,
// language=cs
@"
public class MyValidator
{
public ValidationResult Validate(string value) => ValidationResult.Ok();
}
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0, Validators = new[] {typeof(MyValidator)})]
public string Param { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Option with an empty name",
DiagnosticDescriptors.CliFx0041,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandOption("""")]
public string Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Option with a name which is too short",
DiagnosticDescriptors.CliFx0042,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandOption(""a"")]
public string Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Options with duplicate names",
DiagnosticDescriptors.CliFx0043,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"")]
public string OptionA { get; set; }
[CommandOption(""foo"")]
public string OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Options with duplicate short names",
DiagnosticDescriptors.CliFx0044,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandOption('f')]
public string OptionA { get; set; }
[CommandOption('f')]
public string OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Options with duplicate environment variable names",
DiagnosticDescriptors.CliFx0045,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandOption('a', EnvironmentVariableName = ""env_var"")]
public string OptionA { get; set; }
[CommandOption('b', EnvironmentVariableName = ""env_var"")]
public string OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Option with invalid converter",
DiagnosticDescriptors.CliFx0046,
// language=cs
@"
public class MyConverter
{
public object ConvertFrom(string value) => value;
}
[Command]
public class MyCommand : ICommand
{
[CommandOption('o', Converter = typeof(MyConverter))]
public string Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Option with invalid validator",
DiagnosticDescriptors.CliFx0047,
// language=cs
@"
public class MyValidator
{
public ValidationResult Validate(string value) => ValidationResult.Ok();
}
[Command]
public class MyCommand : ICommand
{
[CommandOption('o', Validators = new[] {typeof(MyValidator)})]
public string Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Option with a name that doesn't start with a letter character",
DiagnosticDescriptors.CliFx0048,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandOption(""0foo"")]
public string Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Option with a short name that isn't a letter character",
DiagnosticDescriptors.CliFx0049,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandOption('0')]
public string Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
}
[Theory]
[MemberData(nameof(GetValidCases))]
public void Valid(AnalyzerTestCase testCase) =>
Analyzer.Should().NotProduceDiagnostics(testCase);
[Theory]
[MemberData(nameof(GetInvalidCases))]
public void Invalid(AnalyzerTestCase testCase) =>
Analyzer.Should().ProduceDiagnostics(testCase);
}
}

View File

@@ -1,144 +0,0 @@
using System.Collections.Generic;
using CliFx.Analyzers.Tests.Internal;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace CliFx.Analyzers.Tests
{
public class ConsoleUsageAnalyzerTests
{
private static DiagnosticAnalyzer Analyzer { get; } = new ConsoleUsageAnalyzer();
public static IEnumerable<object[]> GetValidCases()
{
yield return new object[]
{
new AnalyzerTestCase(
"Using console abstraction",
Analyzer.SupportedDiagnostics,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(""Hello world"");
return default;
}
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Console abstraction is not available in scope",
Analyzer.SupportedDiagnostics,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
public void SomeOtherMethod() => Console.WriteLine(""Test"");
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
}
public static IEnumerable<object[]> GetInvalidCases()
{
yield return new object[]
{
new AnalyzerTestCase(
"Not using available console abstraction in the ExecuteAsync method",
Analyzer.SupportedDiagnostics,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
Console.WriteLine(""Hello world"");
return default;
}
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Not using available console abstraction in the ExecuteAsync method when writing stderr",
Analyzer.SupportedDiagnostics,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
Console.Error.WriteLine(""Hello world"");
return default;
}
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Not using available console abstraction while referencing System.Console by full name",
Analyzer.SupportedDiagnostics,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
System.Console.Error.WriteLine(""Hello world"");
return default;
}
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Not using available console abstraction in another method",
DiagnosticDescriptors.CliFx0100,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
public void SomeOtherMethod(IConsole console) => Console.WriteLine(""Test"");
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
}
[Theory]
[MemberData(nameof(GetValidCases))]
public void Valid(AnalyzerTestCase testCase) =>
Analyzer.Should().NotProduceDiagnostics(testCase);
[Theory]
[MemberData(nameof(GetInvalidCases))]
public void Invalid(AnalyzerTestCase testCase) =>
Analyzer.Should().ProduceDiagnostics(testCase);
}
}

View File

@@ -0,0 +1,31 @@
using System;
using System.Linq;
using FluentAssertions;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace CliFx.Analyzers.Tests
{
public class GeneralSpecs
{
[Fact]
public void All_analyzers_have_unique_diagnostic_IDs()
{
// Arrange
var analyzers = typeof(AnalyzerBase)
.Assembly
.GetTypes()
.Where(t => !t.IsAbstract && t.IsAssignableTo(typeof(DiagnosticAnalyzer)))
.Select(t => (DiagnosticAnalyzer) Activator.CreateInstance(t)!)
.ToArray();
// Act
var diagnosticIds = analyzers
.SelectMany(a => a.SupportedDiagnostics.Select(d => d.Id))
.ToArray();
// Assert
diagnosticIds.Should().OnlyHaveUniqueItems();
}
}
}

View File

@@ -1,107 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions.Execution;
using FluentAssertions.Primitives;
using Gu.Roslyn.Asserts;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers.Tests.Internal
{
internal partial class AnalyzerAssertions : ReferenceTypeAssertions<DiagnosticAnalyzer, AnalyzerAssertions>
{
protected override string Identifier { get; } = "analyzer";
public AnalyzerAssertions(DiagnosticAnalyzer analyzer)
: base(analyzer)
{
}
public void ProduceDiagnostics(
IReadOnlyList<DiagnosticDescriptor> diagnostics,
IReadOnlyList<string> sourceCodes)
{
var producedDiagnostics = GetProducedDiagnostics(Subject, sourceCodes);
var expectedIds = diagnostics.Select(d => d.Id).Distinct().OrderBy(d => d).ToArray();
var producedIds = producedDiagnostics.Select(d => d.Id).Distinct().OrderBy(d => d).ToArray();
var result = expectedIds.Intersect(producedIds).Count() == expectedIds.Length;
Execute.Assertion.ForCondition(result).FailWith($@"
Expected and produced diagnostics do not match.
Expected: {string.Join(", ", expectedIds)}
Produced: {(producedIds.Any() ? string.Join(", ", producedIds) : "<none>")}
".Trim());
}
public void ProduceDiagnostics(AnalyzerTestCase testCase) =>
ProduceDiagnostics(testCase.TestedDiagnostics, testCase.SourceCodes);
public void NotProduceDiagnostics(
IReadOnlyList<DiagnosticDescriptor> diagnostics,
IReadOnlyList<string> sourceCodes)
{
var producedDiagnostics = GetProducedDiagnostics(Subject, sourceCodes);
var expectedIds = diagnostics.Select(d => d.Id).Distinct().OrderBy(d => d).ToArray();
var producedIds = producedDiagnostics.Select(d => d.Id).Distinct().OrderBy(d => d).ToArray();
var result = !expectedIds.Intersect(producedIds).Any();
Execute.Assertion.ForCondition(result).FailWith($@"
Expected no produced diagnostics.
Produced: {string.Join(", ", producedIds)}
".Trim());
}
public void NotProduceDiagnostics(AnalyzerTestCase testCase) =>
NotProduceDiagnostics(testCase.TestedDiagnostics, testCase.SourceCodes);
}
internal partial class AnalyzerAssertions
{
private static IReadOnlyList<MetadataReference> DefaultMetadataReferences { get; } =
MetadataReferences.Transitive(typeof(CliApplication).Assembly).ToArray();
private static string WrapCodeWithUsingDirectives(string code)
{
var usingDirectives = new[]
{
"using System;",
"using System.Collections.Generic;",
"using System.Threading.Tasks;",
"using CliFx;",
"using CliFx.Attributes;",
"using CliFx.Exceptions;",
"using CliFx.Utilities;"
};
return
string.Join(Environment.NewLine, usingDirectives) +
Environment.NewLine +
code;
}
private static IReadOnlyList<Diagnostic> GetProducedDiagnostics(
DiagnosticAnalyzer analyzer,
IReadOnlyList<string> sourceCodes)
{
var compilationOptions = new CSharpCompilationOptions(OutputKind.ConsoleApplication);
var wrappedSourceCodes = sourceCodes.Select(WrapCodeWithUsingDirectives).ToArray();
return Analyze.GetDiagnostics(analyzer, wrappedSourceCodes, compilationOptions, DefaultMetadataReferences)
.SelectMany(d => d)
.ToArray();
}
}
internal static class AnalyzerAssertionsExtensions
{
public static AnalyzerAssertions Should(this DiagnosticAnalyzer analyzer) => new(analyzer);
}
}

View File

@@ -0,0 +1,80 @@
using CliFx.Analyzers.Tests.Utils;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace CliFx.Analyzers.Tests
{
public class OptionMustBeInsideCommandAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustBeInsideCommandAnalyzer();
[Fact]
public void Analyzer_reports_an_error_if_an_option_is_inside_a_class_that_is_not_a_command()
{
// Arrange
// language=cs
const string code = @"
public class MyClass
{
[CommandOption(""foo"")]
public string Foo { get; set; }
}";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_an_option_is_inside_a_command()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"")]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_an_option_is_inside_an_abstract_class()
{
// Arrange
// language=cs
const string code = @"
public abstract class MyCommand
{
[CommandOption(""foo"")]
public string Foo { get; set; }
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
}
}

View File

@@ -0,0 +1,86 @@
using CliFx.Analyzers.Tests.Utils;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace CliFx.Analyzers.Tests
{
public class OptionMustHaveNameOrShortNameAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveNameOrShortNameAnalyzer();
[Fact]
public void Analyzer_reports_an_error_if_an_option_does_not_have_a_name_or_short_name()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandOption(null)]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_an_option_has_a_name()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"")]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_an_option_has_a_short_name()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandOption('f')]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
}
}

View File

@@ -0,0 +1,92 @@
using CliFx.Analyzers.Tests.Utils;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace CliFx.Analyzers.Tests
{
public class OptionMustHaveUniqueNameAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveUniqueNameAnalyzer();
[Fact]
public void Analyzer_reports_an_error_if_an_option_has_the_same_name_as_another_option()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"")]
public string Foo { get; set; }
[CommandOption(""foo"")]
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_an_option_has_a_unique_name()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"")]
public string Foo { get; set; }
[CommandOption(""bar"")]
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_a_name()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandOption('f')]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
}
}

View File

@@ -0,0 +1,114 @@
using CliFx.Analyzers.Tests.Utils;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace CliFx.Analyzers.Tests
{
public class OptionMustHaveUniqueShortNameAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveUniqueShortNameAnalyzer();
[Fact]
public void Analyzer_reports_an_error_if_an_option_has_the_same_short_name_as_another_option()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandOption('f')]
public string Foo { get; set; }
[CommandOption('f')]
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_an_option_has_a_unique_short_name()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandOption('f')]
public string Foo { get; set; }
[CommandOption('b')]
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_an_option_has_a_short_name_which_is_unique_only_in_casing()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandOption('f')]
public string Foo { get; set; }
[CommandOption('F')]
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_a_short_name()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"")]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
}
}

View File

@@ -0,0 +1,96 @@
using CliFx.Analyzers.Tests.Utils;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace CliFx.Analyzers.Tests
{
public class OptionMustHaveValidConverterAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveValidConverterAnalyzer();
[Fact]
public void Analyzer_reports_an_error_if_the_specified_option_converter_does_not_derive_from_BindingConverter()
{
// Arrange
// language=cs
const string code = @"
public class MyConverter
{
public string Convert(string rawValue) => rawValue;
}
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"", Converter = typeof(MyConverter))]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_the_specified_option_converter_derives_from_BindingConverter()
{
// Arrange
// language=cs
const string code = @"
public class MyConverter : BindingConverter<string>
{
public override string Convert(string rawValue) => rawValue;
}
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"", Converter = typeof(MyConverter))]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_a_converter()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"")]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
}
}

View File

@@ -0,0 +1,105 @@
using CliFx.Analyzers.Tests.Utils;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace CliFx.Analyzers.Tests
{
public class OptionMustHaveValidNameAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveValidNameAnalyzer();
[Fact]
public void Analyzer_reports_an_error_if_an_option_has_a_name_which_is_too_short()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandOption(""f"")]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
}
[Fact]
public void Analyzer_reports_an_error_if_an_option_has_a_name_that_starts_with_a_non_letter_character()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandOption(""1foo"")]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_an_option_has_a_valid_name()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"")]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_a_name()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandOption('f')]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
}
}

View File

@@ -0,0 +1,86 @@
using CliFx.Analyzers.Tests.Utils;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace CliFx.Analyzers.Tests
{
public class OptionMustHaveValidShortNameAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveValidShortNameAnalyzer();
[Fact]
public void Analyzer_reports_an_error_if_an_option_has_a_short_name_which_is_not_a_letter_character()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandOption('1')]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_an_option_has_a_valid_short_name()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandOption('f')]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_a_short_name()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"")]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
}
}

View File

@@ -0,0 +1,96 @@
using CliFx.Analyzers.Tests.Utils;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace CliFx.Analyzers.Tests
{
public class OptionMustHaveValidValidatorsAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveValidValidatorsAnalyzer();
[Fact]
public void Analyzer_reports_an_error_if_one_of_the_specified_option_validators_does_not_derive_from_BindingValidator()
{
// Arrange
// language=cs
const string code = @"
public class MyValidator
{
public void Validate(string value) {}
}
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"", Validators = new[] {typeof(MyValidator)})]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_all_specified_option_validators_derive_from_BindingValidator()
{
// Arrange
// language=cs
const string code = @"
public class MyValidator : BindingValidator<string>
{
public override BindingValidationError Validate(string value) => Ok();
}
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"", Validators = new[] {typeof(MyValidator)})]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_validators()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"")]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
}
}

View File

@@ -0,0 +1,80 @@
using CliFx.Analyzers.Tests.Utils;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace CliFx.Analyzers.Tests
{
public class ParameterMustBeInsideCommandAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustBeInsideCommandAnalyzer();
[Fact]
public void Analyzer_reports_an_error_if_a_parameter_is_inside_a_class_that_is_not_a_command()
{
// Arrange
// language=cs
const string code = @"
public class MyClass
{
[CommandParameter(0)]
public string Foo { get; set; }
}";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_a_parameter_is_inside_a_command()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0)]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_a_parameter_is_inside_an_abstract_class()
{
// Arrange
// language=cs
const string code = @"
public abstract class MyCommand
{
[CommandParameter(0)]
public string Foo { get; set; }
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
}
}

View File

@@ -0,0 +1,95 @@
using CliFx.Analyzers.Tests.Utils;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace CliFx.Analyzers.Tests
{
public class ParameterMustBeLastIfNonScalarAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustBeLastIfNonScalarAnalyzer();
[Fact]
public void Analyzer_reports_an_error_if_a_non_scalar_parameter_is_not_last_in_order()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0)]
public string[] Foo { get; set; }
[CommandParameter(1)]
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_a_non_scalar_parameter_is_last_in_order()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0)]
public string Foo { get; set; }
[CommandParameter(1)]
public string[] Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_no_non_scalar_parameters_are_defined()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0)]
public string Foo { get; set; }
[CommandParameter(1)]
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
}
}

View File

@@ -0,0 +1,95 @@
using CliFx.Analyzers.Tests.Utils;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace CliFx.Analyzers.Tests
{
public class ParameterMustBeSingleIfNonScalarAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustBeSingleIfNonScalarAnalyzer();
[Fact]
public void Analyzer_reports_an_error_if_more_than_one_non_scalar_parameters_are_defined()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0)]
public string[] Foo { get; set; }
[CommandParameter(1)]
public string[] Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_only_one_non_scalar_parameter_is_defined()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0)]
public string Foo { get; set; }
[CommandParameter(1)]
public string[] Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_no_non_scalar_parameters_are_defined()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0)]
public string Foo { get; set; }
[CommandParameter(1)]
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
}
}

View File

@@ -0,0 +1,73 @@
using CliFx.Analyzers.Tests.Utils;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace CliFx.Analyzers.Tests
{
public class ParameterMustHaveUniqueNameAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustHaveUniqueNameAnalyzer();
[Fact]
public void Analyzer_reports_an_error_if_a_parameter_has_the_same_name_as_another_parameter()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0, Name = ""foo"")]
public string Foo { get; set; }
[CommandParameter(1, Name = ""foo"")]
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_a_parameter_has_unique_name()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0, Name = ""foo"")]
public string Foo { get; set; }
[CommandParameter(1, Name = ""bar"")]
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
}
}

View File

@@ -0,0 +1,73 @@
using CliFx.Analyzers.Tests.Utils;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace CliFx.Analyzers.Tests
{
public class ParameterMustHaveUniqueOrderAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustHaveUniqueOrderAnalyzer();
[Fact]
public void Analyzer_reports_an_error_if_a_parameter_has_the_same_order_as_another_parameter()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0)]
public string Foo { get; set; }
[CommandParameter(0)]
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_a_parameter_has_unique_order()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0)]
public string Foo { get; set; }
[CommandParameter(1)]
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
}
}

View File

@@ -0,0 +1,96 @@
using CliFx.Analyzers.Tests.Utils;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace CliFx.Analyzers.Tests
{
public class ParameterMustHaveValidConverterAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustHaveValidConverterAnalyzer();
[Fact]
public void Analyzer_reports_an_error_if_the_specified_parameter_converter_does_not_derive_from_BindingConverter()
{
// Arrange
// language=cs
const string code = @"
public class MyConverter
{
public string Convert(string rawValue) => rawValue;
}
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0, Converter = typeof(MyConverter))]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_the_specified_parameter_converter_derives_from_BindingConverter()
{
// Arrange
// language=cs
const string code = @"
public class MyConverter : BindingConverter<string>
{
public override string Convert(string rawValue) => rawValue;
}
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0, Converter = typeof(MyConverter))]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_a_parameter_does_not_have_a_converter()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0)]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
}
}

View File

@@ -0,0 +1,96 @@
using CliFx.Analyzers.Tests.Utils;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace CliFx.Analyzers.Tests
{
public class ParameterMustHaveValidValidatorsAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustHaveValidValidatorsAnalyzer();
[Fact]
public void Analyzer_reports_an_error_if_one_of_the_specified_parameter_validators_does_not_derive_from_BindingValidator()
{
// Arrange
// language=cs
const string code = @"
public class MyValidator
{
public void Validate(string value) {}
}
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0, Validators = new[] {typeof(MyValidator)})]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_all_specified_parameter_validators_derive_from_BindingValidator()
{
// Arrange
// language=cs
const string code = @"
public class MyValidator : BindingValidator<string>
{
public override BindingValidationError Validate(string value) => Ok();
}
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0, Validators = new[] {typeof(MyValidator)})]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_a_parameter_does_not_have_validators()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0)]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
}
}

View File

@@ -0,0 +1,108 @@
using CliFx.Analyzers.Tests.Utils;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace CliFx.Analyzers.Tests
{
public class SystemConsoleShouldBeAvoidedAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new SystemConsoleShouldBeAvoidedAnalyzer();
[Fact]
public void Analyzer_reports_an_error_if_a_command_calls_a_method_on_SystemConsole()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
Console.WriteLine(""Hello world"");
return default;
}
}";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
}
[Fact]
public void Analyzer_reports_an_error_if_a_command_accesses_a_property_on_SystemConsole()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
Console.ForegroundColor = ConsoleColor.Black;
return default;
}
}";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
}
[Fact]
public void Analyzer_reports_an_error_if_a_command_calls_a_method_on_a_property_of_SystemConsole()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
Console.Error.WriteLine(""Hello world"");
return default;
}
}";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_a_command_interacts_with_the_console_through_IConsole()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(""Hello world"");
return default;
}
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_IConsole_is_not_available_in_the_current_method()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
public void SomeOtherMethod() => Console.WriteLine(""Test"");
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
}
}

View File

@@ -0,0 +1,174 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Text;
using FluentAssertions.Execution;
using FluentAssertions.Primitives;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
namespace CliFx.Analyzers.Tests.Utils
{
internal class AnalyzerAssertions : ReferenceTypeAssertions<DiagnosticAnalyzer, AnalyzerAssertions>
{
protected override string Identifier { get; } = "analyzer";
public AnalyzerAssertions(DiagnosticAnalyzer analyzer)
: base(analyzer)
{
}
private Compilation Compile(string sourceCode)
{
// Get default system namespaces
var defaultSystemNamespaces = new[]
{
"System",
"System.Collections.Generic",
"System.Threading.Tasks"
};
// Get default CliFx namespaces
var defaultCliFxNamespaces = typeof(ICommand)
.Assembly
.GetTypes()
.Where(t => t.IsPublic)
.Select(t => t.Namespace)
.Distinct()
.ToArray();
// Append default imports to the source code
var sourceCodeWithUsings =
string.Join(Environment.NewLine, defaultSystemNamespaces.Select(n => $"using {n};")) +
string.Join(Environment.NewLine, defaultCliFxNamespaces.Select(n => $"using {n};")) +
Environment.NewLine +
sourceCode;
// Parse the source code
var ast = SyntaxFactory.ParseSyntaxTree(
SourceText.From(sourceCodeWithUsings),
CSharpParseOptions.Default
);
// Compile the code to IL
var compilation = CSharpCompilation.Create(
"CliFxTests_DynamicAssembly_" + Guid.NewGuid(),
new[] {ast},
new[]
{
MetadataReference.CreateFromFile(Assembly.Load("netstandard").Location),
MetadataReference.CreateFromFile(Assembly.Load("System.Runtime").Location),
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Console).Assembly.Location),
MetadataReference.CreateFromFile(typeof(ICommand).Assembly.Location)
},
// DLL to avoid having to define the Main() method
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
);
var compilationErrors = compilation
.GetDiagnostics()
.Where(d => d.Severity >= DiagnosticSeverity.Error)
.ToArray();
if (compilationErrors.Any())
{
throw new InvalidOperationException(
"Failed to compile code." +
Environment.NewLine +
string.Join(Environment.NewLine, compilationErrors.Select(e => e.ToString()))
);
}
return compilation;
}
private IReadOnlyList<Diagnostic> GetProducedDiagnostics(string sourceCode)
{
var analyzers = ImmutableArray.Create(Subject);
var compilation = Compile(sourceCode);
return compilation
.WithAnalyzers(analyzers)
.GetAnalyzerDiagnosticsAsync(analyzers, default)
.GetAwaiter()
.GetResult();
}
public void ProduceDiagnostics(string sourceCode)
{
var expectedDiagnostics = Subject.SupportedDiagnostics;
var producedDiagnostics = GetProducedDiagnostics(sourceCode);
var expectedDiagnosticIds = expectedDiagnostics.Select(d => d.Id).Distinct().ToArray();
var producedDiagnosticIds = producedDiagnostics.Select(d => d.Id).Distinct().ToArray();
var result =
expectedDiagnosticIds.Intersect(producedDiagnosticIds).Count() ==
expectedDiagnosticIds.Length;
Execute.Assertion.ForCondition(result).FailWith(() =>
{
var buffer = new StringBuilder();
buffer.AppendLine("Expected and produced diagnostics do not match.");
buffer.AppendLine();
buffer.AppendLine("Expected diagnostics:");
foreach (var expectedDiagnostic in expectedDiagnostics)
{
buffer.Append(" - ");
buffer.Append(expectedDiagnostic.Id);
buffer.AppendLine();
}
buffer.AppendLine();
buffer.AppendLine("Produced diagnostics:");
foreach (var producedDiagnostic in producedDiagnostics)
{
buffer.Append(" - ");
buffer.Append(producedDiagnostic);
}
return new FailReason(buffer.ToString());
});
}
public void NotProduceDiagnostics(string sourceCode)
{
var producedDiagnostics = GetProducedDiagnostics(sourceCode);
var result = !producedDiagnostics.Any();
Execute.Assertion.ForCondition(result).FailWith(() =>
{
var buffer = new StringBuilder();
buffer.AppendLine("Expected no produced diagnostics.");
buffer.AppendLine();
buffer.AppendLine("Produced diagnostics:");
foreach (var producedDiagnostic in producedDiagnostics)
{
buffer.Append(" - ");
buffer.Append(producedDiagnostic);
}
return new FailReason(buffer.ToString());
});
}
}
internal static class AnalyzerAssertionsExtensions
{
public static AnalyzerAssertions Should(this DiagnosticAnalyzer analyzer) => new(analyzer);
}
}

View File

@@ -0,0 +1,5 @@
{
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
"methodDisplayOptions": "all",
"methodDisplay": "method"
}