blob: 1103b4b6a16a488cc22756bec02b5167f6c1696b [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Lucene.Net.CodeAnalysis.Dev.Utility;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev1xxx
{
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev1007_1008_DictionaryIndexerCodeFixProvider)), Shared]
public sealed class LuceneDev1007_1008_DictionaryIndexerCodeFixProvider : CodeFixProvider
{
private const string TitleReturn = "Use TryGetValue and return default on missing key";
public override ImmutableArray<string> FixableDiagnosticIds =>
ImmutableArray.Create(
Descriptors.LuceneDev1007_GenericDictionaryIndexerValueType.Id,
Descriptors.LuceneDev1008_GenericDictionaryIndexerReferenceType.Id);
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
if (root == null) return;
var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
if (semanticModel == null) return;
foreach (var diagnostic in context.Diagnostics)
{
var elementAccess = root.FindToken(diagnostic.Location.SourceSpan.Start)
.Parent?
.AncestorsAndSelf()
.OfType<ElementAccessExpressionSyntax>()
.FirstOrDefault(e => e.Span.Contains(diagnostic.Location.SourceSpan));
if (elementAccess == null)
continue;
// Only handle the "return dict[key];" pattern automatically.
if (elementAccess.Parent is not ReturnStatementSyntax returnStmt
|| returnStmt.Expression != elementAccess)
{
continue;
}
// If the receiver type doesn't expose an accessible TryGetValue method
// (e.g. only via explicit interface implementation), skip — the rewrite would not compile.
if (!HasAccessibleTryGetValue(semanticModel, elementAccess))
continue;
context.RegisterCodeFix(
CodeAction.Create(
title: TitleReturn,
createChangedDocument: c => ConvertReturnAsync(context.Document, returnStmt, elementAccess, c),
equivalenceKey: TitleReturn),
diagnostic);
}
}
private static bool HasAccessibleTryGetValue(SemanticModel semanticModel, ElementAccessExpressionSyntax elementAccess)
{
var receiverType = semanticModel.GetTypeInfo(elementAccess.Expression).Type;
if (receiverType == null)
return false;
foreach (var member in receiverType.GetMembers("TryGetValue"))
{
if (member is not IMethodSymbol method)
continue;
if (method.IsStatic)
continue;
if (method.DeclaredAccessibility != Accessibility.Public)
continue;
if (method.Parameters.Length != 2)
continue;
if (method.Parameters[1].RefKind != RefKind.Out)
continue;
return true;
}
return false;
}
private static async Task<Document> ConvertReturnAsync(
Document document,
ReturnStatementSyntax returnStmt,
ElementAccessExpressionSyntax elementAccess,
CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
if (root == null) return document;
var receiver = elementAccess.Expression;
var keyArg = elementAccess.ArgumentList.Arguments.FirstOrDefault();
if (keyArg == null) return document;
var outName = PickLocalName(returnStmt);
// receiver.TryGetValue(key, out var <outName>)
var tryGetValueInvocation = SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
receiver.WithoutTrivia(),
SyntaxFactory.IdentifierName("TryGetValue")),
SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(new[]
{
keyArg.WithoutTrivia(),
SyntaxFactory.Argument(
SyntaxFactory.DeclarationExpression(
SyntaxFactory.IdentifierName(
SyntaxFactory.Identifier("var")),
SyntaxFactory.SingleVariableDesignation(SyntaxFactory.Identifier(outName))))
.WithRefOrOutKeyword(SyntaxFactory.Token(SyntaxKind.OutKeyword))
})));
// tryGetValueInvocation ? <outName> : default
var ternary = SyntaxFactory.ConditionalExpression(
tryGetValueInvocation,
SyntaxFactory.IdentifierName(outName),
SyntaxFactory.LiteralExpression(SyntaxKind.DefaultLiteralExpression,
SyntaxFactory.Token(SyntaxKind.DefaultKeyword)));
var newReturn = returnStmt.WithExpression(ternary).WithAdditionalAnnotations(Formatter.Annotation);
var newRoot = root.ReplaceNode(returnStmt, newReturn);
return document.WithSyntaxRoot(newRoot);
}
private static string PickLocalName(SyntaxNode context)
{
// Avoid collisions with identifiers in the enclosing member.
var member = context.AncestorsAndSelf().OfType<MemberDeclarationSyntax>().FirstOrDefault();
var names = member == null
? ImmutableHashSet<string>.Empty
: member.DescendantTokens()
.Where(t => t.IsKind(SyntaxKind.IdentifierToken))
.Select(t => t.ValueText)
.ToImmutableHashSet();
if (!names.Contains("value"))
return "value";
for (int i = 1; i < 100; i++)
{
var candidate = "value" + i;
if (!names.Contains(candidate))
return candidate;
}
return "value";
}
}
}