Skip to content

Commit

Permalink
[Microsoft.Android.Sdk.Analysis] Warn on missing activation ctors (#9447
Browse files Browse the repository at this point in the history
)

Fixes: #8410

Context: b3079db
Context: https://learn.microsoft.com/dotnet/csharp/roslyn-sdk/tutorials/how-to-write-csharp-analyzer-code-fix

Commit b3079db added a `Microsoft.Android.Sdk.Analysis.dll` assembly
which contained a [`DiagnosticAnalyzer`][0] to suppress IDE0002
warnings.

Extend that support to add a `CustomApplicationAnalyzer` which checks
for the [`(IntPtr, JniHandleOwnership)`][1] constructor on
[`Android.App.Application`][2] subclasses, as this constructor is
currently required.

If the constructor is missing:

	[Application]
	public class MyApp : Application {
	}

then the app will crash during startup:

	System.NotSupportedException: Unable to activate instance of type MyApp from native handle 0x7ff1f3b468 (key_handle 0x466b26f).

If we find an `Application` subclass that is missing the required
constructor, we'll emit a DNAA0001 warning:

	warning DNAA0001: Application class 'MyApp' does not have an Activation Constructor.

Additionally, provide a `CustomApplicationCodeFixProvider` which will
inject the required constructor into the syntax tree.

Finally, rename the previous `XAD0001` warning to `DNAS0001`.
Our convention is as follows:

  * `DNA`: prefix for .NET for Android messages
  * `A` for *Analyzers*, `S` for *Suppressors*
  * 4 digit code.

[0]: https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.diagnostics.diagnosticanalyzer?view=roslyn-dotnet-4.9.0
[1]: https://learn.microsoft.com/en-us/dotnet/api/android.app.application.-ctor?view=net-android-34.0#android-app-application-ctor(system-intptr-android-runtime-jnihandleownership)
[2]: https://learn.microsoft.com/en-us/dotnet/api/android.app.application?view=net-android-34.0
  • Loading branch information
dellis1972 authored Nov 6, 2024
1 parent 517bc21 commit 715a36a
Show file tree
Hide file tree
Showing 10 changed files with 486 additions and 88 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
; Shipped analyzer releases
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
; Unshipped analyzer release
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
DNAA0001 | Usage | Warning | CustomApplicationAnalyzer
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System.Collections.Immutable;
using System.Linq;
using Microsoft.Android.Sdk.Analysis;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

[DiagnosticAnalyzer (LanguageNames.CSharp)]
public class CustomApplicationAnalyzer : DiagnosticAnalyzer
{
private const string AndroidApplication = "Android.App.Application";
public const string DiagnosticId = "DNAA0001";
private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor (
id: DiagnosticId,
title: Resources.DNAA0001_Title,
messageFormat: Resources.DNAA0001_MessageFormat,
category: "Usage",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true
);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create (Rule);

public override void Initialize (AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis (GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution ();

// Register a syntax node action to analyze method declarations
context.RegisterSyntaxNodeAction (AnalyzeClass, SyntaxKind.ClassDeclaration);
}

private static void AnalyzeClass (SyntaxNodeAnalysisContext context)
{
var classDeclarationSyntax = context.Node as ClassDeclarationSyntax;
if (classDeclarationSyntax == null)
return;

var classSymbol = context.SemanticModel.GetDeclaredSymbol (classDeclarationSyntax) as INamedTypeSymbol;
if (classSymbol == null)
return;

if (!Utilities.IsDerivedFrom (classSymbol, AndroidApplication))
return;

var constructors = classDeclarationSyntax.Members
.OfType<ConstructorDeclarationSyntax> ();

bool foundActivationConstructor = false;
foreach (var constructor in constructors) {
var parameters = constructor.ParameterList.Parameters;
if (parameters.Count != 2)
continue;
if (parameters [0].Type.ToString () != "IntPtr")
continue;
var ns = Utilities.GetNamespaceForParameterType (parameters [1], context.SemanticModel);
var type = parameters [1].Type.ToString();
var isJniHandle = (ns == "Android.Runtime") && (type == "JniHandleOwnership") || (type == "Android.Runtime.JniHandleOwnership");
if (!isJniHandle)
continue;
foundActivationConstructor = true;
}
if (!foundActivationConstructor) {
var diagnostic = Diagnostic.Create (Rule, classDeclarationSyntax.Identifier.GetLocation (), classDeclarationSyntax.Identifier.Text);
context.ReportDiagnostic (diagnostic);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

[ExportCodeFixProvider (LanguageNames.CSharp, Name = nameof (CustomApplicationCodeFixProvider)), Shared]
public class CustomApplicationCodeFixProvider : CodeFixProvider
{
private const string title = "Fix Activation Constructor";
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create (CustomApplicationAnalyzer.DiagnosticId);

public sealed override FixAllProvider GetFixAllProvider ()
{
return WellKnownFixAllProviders.BatchFixer;
}

public override async Task RegisterCodeFixesAsync (CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync (context.CancellationToken).ConfigureAwait (false);
var diagnostic = context.Diagnostics.First ();
var diagnosticSpan = diagnostic.Location.SourceSpan;
var classDeclaration = root.FindToken (diagnosticSpan.Start).Parent.AncestorsAndSelf ()
.OfType<ClassDeclarationSyntax> ().First ();
context.RegisterCodeFix (CodeAction.Create (title, c =>
InjectConstructorAsync (context.Document, classDeclaration, c), equivalenceKey: title), diagnostic);
}

private async Task<Document> InjectConstructorAsync (Document document, ClassDeclarationSyntax classDeclaration, CancellationToken cancellationToken)
{
var constructor = CreateConstructorWithParameters (classDeclaration.Identifier);
var newClassDeclaration = classDeclaration.AddMembers (constructor);
var root = await document.GetSyntaxRootAsync (cancellationToken);
var newRoot = root.ReplaceNode (classDeclaration, newClassDeclaration);
return document.WithSyntaxRoot (newRoot);
}

private ConstructorDeclarationSyntax CreateConstructorWithParameters (SyntaxToken identifier)
{
var parameters = SyntaxFactory.ParameterList (SyntaxFactory.SeparatedList (new [] {
SyntaxFactory.Parameter (SyntaxFactory.Identifier ("javaReference"))
.WithType (SyntaxFactory.ParseTypeName ("IntPtr")),
SyntaxFactory.Parameter (SyntaxFactory.Identifier ("transfer"))
.WithType (SyntaxFactory.ParseTypeName ("Android.Runtime.JniHandleOwnership"))
}));
var baseArguments = SyntaxFactory.ArgumentList (SyntaxFactory.SeparatedList (new [] {
SyntaxFactory.Argument (SyntaxFactory.IdentifierName ("javaReference")),
SyntaxFactory.Argument (SyntaxFactory.IdentifierName ("transfer"))
}));
var constructorInitializer = SyntaxFactory.ConstructorInitializer (SyntaxKind.BaseConstructorInitializer, baseArguments);
var body = SyntaxFactory.Block ();
var constructor = SyntaxFactory.ConstructorDeclaration (identifier)
.WithModifiers (SyntaxFactory.TokenList (SyntaxFactory.Token (SyntaxKind.PublicKeyword)))
.WithParameterList (parameters)
.WithInitializer (constructorInitializer)
.WithBody (body);
return constructor;
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\Configuration.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputPath>$(MicrosoftAndroidSdkAnalysisOutDir)</OutputPath>
<IsRoslynComponent>true</IsRoslynComponent>
<LangVersion>latest</LangVersion>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<AssemblyOriginatorKeyFile>..\..\product.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
<Import Project="..\..\Configuration.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputPath>$(MicrosoftAndroidSdkAnalysisOutDir)</OutputPath>
<IsRoslynComponent>true</IsRoslynComponent>
<LangVersion>latest</LangVersion>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<AssemblyOriginatorKeyFile>..\..\product.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.11.0" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="AnalyzerReleases.Shipped.md" />
<AdditionalFiles Include="AnalyzerReleases.Unshipped.md" />
</ItemGroup>
<ItemGroup>
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
</Project>

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 715a36a

Please sign in to comment.