File: Analyzers\PinvokeAnalyzer.cs
Web Access
Project: Microsoft.DotNet.CodeAnalysis.csproj (Microsoft.DotNet.CodeAnalysis)
// 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.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.IO;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
using System.Diagnostics;

namespace Microsoft.DotNet.CodeAnalysis.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class PinvokeAnalyzer : BaseAnalyzer
{
    private const string Title = "Invalid Pinvoke call";
    private const string MessageFormat = @"{0} is not supported on one\more targeted platforms.{1}";
    private const string Description = "The Pinvoke call should be removed.";
    private const string AnalyzerName = "PinvokeAnalyzer";

    private static DiagnosticDescriptor InvalidPinvokeCall = new DiagnosticDescriptor(DiagnosticIds.BCL0015.ToString(), Title, MessageFormat, AnalyzerName, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description);

    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(InvalidPinvokeCall); } }

    public override void OnCompilationStart(CompilationStartAnalysisContext obj)
    {
        _allowedPinvokeFile = obj.Options.AdditionalFiles.FirstOrDefault(f => Path.GetFileName(f.Path).IndexOf("PinvokeAnalyzer_", StringComparison.OrdinalIgnoreCase) >= 0);
        _exceptionFile = obj.Options.AdditionalFiles.FirstOrDefault(f => Path.GetFileName(f.Path).IndexOf("PinvokeAnalyzerExceptionList.analyzerdata", StringComparison.OrdinalIgnoreCase) >= 0);
        obj.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method);
    }

    private Dictionary<string, string> _allowedPinvokes;
    private Dictionary<string, string> _exceptionPinvokes;

    private Dictionary<string, string> AllowedPinvokes
    {
        get
        {
            if (_allowedPinvokes == null)
            {
                _allowedPinvokes = ParseAdditionalFile(_allowedPinvokeFile);
            }

            return _allowedPinvokes;
        }
    }

    private Dictionary<string, string> ExceptionPinvokes
    {
        get
        {
            if (_exceptionPinvokes == null)
            {
                _exceptionPinvokes = ParseAdditionalFile(_exceptionFile);
            }

            return _exceptionPinvokes;
        }
    }

    private AdditionalText _exceptionFile;
    private AdditionalText _allowedPinvokeFile;

    private HashSet<string> _isNotSupportedOnWin7 = new HashSet<string>(StringComparer.OrdinalIgnoreCase);


    private Dictionary<string, string> ParseAdditionalFile(AdditionalText additionalFile)
    {
        Dictionary<string, string> parsedPinvokes = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

        if (additionalFile == null) return parsedPinvokes;

        SourceText fileContents = additionalFile.GetText();
        foreach (TextLine line in fileContents.Lines)
        {
            string lineStr = line.ToString();
            if (!string.IsNullOrWhiteSpace(lineStr) && !lineStr.StartsWith("<!--"))
            {
                string[] splitCount = lineStr.Split('!');

                if (splitCount.Length == 2 || splitCount.Length == 3)
                {
                    parsedPinvokes[splitCount[1]] = splitCount[0];
                    if (splitCount.Length == 3)
                    {
                        _isNotSupportedOnWin7.Add(splitCount[1]);
                    }
                }
            }
        }

        return parsedPinvokes;
    }

    private const string AltMsgString = @"Consider using {0} instead.";
    private const string NotSupportedOnWin7 = @"{0} is not supported on win7";


    // This method looks at the additional lists passed to ensure if the method call is valid.
    // This is the algorithm used by the method
    // 1. a. Check whether the moduleName!methodName is present as-is in the allowed list.
    //    b. If present validate whether it is also present on win7.
    // 2. Check if the moduleName!methodName is present in exception list.
    // 3. If not, check whether the methodName is present in another module than what is referenced.
    //    if so, make sure the error msg gives this as a suggestion to the user.
    private bool CheckIfMemberPresent(string methodName, string moduleName, ref string altMsg)
    {
        if (AllowedPinvokes.ContainsKey(methodName))
        {
            if (AllowedPinvokes[methodName].Equals(moduleName, StringComparison.OrdinalIgnoreCase))
            {
                if (_isNotSupportedOnWin7.Contains(methodName))
                {
                    altMsg = String.Format(NotSupportedOnWin7, AllowedPinvokes[methodName] + "!" + methodName);
                    return false;
                }

                return true;
            }
            else
            {
                if (String.IsNullOrEmpty(altMsg))
                    altMsg = String.Format(AltMsgString, AllowedPinvokes[methodName] + "!" + methodName);
            }
        }

        return (ExceptionPinvokes.ContainsKey(methodName) && ExceptionPinvokes[methodName].Equals(moduleName, StringComparison.OrdinalIgnoreCase));
    }

    private void AnalyzeMethod(SymbolAnalysisContext context)
    {
        var methodSymbol = context.Symbol as IMethodSymbol;
        if (methodSymbol == null) return;

        DllImportData data = methodSymbol.GetDllImportData();
        if (data == null) return;

        // Ignore QCall
        if (data.ModuleName.Equals("QCall", StringComparison.OrdinalIgnoreCase)) return;

        bool isPresent = false;
        string altMsg = string.Empty;

        // 1. If the method has an explicit entry point defined we validate if the moduleName!entryPoint is a valid combination.
        // 2. If not, we check whether the moduleName!methodName is valid or moduleName!methodNameW (we only support Unicode) is valid.
        if (data.EntryPointName != null)
        {
            isPresent = CheckIfMemberPresent(data.EntryPointName, data.ModuleName, ref altMsg);
        }
        else
        {
            isPresent = CheckIfMemberPresent(methodSymbol.Name, data.ModuleName, ref altMsg)
                || CheckIfMemberPresent(methodSymbol.Name + 'W', data.ModuleName, ref altMsg);
        }

        if (!isPresent)
        {
            foreach (SyntaxReference synref in methodSymbol.DeclaringSyntaxReferences)
            {
                context.ReportDiagnostic(Diagnostic.Create(InvalidPinvokeCall, synref.GetSyntax().GetLocation(), data.ModuleName + "!" + (data.EntryPointName ?? methodSymbol.Name), altMsg));
            }
        }
    }
}