-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Move diagnostic generation from LibraryImportGenerator to LibraryImportDiagnosticsAnalyzer #123780
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
jkoritzinsky
merged 17 commits into
main
from
copilot/move-diagnostic-generation-to-analyzers
Feb 20, 2026
Merged
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
5ab07b3
Initial plan
Copilot 87827dd
Add LibraryImportDiagnosticsAnalyzer and update generator to not emit…
Copilot 4e3556e
Fix duplicate diagnostics in LibraryImportDiagnosticsAnalyzer
Copilot b0d4d92
Address code review feedback - fix RequiresAllowUnsafeBlocks and cond…
Copilot deaee03
Add LibraryImportDiagnosticsAnalyzer alongside generator (generator s…
Copilot f8ec08c
Update CSharpSourceGeneratorVerifier to use TAnalyzer type parameter …
Copilot 6232571
Address review feedback: use SymbolEqualityComparer, return early on …
Copilot 7a1d4df
Filter diagnostics earlier in pipeline and run diagnostic tests with …
Copilot 831d431
Address review feedback: share GetDiagnosticIfInvalidMethodForGenerat…
Copilot f33ac74
Address feedback: check DisableRuntimeMarshallingAttribute on assembl…
Copilot 9f85c66
Fix AddDisableRuntimeMarshallingAttributeFixerTests to use LibraryImp…
Copilot eb6466c
Update CompileFails tests to use CSharpAnalyzerVerifier for diagnosti…
Copilot cd49aed
Fix CS8795 expectation and update ByValueContentsMarshalling to use a…
Copilot ebe5dc3
Fix remaining test failures - all 702 tests now pass
Copilot 6fbe38b
Inline Comparers.GeneratedSyntax and remove comment
Copilot 73b60de
thread safety
jkoritzinsky 16b03c9
Use targeted compiler diagnostic exclusion instead of disabling all
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
300 changes: 300 additions & 0 deletions
300
....InteropServices/gen/LibraryImportGenerator/Analyzers/LibraryImportDiagnosticsAnalyzer.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.