diff --git a/CliFx.Analyzers.Tests/CommandSchemaAnalyzerTests.cs b/CliFx.Analyzers.Tests/CommandSchemaAnalyzerTests.cs index 2d2b0df..c612eb0 100644 --- a/CliFx.Analyzers.Tests/CommandSchemaAnalyzerTests.cs +++ b/CliFx.Analyzers.Tests/CommandSchemaAnalyzerTests.cs @@ -164,6 +164,30 @@ 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 +{ + 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; }" ) @@ -292,6 +316,30 @@ 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 +{ + 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; }" ) @@ -438,6 +486,30 @@ 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; }" ) @@ -566,6 +638,30 @@ 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; }" ) diff --git a/CliFx.Analyzers/CommandSchemaAnalyzer.cs b/CliFx.Analyzers/CommandSchemaAnalyzer.cs index cf652fb..5c0a307 100644 --- a/CliFx.Analyzers/CommandSchemaAnalyzer.cs +++ b/CliFx.Analyzers/CommandSchemaAnalyzer.cs @@ -18,12 +18,14 @@ namespace CliFx.Analyzers DiagnosticDescriptors.CliFx0023, DiagnosticDescriptors.CliFx0024, DiagnosticDescriptors.CliFx0025, + DiagnosticDescriptors.CliFx0026, DiagnosticDescriptors.CliFx0041, DiagnosticDescriptors.CliFx0042, DiagnosticDescriptors.CliFx0043, DiagnosticDescriptors.CliFx0044, DiagnosticDescriptors.CliFx0045, - DiagnosticDescriptors.CliFx0046 + DiagnosticDescriptors.CliFx0046, + DiagnosticDescriptors.CliFx0047 ); private static bool IsScalarType(ITypeSymbol typeSymbol) => @@ -57,14 +59,24 @@ namespace CliFx.Analyzers .NamedArguments .Where(a => a.Key == "Converter") .Select(a => a.Value.Value) - .FirstOrDefault() as ITypeSymbol; + .Cast() + .FirstOrDefault(); + + var validators = attribute + .NamedArguments + .Where(a => a.Key == "Validators") + .SelectMany(a => a.Value.Values) + .Select(c => c.Value) + .Cast() + .ToArray(); return new { Property = p, Order = order, Name = name, - Converter = converter + Converter = converter, + Validators = validators }; }) .ToArray(); @@ -140,6 +152,18 @@ namespace CliFx.Analyzers DiagnosticDescriptors.CliFx0025, parameter.Property.Locations.First() )); } + + // Invalid validators + var invalidValidatorsParameters = parameters + .Where(p => !p.Validators.All(v => v.AllInterfaces.Any(KnownSymbols.IsArgumentValueValidatorInterface))) + .ToArray(); + + foreach (var parameter in invalidValidatorsParameters) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.CliFx0026, parameter.Property.Locations.First() + )); + } } private static void CheckCommandOptionProperties( @@ -175,7 +199,16 @@ namespace CliFx.Analyzers .NamedArguments .Where(a => a.Key == "Converter") .Select(a => a.Value.Value) - .FirstOrDefault() as ITypeSymbol; + .Cast() + .FirstOrDefault(); + + var validators = attribute + .NamedArguments + .Where(a => a.Key == "Validators") + .SelectMany(a => a.Value.Values) + .Select(c => c.Value) + .Cast() + .ToArray(); return new { @@ -183,7 +216,8 @@ namespace CliFx.Analyzers Name = name, ShortName = shortName, EnvironmentVariableName = envVarName, - Converter = converter + Converter = converter, + Validators = validators }; }) .ToArray(); @@ -270,6 +304,18 @@ namespace CliFx.Analyzers DiagnosticDescriptors.CliFx0046, option.Property.Locations.First() )); } + + // Invalid validators + var invalidValidatorsOptions = options + .Where(p => !p.Validators.All(v => v.AllInterfaces.Any(KnownSymbols.IsArgumentValueValidatorInterface))) + .ToArray(); + + foreach (var option in invalidValidatorsOptions) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.CliFx0047, option.Property.Locations.First() + )); + } } private static void CheckCommandType(SymbolAnalysisContext context) diff --git a/CliFx.Analyzers/DiagnosticDescriptors.cs b/CliFx.Analyzers/DiagnosticDescriptors.cs index d6a20d4..7da2dff 100644 --- a/CliFx.Analyzers/DiagnosticDescriptors.cs +++ b/CliFx.Analyzers/DiagnosticDescriptors.cs @@ -53,6 +53,13 @@ namespace CliFx.Analyzers "Usage", DiagnosticSeverity.Error, true ); + public static readonly DiagnosticDescriptor CliFx0026 = + new DiagnosticDescriptor(nameof(CliFx0026), + "Parameter validator must implement 'CliFx.ArgumentValueValidator'", + "Parameter validator must implement 'CliFx.ArgumentValueValidator'", + "Usage", DiagnosticSeverity.Error, true + ); + public static readonly DiagnosticDescriptor CliFx0041 = new DiagnosticDescriptor(nameof(CliFx0041), "Option must have a name or short name specified", @@ -95,6 +102,13 @@ namespace CliFx.Analyzers "Usage", DiagnosticSeverity.Error, true ); + public static readonly DiagnosticDescriptor CliFx0047 = + new DiagnosticDescriptor(nameof(CliFx0047), + "Option validator must implement 'CliFx.ArgumentValueValidator'", + "Option validator must implement 'CliFx.ArgumentValueValidator'", + "Usage", DiagnosticSeverity.Error, true + ); + public static readonly DiagnosticDescriptor CliFx0100 = new DiagnosticDescriptor(nameof(CliFx0100), "Use the provided IConsole abstraction instead of System.Console to ensure that the command can be tested in isolation", diff --git a/CliFx.Analyzers/KnownSymbols.cs b/CliFx.Analyzers/KnownSymbols.cs index 2510d47..329a0a4 100644 --- a/CliFx.Analyzers/KnownSymbols.cs +++ b/CliFx.Analyzers/KnownSymbols.cs @@ -28,6 +28,9 @@ namespace CliFx.Analyzers public static bool IsArgumentValueConverterInterface(ISymbol symbol) => symbol.DisplayNameMatches("CliFx.IArgumentValueConverter"); + public static bool IsArgumentValueValidatorInterface(ISymbol symbol) => + symbol.DisplayNameMatches("CliFx.IArgumentValueValidator"); + public static bool IsCommandAttribute(ISymbol symbol) => symbol.DisplayNameMatches("CliFx.Attributes.CommandAttribute");