Skip to content

Commit

Permalink
Merge pull request #284 from reduckted/feature/options-page-analyzer
Browse files Browse the repository at this point in the history
Code analyzer to check the type passed to ProvideOptionDialogPageAttribute and ProvideProfileAttribute
  • Loading branch information
madskristensen authored Jan 26, 2022
2 parents 171b240 + 2331ba0 commit e2d88dd
Show file tree
Hide file tree
Showing 18 changed files with 619 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ public override void Initialize(AnalysisContext context)

private void OnCompilationStart(CompilationStartAnalysisContext context)
{
INamedTypeSymbol? dialogPageType = context.Compilation.GetTypeByMetadataName("Microsoft.VisualStudio.Shell.DialogPage");
INamedTypeSymbol? comVisibleType = context.Compilation.GetTypeByMetadataName("System.Runtime.InteropServices.ComVisibleAttribute");
INamedTypeSymbol? dialogPageType = context.Compilation.GetTypeByMetadataName(KnownTypeNames.DialogPage);
INamedTypeSymbol? comVisibleType = context.Compilation.GetTypeByMetadataName(KnownTypeNames.ComVisibleAttribute);

if (dialogPageType is not null && comVisibleType is not null)
{
Expand All @@ -45,7 +45,7 @@ private static void AnalyzeClass(SyntaxNodeAnalysisContext context, INamedTypeSy
ClassDeclarationSyntax classDeclaration = (ClassDeclarationSyntax)context.Node;
ITypeSymbol? type = context.ContainingSymbol as ITypeSymbol;

if (type is not null && IsDialogPage(type, dialogPageType))
if (type is not null && type.IsSubclassOf(dialogPageType))
{
// This class inherits from `DialogPage`. It should contain
// a `ComVisible` attribute with a parameter of `true`.
Expand All @@ -67,20 +67,5 @@ private static void AnalyzeClass(SyntaxNodeAnalysisContext context, INamedTypeSy
context.ReportDiagnostic(Diagnostic.Create(_rule, classDeclaration.Identifier.GetLocation()));
}
}

private static bool IsDialogPage(ITypeSymbol? type, INamedTypeSymbol dialogPageType)
{
while (type is not null)
{
if (type.Equals(dialogPageType))
{
return true;
}

type = type.BaseType;
}

return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

namespace Community.VisualStudio.Toolkit.Analyzers
{
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CVST001CastInteropServicesCodeFixProvider))]
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CVST002DialogPageShouldBeComVisibleCodeFixProvider))]
[Shared]
public class CVST002DialogPageShouldBeComVisibleCodeFixProvider : CodeFixProviderBase
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Community.VisualStudio.Toolkit.Analyzers
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class CVST003UseCorrectTypeInProvideOptionDialogPageAttributeAnalyzer : IncorrectProvidedTypeAnalyzerBase
{
internal const string DiagnosticId = Diagnostics.UseCorrectTypeInProvideOptionDialogPageAttribute;

private static readonly DiagnosticDescriptor _rule = new(
DiagnosticId,
GetLocalizableString(nameof(Resources.CVST003_Title)),
GetLocalizableString(nameof(Resources.IncorrectProvidedType_MessageFormat)),
"Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

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

protected override string AttributeTypeName => KnownTypeNames.ProvideOptionDialogPageAttribute;

protected override string ExpectedTypeName => KnownTypeNames.DialogPage;

protected override DiagnosticDescriptor Descriptor => _rule;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Composition;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;

namespace Community.VisualStudio.Toolkit.Analyzers
{
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CVST003UseCorrectTypeInProvideOptionDialogPageAttributeCodeFixProvider))]
[Shared]
public class CVST003UseCorrectTypeInProvideOptionDialogPageAttributeCodeFixProvider : IncorrectProvidedTypeCodeFixProviderBase
{
protected override string FixableDiagnosticId => CVST003UseCorrectTypeInProvideOptionDialogPageAttributeAnalyzer.DiagnosticId;

protected override string ExpectedTypeName => KnownTypeNames.DialogPage;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Community.VisualStudio.Toolkit.Analyzers
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class CVST004UseCorrectTypeInProvideProfileAttributeAnalyzer : IncorrectProvidedTypeAnalyzerBase
{
internal const string DiagnosticId = Diagnostics.UseCorrectTypeInProvideProfileAttribute;

private static readonly DiagnosticDescriptor _rule = new(
DiagnosticId,
GetLocalizableString(nameof(Resources.CVST004_Title)),
GetLocalizableString(nameof(Resources.IncorrectProvidedType_MessageFormat)),
"Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

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

protected override string AttributeTypeName => KnownTypeNames.ProvideProfileAttribute;

protected override string ExpectedTypeName => KnownTypeNames.IProfileManager;

protected override DiagnosticDescriptor Descriptor => _rule;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Composition;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;

namespace Community.VisualStudio.Toolkit.Analyzers
{
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CVST004UseCorrectTypeInProvideProfileAttributeCodeFixProvider))]
[Shared]
public class CVST004UseCorrectTypeInProvideProfileAttributeCodeFixProvider : IncorrectProvidedTypeCodeFixProviderBase
{
protected override string FixableDiagnosticId => CVST004UseCorrectTypeInProvideProfileAttributeAnalyzer.DiagnosticId;

protected override string ExpectedTypeName => KnownTypeNames.IProfileManager;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Community.VisualStudio.Toolkit.Analyzers
{
public abstract class IncorrectProvidedTypeAnalyzerBase : AnalyzerBase
{
protected abstract string AttributeTypeName { get; }

protected abstract string ExpectedTypeName { get; }

protected abstract DiagnosticDescriptor Descriptor { get; }

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

context.RegisterCompilationStartAction(OnCompilationStart);
}

private void OnCompilationStart(CompilationStartAnalysisContext context)
{
INamedTypeSymbol? attributeType = context.Compilation.GetTypeByMetadataName(AttributeTypeName);
INamedTypeSymbol? expectedType = context.Compilation.GetTypeByMetadataName(ExpectedTypeName);

if (attributeType is not null && expectedType is not null)
{
context.RegisterSyntaxNodeAction(
(c) => AnalyzeAttribute(c, attributeType, expectedType),
SyntaxKind.Attribute
);
}
}

private void AnalyzeAttribute(SyntaxNodeAnalysisContext context, INamedTypeSymbol attributeType, INamedTypeSymbol expectedType)
{
AttributeSyntax attribute = (AttributeSyntax)context.Node;
if (context.SemanticModel.GetTypeInfo(attribute).Type?.IsAssignableTo(attributeType) == true)
{
// The type that is provided is always the first argument to the attribute's constructor.
AttributeArgumentSyntax? typeArgument = attribute.ArgumentList?.Arguments.FirstOrDefault();
if (typeArgument?.Expression is TypeOfExpressionSyntax typeOfExpression)
{
ISymbol? argumentSymbol = context.SemanticModel.GetSymbolInfo(typeOfExpression.Type).Symbol;
if (argumentSymbol is ITypeSymbol argumentTypeSymbol)
{
if (!argumentTypeSymbol.IsAssignableTo(expectedType))
{
context.ReportDiagnostic(
Diagnostic.Create(
Descriptor,
typeOfExpression.Type.GetLocation(),
typeOfExpression.Type.GetText(),
ExpectedTypeName
)
);
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System.Collections.Generic;
using System.Collections.Immutable;
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.Syntax;
using Microsoft.CodeAnalysis.Editing;

namespace Community.VisualStudio.Toolkit.Analyzers
{
public abstract class IncorrectProvidedTypeCodeFixProviderBase : CodeFixProviderBase
{
private ImmutableArray<string>? _fixableDiagnosticIds;

protected abstract string ExpectedTypeName { get; }

protected abstract string FixableDiagnosticId { get; }

public sealed override ImmutableArray<string> FixableDiagnosticIds => _fixableDiagnosticIds ??= ImmutableArray.Create(FixableDiagnosticId);

public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
SyntaxNode root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

foreach (Diagnostic diagnostic in context.Diagnostics)
{
// The convention used by the toolkit for some types is to define a container
// class for implementations of the provided types. For example, you might
// define a container class for all of the DialogPage implementations.
//
// So, find all of the nested types that inherit from
// the expected type and use their name as a suggested fix.
TypeSyntax? actualTypeSyntax = root.FindNode(diagnostic.Location.SourceSpan) as TypeSyntax;
if (actualTypeSyntax is not null)
{
SemanticModel semanticModel = await context.Document.GetSemanticModelAsync();
INamedTypeSymbol? expectedType = semanticModel.Compilation.GetTypeByMetadataName(ExpectedTypeName);
if (expectedType is not null)
{
if (semanticModel.GetSymbolInfo(actualTypeSyntax).Symbol is ITypeSymbol argumentType)
{
IEnumerable<INamedTypeSymbol> nestedTypes = argumentType
.GetTypeMembers()
.Where((x) => x.IsAssignableTo(expectedType))
.OrderBy((x) => x.Name);

foreach (INamedTypeSymbol nestedType in nestedTypes)
{
string title = string.Format(Resources.IncorrectProvidedType_CodeFix, $"{argumentType.Name}.{nestedType.Name}");
context.RegisterCodeFix(
CodeAction.Create(
title,
c => ChangeTypeNameAsync(context.Document, actualTypeSyntax, nestedType, c),
equivalenceKey: $"{FixableDiagnosticId}:{title}"
),
diagnostic
);
}
}
}
}
}
}

private static async Task<Document> ChangeTypeNameAsync(Document document, SyntaxNode nodeToChange, INamedTypeSymbol newType, CancellationToken cancellationToken)
{
SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken);
SyntaxEditor editor = new(root, document.Project.Solution.Workspace);
SyntaxGenerator generator = editor.Generator;

editor.ReplaceNode(nodeToChange, generator.NameExpression(newType));

return document.WithSyntaxRoot(editor.GetChangedRoot());
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ namespace Community.VisualStudio.Toolkit.Analyzers
{
public abstract class CodeFixProviderBase : CodeFixProvider
{
public abstract override FixAllProvider GetFixAllProvider();

protected static SyntaxList<UsingDirectiveSyntax> AddUsingDirectiveIfMissing(SyntaxList<UsingDirectiveSyntax> usings, NameSyntax namespaceName)
{
string namespaceToImport = namespaceName.ToString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.2">
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ public static class Diagnostics
{
public const string CastInteropTypes = "CVST001";
public const string DialogPageShouldBeComVisible = "CVST002";
public const string UseCorrectTypeInProvideOptionDialogPageAttribute = "CVST003";
public const string UseCorrectTypeInProvideProfileAttribute = "CVST004";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,49 @@ internal static bool IsVS(this ExpressionSyntax expression)
return expression.IsKind(SyntaxKind.IdentifierName) &&
string.Equals(((IdentifierNameSyntax)expression).Identifier.Text, "VS");
}

internal static bool IsSubclassOf(this ITypeSymbol? type, INamedTypeSymbol baseType)
{
while (type is not null)
{
if (type.Equals(baseType))
{
return true;
}

type = type.BaseType;
}

return false;
}

internal static bool IsAssignableTo(this ITypeSymbol? type, INamedTypeSymbol targetType)
{
if (type is null)
{
return false;
}

switch (targetType.TypeKind)
{
case TypeKind.Class:
// If the target type is a class, then the type can only be assigned to
// it if it inherits from that type. There is no need to look at interfaces.
return type.IsSubclassOf(targetType);

case TypeKind.Interface:
foreach (INamedTypeSymbol? interfaceType in type.AllInterfaces)
{
if (interfaceType.Equals(targetType))
{
return true;
}
}
return false;

default:
return false;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Community.VisualStudio.Toolkit.Analyzers
{
internal static class KnownTypeNames
{
public const string ComVisibleAttribute = "System.Runtime.InteropServices.ComVisibleAttribute";
public const string DialogPage = "Microsoft.VisualStudio.Shell.DialogPage";
public const string IProfileManager = "Microsoft.VisualStudio.Shell.IProfileManager";
public const string ProvideOptionDialogPageAttribute = "Microsoft.VisualStudio.Shell.ProvideOptionDialogPageAttribute";
public const string ProvideProfileAttribute = "Microsoft.VisualStudio.Shell.ProvideProfileAttribute";
}
}
Loading

0 comments on commit e2d88dd

Please sign in to comment.