diff --git a/src/tools/Avalonia.Analyzers/Avalonia.Analyzers.csproj b/src/tools/Avalonia.Analyzers/Avalonia.Analyzers.csproj
index 6eb398cbd49..23f1062e683 100644
--- a/src/tools/Avalonia.Analyzers/Avalonia.Analyzers.csproj
+++ b/src/tools/Avalonia.Analyzers/Avalonia.Analyzers.csproj
@@ -12,6 +12,7 @@
+
diff --git a/src/tools/Avalonia.Analyzers/BitmapAnalyzer.cs b/src/tools/Avalonia.Analyzers/BitmapAnalyzer.cs
new file mode 100644
index 00000000000..9db59e7fbd6
--- /dev/null
+++ b/src/tools/Avalonia.Analyzers/BitmapAnalyzer.cs
@@ -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;
+
+///
+/// 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.
+///
+[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);
+
+ ///
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+ context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.ObjectCreationExpression);
+ }
+
+ ///
+ public override ImmutableArray 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);
+ }
+ }
+ }
+
+}
diff --git a/src/tools/Avalonia.Analyzers/BitmapAnalyzerCSCodeFixProvider.cs b/src/tools/Avalonia.Analyzers/BitmapAnalyzerCSCodeFixProvider.cs
new file mode 100644
index 00000000000..c3a0536f943
--- /dev/null
+++ b/src/tools/Avalonia.Analyzers/BitmapAnalyzerCSCodeFixProvider.cs
@@ -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;
+
+///
+/// Provides a code fix for the BitmapAnalyzer diagnostic, which replaces "avares://" string arguments
+/// with a call to AssetLoader.Open(new Uri("avares://...")).
+///
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(BitmapAnalyzerCSCodeFixProvider))]
+[Shared]
+public class BitmapAnalyzerCSCodeFixProvider : CodeFixProvider
+{
+ private const string _title = "Use AssetLoader to open assets as stream first";
+
+ ///
+ public override ImmutableArray FixableDiagnosticIds { get; } =
+ ImmutableArray.Create(BitmapAnalyzer.DiagnosticId);
+
+ ///
+ public override FixAllProvider? GetFixAllProvider()
+ {
+ return WellKnownFixAllProviders.BatchFixer;
+ }
+
+ ///
+ 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().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 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().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();
+ 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);
+ }
+}