提交 a5309d1f 编写于 作者: C Cyrus Najmabadi

Add more fix-all support.

上级 40d893ee
......@@ -408,15 +408,15 @@ class C
{
void Goo(string[] s)
{
var v1 = s[{|FixAllInDocument:|}s[0][s[0].Length - 1].Length - 1];
var v1 = s[{|FixAllInDocument:|}s.Length - 2][s[s.Length - 2].Length - 1];
}
}",
@"
class C
{
void Goo(string s)
void Goo(string[] s)
{
var v1 = s[^(s[0][^1])];
var v1 = s[^2][^1];
}
}", parseOptions: s_parseOptions);
}
......
......@@ -152,6 +152,30 @@ void Goo(S s)
{
var v = s.Slice(1..^1);
}
}", parseOptions: s_parseOptions);
}
[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseRangeOperator)]
public async Task TestFixAllInvocationToElementAccess1()
{
// Note: once the IOp tree has support for range operators, this should
// simplify even further.
await TestAsync(
@"
class C
{
void Goo(string s, string t)
{
var v = t.Substring(s.Substring({|FixAllInDocument:|}1, s.Length - 2)[0], t.Length - s.Substring(1, s.Length - 2)[0]);
}
}",
@"
class C
{
void Goo(string s, string t)
{
var v = t.Substring(s[1..^1][0], t.Length - s[1..^1][0]);
}
}", parseOptions: s_parseOptions);
}
}
......
......@@ -36,6 +36,7 @@ public override Task RegisterCodeFixesAsync(CodeFixContext context)
Document document, ImmutableArray<Diagnostic> diagnostics,
SyntaxEditor editor, CancellationToken cancellationToken)
{
// Process diagnostics from innermost to outermost in case any are nested.
foreach (var diagnostic in diagnostics.OrderByDescending(d => d.Location.SourceSpan.Start))
{
var node = diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken);
......
......@@ -3,6 +3,7 @@
using System;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
......@@ -41,60 +42,87 @@ public override Task RegisterCodeFixesAsync(CodeFixContext context)
Document document, ImmutableArray<Diagnostic> diagnostics,
SyntaxEditor editor, CancellationToken cancellationToken)
{
var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
var invocationNodes = diagnostics.Select(d => GetInvocationExpression(d, cancellationToken))
.OrderByDescending(i => i.SpanStart)
.ToImmutableArray();
await editor.ApplyExpressionLevelSemanticEditsAsync(
document, invocationNodes,
canReplace: (_1, _2) => true,
(semanticModel, currentRoot, currentInvocation) =>
UpdateInvocation(semanticModel, currentRoot, currentInvocation, cancellationToken),
cancellationToken);
}
foreach (var diagnostic in diagnostics)
private SyntaxNode UpdateInvocation(
SemanticModel semanticModel, SyntaxNode currentRoot,
InvocationExpressionSyntax currentInvocation,
CancellationToken cancellationToken)
{
var invocation = semanticModel.GetOperation(currentInvocation, cancellationToken) as IInvocationOperation;
if (invocation != null)
{
FixOne(semanticModel, editor, diagnostic, cancellationToken);
var infoCache = new InfoCache(semanticModel.Compilation);
var resultOpt = AnalyzeInvocation(
invocation, infoCache, analyzerOptionsOpt: null, cancellationToken);
if (resultOpt != null)
{
var result = resultOpt.Value;
var updatedNode = FixOne(semanticModel, result, cancellationToken);
if (updatedNode != null)
{
return currentRoot.ReplaceNode(result.Invocation, updatedNode);
}
}
}
return currentRoot;
}
private void FixOne(
SemanticModel semanticModel, SyntaxEditor editor,
Diagnostic diagnostic, CancellationToken cancellationToken)
private static InvocationExpressionSyntax GetInvocationExpression(Diagnostic d, CancellationToken cancellationToken)
=> (InvocationExpressionSyntax)d.AdditionalLocations[0].FindNode(getInnermostNodeForTie: true, cancellationToken);
private ExpressionSyntax FixOne(
SemanticModel semanticModel, Result result, CancellationToken cancellationToken)
{
var invocation = (InvocationExpressionSyntax)diagnostic.AdditionalLocations[0].FindNode(getInnermostNodeForTie: true, cancellationToken);
var invocation = result.Invocation;
var expression = invocation.Expression is MemberAccessExpressionSyntax memberAccess
? memberAccess.Expression
: invocation.Expression;
var rangeExpression = CreateRangeExpression(
semanticModel, diagnostic, invocation, cancellationToken);
var rangeExpression = CreateRangeExpression(semanticModel, result, cancellationToken);
var argument = Argument(rangeExpression).WithAdditionalAnnotations(Formatter.Annotation);
var arguments = SingletonSeparatedList(argument);
if (diagnostic.Properties.ContainsKey(UseIndexer))
if (result.MemberInfo.OverloadedMethodOpt == null)
{
var argList = invocation.ArgumentList;
var elementAccess = ElementAccessExpression(
return ElementAccessExpression(
expression,
BracketedArgumentList(
Token(SyntaxKind.OpenBracketToken).WithTriviaFrom(argList.OpenParenToken),
arguments,
Token(SyntaxKind.CloseBracketToken).WithTriviaFrom(argList.CloseParenToken)));
editor.ReplaceNode(invocation, elementAccess);
}
else
{
editor.ReplaceNode(
return invocation.ReplaceNode(
invocation.ArgumentList,
invocation.ArgumentList.WithArguments(arguments));
}
}
private RangeExpressionSyntax CreateRangeExpression(
SemanticModel semanticModel, Diagnostic diagnostic,
InvocationExpressionSyntax invocation, CancellationToken cancellationToken)
SemanticModel semanticModel, Result result, CancellationToken cancellationToken)
{
var properties = diagnostic.Properties;
if (properties.ContainsKey(ComputedRange))
if (result.RangeKind == ComputedRange)
{
return CreateComputedRange(semanticModel, diagnostic, invocation, cancellationToken);
return CreateComputedRange(semanticModel, result, cancellationToken);
}
else if (properties.ContainsKey(ConstantRange))
else if (result.RangeKind == ConstantRange)
{
return CreateConstantRange(semanticModel, diagnostic, cancellationToken);
return CreateConstantRange(semanticModel, result, cancellationToken);
}
else
{
......@@ -103,59 +131,48 @@ public override Task RegisterCodeFixesAsync(CodeFixContext context)
}
private RangeExpressionSyntax CreateComputedRange(
SemanticModel semanticModel, Diagnostic diagnostic,
InvocationExpressionSyntax invocation, CancellationToken cancellationToken)
SemanticModel semanticModel, Result result, CancellationToken cancellationToken)
{
var startExpr = (ExpressionSyntax)diagnostic.AdditionalLocations[1].FindNode(getInnermostNodeForTie: true, cancellationToken);
var endExpr = (ExpressionSyntax)diagnostic.AdditionalLocations[2].FindNode(getInnermostNodeForTie: true, cancellationToken);
// We have enough information now to generate `start..end`. However, this will often
// not be what the user wants. For example, generating `start..expr.Length` is not as
// desirable as `start..`. Similarly, `start..(expr.Length - 1)` is not as desirable as
// `start..^1`.
var startOperation = result.Op1;
var endOperation = result.Op2;
var startExpr = (ExpressionSyntax)startOperation.Syntax;
var endExpr = (ExpressionSyntax)endOperation.Syntax;
var startFromEnd = false;
var endFromEnd = false;
if (semanticModel.GetOperation(invocation, cancellationToken) is IInvocationOperation invocationOp &&
IsSliceLikeMethod(invocationOp.TargetMethod) &&
invocationOp.Instance != null)
{
var startOperation = semanticModel.GetOperation(startExpr, cancellationToken);
var endOperation = semanticModel.GetOperation(endExpr, cancellationToken);
var checker = new InfoCache(semanticModel.Compilation);
if (startOperation != null &&
endOperation != null &&
checker.TryGetMemberInfo(invocationOp.TargetMethod, out var memberInfo))
{
var lengthLikeProperty = memberInfo.LengthLikeProperty;
var lengthLikeProperty = result.MemberInfo.LengthLikeProperty;
var instance = result.InvocationOperation.Instance;
// If our start-op is actually equivalent to `expr.Length - val`, then just change our
// start-op to be `val` and record that we should emit it as `^val`.
startFromEnd = IsFromEnd(lengthLikeProperty, invocationOp.Instance, ref startOperation);
startExpr = (ExpressionSyntax)startOperation.Syntax;
// If our start-op is actually equivalent to `expr.Length - val`, then just change our
// start-op to be `val` and record that we should emit it as `^val`.
startFromEnd = IsFromEnd(lengthLikeProperty, instance, ref startOperation);
startExpr = (ExpressionSyntax)startOperation.Syntax;
// Similarly, if our end-op is actually equivalent to `expr.Length - val`, then just
// change our end-op to be `val` and record that we should emit it as `^val`.
endFromEnd = IsFromEnd(lengthLikeProperty, invocationOp.Instance, ref endOperation);
endExpr = (ExpressionSyntax)endOperation.Syntax;
// Similarly, if our end-op is actually equivalent to `expr.Length - val`, then just
// change our end-op to be `val` and record that we should emit it as `^val`.
endFromEnd = IsFromEnd(lengthLikeProperty, instance, ref endOperation);
endExpr = (ExpressionSyntax)endOperation.Syntax;
// If the range operation goes to 'expr.Length' then we can just leave off the end part
// of the range. i.e. `start..`
if (IsInstanceLengthCheck(lengthLikeProperty, invocationOp.Instance, endOperation))
{
endExpr = null;
}
// If the range operation goes to 'expr.Length' then we can just leave off the end part
// of the range. i.e. `start..`
if (IsInstanceLengthCheck(lengthLikeProperty, instance, endOperation))
{
endExpr = null;
}
// If we're starting the range operation from 0, then we can just leave off the start of
// the range. i.e. `..end`
if (startOperation.ConstantValue.HasValue &&
startOperation.ConstantValue.Value is 0)
{
startExpr = null;
}
}
// If we're starting the range operation from 0, then we can just leave off the start of
// the range. i.e. `..end`
if (startOperation.ConstantValue.HasValue &&
startOperation.ConstantValue.Value is 0)
{
startExpr = null;
}
return RangeExpression(
......@@ -169,15 +186,15 @@ private static ExpressionSyntax GetExpression(ImmutableDictionary<string, string
: props.ContainsKey(fromEndKey) ? IndexExpression(expr) : expr;
private static RangeExpressionSyntax CreateConstantRange(
SemanticModel semanticModel, Diagnostic diagnostic, CancellationToken cancellationToken)
SemanticModel semanticModel, Result result, CancellationToken cancellationToken)
{
var constant1Syntax = (ExpressionSyntax)diagnostic.AdditionalLocations[1].FindNode(getInnermostNodeForTie: true, cancellationToken);
var constant2Syntax = (ExpressionSyntax)diagnostic.AdditionalLocations[2].FindNode(getInnermostNodeForTie: true, cancellationToken);
var constant1Syntax = (ExpressionSyntax)result.Op1.Syntax;
var constant2Syntax = (ExpressionSyntax)result.Op2.Syntax;
// the form is s.Slice(constant1, s.Length - constant2). Want to generate
// s[constant1..(constant2-constant1)]
var constant1 = GetInt32Value(semanticModel.GetOperation(constant1Syntax));
var constant2 = GetInt32Value(semanticModel.GetOperation(constant2Syntax));
var constant1 = GetInt32Value(result.Op1);
var constant2 = GetInt32Value(result.Op2);
var endExpr = (ExpressionSyntax)CSharpSyntaxGenerator.Instance.LiteralExpression(constant2 - constant1);
return RangeExpression(
......
......@@ -2,6 +2,7 @@
using System.Collections.Immutable;
using System.Composition;
using System.Threading;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Syntax;
......@@ -29,11 +30,11 @@ namespace Microsoft.CodeAnalysis.CSharp.UseIndexOrRangeOperator
[DiagnosticAnalyzer(LanguageNames.CSharp), Shared]
internal partial class CSharpUseRangeOperatorDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer
{
public const string UseIndexer = nameof(UseIndexer);
// public const string UseIndexer = nameof(UseIndexer);
public const string ComputedRange = nameof(ComputedRange);
public const string ConstantRange = nameof(ConstantRange);
public CSharpUseRangeOperatorDiagnosticAnalyzer()
public CSharpUseRangeOperatorDiagnosticAnalyzer()
: base(IDEDiagnosticIds.UseRangeOperatorDiagnosticId,
new LocalizableResourceString(nameof(FeaturesResources.Use_range_operator), FeaturesResources.ResourceManager, typeof(FeaturesResources)),
new LocalizableResourceString(nameof(FeaturesResources._0_can_be_simplified), FeaturesResources.ResourceManager, typeof(FeaturesResources)))
......@@ -59,44 +60,67 @@ protected override void InitializeWorker(AnalysisContext context)
private void AnalyzeInvocation(
OperationAnalysisContext context, InfoCache infoCache)
{
var cancellationToken = context.CancellationToken;
var invocation = (IInvocationOperation)context.Operation;
var resultOpt = AnalyzeInvocation(
(IInvocationOperation)context.Operation, infoCache, context.Options, context.CancellationToken);
// Validate we're on a piece of syntax we expect. While not necessary for analysis, we
// want to make sure we're on something the fixer will know how to actually fix.
var invocationSyntax = invocation.Syntax as InvocationExpressionSyntax;
if (invocationSyntax is null ||
invocationSyntax.ArgumentList is null)
if (resultOpt == null)
{
return;
}
// Check if we're at least on C# 8, and that the user wants these operators.
var syntaxTree = invocationSyntax.SyntaxTree;
var parseOptions = (CSharpParseOptions)syntaxTree.Options;
if (parseOptions.LanguageVersion < LanguageVersion.CSharp8)
{
return;
}
var result = resultOpt.Value;
context.ReportDiagnostic(CreateDiagnostic(
result.RangeKind,
result.Option,
result.Invocation,
result.SliceLikeMethod,
result.MemberInfo,
result.Op1,
result.Op2));
}
var optionSet = context.Options.GetDocumentOptionSetAsync(syntaxTree, cancellationToken).GetAwaiter().GetResult();
if (optionSet is null)
public static Result? AnalyzeInvocation(
IInvocationOperation invocation, InfoCache infoCache,
AnalyzerOptions analyzerOptionsOpt, CancellationToken cancellationToken)
{
// Validate we're on a piece of syntax we expect. While not necessary for analysis, we
// want to make sure we're on something the fixer will know how to actually fix.
var invocationSyntax = invocation.Syntax as InvocationExpressionSyntax;
if (invocationSyntax is null ||
invocationSyntax.ArgumentList is null)
{
return;
return default;
}
var option = optionSet.GetOption(CSharpCodeStyleOptions.PreferRangeOperator);
if (!option.Value)
CodeStyleOption<bool> option = null;
if (analyzerOptionsOpt != null)
{
return;
// Check if we're at least on C# 8, and that the user wants these operators.
var syntaxTree = invocationSyntax.SyntaxTree;
var parseOptions = (CSharpParseOptions)syntaxTree.Options;
if (parseOptions.LanguageVersion < LanguageVersion.CSharp8)
{
return default;
}
var optionSet = analyzerOptionsOpt.GetDocumentOptionSetAsync(syntaxTree, cancellationToken).GetAwaiter().GetResult();
if (optionSet is null)
{
return default;
}
option = optionSet.GetOption(CSharpCodeStyleOptions.PreferRangeOperator);
if (!option.Value)
{
return default;
}
}
// look for `s.Slice(e1, end - e2)`
if (invocation.Instance is null ||
invocation.Arguments.Length != 2)
{
return;
return default;
}
// See if the call is to something slice-like.
......@@ -108,7 +132,7 @@ protected override void InitializeWorker(AnalysisContext context)
if (!IsSubtraction(invocation.Arguments[1].Value, out var subtraction) ||
!infoCache.TryGetMemberInfo(targetMethod, out var memberInfo))
{
return;
return default;
}
// See if we have: (start, end - start). Specifically where the start operation it the
......@@ -117,10 +141,11 @@ protected override void InitializeWorker(AnalysisContext context)
if (CSharpSyntaxFactsService.Instance.AreEquivalent(startOperation.Syntax, subtraction.RightOperand.Syntax))
{
context.ReportDiagnostic(CreateDiagnostic(
ComputedRange, option, invocationSyntax, targetMethod,
memberInfo, startOperation, subtraction.LeftOperand));
return;
return new Result(
ComputedRange, option,
invocation, invocationSyntax,
targetMethod, memberInfo,
startOperation, subtraction.LeftOperand);
}
// See if we have: (constant1, s.Length - constant2). The constants don't have to be
......@@ -129,30 +154,23 @@ protected override void InitializeWorker(AnalysisContext context)
IsConstantInt32(subtraction.RightOperand) &&
IsInstanceLengthCheck(memberInfo.LengthLikeProperty, invocation.Instance, subtraction.LeftOperand))
{
context.ReportDiagnostic(CreateDiagnostic(
ConstantRange, option, invocationSyntax, targetMethod,
memberInfo, startOperation, subtraction.RightOperand));
return;
return new Result(
ConstantRange, option,
invocation, invocationSyntax,
targetMethod, memberInfo,
startOperation, subtraction.RightOperand);
}
return default;
}
private Diagnostic CreateDiagnostic(
string rangeKind, CodeStyleOption<bool> option, InvocationExpressionSyntax invocation,
string rangeKind, CodeStyleOption<bool> option, InvocationExpressionSyntax invocation,
IMethodSymbol sliceLikeMethod, MemberInfo memberInfo, IOperation op1, IOperation op2)
{
var properties = ImmutableDictionary<string, string>.Empty.Add(rangeKind, rangeKind);
if (memberInfo.OverloadedMethodOpt == null)
{
properties = properties.Add(UseIndexer, UseIndexer);
}
// Keep track of the syntax nodes from the start/end ops so that we can easily
// generate the range-expression in the fixer.
// Keep track of the invocation node
var additionalLocations = ImmutableArray.Create(
invocation.GetLocation(),
op1.Syntax.GetLocation(),
op2.Syntax.GetLocation());
invocation.GetLocation());
// Mark the span under the two arguments to .Slice(..., ...) as what we will be
// updating.
......@@ -165,11 +183,39 @@ protected override void InitializeWorker(AnalysisContext context)
location,
option.Notification.Severity,
additionalLocations,
properties,
ImmutableDictionary<string, string>.Empty,
sliceLikeMethod.Name);
}
private static bool IsConstantInt32(IOperation operation)
=> operation.ConstantValue.HasValue && operation.ConstantValue.Value is int;
public readonly struct Result
{
public readonly string RangeKind;
public readonly CodeStyleOption<bool> Option;
public readonly IInvocationOperation InvocationOperation;
public readonly InvocationExpressionSyntax Invocation;
public readonly IMethodSymbol SliceLikeMethod;
public readonly MemberInfo MemberInfo;
public readonly IOperation Op1;
public readonly IOperation Op2;
public Result(
string rangeKind, CodeStyleOption<bool> option,
IInvocationOperation invocationOperation, InvocationExpressionSyntax invocation,
IMethodSymbol sliceLikeMethod, MemberInfo memberInfo,
IOperation op1, IOperation op2)
{
RangeKind = rangeKind;
Option = option;
InvocationOperation = invocationOperation;
Invocation = invocation;
SliceLikeMethod = sliceLikeMethod;
MemberInfo = memberInfo;
Op1 = op1;
Op2 = op2;
}
}
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册