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
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,30 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
if (nodeToFix is null)
return;

var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
if (semanticModel is null)
return;

var compilation = semanticModel.Compilation;
if (compilation.GetBestTypeByMetadataName("System.Text.RegularExpressions.Regex") is null)
return;

if (compilation.GetBestTypeByMetadataName("System.Text.RegularExpressions.GeneratedRegexAttribute") is null)
return;

if (compilation.GetBestTypeByMetadataName("System.Text.RegularExpressions.RegexOptions") is null)
return;

var arguments = semanticModel.GetOperation(nodeToFix, context.CancellationToken) switch
{
IObjectCreationOperation objectCreationOperation => objectCreationOperation.Arguments,
IInvocationOperation invocationOperation => invocationOperation.Arguments,
_ => default,
};

if (arguments.IsDefault || !AreArgumentIndicesValid(arguments, context.Diagnostics[0].Properties))
return;

// Check if C# 14 or later is available
var isCSharp14OrAbove = false;
if (context.Document.Project.ParseOptions is CSharpParseOptions parseOptions)
Expand Down Expand Up @@ -239,28 +263,18 @@ private static async Task<Document> ConvertToSourceGenerator(Document document,
}
else if (operation is IInvocationOperation invocationOperation)
{
var arguments = invocationOperation.Arguments;
var indices = new[]
{
TryParseInt32(properties, UseRegexSourceGeneratorAnalyzerCommon.PatternIndexName),
TryParseInt32(properties, UseRegexSourceGeneratorAnalyzerCommon.RegexOptionsIndexName),
TryParseInt32(properties, UseRegexSourceGeneratorAnalyzerCommon.RegexTimeoutIndexName),
};
foreach (var index in indices.Where(value => value is not null).OrderDescending())
{
arguments = arguments.RemoveAt(index.GetValueOrDefault());
}
var arguments = GetRemainingArgumentSyntaxes(invocationOperation.Arguments, properties);

if (usePartialProperty)
{
var accessProperty = generator.IdentifierName(methodName);
var method = generator.InvocationExpression(generator.MemberAccessExpression(accessProperty, invocationOperation.TargetMethod.Name), [.. arguments.Select(arg => arg.Syntax)]);
var method = generator.InvocationExpression(generator.MemberAccessExpression(accessProperty, invocationOperation.TargetMethod.Name), arguments);
newTypeDeclaration = newTypeDeclaration.ReplaceNode(nodeToFix, method);
}
else
{
var createRegexMethod = generator.InvocationExpression(generator.IdentifierName(methodName));
var method = generator.InvocationExpression(generator.MemberAccessExpression(createRegexMethod, invocationOperation.TargetMethod.Name), [.. arguments.Select(arg => arg.Syntax)]);
var method = generator.InvocationExpression(generator.MemberAccessExpression(createRegexMethod, invocationOperation.TargetMethod.Name), arguments);
newTypeDeclaration = newTypeDeclaration.ReplaceNode(nodeToFix, method);
}
}
Expand Down Expand Up @@ -369,9 +383,49 @@ private static async Task<Document> ConvertToSourceGenerator(Document document,
return document.WithSyntaxRoot(root);
}

private static SyntaxNode? GetNode(ImmutableArray<IArgumentOperation> args, ImmutableDictionary<string, string?> properties, string name)
/// <summary>
/// Returns the syntax of the arguments that must be kept on the instance method, in their source order, once the
/// arguments lifted to the <c>GeneratedRegex</c> attribute are removed.
/// </summary>
private static SyntaxNode[] GetRemainingArgumentSyntaxes(ImmutableArray<IArgumentOperation> args, ImmutableDictionary<string, string?> properties)
{
var liftedIndices = new[]
{
GetArgumentIndex(args, properties, UseRegexSourceGeneratorAnalyzerCommon.PatternIndexName),
GetArgumentIndex(args, properties, UseRegexSourceGeneratorAnalyzerCommon.RegexOptionsIndexName),
GetArgumentIndex(args, properties, UseRegexSourceGeneratorAnalyzerCommon.RegexTimeoutIndexName),
};

return [.. args.Where((_, index) => !liftedIndices.Contains(index)).Select(arg => arg.Syntax)];
}

/// <summary>
/// Ensures the argument indices reported by the analyzer can be applied to <paramref name="args"/>, so the fix is
/// only registered when it can produce a valid replacement.
/// </summary>
private static bool AreArgumentIndicesValid(ImmutableArray<IArgumentOperation> args, ImmutableDictionary<string, string?> properties)
{
if (GetArgumentIndex(args, properties, UseRegexSourceGeneratorAnalyzerCommon.PatternIndexName) is null)
return false;

return IsIndexValid(UseRegexSourceGeneratorAnalyzerCommon.RegexOptionsIndexName)
&& IsIndexValid(UseRegexSourceGeneratorAnalyzerCommon.RegexTimeoutIndexName);

bool IsIndexValid(string name) => TryParseInt32(properties, name) is not { } index || (index >= 0 && index < args.Length);
}

private static int? GetArgumentIndex(ImmutableArray<IArgumentOperation> args, ImmutableDictionary<string, string?> properties, string name)
{
var index = TryParseInt32(properties, name);
if (index is null || index.Value < 0 || index.Value >= args.Length)
return null;

return index;
}

private static SyntaxNode? GetNode(ImmutableArray<IArgumentOperation> args, ImmutableDictionary<string, string?> properties, string name)
{
var index = GetArgumentIndex(args, properties, name);
if (index is null)
return null;

Expand Down Expand Up @@ -606,27 +660,17 @@ private static Task<Document> ConvertTopLevelStatementToSourceGenerator(
}
else if (operation is IInvocationOperation invocationOperation)
{
var arguments = invocationOperation.Arguments;
var indices = new[]
{
TryParseInt32(properties, UseRegexSourceGeneratorAnalyzerCommon.PatternIndexName),
TryParseInt32(properties, UseRegexSourceGeneratorAnalyzerCommon.RegexOptionsIndexName),
TryParseInt32(properties, UseRegexSourceGeneratorAnalyzerCommon.RegexTimeoutIndexName),
};
foreach (var index in indices.Where(value => value is not null).OrderDescending())
{
arguments = arguments.RemoveAt(index.GetValueOrDefault());
}
var arguments = GetRemainingArgumentSyntaxes(invocationOperation.Arguments, properties);

if (usePartialProperty)
{
var accessProperty = generator.IdentifierName(methodName);
replacementNode = generator.InvocationExpression(generator.MemberAccessExpression(accessProperty, invocationOperation.TargetMethod.Name), [.. arguments.Select(arg => arg.Syntax)]);
replacementNode = generator.InvocationExpression(generator.MemberAccessExpression(accessProperty, invocationOperation.TargetMethod.Name), arguments);
}
else
{
var createRegexMethod = generator.InvocationExpression(generator.IdentifierName(methodName));
replacementNode = generator.InvocationExpression(generator.MemberAccessExpression(createRegexMethod, invocationOperation.TargetMethod.Name), [.. arguments.Select(arg => arg.Syntax)]);
replacementNode = generator.InvocationExpression(generator.MemberAccessExpression(createRegexMethod, invocationOperation.TargetMethod.Name), arguments);
}
}
else
Expand Down
145 changes: 74 additions & 71 deletions src/Meziantou.Analyzer/Rules/UseRegexSourceGeneratorAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ public override void Initialize(AnalysisContext context)

private sealed class AnalyzerContext(Compilation compilation)
{
// The parameters lifted to the GeneratedRegex attribute
private const string PatternParameterName = "pattern";
private const string OptionsParameterName = "options";
private const string MatchTimeoutParameterName = "matchTimeout";

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");
Expand Down Expand Up @@ -59,19 +64,12 @@ public void AnalyzeObjectCreation(OperationAnalysisContext context)
if (!op.Type.IsEqualTo(_regexSymbol))
return;

foreach (var arg in op.Arguments)
{
if (!IsConstant(arg))
return;
}

var properties = ImmutableDictionary.CreateRange(
[
new KeyValuePair<string, string?>(UseRegexSourceGeneratorAnalyzerCommon.PatternIndexName, "0"),
new KeyValuePair<string, string?>(UseRegexSourceGeneratorAnalyzerCommon.RegexOptionsIndexName, op.Arguments.Length > 1 ? "1" : null),
new KeyValuePair<string, string?>(UseRegexSourceGeneratorAnalyzerCommon.RegexTimeoutIndexName, op.Arguments.Length > 2 ? "2" : null),
new KeyValuePair<string, string?>(UseRegexSourceGeneratorAnalyzerCommon.RegexTimeoutName, op.Arguments.Length > 2 ? _timeSpanOperation.GetMilliseconds(op.Arguments[2].Value)?.ToString(CultureInfo.InvariantCulture) : null),
]);
// Regex(string pattern)
// Regex(string pattern, RegexOptions options)
// Regex(string pattern, RegexOptions options, TimeSpan matchTimeout)
var properties = TryCreateProperties(op.Arguments);
if (properties is null)
return;

context.ReportDiagnostic(RegexSourceGeneratorRule, properties, op);
}
Expand All @@ -86,68 +84,73 @@ public void AnalyzeInvocation(OperationAnalysisContext context)
if (!method.IsStatic || !method.ContainingType.IsEqualTo(_regexSymbol))
return;

if (method.Name is "IsMatch" or "Match" or "Matches" or "Split")
// IsMatch/Match/Matches/Split(string input, string pattern[, RegexOptions options[, TimeSpan matchTimeout]])
// Replace(string input, string pattern, string replacement[, RegexOptions options[, TimeSpan matchTimeout]])
// Replace(string input, string pattern, MatchEvaluator evaluator[, RegexOptions options[, TimeSpan matchTimeout]])
if (method.Name is not ("IsMatch" or "Match" or "Matches" or "Split" or "Replace"))
return;

var properties = TryCreateProperties(op.Arguments);
if (properties is null)
return;

context.ReportDiagnostic(RegexSourceGeneratorRule, properties, op);
}

/// <summary>
/// Computes the diagnostic properties describing the arguments the code fixer must lift to the
/// <c>GeneratedRegex</c> attribute, or <see langword="null"/> when the operation cannot be converted.
/// </summary>
/// <remarks>
/// The arguments are located from <see cref="IArgumentOperation.Parameter"/> instead of their position in
/// <paramref name="arguments"/>, as reordered named arguments are listed in evaluation order.
/// </remarks>
private ImmutableDictionary<string, string?>? TryCreateProperties(ImmutableArray<IArgumentOperation> arguments)
{
var patternIndex = GetArgumentIndex(arguments, PatternParameterName);
if (patternIndex is null)
return null;

var optionsIndex = GetArgumentIndex(arguments, OptionsParameterName);
var timeoutIndex = GetArgumentIndex(arguments, MatchTimeoutParameterName);

// An overload with an unknown parameter cannot be converted, as the parameter would be silently dropped
for (var i = 0; i < arguments.Length; i++)
{
// 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<string, string?>(UseRegexSourceGeneratorAnalyzerCommon.PatternIndexName, "1"),
new KeyValuePair<string, string?>(UseRegexSourceGeneratorAnalyzerCommon.RegexOptionsIndexName, op.Arguments.Length > 2 ? "2" : null),
new KeyValuePair<string, string?>(UseRegexSourceGeneratorAnalyzerCommon.RegexTimeoutIndexName, op.Arguments.Length > 3 ? "3" : null),
new KeyValuePair<string, string?>(UseRegexSourceGeneratorAnalyzerCommon.RegexTimeoutName, op.Arguments.Length > 3 ? _timeSpanOperation.GetMilliseconds(op.Arguments[3].Value)?.ToString(CultureInfo.InvariantCulture) : null),
]);

context.ReportDiagnostic(RegexSourceGeneratorRule, properties, op);
if (i == patternIndex || i == optionsIndex || i == timeoutIndex)
continue;

if (arguments[i].Parameter?.Name is not ("input" or "replacement" or "evaluator"))
return null;
}
else if (method.Name is "Replace")

if (!IsConstant(arguments[patternIndex.Value]))
return null;

if (optionsIndex is not null && !IsConstant(arguments[optionsIndex.Value]))
return null;

if (timeoutIndex is not null && !IsConstant(arguments[timeoutIndex.Value]))
return null;

return ImmutableDictionary.CreateRange(
[
new KeyValuePair<string, string?>(UseRegexSourceGeneratorAnalyzerCommon.PatternIndexName, patternIndex.Value.ToString(CultureInfo.InvariantCulture)),
new KeyValuePair<string, string?>(UseRegexSourceGeneratorAnalyzerCommon.RegexOptionsIndexName, optionsIndex?.ToString(CultureInfo.InvariantCulture)),
new KeyValuePair<string, string?>(UseRegexSourceGeneratorAnalyzerCommon.RegexTimeoutIndexName, timeoutIndex?.ToString(CultureInfo.InvariantCulture)),
new KeyValuePair<string, string?>(UseRegexSourceGeneratorAnalyzerCommon.RegexTimeoutName, timeoutIndex is null ? null : _timeSpanOperation.GetMilliseconds(arguments[timeoutIndex.Value].Value)?.ToString(CultureInfo.InvariantCulture)),
]);
}

private static int? GetArgumentIndex(ImmutableArray<IArgumentOperation> arguments, string parameterName)
{
for (var i = 0; i < arguments.Length; i++)
{
// 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<string, string?>(UseRegexSourceGeneratorAnalyzerCommon.PatternIndexName, "1"),
new KeyValuePair<string, string?>(UseRegexSourceGeneratorAnalyzerCommon.RegexOptionsIndexName, op.Arguments.Length > 3 ? "3" : null),
new KeyValuePair<string, string?>(UseRegexSourceGeneratorAnalyzerCommon.RegexTimeoutIndexName, op.Arguments.Length > 4 ? "4" : null),
new KeyValuePair<string, string?>(UseRegexSourceGeneratorAnalyzerCommon.RegexTimeoutName, op.Arguments.Length > 4 ? _timeSpanOperation.GetMilliseconds(op.Arguments[4].Value)?.ToString(CultureInfo.InvariantCulture) : null),
]);

context.ReportDiagnostic(RegexSourceGeneratorRule, properties, op);
if (arguments[i].Parameter?.Name == parameterName)
return i;
}

return null;
}

private bool IsConstant(IArgumentOperation argumentOperation)
Expand Down
Loading