Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions doc/analyzers/VSMEF003.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# VSMEF003: Exported type not implemented by exporting class

A class that declares `[Export(typeof(T))]` should implement interface `T` or inherit from class `T`.

## Cause

A class is decorated with `[Export(typeof(T))]` but does not implement the interface or inherit from the class specified by `T`.

## Rule description

When using MEF Export attributes with an explicit type parameter, the exporting class should implement that type. If the class does not implement the specified interface or inherit from the specified base class, it will likely cause runtime composition failures or unexpected behavior.

## How to fix violations

Either:

- Make the exporting class implement the specified interface, or
- Make the exporting class inherit from the specified base class, or
- Change the Export attribute to export the correct type, or
- Remove the type parameter to export the class's own type

## When to suppress warnings

This warning can be suppressed if you intentionally want to export a type that is not implemented by the exporting class, though this is rarely a good practice and may cause composition issues at runtime.

## Example

### Violates

```csharp
interface ICalculator
{
int Add(int a, int b);
}

[Export(typeof(ICalculator))] // ❌ Violates VSMEF003
public class TextProcessor
{
public string ProcessText(string input) => input.ToUpper();
}
```

### Does not violate

```csharp
interface ICalculator
{
int Add(int a, int b);
}

[Export(typeof(ICalculator))] // ✅ OK
public class Calculator : ICalculator
{
public int Add(int a, int b) => a + b;
}

[Export] // ✅ OK - exports Calculator type
public class Calculator
{
public int Add(int a, int b) => a + b;
}
```
2 changes: 2 additions & 0 deletions doc/analyzers/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ to help you avoid common mistakes while authoring MEF parts.
ID | Title
--|--
VSMEF001 | Importing property must have setter
VSMEF002 | Avoid mixing MEF attribute libraries
VSMEF003 | Exported type not implemented by exporting class
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
### New Rules
Rule ID | Category | Severity | Notes
--------|----------|----------|-------
VSMEF003 | Usage | Warning | Exported type not implemented by exporting class
1 change: 1 addition & 0 deletions src/Microsoft.VisualStudio.Composition.Analyzers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ Analyzers for MEF consumers to help identify common errors in MEF parts.
Analyzer ID | Description
--|--
VSMEF001 | Ensures that importing properties define a `set` accessor.
VSMEF003 | Ensures that exported types are implemented by the exporting class.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/Microsoft.VisualStudio.Composition.Analyzers/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,10 @@
<data name="VSMEF002_Title" xml:space="preserve">
<value>Avoid mixing MEF attribute varieties</value>
</data>
<data name="VSMEF003_MessageFormat" xml:space="preserve">
<value>The type "{0}" does not implement the exported type "{1}". This may be an authoring mistake.</value>
</data>
<data name="VSMEF003_Title" xml:space="preserve">
<value>Exported type not implemented by exporting class</value>
</data>
</root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.VisualStudio.Composition.Analyzers;

/// <summary>
/// Creates a diagnostic when `[Export(typeof(T))]` is applied to a class that does not implement T,
/// or to a property whose type is not compatible with T.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public class VSMEF003ExportTypeMismatchAnalyzer : DiagnosticAnalyzer
{
/// <summary>
/// The ID for diagnostics reported by this analyzer.
/// </summary>
public const string Id = "VSMEF003";

/// <summary>
/// The descriptor used for diagnostics created by this rule.
/// </summary>
internal static readonly DiagnosticDescriptor Descriptor = new(
id: Id,
title: Strings.VSMEF003_Title,
messageFormat: Strings.VSMEF003_MessageFormat,
helpLinkUri: Utils.GetHelpLink(Id),
category: "Usage",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);

/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Descriptor);

/// <inheritdoc/>
public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);

context.RegisterCompilationStartAction(context =>
{
// Only scan further if the compilation references the assemblies that define the attributes we'll be looking for.
if (context.Compilation.ReferencedAssemblyNames.Any(i =>
string.Equals(i.Name, "System.ComponentModel.Composition", StringComparison.OrdinalIgnoreCase) ||
string.Equals(i.Name, "System.Composition.AttributedModel", StringComparison.OrdinalIgnoreCase)))
{
INamedTypeSymbol? mefV1ExportAttribute = context.Compilation.GetTypeByMetadataName("System.ComponentModel.Composition.ExportAttribute");
INamedTypeSymbol? mefV2ExportAttribute = context.Compilation.GetTypeByMetadataName("System.Composition.ExportAttribute");
context.RegisterSymbolAction(
context => AnalyzeTypeDeclaration(context, mefV1ExportAttribute, mefV2ExportAttribute),
SymbolKind.NamedType);
context.RegisterSymbolAction(
context => AnalyzePropertyDeclaration(context, mefV1ExportAttribute, mefV2ExportAttribute),
SymbolKind.Property);
}
});
}

private static void AnalyzeTypeDeclaration(SymbolAnalysisContext context, INamedTypeSymbol? mefV1ExportAttribute, INamedTypeSymbol? mefV2ExportAttribute)
{
var namedType = (INamedTypeSymbol)context.Symbol;

// Skip interfaces, enums, delegates - only analyze classes
if (namedType.TypeKind != TypeKind.Class)
{
return;
}

Location? location = namedType.Locations.FirstOrDefault();
if (location is null)
{
// We won't have anywhere to publish a diagnostic anyway.
return;
}

foreach (var attributeData in namedType.GetAttributes())
{
// Check if this is an Export attribute
if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, mefV1ExportAttribute) ||
SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, mefV2ExportAttribute))
{
// Check if the export attribute has a type argument
if (attributeData.ConstructorArguments is [{ Kind: TypedConstantKind.Type, Value: INamedTypeSymbol exportedType }, ..])
{
// Check if the exporting type implements or inherits from the exported type
if (!IsTypeCompatible(namedType, exportedType))
{
context.ReportDiagnostic(Diagnostic.Create(
Descriptor,
location,
namedType.Name,
exportedType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)));
}
}
}
}
}

private static void AnalyzePropertyDeclaration(SymbolAnalysisContext context, INamedTypeSymbol? mefV1ExportAttribute, INamedTypeSymbol? mefV2ExportAttribute)
{
var property = (IPropertySymbol)context.Symbol;

Location? location = property.Locations.FirstOrDefault();
if (location is null)
{
// We won't have anywhere to publish a diagnostic anyway.
return;
}

foreach (var attributeData in property.GetAttributes())
{
// Check if this is an Export attribute
if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, mefV1ExportAttribute) ||
SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, mefV2ExportAttribute))
{
// Check if the export attribute has a type argument
if (attributeData.ConstructorArguments is [{ Kind: TypedConstantKind.Type, Value: INamedTypeSymbol exportedType }, ..])
{
// Check if the property type is compatible with the exported type
if (!IsPropertyTypeCompatible(property.Type, exportedType))
{
context.ReportDiagnostic(Diagnostic.Create(
Descriptor,
location,
property.Name,
exportedType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)));
}
}
}
}
}

private static bool IsTypeCompatible(INamedTypeSymbol implementingType, INamedTypeSymbol exportedType)
{
// If they're the same type, it's compatible
if (SymbolEqualityComparer.Default.Equals(implementingType, exportedType))
{
return true;
}

// Check if implementing type inherits from exported type (for classes)
if (exportedType.TypeKind == TypeKind.Class)
{
var currentType = implementingType.BaseType;
while (currentType != null)
{
if (SymbolEqualityComparer.Default.Equals(currentType, exportedType))
{
return true;
}

currentType = currentType.BaseType;
}
}

// Check if implementing type implements exported interface
if (exportedType.TypeKind == TypeKind.Interface)
{
return implementingType.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, exportedType));
}

return false;
}

private static bool IsPropertyTypeCompatible(ITypeSymbol propertyType, INamedTypeSymbol exportedType)
{
// If they're the same type, it's compatible
if (SymbolEqualityComparer.Default.Equals(propertyType, exportedType))
{
return true;
}

// If property type is a named type, use the same logic as for classes
if (propertyType is INamedTypeSymbol namedPropertyType)
{
return IsTypeCompatible(namedPropertyType, exportedType);
}

return false;
}
}
Loading
Loading