Skip to content

Commit d3de72e

Browse files
authored
Merge pull request #682 from microsoft/copilot/fix-vs-sdk-vsmef008-errors
Reduce VSMEF008 false positives: lower severity, soften wording, add allow-list support
1 parent d7890fd commit d3de72e

File tree

5 files changed

+196
-9
lines changed

5 files changed

+196
-9
lines changed

docfx/analyzers/VSMEF008.md

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ When using `[Import(typeof(T))]` or `[ImportMany(typeof(T))]` with an explicit c
44

55
## Cause
66

7-
An import specifies a `ContractType` that is incompatible with the member type receiving the import. This will cause a composition failure at runtime.
7+
An import specifies a `ContractType` that is incompatible with the member type receiving the import. This may cause a composition failure at runtime.
88

99
## Rule description
1010

11-
MEF allows you to specify an explicit contract type using `[Import(typeof(T))]` or `[ImportMany(typeof(T))]`. The exports matching this contract must be assignable to the member where the import is declared. If the types are incompatible, composition will fail at runtime.
11+
MEF allows you to specify an explicit contract type using `[Import(typeof(T))]` or `[ImportMany(typeof(T))]`. The exports matching this contract must be assignable to the member where the import is declared. If the types are incompatible, composition may fail at runtime.
1212

1313
This analyzer detects the following incompatibilities:
1414

@@ -152,9 +152,32 @@ public IDatabase Database { get; set; } // Change contract to match member type
152152
public ILogger Logger { get; set; } // Let MEF infer the contract type
153153
```
154154

155-
## When to suppress errors
155+
### Option 4: Add an allow-list entry (for SDK scenarios)
156156

157-
This error should generally not be suppressed as it indicates a bug that will cause runtime failures. If you're doing something unusual with MEF that the analyzer doesn't understand, ensure you have tests that verify the composition works correctly.
157+
Some SDKs use a type identity as a contract name even when that type is not assignable to the import type. For example, the Visual Studio SDK uses `SVsFullAccessServiceBroker` as a contract name for imports of type `IServiceBroker`.
158+
159+
If your project relies on such a convention, you can suppress this warning for specific pairs by adding an `AdditionalFiles` entry named `vs-mef.ContractNamesAssignability.txt` to your project:
160+
161+
```xml
162+
<ItemGroup>
163+
<AdditionalFiles Include="vs-mef.ContractNamesAssignability.txt" />
164+
</ItemGroup>
165+
```
166+
167+
The file format is one entry per line, using the pattern `MemberType <= ContractType`:
168+
169+
```
170+
# Lines starting with # are comments
171+
Microsoft.ServiceHub.Framework.IServiceBroker <= Microsoft.VisualStudio.Shell.ServiceBroker.SVsFullAccessServiceBroker
172+
Microsoft.VisualStudio.Shell.IAsyncServiceProvider <= Microsoft.VisualStudio.Shell.Interop.SAsyncServiceProvider
173+
Microsoft.VisualStudio.Shell.IAsyncServiceProvider2 <= Microsoft.VisualStudio.Shell.Interop.SAsyncServiceProvider
174+
```
175+
176+
Each line declares that `ContractType` is a known-good contract name for an import of `MemberType`, even though the types are not statically assignable.
177+
178+
## When to suppress warnings
179+
180+
This warning indicates a potential bug in most cases. However, if you're using a contract name that is a type identity not assignable to the import type (a common SDK pattern), consider using the allow-list approach described in Option 4 instead of suppressing the diagnostic globally.
158181

159182
## Notes
160183

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ VSMEF004 | Usage | Error | Exported type missing importing constructor
1010
VSMEF005 | Usage | Error | Multiple importing constructors
1111
VSMEF006 | Usage | Warning | Import nullability and AllowDefault mismatch
1212
VSMEF007 | Usage | Warning | Duplicate import contract
13-
VSMEF008 | Usage | Error | Import contract type not assignable to member type
13+
VSMEF008 | Usage | Warning | Import contract type not assignable to member type
1414
VSMEF009 | Usage | Error | ImportMany on non-collection type
1515
VSMEF010 | Usage | Error | ImportMany with unsupported collection type in constructor
1616
VSMEF011 | Usage | Error | Both Import and ImportMany applied to same member

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@
199199
<comment>{Locked="AllowDefault"} {Locked="true"} The property name and value should not be localized.</comment>
200200
</data>
201201
<data name="VSMEF008_MessageFormat" xml:space="preserve">
202-
<value>The contract type "{0}" is not assignable to the member type "{1}". The import will fail at runtime.</value>
202+
<value>The contract type "{0}" is not assignable to the member type "{1}". The import may fail at runtime.</value>
203203
</data>
204204
<data name="VSMEF008_Title" xml:space="preserve">
205205
<value>Import contract type not assignable to member type</value>

src/Microsoft.VisualStudio.Composition.Analyzers/VSMEF008ImportContractTypeMismatchAnalyzer.cs

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ namespace Microsoft.VisualStudio.Composition.Analyzers;
66
using System.Collections.Immutable;
77
using Microsoft.CodeAnalysis;
88
using Microsoft.CodeAnalysis.Diagnostics;
9+
using Microsoft.CodeAnalysis.Text;
910

1011
/// <summary>
1112
/// Creates a diagnostic when an Import attribute specifies a contract type that is not assignable to the member type.
@@ -28,6 +29,21 @@ public class VSMEF008ImportContractTypeMismatchAnalyzer : DiagnosticAnalyzer
2829
/// </summary>
2930
public const string Id = "VSMEF008";
3031

32+
/// <summary>
33+
/// The filename for AdditionalFiles that declare allowed contract type assignments.
34+
/// </summary>
35+
/// <remarks>
36+
/// Each line in this file should follow the pattern: <c>MemberType &lt;= ContractType</c>.
37+
/// Lines starting with <c>#</c> and blank lines are ignored.
38+
/// </remarks>
39+
public const string ContractNamesAssignabilityFileName = "vs-mef.ContractNamesAssignability.txt";
40+
41+
/// <summary>
42+
/// The display format used to produce fully-qualified type names for allow-list matching.
43+
/// </summary>
44+
private static readonly SymbolDisplayFormat FullyQualifiedTypeNameFormat = new(
45+
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces);
46+
3147
/// <summary>
3248
/// The descriptor used for diagnostics created by this rule.
3349
/// </summary>
@@ -37,7 +53,7 @@ public class VSMEF008ImportContractTypeMismatchAnalyzer : DiagnosticAnalyzer
3753
messageFormat: Strings.VSMEF008_MessageFormat,
3854
helpLinkUri: Utils.GetHelpLink(Id),
3955
category: "Usage",
40-
defaultSeverity: DiagnosticSeverity.Error,
56+
defaultSeverity: DiagnosticSeverity.Warning,
4157
isEnabledByDefault: true);
4258

4359
/// <inheritdoc/>
@@ -78,7 +94,8 @@ public override void Initialize(AnalysisContext context)
7894
lazyWithMetadataType,
7995
exportFactoryType,
8096
exportFactoryWithMetadataType,
81-
ienumerableType);
97+
ienumerableType,
98+
ParseAllowedAssignments(context.Options.AdditionalFiles, context.CancellationToken));
8299

83100
context.RegisterSymbolAction(ctx => AnalyzeProperty(ctx, state), SymbolKind.Property);
84101
context.RegisterSymbolAction(ctx => AnalyzeField(ctx, state), SymbolKind.Field);
@@ -156,6 +173,13 @@ private static void AnalyzeMember(
156173
// Check if the contract type is assignable to the expected type
157174
if (!IsAssignableTo(explicitContractType, expectedType, context.Compilation))
158175
{
176+
// Check the allow-list before reporting
177+
if (state.AllowedAssignments.Contains(
178+
(GetFullTypeName(expectedType), GetFullTypeName(explicitContractType))))
179+
{
180+
continue;
181+
}
182+
159183
context.ReportDiagnostic(Diagnostic.Create(
160184
Descriptor,
161185
location,
@@ -257,6 +281,58 @@ private static ITypeSymbol UnwrapLazyOrExportFactory(ITypeSymbol type, AnalyzerS
257281
return type;
258282
}
259283

284+
private static string GetFullTypeName(ITypeSymbol type) => type.ToDisplayString(FullyQualifiedTypeNameFormat);
285+
286+
private static ImmutableHashSet<(string MemberType, string ContractType)> ParseAllowedAssignments(
287+
ImmutableArray<AdditionalText> additionalFiles,
288+
System.Threading.CancellationToken cancellationToken)
289+
{
290+
ImmutableHashSet<(string, string)>.Builder? builder = null;
291+
292+
foreach (AdditionalText file in additionalFiles)
293+
{
294+
if (!string.Equals(
295+
System.IO.Path.GetFileName(file.Path),
296+
ContractNamesAssignabilityFileName,
297+
StringComparison.OrdinalIgnoreCase))
298+
{
299+
continue;
300+
}
301+
302+
SourceText? text = file.GetText(cancellationToken);
303+
if (text is null)
304+
{
305+
continue;
306+
}
307+
308+
foreach (TextLine line in text.Lines)
309+
{
310+
string lineText = line.ToString().Trim();
311+
if (lineText.Length == 0 || lineText[0] == '#')
312+
{
313+
continue;
314+
}
315+
316+
int separatorIndex = lineText.IndexOf("<=", StringComparison.Ordinal);
317+
if (separatorIndex < 0)
318+
{
319+
continue;
320+
}
321+
322+
string memberType = lineText.Substring(0, separatorIndex).Trim();
323+
string contractType = lineText.Substring(separatorIndex + 2).Trim();
324+
325+
if (memberType.Length > 0 && contractType.Length > 0)
326+
{
327+
builder ??= ImmutableHashSet.CreateBuilder<(string, string)>();
328+
builder.Add((memberType, contractType));
329+
}
330+
}
331+
}
332+
333+
return builder?.ToImmutable() ?? ImmutableHashSet<(string, string)>.Empty;
334+
}
335+
260336
private static bool IsAssignableTo(ITypeSymbol source, ITypeSymbol target, Compilation compilation)
261337
{
262338
// Direct equality check
@@ -308,5 +384,6 @@ private sealed record AnalyzerState(
308384
INamedTypeSymbol? LazyWithMetadataType,
309385
INamedTypeSymbol? ExportFactoryType,
310386
INamedTypeSymbol? ExportFactoryWithMetadataType,
311-
INamedTypeSymbol? IEnumerableType);
387+
INamedTypeSymbol? IEnumerableType,
388+
ImmutableHashSet<(string MemberType, string ContractType)> AllowedAssignments);
312389
}

test/Microsoft.VisualStudio.Composition.Analyzers.Tests/VSMEF008ImportContractTypeMismatchAnalyzerTests.cs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

4+
using Microsoft.CodeAnalysis.Text;
45
using VerifyCS = CSharpCodeFixVerifier<Microsoft.VisualStudio.Composition.Analyzers.VSMEF008ImportContractTypeMismatchAnalyzer, Microsoft.CodeAnalysis.Testing.EmptyCodeFixProvider>;
56

67
public class VSMEF008ImportContractTypeMismatchAnalyzerTests
@@ -193,4 +194,90 @@ class Foo
193194

194195
await VerifyCS.VerifyAnalyzerAsync(test);
195196
}
197+
198+
[Fact]
199+
public async Task ImportWithAllowListedAssignment_NoWarning()
200+
{
201+
string test = """
202+
using System.ComponentModel.Composition;
203+
204+
namespace MyNS
205+
{
206+
interface IServiceBroker { }
207+
class SVsFullAccessServiceBroker { }
208+
209+
class Foo
210+
{
211+
[Import(typeof(SVsFullAccessServiceBroker))]
212+
public IServiceBroker Value { get; set; }
213+
}
214+
}
215+
""";
216+
217+
string allowList = "MyNS.IServiceBroker <= MyNS.SVsFullAccessServiceBroker\n";
218+
219+
var verifier = new VerifyCS.Test { TestCode = test };
220+
verifier.TestState.AdditionalFiles.Add(
221+
("vs-mef.ContractNamesAssignability.txt", SourceText.From(allowList)));
222+
await verifier.RunAsync();
223+
}
224+
225+
[Fact]
226+
public async Task ImportWithAllowListedAssignmentWithComments_NoWarning()
227+
{
228+
string test = """
229+
using System.ComponentModel.Composition;
230+
231+
namespace MyNS
232+
{
233+
interface IServiceBroker { }
234+
class SVsFullAccessServiceBroker { }
235+
236+
class Foo
237+
{
238+
[Import(typeof(SVsFullAccessServiceBroker))]
239+
public IServiceBroker Value { get; set; }
240+
}
241+
}
242+
""";
243+
244+
string allowList = """
245+
# This is a comment
246+
MyNS.IServiceBroker <= MyNS.SVsFullAccessServiceBroker
247+
248+
""";
249+
250+
var verifier = new VerifyCS.Test { TestCode = test };
251+
verifier.TestState.AdditionalFiles.Add(
252+
("vs-mef.ContractNamesAssignability.txt", SourceText.From(allowList)));
253+
await verifier.RunAsync();
254+
}
255+
256+
[Fact]
257+
public async Task ImportWithUnrelatedAllowList_StillReportsWarning()
258+
{
259+
string test = """
260+
using System.ComponentModel.Composition;
261+
262+
namespace MyNS
263+
{
264+
interface IServiceBroker { }
265+
class SVsFullAccessServiceBroker { }
266+
267+
class Foo
268+
{
269+
[Import(typeof(SVsFullAccessServiceBroker))]
270+
public IServiceBroker {|VSMEF008:Value|} { get; set; }
271+
}
272+
}
273+
""";
274+
275+
// The allow-list entry maps a different pair so the warning should still fire
276+
string allowList = "MyNS.IOtherService <= MyNS.SVsFullAccessServiceBroker\n";
277+
278+
var verifier = new VerifyCS.Test { TestCode = test };
279+
verifier.TestState.AdditionalFiles.Add(
280+
("vs-mef.ContractNamesAssignability.txt", SourceText.From(allowList)));
281+
await verifier.RunAsync();
282+
}
196283
}

0 commit comments

Comments
 (0)