diff --git a/src/Meziantou.Analyzer/Internals/TimeSpanOperation.cs b/src/Meziantou.Analyzer/Internals/TimeSpanOperation.cs index bdda8bb0..023673c3 100644 --- a/src/Meziantou.Analyzer/Internals/TimeSpanOperation.cs +++ b/src/Meziantou.Analyzer/Internals/TimeSpanOperation.cs @@ -2,146 +2,149 @@ using Microsoft.CodeAnalysis.Operations; namespace Meziantou.Analyzer.Internals; -internal static class TimeSpanOperation + +internal sealed class TimeSpanOperation(Compilation compilation) { - internal static long? GetMilliseconds(IOperation op) + private readonly ISymbol? _timeSpanSymbol = compilation.GetBestTypeByMetadataName("System.TimeSpan"); + private readonly ISymbol? _regexSymbol = compilation.GetBestTypeByMetadataName("System.Text.RegularExpressions.Regex"); + private readonly ISymbol? _timeoutSymbol = compilation.GetBestTypeByMetadataName("System.Threading.Timeout"); + + public long? GetMilliseconds(IOperation op) { if (op.SemanticModel is null) return null; - if (!op.Type.IsEqualTo(op.SemanticModel.Compilation.GetBestTypeByMetadataName("System.TimeSpan"))) + if (!op.Type.IsEqualTo(_timeSpanSymbol)) return null; return GetMilliseconds(op, 1d); + } - static long? GetMilliseconds(IOperation op, double factor) + private long? GetMilliseconds(IOperation op, double factor) + { + const double TicksToMilliseconds = 1d / TimeSpan.TicksPerMillisecond; + const double SecondsToMilliseconds = 1000; + const double MinutesToMilliseconds = 60 * 1000; + const double HoursToMilliseconds = 60 * 60 * 1000; + const double DaysToMilliseconds = 24 * 60 * 60 * 1000; + + op = op.UnwrapImplicitConversionOperations(); + if (op.ConstantValue.HasValue) { - var compilation = op.SemanticModel!.Compilation; + if (op.ConstantValue.HasValue && op.ConstantValue.Value is long int64Value) + return (long)(int64Value * factor); - const double TicksToMilliseconds = 1d / TimeSpan.TicksPerMillisecond; - const double SecondsToMilliseconds = 1000; - const double MinutesToMilliseconds = 60 * 1000; - const double HoursToMilliseconds = 60 * 60 * 1000; - const double DaysToMilliseconds = 24 * 60 * 60 * 1000; + if (op.ConstantValue.HasValue && op.ConstantValue.Value is int int32Value) + return (long)(int32Value * factor); - op = op.UnwrapImplicitConversionOperations(); - if (op.ConstantValue.HasValue) - { - if (op.ConstantValue.HasValue && op.ConstantValue.Value is long int64Value) - return (long)(int64Value * factor); + if (op.ConstantValue.HasValue && op.ConstantValue.Value is double doubleValue) + return (long)(doubleValue * factor); + } - if (op.ConstantValue.HasValue && op.ConstantValue.Value is int int32Value) - return (long)(int32Value * factor); + if (op is IDefaultValueOperation) + return 0L; - if (op.ConstantValue.HasValue && op.ConstantValue.Value is double doubleValue) - return (long)(doubleValue * factor); + if (op is IInvocationOperation invocationOperation) + { + var method = invocationOperation.TargetMethod; + if (method.IsStatic && method.ContainingType.IsEqualTo(_timeSpanSymbol)) + { + return method.Name switch + { + "FromTicks" => GetMilliseconds(invocationOperation.Arguments[0].Value, TicksToMilliseconds), + "FromMilliseconds" => GetMilliseconds(invocationOperation.Arguments[0].Value, 1), + "FromSeconds" => GetMilliseconds(invocationOperation.Arguments[0].Value, SecondsToMilliseconds), + "FromMinutes" => GetMilliseconds(invocationOperation.Arguments[0].Value, MinutesToMilliseconds), + "FromHours" => GetMilliseconds(invocationOperation.Arguments[0].Value, HoursToMilliseconds), + "FromDays" => GetMilliseconds(invocationOperation.Arguments[0].Value, DaysToMilliseconds), + _ => null, + }; } - if (op is IDefaultValueOperation) - return 0L; + return null; + } - if (op is IInvocationOperation invocationOperation) + if (op is IFieldReferenceOperation fieldReferenceOperation) + { + var member = fieldReferenceOperation.Member; + if (member.IsStatic && member.ContainingType.IsEqualTo(_timeSpanSymbol)) { - var method = invocationOperation.TargetMethod; - if (method.IsStatic && method.ContainingType.IsEqualTo(compilation.GetBestTypeByMetadataName("System.TimeSpan"))) + return member.Name switch { - return method.Name switch - { - "FromTicks" => GetMilliseconds(invocationOperation.Arguments[0].Value, TicksToMilliseconds), - "FromMilliseconds" => GetMilliseconds(invocationOperation.Arguments[0].Value, 1), - "FromSeconds" => GetMilliseconds(invocationOperation.Arguments[0].Value, SecondsToMilliseconds), - "FromMinutes" => GetMilliseconds(invocationOperation.Arguments[0].Value, MinutesToMilliseconds), - "FromHours" => GetMilliseconds(invocationOperation.Arguments[0].Value, HoursToMilliseconds), - "FromDays" => GetMilliseconds(invocationOperation.Arguments[0].Value, DaysToMilliseconds), - _ => null, - }; - } - - return null; + "Zero" => 0, + "MinValue" => (long)TimeSpan.MinValue.TotalMilliseconds, + "MaxValue" => (long)TimeSpan.MaxValue.TotalMilliseconds, + _ => null, + }; } - if (op is IFieldReferenceOperation fieldReferenceOperation) + if (member.IsStatic && member.ContainingType.IsEqualTo(_regexSymbol)) { - var member = fieldReferenceOperation.Member; - if (member.IsStatic && member.ContainingType.IsEqualTo(compilation.GetBestTypeByMetadataName("System.TimeSpan"))) - { - return member.Name switch - { - "Zero" => 0, - "MinValue" => (long)TimeSpan.MinValue.TotalMilliseconds, - "MaxValue" => (long)TimeSpan.MaxValue.TotalMilliseconds, - _ => null, - }; - } - - if (member.IsStatic && member.ContainingType.IsEqualTo(compilation.GetBestTypeByMetadataName("System.Text.RegularExpressions.Regex"))) - { - return member.Name switch - { - "InfiniteMatchTimeout" => -1L, - _ => null, - }; - } - - if (member.IsStatic && member.ContainingType.IsEqualTo(compilation.GetBestTypeByMetadataName("System.Threading.Timeout"))) + return member.Name switch { - return member.Name switch - { - "InfiniteTimeSpan" => -1L, - "Infinite" => -1L, - _ => null, - }; - } - - return null; + "InfiniteMatchTimeout" => -1L, + _ => null, + }; } - if (op is IObjectCreationOperation objectCreationOperation) + if (member.IsStatic && member.ContainingType.IsEqualTo(_timeoutSymbol)) { - if (objectCreationOperation.Type.IsEqualTo(compilation.GetBestTypeByMetadataName("System.TimeSpan"))) + return member.Name switch { - return objectCreationOperation.Arguments.Length switch - { - // new TimeSpan(long ticks) - 1 => GetMilliseconds(objectCreationOperation.Arguments[0].Value, 1d / TimeSpan.TicksPerMillisecond), - - // new TimeSpan(int hours, int minutes, int seconds) - 3 => AddValues(GetMilliseconds(objectCreationOperation.Arguments[0].Value, HoursToMilliseconds), - GetMilliseconds(objectCreationOperation.Arguments[1].Value, MinutesToMilliseconds), - GetMilliseconds(objectCreationOperation.Arguments[2].Value, SecondsToMilliseconds)), - - // new TimeSpan(int days, int hours, int minutes, int seconds) - 4 => AddValues(GetMilliseconds(objectCreationOperation.Arguments[0].Value, DaysToMilliseconds), - GetMilliseconds(objectCreationOperation.Arguments[1].Value, HoursToMilliseconds), - GetMilliseconds(objectCreationOperation.Arguments[2].Value, MinutesToMilliseconds), - GetMilliseconds(objectCreationOperation.Arguments[3].Value, SecondsToMilliseconds)), - - // new TimeSpan(int days, int hours, int minutes, int seconds, int milliseconds) - 5 => AddValues(GetMilliseconds(objectCreationOperation.Arguments[0].Value, DaysToMilliseconds), - GetMilliseconds(objectCreationOperation.Arguments[1].Value, HoursToMilliseconds), - GetMilliseconds(objectCreationOperation.Arguments[2].Value, MinutesToMilliseconds), - GetMilliseconds(objectCreationOperation.Arguments[3].Value, SecondsToMilliseconds), - GetMilliseconds(objectCreationOperation.Arguments[4].Value, 1)), - _ => null, - }; - } + "InfiniteTimeSpan" => -1L, + "Infinite" => -1L, + _ => null, + }; } return null; + } - static long? AddValues(params ReadOnlySpan values) + if (op is IObjectCreationOperation objectCreationOperation) + { + if (objectCreationOperation.Type.IsEqualTo(_timeSpanSymbol)) { - var result = 0L; - foreach (var value in values) + return objectCreationOperation.Arguments.Length switch { - if (!value.HasValue) - return null; + // new TimeSpan(long ticks) + 1 => GetMilliseconds(objectCreationOperation.Arguments[0].Value, 1d / TimeSpan.TicksPerMillisecond), + + // new TimeSpan(int hours, int minutes, int seconds) + 3 => AddValues(GetMilliseconds(objectCreationOperation.Arguments[0].Value, HoursToMilliseconds), + GetMilliseconds(objectCreationOperation.Arguments[1].Value, MinutesToMilliseconds), + GetMilliseconds(objectCreationOperation.Arguments[2].Value, SecondsToMilliseconds)), + + // new TimeSpan(int days, int hours, int minutes, int seconds) + 4 => AddValues(GetMilliseconds(objectCreationOperation.Arguments[0].Value, DaysToMilliseconds), + GetMilliseconds(objectCreationOperation.Arguments[1].Value, HoursToMilliseconds), + GetMilliseconds(objectCreationOperation.Arguments[2].Value, MinutesToMilliseconds), + GetMilliseconds(objectCreationOperation.Arguments[3].Value, SecondsToMilliseconds)), + + // new TimeSpan(int days, int hours, int minutes, int seconds, int milliseconds) + 5 => AddValues(GetMilliseconds(objectCreationOperation.Arguments[0].Value, DaysToMilliseconds), + GetMilliseconds(objectCreationOperation.Arguments[1].Value, HoursToMilliseconds), + GetMilliseconds(objectCreationOperation.Arguments[2].Value, MinutesToMilliseconds), + GetMilliseconds(objectCreationOperation.Arguments[3].Value, SecondsToMilliseconds), + GetMilliseconds(objectCreationOperation.Arguments[4].Value, 1)), + _ => null, + }; + } + } - result += value.GetValueOrDefault(); - } + return null; - return result; + static long? AddValues(params ReadOnlySpan values) + { + var result = 0L; + foreach (var value in values) + { + if (!value.HasValue) + return null; + + result += value.GetValueOrDefault(); } + + return result; } } } diff --git a/src/Meziantou.Analyzer/Internals/TypeSymbolExtensions.cs b/src/Meziantou.Analyzer/Internals/TypeSymbolExtensions.cs index 505f0cb2..f26fb8de 100755 --- a/src/Meziantou.Analyzer/Internals/TypeSymbolExtensions.cs +++ b/src/Meziantou.Analyzer/Internals/TypeSymbolExtensions.cs @@ -23,7 +23,7 @@ public static IList GetAllInterfacesIncludingThis(this ITypeSy return allInterfaces; } - public static bool InheritsFrom(this ITypeSymbol classSymbol, ITypeSymbol? baseClassType) + public static bool InheritsFrom(this ITypeSymbol classSymbol, [NotNullWhen(true)] ITypeSymbol? baseClassType) { if (baseClassType is null) return false; @@ -40,7 +40,7 @@ public static bool InheritsFrom(this ITypeSymbol classSymbol, ITypeSymbol? baseC return false; } - public static bool Implements(this ITypeSymbol classSymbol, ITypeSymbol? interfaceType) + public static bool Implements(this ITypeSymbol classSymbol, [NotNullWhen(true)] ITypeSymbol? interfaceType) { if (interfaceType is null) return false; @@ -54,7 +54,7 @@ public static bool Implements(this ITypeSymbol classSymbol, ITypeSymbol? interfa return false; } - public static bool ImplementsGenericInterface(this ITypeSymbol classSymbol, ITypeSymbol? interfaceType) + public static bool ImplementsGenericInterface(this ITypeSymbol classSymbol, [NotNullWhen(true)] ITypeSymbol? interfaceType) { if (interfaceType is null) return false; @@ -68,7 +68,7 @@ public static bool ImplementsGenericInterface(this ITypeSymbol classSymbol, ITyp return false; } - public static bool IsOrImplements(this ITypeSymbol symbol, ITypeSymbol? interfaceType) + public static bool IsOrImplements(this ITypeSymbol symbol, [NotNullWhen(true)] ITypeSymbol? interfaceType) { if (interfaceType is null) return false; @@ -142,12 +142,12 @@ public static IEnumerable GetAttributes(this ISymbol symbol, ITyp return null; } - public static bool HasAttribute(this ISymbol symbol, ITypeSymbol? attributeType, bool inherits = true) + public static bool HasAttribute(this ISymbol symbol, [NotNullWhen(true)] ITypeSymbol? attributeType, bool inherits = true) { return GetAttribute(symbol, attributeType, inherits) is not null; } - public static bool IsOrInheritFrom(this ITypeSymbol symbol, ITypeSymbol? expectedType) + public static bool IsOrInheritFrom(this ITypeSymbol symbol, [NotNullWhen(true)] ITypeSymbol? expectedType) { if (expectedType is null) return false; @@ -155,7 +155,7 @@ public static bool IsOrInheritFrom(this ITypeSymbol symbol, ITypeSymbol? expecte return symbol.IsEqualTo(expectedType) || (!expectedType.IsSealed && symbol.InheritsFrom(expectedType)); } - public static bool IsEqualToAny(this ITypeSymbol? symbol, params ReadOnlySpan expectedTypes) + public static bool IsEqualToAny([NotNullWhen(true)] this ITypeSymbol? symbol, params ReadOnlySpan expectedTypes) { if (symbol is null || expectedTypes.IsEmpty) return false; @@ -169,7 +169,7 @@ public static bool IsEqualToAny(this ITypeSymbol? symbol, params ReadOnlySpan - public static bool IsBlittableType(this ITypeSymbol? symbol) + public static bool IsBlittableType([NotNullWhen(true)] this ITypeSymbol? symbol) { if (symbol is null) return false; diff --git a/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs b/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs index 15e91c94..ea304e24 100644 --- a/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs @@ -40,248 +40,251 @@ public override void Initialize(AnalysisContext context) context.RegisterCompilationStartAction(context => { - var argumentExceptionType = context.Compilation.GetBestTypeByMetadataName("System.ArgumentException"); - var argumentNullExceptionType = context.Compilation.GetBestTypeByMetadataName("System.ArgumentNullException"); - var argumentOutOfRangeExceptionType = context.Compilation.GetBestTypeByMetadataName("System.ArgumentOutOfRangeException"); - var callerArgumentExpressionAttribute = context.Compilation.GetBestTypeByMetadataName("System.Runtime.CompilerServices.CallerArgumentExpressionAttribute"); - - if (argumentExceptionType is null || argumentNullExceptionType is null) + var analyzerContext = new AnalyzerContext(context.Compilation); + if (!analyzerContext.IsValid) return; - context.RegisterOperationAction(AnalyzeObjectCreation, OperationKind.ObjectCreation); + context.RegisterOperationAction(analyzerContext.AnalyzeObjectCreation, OperationKind.ObjectCreation); - if (callerArgumentExpressionAttribute is not null) + if (analyzerContext.CallerArgumentExpressionAttribute is not null) { - context.RegisterOperationAction(ctx => AnalyzeInvocation(ctx, argumentExceptionType, argumentNullExceptionType, argumentOutOfRangeExceptionType, callerArgumentExpressionAttribute), OperationKind.Invocation); + context.RegisterOperationAction(analyzerContext.AnalyzeInvocation, OperationKind.Invocation); } }); } - // Validate throw new ArgumentException("message", "paramName"); - private static void AnalyzeObjectCreation(OperationAnalysisContext context) + private sealed class AnalyzerContext(Compilation compilation) { - var op = (IObjectCreationOperation)context.Operation; - if (op is null) - return; + public INamedTypeSymbol ArgumentExceptionType { get; } = compilation.GetBestTypeByMetadataName("System.ArgumentException")!; + public INamedTypeSymbol ArgumentNullExceptionType { get; } = compilation.GetBestTypeByMetadataName("System.ArgumentNullException")!; + public INamedTypeSymbol? ArgumentOutOfRangeExceptionType { get; } = compilation.GetBestTypeByMetadataName("System.ArgumentOutOfRangeException"); + public INamedTypeSymbol? InvalidEnumArgumentExceptionType { get; } = compilation.GetBestTypeByMetadataName("System.ComponentModel.InvalidEnumArgumentException"); + public INamedTypeSymbol? CallerArgumentExpressionAttribute { get; } = compilation.GetBestTypeByMetadataName("System.Runtime.CompilerServices.CallerArgumentExpressionAttribute"); - var type = op.Type; - if (type is null) - return; + public bool IsValid => ArgumentExceptionType is not null && ArgumentNullExceptionType is not null; - var exceptionType = context.Compilation.GetBestTypeByMetadataName("System.ArgumentException"); - if (exceptionType is null) - return; + // Validate throw new ArgumentException("message", "paramName"); + public void AnalyzeObjectCreation(OperationAnalysisContext context) + { + var op = (IObjectCreationOperation)context.Operation; + if (op is null) + return; - if (!type.IsOrInheritFrom(exceptionType)) - return; + var type = op.Type; + if (type is null) + return; - var parameterName = "paramName"; - if (type.IsEqualTo(context.Compilation.GetBestTypeByMetadataName("System.ComponentModel.InvalidEnumArgumentException"))) - { - parameterName = "argumentName"; - } + if (!type.IsOrInheritFrom(ArgumentExceptionType)) + return; - foreach (var argument in op.Arguments) - { - if (argument.Parameter is null || !string.Equals(argument.Parameter.Name, parameterName, StringComparison.Ordinal)) - continue; + var parameterName = "paramName"; + if (type.IsEqualTo(InvalidEnumArgumentExceptionType)) + { + parameterName = "argumentName"; + } - if (argument.Value.ConstantValue.HasValue) + foreach (var argument in op.Arguments) { - if (argument.Value.ConstantValue.Value is string value) + if (argument.Parameter is null || !string.Equals(argument.Parameter.Name, parameterName, StringComparison.Ordinal)) + continue; + + if (argument.Value.ConstantValue.HasValue) { - var parameterNames = GetParameterNames(op, context.CancellationToken); - if (parameterNames.Contains(value, StringComparer.Ordinal)) + if (argument.Value.ConstantValue.Value is string value) { - if (argument.Value is not INameOfOperation) + var parameterNames = GetParameterNames(op, context.CancellationToken); + if (parameterNames.Contains(value, StringComparer.Ordinal)) { - var properties = ImmutableDictionary.Empty.Add(ArgumentExceptionShouldSpecifyArgumentNameAnalyzerCommon.ArgumentNameKey, value); - context.ReportDiagnostic(NameofRule, properties, argument.Value); + if (argument.Value is not INameOfOperation) + { + var properties = ImmutableDictionary.Empty.Add(ArgumentExceptionShouldSpecifyArgumentNameAnalyzerCommon.ArgumentNameKey, value); + context.ReportDiagnostic(NameofRule, properties, argument.Value); + } + + return; } - return; - } + var considerMemberAccessAsParameter = ConsiderMemberAccessAsParameter(context, argument.Value); + if (considerMemberAccessAsParameter) + { + var dotIndex = value.IndexOf('.', StringComparison.Ordinal); + if (dotIndex > 0 && parameterNames.Contains(value[..dotIndex], StringComparer.Ordinal)) + return; + } - var considerMemberAccessAsParameter = ConsiderMemberAccessAsParameter(context, argument.Value); - if (considerMemberAccessAsParameter) - { - var dotIndex = value.IndexOf('.', StringComparison.Ordinal); - if (dotIndex > 0 && parameterNames.Contains(value[..dotIndex], StringComparer.Ordinal)) - return; - } + if (argument.Syntax is ArgumentSyntax argumentSyntax) + { + context.ReportDiagnostic(Rule, argumentSyntax.Expression, $"'{value}' is not a valid parameter name"); + } + else + { + context.ReportDiagnostic(Rule, argument, $"'{value}' is not a valid parameter name"); + } - if (argument.Syntax is ArgumentSyntax argumentSyntax) - { - context.ReportDiagnostic(Rule, argumentSyntax.Expression, $"'{value}' is not a valid parameter name"); - } - else - { - context.ReportDiagnostic(Rule, argument, $"'{value}' is not a valid parameter name"); + return; } + } + // Cannot determine the value of the argument + return; + } + + var ctors = type.GetMembers(".ctor").OfType().Where(m => m.MethodKind is MethodKind.Constructor); + foreach (var ctor in ctors) + { + if (ctor.Parameters.Any(p => p.Name is "paramName" or "argumentName" && p.Type.IsString())) + { + context.ReportDiagnostic(Diagnostic.Create(Rule, op.Syntax.GetLocation(), $"Use an overload of '{type.ToDisplayString()}' with the parameter name")); return; } } - - // Cannot determine the value of the argument - return; } - var ctors = type.GetMembers(".ctor").OfType().Where(m => m.MethodKind == MethodKind.Constructor); - foreach (var ctor in ctors) + public void AnalyzeInvocation(OperationAnalysisContext context) { - if (ctor.Parameters.Any(p => p.Name is "paramName" or "argumentName" && p.Type.IsString())) - { - context.ReportDiagnostic(Diagnostic.Create(Rule, op.Syntax.GetLocation(), $"Use an overload of '{type.ToDisplayString()}' with the parameter name")); + var op = (IInvocationOperation)context.Operation; + if (op is null) return; - } - } - } - private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTypeSymbol argumentExceptionType, INamedTypeSymbol argumentNullExceptionType, INamedTypeSymbol? argumentOutOfRangeExceptionType, INamedTypeSymbol callerArgumentExpressionAttribute) - { - var op = (IInvocationOperation)context.Operation; - if (op is null) - return; + var method = op.TargetMethod; + if (method is null || !method.IsStatic) + return; + + // Check if the method name starts with "ThrowIf" + if (!method.Name.StartsWith("ThrowIf", StringComparison.Ordinal)) + return; + + // There must be at least one argument + if (op.Arguments.Length == 0) + return; - var method = op.TargetMethod; - if (method is null || !method.IsStatic) - return; + // Check if this is a ThrowIfXxx method on ArgumentException, ArgumentNullException, or ArgumentOutOfRangeException + var containingType = method.ContainingType; + if (containingType is null) + return; - // Check if the method name starts with "ThrowIf" - if (!method.Name.StartsWith("ThrowIf", StringComparison.Ordinal)) - return; + if (!containingType.IsEqualToAny(ArgumentExceptionType, ArgumentNullExceptionType, ArgumentOutOfRangeExceptionType)) + return; - // There must be at least one argument - if (op.Arguments.Length == 0) - return; + // Find the parameter with CallerArgumentExpressionAttribute + foreach (var parameter in method.Parameters) + { + if (!parameter.Type.IsString()) + continue; - // Check if this is a ThrowIfXxx method on ArgumentException, ArgumentNullException, or ArgumentOutOfRangeException - var containingType = method.ContainingType; - if (containingType is null) - return; + var attribute = parameter.GetAttribute(CallerArgumentExpressionAttribute); + if (attribute is null) + continue; - if (!containingType.IsEqualToAny(argumentExceptionType, argumentNullExceptionType, argumentOutOfRangeExceptionType)) - return; + if (attribute.ConstructorArguments.Length == 0) + continue; - // Find the parameter with CallerArgumentExpressionAttribute - foreach (var parameter in method.Parameters) - { - if (!parameter.Type.IsString()) - continue; + // Get the parameter name referenced by the CallerArgumentExpressionAttribute + var referencedParameterName = attribute.ConstructorArguments[0].Value as string; + if (string.IsNullOrEmpty(referencedParameterName)) + continue; - var attribute = parameter.GetAttribute(callerArgumentExpressionAttribute); - if (attribute is null) - continue; + // Find the parameter being referenced + var referencedParameter = method.Parameters.FirstOrDefault(p => p.Name == referencedParameterName); + if (referencedParameter is null) + continue; - if (attribute.ConstructorArguments.Length == 0) - continue; + // Find the argument for the paramName parameter + var paramNameArgument = op.Arguments.FirstOrDefault(arg => arg.Parameter is not null && arg.Parameter.IsEqualTo(parameter)); + if (paramNameArgument is not null && paramNameArgument.ArgumentKind is ArgumentKind.Explicit && paramNameArgument.Value is not null) + { + ValidateParamNameArgument(context, paramNameArgument); + return; + } - // Get the parameter name referenced by the CallerArgumentExpressionAttribute - var referencedParameterName = attribute.ConstructorArguments[0].Value as string; - if (string.IsNullOrEmpty(referencedParameterName)) - continue; + // Find the argument for the referenced parameter (the one being validated) + var referencedArgument = op.Arguments.FirstOrDefault(arg => arg.Parameter is not null && arg.Parameter.IsEqualTo(referencedParameter)); + if (referencedArgument is not null) + { + ValidateExpression(context, referencedArgument); + return; + } + } + } - // Find the parameter being referenced - var referencedParameter = method.Parameters.FirstOrDefault(p => p.Name == referencedParameterName); - if (referencedParameter is null) - continue; + private static bool ConsiderMemberAccessAsParameter(OperationAnalysisContext context, IOperation operation) + => context.Options.GetConfigurationValue(operation, RuleIdentifiers.ArgumentExceptionShouldSpecifyArgumentName + ".consider_member_access_as_parameter", defaultValue: false); - // Find the argument for the paramName parameter - var paramNameArgument = op.Arguments.FirstOrDefault(arg => arg.Parameter is not null && arg.Parameter.IsEqualTo(parameter)); - if (paramNameArgument is not null && paramNameArgument.ArgumentKind is ArgumentKind.Explicit && paramNameArgument.Value is not null) - { - ValidateParamNameArgument(context, paramNameArgument); + private static void ValidateParamNameArgument(OperationAnalysisContext context, IArgumentOperation paramNameArgument) + { + // Check if the argument is a constant string value + if (!paramNameArgument.Value.ConstantValue.HasValue || paramNameArgument.Value.ConstantValue.Value is not string paramNameValue) return; - } - // Find the argument for the referenced parameter (the one being validated) - var referencedArgument = op.Arguments.FirstOrDefault(arg => arg.Parameter is not null && arg.Parameter.IsEqualTo(referencedParameter)); - if (referencedArgument is not null) + var availableParameterNames = GetParameterNames(paramNameArgument, context.CancellationToken); + if (availableParameterNames.Contains(paramNameValue, StringComparer.Ordinal)) { - ValidateExpression(context, referencedArgument); + if (paramNameArgument.Value is not INameOfOperation) + { + var properties = ImmutableDictionary.Empty.Add(ArgumentExceptionShouldSpecifyArgumentNameAnalyzerCommon.ArgumentNameKey, paramNameValue); + context.ReportDiagnostic(NameofRule, properties, paramNameArgument.Value); + } + return; } - } - } - - private static bool ConsiderMemberAccessAsParameter(OperationAnalysisContext context, IOperation operation) - => context.Options.GetConfigurationValue(operation, RuleIdentifiers.ArgumentExceptionShouldSpecifyArgumentName + ".consider_member_access_as_parameter", defaultValue: false); - - private static void ValidateParamNameArgument(OperationAnalysisContext context, IArgumentOperation paramNameArgument) - { - // Check if the argument is a constant string value - if (!paramNameArgument.Value.ConstantValue.HasValue || paramNameArgument.Value.ConstantValue.Value is not string paramNameValue) - return; - var availableParameterNames = GetParameterNames(paramNameArgument, context.CancellationToken); - if (availableParameterNames.Contains(paramNameValue, StringComparer.Ordinal)) - { - if (paramNameArgument.Value is not INameOfOperation) + var considerMemberAccessAsParameter = ConsiderMemberAccessAsParameter(context, paramNameArgument.Value); + if (considerMemberAccessAsParameter) { - var properties = ImmutableDictionary.Empty.Add(ArgumentExceptionShouldSpecifyArgumentNameAnalyzerCommon.ArgumentNameKey, paramNameValue); - context.ReportDiagnostic(NameofRule, properties, paramNameArgument.Value); + var dotIndex = paramNameValue.IndexOf('.', StringComparison.Ordinal); + if (dotIndex > 0 && availableParameterNames.Contains(paramNameValue[..dotIndex], StringComparer.Ordinal)) + return; } - return; + context.ReportDiagnostic(Rule, paramNameArgument, $"'{paramNameValue}' is not a valid parameter name"); } - var considerMemberAccessAsParameter = ConsiderMemberAccessAsParameter(context, paramNameArgument.Value); - if (considerMemberAccessAsParameter) + private static void ValidateExpression(OperationAnalysisContext context, IArgumentOperation argument) { - var dotIndex = paramNameValue.IndexOf('.', StringComparison.Ordinal); - if (dotIndex > 0 && availableParameterNames.Contains(paramNameValue[..dotIndex], StringComparer.Ordinal)) + if (argument.Value is null) return; - } - context.ReportDiagnostic(Rule, paramNameArgument, $"'{paramNameValue}' is not a valid parameter name"); - } + var unwrappedValue = argument.Value.UnwrapImplicitConversionOperations(); + if (unwrappedValue is IParameterReferenceOperation) + { + // Parameter references are always valid - no need to validate the name + return; + } - private static void ValidateExpression(OperationAnalysisContext context, IArgumentOperation argument) - { - if (argument.Value is null) - return; + var considerMemberAccessAsParameter = ConsiderMemberAccessAsParameter(context, argument.Value); + if (considerMemberAccessAsParameter && IsRootParameterReference(unwrappedValue)) + return; - var unwrappedValue = argument.Value.UnwrapImplicitConversionOperations(); - if (unwrappedValue is IParameterReferenceOperation) - { - // Parameter references are always valid - no need to validate the name - return; + context.ReportDiagnostic(Rule, argument, "The expression does not match a parameter"); } - var considerMemberAccessAsParameter = ConsiderMemberAccessAsParameter(context, argument.Value); - if (considerMemberAccessAsParameter && IsRootParameterReference(unwrappedValue)) - return; - - context.ReportDiagnostic(Rule, argument, "The expression does not match a parameter"); - } - - private static bool IsRootParameterReference(IOperation operation) - { - var current = operation; - while (current is IMemberReferenceOperation memberRef) + private static bool IsRootParameterReference(IOperation operation) { - // A null instance means this is a static member access (no receiver), - // which cannot be rooted in a parameter reference. - if (memberRef.Instance is null) - return false; + var current = operation; + while (current is IMemberReferenceOperation memberRef) + { + // A null instance means this is a static member access (no receiver), + // which cannot be rooted in a parameter reference. + if (memberRef.Instance is null) + return false; - current = memberRef.Instance.UnwrapImplicitConversionOperations(); - } + current = memberRef.Instance.UnwrapImplicitConversionOperations(); + } - return current is IParameterReferenceOperation; - } + return current is IParameterReferenceOperation; + } - private static IEnumerable GetParameterNames(IOperation operation, CancellationToken cancellationToken) - { - var symbols = operation.LookupAvailableSymbols(cancellationToken); - foreach (var symbol in symbols) + private static IEnumerable GetParameterNames(IOperation operation, CancellationToken cancellationToken) { - switch (symbol) + var symbols = operation.LookupAvailableSymbols(cancellationToken); + foreach (var symbol in symbols) { - case IParameterSymbol parameterSymbol: - yield return parameterSymbol.Name; - break; + switch (symbol) + { + case IParameterSymbol parameterSymbol: + yield return parameterSymbol.Name; + break; + } } } } diff --git a/src/Meziantou.Analyzer/Rules/DoNotUseImplicitCultureSensitiveToStringAnalyzer.cs b/src/Meziantou.Analyzer/Rules/DoNotUseImplicitCultureSensitiveToStringAnalyzer.cs index 6a380780..6010717e 100644 --- a/src/Meziantou.Analyzer/Rules/DoNotUseImplicitCultureSensitiveToStringAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/DoNotUseImplicitCultureSensitiveToStringAnalyzer.cs @@ -62,6 +62,7 @@ private sealed class AnalyzerContext(Compilation compilation) { private readonly CultureSensitiveFormattingContext _cultureSensitiveContext = new(compilation); + public static void AnalyzeInvocation(OperationAnalysisContext context) { var operation = (IInvocationOperation)context.Operation; @@ -120,7 +121,7 @@ public void AnalyzeInterpolatedString(OperationAnalysisContext context) if (parent is IConversionOperation conversionOperation) { // `FormattableString _ = $""` is valid whereas `string _ = $""` may not be - if (conversionOperation.Type.IsEqualTo(context.Compilation.GetBestTypeByMetadataName("System.FormattableString"))) + if (conversionOperation.Type.IsEqualTo(_cultureSensitiveContext.FormattableStringSymbol)) return; } diff --git a/src/Meziantou.Analyzer/Rules/GeneratedRegexAttributeUsageAnalyzer.cs b/src/Meziantou.Analyzer/Rules/GeneratedRegexAttributeUsageAnalyzer.cs index 280aee7b..d5eebbd7 100644 --- a/src/Meziantou.Analyzer/Rules/GeneratedRegexAttributeUsageAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/GeneratedRegexAttributeUsageAnalyzer.cs @@ -10,7 +10,12 @@ public override void Initialize(AnalysisContext context) { context.EnableConcurrentExecution(); context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); - - context.RegisterSymbolAction(AnalyzeGeneratedRegexSymbol, SymbolKind.Method, SymbolKind.Property); + context.RegisterCompilationStartAction(context => + { + var analyzerContext = new AnalyzerContext(context.Compilation); + context.RegisterSymbolAction(analyzerContext.AnalyzeGeneratedRegexSymbol, SymbolKind.Method, SymbolKind.Property); + }); } + + } diff --git a/src/Meziantou.Analyzer/Rules/MakeMethodStaticAnalyzer.cs b/src/Meziantou.Analyzer/Rules/MakeMethodStaticAnalyzer.cs index 60480771..38df531d 100644 --- a/src/Meziantou.Analyzer/Rules/MakeMethodStaticAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/MakeMethodStaticAnalyzer.cs @@ -40,7 +40,7 @@ public override void Initialize(AnalysisContext context) context.RegisterCompilationStartAction(ctx => { - var analyzerContext = new AnalyzerContext(); + var analyzerContext = new AnalyzerContext(ctx.Compilation); ctx.RegisterSyntaxNodeAction(analyzerContext.AnalyzeMethod, SyntaxKind.MethodDeclaration); ctx.RegisterSyntaxNodeAction(analyzerContext.AnalyzeProperty, SyntaxKind.PropertyDeclaration); @@ -49,11 +49,16 @@ public override void Initialize(AnalysisContext context) }); } - private sealed class AnalyzerContext + private sealed class AnalyzerContext(Compilation compilation) { private readonly ConcurrentHashSet _potentialSymbols = new(SymbolEqualityComparer.Default); private readonly ConcurrentHashSet _cannotBeStaticSymbols = new(SymbolEqualityComparer.Default); + private readonly ITypeSymbol? _httpContextSymbol = compilation.GetBestTypeByMetadataName("Microsoft.AspNetCore.Http.HttpContext"); + private readonly ITypeSymbol? _iapplicationBuilder = compilation.GetBestTypeByMetadataName("Microsoft.AspNetCore.Builder.IApplicationBuilder"); + private readonly ITypeSymbol? _iserviceCollectionSymbol = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.DependencyInjection.IServiceCollection"); + private readonly ITypeSymbol? _imiddlewareSymbol = compilation.GetBestTypeByMetadataName("Microsoft.AspNetCore.Http.IMiddleware"); + public void CompilationEnd(CompilationAnalysisContext context) { foreach (var symbol in _potentialSymbols) @@ -86,10 +91,7 @@ public void AnalyzeMethod(SyntaxNodeAnalysisContext context) if (context.Compilation is null) return; - if (!IsPotentialStatic(methodSymbol) || - methodSymbol.IsUnitTestMethod() || - IsAspNetCoreMiddleware(context.Compilation, methodSymbol) || - IsAspNetCoreStartup(context.Compilation, methodSymbol)) + if (!IsPotentialStatic(methodSymbol) || methodSymbol.IsUnitTestMethod() || IsAspNetCoreMiddleware(methodSymbol) || IsAspNetCoreStartup(methodSymbol)) { return; } @@ -199,43 +201,37 @@ private static bool HasInstanceUsages(IOperation operation) return false; } - private static bool IsAspNetCoreMiddleware(Compilation compilation, IMethodSymbol methodSymbol) + private bool IsAspNetCoreMiddleware(IMethodSymbol methodSymbol) { if (string.Equals(methodSymbol.Name, "Invoke", StringComparison.Ordinal) || string.Equals(methodSymbol.Name, "InvokeAsync", StringComparison.Ordinal)) { - var httpContextSymbol = compilation.GetBestTypeByMetadataName("Microsoft.AspNetCore.Http.HttpContext"); - if (methodSymbol.Parameters.Length == 0 || !methodSymbol.Parameters[0].Type.IsEqualTo(httpContextSymbol)) + if (methodSymbol.Parameters.Length == 0 || !methodSymbol.Parameters[0].Type.IsEqualTo(_httpContextSymbol)) return false; return true; } - var imiddlewareSymbol = compilation.GetBestTypeByMetadataName("Microsoft.AspNetCore.Http.IMiddleware"); - if (imiddlewareSymbol is not null) + if (methodSymbol.ContainingType.Implements(_imiddlewareSymbol)) { - if (methodSymbol.ContainingType.Implements(imiddlewareSymbol)) + var invokeAsyncSymbol = _imiddlewareSymbol.GetMembers("InvokeAsync").FirstOrDefault(); + if (invokeAsyncSymbol is not null) { - var invokeAsyncSymbol = imiddlewareSymbol.GetMembers("InvokeAsync").FirstOrDefault(); - if (invokeAsyncSymbol is not null) - { - var implementationMember = methodSymbol.ContainingType.FindImplementationForInterfaceMember(invokeAsyncSymbol); - if (methodSymbol.IsEqualTo(implementationMember)) - return true; - } + var implementationMember = methodSymbol.ContainingType.FindImplementationForInterfaceMember(invokeAsyncSymbol); + if (methodSymbol.IsEqualTo(implementationMember)) + return true; } } return false; } - private static bool IsAspNetCoreStartup(Compilation compilation, IMethodSymbol methodSymbol) + private bool IsAspNetCoreStartup(IMethodSymbol methodSymbol) { // void ConfigureServices Microsoft.Extensions.DependencyInjection.IServiceCollection if (string.Equals(methodSymbol.Name, "ConfigureServices", StringComparison.Ordinal)) { - var iserviceCollectionSymbol = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.DependencyInjection.IServiceCollection"); - if (methodSymbol.ReturnsVoid && methodSymbol.Parameters.Length == 1 && methodSymbol.Parameters[0].Type.IsEqualTo(iserviceCollectionSymbol)) + if (methodSymbol.ReturnsVoid && methodSymbol.Parameters.Length == 1 && methodSymbol.Parameters[0].Type.IsEqualTo(_iserviceCollectionSymbol)) return true; return false; @@ -244,8 +240,7 @@ private static bool IsAspNetCoreStartup(Compilation compilation, IMethodSymbol m // void Configure Microsoft.AspNetCore.Builder.IApplicationBuilder if (string.Equals(methodSymbol.Name, "Configure", StringComparison.Ordinal)) { - var iapplicationBuilder = compilation.GetBestTypeByMetadataName("Microsoft.AspNetCore.Builder.IApplicationBuilder"); - if (methodSymbol.Parameters.Length > 0 && methodSymbol.Parameters[0].Type.IsEqualTo(iapplicationBuilder)) + if (methodSymbol.Parameters.Length > 0 && methodSymbol.Parameters[0].Type.IsEqualTo(_iapplicationBuilder)) return true; return false; diff --git a/src/Meziantou.Analyzer/Rules/RegexMethodUsageAnalyzer.cs b/src/Meziantou.Analyzer/Rules/RegexMethodUsageAnalyzer.cs index e93f9cc3..1b221923 100644 --- a/src/Meziantou.Analyzer/Rules/RegexMethodUsageAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/RegexMethodUsageAnalyzer.cs @@ -11,7 +11,11 @@ public override void Initialize(AnalysisContext context) context.EnableConcurrentExecution(); context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterOperationAction(AnalyzeObjectCreation, OperationKind.ObjectCreation); - context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation); + context.RegisterCompilationStartAction(context => + { + var analyzerContext = new AnalyzerContext(context.Compilation); + context.RegisterOperationAction(analyzerContext.AnalyzeObjectCreation, OperationKind.ObjectCreation); + context.RegisterOperationAction(analyzerContext.AnalyzeInvocation, OperationKind.Invocation); + }); } } diff --git a/src/Meziantou.Analyzer/Rules/RegexUsageAnalyzerBase.cs b/src/Meziantou.Analyzer/Rules/RegexUsageAnalyzerBase.cs index 3fdc9bab..8ea4fa6c 100644 --- a/src/Meziantou.Analyzer/Rules/RegexUsageAnalyzerBase.cs +++ b/src/Meziantou.Analyzer/Rules/RegexUsageAnalyzerBase.cs @@ -33,23 +33,25 @@ public abstract class RegexUsageAnalyzerBase : DiagnosticAnalyzer public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(TimeoutRule, ExplicitCaptureRule); - protected static void AnalyzeGeneratedRegexSymbol(SymbolAnalysisContext context) + private protected sealed class AnalyzerContext(Compilation compilation) { - if (context.Symbol is IMethodSymbol method && method.MethodKind is not MethodKind.Ordinary) - return; + private readonly ITypeSymbol? _regexSymbol = compilation.GetBestTypeByMetadataName("System.Text.RegularExpressions.Regex"); + private readonly ITypeSymbol? _regexOptionsSymbol = compilation.GetBestTypeByMetadataName("System.Text.RegularExpressions.RegexOptions"); + private readonly ITypeSymbol? _generatedRegexAttributeSymbol = compilation.GetBestTypeByMetadataName("System.Text.RegularExpressions.GeneratedRegexAttribute"); + private readonly ITypeSymbol? _timeSpanSymbol = compilation.GetBestTypeByMetadataName("System.TimeSpan"); - if (context.Symbol is not IMethodSymbol and not IPropertySymbol) - return; + public void AnalyzeGeneratedRegexSymbol(SymbolAnalysisContext context) + { + if (context.Symbol is not (IPropertySymbol or IMethodSymbol { MethodKind: MethodKind.Ordinary })) + return; - var generatorAttributeSymbol = context.Compilation.GetBestTypeByMetadataName("System.Text.RegularExpressions.GeneratedRegexAttribute"); - if (generatorAttributeSymbol is null) - return; + if (_generatedRegexAttributeSymbol is null) + return; - foreach (var attribute in context.Symbol.GetAttributes()) - { - // https://github.com/dotnet/runtime/issues/58880 - if (attribute.AttributeClass.IsEqualTo(generatorAttributeSymbol)) + var attributes = context.Symbol.GetAttributes(_generatedRegexAttributeSymbol, inherits: false); + foreach (var attribute in attributes) { + // Only analyze the symbol in the files that declared the attribute to avoid reporting multiple times for the same symbol var attributeSyntaxReference = attribute.ApplicationSyntaxReference; if (attributeSyntaxReference is not null && !context.Symbol.DeclaringSyntaxReferences.Any(reference => reference.SyntaxTree == attributeSyntaxReference.SyntaxTree && reference.Span.Contains(attributeSyntaxReference.Span))) continue; @@ -63,153 +65,142 @@ protected static void AnalyzeGeneratedRegexSymbol(SymbolAnalysisContext context) regexOptions = (RegexOptions)(int)attribute.ConstructorArguments[1].Value!; if (pattern is not null && ShouldAddExplicitCapture(pattern, regexOptions)) { - if (attributeSyntaxReference is not null) - { - context.ReportDiagnostic(ExplicitCaptureRule, attributeSyntaxReference); - } - else - { - context.ReportDiagnostic(ExplicitCaptureRule, context.Symbol); - } + context.ReportDiagnostic(ExplicitCaptureRule, attribute); } } // Timeout if (!HasNonBacktracking(regexOptions) && attribute.ConstructorArguments.Length < 3) { - if (attributeSyntaxReference is not null) - { - context.ReportDiagnostic(TimeoutRule, attributeSyntaxReference); - } - else - { - context.ReportDiagnostic(TimeoutRule, context.Symbol); - } + context.ReportDiagnostic(TimeoutRule, attribute); } } } - } - private static bool HasNonBacktracking(RegexOptions options) => ((int)options & 1024) == 1024; + private static bool HasNonBacktracking(RegexOptions options) => ((int)options & 1024) == 1024; - protected static void AnalyzeInvocation(OperationAnalysisContext context) - { - var op = (IInvocationOperation)context.Operation; - if (op is null || op.TargetMethod is null) - return; + public void AnalyzeInvocation(OperationAnalysisContext context) + { + var op = (IInvocationOperation)context.Operation; + if (op is null || op.TargetMethod is null) + return; - if (!op.TargetMethod.IsStatic) - return; + if (!op.TargetMethod.IsStatic) + return; - if (!MethodNames.Contains(op.TargetMethod.Name, StringComparer.Ordinal)) - return; + if (!MethodNames.Contains(op.TargetMethod.Name, StringComparer.Ordinal)) + return; - if (!op.TargetMethod.ContainingType.IsEqualTo(context.Compilation.GetBestTypeByMetadataName("System.Text.RegularExpressions.Regex"))) - return; + if (!op.TargetMethod.ContainingType.IsEqualTo(_regexSymbol)) + return; - if (op.Arguments.Length == 0) - return; + if (op.Arguments.Length == 0) + return; - var regexOptions = CheckRegexOptionsArgument(context, op.TargetMethod.IsStatic ? 1 : 0, op.Arguments, context.Compilation.GetBestTypeByMetadataName("System.Text.RegularExpressions.RegexOptions")); - if (!HasNonBacktracking(regexOptions) && !CheckTimeout(context, op.Arguments)) - { - context.ReportDiagnostic(TimeoutRule, op); + var regexOptions = CheckRegexOptionsArgument(context, op.TargetMethod.IsStatic ? 1 : 0, op.Arguments, _regexOptionsSymbol); + if (!HasNonBacktracking(regexOptions) && !CheckTimeout(op.Arguments)) + { + context.ReportDiagnostic(TimeoutRule, op); + } } - } - - protected static void AnalyzeObjectCreation(OperationAnalysisContext context) - { - var op = (IObjectCreationOperation)context.Operation; - if (op is null) - return; - - if (op.Arguments.Length == 0) - return; - if (!op.Type.IsEqualTo(context.Compilation.GetBestTypeByMetadataName("System.Text.RegularExpressions.Regex"))) - return; - - var regexOptions = CheckRegexOptionsArgument(context, 0, op.Arguments, context.Compilation.GetBestTypeByMetadataName("System.Text.RegularExpressions.RegexOptions")); - if (!HasNonBacktracking(regexOptions) && !CheckTimeout(context, op.Arguments)) + public void AnalyzeObjectCreation(OperationAnalysisContext context) { - context.ReportDiagnostic(TimeoutRule, op); - } - } + var op = (IObjectCreationOperation)context.Operation; + if (op is null) + return; - private static bool CheckTimeout(OperationAnalysisContext context, ImmutableArray args) - { - return args.Last().Value.Type.IsEqualTo(context.Compilation.GetBestTypeByMetadataName("System.TimeSpan")); - } + if (op.Arguments.Length == 0) + return; - private static RegexOptions CheckRegexOptionsArgument(OperationAnalysisContext context, int patternArgumentIndex, ImmutableArray arguments, ITypeSymbol? regexOptionsSymbol) - { - var pattern = GetPattern(); - var (regexOptions, regexOptionsArgument) = GetRegexOptions(); - if (pattern is not null && regexOptions is not null && regexOptionsArgument is not null) - { - if (ShouldAddExplicitCapture(pattern, regexOptions.Value)) + if (!op.Type.IsEqualTo(_regexSymbol)) + return; + + var regexOptions = CheckRegexOptionsArgument(context, 0, op.Arguments, _regexOptionsSymbol); + if (!HasNonBacktracking(regexOptions) && !CheckTimeout(op.Arguments)) { - context.ReportDiagnostic(ExplicitCaptureRule, regexOptionsArgument); + context.ReportDiagnostic(TimeoutRule, op); } } - return regexOptions ?? RegexOptions.None; + private bool CheckTimeout(ImmutableArray args) + { + if (_timeSpanSymbol is null) + return false; + + return args.Last().Value.Type.IsEqualTo(_timeSpanSymbol); + } - string? GetPattern() + private static RegexOptions CheckRegexOptionsArgument(OperationAnalysisContext context, int patternArgumentIndex, ImmutableArray arguments, ITypeSymbol? regexOptionsSymbol) { - if (patternArgumentIndex < arguments.Length) + var pattern = GetPattern(); + var (regexOptions, regexOptionsArgument) = GetRegexOptions(); + if (pattern is not null && regexOptions is not null && regexOptionsArgument is not null) { - var argument = arguments[patternArgumentIndex]; - if (argument.Value is not null && argument.Value.ConstantValue.HasValue && argument.Value.ConstantValue.Value is string pattern) - return pattern; + if (ShouldAddExplicitCapture(pattern, regexOptions.Value)) + { + context.ReportDiagnostic(ExplicitCaptureRule, regexOptionsArgument); + } } - return null; - } + return regexOptions ?? RegexOptions.None; - (RegexOptions?, IArgumentOperation?) GetRegexOptions() - { - if (regexOptionsSymbol is null) - return (null, null); + string? GetPattern() + { + if (patternArgumentIndex < arguments.Length) + { + var argument = arguments[patternArgumentIndex]; + if (argument.Value is not null && argument.Value.ConstantValue.HasValue && argument.Value.ConstantValue.Value is string pattern) + return pattern; + } - var arg = arguments.FirstOrDefault(a => a.Parameter is not null && a.Parameter.Type.IsEqualTo(regexOptionsSymbol)); - if (arg is null || arg.Value is null || !arg.Value.ConstantValue.HasValue) - return (null, arg); + return null; + } - return ((RegexOptions)arg.Value.ConstantValue.Value!, arg); - } - } + (RegexOptions?, IArgumentOperation?) GetRegexOptions() + { + if (regexOptionsSymbol is null) + return (null, null); - private static bool ShouldAddExplicitCapture(string pattern, RegexOptions regexOptions) - { - if (!regexOptions.HasFlag(RegexOptions.ExplicitCapture) && !regexOptions.HasFlag(RegexOptions.ECMAScript)) // The 2 options are exclusives - { - // early exit - if (!pattern.Contains('(', StringComparison.Ordinal)) - return false; + var arg = arguments.FirstOrDefault(a => a.Parameter is not null && a.Parameter.Type.IsEqualTo(regexOptionsSymbol)); + if (arg is null || arg.Value is null || !arg.Value.ConstantValue.HasValue) + return (null, arg); - return HasUnnamedGroups(pattern, regexOptions); + return ((RegexOptions)arg.Value.ConstantValue.Value!, arg); + } } - return false; - - static bool HasUnnamedGroups(string pattern, RegexOptions options) + private static bool ShouldAddExplicitCapture(string pattern, RegexOptions regexOptions) { - try + if (!regexOptions.HasFlag(RegexOptions.ExplicitCapture) && !regexOptions.HasFlag(RegexOptions.ECMAScript)) // The 2 options are exclusives { - options &= ~RegexOptions.Compiled; // Compiled options doesn't change anything but is much more resource consuming - var regex1 = new Regex(pattern, options, Regex.InfiniteMatchTimeout); - var regex2 = new Regex(pattern, options | RegexOptions.ExplicitCapture, Regex.InfiniteMatchTimeout); - - // All groups are named => No need for explicit capture - if (regex1.GetGroupNames().Length == regex2.GetGroupNames().Length) + // early exit + if (!pattern.Contains('(', StringComparison.Ordinal)) return false; + + return HasUnnamedGroups(pattern, regexOptions); } - catch + + return false; + + static bool HasUnnamedGroups(string pattern, RegexOptions options) { - } + try + { + options &= ~RegexOptions.Compiled; // Compiled options doesn't change anything but is much more resource consuming + var regex1 = new Regex(pattern, options, Regex.InfiniteMatchTimeout); + var regex2 = new Regex(pattern, options | RegexOptions.ExplicitCapture, Regex.InfiniteMatchTimeout); + + // All groups are named => No need for explicit capture + if (regex1.GetGroupNames().Length == regex2.GetGroupNames().Length) + return false; + } + catch + { + } - return true; + return true; + } } } -} +} \ No newline at end of file diff --git a/src/Meziantou.Analyzer/Rules/SimplifyCallerArgumentExpressionAnalyzer.cs b/src/Meziantou.Analyzer/Rules/SimplifyCallerArgumentExpressionAnalyzer.cs index 08d8c613..a228a189 100644 --- a/src/Meziantou.Analyzer/Rules/SimplifyCallerArgumentExpressionAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/SimplifyCallerArgumentExpressionAnalyzer.cs @@ -25,66 +25,75 @@ public override void Initialize(AnalysisContext context) { context.EnableConcurrentExecution(); context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.RegisterCompilationStartAction(context => + { + var analyzerContext = new AnalyzerContext(context.Compilation); + if (!analyzerContext.IsValid) + return; - context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation); + context.RegisterOperationAction(analyzerContext.AnalyzeInvocation, OperationKind.Invocation); + }); } - private static void AnalyzeInvocation(OperationAnalysisContext context) + private sealed class AnalyzerContext(Compilation compilation) { - var operation = (IInvocationOperation)context.Operation; - if (!operation.GetCSharpLanguageVersion().IsCSharp10OrAbove()) - return; + private readonly ITypeSymbol _callerArgumentExpressionAttribute = compilation.GetBestTypeByMetadataName("System.Runtime.CompilerServices.CallerArgumentExpressionAttribute")!; + + public bool IsValid => _callerArgumentExpressionAttribute is not null; - foreach (var argument in operation.Arguments) + public void AnalyzeInvocation(OperationAnalysisContext context) { - AnalyzeArgument(context, operation, argument); - } - } + var operation = (IInvocationOperation)context.Operation; + if (!operation.GetCSharpLanguageVersion().IsCSharp10OrAbove()) + return; - private static void AnalyzeArgument(OperationAnalysisContext context, IInvocationOperation invocation, IArgumentOperation argument) - { - if (!argument.Value.ConstantValue.HasValue) - return; + foreach (var argument in operation.Arguments) + { + AnalyzeArgument(context, operation, argument); + } + } - if (argument.ArgumentKind != ArgumentKind.Explicit) - return; + private void AnalyzeArgument(OperationAnalysisContext context, IInvocationOperation invocation, IArgumentOperation argument) + { + if (!argument.Value.ConstantValue.HasValue) + return; - if (argument.Parameter is null) - return; + if (argument.ArgumentKind is not ArgumentKind.Explicit) + return; - if (!argument.Parameter.Type.IsString()) - return; + if (argument.Parameter is null) + return; - if (!argument.Parameter.HasExplicitDefaultValue) - return; + if (!argument.Parameter.Type.IsString()) + return; - if (argument.Parameter.ExplicitDefaultValue is not null) - return; + if (!argument.Parameter.HasExplicitDefaultValue) + return; - var attributeSymbol = context.Compilation.GetBestTypeByMetadataName("System.Runtime.CompilerServices.CallerArgumentExpressionAttribute"); - if (attributeSymbol is null) - return; + if (argument.Parameter.ExplicitDefaultValue is not null) + return; - foreach (var attribute in argument.Parameter.GetAttributes()) - { - if (attribute.ConstructorArguments.Length == 0) - continue; + foreach (var attribute in argument.Parameter.GetAttributes()) + { + if (attribute.ConstructorArguments.Length == 0) + continue; - if (!attribute.AttributeClass.IsEqualTo(attributeSymbol)) - continue; + if (!attribute.AttributeClass.IsEqualTo(_callerArgumentExpressionAttribute)) + continue; - var parameterName = attribute.ConstructorArguments[0].Value as string; - if (string.IsNullOrEmpty(parameterName)) - continue; + var parameterName = attribute.ConstructorArguments[0].Value as string; + if (string.IsNullOrEmpty(parameterName)) + continue; - var targetArgument = invocation.Arguments.FirstOrDefault(arg => arg.Parameter?.Name == parameterName); - if (targetArgument is null) - continue; + var targetArgument = invocation.Arguments.FirstOrDefault(arg => arg.Parameter?.Name == parameterName); + if (targetArgument is null) + continue; - var defaultValue = targetArgument.Value.Syntax.ToString(); - if (defaultValue == (string?)argument.Value.ConstantValue.Value) - { - context.ReportDiagnostic(Rule, argument); + var defaultValue = targetArgument.Value.Syntax.ToString(); + if (defaultValue == (string?)argument.Value.ConstantValue.Value) + { + context.ReportDiagnostic(Rule, argument); + } } } } diff --git a/src/Meziantou.Analyzer/Rules/UseDateTimeUnixEpochAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseDateTimeUnixEpochAnalyzer.cs index 59ddad12..219ac771 100644 --- a/src/Meziantou.Analyzer/Rules/UseDateTimeUnixEpochAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/UseDateTimeUnixEpochAnalyzer.cs @@ -38,149 +38,159 @@ public override void Initialize(AnalysisContext context) context.RegisterCompilationStartAction(ctx => { - var dateTimeSymbol = ctx.Compilation.GetBestTypeByMetadataName("System.DateTime"); - var dateTimeOffsetSymbol = ctx.Compilation.GetBestTypeByMetadataName("System.DateTimeOffset"); + var analyzerContext = new AnalyzerContext(ctx.Compilation); - if (dateTimeSymbol is not null && dateTimeSymbol.GetMembers("UnixEpoch").Length > 0) + if (analyzerContext.HasDateTimeUnixEpoch) { - ctx.RegisterOperationAction(ctx => AnalyzeDateTimeObjectCreation(ctx, dateTimeSymbol), OperationKind.ObjectCreation); + ctx.RegisterOperationAction(ctx => analyzerContext.AnalyzeDateTimeObjectCreation(ctx), OperationKind.ObjectCreation); } - if (dateTimeSymbol is not null && dateTimeOffsetSymbol is not null && dateTimeOffsetSymbol.GetMembers("UnixEpoch").Length > 0) + if (analyzerContext.HasDateTimeOffsetUnixEpoch) { - ctx.RegisterOperationAction(ctx => AnalyzeDateTimeOffsetObjectCreation(ctx, dateTimeSymbol, dateTimeOffsetSymbol), OperationKind.ObjectCreation); + ctx.RegisterOperationAction(ctx => analyzerContext.AnalyzeDateTimeOffsetObjectCreation(ctx), OperationKind.ObjectCreation); } }); } - private static void AnalyzeDateTimeObjectCreation(OperationAnalysisContext context, ITypeSymbol dateTimeSymbol) + private sealed class AnalyzerContext(Compilation compilation) { - var operation = (IObjectCreationOperation)context.Operation; - if (IsDateTimeUnixEpoch(operation, context.Compilation, dateTimeSymbol)) + private readonly TimeSpanOperation _timeSpanOperation = new(compilation); + private readonly ITypeSymbol? _dateTimeSymbol = compilation.GetBestTypeByMetadataName("System.DateTime"); + private readonly ITypeSymbol? _dateTimeOffsetSymbol = compilation.GetBestTypeByMetadataName("System.DateTimeOffset"); + private readonly ITypeSymbol? _dateTimeKindSymbol = compilation.GetBestTypeByMetadataName("System.DateTimeKind"); + + public bool HasDateTimeUnixEpoch => _dateTimeSymbol is not null && _dateTimeSymbol.GetMembers("UnixEpoch").Length > 0; + public bool HasDateTimeOffsetUnixEpoch => _dateTimeOffsetSymbol is not null && _dateTimeOffsetSymbol.GetMembers("UnixEpoch").Length > 0; + + public void AnalyzeDateTimeObjectCreation(OperationAnalysisContext context) { - context.ReportDiagnostic(DateTimeRule, operation); + var operation = (IObjectCreationOperation)context.Operation; + if (IsDateTimeUnixEpoch(operation)) + { + context.ReportDiagnostic(DateTimeRule, operation); + } } - } - private static void AnalyzeDateTimeOffsetObjectCreation(OperationAnalysisContext context, ITypeSymbol dateTimeSymbol, ITypeSymbol dateTimeOffsetSymbol) - { - var operation = (IObjectCreationOperation)context.Operation; - if (IsDateTimeOffsetUnixEpoch()) + + public void AnalyzeDateTimeOffsetObjectCreation(OperationAnalysisContext context) { - context.ReportDiagnostic(DateTimeOffsetRule, operation); + var operation = (IObjectCreationOperation)context.Operation; + if (IsDateTimeOffsetUnixEpoch()) + { + context.ReportDiagnostic(DateTimeOffsetRule, operation); + } + + bool IsDateTimeOffsetUnixEpoch() + { + if (!operation.Type.IsEqualTo(_dateTimeOffsetSymbol)) + return false; + + if (operation.Arguments.Length == 1) + { + if (ArgumentsEquals(operation.Arguments.AsSpan(), [621355968000000000L])) + return true; + + if (IsUnixEpochProperty(operation.Arguments[0])) + return true; + } + else if (operation.Arguments.Length == 2) + { + if (ArgumentsEquals(operation.Arguments.AsSpan(0, 1), [621355968000000000L]) && IsTimeSpanZero(operation.Arguments[1])) + return true; + + if (IsUnixEpochProperty(operation.Arguments[0]) && IsTimeSpanZero(operation.Arguments[1])) + return true; + } + else if (operation.Arguments.Length == 7) + { + if (ArgumentsEquals(operation.Arguments.AsSpan(0, 6), [1970, 1, 1, 0, 0, 0]) && IsTimeSpanZero(operation.Arguments[6])) + return true; + } + else if (operation.Arguments.Length == 8) + { + if (ArgumentsEquals(operation.Arguments.AsSpan(0, 7), [1970, 1, 1, 0, 0, 0, 0]) && IsTimeSpanZero(operation.Arguments[7])) + return true; + } + else if (operation.Arguments.Length == 9) + { + if (ArgumentsEquals(operation.Arguments.AsSpan(0, 8), [1970, 1, 1, 0, 0, 0, 0, 0]) && IsTimeSpanZero(operation.Arguments[8])) + return true; + } + + return false; + } + + bool IsUnixEpochProperty(IArgumentOperation argumentOperation) + { + if (argumentOperation.Value is IMemberReferenceOperation memberReference) + { + if (memberReference.Member.Name == "UnixEpoch" && memberReference.Member.ContainingType.IsEqualTo(_dateTimeSymbol)) + return true; + } + + return false; + } } - bool IsDateTimeOffsetUnixEpoch() + private bool IsDateTimeUnixEpoch(IObjectCreationOperation operation) { - if (!operation.Type.IsEqualTo(dateTimeOffsetSymbol)) + if (!operation.Type.IsEqualTo(_dateTimeSymbol)) return false; if (operation.Arguments.Length == 1) { if (ArgumentsEquals(operation.Arguments.AsSpan(), [621355968000000000L])) return true; - - if (IsUnixEpochProperty(operation.Arguments[0])) - return true; } else if (operation.Arguments.Length == 2) { - if (ArgumentsEquals(operation.Arguments.AsSpan(0, 1), [621355968000000000L]) && IsTimeSpanZero(operation.Arguments[1])) - return true; - - if (IsUnixEpochProperty(operation.Arguments[0]) && IsTimeSpanZero(operation.Arguments[1])) + if (ArgumentsEquals(operation.Arguments.AsSpan(0, 1), [621355968000000000L]) && IsDateTimeKindUtc(operation.Arguments[1])) return true; } - else if (operation.Arguments.Length == 7) + else if (operation.Arguments.Length == 3) { - if (ArgumentsEquals(operation.Arguments.AsSpan(0, 6), [1970, 1, 1, 0, 0, 0]) && IsTimeSpanZero(operation.Arguments[6])) + if (ArgumentsEquals(operation.Arguments.AsSpan(), [1970, 1, 1])) return true; } - else if (operation.Arguments.Length == 8) + else if (operation.Arguments.Length == 6) { - if (ArgumentsEquals(operation.Arguments.AsSpan(0, 7), [1970, 1, 1, 0, 0, 0, 0]) && IsTimeSpanZero(operation.Arguments[7])) + if (ArgumentsEquals(operation.Arguments.AsSpan(), [1970, 1, 1, 0, 0, 0])) return true; } - else if (operation.Arguments.Length == 9) + else if (operation.Arguments.Length == 7) { - if (ArgumentsEquals(operation.Arguments.AsSpan(0, 8), [1970, 1, 1, 0, 0, 0, 0, 0]) && IsTimeSpanZero(operation.Arguments[8])) + if (ArgumentsEquals(operation.Arguments.AsSpan(0, 6), [1970, 1, 1, 0, 0, 0]) && IsDateTimeKindUtc(operation.Arguments[6])) return true; } return false; } - bool IsUnixEpochProperty(IArgumentOperation argumentOperation) + private bool IsDateTimeKindUtc(IArgumentOperation argument) { - if (argumentOperation.Value is IMemberReferenceOperation memberReference) - { - if (memberReference.Member.Name == "UnixEpoch" && memberReference.Member.ContainingType.IsEqualTo(dateTimeSymbol)) - return true; - } + if (_dateTimeKindSymbol is null) + return false; - return false; + return argument.Value.ConstantValue.HasValue && (DateTimeKind)argument.Value.ConstantValue.Value! == DateTimeKind.Utc; } - } - - private static bool IsDateTimeUnixEpoch(IObjectCreationOperation operation, Compilation compilation, ITypeSymbol dateTimeSymbol) - { - if (!operation.Type.IsEqualTo(dateTimeSymbol)) - return false; - if (operation.Arguments.Length == 1) - { - if (ArgumentsEquals(operation.Arguments.AsSpan(), [621355968000000000L])) - return true; - } - else if (operation.Arguments.Length == 2) - { - if (ArgumentsEquals(operation.Arguments.AsSpan(0, 1), [621355968000000000L]) && IsDateTimeKindUtc(compilation, operation.Arguments[1])) - return true; - } - else if (operation.Arguments.Length == 3) + private static bool ArgumentsEquals(ReadOnlySpan arguments, object[] expectedValues) { - if (ArgumentsEquals(operation.Arguments.AsSpan(), [1970, 1, 1])) - return true; - } - else if (operation.Arguments.Length == 6) - { - if (ArgumentsEquals(operation.Arguments.AsSpan(), [1970, 1, 1, 0, 0, 0])) - return true; - } - else if (operation.Arguments.Length == 7) - { - if (ArgumentsEquals(operation.Arguments.AsSpan(0, 6), [1970, 1, 1, 0, 0, 0]) && IsDateTimeKindUtc(compilation, operation.Arguments[6])) - return true; - } - - return false; - } + for (var i = 0; i < arguments.Length; i++) + { + var argument = arguments[i]; + if (!argument.Value.ConstantValue.HasValue) + return false; - private static bool IsDateTimeKindUtc(Compilation compilation, IArgumentOperation argument) - { - var dateTimeKindSymbol = compilation.GetBestTypeByMetadataName("System.DateTimeKind"); - if (dateTimeKindSymbol is null) - return false; + if (!Equals(argument.Value.ConstantValue.Value, expectedValues[i])) + return false; + } - return argument.Value.ConstantValue.HasValue && (DateTimeKind)argument.Value.ConstantValue.Value! == DateTimeKind.Utc; - } + return true; + } - private static bool ArgumentsEquals(ReadOnlySpan arguments, object[] expectedValues) - { - for (var i = 0; i < arguments.Length; i++) + private bool IsTimeSpanZero(IArgumentOperation operation) { - var argument = arguments[i]; - if (!argument.Value.ConstantValue.HasValue) - return false; - - if (!Equals(argument.Value.ConstantValue.Value, expectedValues[i])) - return false; + return _timeSpanOperation.GetMilliseconds(operation.Value) is 0L; } - - return true; - } - - private static bool IsTimeSpanZero(IArgumentOperation operation) - { - return TimeSpanOperation.GetMilliseconds(operation.Value) is 0L; } -} +} \ No newline at end of file diff --git a/src/Meziantou.Analyzer/Rules/UseEqualsMethodInsteadOfOperatorAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseEqualsMethodInsteadOfOperatorAnalyzer.cs index 8d808d8e..0ee81918 100644 --- a/src/Meziantou.Analyzer/Rules/UseEqualsMethodInsteadOfOperatorAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/UseEqualsMethodInsteadOfOperatorAnalyzer.cs @@ -31,11 +31,15 @@ public override void Initialize(AnalysisContext context) if (context.Compilation.GetSpecialType(SpecialType.System_Object).GetMembers("Equals").FirstOrDefault() is not IMethodSymbol objectEqualsSymbol) return; - context.RegisterOperationAction(context => AnalyzerBinaryOperation(context, objectEqualsSymbol, context.Compilation.GetBestTypeByMetadataName("System.Globalization.CultureInfo")), OperationKind.Binary); + var cultureInfoSymbol = context.Compilation.GetBestTypeByMetadataName("System.Globalization.CultureInfo"); + if (cultureInfoSymbol is null) + return; + + context.RegisterOperationAction(context => AnalyzerBinaryOperation(context, objectEqualsSymbol, cultureInfoSymbol), OperationKind.Binary); }); } - private static void AnalyzerBinaryOperation(OperationAnalysisContext context, IMethodSymbol objectEqualsSymbol, ITypeSymbol? cultureInfoSymbol) + private static void AnalyzerBinaryOperation(OperationAnalysisContext context, IMethodSymbol objectEqualsSymbol, ITypeSymbol cultureInfoSymbol) { var operation = (IBinaryOperation)context.Operation; if (operation is { OperatorKind: BinaryOperatorKind.Equals or BinaryOperatorKind.NotEquals, OperatorMethod: null }) diff --git a/src/Meziantou.Analyzer/Rules/UseEventArgsEmptyAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseEventArgsEmptyAnalyzer.cs index a917e74d..a2c12497 100644 --- a/src/Meziantou.Analyzer/Rules/UseEventArgsEmptyAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/UseEventArgsEmptyAnalyzer.cs @@ -26,10 +26,17 @@ public override void Initialize(AnalysisContext context) context.EnableConcurrentExecution(); context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterOperationAction(Analyze, OperationKind.ObjectCreation); + context.RegisterCompilationStartAction(context => + { + var type = context.Compilation.GetBestTypeByMetadataName("System.EventArgs"); + if (type is null) + return; + + context.RegisterOperationAction(context => Analyze(context, type), OperationKind.ObjectCreation); + }); } - private static void Analyze(OperationAnalysisContext context) + private static void Analyze(OperationAnalysisContext context, INamedTypeSymbol type) { var operation = (IObjectCreationOperation)context.Operation; if (operation is null || operation.Constructor is null) @@ -38,7 +45,6 @@ private static void Analyze(OperationAnalysisContext context) if (operation.Arguments.Length > 0) return; - var type = context.Compilation.GetBestTypeByMetadataName("System.EventArgs"); if (operation.Constructor.ContainingType.IsEqualTo(type)) { context.ReportDiagnostic(Rule, operation); diff --git a/src/Meziantou.Analyzer/Rules/UseRegexSourceGeneratorAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseRegexSourceGeneratorAnalyzer.cs index d72fa256..faf2dc5b 100644 --- a/src/Meziantou.Analyzer/Rules/UseRegexSourceGeneratorAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/UseRegexSourceGeneratorAnalyzer.cs @@ -26,140 +26,146 @@ public override void Initialize(AnalysisContext context) context.EnableConcurrentExecution(); context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterOperationAction(AnalyzeObjectCreation, OperationKind.ObjectCreation); - context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation); - } - - private static bool CanReport(IOperation operation) - { - var compilation = operation.SemanticModel!.Compilation; - var regexSymbol = compilation.GetBestTypeByMetadataName("System.Text.RegularExpressions.Regex"); - if (regexSymbol is null) - return false; - - var regexGeneratorAttributeSymbol = compilation.GetBestTypeByMetadataName("System.Text.RegularExpressions.GeneratedRegexAttribute"); - if (regexGeneratorAttributeSymbol is null) - return false; - - // https://github.com/dotnet/runtime/pull/66111 - if (!operation.GetCSharpLanguageVersion().IsCSharp10OrAbove()) - return false; - - return true; + context.RegisterCompilationStartAction(ctx => + { + var analyzerContext = new AnalyzerContext(ctx.Compilation); + ctx.RegisterOperationAction(ctx => analyzerContext.AnalyzeObjectCreation(ctx), OperationKind.ObjectCreation); + ctx.RegisterOperationAction(ctx => analyzerContext.AnalyzeInvocation(ctx), OperationKind.Invocation); + }); } - private static void AnalyzeObjectCreation(OperationAnalysisContext context) + private sealed class AnalyzerContext(Compilation compilation) { - if (!CanReport(context.Operation)) - return; - - var op = (IObjectCreationOperation)context.Operation; - if (!op.Type.IsEqualTo(context.Compilation.GetBestTypeByMetadataName("System.Text.RegularExpressions.Regex"))) - return; + private readonly TimeSpanOperation _timeSpanOperation = new(compilation); + private readonly ITypeSymbol? _regexSymbol = compilation.GetBestTypeByMetadataName("System.Text.RegularExpressions.Regex"); + private readonly ITypeSymbol? _regexGeneratorAttributeSymbol = compilation.GetBestTypeByMetadataName("System.Text.RegularExpressions.GeneratedRegexAttribute"); + private readonly ITypeSymbol? _timespanSymbol = compilation.GetBestTypeByMetadataName("System.TimeSpan"); - foreach (var arg in op.Arguments) + private bool CanReport(IOperation operation) { - if (!IsConstant(arg)) - return; - } - - var properties = ImmutableDictionary.CreateRange( - [ - new KeyValuePair(UseRegexSourceGeneratorAnalyzerCommon.PatternIndexName, "0"), - new KeyValuePair(UseRegexSourceGeneratorAnalyzerCommon.RegexOptionsIndexName, op.Arguments.Length > 1 ? "1" : null), - new KeyValuePair(UseRegexSourceGeneratorAnalyzerCommon.RegexTimeoutIndexName, op.Arguments.Length > 2 ? "2" : null), - new KeyValuePair(UseRegexSourceGeneratorAnalyzerCommon.RegexTimeoutName, op.Arguments.Length > 2 ? TimeSpanOperation.GetMilliseconds(op.Arguments[2].Value)?.ToString(CultureInfo.InvariantCulture) : null), - ]); + if (_regexSymbol is null) + return false; - context.ReportDiagnostic(RegexSourceGeneratorRule, properties, op); - } + if (_regexGeneratorAttributeSymbol is null) + return false; - private static void AnalyzeInvocation(OperationAnalysisContext context) - { - if (!CanReport(context.Operation)) - return; + // https://github.com/dotnet/runtime/pull/66111 + if (!operation.GetCSharpLanguageVersion().IsCSharp10OrAbove()) + return false; - var op = (IInvocationOperation)context.Operation; - var method = op.TargetMethod; - if (!method.IsStatic || !method.ContainingType.IsEqualTo(context.Compilation.GetBestTypeByMetadataName("System.Text.RegularExpressions.Regex"))) - return; + return true; + } - if (method.Name is "IsMatch" or "Match" or "Matches" or "Split") + public void AnalyzeObjectCreation(OperationAnalysisContext context) { - // IsMatch(string _, string) - // IsMatch(string _, string, RegexOptions) - // IsMatch(string _, string, RegexOptions, TimeSpan) - - // Match(string _, string) - // Match(string _, string, RegexOptions) - // Match(string _, string, RegexOptions, TimeSpan) - - // Matches(string _, string) - // Matches(string _, string, RegexOptions) - // Matches(string _, string, RegexOptions, TimeSpan) + if (!CanReport(context.Operation)) + return; - // Split(string _, string) - // Split(string _, string, RegexOptions) - // Split(string _, string, RegexOptions, TimeSpan) + var op = (IObjectCreationOperation)context.Operation; + if (!op.Type.IsEqualTo(_regexSymbol)) + return; - for (var i = 1; i < op.Arguments.Length; i++) + foreach (var arg in op.Arguments) { - if (!IsConstant(op.Arguments[i])) + if (!IsConstant(arg)) return; } var properties = ImmutableDictionary.CreateRange( [ - new KeyValuePair(UseRegexSourceGeneratorAnalyzerCommon.PatternIndexName, "1"), - new KeyValuePair(UseRegexSourceGeneratorAnalyzerCommon.RegexOptionsIndexName, op.Arguments.Length > 2 ? "2" : null), - new KeyValuePair(UseRegexSourceGeneratorAnalyzerCommon.RegexTimeoutIndexName, op.Arguments.Length > 3 ? "3" : null), - new KeyValuePair(UseRegexSourceGeneratorAnalyzerCommon.RegexTimeoutName, op.Arguments.Length > 3 ? TimeSpanOperation.GetMilliseconds(op.Arguments[3].Value)?.ToString(CultureInfo.InvariantCulture) : null), + new KeyValuePair(UseRegexSourceGeneratorAnalyzerCommon.PatternIndexName, "0"), + new KeyValuePair(UseRegexSourceGeneratorAnalyzerCommon.RegexOptionsIndexName, op.Arguments.Length > 1 ? "1" : null), + new KeyValuePair(UseRegexSourceGeneratorAnalyzerCommon.RegexTimeoutIndexName, op.Arguments.Length > 2 ? "2" : null), + new KeyValuePair(UseRegexSourceGeneratorAnalyzerCommon.RegexTimeoutName, op.Arguments.Length > 2 ? _timeSpanOperation.GetMilliseconds(op.Arguments[2].Value)?.ToString(CultureInfo.InvariantCulture) : null), ]); context.ReportDiagnostic(RegexSourceGeneratorRule, properties, op); } - else if (method.Name is "Replace") - { - // Replace(string _, string, MatchEvaluator _, RegexOptions, TimeSpan) - // Replace(string _, string, MatchEvaluator _, RegexOptions) - // Replace(string _, string, MatchEvaluator _) - // Replace(string _, string, string _, RegexOptions, TimeSpan) - // Replace(string _, string, string _, RegexOptions) - // Replace(string _, string, string _) - - for (var i = 1; i < op.Arguments.Length; i++) - { - if (i == 2) - continue; - if (!IsConstant(op.Arguments[i])) - return; - } + public void AnalyzeInvocation(OperationAnalysisContext context) + { + if (!CanReport(context.Operation)) + return; - var properties = ImmutableDictionary.CreateRange( - [ - new KeyValuePair(UseRegexSourceGeneratorAnalyzerCommon.PatternIndexName, "1"), - new KeyValuePair(UseRegexSourceGeneratorAnalyzerCommon.RegexOptionsIndexName, op.Arguments.Length > 3 ? "3" : null), - new KeyValuePair(UseRegexSourceGeneratorAnalyzerCommon.RegexTimeoutIndexName, op.Arguments.Length > 4 ? "4" : null), - new KeyValuePair(UseRegexSourceGeneratorAnalyzerCommon.RegexTimeoutName, op.Arguments.Length > 4 ? TimeSpanOperation.GetMilliseconds(op.Arguments[4].Value)?.ToString(CultureInfo.InvariantCulture) : null), - ]); + var op = (IInvocationOperation)context.Operation; + var method = op.TargetMethod; + if (!method.IsStatic || !method.ContainingType.IsEqualTo(_regexSymbol)) + return; - context.ReportDiagnostic(RegexSourceGeneratorRule, properties, op); + if (method.Name is "IsMatch" or "Match" or "Matches" or "Split") + { + // IsMatch(string _, string) + // IsMatch(string _, string, RegexOptions) + // IsMatch(string _, string, RegexOptions, TimeSpan) + + // Match(string _, string) + // Match(string _, string, RegexOptions) + // Match(string _, string, RegexOptions, TimeSpan) + + // Matches(string _, string) + // Matches(string _, string, RegexOptions) + // Matches(string _, string, RegexOptions, TimeSpan) + + // Split(string _, string) + // Split(string _, string, RegexOptions) + // Split(string _, string, RegexOptions, TimeSpan) + + for (var i = 1; i < op.Arguments.Length; i++) + { + if (!IsConstant(op.Arguments[i])) + return; + } + + var properties = ImmutableDictionary.CreateRange( + [ + new KeyValuePair(UseRegexSourceGeneratorAnalyzerCommon.PatternIndexName, "1"), + new KeyValuePair(UseRegexSourceGeneratorAnalyzerCommon.RegexOptionsIndexName, op.Arguments.Length > 2 ? "2" : null), + new KeyValuePair(UseRegexSourceGeneratorAnalyzerCommon.RegexTimeoutIndexName, op.Arguments.Length > 3 ? "3" : null), + new KeyValuePair(UseRegexSourceGeneratorAnalyzerCommon.RegexTimeoutName, op.Arguments.Length > 3 ? _timeSpanOperation.GetMilliseconds(op.Arguments[3].Value)?.ToString(CultureInfo.InvariantCulture) : null), + ]); + + context.ReportDiagnostic(RegexSourceGeneratorRule, properties, op); + } + else if (method.Name is "Replace") + { + // Replace(string _, string, MatchEvaluator _, RegexOptions, TimeSpan) + // Replace(string _, string, MatchEvaluator _, RegexOptions) + // Replace(string _, string, MatchEvaluator _) + // Replace(string _, string, string _, RegexOptions, TimeSpan) + // Replace(string _, string, string _, RegexOptions) + // Replace(string _, string, string _) + + for (var i = 1; i < op.Arguments.Length; i++) + { + if (i == 2) + continue; + + if (!IsConstant(op.Arguments[i])) + return; + } + + var properties = ImmutableDictionary.CreateRange( + [ + new KeyValuePair(UseRegexSourceGeneratorAnalyzerCommon.PatternIndexName, "1"), + new KeyValuePair(UseRegexSourceGeneratorAnalyzerCommon.RegexOptionsIndexName, op.Arguments.Length > 3 ? "3" : null), + new KeyValuePair(UseRegexSourceGeneratorAnalyzerCommon.RegexTimeoutIndexName, op.Arguments.Length > 4 ? "4" : null), + new KeyValuePair(UseRegexSourceGeneratorAnalyzerCommon.RegexTimeoutName, op.Arguments.Length > 4 ? _timeSpanOperation.GetMilliseconds(op.Arguments[4].Value)?.ToString(CultureInfo.InvariantCulture) : null), + ]); + + context.ReportDiagnostic(RegexSourceGeneratorRule, properties, op); + } } - } - private static bool IsConstant(IArgumentOperation argumentOperation) - { - var valueOperation = argumentOperation.Value; - if (valueOperation.ConstantValue.HasValue) - return true; - - var compilation = argumentOperation.SemanticModel!.Compilation; - if (valueOperation.Type.IsEqualTo(compilation.GetBestTypeByMetadataName("System.TimeSpan"))) + private bool IsConstant(IArgumentOperation argumentOperation) { - return TimeSpanOperation.GetMilliseconds(valueOperation).HasValue; - } + var valueOperation = argumentOperation.Value; + if (valueOperation.ConstantValue.HasValue) + return true; + + if (valueOperation.Type.IsEqualTo(_timespanSymbol)) + return _timeSpanOperation.GetMilliseconds(valueOperation).HasValue; - return false; + return false; + } } } diff --git a/src/Meziantou.Analyzer/Rules/UseStructLayoutAttributeAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseStructLayoutAttributeAnalyzer.cs index 02c7ad91..e207a13c 100644 --- a/src/Meziantou.Analyzer/Rules/UseStructLayoutAttributeAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/UseStructLayoutAttributeAnalyzer.cs @@ -31,21 +31,17 @@ public override void Initialize(AnalysisContext context) if (attributeType is null) return; - compilationContext.RegisterSymbolAction(Analyze, SymbolKind.NamedType); + compilationContext.RegisterSymbolAction(context => Analyze(context, attributeType), SymbolKind.NamedType); }); } - private static void Analyze(SymbolAnalysisContext context) + private static void Analyze(SymbolAnalysisContext context, INamedTypeSymbol attributeType) { var symbol = (INamedTypeSymbol)context.Symbol; if (!symbol.IsValueType || symbol.EnumUnderlyingType is not null) // Only support struct return; - var attributeType = context.Compilation.GetBestTypeByMetadataName("System.Runtime.InteropServices.StructLayoutAttribute"); - if (attributeType is null) - return; - - if (symbol.GetAttributes().Any(attr => attributeType.IsEqualTo(attr.AttributeClass))) + if (symbol.HasAttribute(attributeType)) return; var memberCount = 0;