提交 438150c3 编写于 作者: C Cyrus Najmabadi

Add docs.

上级 ee4f1639
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections;
using System.Collections.Concurrent;
using System.Linq;
namespace Microsoft.CodeAnalysis.CSharp.UseIndexOperator
{
using static Helpers;
internal partial class CSharpUseIndexOperatorDiagnosticAnalyzer
{
/// <summary>
/// Helper type to cache information about types while analyzing the compilation.
/// </summary>
private class TypeChecker
{
/// <summary>
/// The System.Index type. Needed so that we only fixup code if we see the type
/// we're using has an indexer that takes an Index.
/// </summary>
private readonly INamedTypeSymbol _indexType;
private readonly ConcurrentDictionary<INamedTypeSymbol, IPropertySymbol> _typeToLengthOrCountProperty;
......@@ -19,6 +26,9 @@ public TypeChecker(Compilation compilation)
_typeToLengthOrCountProperty = new ConcurrentDictionary<INamedTypeSymbol, IPropertySymbol>();
// Always allow using System.Index indexers with System.String. The compiler has
// hard-coded knowledge on how to use this type, even if there is no this[Index]
// indexer declared on it directly.
var stringType = compilation.GetSpecialType(SpecialType.System_String);
_typeToLengthOrCountProperty[stringType] = Initialize(stringType, requireIndexer: false);
}
......@@ -28,15 +38,20 @@ public IPropertySymbol GetLengthOrCountProperty(INamedTypeSymbol namedType)
private IPropertySymbol Initialize(INamedTypeSymbol namedType, bool requireIndexer)
{
var lengthOrCountProperty = Helpers.GetLengthOrCountProperty(namedType);
// Check that the type has an int32 'Length' or 'Count' property. If not, we don't
// consider it something indexable.
var lengthOrCountProperty = GetLengthOrCountProperty(namedType);
if (lengthOrCountProperty == null)
{
return null;
}
// if we require an indexer, make sure this type has a this[Index] indexer. If not,
// return 'null' so we'll consider this named-type non-viable. Otherwise, return the
// lengthOrCount property, marking this type as viable.
if (requireIndexer)
{
var indexer = Helpers.GetIndexer(namedType, _indexType);
var indexer = GetIndexer(namedType, _indexType);
if (indexer == null)
{
return null;
......
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Collections.Immutable;
using System.Composition;
using Microsoft.CodeAnalysis.CodeStyle;
......@@ -12,6 +10,16 @@
namespace Microsoft.CodeAnalysis.CSharp.UseIndexOperator
{
/// <summary>
/// Analyzer that looks for code like `s[s.Length - n]` and offers to change that to `s[^n]`. In
/// order to do this, the type must look 'indexable'. Meaning, it must have an int-returning
/// property called 'Length' or 'Count', and it must have both an int-indexer, and a
/// System.Index indexer.
///
/// It is assumed that if the type follows this shape that it is well behaved and that this
/// transformation will preserve semantics. If this assumption is not good in practice, we
/// could always limit the feature to only work on a whitelist of known safe types.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp), Shared]
internal partial class CSharpUseIndexOperatorDiagnosticAnalyzer : AbstractCodeStyleDiagnosticAnalyzer
{
......@@ -26,6 +34,9 @@ protected override void InitializeWorker(AnalysisContext context)
{
context.RegisterCompilationStartAction(startContext =>
{
// We're going to be checking every property-reference in the compilation. Cache
// information we compute in this TypeChecker object so we don't have to continually
// recompute it.
var typeChecker = new TypeChecker(startContext.Compilation);
context.RegisterOperationAction(
c => AnalyzePropertyReference(c, typeChecker),
......@@ -40,25 +51,28 @@ protected override void InitializeWorker(AnalysisContext context)
var propertyReference = (IPropertyReferenceOperation)context.Operation;
var property = propertyReference.Property;
// Only analyze indexer calls.
if (!property.IsIndexer)
{
return;
}
// Make sure we're actually on something like `s[...]`.
var elementAccess = propertyReference.Syntax as ElementAccessExpressionSyntax;
if (elementAccess == null)
{
return;
}
// Only supported on C# 8 and above.
var syntaxTree = elementAccess.SyntaxTree;
var parseOptions = (CSharpParseOptions)syntaxTree.Options;
//if (parseOptions.LanguageVersion < LanguageVersion.CSharp8)
//{
// return;
//}
// Don't bother analyzing if the user doesn't like using Index/Range operators.
var optionSet = context.Options.GetDocumentOptionSetAsync(syntaxTree, cancellationToken).GetAwaiter().GetResult();
if (optionSet == null)
{
......@@ -87,16 +101,16 @@ protected override void InitializeWorker(AnalysisContext context)
return;
}
// look for `s[s.Length - index.Value]` and convert to `s[^index]`
// look for `s[s.Length - value]` and convert to `s[^val]`
// Needs to have the one arg for `[s.Length - index.Value]`
// Needs to have the one arg for `[s.Length - value]`
if (propertyReference.Instance is null ||
propertyReference.Arguments.Length != 1)
{
return;
}
// Arg needs to be a subtraction for: `s.Length - index.Value`
// Arg needs to be a subtraction for: `s.Length - value`
var arg = propertyReference.Arguments[0];
if (!(arg.Value is IBinaryOperation binaryOperation) ||
binaryOperation.OperatorKind != BinaryOperatorKind.Subtract)
......@@ -104,8 +118,7 @@ protected override void InitializeWorker(AnalysisContext context)
return;
}
// Left side of the subtraction needs to be `s.Length` or `s.Count`. First make
// sure we're referencing String.Length.
// Left side of the subtraction needs to be `s.Length`
if (!(binaryOperation.LeftOperand is IPropertyReferenceOperation leftPropertyRef) ||
leftPropertyRef.Instance is null ||
!lengthOrCountProp.Equals(leftPropertyRef.Property))
......@@ -124,6 +137,8 @@ protected override void InitializeWorker(AnalysisContext context)
return;
}
// Found a match. Report a diagnostic on the element-access, and also record the
// location of 'val'. That will make it easy to convert things to be `s[^val]` easily.
var additionalLocations = ImmutableArray.Create(
binaryOperation.RightOperand.Syntax.GetLocation());
......
......@@ -9,8 +9,15 @@ namespace Microsoft.CodeAnalysis.CSharp.UseIndexOperator
internal partial class CSharpUseRangeOperatorDiagnosticAnalyzer
{
/// <summary>
/// Helper type to cache information about types while analyzing the compilation.
/// </summary>
private class TypeChecker
{
/// <summary>
/// The System.Range type. Needed so that we only fixup code if we see the type
/// we're using has an indexer that takes a Range.
/// </summary>
private readonly INamedTypeSymbol _rangeType;
private readonly ConcurrentDictionary<INamedTypeSymbol, MemberInfo> _typeToMemberInfo;
......@@ -20,17 +27,18 @@ public TypeChecker(Compilation compilation)
_typeToMemberInfo = new ConcurrentDictionary<INamedTypeSymbol, MemberInfo>();
// Always allow using System.Range indexers with System.String. The compiler has
// hard-coded knowledge on how to use this type, even if there is no this[Range]
// indexer declared on it directly.
var stringType = compilation.GetSpecialType(SpecialType.System_String);
_typeToMemberInfo[stringType] = Initialize(stringType, requireIndexer: false);
}
private IMethodSymbol GetSliceLikeMethod(INamedTypeSymbol namedType)
{
return namedType.GetMembers()
.OfType<IMethodSymbol>()
.Where(m => IsSliceLikeMethod(m))
.FirstOrDefault();
}
=> namedType.GetMembers()
.OfType<IMethodSymbol>()
.Where(m => IsSliceLikeMethod(m))
.FirstOrDefault();
public bool TryGetMemberInfo(INamedTypeSymbol namedType, out MemberInfo memberInfo)
{
......@@ -40,18 +48,24 @@ public bool TryGetMemberInfo(INamedTypeSymbol namedType, out MemberInfo memberIn
private MemberInfo Initialize(INamedTypeSymbol namedType, bool requireIndexer)
{
// Check that the type has an int32 'Length' or 'Count' property. If not, we don't
// consider it something indexable.
var lengthOrCountProp = GetLengthOrCountProperty(namedType);
if (lengthOrCountProp == null)
{
return default;
}
// Look for something that appears to be a Slice method. If we can't find one, then
// this definitely isn't a type we can update code to use an indexer for.
var sliceLikeMethod = GetSliceLikeMethod(namedType);
if (sliceLikeMethod == null)
{
return default;
}
// if we require an indexer, make sure this type has a this[Range] indexer. If not,
// return 'default' so we'll consider this named-type non-viable.
if (requireIndexer)
{
var indexer = GetIndexer(namedType, _rangeType);
......@@ -61,6 +75,7 @@ private MemberInfo Initialize(INamedTypeSymbol namedType, bool requireIndexer)
}
// The "this[Range]" indexer has to return the same type as the Slice method.
// If not, this type isn't one that matches the pattern we're looking for.
if (!indexer.Type.Equals(sliceLikeMethod.ReturnType))
{
return default;
......
......@@ -9,8 +9,20 @@
namespace Microsoft.CodeAnalysis.CSharp.UseIndexOperator
{
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using static Helpers;
/// <summary>
/// Analyzer that looks for several variants of code like `s.Slice(start, end - start)` and
/// offers to update to `s[start..end]`. In order to convert, the type being called on needs a
/// slice-like method that takes two ints, and returns an instance of the same type. It also
/// needs a Length/Count property, as well as an indexer that takes a System.Range instance.
///
/// It is assumed that if the type follows this shape that it is well behaved and that this
/// transformation will preserve semantics. If this assumption is not good in practice, we
/// could always limit the feature to only work on a whitelist of known safe types.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp), Shared]
internal partial class CSharpUseRangeOperatorDiagnosticAnalyzer : AbstractCodeStyleDiagnosticAnalyzer
{
......@@ -27,7 +39,7 @@ public CSharpUseRangeOperatorDiagnosticAnalyzer()
}
/// <summary>
/// Look for methods like "ContainingType Slice(int start, int length)"
/// Look for methods like "ContainingType Slice(int start, int length)".
/// </summary>
private static bool IsSliceLikeMethod(IMethodSymbol method)
=> IsPublicInstance(method) &&
......@@ -53,24 +65,14 @@ protected override void InitializeWorker(AnalysisContext context)
var cancellationToken = context.CancellationToken;
var invocation = (IInvocationOperation)context.Operation;
var invocationSyntax = invocation.Syntax;
if (invocationSyntax is null || invocationSyntax.Kind() != SyntaxKind.InvocationExpression)
{
return;
}
var targetMethod = invocation.TargetMethod;
if (!IsSliceLikeMethod(invocation.TargetMethod))
{
return;
}
if (!typeChecker.TryGetMemberInfo(targetMethod.ContainingType, out var memberInfo) ||
!targetMethod.Equals(memberInfo.SliceLikeMethod))
var invocationSyntax = invocation.Syntax as InvocationExpressionSyntax;
if (invocationSyntax is null ||
invocationSyntax.ArgumentList is 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)
......@@ -90,7 +92,22 @@ protected override void InitializeWorker(AnalysisContext context)
return;
}
// look for `s.SliceMethod(start, end - start)` and convert to `s[Range]`
// See if the call is to something slice-like.
var targetMethod = invocation.TargetMethod;
if (!IsSliceLikeMethod(invocation.TargetMethod))
{
return;
}
// Use the type checker to see if this is a type we can use range-indexer for, and also
// if this is a call to the Slice-Like method we've found for that type.
if (!typeChecker.TryGetMemberInfo(targetMethod.ContainingType, out var memberInfo) ||
!targetMethod.Equals(memberInfo.SliceLikeMethod))
{
return;
}
// look for `s.Slice(start, end - start)` and convert to `s[Range]`
// Needs to have the two args for `start` and `end - start`
if (invocation.Instance is null ||
......@@ -108,38 +125,49 @@ protected override void InitializeWorker(AnalysisContext context)
return;
}
var arg1 = invocation.Arguments[0];
var arg1Syntax = arg1.Value.Syntax;
var subtractRightSyntax = binaryOperation.RightOperand.Syntax;
var startOperation = invocation.Arguments[0].Value;
var startSyntax = startOperation.Syntax;
// Make sure we have: (start, end - start). The start operation has to be
// the same as the right side of the subtraction.
var syntaxFacts = CSharpSyntaxFactsService.Instance;
if (!syntaxFacts.AreEquivalent(arg1Syntax, subtractRightSyntax))
if (!syntaxFacts.AreEquivalent(startSyntax, binaryOperation.RightOperand.Syntax))
{
return;
}
var startOperation = arg1.Value;
// The end operation is the left side of `end - start`
var endOperation = binaryOperation.LeftOperand;
// var start = range.Start.FromEnd ? array.Length - range.Start.Value : range.Start.Value;
// var end = range.End.FromEnd ? array.Length - range.End.Value : range.End.Value;
// 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`. Look for these patterns and record what we have so we can produce more
// idiomatic results in the fixer.
//
// Note: we could also compute this in the fixer. But it's nice and easy to do here
// given that we already have the options, and it's cheap to do now.
var properties = ImmutableDictionary<string, string>.Empty;
var lengthOrCountProp = memberInfo.LengthOrCountProperty;
// 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`.
if (IsFromEnd(lengthOrCountProp, invocation.Instance, ref startOperation))
{
properties = properties.Add(StartFromEnd, StartFromEnd);
}
// 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`.
if (IsFromEnd(lengthOrCountProp, invocation.Instance, ref endOperation))
{
properties = properties.Add(EndFromEnd, EndFromEnd);
}
// If the range operation goes to 'instance.Length' then we can just leave off the end
// part of the range. i.e. `start..`
// 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(lengthOrCountProp, invocation.Instance, endOperation))
{
properties = properties.Add(OmitEnd, OmitEnd);
......@@ -153,24 +181,35 @@ protected override void InitializeWorker(AnalysisContext context)
properties = properties.Add(OmitStart, OmitStart);
}
// Keep track of the syntax nodes from the start/end ops so that we can easily
// generate the range-expression in the fixer.
var additionalLocations = ImmutableArray.Create(
startOperation.Syntax.GetLocation(),
endOperation.Syntax.GetLocation());
// Mark the span under the two arguments to .Slice(..., ...) as what we will be
// updating.
var arguments = invocationSyntax.ArgumentList.Arguments;
var location = Location.Create(syntaxTree,
TextSpan.FromBounds(arguments.First().SpanStart, arguments.Last().Span.End));
context.ReportDiagnostic(
DiagnosticHelper.Create(
Descriptor,
invocationSyntax.GetLocation(),
location,
option.Notification.Severity,
additionalLocations,
properties,
memberInfo.SliceLikeMethod.Name));
}
/// <summary>
/// check if its the form: `expr.Length - value`. If so, update rangeOperation to then
/// point to 'value'.
/// </summary>
private bool IsFromEnd(
IPropertySymbol lengthOrCountProp, IOperation instance, ref IOperation rangeOperation)
{
// check if its the form: `stringExpr.Length - value`
if (rangeOperation is IBinaryOperation binaryOperation &&
binaryOperation.OperatorKind == BinaryOperatorKind.Subtract &&
IsInstanceLengthCheck(lengthOrCountProp, instance, binaryOperation.LeftOperand))
......@@ -184,7 +223,7 @@ protected override void InitializeWorker(AnalysisContext context)
/// <summary>
/// Checks if this is an expression `expr.Length` where `expr` is equivalent to
/// the instance we were calling .Substring off of.
/// the instance we were calling .Slice off of.
/// </summary>
private bool IsInstanceLengthCheck(
IPropertySymbol lengthOrCountProp, IOperation instance, IOperation operation)
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册