Skip to content

Commit

Permalink
Created code analyzers to detect mistakes when defining font and colo…
Browse files Browse the repository at this point in the history
…r providers.
  • Loading branch information
reduckted committed Feb 9, 2023
1 parent 5c4947a commit 6496bcc
Show file tree
Hide file tree
Showing 31 changed files with 1,587 additions and 101 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,9 @@ private static void CheckInitialization(State state, CompilationAnalysisContext
.OrderBy((x) => x.Identifier.ValueText)
.First();

MethodDeclarationSyntax? initializeAsyncMethod = FindInitializeAsyncMethod(packageToReport);
MethodDeclarationSyntax? initializeAsyncMethod = packageToReport.FindAsyncPackageInitializeAsyncMethod();

foreach (INamedTypeSymbol command in state.Commands.Where((x) => !x.Value).Select((x) => x.Key))
foreach (ISymbol command in state.Commands.Where((x) => !x.Value).Select((x) => x.Key))
{

// If the commands should be initialized individually,
Expand Down Expand Up @@ -172,7 +172,7 @@ ConcurrentDictionary<ISymbol, bool> commands
{
InitializationMode? mode = null;

MethodDeclarationSyntax? initializeAsync = FindInitializeAsyncMethod(package);
MethodDeclarationSyntax? initializeAsync = package.FindAsyncPackageInitializeAsyncMethod();
if (initializeAsync is not null)
{
foreach (StatementSyntax statement in initializeAsync.Body.Statements)
Expand Down Expand Up @@ -213,22 +213,6 @@ ConcurrentDictionary<ISymbol, bool> commands
return mode;
}

internal static MethodDeclarationSyntax? FindInitializeAsyncMethod(ClassDeclarationSyntax package)
{
foreach (MethodDeclarationSyntax method in package.Members.OfType<MethodDeclarationSyntax>())
{
if (method.Modifiers.Any(SyntaxKind.ProtectedKeyword) && method.Modifiers.Any(SyntaxKind.OverrideKeyword))
{
if (string.Equals(method.Identifier.ValueText, "InitializeAsync"))
{
return method;
}
}
}

return null;
}

private enum InitializationMode
{
Individual,
Expand All @@ -253,7 +237,7 @@ public State(INamedTypeSymbol asyncPackageType, INamedTypeSymbol baseCommandType

public void Dispose()
{
// ConcurrentBag stores data per-thread, and thata data remains in memory
// ConcurrentBag stores data per-thread, and that data remains in memory
// until it is removed from the bag. Prevent a memory leak by emptying the bag.
while (Packages.Count > 0)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
CodeAction.Create(
Resources.CVST005_CodeFix,
cancellation => AddInitializeAsyncMethodAsync(context.Document, declaration, cancellation),
equivalenceKey: nameof(Resources.CVST001_CodeFix)
equivalenceKey: nameof(Resources.CVST005_CodeFix)
),
diagnostic
);
Expand All @@ -61,7 +61,7 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
CodeAction.Create(
Resources.CVST005_CodeFix,
cancellation => InitializeSpecificCommandsAsync(context.Document, method, commandNames, cancellation),
equivalenceKey: nameof(Resources.CVST001_CodeFix)
equivalenceKey: nameof(Resources.CVST005_CodeFix)
),
diagnostic
);
Expand All @@ -76,7 +76,7 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
CodeAction.Create(
Resources.CVST005_CodeFix,
cancellation => InitializeAllCommandsAsync(context.Document, method, cancellation),
equivalenceKey: nameof(Resources.CVST001_CodeFix)
equivalenceKey: nameof(Resources.CVST005_CodeFix)
),
diagnostic
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Community.VisualStudio.Toolkit.Analyzers
{
/// <summary>
/// Detects font and color providers without an explicit GUID.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class CVST006SpecifyFontAndColorProviderGuidAnalyzer : MissingGuidAttributeAnalyzerBase
{
internal const string DiagnosticId = Diagnostics.DefineExplicitGuidForFontAndColorProviders;

private static readonly DiagnosticDescriptor _rule = new(
DiagnosticId,
GetLocalizableString(nameof(Resources.CVST006_Title)),
GetLocalizableString(nameof(Resources.CVST006_MessageFormat)),
"Usage",
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: GetLocalizableString(nameof(Resources.CVST006_Description)));

protected override DiagnosticDescriptor Descriptor => _rule;

protected override string BaseTypeName => KnownTypeNames.BaseFontAndColorProvider;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Collections.Immutable;
using System.Composition;
using Community.VisualStudio.Toolkit.Analyzers.Analyzers;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;

namespace Community.VisualStudio.Toolkit.Analyzers
{
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CVST006SpecifyFontAndColorProviderGuidCodeFixProvider))]
[Shared]
public class CVST006SpecifyFontAndColorProviderGuidCodeFixProvider : MissingGuidAttributeCodeFixProviderBase
{
public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(CVST006SpecifyFontAndColorProviderGuidAnalyzer.DiagnosticId);

protected override string Title => Resources.CVST006_CodeFix;

protected override string EquivalenceKey => nameof(Resources.CVST006_CodeFix);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Community.VisualStudio.Toolkit.Analyzers
{
/// <summary>
/// Detects font and color categories without an explicit GUID.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class CVST007SpecifyFontAndColorCategoryGuidAnalyzer : MissingGuidAttributeAnalyzerBase
{
internal const string DiagnosticId = Diagnostics.DefineExplicitGuidForFontAndColorCategories;

private static readonly DiagnosticDescriptor _rule = new(
DiagnosticId,
GetLocalizableString(nameof(Resources.CVST007_Title)),
GetLocalizableString(nameof(Resources.CVST007_MessageFormat)),
"Usage",
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: GetLocalizableString(nameof(Resources.CVST007_Description)));

protected override DiagnosticDescriptor Descriptor => _rule;

protected override string BaseTypeName => KnownTypeNames.BaseFontAndColorCategory;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Collections.Immutable;
using System.Composition;
using Community.VisualStudio.Toolkit.Analyzers.Analyzers;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;

namespace Community.VisualStudio.Toolkit.Analyzers
{
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CVST007SpecifyFontAndColorCategoryGuidCodeFixProvider))]
[Shared]
public class CVST007SpecifyFontAndColorCategoryGuidCodeFixProvider : MissingGuidAttributeCodeFixProviderBase
{
public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(CVST007SpecifyFontAndColorCategoryGuidAnalyzer.DiagnosticId);

protected override string Title => Resources.CVST007_CodeFix;

protected override string EquivalenceKey => nameof(Resources.CVST007_CodeFix);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Community.VisualStudio.Toolkit.Analyzers
{
/// <summary>
/// Detects a missing font and color provider.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class CVST008FontAndColorCategoryShouldHaveProviderAnalyzer : AnalyzerBase
{
internal const string DiagnosticId = Diagnostics.DefineProviderForFontAndColorCategories;

private static readonly DiagnosticDescriptor _rule = new(
DiagnosticId,
GetLocalizableString(nameof(Resources.CVST008_Title)),
GetLocalizableString(nameof(Resources.CVST008_MessageFormat)),
"Usage",
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: GetLocalizableString(nameof(Resources.CVST008_Description)));

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

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

context.RegisterCompilationStartAction(OnCompilationStart);
}

private static void OnCompilationStart(CompilationStartAnalysisContext context)
{
INamedTypeSymbol baseFontAndColorProviderType;
INamedTypeSymbol baseFontAndColorCategoryType;

baseFontAndColorProviderType = context.Compilation.GetTypeByMetadataName(KnownTypeNames.BaseFontAndColorProvider);
baseFontAndColorCategoryType = context.Compilation.GetTypeByMetadataName(KnownTypeNames.BaseFontAndColorCategory);

if ((baseFontAndColorProviderType is not null) && (baseFontAndColorCategoryType is not null))
{
State state = new(baseFontAndColorProviderType, baseFontAndColorCategoryType);

context.RegisterSyntaxNodeAction(
(x) => RecordProvidersAndCategories(state, x),
SyntaxKind.ClassDeclaration
);

context.RegisterCompilationEndAction((endContext) =>
{
if (state.Categories.Any() && !state.HasProviders)
{
// There are categories but no providers. Add a diagnostic to each category.
foreach (SyntaxToken token in state.Categories.Values)
{
endContext.ReportDiagnostic(Diagnostic.Create(_rule, token.GetLocation()));
}
}
});
}
}

private static void RecordProvidersAndCategories(State state, SyntaxNodeAnalysisContext context)
{
ClassDeclarationSyntax declaration = (ClassDeclarationSyntax)context.Node;
INamedTypeSymbol classSymbol = context.SemanticModel.GetDeclaredSymbol(declaration, context.CancellationToken);

if (classSymbol.IsSubclassOf(state.BaseFontAndColorProviderType))
{
state.HasProviders = true;
}
else if (classSymbol.IsSubclassOf(state.BaseFontAndColorCategoryType))
{
state.Categories[classSymbol] = declaration.Identifier;
}
}

private class State
{
private int _hasProviders;

public State(INamedTypeSymbol baseFontAndColorProviderType, INamedTypeSymbol baseFontAndColorCategoryType)
{
BaseFontAndColorProviderType = baseFontAndColorProviderType;
BaseFontAndColorCategoryType = baseFontAndColorCategoryType;
}

public INamedTypeSymbol BaseFontAndColorProviderType { get; }

public INamedTypeSymbol BaseFontAndColorCategoryType { get; }

public ConcurrentDictionary<ISymbol, SyntaxToken> Categories { get; } = new();

public bool HasProviders
{
get => Interlocked.CompareExchange(ref _hasProviders, 0, 0) != 0;
set => Interlocked.Exchange(ref _hasProviders, value ? 1 : 0);
}
}
}
}
Loading

0 comments on commit 6496bcc

Please sign in to comment.