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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/ANcpLua.Analyzers.CodeFixes/CodeFixResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,6 @@
</data>
<!-- AL0138 Code Fix Title -->
<data name="AL0138CodeFixTitle" xml:space="preserve">
<value>Add MidpointRounding.AwayFromZero</value>
<value>Add MidpointRounding.ToEven</value>
</data>
</root>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) {
return;
}

if (await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false)
is not { } semanticModel) {
return;
}

var ixmlSerializable = semanticModel.Compilation.GetTypeByMetadataName("System.Xml.Serialization.IXmlSerializable");
var getSchemaMethod = ixmlSerializable?.GetMembers("GetSchema").OfType<IMethodSymbol>()
.FirstOrDefault(m => m.Parameters.Length is 0);
if (ixmlSerializable is null || getSchemaMethod is null) {
return;
}

foreach (var diagnostic in context.Diagnostics) {
if (diagnostic.Id != Al0007ToAl0009IXmlSerializableAnalyzer.DiagnosticIdAl0008) {
continue;
Expand All @@ -32,6 +44,12 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) {
continue;
}

if (target.FirstAncestorOrSelf<MethodDeclarationSyntax>() is not { } methodDeclaration ||
semanticModel.GetDeclaredSymbol(methodDeclaration, context.CancellationToken) is not { } methodSymbol ||
!IsActualGetSchemaImplementation(methodSymbol, ixmlSerializable, getSchemaMethod)) {
continue;
}

context.RegisterCodeFix(
CodeAction.Create(
CodeFixResources.AL0008CodeFixTitle,
Expand Down Expand Up @@ -89,4 +107,26 @@ private static SyntaxNode ReplaceArrowWithNull(SyntaxNode arrow, SyntaxNode root
private static ArrowExpressionClauseSyntax CreateNullArrowExpression() =>
SyntaxFactory.ArrowExpressionClause(
SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression));

private static bool IsActualGetSchemaImplementation(
IMethodSymbol method,
INamedTypeSymbol ixmlSerializable,
IMethodSymbol interfaceGetSchema) {
if (method.ExplicitInterfaceImplementations.Any(interfaceMethod =>
interfaceMethod.IsEqualTo(interfaceGetSchema))) {
return true;
}

if (method.ContainingType is not INamedTypeSymbol containingType ||
!containingType.AllInterfaces.Contains(ixmlSerializable, SymbolEqualityComparer.Default)) {
return false;
}

return method.Arity == interfaceGetSchema.Arity &&
method.Parameters.Length == interfaceGetSchema.Parameters.Length &&
method.Name == "GetSchema" &&
method.ReturnType.IsEqualTo(interfaceGetSchema.ReturnType) &&
containingType.FindImplementationForInterfaceMember(interfaceGetSchema) is { } implementation &&
implementation.IsEqualTo(method);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,12 @@ private static (ExpressionSyntax Expression, ExpressionSyntax Literal) GetExpres

private static bool IsLiteral(SyntaxNode expression) =>
expression.IsKind(SyntaxKind.NullLiteralExpression) ||
expression is LiteralExpressionSyntax { Token.ValueText: "0" };
expression is LiteralExpressionSyntax { Token.Value: var value } &&
IsZeroValue(value);

private static bool IsZeroValue(object? value) =>
value is 0 or 0L or 0U or 0UL or (short)0 or (ushort)0 or (byte)0 or (sbyte)0
or 0f or 0d or 0m;

private static PatternSyntax CreatePattern(ExpressionSyntax literal, bool isNegated) {
PatternSyntax constantPattern = SyntaxFactory.ConstantPattern(literal);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ private static bool TryGetJsonConvertReplacement(
}

// Check for JsonConvert class
if (memberAccess.Expression is not IdentifierNameSyntax { Identifier.Text: "JsonConvert" }) {
if (memberAccess.Expression is not { } jsonConvertExpression ||
!IsJsonConvertType(jsonConvertExpression)) {
return false;
}

Expand All @@ -75,9 +76,30 @@ private static bool TryGetJsonConvertReplacement(
typeArgs = genericName.TypeArgumentList;
}

var argCount = invocation.ArgumentList.Arguments.Count;
if (methodName is "SerializeObject" && typeArgs is not null) {
return false;
}

if (methodName is "SerializeObject" && argCount != 1) {
return false;
}

if (methodName is "DeserializeObject" && (typeArgs is null || typeArgs.Arguments.Count != 1)) {
return false;
}

if (methodName is "DeserializeObject" && argCount != 1) {
return false;
}

return true;
}

private static bool IsJsonConvertType(ExpressionSyntax expression) {
return expression.ToString() is "JsonConvert" or "Newtonsoft.Json.JsonConvert" or "global::Newtonsoft.Json.JsonConvert";
}

private static Task<Document> ConvertToSystemTextJson(
Document document,
SyntaxNode root,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace ANcpLua.Analyzers.CodeFixes.CodeFixes;
/// </summary>
/// <remarks>
/// <list type="bullet">
/// <item><c>invocation.TargetMethod.Name == "name"</c> → <c>invocation.IsMethodNamed("name")</c></item>
/// <item><c>invocation.TargetMethod.Name == "name"</c> → <c>invocation.IsMethodNamed("type", "name")</c></item>
/// <item>
/// <c>op.ConstantValue.HasValue &amp;&amp; op.ConstantValue.Value is T name</c> →
/// <c>op.TryGetConstantValue&lt;T&gt;(out var name)</c>
Expand All @@ -25,12 +25,9 @@ public sealed partial class Al0031UseOperationExtensionsCodeFixProvider : AlCode
SyntaxNode root,
Diagnostic diagnostic) {
// Pattern 1: TargetMethod.Name == "name" → IsMethodNamed
if (TryGetMethodNameComparison(binary, out var invocationExpr, out var methodName)) {
// Only offer fix if we can determine the containing type from the method name
if (GetContainingTypeFromMethodName(methodName) is not { } containingType) {
return null;
}

if (TryGetMethodNameComparison(binary, out var invocationExpr, out var methodName) &&
diagnostic.Properties.TryGetValue(Al0031UseOperationExtensionsAnalyzer.PropertyContainingType, out var containingType) &&
containingType is { Length: > 0 }) {
return CodeAction.Create(
CodeFixResources.AL0031CodeFixTitle,
_ => ConvertToIsMethodNamed(document, binary, root, invocationExpr, containingType, methodName),
Expand All @@ -48,44 +45,6 @@ public sealed partial class Al0031UseOperationExtensionsCodeFixProvider : AlCode
return null;
}

/// <summary>
/// Attempts to determine the containing type name from the method name.
/// Returns null if the containing type cannot be determined (code fix should not be offered).
/// </summary>
private static string? GetContainingTypeFromMethodName(string methodName) =>
// Map well-known method names to their containing types.
// Only offer code fix for methods where we can confidently determine the containing type.
methodName switch {
// Object methods
"ToString" or "GetHashCode" or "Equals" or "ReferenceEquals" or "GetType" => "Object",

// IDisposable
"Dispose" => "IDisposable",

// IAsyncDisposable
"DisposeAsync" => "IAsyncDisposable",

// Common collection methods - too ambiguous, don't offer fix
"Add" or "Remove" or "Clear" or "Contains" => null,

// Task methods
"ConfigureAwait" or "GetAwaiter" => "Task",
"Wait" or "WaitAll" or "WaitAny" or "WhenAll" or "WhenAny" => "Task",

// String methods
"IsNullOrEmpty" or "IsNullOrWhiteSpace" or "Format" or "Join" or "Concat" => "String",

// LINQ methods - these come from Enumerable static class
"Select" or "Where" or "OrderBy" or "OrderByDescending" or "GroupBy" or "First" or "FirstOrDefault"
or "Single" or "SingleOrDefault" or "Last" or "LastOrDefault" or "Any" or "All" or "Count"
or "ToList" or "ToArray" or "ToDictionary" or "Aggregate" or "Sum" or "Max" or "Min" or "Average"
or "Skip" or "Take" or "SkipWhile" or "TakeWhile" or "Distinct" or "Union" or "Intersect" or "Except"
or "Zip" or "SelectMany" or "Cast" or "OfType" => "Enumerable",

// Unknown method - cannot determine containing type, don't offer fix
_ => null
};

private static bool TryGetMethodNameComparison(
BinaryExpressionSyntax binary,
[NotNullWhen(true)] out ExpressionSyntax? invocationExpr,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,6 @@ namespace ANcpLua.Analyzers.CodeFixes.CodeFixes;
[Shared]
public sealed partial class Al0037UseTryParseExtensionsCodeFixProvider
: AlCodeFixProvider<ConditionalExpressionSyntax> {
// Mapping from type name to extension method name
private static readonly Dictionary<string, string> s_typeToExtension = new(StringComparer.Ordinal) {
["int"] = "TryParseInt32",
["Int32"] = "TryParseInt32",
["long"] = "TryParseInt64",
["Int64"] = "TryParseInt64",
["double"] = "TryParseDouble",
["Double"] = "TryParseDouble",
["decimal"] = "TryParseDecimal",
["Decimal"] = "TryParseDecimal",
["bool"] = "TryParseBool",
["Boolean"] = "TryParseBool",
["Guid"] = "TryParseGuid",
["DateTime"] = "TryParseDateTime",
["DateTimeOffset"] = "TryParseDateTimeOffset",
["TimeSpan"] = "TryParseTimeSpan",
["byte"] = "TryParseByte",
["Byte"] = "TryParseByte",
["short"] = "TryParseInt16",
["Int16"] = "TryParseInt16",
["float"] = "TryParseSingle",
["Single"] = "TryParseSingle"
};

/// <summary>Gets the diagnostic IDs this code fix can fix.</summary>
public override ImmutableArray<string> FixableDiagnosticIds => [Al0037UseTryParseExtensionsAnalyzer.DiagnosticId];

Expand Down Expand Up @@ -68,7 +44,7 @@ private static Task<Document> ConvertToExtension(
}

// Get the type and extension method name
var (stringArg, extensionName) = ExtractInfo(tryParseInvocation);
var (stringArg, extensionName) = ExtractInfo(conditional, tryParseInvocation);
if (stringArg is null || extensionName is null) {
return Task.FromResult(document);
}
Expand All @@ -86,6 +62,7 @@ private static Task<Document> ConvertToExtension(
}

private static (ExpressionSyntax? stringArg, string? extensionName) ExtractInfo(
ConditionalExpressionSyntax conditional,
InvocationExpressionSyntax invocation) {
// Pattern: Type.TryParse(stringArg, out var result)
if (invocation.Expression is not MemberAccessExpressionSyntax {
Expand All @@ -94,14 +71,12 @@ private static (ExpressionSyntax? stringArg, string? extensionName) ExtractInfo(
return (null, null);
}

// Get the type name
var typeName = memberAccess.Expression switch {
IdentifierNameSyntax id => id.Identifier.Text,
PredefinedTypeSyntax predefined => predefined.Keyword.Text,
_ => null
};
if (memberAccess.Expression is not { } receiverSyntax) {
return (null, null);
}

if (typeName is null || !s_typeToExtension.TryGetValue(typeName, out var extensionName)) {
if (GetTypeName(receiverSyntax) is not { } receiverTypeName ||
GetTryParseExtension(receiverTypeName) is not { } extensionName) {
return (null, null);
}

Expand All @@ -110,7 +85,74 @@ private static (ExpressionSyntax? stringArg, string? extensionName) ExtractInfo(
return (null, null);
}

if (invocation.ArgumentList.Arguments.Count != 2) {
return (null, null);
}

if (invocation.ArgumentList.Arguments[0].Expression is null or AssignmentExpressionSyntax) {
return (null, null);
}

if (invocation.ArgumentList.Arguments[1].RefKindKeyword is not { RawKind: (int)SyntaxKind.OutKeyword }) {
return (null, null);
}

if (!TryGetOutVariableName(invocation.ArgumentList.Arguments[1].Expression, out var outVarName)) {
return (null, null);
}

var whenTrue = conditional.WhenTrue;
while (whenTrue is ParenthesizedExpressionSyntax trueParen) {
whenTrue = trueParen.Expression;
}

if (whenTrue is not IdentifierNameSyntax whenTrueVar ||
whenTrueVar.Identifier.Text != outVarName) {
return (null, null);
}

var stringArg = invocation.ArgumentList.Arguments[0].Expression;
return (stringArg, extensionName);
}

private static string? GetTypeName(ExpressionSyntax expression) =>
expression switch {
IdentifierNameSyntax id => id.Identifier.Text,
PredefinedTypeSyntax predefined => predefined.Keyword.Text,
QualifiedNameSyntax qualified => qualified.ToString(),
AliasQualifiedNameSyntax aliasQualified => aliasQualified.Name.ToString(),
MemberAccessExpressionSyntax memberAccess => memberAccess.ToString(),
_ => null
};

private static string? GetTryParseExtension(string typeName) =>
typeName switch {
"System.Int32" or "int" => "TryParseInt32",
"System.Int64" or "long" => "TryParseInt64",
"System.Double" or "double" => "TryParseDouble",
"System.Decimal" or "decimal" => "TryParseDecimal",
"System.Boolean" or "bool" => "TryParseBool",
"System.Guid" or "Guid" => "TryParseGuid",
"System.DateTime" or "DateTime" => "TryParseDateTime",
"System.DateTimeOffset" or "DateTimeOffset" => "TryParseDateTimeOffset",
"System.TimeSpan" or "TimeSpan" => "TryParseTimeSpan",
"System.Byte" or "byte" => "TryParseByte",
"System.Int16" or "short" => "TryParseInt16",
"System.Single" or "float" => "TryParseSingle",
_ => null
};

private static bool TryGetOutVariableName(ExpressionSyntax expression, out string variableName) {
switch (expression) {
case DeclarationExpressionSyntax { Designation: SingleVariableDesignationSyntax { Identifier.Text: var variable } }:
variableName = variable;
return true;
case IdentifierNameSyntax { Identifier.Text: var variable }:
variableName = variable;
return true;
default:
variableName = "";
return false;
}
}
}
Loading
Loading