File: Microsoft.NetCore.Analyzers\InteropServices\UseValidPlatformString.cs
Web Access
Project: src\sdk\src\Microsoft.CodeAnalysis.NetAnalyzers\src\Microsoft.CodeAnalysis.NetAnalyzers\Microsoft.CodeAnalysis.NetAnalyzers.csproj (Microsoft.CodeAnalysis.NetAnalyzers)
// 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.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;

namespace Microsoft.NetCore.Analyzers.InteropServices
{
    using static MicrosoftNetCoreAnalyzersResources;

    /// <summary>
    /// CA1418: <inheritdoc cref="UseValidPlatformStringTitle"/>
    /// </summary>
    [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
    public sealed class UseValidPlatformString : DiagnosticAnalyzer
    {
        internal const string RuleId = "CA1418";
        private static readonly ImmutableArray<SymbolKind> s_symbols = ImmutableArray.Create(SymbolKind.NamedType, SymbolKind.Method, SymbolKind.Property, SymbolKind.Field, SymbolKind.Event);
        private static readonly ImmutableArray<string> methodNames = ImmutableArray.Create("IsOSPlatform", "IsOSPlatformVersionAtLeast");
        private const string IsPrefix = "Is";
        private const string VersionSuffix = "VersionAtLeast";
        private const string macOS = nameof(macOS);
        private const string Browser = nameof(Browser);

        private static readonly LocalizableString s_localizableTitle = CreateLocalizableResourceString(nameof(UseValidPlatformStringTitle));
        private static readonly LocalizableString s_localizableDescription = CreateLocalizableResourceString(nameof(UseValidPlatformStringDescription));

        internal static readonly DiagnosticDescriptor UnknownPlatform = DiagnosticDescriptorHelper.Create(RuleId,
                                                                              s_localizableTitle,
                                                                              CreateLocalizableResourceString(nameof(UseValidPlatformStringUnknownPlatform)),
                                                                              DiagnosticCategory.Interoperability,
                                                                              RuleLevel.BuildWarning,
                                                                              description: s_localizableDescription,
                                                                              isPortedFxCopRule: false,
                                                                              isDataflowRule: false);

        internal static readonly DiagnosticDescriptor InvalidVersion = DiagnosticDescriptorHelper.Create(RuleId,
                                                                              s_localizableTitle,
                                                                              CreateLocalizableResourceString(nameof(UseValidPlatformStringInvalidVersion)),
                                                                              DiagnosticCategory.Interoperability,
                                                                              RuleLevel.BuildWarning,
                                                                              description: s_localizableDescription,
                                                                              isPortedFxCopRule: false,
                                                                              isDataflowRule: false);

        internal static readonly DiagnosticDescriptor NoVersion = DiagnosticDescriptorHelper.Create(RuleId,
                                                                              s_localizableTitle,
                                                                              CreateLocalizableResourceString(nameof(UseValidPlatformStringNoVersion)),
                                                                              DiagnosticCategory.Interoperability,
                                                                              RuleLevel.BuildWarning,
                                                                              description: s_localizableDescription,
                                                                              isPortedFxCopRule: false,
                                                                              isDataflowRule: false);

        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(UnknownPlatform, InvalidVersion, NoVersion);

        public override void Initialize(AnalysisContext context)
        {
            context.EnableConcurrentExecution();
            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);

            context.RegisterCompilationStartAction(context =>
            {
                if (!context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemOperatingSystem, out var operatingSystemType) ||
                    !context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemRuntimeVersioningSupportedOSPlatformAttribute, out var supportedAttriubte) ||
                    !context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemRuntimeVersioningUnsupportedOSPlatformAttribute, out var unsupportedAttribute))
                {
                    return;
                }

                var knownPlatforms = PooledDictionary<string, int>.GetInstance(StringComparer.OrdinalIgnoreCase);
                AddPlatformsAndVersionCountFromGuardMethods(operatingSystemType, knownPlatforms);
                AddPlatformsFromMsBuildOptions(knownPlatforms, context.Options.GetMSBuildItemMetadataValues(
                    MSBuildItemOptionNames.SupportedPlatform, context.Compilation));

                if (knownPlatforms.TryGetValue(macOS, out var versions))
                {
                    knownPlatforms.Add("OSX", versions);
                }

                if (knownPlatforms.TryGetValue(Browser, out versions) && versions == 0)
                {
                    // Browser is a special case, we don't have a version guard method for it but we need to support a browser1.0 target platform
                    knownPlatforms[Browser] = 2;
                }

                context.RegisterOperationAction(context => AnalyzeOperation(context.Operation, context, knownPlatforms), OperationKind.Invocation);
                context.RegisterSymbolAction(context => AnalyzeSymbol(
                    static (context, diagnostic) => context.ReportDiagnostic(diagnostic),
                    context,
                    context.Symbol,
                    supportedAttriubte, unsupportedAttribute, knownPlatforms, context.CancellationToken), s_symbols);
                context.RegisterCompilationEndAction(context => AnalyzeSymbol(
                    static (context, diagnostic) => context.ReportDiagnostic(diagnostic),
                    context,
                    context.Compilation.Assembly,
                    supportedAttriubte, unsupportedAttribute, knownPlatforms, context.CancellationToken));
            });

            static void AddPlatformsAndVersionCountFromGuardMethods(INamedTypeSymbol operatingSystemType, PooledDictionary<string, int> knownPlatforms)
            {
                var methods = operatingSystemType.GetMembers().OfType<IMethodSymbol>();
                foreach (var m in methods)
                {
                    if (m.IsStatic &&
                        m.ReturnType.SpecialType == SpecialType.System_Boolean &&
                        NameAndParametersValid(m))
                    {
                        var (platformName, versionPartsCount) = ExtractPlatformAndVersionCount(m);
                        if (!knownPlatforms.TryGetValue(platformName, out var count) ||
                            versionPartsCount > count)
                        {
                            knownPlatforms[platformName] = versionPartsCount; // only keep highest count
                        }
                    }
                }
            }

            static void AddPlatformsFromMsBuildOptions(PooledDictionary<string, int> knownPlatforms, ImmutableArray<string> msBuildPlatforms)
            {
                foreach (var platform in msBuildPlatforms)
                {
                    if (!knownPlatforms.ContainsKey(platform))
                    {
                        knownPlatforms.Add(platform, 4); // Default version count is 4
                    }
                }
            }

            static (string platformName, int versionPartsCount) ExtractPlatformAndVersionCount(IMethodSymbol method)
            {
                var name = method.Name;
                if (name.EndsWith(VersionSuffix, StringComparison.Ordinal))
                {
                    return (name[2..(name.Length - VersionSuffix.Length)], method.Parameters.Length);
                }

                return (name[2..], 0);
            }

            static bool NameAndParametersValid(IMethodSymbol method) =>
                method.Name.StartsWith(IsPrefix, StringComparison.Ordinal) &&
                (method.Parameters.Length == 0 || method.Name.EndsWith(VersionSuffix, StringComparison.Ordinal));
        }

        private static void AnalyzeOperation(IOperation operation, OperationAnalysisContext context, PooledDictionary<string, int> knownPlatforms)
        {
            var invocation = (IInvocationOperation)operation;
            if (methodNames.Contains(invocation.TargetMethod.Name) &&
                invocation.Arguments.Length > 0 &&
                invocation.Arguments[0].Value is { } argument &&
                argument.ConstantValue.HasValue &&
                argument.ConstantValue.Value is string platformName &&
                IsNotKnownPlatform(knownPlatforms, platformName))
            {
                context.ReportDiagnostic(argument.Syntax.CreateDiagnostic(UnknownPlatform, platformName));
            }
        }

        private static void AnalyzeSymbol<TContext>(Action<TContext, Diagnostic> reportDiagnostic, TContext context, ISymbol symbol, INamedTypeSymbol supportedAttribute,
            INamedTypeSymbol unsupportedAttribute, PooledDictionary<string, int> knownPlatforms, CancellationToken token)
        {
            foreach (var attribute in symbol.GetAttributes(supportedAttribute, unsupportedAttribute))
            {
                AnalyzeAttribute(reportDiagnostic, context, attribute, knownPlatforms, token);
            }
        }

        private static void AnalyzeAttribute<TContext>(Action<TContext, Diagnostic> reportDiagnostic, TContext context, AttributeData attributeData, PooledDictionary<string, int> knownPlatforms, CancellationToken token)
        {
            var constructorArguments = attributeData.ConstructorArguments;
            var syntaxReference = attributeData.ApplicationSyntaxReference;

            if (constructorArguments.Length == 1 && syntaxReference != null)
            {
                if (constructorArguments[0].Value is string value)
                {
                    AnalyzeStringParameter(reportDiagnostic, context, syntaxReference.GetSyntax(token), knownPlatforms, value);
                }
                else
                {
                    reportDiagnostic(context, syntaxReference.GetSyntax(token).CreateDiagnostic(UnknownPlatform, "null"));
                }
            }

            static void AnalyzeStringParameter(Action<TContext, Diagnostic> reportDiagnostic, TContext context, SyntaxNode syntax, PooledDictionary<string, int> knownPlatforms, string value)
            {
                if (TryParsePlatformNameAndVersion(value, out var platformName, out var versionPart, out var versionCount))
                {
                    if (!knownPlatforms.TryGetValue(platformName, out var count))
                    {
                        reportDiagnostic(context, syntax.CreateDiagnostic(UnknownPlatform, platformName));
                    }
                    else if (count == 0 && versionCount != 0)
                    {
                        reportDiagnostic(context, syntax.CreateDiagnostic(NoVersion, versionPart, platformName));
                    }
                    else if (count < versionCount)
                    {
                        var maxCount = count == 2 ? string.Empty : $"-{count}";
                        reportDiagnostic(context, syntax.CreateDiagnostic(InvalidVersion, versionPart, platformName, maxCount));
                    }
                }
                else
                {
                    // version were not parsable, check the platform name and version count
                    if (!knownPlatforms.TryGetValue(platformName, out var count))
                    {
                        reportDiagnostic(context, syntax.CreateDiagnostic(UnknownPlatform, platformName));
                    }
                    else if (count == 0 && versionPart.Length != 0)
                    {
                        reportDiagnostic(context, syntax.CreateDiagnostic(NoVersion, versionPart, platformName));
                    }
                    else
                    {
                        var maxCount = count == 2 ? string.Empty : $"-{count}";
                        reportDiagnostic(context, syntax.CreateDiagnostic(InvalidVersion, versionPart, platformName, maxCount));
                    }
                }
            }
        }

        private static bool IsNotKnownPlatform(PooledDictionary<string, int> knownPlatforms, string platformName) =>
            platformName.Length == 0 || !knownPlatforms.ContainsKey(platformName);

        private static bool TryParsePlatformNameAndVersion(string osString, out string osPlatformName, out string versionPart, out int versionCount)
        {
            versionCount = 0;
            versionPart = string.Empty;

            for (int i = 0; i < osString.Length; i++)
            {
                if (char.IsDigit(osString[i]))
                {
                    osPlatformName = osString[..i];
                    versionPart = osString[i..];
                    if (i > 0 && Version.TryParse(osString[i..], out Version _))
                    {
                        versionCount = osString.Count(ch => ch == '.') + 1;
                        return true;
                    }

                    return false;
                }
            }

            osPlatformName = osString;
            return true;
        }
    }
}