From 2331ba0871bd4cdf619dc96843bf7b4d7a0d1be1 Mon Sep 17 00:00:00 2001 From: reduckted Date: Wed, 26 Jan 2022 17:29:18 +1000 Subject: [PATCH] Created an analyzer that checks the types passed to ProvideOptionDialogPageAttribute and ProvideProfileAttribute. --- ...002DialogPageShouldBeComVisibleAnalyzer.cs | 21 +-- ...ogPageShouldBeComVisibleCodeFixProvider.cs | 2 +- ...rovideOptionDialogPageAttributeAnalyzer.cs | 28 ++++ ...ptionDialogPageAttributeCodeFixProvider.cs | 15 ++ ...ctTypeInProvideProfileAttributeAnalyzer.cs | 28 ++++ ...nProvideProfileAttributeCodeFixProvider.cs | 15 ++ .../IncorrectProvidedTypeAnalyzerBase.cs | 66 +++++++++ ...ncorrectProvidedTypeCodeFixProviderBase.cs | 82 +++++++++++ .../CodeFixProviderBase.cs | 2 + ...nity.VisualStudio.Toolkit.Analyzers.csproj | 2 +- .../Diagnostics.cs | 2 + .../Extensions.cs | 44 ++++++ .../KnownTypeNames.cs | 11 ++ .../Resources.Designer.cs | 36 +++++ .../Resources.resx | 12 ++ ...eOptionDialogPageAttributeAnalyzerTests.cs | 138 ++++++++++++++++++ ...eInProvideProfileAttributeAnalyzerTests.cs | 124 ++++++++++++++++ .../Helpers/TestBase.cs | 11 ++ 18 files changed, 619 insertions(+), 20 deletions(-) create mode 100644 src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/CVST003UseCorrectTypeInProvideOptionDialogPageAttributeAnalyzer.cs create mode 100644 src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/CVST003UseCorrectTypeInProvideOptionDialogPageAttributeCodeFixProvider.cs create mode 100644 src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/CVST004UseCorrectTypeInProvideProfileAttributeAnalyzer.cs create mode 100644 src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/CVST004UseCorrectTypeInProvideProfileAttributeCodeFixProvider.cs create mode 100644 src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/IncorrectProvidedTypeAnalyzerBase.cs create mode 100644 src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/IncorrectProvidedTypeCodeFixProviderBase.cs create mode 100644 src/analyzers/Community.VisualStudio.Toolkit.Analyzers/KnownTypeNames.cs create mode 100644 test/analyzers/Community.VisualStudio.Toolkit.Analyzers.UnitTests/Analyzers/CVST003UseCorrectTypeInProvideOptionDialogPageAttributeAnalyzerTests.cs create mode 100644 test/analyzers/Community.VisualStudio.Toolkit.Analyzers.UnitTests/Analyzers/CVST004UseCorrectTypeInProvideProfileAttributeAnalyzerTests.cs diff --git a/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/CVST002DialogPageShouldBeComVisibleAnalyzer.cs b/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/CVST002DialogPageShouldBeComVisibleAnalyzer.cs index 929121d..bb59d64 100644 --- a/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/CVST002DialogPageShouldBeComVisibleAnalyzer.cs +++ b/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/CVST002DialogPageShouldBeComVisibleAnalyzer.cs @@ -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) { @@ -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`. @@ -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; - } } } diff --git a/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/CVST002DialogPageShouldBeComVisibleCodeFixProvider.cs b/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/CVST002DialogPageShouldBeComVisibleCodeFixProvider.cs index 6e9faa5..83cadba 100644 --- a/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/CVST002DialogPageShouldBeComVisibleCodeFixProvider.cs +++ b/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/CVST002DialogPageShouldBeComVisibleCodeFixProvider.cs @@ -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 { diff --git a/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/CVST003UseCorrectTypeInProvideOptionDialogPageAttributeAnalyzer.cs b/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/CVST003UseCorrectTypeInProvideOptionDialogPageAttributeAnalyzer.cs new file mode 100644 index 0000000..578a4f1 --- /dev/null +++ b/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/CVST003UseCorrectTypeInProvideOptionDialogPageAttributeAnalyzer.cs @@ -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 SupportedDiagnostics { get; } = ImmutableArray.Create(_rule); + + protected override string AttributeTypeName => KnownTypeNames.ProvideOptionDialogPageAttribute; + + protected override string ExpectedTypeName => KnownTypeNames.DialogPage; + + protected override DiagnosticDescriptor Descriptor => _rule; + } +} diff --git a/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/CVST003UseCorrectTypeInProvideOptionDialogPageAttributeCodeFixProvider.cs b/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/CVST003UseCorrectTypeInProvideOptionDialogPageAttributeCodeFixProvider.cs new file mode 100644 index 0000000..c38dee2 --- /dev/null +++ b/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/CVST003UseCorrectTypeInProvideOptionDialogPageAttributeCodeFixProvider.cs @@ -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; + } +} diff --git a/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/CVST004UseCorrectTypeInProvideProfileAttributeAnalyzer.cs b/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/CVST004UseCorrectTypeInProvideProfileAttributeAnalyzer.cs new file mode 100644 index 0000000..6e1ab09 --- /dev/null +++ b/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/CVST004UseCorrectTypeInProvideProfileAttributeAnalyzer.cs @@ -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 SupportedDiagnostics { get; } = ImmutableArray.Create(_rule); + + protected override string AttributeTypeName => KnownTypeNames.ProvideProfileAttribute; + + protected override string ExpectedTypeName => KnownTypeNames.IProfileManager; + + protected override DiagnosticDescriptor Descriptor => _rule; + } +} diff --git a/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/CVST004UseCorrectTypeInProvideProfileAttributeCodeFixProvider.cs b/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/CVST004UseCorrectTypeInProvideProfileAttributeCodeFixProvider.cs new file mode 100644 index 0000000..768fb3b --- /dev/null +++ b/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/CVST004UseCorrectTypeInProvideProfileAttributeCodeFixProvider.cs @@ -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; + } +} diff --git a/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/IncorrectProvidedTypeAnalyzerBase.cs b/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/IncorrectProvidedTypeAnalyzerBase.cs new file mode 100644 index 0000000..e34e343 --- /dev/null +++ b/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/IncorrectProvidedTypeAnalyzerBase.cs @@ -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 + ) + ); + } + } + } + } + } + } +} diff --git a/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/IncorrectProvidedTypeCodeFixProviderBase.cs b/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/IncorrectProvidedTypeCodeFixProviderBase.cs new file mode 100644 index 0000000..60ed8d8 --- /dev/null +++ b/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Analyzers/IncorrectProvidedTypeCodeFixProviderBase.cs @@ -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? _fixableDiagnosticIds; + + protected abstract string ExpectedTypeName { get; } + + protected abstract string FixableDiagnosticId { get; } + + public sealed override ImmutableArray 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 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 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()); + } + + } +} diff --git a/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/CodeFixProviderBase.cs b/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/CodeFixProviderBase.cs index 8d80a44..b7f1afd 100644 --- a/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/CodeFixProviderBase.cs +++ b/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/CodeFixProviderBase.cs @@ -7,6 +7,8 @@ namespace Community.VisualStudio.Toolkit.Analyzers { public abstract class CodeFixProviderBase : CodeFixProvider { + public abstract override FixAllProvider GetFixAllProvider(); + protected static SyntaxList AddUsingDirectiveIfMissing(SyntaxList usings, NameSyntax namespaceName) { string namespaceToImport = namespaceName.ToString(); diff --git a/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Community.VisualStudio.Toolkit.Analyzers.csproj b/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Community.VisualStudio.Toolkit.Analyzers.csproj index b84a6b4..11dd8a9 100644 --- a/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Community.VisualStudio.Toolkit.Analyzers.csproj +++ b/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Community.VisualStudio.Toolkit.Analyzers.csproj @@ -20,7 +20,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Diagnostics.cs b/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Diagnostics.cs index 7214ec9..dab370f 100644 --- a/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Diagnostics.cs +++ b/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Diagnostics.cs @@ -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"; } } diff --git a/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Extensions.cs b/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Extensions.cs index 0a467e9..3bb7ccf 100644 --- a/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Extensions.cs +++ b/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Extensions.cs @@ -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; + } + } } } diff --git a/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/KnownTypeNames.cs b/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/KnownTypeNames.cs new file mode 100644 index 0000000..676c602 --- /dev/null +++ b/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/KnownTypeNames.cs @@ -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"; + } +} diff --git a/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Resources.Designer.cs b/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Resources.Designer.cs index 7769b1b..79697aa 100644 --- a/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Resources.Designer.cs +++ b/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Resources.Designer.cs @@ -123,5 +123,41 @@ internal static string CVST002_Title { return ResourceManager.GetString("CVST002_Title", resourceCulture); } } + + /// + /// Looks up a localized string similar to Register correct type in ProvideOptionPageAttribute. + /// + internal static string CVST003_Title { + get { + return ResourceManager.GetString("CVST003_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Register correct type in ProvideProfileAttribute. + /// + internal static string CVST004_Title { + get { + return ResourceManager.GetString("CVST004_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Change to '{0}'. + /// + internal static string IncorrectProvidedType_CodeFix { + get { + return ResourceManager.GetString("IncorrectProvidedType_CodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The type '{0}' is not assignable to '{1}'. + /// + internal static string IncorrectProvidedType_MessageFormat { + get { + return ResourceManager.GetString("IncorrectProvidedType_MessageFormat", resourceCulture); + } + } } } diff --git a/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Resources.resx b/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Resources.resx index 35f3925..c7c5533 100644 --- a/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Resources.resx +++ b/src/analyzers/Community.VisualStudio.Toolkit.Analyzers/Resources.resx @@ -138,4 +138,16 @@ Make DialogPage implementations visible to COM + + Register correct type in ProvideOptionPageAttribute + + + Register correct type in ProvideProfileAttribute + + + Change to '{0}' + + + The type '{0}' is not assignable to '{1}' + \ No newline at end of file diff --git a/test/analyzers/Community.VisualStudio.Toolkit.Analyzers.UnitTests/Analyzers/CVST003UseCorrectTypeInProvideOptionDialogPageAttributeAnalyzerTests.cs b/test/analyzers/Community.VisualStudio.Toolkit.Analyzers.UnitTests/Analyzers/CVST003UseCorrectTypeInProvideOptionDialogPageAttributeAnalyzerTests.cs new file mode 100644 index 0000000..d35a102 --- /dev/null +++ b/test/analyzers/Community.VisualStudio.Toolkit.Analyzers.UnitTests/Analyzers/CVST003UseCorrectTypeInProvideOptionDialogPageAttributeAnalyzerTests.cs @@ -0,0 +1,138 @@ +using System.Threading.Tasks; +using Microsoft.VisualStudio.Shell; +using Xunit; + +namespace Community.VisualStudio.Toolkit.Analyzers.UnitTests +{ + public class CVST003UseCorrectTypeInProvideOptionDialogPageAttributeAnalyzerTests : TestBase + { + public CVST003UseCorrectTypeInProvideOptionDialogPageAttributeAnalyzerTests() + { + AddReference(typeof(DialogPage)); + AddReference(typeof(RegistrationAttribute)); + AddReference(typeof(ProvideOptionPageAttribute)); + } + + [Fact] + public async Task DoesNotFlagWhenDialogPageIsUsedInAttributeAsync() + { + TestCode = @" +using Microsoft.VisualStudio.Shell; +using System.Runtime.InteropServices; + +[ProvideOptionPage(typeof(MyDialogPage), ""Foo"", ""Bar"", 0, 0, false, 0)] +class Package {} +class MyDialogPage : DialogPage {} +"; + + await VerifyAsync(); + } + + [Fact] + public async Task FlagsIncorrectTypeInProvideOptionPageAttributeAsync() + { + TestCode = @" +using Microsoft.VisualStudio.Shell; +using System.Runtime.InteropServices; + +[ProvideOptionPage(typeof({|#0:MyDialogPage|}), ""Foo"", ""Bar"", 0, 0, false, 0)] +class Package {} +class MyDialogPage {} +"; + + Expect( + Diagnostic(Diagnostics.UseCorrectTypeInProvideOptionDialogPageAttribute) + .WithLocation(0) + .WithMessage("The type 'MyDialogPage' is not assignable to 'Microsoft.VisualStudio.Shell.DialogPage'") + ); + + await VerifyAsync(); + } + + [Fact] + public async Task FlagsIncorrectTypeInProvideToolboxPageAttributeAsync() + { + TestCode = @" +using Microsoft.VisualStudio.Shell; +using System.Runtime.InteropServices; + +[ProvideToolboxPage(typeof({|#0:MyDialogPage|}), 0)] +class Package {} +class MyDialogPage {} +"; + + Expect( + Diagnostic(Diagnostics.UseCorrectTypeInProvideOptionDialogPageAttribute) + .WithLocation(0) + .WithMessage("The type 'MyDialogPage' is not assignable to 'Microsoft.VisualStudio.Shell.DialogPage'") + ); + + await VerifyAsync(); + } + + [Fact] + public async Task FlagsIncorrectTypeInProvideLanguageEditorOptionPageAttributeAsync() + { + TestCode = @" +using Microsoft.VisualStudio.Shell; +using System.Runtime.InteropServices; + +[ProvideLanguageEditorOptionPage(typeof({|#0:MyDialogPage|}), ""a"", ""b"", ""c"", ""d"")] +class Package {} +class MyDialogPage {} +"; + + Expect( + Diagnostic(Diagnostics.UseCorrectTypeInProvideOptionDialogPageAttribute) + .WithLocation(0) + .WithMessage("The type 'MyDialogPage' is not assignable to 'Microsoft.VisualStudio.Shell.DialogPage'") + ); + + await VerifyAsync(); + } + + [Theory] + [InlineData(0, "MyDialogPage.Alpha")] + [InlineData(1, "MyDialogPage.Beta")] + [InlineData(2, "MyDialogPage.Gamma")] + public async Task CanFixIncorrectTypeWhenSpecifiedTypeContainsNestedDialogPageTypesAsync(int codeFixIndex, string expectedTypeName) + { + TestCode = @" +using Microsoft.VisualStudio.Shell; +using System.Runtime.InteropServices; + +[ProvideOptionPage(typeof({|#0:MyDialogPage|}), ""Foo"", ""Bar"", 0, 0, false, 0)] +class Package {} +class MyDialogPage { + public class Alpha : DialogPage {} + public class Gamma : DialogPage {} + public class Beta : DialogPage {} +} +"; + + Expect( + Diagnostic(Diagnostics.UseCorrectTypeInProvideOptionDialogPageAttribute) + .WithLocation(0) + .WithMessage("The type 'MyDialogPage' is not assignable to 'Microsoft.VisualStudio.Shell.DialogPage'") + ); + + CodeActionIndex = codeFixIndex; + CodeActionVerifier = (action, verifier) => verifier.Equal(action.Title, $"Change to '{expectedTypeName}'"); + + FixedCode = $@" +using Microsoft.VisualStudio.Shell; +using System.Runtime.InteropServices; + +[ProvideOptionPage(typeof({expectedTypeName}), ""Foo"", ""Bar"", 0, 0, false, 0)] +class Package {{}} +class MyDialogPage {{ + public class Alpha : DialogPage {{}} + public class Gamma : DialogPage {{}} + public class Beta : DialogPage {{}} +}} +"; + + await VerifyAsync(); + } + } +} diff --git a/test/analyzers/Community.VisualStudio.Toolkit.Analyzers.UnitTests/Analyzers/CVST004UseCorrectTypeInProvideProfileAttributeAnalyzerTests.cs b/test/analyzers/Community.VisualStudio.Toolkit.Analyzers.UnitTests/Analyzers/CVST004UseCorrectTypeInProvideProfileAttributeAnalyzerTests.cs new file mode 100644 index 0000000..f4ebc74 --- /dev/null +++ b/test/analyzers/Community.VisualStudio.Toolkit.Analyzers.UnitTests/Analyzers/CVST004UseCorrectTypeInProvideProfileAttributeAnalyzerTests.cs @@ -0,0 +1,124 @@ +using System.Threading.Tasks; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; +using Xunit; + +namespace Community.VisualStudio.Toolkit.Analyzers.UnitTests +{ + public class CVST004UseCorrectTypeInProvideProfileAttributeAnalyzerTests : TestBase + { + private const string _profileManagerImplementationBody = @" + public void SaveSettingsToXml(IVsSettingsWriter writer) {} + public void LoadSettingsFromXml(IVsSettingsReader reader) {} + public void SaveSettingsToStorage() {} + public void LoadSettingsFromStorage() {} + public void ResetSettings() {} + "; + + public CVST004UseCorrectTypeInProvideProfileAttributeAnalyzerTests() + { + AddReference(typeof(IProfileManager)); + AddReference(typeof(RegistrationAttribute)); + AddReference(typeof(ProvideProfileAttribute)); + AddReference(typeof(IVsSettingsReader)); + } + + [Fact] + public async Task DoesNotFlagWhenProfileManagerIsUsedInAttributeAsync() + { + TestCode = $@" +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; +using System.Runtime.InteropServices; + +[ProvideProfile(typeof(MyProfile), ""Foo"", ""Bar"", 0, 0, false)] +class Package {{}} +class MyProfile : IProfileManager {{ + {_profileManagerImplementationBody} +}} +"; + + await VerifyAsync(); + } + + [Fact] + public async Task FlagsIncorrectTypeInAttributeAsync() + { + TestCode = @" +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; +using System.Runtime.InteropServices; + +[ProvideProfile(typeof({|#0:MyProfile|}), ""Foo"", ""Bar"", 0, 0, false)] +class Package {} +class MyProfile {} +"; + + Expect( + Diagnostic(Diagnostics.UseCorrectTypeInProvideProfileAttribute) + .WithLocation(0) + .WithMessage("The type 'MyProfile' is not assignable to 'Microsoft.VisualStudio.Shell.IProfileManager'") + ); + + await VerifyAsync(); + } + + [Theory] + [InlineData(0, "MyProfile.Alpha")] + [InlineData(1, "MyProfile.Beta")] + [InlineData(2, "MyProfile.Gamma")] + public async Task CanFixIncorrectTypeWhenSpecifiedTypeContainsNestedDialogPageTypesAsync(int codeFixIndex, string expectedTypeName) + { + TestCode = $@" +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; +using System.Runtime.InteropServices; + +[ProvideProfile(typeof({{|#0:MyProfile|}}), ""Foo"", ""Bar"", 0, 0, false)] +class Package {{}} +class MyProfile {{ + public class Alpha : IProfileManager {{ + {_profileManagerImplementationBody} + }} + public class Gamma : IProfileManager {{ + {_profileManagerImplementationBody} + }} + public class Beta : IProfileManager {{ + {_profileManagerImplementationBody} + }} +}} +"; + + Expect( + Diagnostic(Diagnostics.UseCorrectTypeInProvideProfileAttribute) + .WithLocation(0) + .WithMessage("The type 'MyProfile' is not assignable to 'Microsoft.VisualStudio.Shell.IProfileManager'") + ); + + CodeActionIndex = codeFixIndex; + CodeActionVerifier = (action, verifier) => verifier.Equal(action.Title, $"Change to '{expectedTypeName}'"); + + FixedCode = $@" +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; +using System.Runtime.InteropServices; + +[ProvideProfile(typeof({expectedTypeName}), ""Foo"", ""Bar"", 0, 0, false)] +class Package {{}} +class MyProfile {{ + public class Alpha : IProfileManager {{ + {_profileManagerImplementationBody} + }} + public class Gamma : IProfileManager {{ + {_profileManagerImplementationBody} + }} + public class Beta : IProfileManager {{ + {_profileManagerImplementationBody} + }} +}} +"; + + await VerifyAsync(); + } + } +} diff --git a/test/analyzers/Community.VisualStudio.Toolkit.Analyzers.UnitTests/Helpers/TestBase.cs b/test/analyzers/Community.VisualStudio.Toolkit.Analyzers.UnitTests/Helpers/TestBase.cs index db65db8..9adafa9 100644 --- a/test/analyzers/Community.VisualStudio.Toolkit.Analyzers.UnitTests/Helpers/TestBase.cs +++ b/test/analyzers/Community.VisualStudio.Toolkit.Analyzers.UnitTests/Helpers/TestBase.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp.Testing; using Microsoft.CodeAnalysis.Diagnostics; @@ -37,6 +38,16 @@ private static string NormalizeLineEndings(string value) return Regex.Replace(value, "\r?\n", Environment.NewLine); } + protected int? CodeActionIndex + { + set { _test.CodeActionIndex = value; } + } + + protected Action? CodeActionVerifier + { + set { _test.CodeActionVerifier = value; } + } + protected void AddReference(Type typeInAssemblyToReference) { _test.References.Add(typeInAssemblyToReference);