提交 71360a43 编写于 作者: A Alexander Luzgarev

Support basic cases in UseRangeOperator

上级 31381123
...@@ -69,6 +69,42 @@ public bool TryGetMemberInfo(IMethodSymbol method, out MemberInfo memberInfo) ...@@ -69,6 +69,42 @@ public bool TryGetMemberInfo(IMethodSymbol method, out MemberInfo memberInfo)
return memberInfo.LengthLikeProperty != null; return memberInfo.LengthLikeProperty != null;
} }
public bool TryGetMemberInfoOneArgument(IMethodSymbol method, out MemberInfo memberInfo)
{
if (!IsSliceLikeMethodWithOneArgument(method))
{
memberInfo = default;
return false;
}
if (_methodToMemberInfo.TryGetValue(method, out memberInfo))
{
return memberInfo.LengthLikeProperty != null;
}
// Find overload of our method that is a slice-like method with two arguments.
// Computing member info for this method will also check that the containing type
// has an int32 'Length' or 'Count' property, and has a suitable indexer,
// so we don't have to.
var overloadWithTwoArguments = method.ContainingType
.GetMembers(method.Name)
.OfType<IMethodSymbol>()
.FirstOrDefault(s => IsSliceLikeMethod(s));
if (overloadWithTwoArguments is null)
{
memberInfo = default;
return false;
}
// Since the search is expensive, we keep both the original one-argument and
// two-arguments overload as keys in the cache, pointing to the same
// member information object.
var newMemberInfo = _methodToMemberInfo.GetOrAdd(overloadWithTwoArguments, _ => ComputeMemberInfo(overloadWithTwoArguments, requireRangeMember: true));
_methodToMemberInfo.GetOrAdd(method, _ => newMemberInfo);
memberInfo = newMemberInfo;
return memberInfo.LengthLikeProperty != null;
}
private MemberInfo ComputeMemberInfo(IMethodSymbol sliceLikeMethod, bool requireRangeMember) private MemberInfo ComputeMemberInfo(IMethodSymbol sliceLikeMethod, bool requireRangeMember)
{ {
Debug.Assert(IsSliceLikeMethod(sliceLikeMethod)); Debug.Assert(IsSliceLikeMethod(sliceLikeMethod));
......
...@@ -12,7 +12,7 @@ internal partial class CSharpUseRangeOperatorDiagnosticAnalyzer ...@@ -12,7 +12,7 @@ internal partial class CSharpUseRangeOperatorDiagnosticAnalyzer
{ {
public enum ResultKind public enum ResultKind
{ {
// like s.Substring(expr, s.Length - expr). 'expr' has to match on both sides. // like s.Substring(expr, s.Length - expr) or s.Substring(expr). 'expr' has to match on both sides.
Computed, Computed,
// like s.Substring(constant1, s.Length - constant2). the constants don't have to match. // like s.Substring(constant1, s.Length - constant2). the constants don't have to match.
...@@ -28,6 +28,10 @@ public enum ResultKind ...@@ -28,6 +28,10 @@ public enum ResultKind
public readonly IMethodSymbol SliceLikeMethod; public readonly IMethodSymbol SliceLikeMethod;
public readonly MemberInfo MemberInfo; public readonly MemberInfo MemberInfo;
public readonly IOperation Op1; public readonly IOperation Op1;
/// <summary>
/// Can be null, if we are dealing with one-argument call to a slice-like method.
/// </summary>
public readonly IOperation Op2; public readonly IOperation Op2;
public Result( public Result(
......
...@@ -111,13 +111,54 @@ protected override void InitializeWorker(AnalysisContext context) ...@@ -111,13 +111,54 @@ protected override void InitializeWorker(AnalysisContext context)
} }
} }
// look for `s.Slice(e1, end - e2)` // look for `s.Slice(e1, end - e2)` or `s.Slice(e1)`
if (invocation.Instance is null || if (invocation.Instance is null)
invocation.Arguments.Length != 2)
{ {
return null; return null;
} }
return invocation.Arguments.Length switch
{
1 => AnalyzeInvocationWithOneArgument(invocation, infoCache, invocationSyntax, option),
2 => AnalyzeInvocationWithTwoArguments(invocation, infoCache, invocationSyntax, option),
_ => null,
};
}
private static Result? AnalyzeInvocationWithOneArgument(
IInvocationOperation invocation,
InfoCache infoCache,
InvocationExpressionSyntax invocationSyntax,
CodeStyleOption2<bool> option)
{
// See if the call is to something slice-like.
var targetMethod = invocation.TargetMethod;
// Try to see if we're calling into some sort of Slice method with a matching
// indexer or overload
if (!infoCache.TryGetMemberInfoOneArgument(targetMethod, out var memberInfo))
{
return null;
}
var startOperation = invocation.Arguments[0].Value;
return new Result(
ResultKind.Computed,
option,
invocation,
invocationSyntax,
targetMethod,
memberInfo,
startOperation,
null); // secondOperation is null: the range will run to the end.
}
private static Result? AnalyzeInvocationWithTwoArguments(
IInvocationOperation invocation,
InfoCache infoCache,
InvocationExpressionSyntax invocationSyntax,
CodeStyleOption2<bool> option)
{
// See if the call is to something slice-like. // See if the call is to something slice-like.
var targetMethod = invocation.TargetMethod; var targetMethod = invocation.TargetMethod;
......
...@@ -91,6 +91,21 @@ public static bool IsSliceLikeMethod(IMethodSymbol method) ...@@ -91,6 +91,21 @@ public static bool IsSliceLikeMethod(IMethodSymbol method)
IsSliceFirstParameter(method.OriginalDefinition.Parameters[0]) && IsSliceFirstParameter(method.OriginalDefinition.Parameters[0]) &&
IsSliceSecondParameter(method.OriginalDefinition.Parameters[1]); IsSliceSecondParameter(method.OriginalDefinition.Parameters[1]);
/// <summary>
/// Look for methods like "SomeType MyType.Slice(int start)". Note that the
/// name of the parameter is checked to ensure it is appropriate slice-like.
/// This name was picked by examining the patterns in the BCL for slicing members.
/// </summary>
public static bool IsSliceLikeMethodWithOneArgument(IMethodSymbol method)
=> method != null &&
IsPublicInstance(method) &&
method.Parameters.Length == 1 &&
// From: https://github.com/dotnet/csharplang/blob/master/proposals/csharp-8.0/ranges.md#decisions-made-during-implementation
//
// When looking for the pattern members, we look for original definitions, not
// constructed members
IsSliceFirstParameter(method.OriginalDefinition.Parameters[0]);
private static bool IsSliceFirstParameter(IParameterSymbol parameter) private static bool IsSliceFirstParameter(IParameterSymbol parameter)
=> parameter.Type.SpecialType == SpecialType.System_Int32 && => parameter.Type.SpecialType == SpecialType.System_Int32 &&
(parameter.Name == "start" || parameter.Name == "startIndex"); (parameter.Name == "start" || parameter.Name == "startIndex");
......
...@@ -150,15 +150,28 @@ private static RangeExpressionSyntax CreateComputedRange(Result result) ...@@ -150,15 +150,28 @@ private static RangeExpressionSyntax CreateComputedRange(Result result)
var startFromEnd = IsFromEnd(lengthLikeProperty, instance, ref startOperation); var startFromEnd = IsFromEnd(lengthLikeProperty, instance, ref startOperation);
var startExpr = (ExpressionSyntax)startOperation.Syntax; var startExpr = (ExpressionSyntax)startOperation.Syntax;
// Similarly, if our end-op is actually equivalent to `expr.Length - val`, then just var endFromEnd = false;
// change our end-op to be `val` and record that we should emit it as `^val`. ExpressionSyntax endExpr;
var endFromEnd = IsFromEnd(lengthLikeProperty, instance, ref endOperation);
var endExpr = (ExpressionSyntax)endOperation.Syntax; if (!(endOperation is null))
{
// If the range operation goes to 'expr.Length' then we can just leave off the end part // We need to do the same for the second argument, since it's present.
// of the range. i.e. `start..` // Similarly, if our end-op is actually equivalent to `expr.Length - val`, then just
if (IsInstanceLengthCheck(lengthLikeProperty, instance, endOperation)) // 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, instance, endOperation))
{
endExpr = null;
}
}
else
{ {
// We are dealing with one-argument case, so we should leave off
// the end part of the range: `start..`.
endExpr = null; endExpr = null;
} }
......
...@@ -204,6 +204,96 @@ void Goo(string s, int bar, int baz) ...@@ -204,6 +204,96 @@ void Goo(string s, int bar, int baz)
}.RunAsync(); }.RunAsync();
} }
[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseRangeOperator)]
public async Task TestSubstringOneArgument()
{
var source =
@"
class C
{
void Goo(string s)
{
var v = s.Substring([|1|]);
}
}";
var fixedSource =
@"
class C
{
void Goo(string s)
{
var v = s[1..];
}
}";
await new VerifyCS.Test
{
ReferenceAssemblies = ReferenceAssemblies.NetCore.NetCoreApp31,
TestCode = source,
FixedCode = fixedSource,
}.RunAsync();
}
[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseRangeOperator)]
public async Task TestSliceOneArgument()
{
var source =
@"
class C
{
void Goo(Span<int> s)
{
var v = s.Slice([|1|]);
}
}";
var fixedSource =
@"
class C
{
void Goo(Span<int> s)
{
var v = s[1..];
}
}";
await new VerifyCS.Test
{
ReferenceAssemblies = ReferenceAssemblies.NetCore.NetCoreApp31,
TestCode = source,
FixedCode = fixedSource,
}.RunAsync();
}
[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseRangeOperator)]
public async Task TestExpressionOneArgument()
{
var source =
@"
class C
{
void Goo(string s, int bar)
{
var v = s.Substring([|bar|]);
}
}";
var fixedSource =
@"
class C
{
void Goo(string s, int bar)
{
var v = s[bar..];
}
}";
await new VerifyCS.Test
{
ReferenceAssemblies = ReferenceAssemblies.NetCore.NetCoreApp31,
TestCode = source,
FixedCode = fixedSource,
}.RunAsync();
}
[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseRangeOperator)] [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseRangeOperator)]
public async Task TestConstantSubtraction1() public async Task TestConstantSubtraction1()
{ {
...@@ -416,6 +506,38 @@ void Goo(string s, string t) ...@@ -416,6 +506,38 @@ void Goo(string s, string t)
}.RunAsync(); }.RunAsync();
} }
[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseRangeOperator)]
public async Task TestFixAllInvocationToElementAccess2()
{
// Note: once the IOp tree has support for range operators, this should
// simplify even further.
var source =
@"
class C
{
void Goo(string s, string t)
{
var v = t.Substring([|s.Substring([|1|])[0]|]);
}
}";
var fixedSource =
@"
class C
{
void Goo(string s, string t)
{
var v = t[s[1..][0]..];
}
}";
await new VerifyCS.Test
{
ReferenceAssemblies = ReferenceAssemblies.NetCore.NetCoreApp31,
TestCode = source,
FixedCode = fixedSource,
}.RunAsync();
}
[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseRangeOperator)] [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseRangeOperator)]
public async Task TestWithTypeWithActualSliceMethod1() public async Task TestWithTypeWithActualSliceMethod1()
{ {
...@@ -479,5 +601,37 @@ void Goo(Span<int> s) ...@@ -479,5 +601,37 @@ void Goo(Span<int> s)
FixedCode = fixedSource, FixedCode = fixedSource,
}.RunAsync(); }.RunAsync();
} }
[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseRangeOperator)]
public async Task TestWithTypeWithActualSliceMethod3()
{
var source =
@"
using System;
class C
{
void Goo(Span<int> s)
{
var v = s.Slice([|1|]);
}
}";
var fixedSource =
@"
using System;
class C
{
void Goo(Span<int> s)
{
var v = s[1..];
}
}";
await new VerifyCS.Test
{
ReferenceAssemblies = ReferenceAssemblies.NetCore.NetCoreApp31,
TestCode = source,
FixedCode = fixedSource,
}.RunAsync();
}
} }
} }
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册