Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
5ab07b3
Initial plan
Copilot Jan 29, 2026
87827dd
Add LibraryImportDiagnosticsAnalyzer and update generator to not emit…
Copilot Jan 29, 2026
4e3556e
Fix duplicate diagnostics in LibraryImportDiagnosticsAnalyzer
Copilot Jan 29, 2026
b0d4d92
Address code review feedback - fix RequiresAllowUnsafeBlocks and cond…
Copilot Jan 29, 2026
deaee03
Add LibraryImportDiagnosticsAnalyzer alongside generator (generator s…
Copilot Jan 29, 2026
f8ec08c
Update CSharpSourceGeneratorVerifier to use TAnalyzer type parameter …
Copilot Jan 30, 2026
6232571
Address review feedback: use SymbolEqualityComparer, return early on …
Copilot Jan 30, 2026
7a1d4df
Filter diagnostics earlier in pipeline and run diagnostic tests with …
Copilot Jan 30, 2026
831d431
Address review feedback: share GetDiagnosticIfInvalidMethodForGenerat…
Copilot Jan 30, 2026
f33ac74
Address feedback: check DisableRuntimeMarshallingAttribute on assembl…
Copilot Jan 31, 2026
9f85c66
Fix AddDisableRuntimeMarshallingAttributeFixerTests to use LibraryImp…
Copilot Jan 31, 2026
eb6466c
Update CompileFails tests to use CSharpAnalyzerVerifier for diagnosti…
Copilot Feb 2, 2026
cd49aed
Fix CS8795 expectation and update ByValueContentsMarshalling to use a…
Copilot Feb 2, 2026
ebe5dc3
Fix remaining test failures - all 702 tests now pass
Copilot Feb 2, 2026
6fbe38b
Inline Comparers.GeneratedSyntax and remove comment
Copilot Feb 10, 2026
73b60de
thread safety
jkoritzinsky Feb 17, 2026
16b03c9
Use targeted compiler diagnostic exclusion instead of disabling all
Copilot Feb 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
// 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());

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

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

context.RegisterSymbolAction(symbolContext =>
{
if (AnalyzeMethod(symbolContext, env, libraryImportAttrType, options))
{
Interlocked.Exchange(ref foundLibraryImportMethod, 1);
}
}, SymbolKind.Method);

// Report RequiresAllowUnsafeBlocks once per compilation if there are LibraryImport methods and unsafe is not enabled
context.RegisterCompilationEndAction(endContext =>
{
if (Volatile.Read(ref foundLibraryImportMethod) != 0 && !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, LibraryImportGeneratorOptions options)
{
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, options);
break;
}
}

return true;
}

private static void AnalyzeMethodSyntax(
SymbolAnalysisContext context,
MethodDeclarationSyntax methodSyntax,
IMethodSymbol method,
AttributeData libraryImportAttr,
StubEnvironment env,
LibraryImportGeneratorOptions options)
{
// 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

// 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);
}

/// <summary>
/// Checks if a method is invalid for generation and returns a diagnostic if so.
/// </summary>
/// <returns>A diagnostic if the method is invalid, null otherwise.</returns>
internal 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 @@ -10,14 +10,6 @@

namespace Microsoft.Interop
{
internal static class Comparers
{
/// <summary>
/// Comparer for an individual generated stub source as a syntax tree and the generated diagnostics for the stub.
/// </summary>
public static readonly IEqualityComparer<(MemberDeclarationSyntax Syntax, ImmutableArray<DiagnosticInfo> Diagnostics)> GeneratedSyntax = new CustomValueTupleElementComparer<MemberDeclarationSyntax, ImmutableArray<DiagnosticInfo>>(SyntaxEquivalentComparer.Instance, new ImmutableArraySequenceEqualComparer<DiagnosticInfo>(EqualityComparer<DiagnosticInfo>.Default));
}

/// <summary>
/// Generic comparer to compare two <see cref="ImmutableArray{T}"/> instances element by element.
/// </summary>
Expand Down
Loading
Loading