Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
// 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.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.DotnetRuntime.Extensions;
using Microsoft.Interop;

namespace Microsoft.Interop.Analyzers
{
/// <summary>
/// Analyzer that reports diagnostics for LibraryImport methods.
/// This analyzer runs the same diagnostic logic as LibraryImportGenerator
/// but reports diagnostics separately from the source generator.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class LibraryImportDiagnosticsAnalyzer : DiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
ImmutableArray.Create(
GeneratorDiagnostics.InvalidAttributedMethodSignature,
GeneratorDiagnostics.InvalidAttributedMethodContainingTypeMissingModifiers,
GeneratorDiagnostics.InvalidStringMarshallingConfiguration,
GeneratorDiagnostics.ParameterTypeNotSupported,
GeneratorDiagnostics.ReturnTypeNotSupported,
GeneratorDiagnostics.ParameterTypeNotSupportedWithDetails,
GeneratorDiagnostics.ReturnTypeNotSupportedWithDetails,
GeneratorDiagnostics.ParameterConfigurationNotSupported,
GeneratorDiagnostics.ReturnConfigurationNotSupported,
GeneratorDiagnostics.MarshalAsParameterConfigurationNotSupported,
GeneratorDiagnostics.MarshalAsReturnConfigurationNotSupported,
GeneratorDiagnostics.ConfigurationNotSupported,
GeneratorDiagnostics.ConfigurationValueNotSupported,
GeneratorDiagnostics.MarshallingAttributeConfigurationNotSupported,
GeneratorDiagnostics.CannotForwardToDllImport,
GeneratorDiagnostics.RequiresAllowUnsafeBlocks,
GeneratorDiagnostics.UnnecessaryParameterMarshallingInfo,
GeneratorDiagnostics.UnnecessaryReturnMarshallingInfo,
GeneratorDiagnostics.SizeOfInCollectionMustBeDefinedAtCallOutParam,
GeneratorDiagnostics.SizeOfInCollectionMustBeDefinedAtCallReturnValue,
GeneratorDiagnostics.LibraryImportUsageDoesNotFollowBestPractices);

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(context =>
{
// Nothing to do if the LibraryImportAttribute is not in the compilation
INamedTypeSymbol? libraryImportAttrType = context.Compilation.GetBestTypeByMetadataName(TypeNames.LibraryImportAttribute);
if (libraryImportAttrType is null)
return;

StubEnvironment env = new StubEnvironment(
context.Compilation,
context.Compilation.GetEnvironmentFlags());

// Track if we found any LibraryImport methods to report RequiresAllowUnsafeBlocks once
bool foundLibraryImportMethod = false;
bool unsafeEnabled = context.Compilation.Options is CSharpCompilationOptions { AllowUnsafe: true };

context.RegisterSymbolAction(symbolContext =>
{
if (AnalyzeMethod(symbolContext, env, libraryImportAttrType))
{
foundLibraryImportMethod = true;
}
}, SymbolKind.Method);

// Report RequiresAllowUnsafeBlocks once per compilation if there are LibraryImport methods and unsafe is not enabled
context.RegisterCompilationEndAction(endContext =>
{
if (foundLibraryImportMethod && !unsafeEnabled)
{
endContext.ReportDiagnostic(DiagnosticInfo.Create(GeneratorDiagnostics.RequiresAllowUnsafeBlocks, null).ToDiagnostic());
}
});
});
}

/// <summary>
/// Analyzes a method for LibraryImport diagnostics.
/// </summary>
/// <returns>True if the method has LibraryImportAttribute, false otherwise.</returns>
private static bool AnalyzeMethod(SymbolAnalysisContext context, StubEnvironment env, INamedTypeSymbol libraryImportAttrType)
{
IMethodSymbol method = (IMethodSymbol)context.Symbol;

// Only analyze methods with LibraryImportAttribute
AttributeData? libraryImportAttr = null;
foreach (AttributeData attr in method.GetAttributes())
{
if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, libraryImportAttrType))
{
libraryImportAttr = attr;
break;
}
}

if (libraryImportAttr is null)
return false;

// Find the method syntax
foreach (SyntaxReference syntaxRef in method.DeclaringSyntaxReferences)
{
if (syntaxRef.GetSyntax(context.CancellationToken) is MethodDeclarationSyntax methodSyntax)
{
AnalyzeMethodSyntax(context, methodSyntax, method, libraryImportAttr, env);
break;
}
}

return true;
}

private static void AnalyzeMethodSyntax(
SymbolAnalysisContext context,
MethodDeclarationSyntax methodSyntax,
IMethodSymbol method,
AttributeData libraryImportAttr,
StubEnvironment env)
{
// Check for invalid method signature
DiagnosticInfo? invalidMethodDiagnostic = GetDiagnosticIfInvalidMethodForGeneration(methodSyntax, method);
if (invalidMethodDiagnostic is not null)
{
context.ReportDiagnostic(invalidMethodDiagnostic.ToDiagnostic());
return; // Don't continue analysis if the method is invalid
}

// Note: RequiresAllowUnsafeBlocks is reported once per compilation in Initialize method

// Get generator options
LibraryImportGeneratorOptions options = new(context.Options.AnalyzerConfigOptionsProvider.GlobalOptions);

// Calculate stub information and collect diagnostics
var diagnostics = CalculateDiagnostics(methodSyntax, method, libraryImportAttr, env, options, context.CancellationToken);

foreach (DiagnosticInfo diagnostic in diagnostics)
{
context.ReportDiagnostic(diagnostic.ToDiagnostic());
}
}

private static ImmutableArray<DiagnosticInfo> CalculateDiagnostics(
MethodDeclarationSyntax originalSyntax,
IMethodSymbol symbol,
AttributeData libraryImportAttr,
StubEnvironment environment,
LibraryImportGeneratorOptions options,
CancellationToken ct)
{
ct.ThrowIfCancellationRequested();

var locations = new MethodSignatureDiagnosticLocations(originalSyntax);
var generatorDiagnostics = new GeneratorDiagnosticsBag(
new DiagnosticDescriptorProvider(),
locations,
SR.ResourceManager,
typeof(FxResources.Microsoft.Interop.LibraryImportGenerator.SR));

// Process the LibraryImport attribute
LibraryImportCompilationData? libraryImportData = ProcessLibraryImportAttribute(libraryImportAttr);

// If we can't parse the attribute, we have an invalid compilation - stop processing
if (libraryImportData is null)
{
return generatorDiagnostics.Diagnostics.ToImmutableArray();
}

if (libraryImportData.IsUserDefined.HasFlag(InteropAttributeMember.StringMarshalling))
{
// User specified StringMarshalling.Custom without specifying StringMarshallingCustomType
if (libraryImportData.StringMarshalling == StringMarshalling.Custom && libraryImportData.StringMarshallingCustomType is null)
{
generatorDiagnostics.ReportInvalidStringMarshallingConfiguration(
libraryImportAttr, symbol.Name, SR.InvalidStringMarshallingConfigurationMissingCustomType);
}

// User specified something other than StringMarshalling.Custom while specifying StringMarshallingCustomType
if (libraryImportData.StringMarshalling != StringMarshalling.Custom && libraryImportData.StringMarshallingCustomType is not null)
{
generatorDiagnostics.ReportInvalidStringMarshallingConfiguration(
libraryImportAttr, symbol.Name, SR.InvalidStringMarshallingConfigurationNotCustom);
}
}

// Check for unsupported LCIDConversion attribute
INamedTypeSymbol? lcidConversionAttrType = environment.LcidConversionAttrType;
if (lcidConversionAttrType is not null)
{
foreach (AttributeData attr in symbol.GetAttributes())
{
if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, lcidConversionAttrType))
{
generatorDiagnostics.ReportConfigurationNotSupported(attr, nameof(TypeNames.LCIDConversionAttribute));
break;
}
}
}

// Create the signature context to collect marshalling-related diagnostics
var signatureContext = SignatureContext.Create(
symbol,
DefaultMarshallingInfoParser.Create(environment, generatorDiagnostics, symbol, libraryImportData, libraryImportAttr),
environment,
new CodeEmitOptions(SkipInit: true),
typeof(LibraryImportGenerator).Assembly);

// If forwarders are not being generated, we need to run stub generation logic to get those diagnostics too
if (!options.GenerateForwarders)
{
IMarshallingGeneratorResolver resolver = DefaultMarshallingGeneratorResolver.Create(
environment.EnvironmentFlags,
MarshalDirection.ManagedToUnmanaged,
TypeNames.LibraryImportAttribute_ShortName,
[]);

// Check marshalling generators - this collects diagnostics for marshalling issues
_ = new ManagedToNativeStubGenerator(
signatureContext.ElementTypeInformation,
LibraryImportData.From(libraryImportData).SetLastError,
generatorDiagnostics,
resolver,
new CodeEmitOptions(SkipInit: true));
}

return generatorDiagnostics.Diagnostics.ToImmutableArray();
}

private static LibraryImportCompilationData? ProcessLibraryImportAttribute(AttributeData attrData)
{
// Found the LibraryImport, but it has an error so report the error.
// This is most likely an issue with targeting an incorrect TFM.
if (attrData.AttributeClass?.TypeKind is null or TypeKind.Error)
{
return null;
}

if (attrData.ConstructorArguments.Length == 0)
{
return null;
}

ImmutableDictionary<string, TypedConstant> namedArguments = ImmutableDictionary.CreateRange(attrData.NamedArguments);

string? entryPoint = null;
if (namedArguments.TryGetValue(nameof(LibraryImportCompilationData.EntryPoint), out TypedConstant entryPointValue))
{
if (entryPointValue.Value is not string)
{
return null;
}
entryPoint = (string)entryPointValue.Value!;
}

return new LibraryImportCompilationData(attrData.ConstructorArguments[0].Value!.ToString())
{
EntryPoint = entryPoint,
}.WithValuesFromNamedArguments(namedArguments);
}

private static DiagnosticInfo? GetDiagnosticIfInvalidMethodForGeneration(MethodDeclarationSyntax methodSyntax, IMethodSymbol method)
{
// Verify the method has no generic types or defined implementation
// and is marked static and partial.
if (methodSyntax.TypeParameterList is not null
|| methodSyntax.Body is not null
|| !methodSyntax.Modifiers.Any(SyntaxKind.StaticKeyword)
|| !methodSyntax.Modifiers.Any(SyntaxKind.PartialKeyword))
{
return DiagnosticInfo.Create(GeneratorDiagnostics.InvalidAttributedMethodSignature, methodSyntax.Identifier.GetLocation(), method.Name);
}

// Verify that the types the method is declared in are marked partial.
if (methodSyntax.Parent is TypeDeclarationSyntax typeDecl && !typeDecl.IsInPartialContext(out var nonPartialIdentifier))
{
return DiagnosticInfo.Create(GeneratorDiagnostics.InvalidAttributedMethodContainingTypeMissingModifiers, methodSyntax.Identifier.GetLocation(), method.Name, nonPartialIdentifier);
}

// Verify the method does not have a ref return
if (method.ReturnsByRef || method.ReturnsByRefReadonly)
{
return DiagnosticInfo.Create(GeneratorDiagnostics.ReturnConfigurationNotSupported, methodSyntax.Identifier.GetLocation(), "ref return", method.ToDisplayString());
}

return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,39 +50,16 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
.Where(
static modelData => modelData is not null);

// Validate if attributed methods can have source generated
var methodsWithDiagnostics = attributedMethods.Select(static (data, ct) =>
{
DiagnosticInfo? diagnostic = GetDiagnosticIfInvalidMethodForGeneration(data.Syntax, data.Symbol);
return diagnostic is not null
? DiagnosticOr<(MethodDeclarationSyntax Syntax, IMethodSymbol Symbol)>.From(diagnostic)
: DiagnosticOr<(MethodDeclarationSyntax Syntax, IMethodSymbol Symbol)>.From((data.Syntax, data.Symbol));
});

var methodsToGenerate = context.FilterAndReportDiagnostics(methodsWithDiagnostics);
// Filter out methods that are invalid for generation (diagnostics are reported by the analyzer)
var methodsToGenerate = attributedMethods
.Where(static data => IsValidMethodForGeneration(data.Syntax, data.Symbol));

// Compute generator options
IncrementalValueProvider<LibraryImportGeneratorOptions> stubOptions = context.AnalyzerConfigOptionsProvider
.Select(static (options, ct) => new LibraryImportGeneratorOptions(options.GlobalOptions));

IncrementalValueProvider<StubEnvironment> stubEnvironment = context.CreateStubEnvironmentProvider();

// Validate environment that is being used to generate stubs.
context.RegisterDiagnostics(
context.CompilationProvider
.Select((comp, ct) => comp.Options is CSharpCompilationOptions { AllowUnsafe: true })
.Combine(attributedMethods.Collect())
.SelectMany((data, ct) =>
{
if (data.Right.IsEmpty // no attributed methods
|| data.Left) // Unsafe code enabled
{
return ImmutableArray<DiagnosticInfo>.Empty;
}

return ImmutableArray.Create(DiagnosticInfo.Create(GeneratorDiagnostics.RequiresAllowUnsafeBlocks, null));
}));

IncrementalValuesProvider<(MemberDeclarationSyntax, ImmutableArray<DiagnosticInfo>)> generateSingleStub = methodsToGenerate
.Combine(stubEnvironment)
.Combine(stubOptions)
Expand All @@ -104,8 +81,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
.WithComparer(Comparers.GeneratedSyntax)
.WithTrackingName(StepNames.GenerateSingleStub);

context.RegisterDiagnostics(generateSingleStub.SelectMany((stubInfo, ct) => stubInfo.Item2));

context.RegisterConcatenatedSyntaxOutputs(generateSingleStub.Select((data, ct) => data.Item1), "LibraryImports.g.cs");
}

Expand Down Expand Up @@ -550,6 +525,33 @@ static ExpressionSyntax CreateEnumExpressionSyntax<T>(T value) where T : Enum
}
}

private static bool IsValidMethodForGeneration(MethodDeclarationSyntax methodSyntax, IMethodSymbol method)
{
// Verify the method has no generic types or defined implementation
// and is marked static and partial.
if (methodSyntax.TypeParameterList is not null
|| methodSyntax.Body is not null
|| !methodSyntax.Modifiers.Any(SyntaxKind.StaticKeyword)
|| !methodSyntax.Modifiers.Any(SyntaxKind.PartialKeyword))
{
return false;
}

// Verify that the types the method is declared in are marked partial.
if (methodSyntax.Parent is TypeDeclarationSyntax typeDecl && !typeDecl.IsInPartialContext(out _))
{
return false;
}

// Verify the method does not have a ref return
if (method.ReturnsByRef || method.ReturnsByRefReadonly)
{
return false;
}

return true;
}

private static DiagnosticInfo? GetDiagnosticIfInvalidMethodForGeneration(MethodDeclarationSyntax methodSyntax, IMethodSymbol method)
{
// Verify the method has no generic types or defined implementation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
using Xunit;
using static Microsoft.Interop.UnitTests.TestUtils;
using StringMarshalling = System.Runtime.InteropServices.StringMarshalling;
using VerifyComInterfaceGenerator = Microsoft.Interop.UnitTests.Verifiers.CSharpSourceGeneratorVerifier<Microsoft.Interop.ComInterfaceGenerator>;
using VerifyComInterfaceGenerator = Microsoft.Interop.UnitTests.Verifiers.CSharpSourceGeneratorVerifier<Microsoft.Interop.ComInterfaceGenerator, Microsoft.CodeAnalysis.Testing.EmptyDiagnosticAnalyzer>;

namespace ComInterfaceGenerator.Unit.Tests
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
using Microsoft.CodeAnalysis.Testing;
using Microsoft.Interop;
using Xunit;
using VerifyCS = Microsoft.Interop.UnitTests.Verifiers.CSharpSourceGeneratorVerifier<Microsoft.Interop.ComClassGenerator>;
using VerifyCS = Microsoft.Interop.UnitTests.Verifiers.CSharpSourceGeneratorVerifier<Microsoft.Interop.ComClassGenerator, Microsoft.CodeAnalysis.Testing.EmptyDiagnosticAnalyzer>;

namespace ComInterfaceGenerator.Unit.Tests
{
Expand Down
Loading
Loading