Skip to content

Commit

Permalink
Platform compatibility analyzer (#3917)
Browse files Browse the repository at this point in the history
Co-authored-by: Manish Vasani <mavasani@microsoft.com>
  • Loading branch information
buyaa-n and mavasani authored Aug 26, 2020
1 parent a4908d8 commit 36307a7
Show file tree
Hide file tree
Showing 33 changed files with 6,970 additions and 1,193 deletions.
2 changes: 1 addition & 1 deletion src/NetAnalyzers/Core/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ CA1308 | Globalization | Disabled | NormalizeStringsToUppercaseAnalyzer, [Docume
CA1309 | Globalization | Hidden | UseOrdinalStringComparisonAnalyzer, [Documentation](https://docs.microsoft.com/visualstudio/code-quality/ca1309)
CA1401 | Interoperability | Info | PInvokeDiagnosticAnalyzer, [Documentation](https://docs.microsoft.com/visualstudio/code-quality/ca1401)
CA1414 | Interoperability | Disabled | MarkBooleanPInvokeArgumentsWithMarshalAsAnalyzer, [Documentation](https://docs.microsoft.com/visualstudio/code-quality/ca1414)
CA1416 | Interoperability | Info | RuntimePlatformCheckAnalyzer, [Documentation](https://docs.microsoft.com/visualstudio/code-quality/ca1416)
CA1416 | Interoperability | Warning | PlatformCompatabilityAnalyzer, [Documentation](https://docs.microsoft.com/visualstudio/code-quality/ca1416)
CA1417 | Interoperability | Warning | DoNotUseOutAttributeStringPInvokeParametersAnalyzer, [Documentation](https://docs.microsoft.com/visualstudio/code-quality/ca1417)
CA1500 | Maintainability | Disabled | VariableNamesShouldNotMatchFieldNamesAnalyzer, [Documentation](https://docs.microsoft.com/visualstudio/code-quality/ca1500)
CA1501 | Maintainability | Disabled | CodeMetricsAnalyzer, [Documentation](https://docs.microsoft.com/visualstudio/code-quality/ca1501)
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

namespace Microsoft.NetCore.Analyzers.InteropServices
{
public sealed partial class PlatformCompatabilityAnalyzer
{
/// <summary>
/// Class used for keeping platform information of an API, all properties are optional.
///
/// We need to keep only 2 values for [SupportedOSPlatform] attribute, first one will be the lowest version found, mostly for assembly level
/// attribute which denotes when the API first introduced, second one would keep new APIs added later and requries higher platform version
/// (if there is multiple version found in the API parents chain we will keep only highest version)
///
/// Same for [UnsupportedOSPlatform] attribute, an API could be unsupported at first and then start supported from some version then eventually removed.
/// So we only keep at most 2 versions of [UnsupportedOSPlatform] first one will be the lowest version found, second one will be second lowest if there is any
///
/// Properties:
/// - SupportedFirst - keeps lowest version of [SupportedOSPlatform] attribute found
/// - SupportedSecond - keeps the highest version of [SupportedOSPlatform] attribute if there is any
/// - UnsupportedFirst - keeps the lowest version of [UnsupportedOSPlatform] attribute found
/// - UnsupportedSecond - keeps the second lowest version of [UnsupportedOSPlatform] attribute found
/// </summary>
private class PlatformAttributes
{
public Version? SupportedFirst { get; set; }
public Version? SupportedSecond { get; set; }
public Version? UnsupportedFirst { get; set; }
public Version? UnsupportedSecond { get; set; }
public bool HasAttribute() => SupportedFirst != null || UnsupportedFirst != null ||
SupportedSecond != null || UnsupportedSecond != null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace Microsoft.NetCore.Analyzers.InteropServices
{
public sealed partial class RuntimePlatformCheckAnalyzer
public sealed partial class PlatformCompatabilityAnalyzer
{
private sealed class OperationVisitor : GlobalFlowStateDataFlowOperationVisitor
{
Expand All @@ -34,16 +34,39 @@ public override GlobalFlowStateAnalysisValueSet VisitInvocation_NonLambdaOrDeleg
{
var value = base.VisitInvocation_NonLambdaOrDelegateOrLocalFunction(method, visitedInstance, visitedArguments, invokedAsDelegate, originalOperation, defaultValue);

if (_platformCheckMethods.Contains(method.OriginalDefinition) &&
!visitedArguments.IsEmpty)
if (_platformCheckMethods.Contains(method.OriginalDefinition))
{
return RuntimeOSPlatformInfo.TryDecode(method, visitedArguments, DataFlowAnalysisContext.ValueContentAnalysisResult, _osPlatformType, out var platformInfo) ?
return PlatformMethodValue.TryDecode(method, visitedArguments, DataFlowAnalysisContext.ValueContentAnalysisResult, _osPlatformType, out var platformInfo) ?
new GlobalFlowStateAnalysisValueSet(platformInfo) :
GlobalFlowStateAnalysisValueSet.Unknown;
}

return GetValueOrDefault(value);
}

public override GlobalFlowStateAnalysisValueSet VisitPropertyReference(IPropertyReferenceOperation operation, object? argument)
{
return GetValueOrDefault(base.VisitPropertyReference(operation, argument));
}

public override GlobalFlowStateAnalysisValueSet VisitFieldReference(IFieldReferenceOperation operation, object? argument)
{
return GetValueOrDefault(base.VisitFieldReference(operation, argument));
}

public override GlobalFlowStateAnalysisValueSet VisitObjectCreation(IObjectCreationOperation operation, object? argument)
{
return GetValueOrDefault(base.VisitObjectCreation(operation, argument));
}

public override GlobalFlowStateAnalysisValueSet VisitEventReference(IEventReferenceOperation operation, object? argument)
{
return GetValueOrDefault(base.VisitEventReference(operation, argument));
}
public override GlobalFlowStateAnalysisValueSet VisitMethodReference(IMethodReferenceOperation operation, object? argument)
{
return GetValueOrDefault(base.VisitMethodReference(operation, argument));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
// 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.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 PlatformCompatabilityAnalyzer
{
private readonly struct PlatformMethodValue : IAbstractAnalysisValue, IEquatable<PlatformMethodValue>
{
private PlatformMethodValue(string invokedPlatformCheckMethodName, string platformPropertyName, Version version, bool negated)
{
InvokedMethodName = invokedPlatformCheckMethodName ?? throw new ArgumentNullException(nameof(invokedPlatformCheckMethodName));
PlatformName = platformPropertyName ?? throw new ArgumentNullException(nameof(platformPropertyName));
Version = version ?? throw new ArgumentNullException(nameof(version));
Negated = negated;
}

public string InvokedMethodName { get; }
public string PlatformName { get; }
public Version Version { get; }
public bool Negated { get; }

public IAbstractAnalysisValue GetNegatedValue()
=> new PlatformMethodValue(InvokedMethodName, PlatformName, Version, !Negated);

public static bool TryDecode(
IMethodSymbol invokedPlatformCheckMethod,
ImmutableArray<IArgumentOperation> arguments,
ValueContentAnalysisResult? valueContentAnalysisResult,
INamedTypeSymbol osPlatformType,
[NotNullWhen(returnValue: true)] out PlatformMethodValue? info)
{
// Accelerators like OperatingSystem.IsPlatformName()
if (arguments.IsEmpty)
{
if (TryExtractPlatformName(invokedPlatformCheckMethod.Name, out var platformName))
{
info = new PlatformMethodValue(invokedPlatformCheckMethod.Name, platformName, new Version(0, 0), negated: false);
return true;
}
}
else
{
if (TryDecodeRuntimeInformationIsOSPlatform(arguments[0].Value, osPlatformType, out string? osPlatformName))
{
info = new PlatformMethodValue(invokedPlatformCheckMethod.Name, osPlatformName, new Version(0, 0), negated: false);
return true;
}

if (arguments.GetArgumentForParameterAtIndex(0).Value is ILiteralOperation literal)
{
if (literal.Type?.SpecialType == SpecialType.System_String &&
literal.ConstantValue.HasValue)
{
// OperatingSystem.IsOSPlatform(string platform)
if (invokedPlatformCheckMethod.Name == IsOSPlatform &&
TryParsePlatformNameAndVersion(literal.ConstantValue.Value.ToString(), out string platformName, out Version? version))
{
info = new PlatformMethodValue(invokedPlatformCheckMethod.Name, platformName, version, negated: false);
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");
info = new PlatformMethodValue(invokedPlatformCheckMethod.Name, literal.ConstantValue.Value.ToString(), version, negated: false);
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))
{
info = new PlatformMethodValue(invokedPlatformCheckMethod.Name, platformName, version, negated: false);
return true;
}
}
}
}

info = default;
return false;
}

private static bool TryExtractPlatformName(string methodName, [NotNullWhen(true)] out string? platformName)
{
if (!methodName.StartsWith(IsPrefix, StringComparison.Ordinal))
{
platformName = null;
return false;
}

if (methodName.EndsWith(OptionalSuffix, StringComparison.Ordinal))
{
platformName = methodName.Substring(2, methodName.Length - 2 - OptionalSuffix.Length);
return true;
}

platformName = methodName.Substring(2);
return true;
}

private static bool TryDecodeRuntimeInformationIsOSPlatform(
IOperation argumentValue,
INamedTypeSymbol osPlatformType,
[NotNullWhen(returnValue: true)] out string? osPlatformName)
{
if ((argumentValue is IPropertyReferenceOperation propertyReference) &&
propertyReference.Property.ContainingType.Equals(osPlatformType))
{
osPlatformName = propertyReference.Property.Name;
return true;
}

osPlatformName = null;
return false;
}

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)
{
if (versionBuilder[3] == 0)
{
if (versionBuilder[2] == 0)
{
return new Version(versionBuilder[0], versionBuilder[1]);
}
else
{
return new Version(versionBuilder[0], versionBuilder[1], versionBuilder[2]);
}
}
else
{
return new Version(versionBuilder[0], versionBuilder[1], versionBuilder[2], versionBuilder[3]);
}
}
}

public override string ToString()
{
var result = $"{InvokedMethodName};{PlatformName};{Version}";
if (Negated)
{
result = $"!{result}";
}

return result;
}

public bool Equals(PlatformMethodValue other)
=> InvokedMethodName.Equals(other.InvokedMethodName, StringComparison.OrdinalIgnoreCase) &&
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()
=> HashUtilities.Combine(InvokedMethodName.GetHashCode(), 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);
}
}
}
}
Loading

0 comments on commit 36307a7

Please sign in to comment.