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); + } +}