Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@
<!-- Disable the recommended analyzer package -->
<DisableRecommendedPackage>Meziantou.Analyzer</DisableRecommendedPackage>
</PropertyGroup>

<PropertyGroup>
<CopyrightYearStart>2025</CopyrightYearStart>
</PropertyGroup>
</Project>
3 changes: 2 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
<GlobalPackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.302" />
<GlobalPackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.301" />
<GlobalPackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="18.7.23" />
<GlobalPackageReference Include="NetEvolve.Defaults" Version="2.6.95" />
<GlobalPackageReference Include="NetEvolve.Analyzer" Version="0.16.13" />
<GlobalPackageReference Include="NetEvolve.Defaults" Version="2.8.0" />
<GlobalPackageReference Include="Roslynator.Analyzers" Version="4.16.0" />
<GlobalPackageReference Include="Roslynator.Formatting.Analyzers" Version="4.16.0" />
<GlobalPackageReference Include="Roslynator.CodeAnalysis.Analyzers" Version="4.16.0" />
Expand Down
34 changes: 20 additions & 14 deletions src/NetEvolve.Arguments.Analyser/SyntaxHelpers.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace NetEvolve.Arguments.Analyser;
namespace NetEvolve.Arguments.Analyser;

using System;
using System.Globalization;
Expand Down Expand Up @@ -39,8 +39,8 @@ public static ExpressionSyntax Unwrap(ExpressionSyntax expression)
return expression;
}

/// <summary>Gets the single <c>throw</c> statement a statement consists of, whether written directly or as the sole statement of a block.</summary>
/// <param name="statement">The statement to inspect, typically the body of an <c>if</c> statement.</param>
/// <summary>Gets the single <see langword="throw"/> statement a statement consists of, whether written directly or as the sole statement of a block.</summary>
/// <param name="statement">The statement to inspect, typically the body of an <see langword="if"/> statement.</param>
/// <returns>The <see cref="ThrowStatementSyntax"/> if <paramref name="statement"/> is a throw statement, or a block containing exactly one; otherwise, <see langword="null"/>.</returns>
public static ThrowStatementSyntax? GetSingleThrowStatement(StatementSyntax statement)
{
Expand All @@ -62,7 +62,7 @@ public static ExpressionSyntax Unwrap(ExpressionSyntax expression)

/// <summary>Determines whether an object-creation expression constructs exactly the given exception type.</summary>
/// <param name="semanticModel">The semantic model used to resolve the created type.</param>
/// <param name="objectCreation">The <c>new</c> expression to inspect.</param>
/// <param name="objectCreation">The <see langword="new"/> expression to inspect.</param>
/// <param name="fullyQualifiedMetadataName">The fully-qualified metadata name of the expected exception type, e.g. <c>System.ArgumentException</c>.</param>
/// <param name="cancellationToken">The token used to cancel semantic-model lookups.</param>
/// <returns><see langword="true"/> if <paramref name="objectCreation"/> constructs exactly the named type; otherwise, <see langword="false"/>.</returns>
Expand All @@ -73,22 +73,24 @@ public static bool IsExceptionType(
CancellationToken cancellationToken
)
{
cancellationToken.ThrowIfCancellationRequested();

var typeInfo = semanticModel.GetTypeInfo(objectCreation, cancellationToken);
var exceptionType = semanticModel.Compilation.GetTypeByMetadataName(fullyQualifiedMetadataName);

return exceptionType is not null && SymbolEqualityComparer.Default.Equals(typeInfo.Type, exceptionType);
}

/// <summary>
/// Recognizes the common shape every rule in this package requires of the <c>if</c> statement's body: no <c>else</c>
/// clause, a single <c>throw</c> statement, and an object-creation expression of exactly the given exception type.
/// Recognizes the common shape every rule in this package requires of the <see langword="if"/> statement's body: no <see langword="else"/>
/// clause, a single <see langword="throw"/> statement, and an object-creation expression of exactly the given exception type.
/// </summary>
/// <param name="ifStatement">The <c>if</c> statement to inspect.</param>
/// <param name="ifStatement">The <see langword="if"/> statement to inspect.</param>
/// <param name="semanticModel">The semantic model used to resolve the thrown exception's type.</param>
/// <param name="exceptionMetadataName">The fully-qualified metadata name of the expected exception type.</param>
/// <param name="cancellationToken">The token used to cancel semantic-model lookups.</param>
/// <param name="objectCreation">When this method returns <see langword="true"/>, the matched <c>new</c> expression; otherwise, <see langword="null"/>.</param>
/// <returns><see langword="true"/> if the <c>if</c> statement matches the shape; otherwise, <see langword="false"/>.</returns>
/// <param name="objectCreation">When this method returns <see langword="true"/>, the matched <see langword="new"/> expression; otherwise, <see langword="null"/>.</param>
/// <returns><see langword="true"/> if the <see langword="if"/> statement matches the shape; otherwise, <see langword="false"/>.</returns>
public static bool TryGetThrownException(
IfStatementSyntax ifStatement,
SemanticModel semanticModel,
Expand All @@ -97,6 +99,8 @@ public static bool TryGetThrownException(
out ObjectCreationExpressionSyntax? objectCreation
)
{
cancellationToken.ThrowIfCancellationRequested();

objectCreation = null;

if (ifStatement.Else is not null)
Expand All @@ -121,10 +125,10 @@ out ObjectCreationExpressionSyntax? objectCreation
}

/// <summary>
/// Recognizes an <c>if</c> condition that is true precisely when an expression is <see langword="null"/> — covering
/// Recognizes an <see langword="if"/> condition that is true precisely when an expression is <see langword="null"/> — covering
/// <c>is null</c>/<c>is not null</c>, <c>==</c>/<c>!=</c>, <c>ReferenceEquals</c>, and any number of enclosing <c>!</c> negations.
/// </summary>
/// <param name="condition">The <c>if</c> statement's condition expression.</param>
/// <param name="condition">The <see langword="if"/> statement's condition expression.</param>
/// <param name="argument">When this method returns <see langword="true"/>, the expression being null-checked; otherwise, <see langword="null"/>.</param>
/// <returns><see langword="true"/> if <paramref name="condition"/> is a recognized "argument is null" shape; otherwise, <see langword="false"/>.</returns>
public static bool TryGetNullCheckedExpression(ExpressionSyntax condition, out ExpressionSyntax? argument)
Expand Down Expand Up @@ -264,6 +268,8 @@ public static bool TryGetCoalesceNullCheck(
out ExpressionSyntax? argument
)
{
cancellationToken.ThrowIfCancellationRequested();

argument = null;

if (!binary.IsKind(SyntaxKind.CoalesceExpression))
Expand Down Expand Up @@ -324,12 +330,12 @@ public static bool IsZeroLiteral(ExpressionSyntax expression)
}

var text = literal.Token.ValueText;
return double.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var value) && value == 0;
return double.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var value) && value == 0D;
}

/// <summary>Determines whether an expression is the <c>default</c> literal or an explicitly-typed <c>default(T)</c> expression.</summary>
/// <summary>Determines whether an expression is the <see langword="default"/> literal or an explicitly-typed <c>default(T)</c> expression.</summary>
/// <param name="expression">The expression to test.</param>
/// <returns><see langword="true"/> if <paramref name="expression"/> is <c>default</c> or <c>default(T)</c>; otherwise, <see langword="false"/>.</returns>
/// <returns><see langword="true"/> if <paramref name="expression"/> is <see langword="default"/> or <c>default(T)</c>; otherwise, <see langword="false"/>.</returns>
public static bool IsDefaultLiteral(ExpressionSyntax expression) =>
Unwrap(expression) switch
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace NetEvolve.Arguments.Analyser;
namespace NetEvolve.Arguments.Analyser;

using System;
using System.Collections.Immutable;
Expand Down Expand Up @@ -32,8 +32,8 @@ public override void Initialize(AnalysisContext context)
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.IfStatement);
}

/// <summary>Analyzes an <c>if</c> statement and reports NEA0008 when it is a white-space-check-then-throw of <see cref="ArgumentException"/>.</summary>
/// <param name="context">The syntax-node analysis context for the <c>if</c> statement being visited.</param>
/// <summary>Analyzes an <see langword="if"/> statement and reports NEA0008 when it is a white-space-check-then-throw of <see cref="ArgumentException"/>.</summary>
/// <param name="context">The syntax-node analysis context for the <see langword="if"/> statement being visited.</param>
private static void Analyze(SyntaxNodeAnalysisContext context)
{
var ifStatement = (IfStatementSyntax)context.Node;
Expand Down Expand Up @@ -106,7 +106,7 @@ out var objectCreation
/// <item><description><c>arg.Where(char.IsWhiteSpace).Any()</c>.</description></item>
/// </list>
/// </summary>
/// <param name="condition">The <c>if</c> statement's condition expression.</param>
/// <param name="condition">The <see langword="if"/> statement's condition expression.</param>
/// <param name="argument">When this method returns <see langword="true"/>, the string argument being checked; otherwise, <see langword="null"/>.</param>
/// <param name="invocationsToVerify">
/// When this method returns <see langword="true"/>, the invocation(s) that must still be confirmed (via the
Expand Down Expand Up @@ -377,7 +377,7 @@ CancellationToken cancellationToken
&& method.Name == expectedMethodName
&& containingType.ToDisplayString() == "System.Linq.Enumerable";

/// <summary>Determines whether an expression is a <c>char.IsWhiteSpace</c> member access, either via the <c>char</c> keyword or the <c>Char</c> identifier.</summary>
/// <summary>Determines whether an expression is a <c>char.IsWhiteSpace</c> member access, either via the <see cref="char"/> keyword or the <c>Char</c> identifier.</summary>
/// <param name="expression">The expression to test.</param>
/// <returns><see langword="true"/> if <paramref name="expression"/> is <c>char.IsWhiteSpace</c> or <c>Char.IsWhiteSpace</c>; otherwise, <see langword="false"/>.</returns>
private static bool IsCharIsWhiteSpaceMemberAccess(ExpressionSyntax expression)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
);
}

/// <summary>Rewrites the matched <c>if</c> statement into a single <c>ArgumentException.ThrowIfContainsWhiteSpace</c> call.</summary>
/// <summary>Rewrites the matched <see langword="if"/> statement into a single <c>ArgumentException.ThrowIfContainsWhiteSpace</c> call.</summary>
/// <param name="document">The document containing the diagnostic.</param>
/// <param name="ifStatement">The <c>if</c> statement to replace.</param>
/// <param name="ifStatement">The <see langword="if"/> statement to replace.</param>
/// <param name="cancellationToken">The token used to cancel the fix.</param>
/// <returns>The updated document, or the original document if the pattern can no longer be matched.</returns>
private static async Task<Document> ApplyFixAsync(
Expand All @@ -65,6 +65,8 @@ private static async Task<Document> ApplyFixAsync(
CancellationToken cancellationToken
)
{
cancellationToken.ThrowIfCancellationRequested();

var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

if (
Expand Down
10 changes: 7 additions & 3 deletions src/NetEvolve.Arguments.Analyser/ThrowIfCountAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ public override void Initialize(AnalysisContext context)
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.IfStatement);
}

/// <summary>Analyzes an <c>if</c> statement and reports NEA0007 when it is a collection-count-comparison-then-throw of <see cref="ArgumentException"/>.</summary>
/// <param name="context">The syntax-node analysis context for the <c>if</c> statement being visited.</param>
/// <summary>Analyzes an <see langword="if"/> statement and reports NEA0007 when it is a collection-count-comparison-then-throw of <see cref="ArgumentException"/>.</summary>
/// <param name="context">The syntax-node analysis context for the <see langword="if"/> statement being visited.</param>
private static void Analyze(SyntaxNodeAnalysisContext context)
{
var ifStatement = (IfStatementSyntax)context.Node;
Expand Down Expand Up @@ -90,7 +90,7 @@ out var objectCreation
}

/// <summary>Recognizes <c>arg.Count &gt; max</c>, <c>arg.Count &lt; min</c>, and the combined range <c>arg.Count &lt; min || arg.Count &gt; max</c> (both the <c>.Count</c> property and the <c>.Count()</c> LINQ extension method).</summary>
/// <param name="condition">The <c>if</c> statement's condition expression.</param>
/// <param name="condition">The <see langword="if"/> statement's condition expression.</param>
/// <param name="comparison">When this method returns <see langword="true"/>, the recognized comparison; otherwise, <see langword="null"/>.</param>
/// <returns><see langword="true"/> if <paramref name="condition"/> is a recognized collection-count comparison shape; otherwise, <see langword="false"/>.</returns>
internal static bool TryGetCountComparison(ExpressionSyntax condition, out ComparisonResult? comparison)
Expand Down Expand Up @@ -184,6 +184,8 @@ private static bool IsSupportedCountAccess(
CancellationToken cancellationToken
)
{
cancellationToken.ThrowIfCancellationRequested();

if (!IsSupportedReceiverType(target, semanticModel, cancellationToken))
{
return false;
Expand Down Expand Up @@ -222,6 +224,8 @@ private static bool IsSupportedReceiverType(
CancellationToken cancellationToken
)
{
cancellationToken.ThrowIfCancellationRequested();

var type = semanticModel.GetTypeInfo(target, cancellationToken).Type;

if (type is null or IErrorTypeSymbol)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ ifStatement is null
);
}

/// <summary>Rewrites the matched <c>if</c> statement into a single call to the given <see cref="ArgumentException"/> collection-count throw-helper.</summary>
/// <summary>Rewrites the matched <see langword="if"/> statement into a single call to the given <see cref="ArgumentException"/> collection-count throw-helper.</summary>
/// <param name="document">The document containing the diagnostic.</param>
/// <param name="ifStatement">The <c>if</c> statement to replace.</param>
/// <param name="ifStatement">The <see langword="if"/> statement to replace.</param>
/// <param name="cancellationToken">The token used to cancel the fix.</param>
/// <returns>The updated document, or the original document if the pattern can no longer be matched.</returns>
private static async Task<Document> ApplyFixAsync(
Expand All @@ -69,6 +69,8 @@ private static async Task<Document> ApplyFixAsync(
CancellationToken cancellationToken
)
{
cancellationToken.ThrowIfCancellationRequested();

var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

if (
Expand Down
9 changes: 5 additions & 4 deletions src/NetEvolve.Arguments.Analyser/ThrowIfDefaultAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ namespace NetEvolve.Arguments.Analyser;

using System;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
Expand Down Expand Up @@ -33,8 +32,8 @@ public override void Initialize(AnalysisContext context)
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.IfStatement);
}

/// <summary>Analyzes an <c>if</c> statement and reports NEA0004 when it is a default-value-check-then-throw of <see cref="ArgumentException"/>.</summary>
/// <param name="context">The syntax-node analysis context for the <c>if</c> statement being visited.</param>
/// <summary>Analyzes an <see langword="if"/> statement and reports NEA0004 when it is a default-value-check-then-throw of <see cref="ArgumentException"/>.</summary>
/// <param name="context">The syntax-node analysis context for the <see langword="if"/> statement being visited.</param>
private static void Analyze(SyntaxNodeAnalysisContext context)
{
var ifStatement = (IfStatementSyntax)context.Node;
Expand Down Expand Up @@ -86,6 +85,8 @@ private static bool SatisfiesThrowIfDefaultConstraint(
CancellationToken cancellationToken
)
{
cancellationToken.ThrowIfCancellationRequested();

var type = semanticModel.GetTypeInfo(argument, cancellationToken).Type;

if (type is null || !type.IsValueType)
Expand All @@ -111,7 +112,7 @@ CancellationToken cancellationToken
}

/// <summary>Recognizes <c>arg.Equals(default)</c>/<c>arg.Equals(default(T))</c> and <c>arg == default</c>/<c>default == arg</c> (and the <c>default(T)</c> variants).</summary>
/// <param name="condition">The <c>if</c> statement's condition expression.</param>
/// <param name="condition">The <see langword="if"/> statement's condition expression.</param>
/// <param name="argument">When this method returns <see langword="true"/>, the expression being checked; otherwise, <see langword="null"/>.</param>
/// <returns><see langword="true"/> if <paramref name="condition"/> is a recognized default-value-check shape; otherwise, <see langword="false"/>.</returns>
internal static bool TryGetDefaultCheckedExpression(ExpressionSyntax condition, out ExpressionSyntax? argument)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
);
}

/// <summary>Rewrites the matched <c>if</c> statement into a single call to <c>ArgumentException.ThrowIfDefault</c>.</summary>
/// <summary>Rewrites the matched <see langword="if"/> statement into a single call to <c>ArgumentException.ThrowIfDefault</c>.</summary>
/// <param name="document">The document containing the diagnostic.</param>
/// <param name="ifStatement">The <c>if</c> statement to replace.</param>
/// <param name="ifStatement">The <see langword="if"/> statement to replace.</param>
/// <param name="cancellationToken">The token used to cancel the fix.</param>
/// <returns>The updated document, or the original document if the pattern can no longer be matched.</returns>
private static async Task<Document> ApplyFixAsync(
Expand All @@ -65,6 +65,8 @@ private static async Task<Document> ApplyFixAsync(
CancellationToken cancellationToken
)
{
cancellationToken.ThrowIfCancellationRequested();

var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

if (
Expand Down
6 changes: 3 additions & 3 deletions src/NetEvolve.Arguments.Analyser/ThrowIfDisposedAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ private static void OnCompilationStart(CompilationStartAnalysisContext context)
}

/// <summary>
/// Analyzes an <c>if</c> statement and reports NEA0005 when it is a disposed-check-then-throw of
/// <see cref="ObjectDisposedException"/> inside an instance member (the fix requires <c>this</c>).
/// Analyzes an <see langword="if"/> statement and reports NEA0005 when it is a disposed-check-then-throw of
/// <see cref="ObjectDisposedException"/> inside an instance member (the fix requires <see langword="this"/>).
/// </summary>
/// <param name="context">The syntax-node analysis context for the <c>if</c> statement being visited.</param>
/// <param name="context">The syntax-node analysis context for the <see langword="if"/> statement being visited.</param>
private static void Analyze(SyntaxNodeAnalysisContext context)
{
var ifStatement = (IfStatementSyntax)context.Node;
Expand Down
Loading
Loading