// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Analyzer.Utilities.PooledObjects;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.FlowAnalysis.DataFlow;
using Microsoft.CodeAnalysis.FlowAnalysis.DataFlow.GlobalFlowStateAnalysis;
using Microsoft.CodeAnalysis.FlowAnalysis.DataFlow.ValueContentAnalysis;
using Microsoft.CodeAnalysis.Operations;
namespace Microsoft.NetCore.Analyzers.InteropServices
{
using ValueContentAnalysisResult = DataFlowAnalysisResult<ValueContentBlockAnalysisResult, ValueContentAbstractValue>;
public sealed partial class PlatformCompatibilityAnalyzer
{
private readonly struct PlatformMethodValue : IAbstractAnalysisValue, IEquatable<PlatformMethodValue>
{
internal PlatformMethodValue(string platformPropertyName, Version version, bool negated)
{
PlatformName = platformPropertyName ?? throw new ArgumentNullException(nameof(platformPropertyName));
Version = version ?? throw new ArgumentNullException(nameof(version));
Negated = negated;
}
public string PlatformName { get; }
public Version Version { get; }
public bool Negated { get; }
public IAbstractAnalysisValue GetNegatedValue()
=> new PlatformMethodValue(PlatformName, Version, !Negated);
public static bool TryDecode(
IMethodSymbol invokedPlatformCheckMethod,
ImmutableArray<IArgumentOperation> arguments,
ValueContentAnalysisResult? valueContentAnalysisResult,
INamedTypeSymbol? osPlatformType,
ArrayBuilder<PlatformMethodValue> infosBuilder)
{
// Accelerators like OperatingSystem.IsPlatformName()
if (arguments.IsEmpty)
{
if (TryExtractPlatformName(invokedPlatformCheckMethod.Name, out var platformName))
{
var info = new PlatformMethodValue(platformName, EmptyVersion, negated: false);
infosBuilder.Add(info);
return true;
}
}
else
{
using var osPlatformNamesBuilder = ArrayBuilder<string>.GetInstance();
if (TryDecodeRuntimeInformationIsOSPlatform(arguments[0].Value, osPlatformType, valueContentAnalysisResult, osPlatformNamesBuilder))
{
Debug.Assert(osPlatformNamesBuilder.Count > 0);
for (var i = 0; i < osPlatformNamesBuilder.Count; i++)
{
var info = new PlatformMethodValue(osPlatformNamesBuilder[i], EmptyVersion, negated: false);
infosBuilder.Add(info);
}
return true;
}
if (arguments.GetArgumentForParameterAtIndex(0).Value is ILiteralOperation literal)
{
if (literal.Type?.SpecialType == SpecialType.System_String &&
literal.ConstantValue.HasValue &&
literal.ConstantValue.Value is { } value)
{
// OperatingSystem.IsOSPlatform(string platform)
if (invokedPlatformCheckMethod.Name == IsOSPlatform &&
TryParsePlatformNameAndVersion(value.ToString(), out string platformName, out Version? version))
{
var info = new PlatformMethodValue(platformName, version, negated: false);
infosBuilder.Add(info);
return true;
}
else if (TryDecodeOSVersion(arguments, valueContentAnalysisResult, out version, 1))
{
// OperatingSystem.IsOSPlatformVersionAtLeast(string platform, int major, int minor = 0, int build = 0, int revision = 0)
Debug.Assert(invokedPlatformCheckMethod.Name == "IsOSPlatformVersionAtLeast");
var info = new PlatformMethodValue(literal.ConstantValue.Value.ToString(), version, negated: false);
infosBuilder.Add(info);
return true;
}
}
else if (literal.Type?.SpecialType == SpecialType.System_Int32)
{
// Accelerators like OperatingSystem.IsPlatformNameVersionAtLeast(int major, int minor = 0, int build = 0, int revision = 0)
if (TryExtractPlatformName(invokedPlatformCheckMethod.Name, out var platformName) &&
TryDecodeOSVersion(arguments, valueContentAnalysisResult, out var version))
{
var info = new PlatformMethodValue(platformName, version, negated: false);
infosBuilder.Add(info);
return true;
}
}
}
}
return false;
}
private static bool TryDecodeRuntimeInformationIsOSPlatform(
IOperation argumentValue,
INamedTypeSymbol? osPlatformType,
ValueContentAnalysisResult? valueContentAnalysisResult,
ArrayBuilder<string> decodedOsPlatformNamesBuilder)
{
if (argumentValue.Type?.Equals(osPlatformType) != true)
{
return false;
}
if ((argumentValue is IPropertyReferenceOperation propertyReference) &&
propertyReference.Property.ContainingType.Equals(osPlatformType))
{
if (propertyReference.Property.Name.Equals(OSX, StringComparison.OrdinalIgnoreCase))
{
decodedOsPlatformNamesBuilder.Add(macOS);
}
else
{
decodedOsPlatformNamesBuilder.Add(propertyReference.Property.Name);
}
return true;
}
if (valueContentAnalysisResult != null)
{
var valueContentValue = valueContentAnalysisResult[argumentValue];
if (valueContentValue.IsLiteralState)
{
decodedOsPlatformNamesBuilder.AddRange(valueContentValue.LiteralValues.OfType<string>());
return decodedOsPlatformNamesBuilder.Count > 0;
}
}
return false;
}
public override string ToString()
{
var result = $"{PlatformName};{Version}";
if (Negated)
{
result = $"!{result}";
}
return result;
}
public bool Equals(PlatformMethodValue other)
=> PlatformName.Equals(other.PlatformName, StringComparison.OrdinalIgnoreCase) &&
Version.Equals(other.Version) &&
Negated == other.Negated;
public override bool Equals(object obj)
=> obj is PlatformMethodValue otherInfo && Equals(otherInfo);
public override int GetHashCode()
{
return RoslynHashCode.Combine(
PlatformName.GetHashCode(),
Version.GetHashCode(),
Negated.GetHashCode());
}
bool IEquatable<IAbstractAnalysisValue>.Equals(IAbstractAnalysisValue other)
=> other is PlatformMethodValue otherInfo && Equals(otherInfo);
public static bool operator ==(PlatformMethodValue left, PlatformMethodValue right)
{
return left.Equals(right);
}
public static bool operator !=(PlatformMethodValue left, PlatformMethodValue right)
{
return !(left == right);
}
}
private static bool TryDecodeOSVersion(
ImmutableArray<IArgumentOperation> arguments,
ValueContentAnalysisResult? valueContentAnalysisResult,
[NotNullWhen(returnValue: true)] out Version? osVersion,
int skip = 0)
{
using var versionBuilder = ArrayBuilder<int>.GetInstance(4, fillWithValue: 0);
var index = 0;
foreach (var argument in arguments.GetArgumentsInParameterOrder().Skip(skip))
{
if (!TryDecodeOSVersionPart(argument, valueContentAnalysisResult, out var osVersionPart))
{
osVersion = null;
return false;
}
versionBuilder[index++] = osVersionPart;
}
osVersion = CreateVersion(versionBuilder);
return true;
static bool TryDecodeOSVersionPart(IArgumentOperation argument, ValueContentAnalysisResult? valueContentAnalysisResult, out int osVersionPart)
{
if (argument.Value.ConstantValue.HasValue &&
argument.Value.ConstantValue.Value is int versionPart)
{
osVersionPart = versionPart;
return true;
}
if (valueContentAnalysisResult != null)
{
var valueContentValue = valueContentAnalysisResult[argument.Value];
if (valueContentValue.IsLiteralState &&
valueContentValue.LiteralValues.Count == 1 &&
valueContentValue.LiteralValues.Single() is int part)
{
osVersionPart = part;
return true;
}
}
osVersionPart = default;
return false;
}
static Version CreateVersion(ArrayBuilder<int> versionBuilder) => versionBuilder switch
{
[int major, int minor, 0, 0] => new Version(major, minor),
[int major, int minor, int build, 0] => new Version(major, minor, build),
[int major, int minor, int build, int revision] => new Version(major, minor, build, revision),
_ => EmptyVersion
};
}
}
}