Skip to content

Commit 06b1548

Browse files
authored
Merge pull request #601 from microsoft/copilot/fix-79
Add VSMEF003 analyzer to detect export type mismatches
2 parents 3b3cbd0 + 502ffde commit 06b1548

File tree

8 files changed

+629
-0
lines changed

8 files changed

+629
-0
lines changed

doc/analyzers/VSMEF003.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# VSMEF003: Exported type not implemented by exporting class
2+
3+
A class that declares `[Export(typeof(T))]` should implement interface `T` or inherit from class `T`.
4+
5+
## Cause
6+
7+
A class is decorated with `[Export(typeof(T))]` but does not implement the interface or inherit from the class specified by `T`.
8+
9+
## Rule description
10+
11+
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.
12+
13+
## How to fix violations
14+
15+
Either:
16+
17+
- Make the exporting class implement the specified interface, or
18+
- Make the exporting class inherit from the specified base class, or
19+
- Change the Export attribute to export the correct type, or
20+
- Remove the type parameter to export the class's own type
21+
22+
## When to suppress warnings
23+
24+
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.
25+
26+
## Example
27+
28+
### Violates
29+
30+
```csharp
31+
interface ICalculator
32+
{
33+
int Add(int a, int b);
34+
}
35+
36+
[Export(typeof(ICalculator))] // ❌ Violates VSMEF003
37+
public class TextProcessor
38+
{
39+
public string ProcessText(string input) => input.ToUpper();
40+
}
41+
```
42+
43+
### Does not violate
44+
45+
```csharp
46+
interface ICalculator
47+
{
48+
int Add(int a, int b);
49+
}
50+
51+
[Export(typeof(ICalculator))] // ✅ OK
52+
public class Calculator : ICalculator
53+
{
54+
public int Add(int a, int b) => a + b;
55+
}
56+
57+
[Export] // ✅ OK - exports Calculator type
58+
public class Calculator
59+
{
60+
public int Add(int a, int b) => a + b;
61+
}
62+
```

doc/analyzers/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ to help you avoid common mistakes while authoring MEF parts.
77
ID | Title
88
--|--
99
VSMEF001 | Importing property must have setter
10+
VSMEF002 | Avoid mixing MEF attribute libraries
11+
VSMEF003 | Exported type not implemented by exporting class

src/Microsoft.VisualStudio.Composition.Analyzers/AnalyzerReleases.Unshipped.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
### New Rules
55
Rule ID | Category | Severity | Notes
66
--------|----------|----------|-------
7+
VSMEF003 | Usage | Warning | Exported type not implemented by exporting class

src/Microsoft.VisualStudio.Composition.Analyzers/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ Analyzers for MEF consumers to help identify common errors in MEF parts.
55
Analyzer ID | Description
66
--|--
77
VSMEF001 | Ensures that importing properties define a `set` accessor.
8+
VSMEF003 | Ensures that exported types are implemented by the exporting class.

src/Microsoft.VisualStudio.Composition.Analyzers/Strings.Designer.cs

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

src/Microsoft.VisualStudio.Composition.Analyzers/Strings.resx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,10 @@
129129
<data name="VSMEF002_Title" xml:space="preserve">
130130
<value>Avoid mixing MEF attribute varieties</value>
131131
</data>
132+
<data name="VSMEF003_MessageFormat" xml:space="preserve">
133+
<value>The type "{0}" does not implement the exported type "{1}". This may be an authoring mistake.</value>
134+
</data>
135+
<data name="VSMEF003_Title" xml:space="preserve">
136+
<value>Exported type not implemented by exporting class</value>
137+
</data>
132138
</root>
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
namespace Microsoft.VisualStudio.Composition.Analyzers;
5+
6+
/// <summary>
7+
/// Creates a diagnostic when `[Export(typeof(T))]` is applied to a class that does not implement T,
8+
/// or to a property whose type is not compatible with T.
9+
/// </summary>
10+
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
11+
public class VSMEF003ExportTypeMismatchAnalyzer : DiagnosticAnalyzer
12+
{
13+
/// <summary>
14+
/// The ID for diagnostics reported by this analyzer.
15+
/// </summary>
16+
public const string Id = "VSMEF003";
17+
18+
/// <summary>
19+
/// The descriptor used for diagnostics created by this rule.
20+
/// </summary>
21+
internal static readonly DiagnosticDescriptor Descriptor = new(
22+
id: Id,
23+
title: Strings.VSMEF003_Title,
24+
messageFormat: Strings.VSMEF003_MessageFormat,
25+
helpLinkUri: Utils.GetHelpLink(Id),
26+
category: "Usage",
27+
defaultSeverity: DiagnosticSeverity.Warning,
28+
isEnabledByDefault: true);
29+
30+
/// <inheritdoc/>
31+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Descriptor);
32+
33+
/// <inheritdoc/>
34+
public override void Initialize(AnalysisContext context)
35+
{
36+
context.EnableConcurrentExecution();
37+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
38+
39+
context.RegisterCompilationStartAction(context =>
40+
{
41+
// Only scan further if the compilation references the assemblies that define the attributes we'll be looking for.
42+
if (context.Compilation.ReferencedAssemblyNames.Any(i =>
43+
string.Equals(i.Name, "System.ComponentModel.Composition", StringComparison.OrdinalIgnoreCase) ||
44+
string.Equals(i.Name, "System.Composition.AttributedModel", StringComparison.OrdinalIgnoreCase)))
45+
{
46+
INamedTypeSymbol? mefV1ExportAttribute = context.Compilation.GetTypeByMetadataName("System.ComponentModel.Composition.ExportAttribute");
47+
INamedTypeSymbol? mefV2ExportAttribute = context.Compilation.GetTypeByMetadataName("System.Composition.ExportAttribute");
48+
context.RegisterSymbolAction(
49+
context => AnalyzeTypeDeclaration(context, mefV1ExportAttribute, mefV2ExportAttribute),
50+
SymbolKind.NamedType);
51+
context.RegisterSymbolAction(
52+
context => AnalyzePropertyDeclaration(context, mefV1ExportAttribute, mefV2ExportAttribute),
53+
SymbolKind.Property);
54+
}
55+
});
56+
}
57+
58+
private static void AnalyzeTypeDeclaration(SymbolAnalysisContext context, INamedTypeSymbol? mefV1ExportAttribute, INamedTypeSymbol? mefV2ExportAttribute)
59+
{
60+
var namedType = (INamedTypeSymbol)context.Symbol;
61+
62+
// Skip interfaces, enums, delegates - only analyze classes
63+
if (namedType.TypeKind != TypeKind.Class)
64+
{
65+
return;
66+
}
67+
68+
Location? location = namedType.Locations.FirstOrDefault();
69+
if (location is null)
70+
{
71+
// We won't have anywhere to publish a diagnostic anyway.
72+
return;
73+
}
74+
75+
foreach (var attributeData in namedType.GetAttributes())
76+
{
77+
// Check if this is an Export attribute
78+
if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, mefV1ExportAttribute) ||
79+
SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, mefV2ExportAttribute))
80+
{
81+
// Check if the export attribute has a type argument
82+
if (attributeData.ConstructorArguments is [{ Kind: TypedConstantKind.Type, Value: INamedTypeSymbol exportedType }, ..])
83+
{
84+
// Check if the exporting type implements or inherits from the exported type
85+
if (!IsTypeCompatible(namedType, exportedType))
86+
{
87+
context.ReportDiagnostic(Diagnostic.Create(
88+
Descriptor,
89+
location,
90+
namedType.Name,
91+
exportedType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)));
92+
}
93+
}
94+
}
95+
}
96+
}
97+
98+
private static void AnalyzePropertyDeclaration(SymbolAnalysisContext context, INamedTypeSymbol? mefV1ExportAttribute, INamedTypeSymbol? mefV2ExportAttribute)
99+
{
100+
var property = (IPropertySymbol)context.Symbol;
101+
102+
Location? location = property.Locations.FirstOrDefault();
103+
if (location is null)
104+
{
105+
// We won't have anywhere to publish a diagnostic anyway.
106+
return;
107+
}
108+
109+
foreach (var attributeData in property.GetAttributes())
110+
{
111+
// Check if this is an Export attribute
112+
if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, mefV1ExportAttribute) ||
113+
SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, mefV2ExportAttribute))
114+
{
115+
// Check if the export attribute has a type argument
116+
if (attributeData.ConstructorArguments is [{ Kind: TypedConstantKind.Type, Value: INamedTypeSymbol exportedType }, ..])
117+
{
118+
// Check if the property type is compatible with the exported type
119+
if (!IsPropertyTypeCompatible(property.Type, exportedType))
120+
{
121+
context.ReportDiagnostic(Diagnostic.Create(
122+
Descriptor,
123+
location,
124+
property.Name,
125+
exportedType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)));
126+
}
127+
}
128+
}
129+
}
130+
}
131+
132+
private static bool IsTypeCompatible(INamedTypeSymbol implementingType, INamedTypeSymbol exportedType)
133+
{
134+
// If they're the same type, it's compatible
135+
if (SymbolEqualityComparer.Default.Equals(implementingType, exportedType))
136+
{
137+
return true;
138+
}
139+
140+
// Check if implementing type inherits from exported type (for classes)
141+
if (exportedType.TypeKind == TypeKind.Class)
142+
{
143+
var currentType = implementingType.BaseType;
144+
while (currentType != null)
145+
{
146+
if (SymbolEqualityComparer.Default.Equals(currentType, exportedType))
147+
{
148+
return true;
149+
}
150+
151+
currentType = currentType.BaseType;
152+
}
153+
}
154+
155+
// Check if implementing type implements exported interface
156+
if (exportedType.TypeKind == TypeKind.Interface)
157+
{
158+
return implementingType.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, exportedType));
159+
}
160+
161+
return false;
162+
}
163+
164+
private static bool IsPropertyTypeCompatible(ITypeSymbol propertyType, INamedTypeSymbol exportedType)
165+
{
166+
// If they're the same type, it's compatible
167+
if (SymbolEqualityComparer.Default.Equals(propertyType, exportedType))
168+
{
169+
return true;
170+
}
171+
172+
// If property type is a named type, use the same logic as for classes
173+
if (propertyType is INamedTypeSymbol namedPropertyType)
174+
{
175+
return IsTypeCompatible(namedPropertyType, exportedType);
176+
}
177+
178+
return false;
179+
}
180+
}

0 commit comments

Comments
 (0)