Browse Source

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

* feat: add bitmap analyzer.

* feat: add code fix provider.

* feat: use actual name. add comment.
pull/18251/head
Dong Bin 12 months ago
committed by GitHub
parent
commit
afa124f505
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      src/tools/Avalonia.Analyzers/Avalonia.Analyzers.csproj
  2. 68
      src/tools/Avalonia.Analyzers/BitmapAnalyzer.cs
  3. 115
      src/tools/Avalonia.Analyzers/BitmapAnalyzerCSCodeFixProvider.cs

1
src/tools/Avalonia.Analyzers/Avalonia.Analyzers.csproj

@ -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>

68
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;
/// <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
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;
/// <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);
}
}
Loading…
Cancel
Save