File: Microsoft.NetCore.Analyzers\InteropServices\PInvokeDiagnosticAnalyzer.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.Collections.Immutable;
using System.Linq;
using System.Runtime.InteropServices;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

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

    /// <summary>
    /// CA1401: <inheritdoc cref="PInvokesShouldNotBeVisibleTitle"/>
    /// CA2101: <inheritdoc cref="SpecifyMarshalingForPInvokeStringArgumentsTitle"/>
    /// </summary>
    [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
    public sealed class PInvokeDiagnosticAnalyzer : DiagnosticAnalyzer
    {
        public const string RuleCA1401Id = "CA1401";
        public const string RuleCA2101Id = "CA2101";

        internal static readonly DiagnosticDescriptor RuleCA1401 = DiagnosticDescriptorHelper.Create(
            RuleCA1401Id,
            CreateLocalizableResourceString(nameof(PInvokesShouldNotBeVisibleTitle)),
            CreateLocalizableResourceString(nameof(PInvokesShouldNotBeVisibleMessage)),
            DiagnosticCategory.Interoperability,
            RuleLevel.IdeSuggestion,
            description: CreateLocalizableResourceString(nameof(PInvokesShouldNotBeVisibleDescription)),
            isPortedFxCopRule: true,
            isDataflowRule: false);

        private static readonly LocalizableString s_localizableMessageAndTitleCA2101 = CreateLocalizableResourceString(nameof(SpecifyMarshalingForPInvokeStringArgumentsTitle));

        internal static readonly DiagnosticDescriptor RuleCA2101 = DiagnosticDescriptorHelper.Create(
            RuleCA2101Id,
            s_localizableMessageAndTitleCA2101,
            s_localizableMessageAndTitleCA2101,
            DiagnosticCategory.Globalization,
            RuleLevel.BuildWarningCandidate,
            description: CreateLocalizableResourceString(nameof(SpecifyMarshalingForPInvokeStringArgumentsDescription)),
            isPortedFxCopRule: true,
            isDataflowRule: false);

        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(RuleCA1401, RuleCA2101);

        public override void Initialize(AnalysisContext context)
        {
            context.EnableConcurrentExecution();
            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

            context.RegisterCompilationStartAction(
                (context) =>
                {
                    INamedTypeSymbol? dllImportType = context.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemRuntimeInteropServicesDllImportAttribute);
                    if (dllImportType == null)
                    {
                        return;
                    }

                    INamedTypeSymbol? marshalAsType = context.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemRuntimeInteropServicesMarshalAsAttribute);
                    if (marshalAsType == null)
                    {
                        return;
                    }

                    INamedTypeSymbol? stringBuilderType = context.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemTextStringBuilder);
                    if (stringBuilderType == null)
                    {
                        return;
                    }

                    INamedTypeSymbol? unmanagedType = context.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemRuntimeInteropServicesUnmanagedType);
                    if (unmanagedType == null)
                    {
                        return;
                    }

                    context.RegisterSymbolAction(new SymbolAnalyzer(dllImportType, marshalAsType, stringBuilderType, unmanagedType).AnalyzeSymbol, SymbolKind.Method);
                });
        }

        private sealed class SymbolAnalyzer
        {
            private readonly INamedTypeSymbol _dllImportType;
            private readonly INamedTypeSymbol _marshalAsType;
            private readonly INamedTypeSymbol _stringBuilderType;
            private readonly INamedTypeSymbol _unmanagedType;

            public SymbolAnalyzer(
                INamedTypeSymbol dllImportType,
                INamedTypeSymbol marshalAsType,
                INamedTypeSymbol stringBuilderType,
                INamedTypeSymbol unmanagedType)
            {
                _dllImportType = dllImportType;
                _marshalAsType = marshalAsType;
                _stringBuilderType = stringBuilderType;
                _unmanagedType = unmanagedType;
            }

            public void AnalyzeSymbol(SymbolAnalysisContext context)
            {
                var methodSymbol = (IMethodSymbol)context.Symbol;
                if (methodSymbol == null)
                {
                    return;
                }

                DllImportData? dllImportData = methodSymbol.GetDllImportData();
                if (dllImportData == null)
                {
                    return;
                }

                AttributeData? dllAttribute = methodSymbol.GetAttribute(_dllImportType);
                Location? defaultLocation = dllAttribute == null ? methodSymbol.Locations.FirstOrDefault() : GetAttributeLocation(dllAttribute);

                // CA1401 - PInvoke methods should not be visible
                if (methodSymbol.IsExternallyVisible())
                {
                    context.ReportDiagnostic(context.Symbol.CreateDiagnostic(RuleCA1401, methodSymbol.Name));
                }

                // CA2101 - Specify marshalling for PInvoke string arguments
                if (dllImportData.BestFitMapping != false &&
                    context.Options.GetMSBuildPropertyValue(MSBuildPropertyOptionNames.InvariantGlobalization, context.Compilation) is not "true")
                {
                    bool appliedCA2101ToMethod = false;
                    foreach (IParameterSymbol parameter in methodSymbol.Parameters)
                    {
                        if (parameter.Type.SpecialType == SpecialType.System_String || parameter.Type.Equals(_stringBuilderType))
                        {
                            AttributeData? marshalAsAttribute = parameter.GetAttribute(_marshalAsType);
                            CharSet? charSet = marshalAsAttribute == null
                                ? dllImportData.CharacterSet
                                : MarshalingToCharSet(GetParameterMarshaling(marshalAsAttribute));

                            // only unicode marshaling is considered safe
                            if (charSet != CharSet.Unicode)
                            {
                                if (marshalAsAttribute != null)
                                {
                                    // track the diagnostic on the [MarshalAs] attribute
                                    Location? marshalAsLocation = GetAttributeLocation(marshalAsAttribute);
                                    context.ReportDiagnostic(Diagnostic.Create(RuleCA2101, marshalAsLocation));
                                }
                                else if (!appliedCA2101ToMethod)
                                {
                                    // track the diagnostic on the [DllImport] attribute
                                    appliedCA2101ToMethod = true;
                                    context.ReportDiagnostic(Diagnostic.Create(RuleCA2101, defaultLocation));
                                }
                            }
                        }
                    }

                    // only unicode marshaling is considered safe, but only check this if we haven't already flagged the attribute
                    if (!appliedCA2101ToMethod && dllImportData.CharacterSet != CharSet.Unicode &&
                        (methodSymbol.ReturnType.SpecialType == SpecialType.System_String || methodSymbol.ReturnType.Equals(_stringBuilderType)))
                    {
                        context.ReportDiagnostic(Diagnostic.Create(RuleCA2101, defaultLocation));
                    }
                }
            }

            private UnmanagedType? GetParameterMarshaling(AttributeData attributeData)
            {
                if (!attributeData.ConstructorArguments.IsEmpty)
                {
                    TypedConstant argument = attributeData.ConstructorArguments.First();
                    if (SymbolEqualityComparer.Default.Equals(argument.Type, _unmanagedType))
                    {
                        return (UnmanagedType)argument.Value!;
                    }
                    else if (argument.Type?.SpecialType == SpecialType.System_Int16)
                    {
                        return (UnmanagedType)(short)argument.Value!;
                    }
                }

                return null;
            }

            private static CharSet? MarshalingToCharSet(UnmanagedType? type)
            {
                if (type == null)
                {
                    return null;
                }

#pragma warning disable CS0618 // Type or member is obsolete
                switch (type)
                {
                    case UnmanagedType.AnsiBStr:
                    case UnmanagedType.LPStr:
                    case UnmanagedType.VBByRefStr:
                        return CharSet.Ansi;
                    case UnmanagedType.BStr:
                    case UnmanagedType.LPWStr:
                        return CharSet.Unicode;
                    case UnmanagedType.ByValTStr:
                    case UnmanagedType.LPTStr:
                    case UnmanagedType.TBStr:
                    default:
                        // CharSet.Auto and CharSet.None are not available in the portable
                        // profiles. We are not interested in those values for our analysis and so simply
                        // return null
                        return null;
                }
#pragma warning restore CS0618 // Type or member is obsolete
            }

            private static Location? GetAttributeLocation(AttributeData attributeData)
            {
                return attributeData.ApplicationSyntaxReference?.SyntaxTree.GetLocation(attributeData.ApplicationSyntaxReference.Span);
            }
        }
    }
}