13 Commits
2.0 ... 2.0.2

Author SHA1 Message Date
Tyrrrz
d7460244b7 Update version 2021-03-31 12:11:50 +03:00
Tyrrrz
02766868fc Streamline analyzer packaging 2021-03-31 12:11:36 +03:00
Tyrrrz
8d7d25a144 Cleanup 2021-03-31 11:40:37 +03:00
Tyrrrz
17ded54e24 Update readme 2021-03-31 11:29:31 +03:00
Tyrrrz
54a4c32ddf Fix nullref in SystemConsoleShouldBeAvoidedAnalyzer 2021-03-31 11:28:48 +03:00
Tyrrrz
6d46e82145 Add test for SystemConsoleShouldBeAvoidedAnalyzer for when System.Console isn't used 2021-03-31 00:11:36 +03:00
Tyrrrz
fd4a2a18fe Improve comment on IConsole.RegisterCancellationHandler() 2021-03-30 16:22:10 +03:00
Tyrrrz
bfe99d620e Refactor IConsole.WithColors(...) 2021-03-30 16:10:11 +03:00
Tyrrrz
c5a111207f Seal attributes 2021-03-25 06:01:46 +02:00
Tyrrrz
544945c0e6 Don't reference analyzer assembly from main assembly 2021-03-24 02:42:03 +02:00
Tyrrrz
c616cdd750 Update version 2021-03-24 02:40:10 +02:00
Tyrrrz
d3c396956d Fix StackFrame.ParseMany(...) being too paranoid about its own failure 2021-03-24 02:34:36 +02:00
Tyrrrz
d0cbbc6d9a Don't highlight valid values in help text 2021-03-23 01:56:28 +02:00
27 changed files with 115 additions and 62 deletions

View File

@@ -1,3 +1,13 @@
### v2.0.2 (31-Mar-2021)
- Fixed an issue where having a transitive reference to CliFx sometimes resulted in `SystemConsoleShouldBeAvoidedAnalyzer` throwing `NullReferenceException` during build.
- Fixed some documentation typos and inconsistencies.
### v2.0.1 (24-Mar-2021)
- Fixed an issue where some exceptions with async stack traces generated on .NET 3.1 or earlier were not parsed and formatted correctly.
- Fixed an issue where help text applied slightly incorrect formatting when displaying choices for enum-based parameters and properties.
### v2.0 (21-Mar-2021) ### v2.0 (21-Mar-2021)
> Note: this major release includes many breaking changes. > Note: this major release includes many breaking changes.

View File

@@ -104,5 +104,24 @@ public class MyCommand : ICommand
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact]
public void Analyzer_does_not_report_an_error_if_a_command_does_not_access_SystemConsole()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
return default;
}
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
} }
} }

View File

@@ -22,6 +22,9 @@ namespace CliFx.Analyzers
PropertyDeclarationSyntax propertyDeclaration, PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property) IPropertySymbol property)
{ {
if (property.ContainingType is null)
return;
if (property.ContainingType.IsAbstract) if (property.ContainingType.IsAbstract)
return; return;

View File

@@ -23,6 +23,9 @@ namespace CliFx.Analyzers
PropertyDeclarationSyntax propertyDeclaration, PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property) IPropertySymbol property)
{ {
if (property.ContainingType is null)
return;
var option = CommandOptionSymbol.TryResolve(property); var option = CommandOptionSymbol.TryResolve(property);
if (option is null) if (option is null)
return; return;

View File

@@ -22,6 +22,9 @@ namespace CliFx.Analyzers
PropertyDeclarationSyntax propertyDeclaration, PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property) IPropertySymbol property)
{ {
if (property.ContainingType is null)
return;
var option = CommandOptionSymbol.TryResolve(property); var option = CommandOptionSymbol.TryResolve(property);
if (option is null) if (option is null)
return; return;

View File

@@ -22,6 +22,9 @@ namespace CliFx.Analyzers
PropertyDeclarationSyntax propertyDeclaration, PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property) IPropertySymbol property)
{ {
if (property.ContainingType is null)
return;
if (property.ContainingType.IsAbstract) if (property.ContainingType.IsAbstract)
return; return;

View File

@@ -29,6 +29,9 @@ namespace CliFx.Analyzers
PropertyDeclarationSyntax propertyDeclaration, PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property) IPropertySymbol property)
{ {
if (property.ContainingType is null)
return;
if (IsScalar(property.Type)) if (IsScalar(property.Type))
return; return;

View File

@@ -29,6 +29,9 @@ namespace CliFx.Analyzers
PropertyDeclarationSyntax propertyDeclaration, PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property) IPropertySymbol property)
{ {
if (property.ContainingType is null)
return;
if (!CommandParameterSymbol.IsParameterProperty(property)) if (!CommandParameterSymbol.IsParameterProperty(property))
return; return;

View File

@@ -23,6 +23,9 @@ namespace CliFx.Analyzers
PropertyDeclarationSyntax propertyDeclaration, PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property) IPropertySymbol property)
{ {
if (property.ContainingType is null)
return;
var parameter = CommandParameterSymbol.TryResolve(property); var parameter = CommandParameterSymbol.TryResolve(property);
if (parameter is null) if (parameter is null)
return; return;

View File

@@ -22,6 +22,9 @@ namespace CliFx.Analyzers
PropertyDeclarationSyntax propertyDeclaration, PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property) IPropertySymbol property)
{ {
if (property.ContainingType is null)
return;
var parameter = CommandParameterSymbol.TryResolve(property); var parameter = CommandParameterSymbol.TryResolve(property);
if (parameter is null) if (parameter is null)
return; return;

View File

@@ -27,9 +27,9 @@ namespace CliFx.Analyzers
while (currentNode is MemberAccessExpressionSyntax memberAccess) while (currentNode is MemberAccessExpressionSyntax memberAccess)
{ {
var symbol = context.SemanticModel.GetSymbolInfo(memberAccess).Symbol; var member = context.SemanticModel.GetSymbolInfo(memberAccess).Symbol;
if (symbol is not null && symbol.ContainingType.DisplayNameMatches("System.Console")) if (member?.ContainingType?.DisplayNameMatches("System.Console") == true)
{ {
return memberAccess; return memberAccess;
} }
@@ -53,7 +53,8 @@ namespace CliFx.Analyzers
return; return;
// Check if IConsole is available in scope as an alternative to System.Console // Check if IConsole is available in scope as an alternative to System.Console
var isConsoleInterfaceAvailable = context.Node var isConsoleInterfaceAvailable = context
.Node
.Ancestors() .Ancestors()
.OfType<MethodDeclarationSyntax>() .OfType<MethodDeclarationSyntax>()
.SelectMany(m => m.ParameterList.Parameters) .SelectMany(m => m.ParameterList.Parameters)

View File

@@ -9,11 +9,16 @@ namespace CliFx.Analyzers.Utils.Extensions
internal static class RoslynExtensions internal static class RoslynExtensions
{ {
public static bool DisplayNameMatches(this ISymbol symbol, string name) => public static bool DisplayNameMatches(this ISymbol symbol, string name) =>
string.Equals(symbol.ToDisplayString(), name, StringComparison.Ordinal); string.Equals(
// Fully qualified name, without `global::`
symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat),
name,
StringComparison.Ordinal
);
public static void HandleClassDeclaration( public static void HandleClassDeclaration(
this AnalysisContext analysisContext, this AnalysisContext analysisContext,
Action<SyntaxNodeAnalysisContext, ClassDeclarationSyntax, ITypeSymbol> handler) Action<SyntaxNodeAnalysisContext, ClassDeclarationSyntax, ITypeSymbol> analyze)
{ {
analysisContext.RegisterSyntaxNodeAction(ctx => analysisContext.RegisterSyntaxNodeAction(ctx =>
{ {
@@ -24,13 +29,13 @@ namespace CliFx.Analyzers.Utils.Extensions
if (type is null) if (type is null)
return; return;
handler(ctx, classDeclaration, type); analyze(ctx, classDeclaration, type);
}, SyntaxKind.ClassDeclaration); }, SyntaxKind.ClassDeclaration);
} }
public static void HandlePropertyDeclaration( public static void HandlePropertyDeclaration(
this AnalysisContext analysisContext, this AnalysisContext analysisContext,
Action<SyntaxNodeAnalysisContext, PropertyDeclarationSyntax, IPropertySymbol> handler) Action<SyntaxNodeAnalysisContext, PropertyDeclarationSyntax, IPropertySymbol> analyze)
{ {
analysisContext.RegisterSyntaxNodeAction(ctx => analysisContext.RegisterSyntaxNodeAction(ctx =>
{ {
@@ -41,7 +46,7 @@ namespace CliFx.Analyzers.Utils.Extensions
if (property is null) if (property is null)
return; return;
handler(ctx, propertyDeclaration, property); analyze(ctx, propertyDeclaration, property);
}, SyntaxKind.PropertyDeclaration); }, SyntaxKind.PropertyDeclaration);
} }
} }

View File

@@ -16,7 +16,7 @@
public string ExecutableName { get; } public string ExecutableName { get; }
/// <summary> /// <summary>
/// Application version text. /// Application version.
/// </summary> /// </summary>
public string Version { get; } public string Version { get; }

View File

@@ -6,7 +6,7 @@ namespace CliFx.Attributes
/// Annotates a type that defines a command. /// Annotates a type that defines a command.
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)] [AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class CommandAttribute : Attribute public sealed class CommandAttribute : Attribute
{ {
/// <summary> /// <summary>
/// Command's name. /// Command's name.

View File

@@ -7,7 +7,7 @@ namespace CliFx.Attributes
/// Annotates a property that defines a command option. /// Annotates a property that defines a command option.
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.Property)] [AttributeUsage(AttributeTargets.Property)]
public class CommandOptionAttribute : Attribute public sealed class CommandOptionAttribute : Attribute
{ {
/// <summary> /// <summary>
/// Option name. /// Option name.

View File

@@ -7,7 +7,7 @@ namespace CliFx.Attributes
/// Annotates a property that defines a command parameter. /// Annotates a property that defines a command parameter.
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.Property)] [AttributeUsage(AttributeTargets.Property)]
public class CommandParameterAttribute : Attribute public sealed class CommandParameterAttribute : Attribute
{ {
/// <summary> /// <summary>
/// Parameter order. /// Parameter order.

View File

@@ -99,7 +99,7 @@ namespace CliFx
// Handle preview directive // Handle preview directive
if (IsPreviewModeEnabled(commandInput)) if (IsPreviewModeEnabled(commandInput))
{ {
_console.WriteCommandInput(commandInput); _console.Output.WriteCommandInput(commandInput);
return 0; return 0;
} }
@@ -125,7 +125,7 @@ namespace CliFx
// Handle help option // Handle help option
if (ShouldShowHelpText(commandSchema, commandInput)) if (ShouldShowHelpText(commandSchema, commandInput))
{ {
_console.WriteHelpText(helpContext); _console.Output.WriteHelpText(helpContext);
return 0; return 0;
} }
@@ -150,12 +150,12 @@ namespace CliFx
} }
catch (CliFxException ex) catch (CliFxException ex)
{ {
_console.WriteException(ex); _console.Error.WriteException(ex);
if (ex.ShowHelp) if (ex.ShowHelp)
{ {
_console.Output.WriteLine(); _console.Output.WriteLine();
_console.WriteHelpText(helpContext); _console.Output.WriteHelpText(helpContext);
} }
return ex.ExitCode; return ex.ExitCode;
@@ -200,7 +200,7 @@ namespace CliFx
// developer, so we don't swallow them in that case. // developer, so we don't swallow them in that case.
catch (Exception ex) when (!Debugger.IsAttached) catch (Exception ex) when (!Debugger.IsAttached)
{ {
_console.WriteException(ex); _console.Error.WriteException(ex);
return 1; return 1;
} }
} }

View File

@@ -37,7 +37,7 @@ namespace CliFx
} }
/// <summary> /// <summary>
/// Adds a command the application. /// Adds a command to the application.
/// </summary> /// </summary>
public CliApplicationBuilder AddCommand<TCommand>() where TCommand : ICommand => public CliApplicationBuilder AddCommand<TCommand>() where TCommand : ICommand =>
AddCommand(typeof(TCommand)); AddCommand(typeof(TCommand));

View File

@@ -28,20 +28,10 @@
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" /> <PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" />
</ItemGroup> </ItemGroup>
<!-- The following elements are responsible for embedding the analyzer assembly within the output NuGet package --> <!-- Pack the analyzer assembly inside the package -->
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\CliFx.Analyzers\CliFx.Analyzers.csproj" ReferenceOutputAssembly="true" IncludeAssets="CliFx.Analyzers.dll" /> <ProjectReference Include="../CliFx.Analyzers/CliFx.Analyzers.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer" />
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/CliFx.Analyzers.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup> </ItemGroup>
<PropertyGroup>
<TargetsForTfmSpecificContentInPackage>$(TargetsForTfmSpecificContentInPackage);CopyAnalyzerToPackage</TargetsForTfmSpecificContentInPackage>
</PropertyGroup>
<Target Name="CopyAnalyzerToPackage">
<ItemGroup>
<TfmSpecificPackageFile Include="$(OutDir)/CliFx.Analyzers.dll" PackagePath="analyzers/dotnet/cs" BuildAction="none" />
</ItemGroup>
</Target>
</Project> </Project>

View File

@@ -93,7 +93,7 @@ namespace CliFx.Formatting
internal static class CommandInputConsoleFormatterExtensions internal static class CommandInputConsoleFormatterExtensions
{ {
public static void WriteCommandInput(this IConsole console, CommandInput commandInput) => public static void WriteCommandInput(this ConsoleWriter consoleWriter, CommandInput commandInput) =>
new CommandInputConsoleFormatter(console.Output).WriteCommandInput(commandInput); new CommandInputConsoleFormatter(consoleWriter).WriteCommandInput(commandInput);
} }
} }

View File

@@ -129,7 +129,7 @@ namespace CliFx.Formatting
internal static class ExceptionConsoleFormatterExtensions internal static class ExceptionConsoleFormatterExtensions
{ {
public static void WriteException(this IConsole console, Exception exception) => public static void WriteException(this ConsoleWriter consoleWriter, Exception exception) =>
new ExceptionConsoleFormatter(console.Error).WriteException(exception); new ExceptionConsoleFormatter(consoleWriter).WriteException(exception);
} }
} }

View File

@@ -191,7 +191,7 @@ namespace CliFx.Formatting
} }
Write(ConsoleColor.DarkGray, '"'); Write(ConsoleColor.DarkGray, '"');
Write(ConsoleColor.White, validValue.ToString()); Write(validValue.ToString());
Write(ConsoleColor.DarkGray, '"'); Write(ConsoleColor.DarkGray, '"');
} }
@@ -270,7 +270,7 @@ namespace CliFx.Formatting
} }
Write(ConsoleColor.DarkGray, '"'); Write(ConsoleColor.DarkGray, '"');
Write(ConsoleColor.White, validValue.ToString()); Write(validValue.ToString());
Write(ConsoleColor.DarkGray, '"'); Write(ConsoleColor.DarkGray, '"');
} }
@@ -443,7 +443,7 @@ namespace CliFx.Formatting
internal static class HelpConsoleFormatterExtensions internal static class HelpConsoleFormatterExtensions
{ {
public static void WriteHelpText(this IConsole console, HelpContext context) => public static void WriteHelpText(this ConsoleWriter consoleWriter, HelpContext context) =>
new HelpConsoleFormatter(console.Output, context).WriteHelpText(); new HelpConsoleFormatter(consoleWriter, context).WriteHelpText();
} }
} }

View File

@@ -70,12 +70,16 @@ namespace CliFx.Infrastructure
/// Subsequent calls to this method have no side-effects and return the same token. /// Subsequent calls to this method have no side-effects and return the same token.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// <para>
/// Calling this method effectively makes the command cancellation-aware, which /// Calling this method effectively makes the command cancellation-aware, which
/// means that sending an interrupt signal won't immediately terminate the application, /// means that sending the interrupt signal won't immediately terminate the application,
/// but will instead trigger a token that the command can use to exit more gracefully. /// but will instead trigger a token that the command can use to exit more gracefully.
/// /// </para>
/// If the user sends a second interrupt signal after the first one, the application /// <para>
/// will terminate immediately. /// Note that the handler is only respected when the user sends the interrupt signal for the first time.
/// If the user decides to issue the signal again, the application will terminate immediately
/// regardless of whether the command is cancellation-aware.
/// </para>
/// </remarks> /// </remarks>
CancellationToken RegisterCancellationHandler(); CancellationToken RegisterCancellationHandler();
} }
@@ -116,16 +120,10 @@ namespace CliFx.Infrastructure
public static IDisposable WithColors( public static IDisposable WithColors(
this IConsole console, this IConsole console,
ConsoleColor foregroundColor, ConsoleColor foregroundColor,
ConsoleColor backgroundColor) ConsoleColor backgroundColor) =>
{ Disposable.Merge(
var foregroundColorRegistration = console.WithForegroundColor(foregroundColor); console.WithForegroundColor(foregroundColor),
var backgroundColorRegistration = console.WithBackgroundColor(backgroundColor); console.WithBackgroundColor(backgroundColor)
);
return Disposable.Create(() =>
{
foregroundColorRegistration.Dispose();
backgroundColorRegistration.Dispose();
});
}
} }
} }

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
namespace CliFx.Utils namespace CliFx.Utils
{ {
@@ -14,5 +15,14 @@ namespace CliFx.Utils
internal partial class Disposable internal partial class Disposable
{ {
public static IDisposable Create(Action dispose) => new Disposable(dispose); public static IDisposable Create(Action dispose) => new Disposable(dispose);
public static IDisposable Merge(IEnumerable<IDisposable> disposables) => Create(() =>
{
foreach (var disposable in disposables)
disposable.Dispose();
});
public static IDisposable Merge(params IDisposable[] disposables) =>
Merge((IEnumerable<IDisposable>) disposables);
} }
} }

View File

@@ -91,12 +91,7 @@ namespace CliFx.Utils
{ {
var matches = Pattern.Matches(stackTrace).Cast<Match>().ToArray(); var matches = Pattern.Matches(stackTrace).Cast<Match>().ToArray();
// Ensure success (all lines should be parsed) if (matches.Length <= 0 || matches.Any(m => !m.Success))
var isSuccess =
matches.Length ==
stackTrace.Split('\n', StringSplitOptions.RemoveEmptyEntries).Length;
if (!isSuccess)
{ {
// If parsing fails, we include the original stacktrace in the // If parsing fails, we include the original stacktrace in the
// exception so that it's shown to the user. // exception so that it's shown to the user.

View File

@@ -1,7 +1,7 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<Version>2.0</Version> <Version>2.0.2</Version>
<Company>Tyrrrz</Company> <Company>Tyrrrz</Company>
<Copyright>Copyright (C) Alexey Golub</Copyright> <Copyright>Copyright (C) Alexey Golub</Copyright>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>

View File

@@ -129,7 +129,8 @@ public class LogCommand : ICommand
[CommandParameter(0, Description = "Value whose logarithm is to be found.")] [CommandParameter(0, Description = "Value whose logarithm is to be found.")]
public double Value { get; init; } public double Value { get; init; }
// Name: --base | Short name: -b // Name: --base
// Short name: -b
[CommandOption("base", 'b', Description = "Logarithm base.")] [CommandOption("base", 'b', Description = "Logarithm base.")]
public double Base { get; init; } = 10; public double Base { get; init; } = 10;