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
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
Expand Down Expand Up @@ -62,15 +61,43 @@ protected override SyntaxNode GetDeclaratorInitializer(SyntaxNode syntax)
// new[] { 'a', 'b', 'c' } => "abc"
// new[] { (byte)'a', (byte)'b', (byte)'c' } => "abc"u8
// "abc".ToCharArray() => "abc"
// ['a', 'b', 'c'] => "abc"
// [(byte)'a', (byte)'b', (byte)'c'] => "abc"u8
protected override SyntaxNode? TryReplaceArrayCreationWithInlineLiteralExpression(IOperation operation)
{
if (operation is IConversionOperation conversion)
{
operation = conversion.Operand;
}

if (operation is IArrayCreationOperation arrayCreation &&
arrayCreation.GetElementType() is { } elementType)
if (operation is IInvocationOperation invocation)
{
if (UseSearchValuesAnalyzer.IsConstantStringToCharArrayInvocation(invocation, out _))
{
Debug.Assert(invocation.Instance is not null);
return invocation.Instance!.Syntax;
}

return null;
}

ITypeSymbol? elementType = null;

if (operation.Type is IArrayTypeSymbol arrayType)
{
elementType = arrayType.ElementType;
}
else if (operation.Type is INamedTypeSymbol namedType)
{
if (namedType.TypeArguments is [var typeArgument])
{
Debug.Assert(namedType.Name.Contains("Span", StringComparison.Ordinal), namedType.Name);

elementType = typeArgument;
}
}

if (elementType is not null)
{
bool isByte = elementType.SpecialType == SpecialType.System_Byte;

Expand All @@ -84,7 +111,7 @@ protected override SyntaxNode GetDeclaratorInitializer(SyntaxNode syntax)

List<char> values = new();

if (arrayCreation.Syntax is ExpressionSyntax creationSyntax &&
if (operation.Syntax is ExpressionSyntax creationSyntax &&
CSharpUseSearchValuesAnalyzer.IsConstantByteOrCharArrayCreationExpression(operation.SemanticModel!, creationSyntax, values, out _) &&
values.Count <= 128 && // Arbitrary limit to avoid emitting huge literals
!ContainsAnyComments(creationSyntax)) // Avoid removing potentially valuable comments
Expand Down Expand Up @@ -118,14 +145,6 @@ protected override SyntaxNode GetDeclaratorInitializer(SyntaxNode syntax)
trailing: default));
}
}
else if (operation is IInvocationOperation invocation)
{
if (UseSearchValuesAnalyzer.IsConstantStringToCharArrayInvocation(invocation, out _))
{
Debug.Assert(invocation.Instance is not null);
return invocation.Instance!.Syntax;
}
}

return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public sealed class CSharpUseSearchValuesAnalyzer : UseSearchValuesAnalyzer
// char[] myField = "abc".ToCharArray();
// char[] myField = ConstString.ToCharArray();
// byte[] myField = new[] { (byte)'a', (byte)'b', (byte)'c' };
// char[] myField = ['a', 'b', 'c'];
// byte[] myField = [(byte)'a', (byte)'b', (byte)'c'];
protected override bool IsConstantByteOrCharArrayVariableDeclaratorSyntax(SemanticModel semanticModel, SyntaxNode syntax, out int length)
{
length = 0;
Expand All @@ -37,14 +39,18 @@ syntax is VariableDeclaratorSyntax variableDeclarator &&
// ReadOnlySpan<byte> myProperty => "abc"u8;
// ReadOnlySpan<byte> myProperty { get => "abc"u8; }
// ReadOnlySpan<byte> myProperty { get { return "abc"u8; } }
// ReadOnlySpan<char> myProperty => ['a', 'b', 'c'];
// ReadOnlySpan<byte> myProperty => [(byte)'a', (byte)'b', (byte)'c'];
protected override bool IsConstantByteOrCharReadOnlySpanPropertyDeclarationSyntax(SemanticModel semanticModel, SyntaxNode syntax, out int length)
{
length = 0;

return
syntax is PropertyDeclarationSyntax propertyDeclaration &&
TryGetPropertyGetterExpression(propertyDeclaration) is { } expression &&
(IsConstantByteOrCharArrayCreationExpression(semanticModel, expression, values: null, out length) || IsUtf8StringLiteralExpression(expression, out length));
(IsConstantByteOrCharArrayCreationExpression(semanticModel, expression, values: null, out length) ||
IsUtf8StringLiteralExpression(expression, out length) ||
(semanticModel.GetOperation(expression) is { } operation && IsConstantByteOrCharCollectionExpression(operation, values: null, out length)));
}

protected override bool IsConstantByteOrCharArrayCreationSyntax(SemanticModel semanticModel, SyntaxNode syntax, out int length)
Expand Down Expand Up @@ -106,6 +112,12 @@ internal static bool IsConstantByteOrCharArrayCreationExpression(SemanticModel s
return true;
}
}
else
{
return
semanticModel.GetOperation(expression) is { } operation &&
IsConstantByteOrCharCollectionExpression(operation, values, out length);
}

if (arrayInitializer?.Expressions is { } valueExpressions)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
Expand Down Expand Up @@ -183,6 +184,12 @@ private bool AreConstantValuesWorthReplacing(IOperation argument, INamedTypeSymb
length >= MinLengthWorthReplacing ||
conversion.Operand is IInvocationOperation;
}
else if (IsConstantByteOrCharCollectionExpression(conversion.Operand, values: null, out length))
{
// text.IndexOfAny(['a', 'b', 'c'])
// text.IndexOfAny([(byte)'a', (byte)'b', (byte)'c'])
return length >= MinLengthWorthReplacing;
}
}
else if (argument.Kind == OperationKindEx.Utf8String)
{
Expand Down Expand Up @@ -310,5 +317,49 @@ operation is ILiteralOperation or IFieldReferenceOperation or ILocalReferenceOpe
value = null;
return false;
}

internal static bool IsConstantByteOrCharCollectionExpression(IOperation operation, List<char>? values, out int length)
{
if (operation.Kind == OperationKindEx.CollectionExpression &&
ICollectionExpressionOperationWrapper.IsInstance(operation) &&
ICollectionExpressionOperationWrapper.FromOperation(operation) is { } collection &&
AllElementsAreConstantByteOrCharLiterals(collection.Elements, values))
{
length = collection.Elements.Length;
return true;
}

length = 0;
return false;

static bool AllElementsAreConstantByteOrCharLiterals(ImmutableArray<IOperation> elements, List<char>? values)
{
foreach (IOperation element in elements)
{
IOperation operation = element;

if (operation is IConversionOperation conversion)
{
if (operation.Type is not { SpecialType: SpecialType.System_Byte })
{
return false;
}

operation = conversion.Operand;
}

if (operation.Type is not { SpecialType: SpecialType.System_Char } ||
operation is not ILiteralOperation literal ||
literal.ConstantValue.Value is not char charValue)
{
return false;
}

values?.Add(charValue);
}

return true;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,48 @@ private void TestMethod(ReadOnlySpan<byte> bytes)
""");
}

[Fact]
public async Task TestCollectionExpressionAnalyzer()
{
await VerifyAnalyzerAsync(LanguageVersion.CSharp12,
"""
using System;

internal sealed class Test
{
private static readonly char[] ShortStaticReadonlyCharArrayField = ['a', 'e', 'i', 'o', 'u'];
private static readonly char[] LongStaticReadonlyCharArrayField = ['a', 'e', 'i', 'o', 'u', 'A'];
private ReadOnlySpan<char> ShortReadOnlySpanOfCharRVAProperty => ['a', 'e', 'i', 'o', 'u'];
private ReadOnlySpan<char> LongReadOnlySpanOfCharRVAProperty => ['a', 'e', 'i', 'o', 'u', 'A'];
private static readonly byte[] ShortStaticReadonlyByteArrayField = [(byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u'];
private static readonly byte[] LongStaticReadonlyByteArrayField = [(byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)'A'];
private ReadOnlySpan<byte> ShortReadOnlySpanOfByteRVAProperty => [(byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u'];
private ReadOnlySpan<byte> LongReadOnlySpanOfByteRVAProperty => [(byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)'A'];

private void TestMethod(ReadOnlySpan<char> chars, ReadOnlySpan<byte> bytes)
{
_ = chars.IndexOfAny([]);
_ = chars.IndexOfAny(['a', 'e', 'i', 'o', 'u']);
_ = chars.IndexOfAny([|['a', 'e', 'i', 'o', 'u', 'A']|]);

_ = chars.IndexOfAny(ShortStaticReadonlyCharArrayField);
_ = chars.IndexOfAny([|LongStaticReadonlyCharArrayField|]);
_ = chars.IndexOfAny(ShortReadOnlySpanOfCharRVAProperty);
_ = chars.IndexOfAny([|LongReadOnlySpanOfCharRVAProperty|]);

_ = bytes.IndexOfAny([]);
_ = bytes.IndexOfAny([(byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u']);
_ = bytes.IndexOfAny([|[(byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)'A']|]);

_ = bytes.IndexOfAny(ShortStaticReadonlyByteArrayField);
_ = bytes.IndexOfAny([|LongStaticReadonlyByteArrayField|]);
_ = bytes.IndexOfAny(ShortReadOnlySpanOfByteRVAProperty);
_ = bytes.IndexOfAny([|LongReadOnlySpanOfByteRVAProperty|]);
}
}
""");
}

public static IEnumerable<object[]> TestAllIndexOfAnyAndContainsAnySpanOverloads_MemberData()
{
return
Expand Down Expand Up @@ -330,19 +372,25 @@ private void TestMethod(string input)
[InlineData("static readonly char[]", "= new[] { 'a', 'e', 'i', 'o', 'u', 'A' };", false)]
[InlineData("static readonly byte[]", "= new[] { (byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)'A' };", false)]
[InlineData("readonly char[]", "= new[] { 'a', 'e', 'i', 'o', 'u', 'A' };", false)]
[InlineData("readonly char[]", "= ['a', 'e', 'i', 'o', 'u', 'A'];", false)]
[InlineData("readonly byte[]", "= new[] { (byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)'A' };", false)]
[InlineData("readonly byte[]", "= [(byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)'A'];", false)]
[InlineData("readonly char[]", "= new char[] { 'a', 'e', 'i', 'o', 'u', 'A' };", false)]
[InlineData("readonly char[]", "= new char[] { 'a', 'e', 'i', 'o', 'u', 'A' };", false)]
[InlineData("ReadOnlySpan<char>", "=> new[] { 'a', 'e', 'i', 'o', 'u', 'A' };", false)]
[InlineData("ReadOnlySpan<char>", "=> ['a', 'e', 'i', 'o', 'u', 'A'];", false)]
[InlineData("ReadOnlySpan<byte>", "=> new[] { (byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)'A' };", false)]
[InlineData("ReadOnlySpan<byte>", "=> [(byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)'A'];", false)]
[InlineData("ReadOnlySpan<byte>", "=> \"aeiouA\"u8;", false)]
[InlineData("static ReadOnlySpan<char>", "=> new[] { 'a', 'e', 'i', 'o', 'u', 'A' };", true)]
[InlineData("static ReadOnlySpan<byte>", "=> new[] { (byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)'A' };", true)]
[InlineData("static ReadOnlySpan<byte>", "=> \"aeiouA\"u8;", true)]
[InlineData("ReadOnlySpan<byte>", "{ get => \"aeiouA\"u8; }", false)]
[InlineData("ReadOnlySpan<byte>", "{ get { return \"aeiouA\"u8; } }", false)]
[InlineData("ReadOnlySpan<char>", "{ get => new[] { 'a', 'e', 'i', 'o', 'u', 'A' }; }", false)]
[InlineData("ReadOnlySpan<char>", "{ get => ['a', 'e', 'i', 'o', 'u', 'A']; }", false)]
[InlineData("ReadOnlySpan<byte>", "{ get { return new[] { (byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)'A' }; } }", false)]
[InlineData("ReadOnlySpan<byte>", "{ get { return [(byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)'A']; } }", false)]
[InlineData("static ReadOnlySpan<byte>", "{ get => \"aeiouA\"u8; }", true)]
[InlineData("static ReadOnlySpan<byte>", "{ get { return \"aeiouA\"u8; } }", true)]
[InlineData("static ReadOnlySpan<char>", "{ get => new[] { 'a', 'e', 'i', 'o', 'u', 'A' }; }", true)]
Expand Down Expand Up @@ -395,6 +443,7 @@ public async Task TestCodeFixerNamedArguments(string modifiersAndType, string in

await TestAsync(LanguageVersion.CSharp7_3, createExpression);
await TestAsync(LanguageVersion.CSharp11, cSharp11CreateExpression ?? createExpression);
await TestAsync(LanguageVersion.CSharp12, cSharp11CreateExpression ?? createExpression);

async Task TestAsync(LanguageVersion languageVersion, string expectedCreateExpression)
{
Expand All @@ -405,6 +454,13 @@ async Task TestAsync(LanguageVersion languageVersion, string expectedCreateExpre
return;
}

if (languageVersion < LanguageVersion.CSharp12 &&
memberDefinition.LastIndexOf(']') - memberDefinition.IndexOf('[', StringComparison.Ordinal) > 1)
{
// Need CSharp 12 or newer to use collection expressions
return;
}

string source =
$$"""
using System;
Expand Down Expand Up @@ -616,6 +672,8 @@ private void TestMethod({{argumentType}} text)
[InlineData(LanguageVersion.CSharp7_3, "\"aeiouA\"", "\"aeiouA\"")]
[InlineData(LanguageVersion.CSharp7_3, "@\"aeiouA\"", "@\"aeiouA\"")]
[InlineData(LanguageVersion.CSharp11, "\"aeiouA\"u8", "\"aeiouA\"u8")]
[InlineData(LanguageVersion.CSharp12, "['a', 'e', 'i', 'o', 'u', 'A']", "\"aeiouA\"")]
[InlineData(LanguageVersion.CSharp12, "[(byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)'A']", "\"aeiouA\"u8")]
[InlineData(LanguageVersion.CSharp7_3, "new[] { 'a', 'e', 'i', 'o', 'u', 'A' }", "\"aeiouA\"")]
[InlineData(LanguageVersion.CSharp7_3, "new char[] { 'a', 'e', 'i', 'o', 'u', 'A' }", "\"aeiouA\"")]
[InlineData(LanguageVersion.CSharp7_3, "new char[] { 'a', 'e', 'i', 'o', 'u', 'A' }", "\"aeiouA\"")]
Expand Down
1 change: 1 addition & 0 deletions src/Utilities/Compiler/Analyzer.Utilities.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Extensions\WellKnownDiagnosticTagsExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Index.cs" />
<Compile Include="$(MSBuildThisFileDirectory)IsExternalInit.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Lightup\ICollectionExpressionOperationWrapper.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Lightup\IOperationWrapper.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Lightup\IFunctionPointerInvocationOperationWrapper.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Lightup\IUtf8StringOperationWrapper.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

#if HAS_IOPERATION

namespace Analyzer.Utilities.Lightup
{
using System;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;

[SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "Not a comparable instance.")]
internal readonly struct ICollectionExpressionOperationWrapper : IOperationWrapper
{
internal const string WrappedTypeName = "Microsoft.CodeAnalysis.Operations.ICollectionExpressionOperation";
private static readonly Type? WrappedType = OperationWrapperHelper.GetWrappedType(typeof(ICollectionExpressionOperationWrapper));

private static readonly Func<IOperation, ImmutableArray<IOperation>> ElementsAccessor = LightupHelpers.CreateOperationPropertyAccessor<IOperation, ImmutableArray<IOperation>>(WrappedType, nameof(Elements), fallbackResult: default);

private ICollectionExpressionOperationWrapper(IOperation operation)
{
WrappedOperation = operation;
}

public IOperation WrappedOperation { get; }
public ITypeSymbol? Type => WrappedOperation.Type;
public ImmutableArray<IOperation> Elements => ElementsAccessor(WrappedOperation);

public static ICollectionExpressionOperationWrapper FromOperation(IOperation operation)
{
if (operation == null)
{
return default;
}

if (!IsInstance(operation))
{
throw new InvalidCastException($"Cannot cast '{operation.GetType().FullName}' to '{WrappedTypeName}'");
}

return new ICollectionExpressionOperationWrapper(operation);
}

public static bool IsInstance(IOperation operation)
{
return operation != null && LightupHelpers.CanWrapOperation(operation, WrappedType);
}
}
}

#endif
3 changes: 2 additions & 1 deletion src/Utilities/Compiler/Lightup/OperationWrapperHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ internal static class OperationWrapperHelper

private static readonly ImmutableDictionary<Type, Type?> WrappedTypes = ImmutableDictionary.Create<Type, Type?>()
.Add(typeof(IFunctionPointerInvocationOperationWrapper), s_codeAnalysisAssembly.GetType(IFunctionPointerInvocationOperationWrapper.WrappedTypeName))
.Add(typeof(IUtf8StringOperationWrapper), s_codeAnalysisAssembly.GetType(IUtf8StringOperationWrapper.WrappedTypeName));
.Add(typeof(IUtf8StringOperationWrapper), s_codeAnalysisAssembly.GetType(IUtf8StringOperationWrapper.WrappedTypeName))
.Add(typeof(ICollectionExpressionOperationWrapper), s_codeAnalysisAssembly.GetType(ICollectionExpressionOperationWrapper.WrappedTypeName));

/// <summary>
/// Gets the type that is wrapped by the given wrapper.
Expand Down