Skip to content

Commit f34299a

Browse files
committed
Created a code analyzer to detect missing ComVisible attributes on DialogPage implementations.
1 parent 89e6075 commit f34299a

File tree

9 files changed

+411
-56
lines changed

9 files changed

+411
-56
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public class CVST001CastInteropServicesAnalyzer : AnalyzerBase
2626
isEnabledByDefault: true,
2727
description: GetLocalizableString(nameof(Resources.CVST001_Description)));
2828

29-
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(_rule); } }
29+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(_rule);
3030

3131
public override void Initialize(AnalysisContext context)
3232
{

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

Lines changed: 2 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ namespace Community.VisualStudio.Toolkit.Analyzers
1313
{
1414
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CVST001CastInteropServicesCodeFixProvider))]
1515
[Shared]
16-
public class CVST001CastInteropServicesCodeFixProvider : CodeFixProvider
16+
public class CVST001CastInteropServicesCodeFixProvider : CodeFixProviderBase
1717
{
18-
public sealed override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(CVST001CastInteropServicesAnalyzer.DiagnosticId);
18+
public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(CVST001CastInteropServicesAnalyzer.DiagnosticId);
1919

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

@@ -208,58 +208,5 @@ private static bool IsDeclaratorForObject(SyntaxNode node, out VariableDeclarati
208208
variableDeclaration = null!;
209209
return false;
210210
}
211-
212-
private static SyntaxList<UsingDirectiveSyntax> AddUsingDirectiveIfMissing(SyntaxList<UsingDirectiveSyntax> usings, NameSyntax namespaceName)
213-
{
214-
string namespaceToImport = namespaceName.ToString();
215-
int? insertionIndex = null;
216-
217-
// If the `using` directive is missing, then when we add it, we'll
218-
// attempt to keep the existing statements in alphabetical order.
219-
for (int i = 0; i < usings.Count; i++)
220-
{
221-
// Type aliases are usually put last, so if we haven't found an
222-
// insertion index yet, then we can insert it before this statement.
223-
if (usings[i].Alias is not null)
224-
{
225-
if (!insertionIndex.HasValue)
226-
{
227-
insertionIndex = i;
228-
}
229-
}
230-
else
231-
{
232-
string name = usings[i].Name.ToString();
233-
// If the namespace is already imported, then we can return
234-
// the original list of `using` directives without modifying them.
235-
if (string.Equals(name, namespaceToImport, System.StringComparison.Ordinal))
236-
{
237-
return usings;
238-
}
239-
240-
// If we don't have an insertion index, and this `using` directive is
241-
// greater than the one we want to insert, then this is the first
242-
// directive that should appear after the one we insert.
243-
if (!insertionIndex.HasValue && string.Compare(name, namespaceToImport) > 0)
244-
{
245-
insertionIndex = i;
246-
}
247-
}
248-
}
249-
250-
UsingDirectiveSyntax directive = SyntaxFactory.UsingDirective(namespaceName);
251-
252-
// If we found where to insert the new directive, then insert
253-
// it at that index; otherwise, it must be greater than all
254-
// existing directives, so add it to the end of the list.
255-
if (insertionIndex.HasValue)
256-
{
257-
return usings.Insert(insertionIndex.Value, directive);
258-
}
259-
else
260-
{
261-
return usings.Add(directive);
262-
}
263-
}
264211
}
265212
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
using System.Collections.Immutable;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CSharp;
4+
using Microsoft.CodeAnalysis.CSharp.Syntax;
5+
using Microsoft.CodeAnalysis.Diagnostics;
6+
7+
namespace Community.VisualStudio.Toolkit.Analyzers
8+
{
9+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
10+
public class CVST002DialogPageShouldBeComVisibleAnalyzer : AnalyzerBase
11+
{
12+
internal const string DiagnosticId = Diagnostics.DialogPageShouldBeComVisible;
13+
14+
private static readonly DiagnosticDescriptor _rule = new(
15+
DiagnosticId,
16+
GetLocalizableString(nameof(Resources.CVST002_Title)),
17+
GetLocalizableString(nameof(Resources.CVST002_MessageFormat)),
18+
"Usage",
19+
DiagnosticSeverity.Error,
20+
isEnabledByDefault: true);
21+
22+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(_rule);
23+
24+
public override void Initialize(AnalysisContext context)
25+
{
26+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
27+
context.EnableConcurrentExecution();
28+
29+
context.RegisterCompilationStartAction(OnCompilationStart);
30+
}
31+
32+
private void OnCompilationStart(CompilationStartAnalysisContext context)
33+
{
34+
INamedTypeSymbol? dialogPageType = context.Compilation.GetTypeByMetadataName("Microsoft.VisualStudio.Shell.DialogPage");
35+
INamedTypeSymbol? comVisibleType = context.Compilation.GetTypeByMetadataName("System.Runtime.InteropServices.ComVisibleAttribute");
36+
37+
if (dialogPageType is not null && comVisibleType is not null)
38+
{
39+
context.RegisterSyntaxNodeAction((c) => AnalyzeClass(c, dialogPageType, comVisibleType), SyntaxKind.ClassDeclaration);
40+
}
41+
}
42+
43+
private static void AnalyzeClass(SyntaxNodeAnalysisContext context, INamedTypeSymbol dialogPageType, INamedTypeSymbol comVisibleType)
44+
{
45+
ClassDeclarationSyntax classDeclaration = (ClassDeclarationSyntax)context.Node;
46+
ITypeSymbol? type = context.ContainingSymbol as ITypeSymbol;
47+
48+
if (type is not null && IsDialogPage(type, dialogPageType))
49+
{
50+
// This class inherits from `DialogPage`. It should contain
51+
// a `ComVisible` attribute with a parameter of `true`.
52+
foreach (AttributeData attribute in type.GetAttributes())
53+
{
54+
if (attribute.AttributeClass.Equals(comVisibleType))
55+
{
56+
if (attribute.ConstructorArguments.Length == 1)
57+
{
58+
if (Equals(attribute.ConstructorArguments[0].Value, true))
59+
{
60+
return;
61+
}
62+
}
63+
}
64+
}
65+
66+
// The `ComVisible` attribute was not found, so report the diagnostic.
67+
context.ReportDiagnostic(Diagnostic.Create(_rule, classDeclaration.Identifier.GetLocation()));
68+
}
69+
}
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+
}
85+
}
86+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using System.Collections.Immutable;
2+
using System.Composition;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CodeActions;
7+
using Microsoft.CodeAnalysis.CodeFixes;
8+
using Microsoft.CodeAnalysis.CSharp;
9+
using Microsoft.CodeAnalysis.CSharp.Syntax;
10+
using Microsoft.CodeAnalysis.Editing;
11+
using Microsoft.CodeAnalysis.Text;
12+
13+
namespace Community.VisualStudio.Toolkit.Analyzers
14+
{
15+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CVST001CastInteropServicesCodeFixProvider))]
16+
[Shared]
17+
public class CVST002DialogPageShouldBeComVisibleCodeFixProvider : CodeFixProviderBase
18+
{
19+
public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(CVST002DialogPageShouldBeComVisibleAnalyzer.DiagnosticId);
20+
21+
public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
22+
23+
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
24+
{
25+
SyntaxNode root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
26+
27+
foreach (Diagnostic diagnostic in context.Diagnostics)
28+
{
29+
TextSpan span = diagnostic.Location.SourceSpan;
30+
ClassDeclarationSyntax? classDeclaration = root.FindToken(span.Start).Parent as ClassDeclarationSyntax;
31+
32+
if (classDeclaration is not null)
33+
{
34+
context.RegisterCodeFix(
35+
CodeAction.Create(
36+
Resources.CVST002_CodeFix,
37+
c => AddComVisibleAttributeAsync(context.Document, classDeclaration, c),
38+
equivalenceKey: nameof(Resources.CVST002_CodeFix)
39+
),
40+
diagnostic
41+
);
42+
}
43+
}
44+
}
45+
46+
private async Task<Document> AddComVisibleAttributeAsync(Document document, ClassDeclarationSyntax classDeclaration, CancellationToken cancellationToken)
47+
{
48+
SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken);
49+
SyntaxEditor editor = new(root, document.Project.Solution.Workspace);
50+
SyntaxGenerator generator = editor.Generator;
51+
52+
SyntaxNode attribute = generator.Attribute("ComVisible", new[] { generator.TrueLiteralExpression() });
53+
editor.AddAttribute(classDeclaration, attribute);
54+
55+
root = editor.GetChangedRoot();
56+
57+
// Add a using directive for the namespace
58+
// if the namespace is not already imported.
59+
if (root is CompilationUnitSyntax unit)
60+
{
61+
root = unit.WithUsings(AddUsingDirectiveIfMissing(unit.Usings, SyntaxFactory.ParseName("System.Runtime.InteropServices")));
62+
}
63+
64+
return document.WithSyntaxRoot(root);
65+
}
66+
}
67+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using Microsoft.CodeAnalysis;
2+
using Microsoft.CodeAnalysis.CodeFixes;
3+
using Microsoft.CodeAnalysis.CSharp;
4+
using Microsoft.CodeAnalysis.CSharp.Syntax;
5+
6+
namespace Community.VisualStudio.Toolkit.Analyzers
7+
{
8+
public abstract class CodeFixProviderBase : CodeFixProvider
9+
{
10+
protected static SyntaxList<UsingDirectiveSyntax> AddUsingDirectiveIfMissing(SyntaxList<UsingDirectiveSyntax> usings, NameSyntax namespaceName)
11+
{
12+
string namespaceToImport = namespaceName.ToString();
13+
int? insertionIndex = null;
14+
15+
// If the `using` directive is missing, then when we add it, we'll
16+
// attempt to keep the existing statements in alphabetical order.
17+
for (int i = 0; i < usings.Count; i++)
18+
{
19+
// Type aliases are usually put last, so if we haven't found an
20+
// insertion index yet, then we can insert it before this statement.
21+
if (usings[i].Alias is not null)
22+
{
23+
if (!insertionIndex.HasValue)
24+
{
25+
insertionIndex = i;
26+
}
27+
}
28+
else
29+
{
30+
string name = usings[i].Name.ToString();
31+
// If the namespace is already imported, then we can return
32+
// the original list of `using` directives without modifying them.
33+
if (string.Equals(name, namespaceToImport, System.StringComparison.Ordinal))
34+
{
35+
return usings;
36+
}
37+
38+
// If we don't have an insertion index, and this `using` directive is
39+
// greater than the one we want to insert, then this is the first
40+
// directive that should appear after the one we insert.
41+
if (!insertionIndex.HasValue && string.Compare(name, namespaceToImport) > 0)
42+
{
43+
insertionIndex = i;
44+
}
45+
}
46+
}
47+
48+
UsingDirectiveSyntax directive = SyntaxFactory.UsingDirective(namespaceName);
49+
50+
// If we found where to insert the new directive, then insert
51+
// it at that index; otherwise, it must be greater than all
52+
// existing directives, so add it to the end of the list.
53+
if (insertionIndex.HasValue)
54+
{
55+
return usings.Insert(insertionIndex.Value, directive);
56+
}
57+
else
58+
{
59+
return usings.Add(directive);
60+
}
61+
}
62+
63+
}
64+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
public static class Diagnostics
44
{
55
public const string CastInteropTypes = "CVST001";
6+
public const string DialogPageShouldBeComVisible = "CVST002";
67
}
78
}

src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Resources.Designer.cs

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Resources.resx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,13 @@
129129
<data name="CVST001_Title" xml:space="preserve">
130130
<value>Cast interop services to their specific type</value>
131131
</data>
132+
<data name="CVST002_CodeFix" xml:space="preserve">
133+
<value>Add [ComVisible(true)]</value>
134+
</data>
135+
<data name="CVST002_MessageFormat" xml:space="preserve">
136+
<value>DialogPage implementations should be visible to COM</value>
137+
</data>
138+
<data name="CVST002_Title" xml:space="preserve">
139+
<value>Make DialogPage implementations visible to COM</value>
140+
</data>
132141
</root>

0 commit comments

Comments
 (0)