|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.SimplifyInterpolation
{
internal abstract class AbstractSimplifyInterpolationHelpers
{
protected abstract bool PermitNonLiteralAlignmentComponents { get; }
protected abstract SyntaxNode GetPreservedInterpolationExpressionSyntax(IOperation operation);
public void UnwrapInterpolation<TInterpolationSyntax, TExpressionSyntax>(
IVirtualCharService virtualCharService, ISyntaxFacts syntaxFacts, IInterpolationOperation interpolation,
out TExpressionSyntax? unwrapped, out TExpressionSyntax? alignment, out bool negate,
out string? formatString, out ImmutableArray<Location> unnecessaryLocations)
where TInterpolationSyntax : SyntaxNode
where TExpressionSyntax : SyntaxNode
{
alignment = null;
negate = false;
formatString = null;
var unnecessarySpans = new List<TextSpan>();
var expression = Unwrap(interpolation.Expression);
if (interpolation.Alignment == null)
UnwrapAlignmentPadding(expression, out expression, out alignment, out negate, unnecessarySpans);
if (interpolation.FormatString == null)
UnwrapFormatString(virtualCharService, syntaxFacts, expression, out expression, out formatString, unnecessarySpans);
unwrapped = GetPreservedInterpolationExpressionSyntax(expression) as TExpressionSyntax;
unnecessaryLocations = unnecessarySpans
.OrderBy(t => t.Start)
.SelectAsArray(interpolation.Syntax.SyntaxTree.GetLocation);
}
[return: NotNullIfNotNull(nameof(expression))]
private static IOperation? Unwrap(IOperation? expression, bool towardsParent = false)
{
while (true)
{
if (towardsParent && expression?.Parent is null)
return expression;
switch (expression)
{
case IParenthesizedOperation parenthesized:
expression = towardsParent ? expression.Parent : parenthesized.Operand;
continue;
case IConversionOperation { IsImplicit: true } conversion:
expression = towardsParent ? expression.Parent : conversion.Operand;
continue;
default:
return expression;
}
}
}
private void UnwrapFormatString(
IVirtualCharService virtualCharService, ISyntaxFacts syntaxFacts, IOperation expression, out IOperation unwrapped,
out string? formatString, List<TextSpan> unnecessarySpans)
{
Contract.ThrowIfNull(expression.SemanticModel);
if (expression is IInvocationOperation { TargetMethod.Name: nameof(ToString) } invocation &&
HasNonImplicitInstance(invocation, out var instance) &&
!syntaxFacts.IsBaseExpression(instance.Syntax) &&
instance.Type != null &&
!instance.Type.IsRefLikeType)
{
if (invocation.Arguments.Length == 1
|| (invocation.Arguments.Length == 2 && UsesInvariantCultureReferenceInsideFormattableStringInvariant(invocation, formatProviderArgumentIndex: 1)))
{
if (invocation.Arguments[0].Value is ILiteralOperation { ConstantValue: { HasValue: true, Value: string value } } literal &&
FindType<IFormattable>(expression.SemanticModel) is { } systemIFormattable &&
instance.Type.Implements(systemIFormattable))
{
unwrapped = instance;
formatString = value;
unnecessarySpans.AddRange(invocation.Syntax.Span
.Subtract(GetPreservedInterpolationExpressionSyntax(instance).FullSpan)
.Subtract(GetSpanWithinLiteralQuotes(virtualCharService, literal.Syntax.GetFirstToken())));
return;
}
}
if (IsObjectToStringOverride(invocation.TargetMethod)
|| (invocation.Arguments.Length == 1 && UsesInvariantCultureReferenceInsideFormattableStringInvariant(invocation, formatProviderArgumentIndex: 0)))
{
// A call to `.ToString()` at the end of the interpolation. This is unnecessary.
// Just remove entirely.
unwrapped = instance;
formatString = "";
unnecessarySpans.AddRange(invocation.Syntax.Span
.Subtract(GetPreservedInterpolationExpressionSyntax(instance).FullSpan));
return;
}
}
unwrapped = expression;
formatString = null;
}
private static bool IsObjectToStringOverride(IMethodSymbol method)
{
while (method.OverriddenMethod is not null)
method = method.OverriddenMethod;
return method.ContainingType.SpecialType == SpecialType.System_Object
&& method.Name == nameof(ToString);
}
private static bool UsesInvariantCultureReferenceInsideFormattableStringInvariant(IInvocationOperation invocation, int formatProviderArgumentIndex)
{
return IsInvariantCultureReference(invocation.Arguments[formatProviderArgumentIndex].Value)
&& IsInsideFormattableStringInvariant(invocation);
}
private static bool IsInvariantCultureReference(IOperation operation)
{
Contract.ThrowIfNull(operation.SemanticModel);
if (Unwrap(operation) is IPropertyReferenceOperation { Member: { } member })
{
if (member.Name == nameof(CultureInfo.InvariantCulture))
return IsType<CultureInfo>(member.ContainingType, operation.SemanticModel);
if (member.Name == "InvariantInfo")
{
return IsType<NumberFormatInfo>(member.ContainingType, operation.SemanticModel)
|| IsType<DateTimeFormatInfo>(member.ContainingType, operation.SemanticModel);
}
}
return false;
}
private static bool IsInsideFormattableStringInvariant(IOperation operation)
{
Contract.ThrowIfNull(operation.SemanticModel);
var interpolatedStringOperation = AncestorsAndSelf(operation).OfType<IInterpolatedStringOperation>().FirstOrDefault();
return Unwrap(interpolatedStringOperation?.Parent, towardsParent: true) is IArgumentOperation
{
Parent: IInvocationOperation
{
TargetMethod: { Name: nameof(FormattableString.Invariant), ContainingType: var containingType },
},
} && IsType<FormattableString>(containingType, operation.SemanticModel);
}
private static bool IsType<T>(INamedTypeSymbol type, SemanticModel semanticModel)
=> SymbolEqualityComparer.Default.Equals(type, FindType<T>(semanticModel));
private static INamedTypeSymbol? FindType<T>(SemanticModel semanticModel)
=> semanticModel.Compilation.GetTypeByMetadataName(typeof(T).FullName!);
private static IEnumerable<IOperation> AncestorsAndSelf(IOperation operation)
{
for (var current = operation; current is not null; current = current.Parent)
yield return current;
}
private static TextSpan GetSpanWithinLiteralQuotes(IVirtualCharService virtualCharService, SyntaxToken formatToken)
{
var sequence = virtualCharService.TryConvertToVirtualChars(formatToken);
return sequence.IsDefaultOrEmpty
? default
: TextSpan.FromBounds(sequence.First().Span.Start, sequence.Last().Span.End);
}
private void UnwrapAlignmentPadding<TExpressionSyntax>(
IOperation expression, out IOperation unwrapped,
out TExpressionSyntax? alignment, out bool negate, List<TextSpan> unnecessarySpans)
where TExpressionSyntax : SyntaxNode
{
if (expression is IInvocationOperation invocation &&
HasNonImplicitInstance(invocation, out var instance))
{
var targetName = invocation.TargetMethod.Name;
if (targetName is nameof(string.PadLeft) or nameof(string.PadRight))
{
var argCount = invocation.Arguments.Length;
if (argCount is 1 or 2)
{
if (argCount == 1 ||
IsSpaceChar(invocation.Arguments[1]))
{
var alignmentOp = invocation.Arguments[0].Value;
if (PermitNonLiteralAlignmentComponents
? alignmentOp is { ConstantValue.HasValue: true }
: alignmentOp is { Kind: OperationKind.Literal })
{
var alignmentSyntax = alignmentOp.Syntax;
unwrapped = instance;
alignment = alignmentSyntax as TExpressionSyntax;
negate = targetName == nameof(string.PadRight);
unnecessarySpans.AddRange(invocation.Syntax.Span
.Subtract(GetPreservedInterpolationExpressionSyntax(instance).FullSpan)
.Subtract(alignmentSyntax.FullSpan));
return;
}
}
}
}
}
unwrapped = expression;
alignment = null;
negate = false;
}
private static bool HasNonImplicitInstance(IInvocationOperation invocation, [NotNullWhen(true)] out IOperation? instance)
{
if (invocation.Instance is { IsImplicit: false })
{
instance = invocation.Instance;
return true;
}
instance = null;
return false;
}
private static bool IsSpaceChar(IArgumentOperation argument)
=> argument.Value.ConstantValue is { HasValue: true, Value: ' ' };
}
}
|