diff --git a/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Runtime/CSharpPreferAsSpanOverSubstring.Fixer.cs b/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Runtime/CSharpPreferAsSpanOverSubstring.Fixer.cs new file mode 100644 index 0000000000..c461c904d9 --- /dev/null +++ b/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Runtime/CSharpPreferAsSpanOverSubstring.Fixer.cs @@ -0,0 +1,35 @@ +// 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.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.NetCore.Analyzers.Runtime; + +namespace Microsoft.NetCore.CSharp.Analyzers.Runtime +{ + [ExportCodeFixProvider(LanguageNames.CSharp)] + public sealed class CSharpPreferAsSpanOverSubstringFixer : PreferAsSpanOverSubstringFixer + { + private protected override void ReplaceNonConditionalInvocationMethodName(SyntaxEditor editor, SyntaxNode memberInvocation, string newName) + { + var cast = (InvocationExpressionSyntax)memberInvocation; + var memberAccessSyntax = (MemberAccessExpressionSyntax)cast.Expression; + var newNameSyntax = SyntaxFactory.IdentifierName(newName); + editor.ReplaceNode(memberAccessSyntax.Name, newNameSyntax); + } + + private protected override void ReplaceNamedArgumentName(SyntaxEditor editor, SyntaxNode invocation, string oldArgumentName, string newArgumentName) + { + var cast = (InvocationExpressionSyntax)invocation; + var oldNameSyntax = cast.ArgumentList.Arguments + .FirstOrDefault(x => x.NameColon is not null && x.NameColon.Name.Identifier.ValueText == oldArgumentName)?.NameColon.Name; + if (oldNameSyntax is null) + return; + var newNameSyntax = SyntaxFactory.IdentifierName(newArgumentName); + editor.ReplaceNode(oldNameSyntax, newNameSyntax); + } + } +} diff --git a/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md b/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md index a82b9f618e..52661da90e 100644 --- a/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md +++ b/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md @@ -12,6 +12,7 @@ CA1842 | Performance | Info | DoNotUseWhenAllOrWaitAllWithSingleArgument, [Docum CA1843 | Performance | Info | DoNotUseWhenAllOrWaitAllWithSingleArgument, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1843) CA1844 | Performance | Info | ProvideStreamMemoryBasedAsyncOverrides, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1844) CA1845 | Performance | Info | UseSpanBasedStringConcat, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1845) +CA1846 | Performance | Info | PreferAsSpanOverSubstring, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1846) CA2250 | Usage | Info | UseCancellationTokenThrowIfCancellationRequested, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2250) ### Removed Rules diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx index 723dba5b18..01e3b55deb 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx @@ -1534,6 +1534,18 @@ and all other platforms This call site is reachable on: 'windows' 10.0.2000 and later, and all other platforms + + 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost. + + + Prefer 'AsSpan' over 'Substring' when span-based overloads are available + + + Prefer 'AsSpan' over 'Substring' + + + Replace 'Substring' with 'AsSpan' + 'ThrowIfCancellationRequested' automatically checks whether the token has been canceled, and throws an 'OperationCanceledException' if it has. diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/PreferAsSpanOverSubstring.Fixer.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/PreferAsSpanOverSubstring.Fixer.cs new file mode 100644 index 0000000000..8d41a9ee4e --- /dev/null +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/PreferAsSpanOverSubstring.Fixer.cs @@ -0,0 +1,105 @@ +// 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.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Analyzer.Utilities.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Operations; +using RequiredSymbols = Microsoft.NetCore.Analyzers.Runtime.PreferAsSpanOverSubstring.RequiredSymbols; + +namespace Microsoft.NetCore.Analyzers.Runtime +{ + public abstract class PreferAsSpanOverSubstringFixer : CodeFixProvider + { + private const string SubstringStartIndexArgumentName = "startIndex"; + private const string AsSpanStartArgumentName = "start"; + + private protected abstract void ReplaceNonConditionalInvocationMethodName(SyntaxEditor editor, SyntaxNode memberInvocation, string newName); + + private protected abstract void ReplaceNamedArgumentName(SyntaxEditor editor, SyntaxNode invocation, string oldArgumentName, string newArgumentName); + + public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(PreferAsSpanOverSubstring.RuleId); + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var document = context.Document; + var token = context.CancellationToken; + SyntaxNode root = await document.GetSyntaxRootAsync(token).ConfigureAwait(false); + SemanticModel model = await document.GetSemanticModelAsync(token).ConfigureAwait(false); + var compilation = model.Compilation; + + if (!RequiredSymbols.TryGetSymbols(compilation, out RequiredSymbols symbols) || + root.FindNode(context.Span, getInnermostNodeForTie: true) is not SyntaxNode reportedNode || + model.GetOperation(reportedNode, token) is not IInvocationOperation reportedInvocation) + { + return; + } + + var bestCandidates = PreferAsSpanOverSubstring.GetBestSpanBasedOverloads(symbols, reportedInvocation, context.CancellationToken); + + // We only apply the fix if there is an unambiguous best overload. + if (bestCandidates.Length != 1) + return; + IMethodSymbol spanBasedOverload = bestCandidates[0]; + + string title = MicrosoftNetCoreAnalyzersResources.PreferAsSpanOverSubstringCodefixTitle; + var codeAction = CodeAction.Create(title, CreateChangedDocument, title); + context.RegisterCodeFix(codeAction, context.Diagnostics); + return; + + async Task CreateChangedDocument(CancellationToken token) + { + var editor = await DocumentEditor.CreateAsync(document, token).ConfigureAwait(false); + + foreach (var argument in reportedInvocation.Arguments) + { + IOperation value = argument.Value.WalkDownConversion(c => c.IsImplicit); + IParameterSymbol newParameter = spanBasedOverload.Parameters[argument.Parameter.Ordinal]; + + // Convert Substring invocations to equivalent AsSpan invocations. + if (symbols.IsAnySubstringInvocation(value) && SymbolEqualityComparer.Default.Equals(newParameter.Type, symbols.ReadOnlySpanOfCharType)) + { + ReplaceNonConditionalInvocationMethodName(editor, value.Syntax, nameof(MemoryExtensions.AsSpan)); + // Ensure named Substring arguments get renamed to their equivalent AsSpan counterparts. + ReplaceNamedArgumentName(editor, value.Syntax, SubstringStartIndexArgumentName, AsSpanStartArgumentName); + } + + // Ensure named arguments on the original overload are renamed to their + // ordinal counterparts on the new overload. + string oldArgumentName = argument.Parameter.Name; + string newArgumentName = newParameter.Name; + ReplaceNamedArgumentName(editor, reportedInvocation.Syntax, oldArgumentName, newArgumentName); + } + + // Import System namespace if necessary. + if (!IsMemoryExtensionsInScope(symbols, reportedInvocation)) + { + SyntaxNode withoutSystemImport = editor.GetChangedRoot(); + SyntaxNode systemNamespaceImportStatement = editor.Generator.NamespaceImportDeclaration(nameof(System)); + SyntaxNode withSystemImport = editor.Generator.AddNamespaceImports(withoutSystemImport, systemNamespaceImportStatement); + editor.ReplaceNode(editor.OriginalRoot, withSystemImport); + } + + return editor.GetChangedDocument(); + } + } + + public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + private static bool IsMemoryExtensionsInScope(in RequiredSymbols symbols, IInvocationOperation invocation) + { + var model = invocation.SemanticModel; + int position = invocation.Syntax.SpanStart; + const string name = nameof(MemoryExtensions); + + return model.LookupNamespacesAndTypes(position, name: name) + .Contains(symbols.MemoryExtensionsType, SymbolEqualityComparer.Default); + } + } +} diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/PreferAsSpanOverSubstring.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/PreferAsSpanOverSubstring.cs new file mode 100644 index 0000000000..f17f83968a --- /dev/null +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/PreferAsSpanOverSubstring.cs @@ -0,0 +1,298 @@ +// 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.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using Analyzer.Utilities; +using Analyzer.Utilities.Extensions; +using Analyzer.Utilities.PooledObjects; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using Resx = Microsoft.NetCore.Analyzers.MicrosoftNetCoreAnalyzersResources; + +namespace Microsoft.NetCore.Analyzers.Runtime +{ + /// + /// CA1842: Prefer 'AsSpan' over 'Substring'. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] + public sealed class PreferAsSpanOverSubstring : DiagnosticAnalyzer + { + internal const string RuleId = "CA1846"; + + private static readonly LocalizableString s_localizableTitle = new LocalizableResourceString(nameof(Resx.PreferAsSpanOverSubstringTitle), Resx.ResourceManager, typeof(Resx)); + private static readonly LocalizableString s_localizableMessage = new LocalizableResourceString(nameof(Resx.PreferAsSpanOverSubstringMessage), Resx.ResourceManager, typeof(Resx)); + private static readonly LocalizableString s_localizableDescription = new LocalizableResourceString(nameof(Resx.PreferAsSpanOverSubstringDescription), Resx.ResourceManager, typeof(Resx)); + + internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create( + RuleId, + s_localizableTitle, + s_localizableMessage, + DiagnosticCategory.Performance, + RuleLevel.IdeSuggestion, + s_localizableDescription, + isPortedFxCopRule: false, + isDataflowRule: false); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.RegisterCompilationStartAction(OnCompilationStart); + } + + private static void OnCompilationStart(CompilationStartAnalysisContext context) + { + if (!RequiredSymbols.TryGetSymbols(context.Compilation, out RequiredSymbols symbols)) + return; + + context.RegisterOperationBlockStartAction(OnOperationBlockStart); + return; + + // Local functions + + void OnOperationBlockStart(OperationBlockStartAnalysisContext context) + { + var invocations = PooledConcurrentSet.GetInstance(); + + context.RegisterOperationAction(context => + { + var argument = (IArgumentOperation)context.Operation; + if (symbols.IsAnySubstringInvocation(argument.Value.WalkDownConversion(c => c.IsImplicit)) && argument.Parent is IInvocationOperation invocation) + { + invocations.Add(invocation); + } + }, OperationKind.Argument); + + context.RegisterOperationBlockEndAction(context => + { + foreach (var invocation in invocations) + { + // We search for an overload of the invoked member whose signature matches the signature of + // the invoked member, except with ReadOnlySpan substituted in for some of the + // arguments that are Substring invocations. + if (!GetBestSpanBasedOverloads(symbols, invocation, context.CancellationToken).IsEmpty) + { + Diagnostic diagnostic = invocation.CreateDiagnostic(Rule); + context.ReportDiagnostic(diagnostic); + } + } + + invocations.Free(context.CancellationToken); + }); + } + } + + /// + /// Gets all the overloads that are tied for being the "best" span-based overload for the specified . + /// An overload is considered "better" if it allows more Substring invocations to be replaced with AsSpan invocations. + /// + /// If there are no overloads that replace any Substring calls, or none of the arguments in the invocation are + /// Substring calls, an empty array is returned. + /// + internal static ImmutableArray GetBestSpanBasedOverloads(in RequiredSymbols symbols, IInvocationOperation invocation, CancellationToken cancellationToken) + { + var method = invocation.TargetMethod; + + // Whether an argument at a particular parameter ordinal is a Substring call. + Span isSubstringLookup = stackalloc bool[method.Parameters.Length]; + int substringCalls = 0; + foreach (var argument in invocation.Arguments) + { + if (symbols.IsAnySubstringInvocation(argument.Value.WalkDownConversion(c => c.IsImplicit))) + { + isSubstringLookup[argument.Parameter.Ordinal] = true; + ++substringCalls; + } + } + + if (substringCalls == 0) + return ImmutableArray.Empty; + + // Find all overloads that are tied for being the "best" overload. An overload is considered + // "better" if it allows more Substring calls to be replaced with AsSpan calls. + var bestCandidates = ImmutableArray.CreateBuilder(); + int resultQuality = 0; + var candidates = GetAllAccessibleOverloadsAtInvocationCallSite(invocation, cancellationToken); + foreach (var candidate in candidates) + { + int quality = EvaluateCandidateQuality(symbols, isSubstringLookup, invocation, candidate); + + // Reject candidates that do not replace at least one Substring call. + if (quality < 1) + { + continue; + } + else if (quality == resultQuality) + { + bestCandidates.Add(candidate); + } + else if (quality > resultQuality) + { + resultQuality = quality; + bestCandidates.Clear(); + bestCandidates.Add(candidate); + } + } + + return bestCandidates.ToImmutable(); + + // Returns a number indicating how good the candidate method is. + // If the candidate is valid, the number of Substring calls that can be replaced with AsSpan calls is returned. + // If the candidate is invalid, -1 is returned. + static int EvaluateCandidateQuality(in RequiredSymbols symbols, ReadOnlySpan isSubstringLookup, IInvocationOperation invocation, IMethodSymbol candidate) + { + var method = invocation.TargetMethod; + + if (candidate.Parameters.Length != method.Parameters.Length) + return -1; + + int replacementCount = 0; + foreach (var parameter in candidate.Parameters) + { + if (isSubstringLookup[parameter.Ordinal] && SymbolEqualityComparer.Default.Equals(parameter.Type, symbols.ReadOnlySpanOfCharType)) + { + ++replacementCount; + continue; + } + + var oldParameter = method.Parameters[parameter.Ordinal]; + if (!SymbolEqualityComparer.Default.Equals(parameter.Type, oldParameter.Type)) + { + return -1; + } + } + + return replacementCount; + } + } + + private static IEnumerable GetAllAccessibleOverloadsAtInvocationCallSite(IInvocationOperation invocation, CancellationToken cancellationToken) + { + var method = invocation.TargetMethod; + var model = invocation.SemanticModel; + int location = invocation.Syntax.SpanStart; + var instance = invocation.Instance; + + IEnumerable allOverloads; + if (method.IsStatic) + { + allOverloads = model.LookupStaticMembers(location, method.ContainingType, method.Name).OfType(); + } + else if (instance is not null) + { + // Ensure protected members can only be invoked on instances that are known to be instances of the accessing class. + var enclosingType = GetEnclosingType(model, location, cancellationToken); + allOverloads = model.LookupSymbols(location, instance.Type, method.Name).OfType(); + if (instance.Type.DerivesFrom(enclosingType, baseTypesOnly: true) || instance is IInstanceReferenceOperation) + { + allOverloads = allOverloads.Union(model.LookupBaseMembers(location, method.Name).OfType()); + } + } + else + { + // This can happen when compiling invalid code. + return Enumerable.Empty(); + } + + return allOverloads.Where(x => x.IsStatic == method.IsStatic && SymbolEqualityComparer.Default.Equals(x.ReturnType, method.ReturnType)); + + static INamedTypeSymbol GetEnclosingType(SemanticModel model, int location, CancellationToken cancellationToken) + { + ISymbol symbol = model.GetEnclosingSymbol(location, cancellationToken); + if (symbol is not INamedTypeSymbol type) + type = symbol.ContainingType; + + return type; + } + } + + // Use struct to avoid allocations. +#pragma warning disable CA1815 // Override equals and operator equals on value types + internal readonly struct RequiredSymbols +#pragma warning restore CA1815 // Override equals and operator equals on value types + { + private RequiredSymbols( + INamedTypeSymbol stringType, INamedTypeSymbol roscharType, + INamedTypeSymbol memoryExtensionsType, + IMethodSymbol substring1, IMethodSymbol substring2, + IMethodSymbol asSpan1, IMethodSymbol asSpan2) + { + StringType = stringType; + ReadOnlySpanOfCharType = roscharType; + MemoryExtensionsType = memoryExtensionsType; + SubstringStart = substring1; + SubstringStartLength = substring2; + AsSpanStart = asSpan1; + AsSpanStartLength = asSpan2; + } + + public static bool TryGetSymbols(Compilation compilation, out RequiredSymbols symbols) + { + var stringType = compilation.GetSpecialType(SpecialType.System_String); + var charType = compilation.GetSpecialType(SpecialType.System_Char); + var int32Type = compilation.GetSpecialType(SpecialType.System_Int32); + + if (stringType is null || charType is null || int32Type is null) + { + symbols = default; + return false; + } + + var readOnlySpanOfCharType = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemReadOnlySpan1)?.Construct(charType); + var memoryExtensionsType = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemMemoryExtensions); + + if (readOnlySpanOfCharType is null || memoryExtensionsType is null) + { + symbols = default; + return false; + } + + var int32ParamInfo = ParameterInfo.GetParameterInfo(int32Type); + var stringParamInfo = ParameterInfo.GetParameterInfo(stringType); + + var substringMembers = stringType.GetMembers(nameof(string.Substring)).OfType(); + var substringStart = substringMembers.GetFirstOrDefaultMemberWithParameterInfos(int32ParamInfo); + var substringStartLength = substringMembers.GetFirstOrDefaultMemberWithParameterInfos(int32ParamInfo, int32ParamInfo); + + var asSpanMembers = memoryExtensionsType.GetMembers(nameof(MemoryExtensions.AsSpan)).OfType(); + var asSpanStart = asSpanMembers.GetFirstOrDefaultMemberWithParameterInfos(stringParamInfo, int32ParamInfo); + var asSpanStartLength = asSpanMembers.GetFirstOrDefaultMemberWithParameterInfos(stringParamInfo, int32ParamInfo, int32ParamInfo); + + if (substringStart is null || substringStartLength is null || asSpanStart is null || asSpanStartLength is null) + { + symbols = default; + return false; + } + + symbols = new RequiredSymbols( + stringType, readOnlySpanOfCharType, + memoryExtensionsType, + substringStart, substringStartLength, + asSpanStart, asSpanStartLength); + return true; + } + + public INamedTypeSymbol StringType { get; } + public INamedTypeSymbol ReadOnlySpanOfCharType { get; } + public INamedTypeSymbol MemoryExtensionsType { get; } + public IMethodSymbol SubstringStart { get; } + public IMethodSymbol SubstringStartLength { get; } + public IMethodSymbol AsSpanStart { get; } + public IMethodSymbol AsSpanStartLength { get; } + + public bool IsAnySubstringInvocation(IOperation operation) + { + if (operation is not IInvocationOperation invocation) + return false; + return SymbolEqualityComparer.Default.Equals(invocation.TargetMethod, SubstringStart) || + SymbolEqualityComparer.Default.Equals(invocation.TargetMethod, SubstringStartLength); + } + } + } +} diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf index 6af04a2095..68812c7030 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf @@ -1617,6 +1617,26 @@ Potenciální cyklus odkazů v deserializovaném grafu objektů + + Replace 'Substring' with 'AsSpan' + Replace 'Substring' with 'AsSpan' + + + + 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost. + 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost. + + + + Prefer 'AsSpan' over 'Substring' when span-based overloads are available + Prefer 'AsSpan' over 'Substring' when span-based overloads are available + + + + Prefer 'AsSpan' over 'Substring' + Prefer 'AsSpan' over 'Substring' + + Use 'ContainsKey' Use 'ContainsKey' diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf index cfaa562463..0785cce804 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf @@ -1617,6 +1617,26 @@ Potenzieller Verweiszyklus in deserialisiertem Objektgraph + + Replace 'Substring' with 'AsSpan' + Replace 'Substring' with 'AsSpan' + + + + 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost. + 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost. + + + + Prefer 'AsSpan' over 'Substring' when span-based overloads are available + Prefer 'AsSpan' over 'Substring' when span-based overloads are available + + + + Prefer 'AsSpan' over 'Substring' + Prefer 'AsSpan' over 'Substring' + + Use 'ContainsKey' Use 'ContainsKey' diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf index 0018af8f92..0dddd9fbe3 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf @@ -1617,6 +1617,26 @@ Posible ciclo de referencia en el gráfico de objetos deserializados + + Replace 'Substring' with 'AsSpan' + Replace 'Substring' with 'AsSpan' + + + + 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost. + 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost. + + + + Prefer 'AsSpan' over 'Substring' when span-based overloads are available + Prefer 'AsSpan' over 'Substring' when span-based overloads are available + + + + Prefer 'AsSpan' over 'Substring' + Prefer 'AsSpan' over 'Substring' + + Use 'ContainsKey' Use 'ContainsKey' diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf index f915bf31cd..bab5e55d08 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf @@ -1617,6 +1617,26 @@ Cycle de référence potentiel dans un graphe d'objet désérialisé + + Replace 'Substring' with 'AsSpan' + Replace 'Substring' with 'AsSpan' + + + + 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost. + 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost. + + + + Prefer 'AsSpan' over 'Substring' when span-based overloads are available + Prefer 'AsSpan' over 'Substring' when span-based overloads are available + + + + Prefer 'AsSpan' over 'Substring' + Prefer 'AsSpan' over 'Substring' + + Use 'ContainsKey' Use 'ContainsKey' diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf index 8eb665ee47..08c50ace13 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf @@ -1617,6 +1617,26 @@ Potenziale ciclo di riferimento nel grafico di oggetti deserializzati + + Replace 'Substring' with 'AsSpan' + Replace 'Substring' with 'AsSpan' + + + + 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost. + 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost. + + + + Prefer 'AsSpan' over 'Substring' when span-based overloads are available + Prefer 'AsSpan' over 'Substring' when span-based overloads are available + + + + Prefer 'AsSpan' over 'Substring' + Prefer 'AsSpan' over 'Substring' + + Use 'ContainsKey' Use 'ContainsKey' diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf index 8e53387376..e13f8d5717 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf @@ -1617,6 +1617,26 @@ 逆シリアル化されたオブジェクト グラフ内の参照サイクルの可能性 + + Replace 'Substring' with 'AsSpan' + Replace 'Substring' with 'AsSpan' + + + + 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost. + 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost. + + + + Prefer 'AsSpan' over 'Substring' when span-based overloads are available + Prefer 'AsSpan' over 'Substring' when span-based overloads are available + + + + Prefer 'AsSpan' over 'Substring' + Prefer 'AsSpan' over 'Substring' + + Use 'ContainsKey' Use 'ContainsKey' diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf index 40a9ba2dee..e28b4caccc 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf @@ -1617,6 +1617,26 @@ 역직렬화된 개체 그래프의 잠재적 참조 주기 + + Replace 'Substring' with 'AsSpan' + Replace 'Substring' with 'AsSpan' + + + + 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost. + 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost. + + + + Prefer 'AsSpan' over 'Substring' when span-based overloads are available + Prefer 'AsSpan' over 'Substring' when span-based overloads are available + + + + Prefer 'AsSpan' over 'Substring' + Prefer 'AsSpan' over 'Substring' + + Use 'ContainsKey' Use 'ContainsKey' diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf index 592daa7204..9e552a899d 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf @@ -1617,6 +1617,26 @@ Potencjalny cykl odwołań w deserializowanym grafie obiektów + + Replace 'Substring' with 'AsSpan' + Replace 'Substring' with 'AsSpan' + + + + 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost. + 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost. + + + + Prefer 'AsSpan' over 'Substring' when span-based overloads are available + Prefer 'AsSpan' over 'Substring' when span-based overloads are available + + + + Prefer 'AsSpan' over 'Substring' + Prefer 'AsSpan' over 'Substring' + + Use 'ContainsKey' Use 'ContainsKey' diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pt-BR.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pt-BR.xlf index e285035d7e..8d9726c1af 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pt-BR.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pt-BR.xlf @@ -1617,6 +1617,26 @@ Ciclo de referência potencial em grafo de objeto desserializado + + Replace 'Substring' with 'AsSpan' + Replace 'Substring' with 'AsSpan' + + + + 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost. + 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost. + + + + Prefer 'AsSpan' over 'Substring' when span-based overloads are available + Prefer 'AsSpan' over 'Substring' when span-based overloads are available + + + + Prefer 'AsSpan' over 'Substring' + Prefer 'AsSpan' over 'Substring' + + Use 'ContainsKey' Use 'ContainsKey' diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf index 85434b1f50..3ff7eabf40 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf @@ -1617,6 +1617,26 @@ Потенциальное зацикливание ссылок в графе десериализованного объекта + + Replace 'Substring' with 'AsSpan' + Replace 'Substring' with 'AsSpan' + + + + 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost. + 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost. + + + + Prefer 'AsSpan' over 'Substring' when span-based overloads are available + Prefer 'AsSpan' over 'Substring' when span-based overloads are available + + + + Prefer 'AsSpan' over 'Substring' + Prefer 'AsSpan' over 'Substring' + + Use 'ContainsKey' Use 'ContainsKey' diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf index dc94be6024..694c65a137 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf @@ -1617,6 +1617,26 @@ Seri durumdan çıkarılan nesne grafındaki olası başvuru döngüsü + + Replace 'Substring' with 'AsSpan' + Replace 'Substring' with 'AsSpan' + + + + 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost. + 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost. + + + + Prefer 'AsSpan' over 'Substring' when span-based overloads are available + Prefer 'AsSpan' over 'Substring' when span-based overloads are available + + + + Prefer 'AsSpan' over 'Substring' + Prefer 'AsSpan' over 'Substring' + + Use 'ContainsKey' Use 'ContainsKey' diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hans.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hans.xlf index dc86c9a46a..e2b26bf1fb 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hans.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hans.xlf @@ -1617,6 +1617,26 @@ 反序列化对象图中的潜在引用循环 + + Replace 'Substring' with 'AsSpan' + Replace 'Substring' with 'AsSpan' + + + + 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost. + 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost. + + + + Prefer 'AsSpan' over 'Substring' when span-based overloads are available + Prefer 'AsSpan' over 'Substring' when span-based overloads are available + + + + Prefer 'AsSpan' over 'Substring' + Prefer 'AsSpan' over 'Substring' + + Use 'ContainsKey' Use 'ContainsKey' diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hant.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hant.xlf index 16bfe4a6d0..c69cab2ec6 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hant.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hant.xlf @@ -1617,6 +1617,26 @@ 還原序列化物件圖中的潛在參考迴圈 + + Replace 'Substring' with 'AsSpan' + Replace 'Substring' with 'AsSpan' + + + + 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost. + 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost. + + + + Prefer 'AsSpan' over 'Substring' when span-based overloads are available + Prefer 'AsSpan' over 'Substring' when span-based overloads are available + + + + Prefer 'AsSpan' over 'Substring' + Prefer 'AsSpan' over 'Substring' + + Use 'ContainsKey' Use 'ContainsKey' diff --git a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md index a3222d52e0..cb6537e39b 100644 --- a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md +++ b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md @@ -1392,6 +1392,18 @@ It is more efficient to use 'AsSpan' and 'string.Concat', instead of 'Substring' |CodeFix|True| --- +## [CA1846](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1846): Prefer 'AsSpan' over 'Substring' + +'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost. + +|Item|Value| +|-|-| +|Category|Performance| +|Enabled|True| +|Severity|Info| +|CodeFix|True| +--- + ## [CA2000](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2000): Dispose objects before losing scope If a disposable object is not explicitly disposed before all references to it are out of scope, the object will be disposed at some indeterminate time when the garbage collector runs the finalizer of the object. Because an exceptional event might occur that will prevent the finalizer of the object from running, the object should be explicitly disposed instead. diff --git a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif index b7a3af4bb9..f67315150f 100644 --- a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif +++ b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif @@ -2582,6 +2582,26 @@ ] } }, + "CA1846": { + "id": "CA1846", + "shortDescription": "Prefer 'AsSpan' over 'Substring'", + "fullDescription": "'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost.", + "defaultLevel": "note", + "helpUri": "https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1846", + "properties": { + "category": "Performance", + "isEnabledByDefault": true, + "typeName": "PreferAsSpanOverSubstring", + "languages": [ + "C#", + "Visual Basic" + ], + "tags": [ + "Telemetry", + "EnabledRuleInAggressiveMode" + ] + } + }, "CA2000": { "id": "CA2000", "shortDescription": "Dispose objects before losing scope", diff --git a/src/NetAnalyzers/RulesMissingDocumentation.md b/src/NetAnalyzers/RulesMissingDocumentation.md index abf017a075..2201c424f8 100644 --- a/src/NetAnalyzers/RulesMissingDocumentation.md +++ b/src/NetAnalyzers/RulesMissingDocumentation.md @@ -6,8 +6,8 @@ CA2250 | | Use valid platform string | CA1839 | | Use 'Environment.ProcessPath' | CA1840 | | Use 'Environment.CurrentManagedThreadId' | -CA1841 | | Prefer Dictionary.Contains methods | CA1842 | | Do not use 'WhenAll' with a single task | CA1843 | | Do not use 'WaitAll' with a single task | CA1844 | | Provide memory-based overrides of async methods when subclassing 'Stream' | CA1845 | | Use span-based 'string.Concat' | +CA1846 | | Prefer 'AsSpan' over 'Substring' | diff --git a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Runtime/PreferAsSpanOverSubstringTests.cs b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Runtime/PreferAsSpanOverSubstringTests.cs new file mode 100644 index 0000000000..dc747f9e90 --- /dev/null +++ b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Runtime/PreferAsSpanOverSubstringTests.cs @@ -0,0 +1,1850 @@ +// 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.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.VisualBasic; +using Xunit; + +using VerifyCS = Test.Utilities.CSharpCodeFixVerifier< + Microsoft.NetCore.Analyzers.Runtime.PreferAsSpanOverSubstring, + Microsoft.NetCore.CSharp.Analyzers.Runtime.CSharpPreferAsSpanOverSubstringFixer>; +using VerifyVB = Test.Utilities.VisualBasicCodeFixVerifier< + Microsoft.NetCore.Analyzers.Runtime.PreferAsSpanOverSubstring, + Microsoft.NetCore.VisualBasic.Analyzers.Runtime.BasicPreferAsSpanOverSubstringFixer>; + +namespace Microsoft.NetCore.Analyzers.Runtime.UnitTests +{ + public class PreferAsSpanOverSubstringTests + { + public static IEnumerable Data_SubstringAsSpanPair_CS + { + get + { + yield return new[] { @"Substring(1)", @"AsSpan(1)" }; + yield return new[] { @"Substring(1, 2)", @"AsSpan(1, 2)" }; + yield return new[] { @"Substring(startIndex: 1)", @"AsSpan(start: 1)" }; + yield return new[] { @"Substring(startIndex: 1, 2)", @"AsSpan(start: 1, 2)" }; + yield return new[] { @"Substring(1, length: 2)", @"AsSpan(1, length: 2)" }; + yield return new[] { @"Substring(startIndex: 1, length: 2)", @"AsSpan(start: 1, length: 2)" }; + yield return new[] { @"Substring(length: 2, startIndex: 1)", @"AsSpan(length: 2, start: 1)" }; + } + } + + public static IEnumerable Data_SubstringAsSpanPair_VB + { + get + { + yield return new[] { @"Substring(1)", @"AsSpan(1)" }; + yield return new[] { @"Substring(1, 2)", @"AsSpan(1, 2)" }; + yield return new[] { @"Substring(startIndex:=1)", @"AsSpan(start:=1)" }; + yield return new[] { @"Substring(startIndex:=1, 2)", @"AsSpan(start:=1, 2)" }; + yield return new[] { @"Substring(1, length:=2)", @"AsSpan(1, length:=2)" }; + yield return new[] { @"Substring(startIndex:=1, length:=2)", @"AsSpan(start:=1, length:=2)" }; + yield return new[] { @"Substring(length:=2, startIndex:=1)", @"AsSpan(length:=2, start:=1)" }; + } + } + + [Theory] + [MemberData(nameof(Data_SubstringAsSpanPair_CS))] + public Task SingleArgumentStaticMethod_ReportsDiagnostic_CS(string substring, string asSpan) + { + string thing = @" +using System; + +public class Thing +{ + public static void Consume(string text) { } + public static void Consume(ReadOnlySpan span) { } +}"; + string testStatements = WithKey($"Thing.Consume(foo.{substring})", 0) + ';'; + string fixedStatements = $"Thing.Consume(foo.{asSpan});"; + + var test = new VerifyCS.Test + { + TestState = + { + Sources = { thing, CS.WithBody(testStatements) }, + ExpectedDiagnostics = { CS.DiagnosticAt(0) } + }, + FixedState = + { + Sources = { thing, CS.WithBody(fixedStatements) } + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + return test.RunAsync(); + } + + [Theory] + [MemberData(nameof(Data_SubstringAsSpanPair_VB))] + public Task SingleArgumentStaticMethod_ReportsDiagnostic_VB(string substring, string asSpan) + { + // 'Thing' needs to be in a C# project because VB doesn't support spans in exposed APIs. + string thing = @" +using System; + +public class Thing +{ + public static void Consume(string text) { } + public static void Consume(ReadOnlySpan span) { } +}"; + var thingProject = new ProjectState("ThingProject", LanguageNames.CSharp, "thing", "cs") + { + Sources = { thing } + }; + string testStatements = WithKey($"Thing.Consume(foo.{substring})", 0); + string fixedStatements = $"Thing.Consume(foo.{asSpan})"; + + var test = new VerifyVB.Test + { + TestState = + { + Sources = { VB.WithBody(testStatements) }, + AdditionalProjects = { { thingProject.Name, thingProject } }, + AdditionalProjectReferences = { thingProject.Name }, + ExpectedDiagnostics = { VB.DiagnosticAt(0) } + }, + FixedState = + { + Sources = { VB.WithBody(fixedStatements) }, + AdditionalProjects = { { thingProject.Name, thingProject } }, + AdditionalProjectReferences = { thingProject.Name } + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + return test.RunAsync(); + } + + [Theory] + [MemberData(nameof(Data_SubstringAsSpanPair_CS))] + public Task SingleArgumentInstanceMethod_ReportsDiagnostic_CS(string substring, string asSpan) + { + string thing = @" +using System; + +public class Thing +{ + public void Consume(string text) { } + public void Consume(ReadOnlySpan span) { } +}"; + string fields = @" +public partial class Body +{ + private Thing thing = new Thing(); +}"; + string testCode = CS.WithBody(WithKey($"thing.Consume(foo.{substring})", 0) + ';'); + string fixedCode = CS.WithBody($"thing.Consume(foo.{asSpan});"); + + var test = new VerifyCS.Test + { + TestState = + { + Sources = { testCode, thing, fields }, + ExpectedDiagnostics = { CS.DiagnosticAt(0) } + }, + FixedState = + { + Sources = { fixedCode, thing, fields } + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + return test.RunAsync(); + } + + [Theory] + [MemberData(nameof(Data_SubstringAsSpanPair_VB))] + public Task SingleArgumentInstanceMethod_ReportsDiagnostic_VB(string substring, string asSpan) + { + // 'Thing' needs to be in a C# project besause VB doesn't support spans in exposed APIs. + string thing = @" +using System; + +public class Thing +{ + public void Consume(string text) { } + public void Consume(ReadOnlySpan span) { } +}"; + var thingProject = new ProjectState("ThingProject", LanguageNames.CSharp, "thing", "cs") + { + Sources = { thing } + }; + string fields = @" +Partial Public Class Body + + Private thing As Thing = New Thing() +End Class"; + string testCode = VB.WithBody(WithKey($"thing.Consume(foo.{substring})", 0)); + string fixedCode = VB.WithBody($"thing.Consume(foo.{asSpan})"); + + var test = new VerifyVB.Test + { + TestState = + { + Sources = { testCode, fields }, + AdditionalProjects = { { thingProject.Name, thingProject } }, + AdditionalProjectReferences = { thingProject.Name }, + ExpectedDiagnostics = { VB.DiagnosticAt(0) } + }, + FixedState = + { + Sources = { fixedCode, fields }, + AdditionalProjects = { { thingProject.Name, thingProject } }, + AdditionalProjectReferences = { thingProject.Name } + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + return test.RunAsync(); + } + + public static IEnumerable Data_MultipleArguments_WithAvailableSpanOverloads + { + get + { + const string usings = @" +using System; +using Roschar = System.ReadOnlySpan;"; + string thing = usings + @" +public class Thing +{ + public static void Consume(string text, int num) { } + public static void Consume(Roschar span, int num) { } + public static void Consume(double[] data, string text, int num) { } + public static void Consume(double[] data, Roschar text, int num) { } + public static void Consume(string text1, string text2, int num) { } + public static void Consume(Roschar span1, Roschar span2, int num) { } + public static void Consume(Roschar span1, string text2, int num) { } + public static void Consume(string text1, Roschar span2, int num) { } + public static void Consume(string text1, int num, string text2) { } + public static void Consume(string text1, int num, Roschar span2) { } + public static void Consume(Roschar span1, int num, string text2) { } + public static void Consume(Roschar span1, int num, Roschar span2) { } +}"; + yield return new[] { thing, @"foo.Substring(1), 17", @"foo.AsSpan(1), 17" }; + yield return new[] { thing, @"_data, foo.Substring(1, 2), 17", @"_data, foo.AsSpan(1, 2), 17" }; + yield return new[] { thing, @"foo.Substring(1), foo.Substring(1, 2), 17", @"foo.AsSpan(1), foo.AsSpan(1, 2), 17" }; + yield return new[] { thing, @"foo.Substring(1), foo, 17", @"foo.AsSpan(1), foo, 17" }; + yield return new[] { thing, @"foo, foo.Substring(1), 17", @"foo, foo.AsSpan(1), 17" }; + yield return new[] { thing, @"foo, 17, foo.Substring(1)", @"foo, 17, foo.AsSpan(1)" }; + yield return new[] { thing, @"foo.Substring(1), 17, foo", @"foo.AsSpan(1), 17, foo" }; + yield return new[] { thing, @"foo.Substring(1), 17, foo.Substring(1, 2)", @"foo.AsSpan(1), 17, foo.AsSpan(1, 2)" }; + } + } + + [Theory] + [MemberData(nameof(Data_MultipleArguments_WithAvailableSpanOverloads))] + public Task MultipleArguments_WithAvailableSpanOverloads_ReportsDiagnostic_CS(string receiverClass, string testArguments, string fixedArguments) + { + string fields = @" +public partial class Body +{ + private double[] _data = new[] { 3.14159, 2.71828 }; +}"; + string testCode = CS.WithBody(WithKey($"Thing.Consume({testArguments})", 0) + ';'); + string fixedCode = CS.WithBody($"Thing.Consume({fixedArguments});"); + + var test = new VerifyCS.Test + { + TestState = + { + Sources = { testCode, receiverClass, fields }, + ExpectedDiagnostics = { CS.DiagnosticAt(0) } + }, + FixedState = + { + Sources = { fixedCode, receiverClass, fields } + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + return test.RunAsync(); + } + + [Theory] + [MemberData(nameof(Data_MultipleArguments_WithAvailableSpanOverloads))] + public Task MultipleArguments_WithAvailableSpanOverloads_ReportsDiagnostic_VB(string receiverClass, string testArguments, string fixedArguments) + { + // Use C# project because VB doesn't support spans in APIs. + var thingProject = new ProjectState("ThingProject", LanguageNames.CSharp, "thing", "cs") + { + Sources = { receiverClass } + }; + string fields = @" +Partial Public Class Body + + Private _data As Double() = {3.14159, 2.71828} +End Class"; + string testCode = VB.WithBody(WithKey($"Thing.Consume({testArguments})", 0)); + string fixedCode = VB.WithBody($"Thing.Consume({fixedArguments})"); + + var test = new VerifyVB.Test + { + TestState = + { + Sources = { testCode, fields }, + AdditionalProjects = { { thingProject.Name, thingProject } }, + AdditionalProjectReferences = { thingProject.Name }, + ExpectedDiagnostics = { VB.DiagnosticAt(0) } + }, + FixedState = + { + Sources = { fixedCode, fields }, + AdditionalProjects = { { thingProject.Name, thingProject } }, + AdditionalProjectReferences = { thingProject.Name } + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + return test.RunAsync(); + } + + public static IEnumerable Data_NamedArguments_CS + { + get + { + string usings = @" +using System; +using Roschar = System.ReadOnlySpan;"; + string thing = usings + @" +public class Thing +{ + public static void Consume(string text, int n) { } + public static void Consume(Roschar span, int c) { } +}"; + yield return new[] + { + thing, + @"Thing.Consume(text: foo.Substring(1), 7)", + @"Thing.Consume(span: foo.AsSpan(1), 7)" + }; + yield return new[] + { + thing, + @"Thing.Consume(foo.Substring(1), n: 7)", + @"Thing.Consume(foo.AsSpan(1), c: 7)" + }; + yield return new[] + { + thing, + @"Thing.Consume(text: foo.Substring(1), n: 7)", + @"Thing.Consume(span: foo.AsSpan(1), c: 7)" + }; + yield return new[] + { + thing, + @"Thing.Consume(n: 7, text: foo.Substring(1))", + @"Thing.Consume(c: 7, span: foo.AsSpan(1))" + }; + yield return new[] + { + thing, + @"Thing.Consume(n: 7, text: foo.Substring(length: 2, startIndex: 1))", + @"Thing.Consume(c: 7, span: foo.AsSpan(length: 2, start: 1))" + }; + + thing = usings + @" +public class Thing +{ + public static void Consume(string text1A, string text2A) { } + public static void Consume(Roschar span1B, string text2B) { } + public static void Consume(string text1C, Roschar span2C) { } + public static void Consume(Roschar span1D, Roschar span2D) { } +}"; + yield return new[] + { + thing, + @"Thing.Consume(text1A: foo, text2A: foo.Substring(2))", + @"Thing.Consume(text1C: foo, span2C: foo.AsSpan(2))" + }; + yield return new[] + { + thing, + @"Thing.Consume(text2A: foo.Substring(2), text1A: foo)", + @"Thing.Consume(span2C: foo.AsSpan(2), text1C: foo)" + }; + yield return new[] + { + thing, + @"Thing.Consume(text1A: foo.Substring(1), text2A: foo)", + @"Thing.Consume(span1B: foo.AsSpan(1), text2B: foo)" + }; + yield return new[] + { + thing, + @"Thing.Consume(text2A: foo, text1A: foo.Substring(1))", + @"Thing.Consume(text2B: foo, span1B: foo.AsSpan(1))" + }; + yield return new[] + { + thing, + @"Thing.Consume(text1A: foo.Substring(1), text2A: foo.Substring(2))", + @"Thing.Consume(span1D: foo.AsSpan(1), span2D: foo.AsSpan(2))" + }; + yield return new[] + { + thing, + @"Thing.Consume(text2A: foo.Substring(2), text1A: foo.Substring(1))", + @"Thing.Consume(span2D: foo.AsSpan(2), span1D: foo.AsSpan(1))" + }; + + thing = usings + @" +public class Thing +{ + public static void Consume(int n1A, string text2A, string text3A) { } + public static void Consume(int n1B, Roschar span2B, Roschar span3B) { } +}"; + yield return new[] + { + thing, + @"Thing.Consume(text3A: foo.Substring(3), n1A: 7, text2A: foo.Substring(2))", + @"Thing.Consume(span3B: foo.AsSpan(3), n1B: 7, span2B: foo.AsSpan(2))" + }; + } + } + + [Theory] + [MemberData(nameof(Data_NamedArguments_CS))] + public Task NamedArguments_AreHandledCorrectly_CS(string receiverClass, string testExpression, string fixedExpression) + { + string testCode = CS.WithBody(WithKey(testExpression, 0) + ';'); + string fixedCode = CS.WithBody(fixedExpression + ';'); + + var test = new VerifyCS.Test + { + TestState = + { + Sources = { testCode, receiverClass }, + ExpectedDiagnostics = { CS.DiagnosticAt(0) } + }, + FixedState = + { + Sources = { fixedCode, receiverClass } + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + return test.RunAsync(); + } + + public static IEnumerable Data_NamedArguments_VB + { + get + { + string usings = @" +using System; +using Roschar = System.ReadOnlySpan;"; + string thing = usings + @" +public class Thing +{ + public static void Consume(string text, int n) { } + public static void Consume(Roschar span, int c) { } +}"; + yield return new[] + { + thing, + @"Thing.Consume(text:=foo.Substring(1), 7)", + @"Thing.Consume(span:=foo.AsSpan(1), 7)" + }; + yield return new[] + { + thing, + @"Thing.Consume(foo.Substring(1), n:=7)", + @"Thing.Consume(foo.AsSpan(1), c:=7)" + }; + yield return new[] + { + thing, + @"Thing.Consume(text:=foo.Substring(1), n:=7)", + @"Thing.Consume(span:=foo.AsSpan(1), c:=7)" + }; + yield return new[] + { + thing, + @"Thing.Consume(n:=7, text:=foo.Substring(1))", + @"Thing.Consume(c:=7, span:=foo.AsSpan(1))" + }; + yield return new[] + { + thing, + @"Thing.Consume(n:=7, text:=foo.Substring(length:=2, startIndex:=1))", + @"Thing.Consume(c:=7, span:=foo.AsSpan(length:=2, start:=1))" + }; + + thing = usings + @" +public class Thing +{ + public static void Consume(string text1A, string text2A) { } + public static void Consume(Roschar span1B, string text2B) { } + public static void Consume(string text1C, Roschar span2C) { } + public static void Consume(Roschar span1D, Roschar span2D) { } +}"; + yield return new[] + { + thing, + @"Thing.Consume(text1A:=foo, text2A:=foo.Substring(2))", + @"Thing.Consume(text1C:=foo, span2C:=foo.AsSpan(2))" + }; + yield return new[] + { + thing, + @"Thing.Consume(text2A:=foo.Substring(2), text1A:=foo)", + @"Thing.Consume(span2C:=foo.AsSpan(2), text1C:=foo)" + }; + yield return new[] + { + thing, + @"Thing.Consume(text1A:=foo.Substring(1), text2A:=foo)", + @"Thing.Consume(span1B:=foo.AsSpan(1), text2B:=foo)" + }; + yield return new[] + { + thing, + @"Thing.Consume(text2A:=foo, text1A:=foo.Substring(1))", + @"Thing.Consume(text2B:=foo, span1B:=foo.AsSpan(1))" + }; + yield return new[] + { + thing, + @"Thing.Consume(text1A:=foo.Substring(1), text2A:=foo.Substring(2))", + @"Thing.Consume(span1D:=foo.AsSpan(1), span2D:=foo.AsSpan(2))" + }; + yield return new[] + { + thing, + @"Thing.Consume(text2A:=foo.Substring(2), text1A:=foo.Substring(1))", + @"Thing.Consume(span2D:=foo.AsSpan(2), span1D:=foo.AsSpan(1))" + }; + + thing = usings + @" +public class Thing +{ + public static void Consume(int n1A, string text2A, string text3A) { } + public static void Consume(int n1B, Roschar span2B, Roschar span3B) { } +}"; + yield return new[] + { + thing, + @"Thing.Consume(text3A:=foo.Substring(3), n1A:=7, text2A:=foo.Substring(2))", + @"Thing.Consume(span3B:=foo.AsSpan(3), n1B:=7, span2B:=foo.AsSpan(2))" + }; + } + } + + [Theory] + [MemberData(nameof(Data_NamedArguments_VB))] + public Task NamedArguments_AreHandledCorrectly_VB(string receiverClass, string testExpression, string fixedExpression) + { + string testCode = VB.WithBody(WithKey(testExpression, 0)); + string fixedCode = VB.WithBody(fixedExpression); + var receiverProject = new ProjectState("ReceiverProject", LanguageNames.CSharp, "receiver", "vb") + { + Sources = { receiverClass } + }; + + var test = new VerifyVB.Test + { + TestState = + { + Sources = { testCode }, + AdditionalProjects = { { receiverProject.Name, receiverProject } }, + AdditionalProjectReferences = { receiverProject.Name }, + ExpectedDiagnostics = { VB.DiagnosticAt(0) } + }, + FixedState = + { + Sources = { fixedCode }, + AdditionalProjects = { { receiverProject.Name, receiverProject } }, + AdditionalProjectReferences = { receiverProject.Name } + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + return test.RunAsync(); + } + + public static IEnumerable Data_WhenRoscharOverloadAlreadySelected_SubstringConvertedToAsSpan + { + get + { + string thing = CS.Usings + @" +public class Thing +{ + public static void Consume(Roschar span) { } + public static void Consume(Roschar span1, Roschar span2) { } + public static void Consume(Roschar span1, Roschar span2, Roschar span3) { } +}"; + yield return new[] + { + thing, + @"Thing.Consume(foo.Substring(1))", + @"Thing.Consume(foo.AsSpan(1))" + }; + yield return new[] + { + thing, + @"Thing.Consume(foo.Substring(1), foo.Substring(2))", + @"Thing.Consume(foo.AsSpan(1), foo.AsSpan(2))" + }; + yield return new[] + { + thing, + @"Thing.Consume(foo.Substring(1), foo.Substring(2), foo.Substring(3))", + @"Thing.Consume(foo.AsSpan(1), foo.AsSpan(2), foo.AsSpan(3))" + }; + } + } + + [Theory] + [MemberData(nameof(Data_WhenRoscharOverloadAlreadySelected_SubstringConvertedToAsSpan))] + public Task WhenRoscharOverloadAlreadySelected_SubstringConvertedToAsSpan_CS(string receiverClass, string testExpression, string fixedExpression) + { + string testCode = CS.WithBody(WithKey(testExpression, 0) + ';'); + string fixedCode = CS.WithBody(fixedExpression + ';'); + + var test = new VerifyCS.Test + { + TestState = + { + Sources = { testCode, receiverClass }, + ExpectedDiagnostics = { CS.DiagnosticAt(0) } + }, + FixedState = + { + Sources = { fixedCode, receiverClass } + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + return test.RunAsync(); + } + + [Theory] + [MemberData(nameof(Data_WhenRoscharOverloadAlreadySelected_SubstringConvertedToAsSpan))] + public Task WhenRoscharOverloadAlreadySelected_SubstringConvertedToAsSpan_VB(string receiverClass, string testExpression, string fixedExpression) + { + string testCode = VB.WithBody(WithKey(testExpression, 0)); + string fixedCode = VB.WithBody(fixedExpression); + var receiverProject = new ProjectState("ReceiverProject", LanguageNames.CSharp, "receiver", "cs") + { + Sources = { receiverClass } + }; + + var test = new VerifyVB.Test + { + TestState = + { + Sources = { testCode }, + AdditionalProjects = { { receiverProject.Name, receiverProject } }, + AdditionalProjectReferences = { receiverProject.Name }, + ExpectedDiagnostics = { VB.DiagnosticAt(0) } + }, + FixedState = + { + Sources = { fixedCode }, + AdditionalProjects = { { receiverProject.Name, receiverProject } }, + AdditionalProjectReferences = { receiverProject.Name } + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + return test.RunAsync(); + } + + public static IEnumerable Data_NestedViolations + { + get + { + string receiver = CS.Usings + @" +public class C +{ + public static string Fwd(string text) => throw null; + public static string Fwd(Roschar span) => throw null; + public static string Fwd(string text1, string text2) => throw null; + public static string Fwd(Roschar span1, Roschar span2) => throw null; + + public static void Consume(string text) { } + public static void Consume(Roschar span) { } + public static void Consume(string text1, string text2) { } + public static void Consume(Roschar span1, Roschar span2) { } +}"; + yield return new object[] + { + receiver, + @"{|#0:C.Consume({|#1:C.Fwd(foo.Substring(1))|}.Substring(2))|}", + @"C.Consume(C.Fwd(foo.AsSpan(1)).AsSpan(2))", + new[] { 0, 1 }, + 2 + }; + yield return new object[] + { + receiver, + @"{|#0:C.Consume({|#1:C.Fwd(foo.Substring(1), foo.Substring(2))|}.Substring(3), foo.Substring(4))|}", + @"C.Consume(C.Fwd(foo.AsSpan(1), foo.AsSpan(2)).AsSpan(3), foo.AsSpan(4))", + new[] { 0, 1 }, + 2 + }; + yield return new object[] + { + receiver, + @"{|#0:C.Consume({|#1:C.Fwd(foo.Substring(1), {|#2:C.Fwd(foo.Substring(2))|}.Substring(3))|}.Substring(4))|}", + @"C.Consume(C.Fwd(foo.AsSpan(1), C.Fwd(foo.AsSpan(2)).AsSpan(3)).AsSpan(4))", + new[] { 0, 1, 2 }, + 3 + }; + } + } + + [Theory] + [MemberData(nameof(Data_NestedViolations))] + public Task NestedViolations_AreAllReportedAndFixed_CS( + string receiverClass, string testExpression, string fixedExpression, int[] locations, + int? incrementalIterations) + { + string testCode = CS.WithBody(testExpression + ';'); + string fixedCode = CS.WithBody(fixedExpression + ';'); + + var test = new VerifyCS.Test + { + TestState = + { + Sources = { testCode, receiverClass } + }, + FixedState = + { + Sources = { fixedCode, receiverClass } + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50, + NumberOfIncrementalIterations = incrementalIterations, + }; + test.TestState.ExpectedDiagnostics.AddRange(locations.Select(x => CS.DiagnosticAt(x))); + return test.RunAsync(); + } + + [Theory] + [MemberData(nameof(Data_NestedViolations))] + public Task NestedViolations_AreAllReportedAndFixed_VB( + string receiverClass, string testExpression, string fixedExpression, int[] locations, + int? incrementalIterations) + { + string testCode = VB.WithBody(testExpression); + string fixedCode = VB.WithBody(fixedExpression); + var receiverProject = new ProjectState("ReceiverProject", LanguageNames.CSharp, "receiver", "cs") + { + Sources = { receiverClass } + }; + + var test = new VerifyVB.Test + { + TestState = + { + Sources = { testCode }, + AdditionalProjects = { { receiverProject.Name, receiverProject } }, + AdditionalProjectReferences = { receiverProject.Name } + }, + FixedState = + { + Sources = { fixedCode }, + AdditionalProjects = { { receiverProject.Name, receiverProject } }, + AdditionalProjectReferences = { receiverProject.Name } + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50, + NumberOfIncrementalIterations = incrementalIterations, + }; + test.TestState.ExpectedDiagnostics.AddRange(locations.Select(x => VB.DiagnosticAt(x))); + return test.RunAsync(); + } + + [Fact] + public Task SystemNamespace_IsAdded_WhenMissing_CS() + { + string receiver = CS.Usings + @" +public class C +{ + public static void Consume(string text) { } + public static void Consume(Roschar span) { } +}"; + string testCode = CS.WithBody(WithKey(@"C.Consume(foo.Substring(1))", 0) + ';', includeUsings: false); + string fixedCode = CS.WithBody(@"C.Consume(foo.AsSpan(1));", includeUsings: true); + + var test = new VerifyCS.Test + { + TestState = + { + Sources = { testCode, receiver }, + ExpectedDiagnostics = { CS.DiagnosticAt(0) } + }, + FixedState = + { + Sources = { fixedCode, receiver } + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + return test.RunAsync(); + } + + [Fact] + public Task SystemNamespace_IsAdded_WhenNotIncludedGlobally_VB() + { + string receiver = CS.Usings + @" +public class C +{ + public static void Consume(string text) { } + public static void Consume(Roschar span) { } +}"; + string testCode = VB.WithBody(WithKey(@"C.Consume(foo.Substring(1))", 0), includeImports: false); + string fixedCode = VB.WithBody(@"C.Consume(foo.AsSpan(1))", includeImports: true); + var project = new ProjectState("Receiver", LanguageNames.CSharp, "receiver", "cs") + { + Sources = { receiver } + }; + + var test = new VerifyVB.Test + { + TestState = + { + Sources = { testCode }, + AdditionalProjects = { { project.Name, project } }, + AdditionalProjectReferences = { project.Name }, + ExpectedDiagnostics = { VB.DiagnosticAt(0) } + }, + FixedState = + { + Sources = { fixedCode }, + AdditionalProjects = { { project.Name, project } }, + AdditionalProjectReferences = { project.Name } + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + return test.RunAsync(); + } + + [Fact] + public Task SystemNamespace_IsNotAdded_WhenIncludedGlobally_VB() + { + string receiver = CS.Usings + @" +public class C +{ + public static void Consume(string text) { } + public static void Consume(Roschar span) { } +}"; + string testCode = VB.WithBody(WithKey(@"C.Consume(foo.Substring(1))", 0), includeImports: false); + string fixedCode = VB.WithBody(@"C.Consume(foo.AsSpan(1))", includeImports: false); + var receiverProject = new ProjectState("Receiver", LanguageNames.CSharp, "receiver", "cs") + { + Sources = { receiver } + }; + + var test = new VerifyVB.Test + { + TestState = + { + Sources = { testCode }, + AdditionalProjects = { { receiverProject.Name, receiverProject } }, + AdditionalProjectReferences = { receiverProject.Name }, + ExpectedDiagnostics = { VB.DiagnosticAt(0) } + }, + FixedState = + { + Sources = { fixedCode }, + AdditionalProjects = { { receiverProject.Name, receiverProject } }, + AdditionalProjectReferences = { receiverProject.Name }, + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + test.SolutionTransforms.Add((solution, id) => + { + var project = solution.GetProject(id); + if (project.Name == receiverProject.Name) + return solution; + var options = (VisualBasicCompilationOptions)project.CompilationOptions; + var globalSystemImport = GlobalImport.Parse(nameof(System)); + options = options.WithGlobalImports(globalSystemImport); + return solution.WithProjectCompilationOptions(id, options); + }); + return test.RunAsync(); + } + + // No VB counterpart because imports must precede all declarations in VB. + [Fact] + public Task SystemNamespace_IsNotAdded_WhenImportedWithinNamespaceDeclaration_CS() + { + string format = @" +using Roschar = System.ReadOnlySpan; + +namespace Testopolis +{{ + using System; + + public class Body + {{ + public void Consume(string text) {{ }} + public void Consume(Roschar span) {{ }} + public void Run(string foo) + {{ + {0} + }} + }} +}}"; + string testCode = string.Format(CultureInfo.InvariantCulture, format, @"{|#0:Consume(foo.Substring(1))|};"); + string fixedCode = string.Format(CultureInfo.InvariantCulture, format, @"Consume(foo.AsSpan(1));"); + + var test = new VerifyCS.Test + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { CS.DiagnosticAt(0) }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + return test.RunAsync(); + } + + [Theory] + [InlineData("System")] + [InlineData("System.Widgets")] + public Task SystemNamespace_IsNotAdded_WhenViolationIsWithinSystemNamespace_CS(string namespaceDeclaration) + { + string format = @" +using Roschar = System.ReadOnlySpan; + +namespace " + namespaceDeclaration + @" +{{ + public class Body + {{ + public void Consume(string text) {{ }} + public void Consume(Roschar span) {{ }} + public void Run(string foo) + {{ + {0} + }} + }} +}}"; + string testCode = string.Format(CultureInfo.InvariantCulture, format, @"{|#0:Consume(foo.Substring(1))|};"); + string fixedCode = string.Format(CultureInfo.InvariantCulture, format, @"Consume(foo.AsSpan(1));"); + + var test = new VerifyCS.Test + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { CS.DiagnosticAt(0) }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + return test.RunAsync(); + } + + [Theory] + [InlineData("System")] + [InlineData("System.Widgets")] + public Task SystemNamespace_IsNotAdded_WhenViolationIsWithinSystemNamespace_VB(string namespaceDeclaration) + { + string helper = @" +using Roschar = System.ReadOnlySpan; + +public class Helper +{ + public void Consume(string text) { } + public void Consume(Roschar span) { } +}"; + var project = new ProjectState("HelperProject", LanguageNames.CSharp, "helper", "cs") + { + Sources = { helper } + }; + string format = @" +Namespace " + namespaceDeclaration + @" + + Public Class Body + + Private helper As Helper + Public Sub Run(foo As String) + + {0} + End Sub + End Class +End Namespace"; + string testCode = string.Format(CultureInfo.InvariantCulture, format, @"{|#0:helper.Consume(foo.Substring(1))|}"); + string fixedCode = string.Format(CultureInfo.InvariantCulture, format, @"helper.Consume(foo.AsSpan(1))"); + + var test = new VerifyVB.Test + { + TestState = + { + Sources = { testCode }, + AdditionalProjects = { { project.Name, project } }, + AdditionalProjectReferences = { project.Name }, + ExpectedDiagnostics = { VB.DiagnosticAt(0) } + }, + FixedState = + { + Sources = { fixedCode }, + AdditionalProjects = { { project.Name, project } }, + AdditionalProjectReferences = { project.Name } + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + return test.RunAsync(); + } + + public static IEnumerable Data_MultipleCandidateOverloads_SingleBestCandidate_CS + { + get + { + string members = @" +public void Consume(string a, string b, string c) { } +public void Consume(Roschar a, string b, string c) { } +public void Consume(string a, string b, Roschar c) { } +public void Consume(Roschar a, Roschar b, string c) { }"; + yield return new[] + { + CS.WithBody(WithKey(@"Consume(foo.Substring(1), foo.Substring(2), foo.Substring(3))", 0) + ';', members), + CS.WithBody(@"Consume(foo.AsSpan(1), foo.AsSpan(2), foo.Substring(3));", members) + }; + + members = @" +public void Consume(int n, string b, string c) { } +public void Consume(double n, Roschar b, Roschar c) { } +public void Consume(int n, string b, Roschar c) { }"; + yield return new[] + { + CS.WithBody(WithKey(@"Consume(7, foo.Substring(2), foo.Substring(3))", 0) + ';', members), + CS.WithBody(@"Consume(7, foo.Substring(2), foo.AsSpan(3));", members) + }; + } + } + + [Theory] + [MemberData(nameof(Data_MultipleCandidateOverloads_SingleBestCandidate_CS))] + public Task MultipleCandidateOverloads_SingleBestCandidate_ReportedAndFixed_CS(string testCode, string fixedCode) + { + var test = new VerifyCS.Test + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { CS.DiagnosticAt(0) }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + return test.RunAsync(); + } + + public static IEnumerable Data_MultipleCandidateOVerloads_SingleBestCandidate_VB + { + get + { + string receiver = CS.Usings + @" +public class R +{ + public static void Consume(string a, string b, string c) { } + public static void Consume(Roschar a, string b, string c) { } + public static void Consume(string a, string b, Roschar c) { } + public static void Consume(Roschar a, Roschar b, string c) { } +}"; + yield return new[] + { + receiver, + VB.WithBody(WithKey(@"R.Consume(foo.Substring(1), foo.Substring(2), foo.Substring(3))", 0)), + VB.WithBody(@"R.Consume(foo.AsSpan(1), foo.AsSpan(2), foo.Substring(3))") + }; + + receiver = CS.Usings + @" +public class R +{ + public static void Consume(int n, string b, string c) { } + public static void Consume(double n, Roschar b, Roschar c) { } + public static void Consume(int n, string b, Roschar c) { } +}"; + yield return new[] + { + receiver, + VB.WithBody(WithKey(@"R.Consume(7, foo.Substring(2), foo.Substring(3))", 0)), + VB.WithBody(@"R.Consume(7, foo.Substring(2), foo.AsSpan(3))") + }; + } + } + + [Theory] + [MemberData(nameof(Data_MultipleCandidateOVerloads_SingleBestCandidate_VB))] + public Task MultipleCandidateOverloads_SingleBestCandidate_ReportedAndFixed_VB(string receiverClass, string testCode, string fixedCode) + { + var project = new ProjectState("ReceiverProject", LanguageNames.CSharp, "receiver", "cs") + { + Sources = { receiverClass } + }; + + var test = new VerifyVB.Test + { + TestState = + { + Sources = { testCode }, + AdditionalProjects = { { project.Name, project } }, + AdditionalProjectReferences = { project.Name }, + ExpectedDiagnostics = { VB.DiagnosticAt(0) } + }, + FixedState = + { + Sources = { fixedCode }, + AdditionalProjects = { { project.Name, project } }, + AdditionalProjectReferences = { project.Name } + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + return test.RunAsync(); + } + + public static IEnumerable Data_MultipleCandidateOverloads_Ambiguous_CS + { + get + { + string members = @" +public void Consume(string a, string b) { } +public void Consume(Roschar a, string b) { } +public void Consume(string a, Roschar b) { }"; + yield return new[] + { + CS.WithBody(WithKey(@"Consume(foo.Substring(1), foo.Substring(2))", 0) + ';', members) + }; + + members = @" +public void Consume(string a, string b, string c) { } +public void Consume(string a, Roschar b, string c) { } +public void Consume(Roschar a, string b, Roschar c) { } +public void Consume(Roschar a, Roschar b, string c) { } +public void Consume(string a, Roschar b, Roschar c) { }"; + yield return new[] + { + CS.WithBody(WithKey(@"Consume(foo.Substring(1), foo.Substring(2), foo.Substring(3))", 0) + ';', members) + }; + } + } + + [Theory] + [MemberData(nameof(Data_MultipleCandidateOverloads_Ambiguous_CS))] + public Task MultipleCandidateOverloads_Ambiguous_ReportedButNotFixed_CS(string testCode) + { + var test = new VerifyCS.Test + { + TestCode = testCode, + ExpectedDiagnostics = { CS.DiagnosticAt(0) }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + return test.RunAsync(); + } + + public static IEnumerable Data_MultipleCandidateOverloads_Ambiguous_VB + { + get + { + string receiver = CS.Usings + @" +public class R +{ + public static void Consume(string a, string b) { } + public static void Consume(Roschar a, string b) { } + public static void Consume(string a, Roschar b) { } +}"; + yield return new[] + { + receiver, + VB.WithBody(WithKey(@"R.Consume(foo.Substring(1), foo.Substring(2))", 0)) + }; + + receiver = CS.Usings + @" +public class R +{ + public static void Consume(string a, string b, string c) { } + public static void Consume(string a, Roschar b, string c) { } + public static void Consume(Roschar a, string b, Roschar c) { } + public static void Consume(Roschar a, Roschar b, string c) { } + public static void Consume(string a, Roschar b, Roschar c) { } +}"; + yield return new[] + { + receiver, + VB.WithBody(WithKey(@"R.Consume(foo.Substring(1), foo.Substring(2), foo.Substring(3))", 0)) + }; + } + } + + [Theory] + [MemberData(nameof(Data_MultipleCandidateOverloads_Ambiguous_VB))] + public Task MultipleCandidateOverloads_Ambiguous_ReportedButNotFixed_VB(string receiverClass, string testCode) + { + var project = new ProjectState("ReceiverProject", LanguageNames.CSharp, "receiver", "cs") + { + Sources = { receiverClass } + }; + + var test = new VerifyVB.Test + { + TestState = + { + Sources = { testCode }, + AdditionalProjects = { { project.Name, project } }, + AdditionalProjectReferences = { project.Name }, + ExpectedDiagnostics = { VB.DiagnosticAt(0) } + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + return test.RunAsync(); + } + + public static IEnumerable Data_NoRoscharOverload_CS + { + get + { + string thing = CS.Usings + @" +public class Thing +{ + public static void Consume(string text) { } +}"; + + yield return new[] { thing, @"Thing.Consume(foo.Substring(1))" }; + + thing = CS.Usings + @" +public class Thing +{ + public static void Consume(string text) { } + public static void Consume(Roschar span1, Roschar span2) { } +}"; + yield return new[] { thing, @"Thing.Consume(foo.Substring(1))" }; + + thing = CS.Usings + @" +public class Thing +{ + public static void Consume(string text, int n) { } + public static void Consume(int n, Roschar span) { } +}"; + yield return new[] { thing, @"Thing.Consume(foo.Substring(1), 17)" }; + yield return new[] { thing, @"Thing.Consume(n: 17, text: foo.Substring(1))" }; + } + } + + [Theory] + [MemberData(nameof(Data_NoRoscharOverload_CS))] + public Task NoRoscharOverload_NoDiagnostic_CS(string receiverClass, string testExpression) + { + string testCode = CS.WithBody(testExpression + ';'); + + var test = new VerifyCS.Test + { + TestState = + { + Sources = { testCode, receiverClass } + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + return test.RunAsync(); + } + + public static IEnumerable Data_NoRoscharOverload_VB + { + get + { + string thing = CS.Usings + @" +public class Thing +{ + public static void Consume(string text) { } +}"; + yield return new[] { thing, @"Thing.Consume(foo.Substring(1))" }; + + thing = CS.Usings + @" +public class Thing +{ + public static void Consume(string text) { } + public static void Consume(Roschar span1, Roschar span2) { } +}"; + yield return new[] { thing, @"Thing.Consume(foo.Substring(1))" }; + + thing = CS.Usings + @" +public class Thing +{ + public static void Consume(string text, int n) { } + public static void Consume(int n, Roschar span) { } +}"; + yield return new[] { thing, @"Thing.Consume(foo.Substring(1), 17)" }; + yield return new[] { thing, @"Thing.Consume(n:=17, text:=foo.Substring(1))" }; + } + } + + [Theory] + [MemberData(nameof(Data_NoRoscharOverload_VB))] + public Task NoRoscharOverload_NoDiagnostic_VB(string receiverClass, string testExpression) + { + string testCode = VB.WithBody(WithKey(testExpression, 0)); + var receiverProject = new ProjectState("ReceiverProject", LanguageNames.CSharp, "receiver", "cs") + { + Sources = { receiverClass } + }; + + var test = new VerifyVB.Test + { + TestState = + { + Sources = { testCode }, + AdditionalProjects = { { receiverProject.Name, receiverProject } }, + AdditionalProjectReferences = { receiverProject.Name } + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + return test.RunAsync(); + } + + public static IEnumerable Data_InvalidOverloads_CS + { + get + { + yield return new[] + { + CS.Usings + @" +public class StaticToInstance +{ + public static void Consume(string text) { } + public void Consume(Roschar span) { } +}", + @"StaticToInstance.Consume(foo.Substring(1));" + }; + + yield return new[] + { + CS.Usings + @" +public class InstanceToStatic +{ + public void Consume(string text) { } + public static void Consume(Roschar span) { } +}", + @"instance.Consume(foo.Substring(1));", + @" +partial class Body +{ + private InstanceToStatic instance = new InstanceToStatic(); +}" + }; + + yield return new[] + { + CS.Usings + @" +public class WrongReturnType +{ + public static string Make(string text) => throw null; + public static int Make(Roschar span) => throw null; +}", + @"var _ = WrongReturnType.Make(foo.Substring(1));" + }; + } + } + + [Theory] + [MemberData(nameof(Data_InvalidOverloads_CS))] + public Task InvalidOverloads_NoDiagnostic_CS(string receiverClass, string testStatements, string extraFields = "") + { + string testCode = CS.WithBody(testStatements); + + var test = new VerifyCS.Test + { + TestState = + { + Sources = { testCode, receiverClass, extraFields } + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + return test.RunAsync(); + } + + public static IEnumerable Data_InvalidOverloads_VB + { + get + { + yield return new[] + { + CS.Usings + @" +public class StaticToInstance +{ + public static void Consume(string text) { } + public void Consume(Roschar span) { } +}", + @"StaticToInstance.Consume(foo.Substring(1))" + }; + + yield return new[] + { + CS.Usings + @" +public class InstanceToStatic +{ + public void Consume(string text) { } + public static void Consume(Roschar span) { } +}", + @"instance.Consume(foo.Substring(1))", + @" +Partial Class Body + + Private instance As InstanceToStatic = New InstanceToStatic() +End Class" + }; + + yield return new[] + { + CS.Usings + @" +public class WrongReturnType +{ + public static string Make(string text) => throw null; + public static int Make(Roschar span) => throw null; +}", + @"Dim m = WrongReturnType.Make(foo.Substring(1))" + }; + } + } + + [Theory] + [MemberData(nameof(Data_InvalidOverloads_VB))] + public Task InvalidOverloads_NoDiagnostic_VB(string receiverClass, string testStatements, string extraFields = "") + { + string testCode = VB.WithBody(testStatements); + var project = new ProjectState("ReceiverProject", LanguageNames.CSharp, "receiver", "cs") + { + Sources = { receiverClass } + }; + + var test = new VerifyVB.Test + { + TestState = + { + Sources = { testCode, extraFields }, + AdditionalProjects = { { project.Name, project } }, + AdditionalProjectReferences = { project.Name } + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + return test.RunAsync(); + } + + [Theory] + [InlineData("parent.Private")] + [InlineData("sibling.Private")] + [InlineData("base.Private")] + [InlineData("this.Private")] + [InlineData("Private")] + [InlineData("parent.ProtectedAndInternal")] + [InlineData("sibling.ProtectedAndInternal")] + [InlineData("base.ProtectedAndInternal")] + [InlineData("this.ProtectedAndInternal")] + [InlineData("ProtectedAndInternal")] + [InlineData("parent.Internal")] + [InlineData("sibling.Internal")] + [InlineData("base.Internal")] + [InlineData("this.Internal")] + [InlineData("Internal")] + [InlineData("parent.Protected")] + [InlineData("parent.ProtectedOrInternal")] + public Task Accessibility_ExternalBaseClass_WithoutDiagnostics_CS(string methodCallWithoutArgumentList) + { + string testCode = CS.Usings + @" +public class ExternalSubclass : External +{ + private string foo; + private External parent; + private ExternalSubclass sibling; + public void NoDiagnostic() + { + " + methodCallWithoutArgumentList + @"(foo.Substring(1)); + } +}"; + var project = new ProjectState("ExternalProject", LanguageNames.CSharp, "external", "cs") + { + Sources = { CS.ExternalBaseClass } + }; + + var test = new VerifyCS.Test + { + TestState = + { + Sources = { testCode }, + AdditionalProjects = { { project.Name, project } }, + AdditionalProjectReferences = { project.Name } + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + return test.RunAsync(); + } + + [Theory] + [InlineData("parent.Private")] + [InlineData("sibling.Private")] + [InlineData("MyBase.Private")] + [InlineData("Me.Private")] + [InlineData("[Private]")] + [InlineData("parent.ProtectedAndInternal")] + [InlineData("sibling.ProtectedAndInternal")] + [InlineData("MyBase.ProtectedAndInternal")] + [InlineData("Me.ProtectedAndInternal")] + [InlineData("ProtectedAndInternal")] + [InlineData("parent.Internal")] + [InlineData("sibling.Internal")] + [InlineData("MyBase.Internal")] + [InlineData("Me.Internal")] + [InlineData("parent.Protected")] + [InlineData("parent.ProtectedOrInternal")] + public Task Accessibility_ExternalBaseClass_WithoutDiagnostics_VB(string methodCallWithoutArgumentList) + { + string testCode = VB.Usings + @" +Public Class ExternalSubclass : Inherits External + + Private foo As String + Private parent As External + Private sibling As ExternalSubclass + Public Sub NoDiagnostic() + + " + methodCallWithoutArgumentList + @"(foo.Substring(1)) + End Sub +End Class"; + var project = new ProjectState("ExternalProject", LanguageNames.CSharp, "external", "cs") + { + Sources = { CS.ExternalBaseClass } + }; + + var test = new VerifyVB.Test + { + TestState = + { + Sources = { testCode }, + AdditionalProjects = { { project.Name, project } }, + AdditionalProjectReferences = { project.Name } + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + return test.RunAsync(); + } + + [Theory] + [InlineData("sibling.Protected")] + [InlineData("base.Protected")] + [InlineData("this.Protected")] + [InlineData("Protected")] + [InlineData("sibling.ProtectedOrInternal")] + [InlineData("base.ProtectedOrInternal")] + [InlineData("this.ProtectedOrInternal")] + [InlineData("ProtectedOrInternal")] + public Task Accessibility_ExternalBaseClass_WithDiagnostics_CS(string methodCallWithoutArgumentList) + { + string testCode = CS.Usings + @" +public class ExternalSubclass : External +{ + private string foo; + private ExternalSubclass sibling; + public void Diagnostic() + { + {|#0:" + methodCallWithoutArgumentList + @"(foo.Substring(1))|}; + } +}"; + string fixedCode = CS.Usings + @" +public class ExternalSubclass : External +{ + private string foo; + private ExternalSubclass sibling; + public void Diagnostic() + { + " + methodCallWithoutArgumentList + @"(foo.AsSpan(1)); + } +}"; + var project = new ProjectState("ExternalProject", LanguageNames.CSharp, "external", "cs") + { + Sources = { CS.ExternalBaseClass } + }; + + var test = new VerifyCS.Test + { + TestState = + { + Sources = { testCode }, + AdditionalProjects = { { project.Name, project } }, + AdditionalProjectReferences = { project.Name }, + ExpectedDiagnostics = { CS.DiagnosticAt(0) } + }, + FixedState = + { + Sources = { fixedCode }, + AdditionalProjects = { { project.Name, project } }, + AdditionalProjectReferences = { project.Name } + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + return test.RunAsync(); + } + + [Theory] + [InlineData("sibling.Protected")] + [InlineData("MyBase.Protected")] + [InlineData("Me.Protected")] + [InlineData("[Protected]")] + [InlineData("sibling.ProtectedOrInternal")] + [InlineData("MyBase.ProtectedOrInternal")] + [InlineData("Me.ProtectedOrInternal")] + [InlineData("ProtectedOrInternal")] + public Task Accessibility_ExternalBaseClass_WithDiagnostics_VB(string methodCallWithoutArgumentList) + { + string testCode = VB.Usings + @" +Public Class ExternalSubclass : Inherits External + + Private foo As String + Private sibling As ExternalSubclass + Private Sub Diagnostic() + + {|#0:" + methodCallWithoutArgumentList + @"(foo.Substring(1))|} + End Sub +End Class"; + string fixedCode = VB.Usings + @" +Public Class ExternalSubclass : Inherits External + + Private foo As String + Private sibling As ExternalSubclass + Private Sub Diagnostic() + + " + methodCallWithoutArgumentList + @"(foo.AsSpan(1)) + End Sub +End Class"; + var project = new ProjectState("ExternalProject", LanguageNames.CSharp, "external", "cs") + { + Sources = { CS.ExternalBaseClass } + }; + + var test = new VerifyVB.Test + { + TestState = + { + Sources = { testCode }, + AdditionalProjects = { { project.Name, project } }, + AdditionalProjectReferences = { project.Name }, + ExpectedDiagnostics = { VB.DiagnosticAt(0) } + }, + FixedState = + { + Sources = { fixedCode }, + AdditionalProjects = { { project.Name, project } }, + AdditionalProjectReferences = { project.Name } + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + return test.RunAsync(); + } + + // No VB counterpart because VB doesn't support ref-like types in APIs. + [Theory] + [InlineData("parent.Private")] + [InlineData("sibling.Private")] + [InlineData("base.Private")] + [InlineData("this.Private")] + [InlineData("Private")] + [InlineData("parent.Protected")] + public Task Accessibility_InternalBaseClass_WithoutDiagnostics_CS(string methodCallWithoutArgumentList) + { + string testCode = CS.Usings + @" +public class InternalSubclass : Internal +{ + private string foo; + private Internal parent; + private InternalSubclass sibling; + public void NoDiagnostic() + { + " + methodCallWithoutArgumentList + @"(foo.Substring(1)); + } +}"; + + var test = new VerifyCS.Test + { + TestState = + { + Sources = { testCode, CS.InternalBaseClass } + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + return test.RunAsync(); + } + + // No VB counterpart because VB doesn't support ref-like types in APIs. + [Theory] + [InlineData("sibling.Protected")] + [InlineData("base.Protected")] + [InlineData("this.Protected")] + [InlineData("Protected")] + public Task Accessibility_InternalBaseClass_WithDiagnostics_CS(string methodCallWithoutArgumentList) + { + string testCode = CS.Usings + @" +public class InternalSubclass : Internal +{ + private string foo; + private InternalSubclass sibling; + public void Diagnostic() + { + {|#0:" + methodCallWithoutArgumentList + @"(foo.Substring(1))|}; + } +}"; + string fixedCode = CS.Usings + @" +public class InternalSubclass : Internal +{ + private string foo; + private InternalSubclass sibling; + public void Diagnostic() + { + " + methodCallWithoutArgumentList + @"(foo.AsSpan(1)); + } +}"; + + var test = new VerifyCS.Test + { + TestState = + { + Sources = { testCode, CS.InternalBaseClass }, + ExpectedDiagnostics = { CS.DiagnosticAt(0) } + }, + FixedState = + { + Sources = { fixedCode, CS.InternalBaseClass } + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + return test.RunAsync(); + } + + [Fact] + public Task ConditionalSubstringAccess_NoDiagnostic_CS() + { + string testCode = CS.Usings + @" +public class Body +{ + public void Consume(string text) { } + public void Consume(Roschar span) { } + public void Run(string foo) + { + Consume(foo?.Substring(1)); + } +}"; + + var test = new VerifyCS.Test + { + TestCode = testCode, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + return test.RunAsync(); + } + + [Fact] + public Task ConditionalSubstringAccess_NoDiagnostic_VB() + { + string receiver = CS.Usings + @" +public class Receiver +{ + public void Consume(string text) { } + public void Consume(Roschar span) { } +}"; + var project = new ProjectState("ReceiverProject", LanguageNames.CSharp, "receiver", "cs") + { + Sources = { receiver } + }; + string testCode = VB.WithBody( + @" +Dim receiver = New Receiver() +receiver.Consume(foo?.Substring(1))"); + + var test = new VerifyVB.Test + { + TestState = + { + Sources = { testCode }, + AdditionalProjects = { { project.Name, project } }, + AdditionalProjectReferences = { project.Name } + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net50 + }; + return test.RunAsync(); + } + + #region Helpers + private static class CS + { + public const string Usings = @" +using System; +using Roschar = System.ReadOnlySpan;"; + public const string ExternalBaseClass = Usings + @" +public class External +{ + public void ProtectedOrInternal(string text) { } + protected internal void ProtectedOrInternal(Roschar span) { } + + public void Protected(string text) { } + protected void Protected(Roschar span) { } + + public void Internal(string text) { } + internal void Internal(Roschar span) { } + + public void ProtectedAndInternal(string text) { } + private protected void ProtectedAndInternal(Roschar span) { } + + public void Private(string text) { } + private void Private(Roschar span) { } +}"; + public const string InternalBaseClass = Usings + @" +public class Internal +{ + public void Private(string text) { } + private void Private(Roschar span) { } + + public void Protected(string text) { } + protected void Protected(Roschar span) { } +}"; + + public static string WithBody(string statements, bool includeUsings = true) + { + string indentedStatements = IndentLines(statements, " "); + string usings = includeUsings ? $"{Environment.NewLine}using System;{Environment.NewLine}" : string.Empty; + + return $@" +{usings} +public partial class Body +{{ + private void Run(string foo) + {{ +{indentedStatements} + }} +}}"; + } + public static string WithBody(string statements, string members) + { + return Usings + $@" +public partial class Body +{{ +{IndentLines(members, " ")} + private void Run(string foo) + {{ +{IndentLines(statements, " ")} + }} +}}"; + } + + public static DiagnosticResult DiagnosticAt(int markupKey) => VerifyCS.Diagnostic(Rule).WithLocation(markupKey); + } + + private static class VB + { + public const string Usings = @" +Imports System"; + + public static string WithBody(string statements, bool includeImports = true) + { + const string indent = " "; + string indentedStatements = indent + statements.TrimStart().Replace(Environment.NewLine, Environment.NewLine + indent, StringComparison.Ordinal); + string imports = includeImports ? $"{Environment.NewLine}Imports System{Environment.NewLine}" : string.Empty; + + return $@" +{imports} +Partial Public Class Body + + Private Sub Run(foo As String) + +{indentedStatements} + End Sub +End Class"; + } + + public static DiagnosticResult DiagnosticAt(int markupKey) => VerifyVB.Diagnostic(Rule).WithLocation(markupKey); + } + + private static string WithKey(string text, int markupKey) => $"{{|#{markupKey}:{text}|}}"; + + private static string IndentLines(string lines, string indent) => indent + lines.TrimStart().Replace(Environment.NewLine, Environment.NewLine + indent, StringComparison.Ordinal); + private static DiagnosticDescriptor Rule => PreferAsSpanOverSubstring.Rule; + #endregion + } +} diff --git a/src/NetAnalyzers/VisualBasic/Microsoft.NetCore.Analyzers/Runtime/BasicPreferAsSpanOverSubstring.Fixer.vb b/src/NetAnalyzers/VisualBasic/Microsoft.NetCore.Analyzers/Runtime/BasicPreferAsSpanOverSubstring.Fixer.vb new file mode 100644 index 0000000000..e3abdf2f16 --- /dev/null +++ b/src/NetAnalyzers/VisualBasic/Microsoft.NetCore.Analyzers/Runtime/BasicPreferAsSpanOverSubstring.Fixer.vb @@ -0,0 +1,39 @@ +' Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +Imports Microsoft.CodeAnalysis +Imports Microsoft.CodeAnalysis.CodeFixes +Imports Microsoft.CodeAnalysis.Editing +Imports Microsoft.CodeAnalysis.VisualBasic +Imports Microsoft.CodeAnalysis.VisualBasic.Syntax +Imports Microsoft.NetCore.Analyzers.Runtime + +Namespace Microsoft.NetCore.VisualBasic.Analyzers.Runtime + + + Public NotInheritable Class BasicPreferAsSpanOverSubstringFixer : Inherits PreferAsSpanOverSubstringFixer + + Private Protected Overrides Sub ReplaceNonConditionalInvocationMethodName(editor As SyntaxEditor, memberInvocation As SyntaxNode, newName As String) + + Dim cast = DirectCast(memberInvocation, InvocationExpressionSyntax) + Dim memberAccessSyntax = DirectCast(cast.Expression, MemberAccessExpressionSyntax) + Dim newNameSyntax = SyntaxFactory.IdentifierName(newName) + editor.ReplaceNode(memberAccessSyntax.Name, newNameSyntax) + End Sub + + Private Protected Overrides Sub ReplaceNamedArgumentName(editor As SyntaxEditor, invocation As SyntaxNode, oldArgumentName As String, newArgumentName As String) + + Dim cast = DirectCast(invocation, InvocationExpressionSyntax) + Dim argumentToReplace = cast.ArgumentList.Arguments.FirstOrDefault( + Function(x) + If Not x.IsNamed Then Return False + Dim simpleArgumentSyntax = TryCast(x, SimpleArgumentSyntax) + If simpleArgumentSyntax Is Nothing Then Return False + Return simpleArgumentSyntax.NameColonEquals.Name.Identifier.ValueText = oldArgumentName + End Function) + If argumentToReplace Is Nothing Then Return + Dim oldNameSyntax = DirectCast(argumentToReplace, SimpleArgumentSyntax).NameColonEquals.Name + Dim newNameSyntax = SyntaxFactory.IdentifierName(newArgumentName) + editor.ReplaceNode(oldNameSyntax, newNameSyntax) + End Sub + End Class +End Namespace diff --git a/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt b/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt index 7b44225310..fe9aa34aba 100644 --- a/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt +++ b/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt @@ -12,7 +12,7 @@ Design: CA2210, CA1000-CA1070 Globalization: CA2101, CA1300-CA1310 Mobility: CA1600-CA1601 -Performance: HA, CA1800-CA1845 +Performance: HA, CA1800-CA1846 Security: CA2100-CA2153, CA2300-CA2330, CA3000-CA3147, CA5300-CA5403 Usage: CA1801, CA1806, CA1816, CA2200-CA2209, CA2211-CA2250 Naming: CA1700-CA1726 diff --git a/src/Utilities/Compiler/Extensions/IEnumerableOfIMethodSymbolExtensions.cs b/src/Utilities/Compiler/Extensions/IEnumerableOfIMethodSymbolExtensions.cs index cfbac56c1f..647069c5cf 100644 --- a/src/Utilities/Compiler/Extensions/IEnumerableOfIMethodSymbolExtensions.cs +++ b/src/Utilities/Compiler/Extensions/IEnumerableOfIMethodSymbolExtensions.cs @@ -147,6 +147,44 @@ public static IEnumerable GetMethodOverloadsWithDesiredParameterA return true; }); } + + /// + /// Given an , returns the whose parameter list + /// matches . + /// + /// + /// Expected types of the member's parameters. + /// + /// The first member in the sequence whose parameters match , + /// or null if no matches are found. + /// + public static IMethodSymbol? GetFirstOrDefaultMemberWithParameterTypes(this IEnumerable? members, IReadOnlyList expectedParameterTypesInOrder) + { + if (members is null) + return null; + + foreach (var member in members) + { + if (Predicate(member)) + return member; + } + + return null; + + bool Predicate(IMethodSymbol member) + { + if (member.Parameters.Length != expectedParameterTypesInOrder.Count) + return false; + + for (int index = 0; index < expectedParameterTypesInOrder.Count; index++) + { + if (!member.Parameters[index].Type.Equals(expectedParameterTypesInOrder[index])) + return false; + } + + return true; + } + } } // Contains the expected properties of a parameter diff --git a/src/Utilities/Compiler/Extensions/IOperationExtensions.cs b/src/Utilities/Compiler/Extensions/IOperationExtensions.cs index 0842ec0986..8bd966ef96 100644 --- a/src/Utilities/Compiler/Extensions/IOperationExtensions.cs +++ b/src/Utilities/Compiler/Extensions/IOperationExtensions.cs @@ -672,6 +672,23 @@ public static IOperation WalkDownConversion(this IOperation operation) return operation; } + /// + /// Walks down consecutive conversion operations that satisfy until an operand is reached that + /// either isn't a conversion or doesn't satisfy . + /// + /// The starting operation. + /// A predicate to filter conversion operations. + /// The first operation that either isn't a conversion or doesn't satisfy . + public static IOperation WalkDownConversion(this IOperation operation, Func predicate) + { + while (operation is IConversionOperation conversionOperation && predicate(conversionOperation)) + { + operation = conversionOperation.Operand; + } + + return operation; + } + [return: NotNullIfNotNull("operation")] public static IOperation? WalkUpConversion(this IOperation? operation) {