Skip to content

Commit e2d88dd

Browse files
Merge pull request #284 from reduckted/feature/options-page-analyzer
Code analyzer to check the type passed to ProvideOptionDialogPageAttribute and ProvideProfileAttribute
2 parents 171b240 + 2331ba0 commit e2d88dd

18 files changed

+619
-20
lines changed

src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/CVST002DialogPageShouldBeComVisibleAnalyzer.cs

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ public override void Initialize(AnalysisContext context)
3131

3232
private void OnCompilationStart(CompilationStartAnalysisContext context)
3333
{
34-
INamedTypeSymbol? dialogPageType = context.Compilation.GetTypeByMetadataName("Microsoft.VisualStudio.Shell.DialogPage");
35-
INamedTypeSymbol? comVisibleType = context.Compilation.GetTypeByMetadataName("System.Runtime.InteropServices.ComVisibleAttribute");
34+
INamedTypeSymbol? dialogPageType = context.Compilation.GetTypeByMetadataName(KnownTypeNames.DialogPage);
35+
INamedTypeSymbol? comVisibleType = context.Compilation.GetTypeByMetadataName(KnownTypeNames.ComVisibleAttribute);
3636

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

48-
if (type is not null && IsDialogPage(type, dialogPageType))
48+
if (type is not null && type.IsSubclassOf(dialogPageType))
4949
{
5050
// This class inherits from `DialogPage`. It should contain
5151
// a `ComVisible` attribute with a parameter of `true`.
@@ -67,20 +67,5 @@ private static void AnalyzeClass(SyntaxNodeAnalysisContext context, INamedTypeSy
6767
context.ReportDiagnostic(Diagnostic.Create(_rule, classDeclaration.Identifier.GetLocation()));
6868
}
6969
}
70-
71-
private static bool IsDialogPage(ITypeSymbol? type, INamedTypeSymbol dialogPageType)
72-
{
73-
while (type is not null)
74-
{
75-
if (type.Equals(dialogPageType))
76-
{
77-
return true;
78-
}
79-
80-
type = type.BaseType;
81-
}
82-
83-
return false;
84-
}
8570
}
8671
}

src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/CVST002DialogPageShouldBeComVisibleCodeFixProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
namespace Community.VisualStudio.Toolkit.Analyzers
1414
{
15-
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CVST001CastInteropServicesCodeFixProvider))]
15+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CVST002DialogPageShouldBeComVisibleCodeFixProvider))]
1616
[Shared]
1717
public class CVST002DialogPageShouldBeComVisibleCodeFixProvider : CodeFixProviderBase
1818
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System.Collections.Immutable;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.Diagnostics;
4+
5+
namespace Community.VisualStudio.Toolkit.Analyzers
6+
{
7+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
8+
public class CVST003UseCorrectTypeInProvideOptionDialogPageAttributeAnalyzer : IncorrectProvidedTypeAnalyzerBase
9+
{
10+
internal const string DiagnosticId = Diagnostics.UseCorrectTypeInProvideOptionDialogPageAttribute;
11+
12+
private static readonly DiagnosticDescriptor _rule = new(
13+
DiagnosticId,
14+
GetLocalizableString(nameof(Resources.CVST003_Title)),
15+
GetLocalizableString(nameof(Resources.IncorrectProvidedType_MessageFormat)),
16+
"Usage",
17+
DiagnosticSeverity.Error,
18+
isEnabledByDefault: true);
19+
20+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(_rule);
21+
22+
protected override string AttributeTypeName => KnownTypeNames.ProvideOptionDialogPageAttribute;
23+
24+
protected override string ExpectedTypeName => KnownTypeNames.DialogPage;
25+
26+
protected override DiagnosticDescriptor Descriptor => _rule;
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System.Composition;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CodeFixes;
4+
5+
namespace Community.VisualStudio.Toolkit.Analyzers
6+
{
7+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CVST003UseCorrectTypeInProvideOptionDialogPageAttributeCodeFixProvider))]
8+
[Shared]
9+
public class CVST003UseCorrectTypeInProvideOptionDialogPageAttributeCodeFixProvider : IncorrectProvidedTypeCodeFixProviderBase
10+
{
11+
protected override string FixableDiagnosticId => CVST003UseCorrectTypeInProvideOptionDialogPageAttributeAnalyzer.DiagnosticId;
12+
13+
protected override string ExpectedTypeName => KnownTypeNames.DialogPage;
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System.Collections.Immutable;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.Diagnostics;
4+
5+
namespace Community.VisualStudio.Toolkit.Analyzers
6+
{
7+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
8+
public class CVST004UseCorrectTypeInProvideProfileAttributeAnalyzer : IncorrectProvidedTypeAnalyzerBase
9+
{
10+
internal const string DiagnosticId = Diagnostics.UseCorrectTypeInProvideProfileAttribute;
11+
12+
private static readonly DiagnosticDescriptor _rule = new(
13+
DiagnosticId,
14+
GetLocalizableString(nameof(Resources.CVST004_Title)),
15+
GetLocalizableString(nameof(Resources.IncorrectProvidedType_MessageFormat)),
16+
"Usage",
17+
DiagnosticSeverity.Error,
18+
isEnabledByDefault: true);
19+
20+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(_rule);
21+
22+
protected override string AttributeTypeName => KnownTypeNames.ProvideProfileAttribute;
23+
24+
protected override string ExpectedTypeName => KnownTypeNames.IProfileManager;
25+
26+
protected override DiagnosticDescriptor Descriptor => _rule;
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System.Composition;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CodeFixes;
4+
5+
namespace Community.VisualStudio.Toolkit.Analyzers
6+
{
7+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CVST004UseCorrectTypeInProvideProfileAttributeCodeFixProvider))]
8+
[Shared]
9+
public class CVST004UseCorrectTypeInProvideProfileAttributeCodeFixProvider : IncorrectProvidedTypeCodeFixProviderBase
10+
{
11+
protected override string FixableDiagnosticId => CVST004UseCorrectTypeInProvideProfileAttributeAnalyzer.DiagnosticId;
12+
13+
protected override string ExpectedTypeName => KnownTypeNames.IProfileManager;
14+
}
15+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using Microsoft.CodeAnalysis;
2+
using Microsoft.CodeAnalysis.CSharp;
3+
using Microsoft.CodeAnalysis.CSharp.Syntax;
4+
using Microsoft.CodeAnalysis.Diagnostics;
5+
6+
namespace Community.VisualStudio.Toolkit.Analyzers
7+
{
8+
public abstract class IncorrectProvidedTypeAnalyzerBase : AnalyzerBase
9+
{
10+
protected abstract string AttributeTypeName { get; }
11+
12+
protected abstract string ExpectedTypeName { get; }
13+
14+
protected abstract DiagnosticDescriptor Descriptor { get; }
15+
16+
public sealed override void Initialize(AnalysisContext context)
17+
{
18+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
19+
context.EnableConcurrentExecution();
20+
21+
context.RegisterCompilationStartAction(OnCompilationStart);
22+
}
23+
24+
private void OnCompilationStart(CompilationStartAnalysisContext context)
25+
{
26+
INamedTypeSymbol? attributeType = context.Compilation.GetTypeByMetadataName(AttributeTypeName);
27+
INamedTypeSymbol? expectedType = context.Compilation.GetTypeByMetadataName(ExpectedTypeName);
28+
29+
if (attributeType is not null && expectedType is not null)
30+
{
31+
context.RegisterSyntaxNodeAction(
32+
(c) => AnalyzeAttribute(c, attributeType, expectedType),
33+
SyntaxKind.Attribute
34+
);
35+
}
36+
}
37+
38+
private void AnalyzeAttribute(SyntaxNodeAnalysisContext context, INamedTypeSymbol attributeType, INamedTypeSymbol expectedType)
39+
{
40+
AttributeSyntax attribute = (AttributeSyntax)context.Node;
41+
if (context.SemanticModel.GetTypeInfo(attribute).Type?.IsAssignableTo(attributeType) == true)
42+
{
43+
// The type that is provided is always the first argument to the attribute's constructor.
44+
AttributeArgumentSyntax? typeArgument = attribute.ArgumentList?.Arguments.FirstOrDefault();
45+
if (typeArgument?.Expression is TypeOfExpressionSyntax typeOfExpression)
46+
{
47+
ISymbol? argumentSymbol = context.SemanticModel.GetSymbolInfo(typeOfExpression.Type).Symbol;
48+
if (argumentSymbol is ITypeSymbol argumentTypeSymbol)
49+
{
50+
if (!argumentTypeSymbol.IsAssignableTo(expectedType))
51+
{
52+
context.ReportDiagnostic(
53+
Diagnostic.Create(
54+
Descriptor,
55+
typeOfExpression.Type.GetLocation(),
56+
typeOfExpression.Type.GetText(),
57+
ExpectedTypeName
58+
)
59+
);
60+
}
61+
}
62+
}
63+
}
64+
}
65+
}
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using System.Collections.Generic;
2+
using System.Collections.Immutable;
3+
using System.Linq;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.CodeActions;
8+
using Microsoft.CodeAnalysis.CodeFixes;
9+
using Microsoft.CodeAnalysis.CSharp.Syntax;
10+
using Microsoft.CodeAnalysis.Editing;
11+
12+
namespace Community.VisualStudio.Toolkit.Analyzers
13+
{
14+
public abstract class IncorrectProvidedTypeCodeFixProviderBase : CodeFixProviderBase
15+
{
16+
private ImmutableArray<string>? _fixableDiagnosticIds;
17+
18+
protected abstract string ExpectedTypeName { get; }
19+
20+
protected abstract string FixableDiagnosticId { get; }
21+
22+
public sealed override ImmutableArray<string> FixableDiagnosticIds => _fixableDiagnosticIds ??= ImmutableArray.Create(FixableDiagnosticId);
23+
24+
public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
25+
26+
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
27+
{
28+
SyntaxNode root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
29+
30+
foreach (Diagnostic diagnostic in context.Diagnostics)
31+
{
32+
// The convention used by the toolkit for some types is to define a container
33+
// class for implementations of the provided types. For example, you might
34+
// define a container class for all of the DialogPage implementations.
35+
//
36+
// So, find all of the nested types that inherit from
37+
// the expected type and use their name as a suggested fix.
38+
TypeSyntax? actualTypeSyntax = root.FindNode(diagnostic.Location.SourceSpan) as TypeSyntax;
39+
if (actualTypeSyntax is not null)
40+
{
41+
SemanticModel semanticModel = await context.Document.GetSemanticModelAsync();
42+
INamedTypeSymbol? expectedType = semanticModel.Compilation.GetTypeByMetadataName(ExpectedTypeName);
43+
if (expectedType is not null)
44+
{
45+
if (semanticModel.GetSymbolInfo(actualTypeSyntax).Symbol is ITypeSymbol argumentType)
46+
{
47+
IEnumerable<INamedTypeSymbol> nestedTypes = argumentType
48+
.GetTypeMembers()
49+
.Where((x) => x.IsAssignableTo(expectedType))
50+
.OrderBy((x) => x.Name);
51+
52+
foreach (INamedTypeSymbol nestedType in nestedTypes)
53+
{
54+
string title = string.Format(Resources.IncorrectProvidedType_CodeFix, $"{argumentType.Name}.{nestedType.Name}");
55+
context.RegisterCodeFix(
56+
CodeAction.Create(
57+
title,
58+
c => ChangeTypeNameAsync(context.Document, actualTypeSyntax, nestedType, c),
59+
equivalenceKey: $"{FixableDiagnosticId}:{title}"
60+
),
61+
diagnostic
62+
);
63+
}
64+
}
65+
}
66+
}
67+
}
68+
}
69+
70+
private static async Task<Document> ChangeTypeNameAsync(Document document, SyntaxNode nodeToChange, INamedTypeSymbol newType, CancellationToken cancellationToken)
71+
{
72+
SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken);
73+
SyntaxEditor editor = new(root, document.Project.Solution.Workspace);
74+
SyntaxGenerator generator = editor.Generator;
75+
76+
editor.ReplaceNode(nodeToChange, generator.NameExpression(newType));
77+
78+
return document.WithSyntaxRoot(editor.GetChangedRoot());
79+
}
80+
81+
}
82+
}

src/analyzers/Community.VisualStudio.Toolkit.Analyzers/CodeFixProviderBase.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ namespace Community.VisualStudio.Toolkit.Analyzers
77
{
88
public abstract class CodeFixProviderBase : CodeFixProvider
99
{
10+
public abstract override FixAllProvider GetFixAllProvider();
11+
1012
protected static SyntaxList<UsingDirectiveSyntax> AddUsingDirectiveIfMissing(SyntaxList<UsingDirectiveSyntax> usings, NameSyntax namespaceName)
1113
{
1214
string namespaceToImport = namespaceName.ToString();

src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Community.VisualStudio.Toolkit.Analyzers.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
</PropertyGroup>
2121

2222
<ItemGroup>
23-
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.2">
23+
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
2424
<PrivateAssets>all</PrivateAssets>
2525
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
2626
</PackageReference>

0 commit comments

Comments
 (0)