Skip to content

Add Analyzer and CodeFixProvider for Initializing Bitmap with a "avares" scheme argument. #18150

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
merged 4 commits into from
Feb 16, 2025
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
1 change: 1 addition & 0 deletions src/tools/Avalonia.Analyzers/Avalonia.Analyzers.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="3.9.0" />
</ItemGroup>

<ItemGroup>
Expand Down
68 changes: 68 additions & 0 deletions src/tools/Avalonia.Analyzers/BitmapAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Avalonia.Analyzers;

/// <summary>
/// Analyzes object creation expressions to detect instances where a Bitmap is initialized
/// from the "avares" scheme directly, which is not allowed. Instead, the AssetLoader should be used
/// to open assets as a stream first.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class BitmapAnalyzer: DiagnosticAnalyzer
{
public const string DiagnosticId = "AVA2002";
private const string Title = "Cannot initialize Bitmap from \"avares\" scheme";
private const string MessageFormat = "Cannot initialize Bitmap from \"avares\" scheme directly";
private const string Description = "Cannot initialize Bitmap from \"avares\" scheme, use AssetLoader to open assets as stream first.";
private const string Category = "Usage";

private static readonly DiagnosticDescriptor _rule = new(
DiagnosticId,
Title,
MessageFormat,
Category,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: Description);

/// <inheritdoc />
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.ObjectCreationExpression);
}

/// <inheritdoc />
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(_rule); } }

private static void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
var objectCreation = (ObjectCreationExpressionSyntax)context.Node;
var semanticModel = context.SemanticModel;

// Check if the object creation is creating an instance of Avalonia.Media.Imaging.Bitmap
var symbol = semanticModel.GetSymbolInfo(objectCreation).Symbol as IMethodSymbol;
if (symbol == null || symbol.ContainingType.ToString() != "Avalonia.Media.Imaging.Bitmap")
{
return;
}

// Check if any argument starts with "avares://"
foreach (var argument in objectCreation.ArgumentList.Arguments)
{
var constantValue = semanticModel.GetConstantValue(argument.Expression);
if (constantValue.HasValue && constantValue.Value is string stringValue && stringValue.StartsWith("avares://"))
{
var diagnostic = Diagnostic.Create(_rule, objectCreation.GetLocation());
context.ReportDiagnostic(diagnostic);
}
}
}

}
115 changes: 115 additions & 0 deletions src/tools/Avalonia.Analyzers/BitmapAnalyzerCSCodeFixProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using System.Collections.Generic;
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;

namespace Avalonia.Analyzers;

/// <summary>
/// Provides a code fix for the BitmapAnalyzer diagnostic, which replaces "avares://" string arguments
/// with a call to AssetLoader.Open(new Uri("avares://...")).
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(BitmapAnalyzerCSCodeFixProvider))]
[Shared]
public class BitmapAnalyzerCSCodeFixProvider : CodeFixProvider
{
private const string _title = "Use AssetLoader to open assets as stream first";

/// <inheritdoc />
public override ImmutableArray<string> FixableDiagnosticIds { get; } =
ImmutableArray.Create(BitmapAnalyzer.DiagnosticId);

/// <inheritdoc />
public override FixAllProvider? GetFixAllProvider()
{
return WellKnownFixAllProviders.BatchFixer;
}

/// <inheritdoc />
public sealed 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;

// Find the type declaration identified by the diagnostic.
var declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf()
.OfType<LocalDeclarationStatementSyntax>().First();

// Register a code action that will invoke the fix.
context.RegisterCodeFix(
CodeAction.Create(
_title,
c => ReplaceArgumentAsync(context.Document, declaration, c),
_title),
diagnostic);
}

private async Task<Document> ReplaceArgumentAsync(Document contextDocument, LocalDeclarationStatementSyntax declaration,
CancellationToken cancellationToken)
{
var root = await contextDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var semanticModel = await contextDocument.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);

var objectCreation = declaration.DescendantNodes().OfType<ObjectCreationExpressionSyntax>().First();
var argumentList = objectCreation.ArgumentList;
var newArguments = argumentList.Arguments.Select(arg =>
{
var constantValue = semanticModel.GetConstantValue(arg.Expression);
if (constantValue.HasValue && constantValue.Value is string stringValue &&
stringValue.StartsWith("avares://"))
{
var newArgument = SyntaxFactory.Argument(
SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.IdentifierName("AssetLoader"),
SyntaxFactory.IdentifierName("Open")))
.WithArgumentList(
SyntaxFactory.ArgumentList(
SyntaxFactory.SingletonSeparatedList(
SyntaxFactory.Argument(
SyntaxFactory.ObjectCreationExpression(
SyntaxFactory.IdentifierName("Uri"))
.WithArgumentList(
SyntaxFactory.ArgumentList(
SyntaxFactory.SingletonSeparatedList(
SyntaxFactory.Argument(
SyntaxFactory.LiteralExpression(
SyntaxKind.StringLiteralExpression,
SyntaxFactory
.Literal(stringValue)))))))))));
return newArgument;
}

return arg;
}).ToArray();

var newArgumentList = SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(newArguments));
var newObjectCreation = objectCreation.WithArgumentList(newArgumentList);
var newRoot = root.ReplaceNode(objectCreation, newObjectCreation);

var usingDirective = ((CompilationUnitSyntax)newRoot).Usings;
var newUsings = new List<UsingDirectiveSyntax>();
if(!usingDirective.Any(a=>a.Name.ToString().Contains("Avalonia.Platform")))
{
newUsings.Add(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("Avalonia.Platform")));
}
if(!usingDirective.Any(a=>a.Name.ToString().Contains("System")))
{
newUsings.Add(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("System")));
}
// Add the new using directives to the root
newRoot = ((CompilationUnitSyntax)newRoot).AddUsings(newUsings.ToArray());

return contextDocument.WithSyntaxRoot(newRoot);
}
}