Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Expand Up @@ -41,7 +41,7 @@ public sealed class DownlevelLibraryImportDiagnosticsAnalyzer : DiagnosticAnalyz

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(context =>
{
Expand Down Expand Up @@ -96,6 +96,12 @@ private static bool AnalyzeMethod(SymbolAnalysisContext context, StubEnvironment
{
IMethodSymbol method = (IMethodSymbol)context.Symbol;

// With Analyze | ReportDiagnostics, the callback fires for both the partial definition
// and the partial implementation as separate symbols. Skip the implementation part to
// avoid reporting duplicate diagnostics — we only want to report from the definition.
if (method.PartialDefinitionPart is not null)
return false;

// Only analyze methods with LibraryImportAttribute
AttributeData? libraryImportAttr = null;
foreach (AttributeData attr in method.GetAttributes())
Expand All @@ -110,40 +116,79 @@ private static bool AnalyzeMethod(SymbolAnalysisContext context, StubEnvironment
if (libraryImportAttr is null)
return false;

// Find the method syntax
bool isGeneratedByOurGenerator = IsGeneratedByOurGenerator(method);

// Find the method syntax - prefer the partial declaration (no body) over the generated implementation.
// With Analyze | ReportDiagnostics, both parts of a partial method appear in DeclaringSyntaxReferences.
// If we encounter the generated implementation (which has a body and is marked with [GeneratedCode]
// from our generator), skip it to find the user's partial declaration.
foreach (SyntaxReference syntaxRef in method.DeclaringSyntaxReferences)
{
if (syntaxRef.GetSyntax(context.CancellationToken) is MethodDeclarationSyntax methodSyntax)
{
AnalyzeMethodSyntax(context, methodSyntax, method, libraryImportAttr, env);
if (methodSyntax.Body is not null && isGeneratedByOurGenerator)
continue;

AnalyzeMethodSyntax(context, methodSyntax, method, libraryImportAttr, env, isGeneratedByOurGenerator);
break;
}
}

return true;
}

private static bool IsGeneratedByOurGenerator(IMethodSymbol method)
{
foreach (AttributeData attr in method.GetAttributes())
{
// The generator marks its output with [GeneratedCode("Microsoft.Interop.LibraryImportGenerator...", "...")]
if (attr.AttributeClass is { Name: "GeneratedCodeAttribute", ContainingNamespace.Name: "Compiler", ContainingNamespace.ContainingNamespace.Name: "CodeDom", ContainingNamespace.ContainingNamespace.ContainingNamespace.Name: "System" }
&& attr.ConstructorArguments.Length >= 1
&& attr.ConstructorArguments[0].Value is string toolName
&& toolName.StartsWith("Microsoft.Interop.LibraryImportGenerator", StringComparison.Ordinal))
{
return true;
}
}
return false;
}

private static void AnalyzeMethodSyntax(
SymbolAnalysisContext context,
MethodDeclarationSyntax methodSyntax,
IMethodSymbol method,
AttributeData libraryImportAttr,
StubEnvironment env)
StubEnvironment env,
bool skipInvalidMethodCheck)
{
// Check for invalid method signature
DiagnosticInfo? invalidMethodDiagnostic = GetDiagnosticIfInvalidMethodForGeneration(methodSyntax, method);
if (invalidMethodDiagnostic is not null)
// When our generator has already produced an implementation, skip the signature validity check:
// the method must already have been valid for the generator to run. We still run the rest of
// the analysis (CalculateDiagnostics) to catch other issues.
if (!skipInvalidMethodCheck)
{
context.ReportDiagnostic(invalidMethodDiagnostic.ToDiagnostic());
return; // Don't continue analysis if the method is invalid
DiagnosticInfo? invalidMethodDiagnostic = GetDiagnosticIfInvalidMethodForGeneration(methodSyntax, method);
if (invalidMethodDiagnostic is not null)
{
context.ReportDiagnostic(invalidMethodDiagnostic.ToDiagnostic());
return; // Don't continue analysis if the method is invalid
}
}

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

SyntaxTree userSyntaxTree = methodSyntax.SyntaxTree;
foreach (DiagnosticInfo diagnostic in diagnostics)
{
context.ReportDiagnostic(diagnostic.ToDiagnostic());
// Only report diagnostics located in the user's (non-generated) source.
// With Analyze | ReportDiagnostics, some diagnostics may have locations in
// generated source; we must filter those out to avoid duplicates.
if (diagnostic.Location is null
|| !diagnostic.Location.IsInSource
|| diagnostic.Location.SourceTree == userSyntaxTree)
{
context.ReportDiagnostic(diagnostic.ToDiagnostic());
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,35 @@ private static LocalFunctionStatementSyntax CreateTargetDllImportAsLocalStatemen
string stubTargetName,
string stubMethodName)
{
var dllImportArgs = new List<AttributeArgumentSyntax>
{
AttributeArgument(LiteralExpression(
SyntaxKind.StringLiteralExpression,
Literal(libraryImportData.ModuleName))),
AttributeArgument(
NameEquals(nameof(DllImportAttribute.EntryPoint)),
null,
LiteralExpression(
SyntaxKind.StringLiteralExpression,
Literal(libraryImportData.EntryPoint ?? stubMethodName))),
AttributeArgument(
NameEquals(nameof(DllImportAttribute.ExactSpelling)),
null,
LiteralExpression(SyntaxKind.TrueLiteralExpression))
};

// When StringMarshalling.Utf16 is specified, forward CharSet = Unicode to the inner
// DllImport. This ensures that any types forwarded to the runtime marshaller (e.g.
// char parameters) use the correct encoding instead of defaulting to Ansi.
if (libraryImportData.IsUserDefined.HasFlag(InteropAttributeMember.StringMarshalling)
&& libraryImportData.StringMarshalling == StringMarshalling.Utf16)
{
dllImportArgs.Add(AttributeArgument(
NameEquals(nameof(DllImportAttribute.CharSet)),
null,
CreateEnumExpressionSyntax(CharSet.Unicode)));
}
Comment thread
jkoritzinsky marked this conversation as resolved.

(ParameterListSyntax parameterList, TypeSyntax returnType, AttributeListSyntax returnTypeAttributes) = stubGenerator.GenerateTargetMethodSignatureData();
LocalFunctionStatementSyntax localDllImport = LocalFunctionStatement(returnType, stubTargetName)
.AddModifiers(
Expand All @@ -333,23 +362,7 @@ private static LocalFunctionStatementSyntax CreateTargetDllImportAsLocalStatemen
Attribute(
NameSyntaxes.DllImportAttribute,
AttributeArgumentList(
SeparatedList(
[
AttributeArgument(LiteralExpression(
SyntaxKind.StringLiteralExpression,
Literal(libraryImportData.ModuleName))),
AttributeArgument(
NameEquals(nameof(DllImportAttribute.EntryPoint)),
null,
LiteralExpression(
SyntaxKind.StringLiteralExpression,
Literal(libraryImportData.EntryPoint ?? stubMethodName))),
AttributeArgument(
NameEquals(nameof(DllImportAttribute.ExactSpelling)),
null,
LiteralExpression(SyntaxKind.TrueLiteralExpression))
]
)))))))
SeparatedList(dllImportArgs)))))))
.WithParameterList(parameterList);
if (returnTypeAttributes is not null)
{
Expand Down Expand Up @@ -379,10 +392,7 @@ private static AttributeSyntax CreateForwarderDllImport(LibraryImportData target
{
Debug.Assert(target.StringMarshalling == StringMarshalling.Utf16);
NameEqualsSyntax name = NameEquals(nameof(DllImportAttribute.CharSet));
ExpressionSyntax value = MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
AliasQualifiedName("global", IdentifierName(typeof(CharSet).FullName)),
IdentifierName(nameof(CharSet.Unicode)));
ExpressionSyntax value = CreateEnumExpressionSyntax(CharSet.Unicode);
newAttributeArgs.Add(AttributeArgument(name, null, value));
}

Expand Down Expand Up @@ -414,5 +424,13 @@ static ExpressionSyntax CreateStringExpressionSyntax(string str)
}
}

private static MemberAccessExpressionSyntax CreateEnumExpressionSyntax<T>(T value) where T : Enum
{
return MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
AliasQualifiedName("global", IdentifierName(typeof(T).FullName)),
IdentifierName(value.ToString()));
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public class LibraryImportDiagnosticsAnalyzer : DiagnosticAnalyzer

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(context =>
{
Expand Down Expand Up @@ -94,6 +94,12 @@ private static bool AnalyzeMethod(SymbolAnalysisContext context, StubEnvironment
{
IMethodSymbol method = (IMethodSymbol)context.Symbol;

// With Analyze | ReportDiagnostics, the callback fires for both the partial definition
// and the partial implementation as separate symbols. Skip the implementation part to
// avoid reporting duplicate diagnostics — we only want to report from the definition.
if (method.PartialDefinitionPart is not null)
return false;

// Only analyze methods with LibraryImportAttribute
AttributeData? libraryImportAttr = null;
foreach (AttributeData attr in method.GetAttributes())
Expand All @@ -108,43 +114,82 @@ private static bool AnalyzeMethod(SymbolAnalysisContext context, StubEnvironment
if (libraryImportAttr is null)
return false;

// Find the method syntax
bool isGeneratedByOurGenerator = IsGeneratedByOurGenerator(method);

// Find the method syntax - prefer the partial declaration (no body) over the generated implementation.
// With Analyze | ReportDiagnostics, both parts of a partial method appear in DeclaringSyntaxReferences.
// If we encounter the generated implementation (which has a body and is marked with [GeneratedCode]
// from our generator), skip it to find the user's partial declaration.
foreach (SyntaxReference syntaxRef in method.DeclaringSyntaxReferences)
{
if (syntaxRef.GetSyntax(context.CancellationToken) is MethodDeclarationSyntax methodSyntax)
{
AnalyzeMethodSyntax(context, methodSyntax, method, libraryImportAttr, env, options);
if (methodSyntax.Body is not null && isGeneratedByOurGenerator)
continue;

AnalyzeMethodSyntax(context, methodSyntax, method, libraryImportAttr, env, options, isGeneratedByOurGenerator);
break;
}
}

return true;
}

private static bool IsGeneratedByOurGenerator(IMethodSymbol method)
{
foreach (AttributeData attr in method.GetAttributes())
{
// The generator marks its output with [GeneratedCode("Microsoft.Interop.LibraryImportGenerator", "...")]
if (attr.AttributeClass is { Name: "GeneratedCodeAttribute", ContainingNamespace.Name: "Compiler", ContainingNamespace.ContainingNamespace.Name: "CodeDom", ContainingNamespace.ContainingNamespace.ContainingNamespace.Name: "System" }
&& attr.ConstructorArguments.Length >= 1
&& attr.ConstructorArguments[0].Value is string toolName
&& toolName.StartsWith("Microsoft.Interop.LibraryImportGenerator", StringComparison.Ordinal))
{
return true;
}
}
return false;
}

private static void AnalyzeMethodSyntax(
SymbolAnalysisContext context,
MethodDeclarationSyntax methodSyntax,
IMethodSymbol method,
AttributeData libraryImportAttr,
StubEnvironment env,
LibraryImportGeneratorOptions options)
LibraryImportGeneratorOptions options,
bool skipInvalidMethodCheck)
{
// Check for invalid method signature
DiagnosticInfo? invalidMethodDiagnostic = GetDiagnosticIfInvalidMethodForGeneration(methodSyntax, method);
if (invalidMethodDiagnostic is not null)
// When our generator has already produced an implementation, skip the signature validity check:
// the method must already have been valid for the generator to run. We still run the rest of
// the analysis (CalculateDiagnostics) to catch other issues.
if (!skipInvalidMethodCheck)
{
context.ReportDiagnostic(invalidMethodDiagnostic.ToDiagnostic());
return; // Don't continue analysis if the method is invalid
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);

SyntaxTree userSyntaxTree = methodSyntax.SyntaxTree;
foreach (DiagnosticInfo diagnostic in diagnostics)
{
context.ReportDiagnostic(diagnostic.ToDiagnostic());
// Only report diagnostics located in the user's (non-generated) source.
// With Analyze | ReportDiagnostics, some diagnostics may have locations in
// generated source; we must filter those out to avoid duplicates.
if (diagnostic.Location is null
|| !diagnostic.Location.IsInSource
|| diagnostic.Location.SourceTree == userSyntaxTree)
{
context.ReportDiagnostic(diagnostic.ToDiagnostic());
}
}
}

Expand Down
Loading
Loading