Skip to content

Commit 4c83c76

Browse files
authored
Merge pull request #648 from drewnoakes/fix-VSMEF007
Fix VSMEF007
2 parents 9bf4fe6 + eb755b8 commit 4c83c76

File tree

4 files changed

+619
-41
lines changed

4 files changed

+619
-41
lines changed

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,22 @@ internal static bool GetAllowDefaultValue(AttributeData importAttribute)
270270
return false; // Default value is false
271271
}
272272

273+
/// <summary>
274+
/// Determines whether an Import attribute requires a NonShared instance.
275+
/// </summary>
276+
/// <param name="importAttribute">The Import attribute to check.</param>
277+
/// <returns>True if RequiredCreationPolicy = NonShared; otherwise false.</returns>
278+
internal static bool IsNonSharedImport(AttributeData importAttribute)
279+
{
280+
TypedConstant creationPolicyArg = importAttribute.NamedArguments.FirstOrDefault(arg => arg.Key == "RequiredCreationPolicy").Value;
281+
282+
// CreationPolicy values:
283+
// Any = 0
284+
// Shared = 1
285+
// NonShared = 2
286+
return creationPolicyArg.Value is 2;
287+
}
288+
273289
/// <summary>
274290
/// Determines which MEF version an attribute type belongs to by walking up its inheritance hierarchy.
275291
/// </summary>
@@ -311,6 +327,12 @@ internal static bool GetAllowDefaultValue(AttributeData importAttribute)
311327

312328
return null;
313329
}
330+
331+
internal static void Deconstruct<TKey, TValue>(this IGrouping<TKey, TValue> grouping, out TKey key, out IEnumerable<TValue> values)
332+
{
333+
key = grouping.Key;
334+
values = grouping;
335+
}
314336
}
315337

316338
/// <summary>

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

Lines changed: 74 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,19 @@ namespace Microsoft.VisualStudio.Composition.Analyzers;
1313
/// Creates a diagnostic when a type imports the same contract more than once.
1414
/// </summary>
1515
/// <remarks>
16+
/// <para>
1617
/// This analyzer detects when a type has multiple imports (properties, constructor parameters, or a mix)
1718
/// that import the same contract. For imports with specific contract names, it only flags duplicates
1819
/// when the contract names match exactly.
20+
/// </para>
21+
/// <para>
22+
/// Note: This analyzer can produce false positives. If a MEF part is exported as non-shared, then it
23+
/// may make sense to have multiple seemingly-identical imports and use them independently. This analyzer
24+
/// considers the import-site RequiredCreationPolicy attribute, but cannot detect cases where the exported
25+
/// type itself is marked as NonShared, since that information is not statically available at the import site.
26+
/// In such cases, runtime composition may succeed even though this analyzer reports a warning, and the developer
27+
/// should suppress the instance of the diagnostic. This should be very rare however.
28+
/// </para>
1929
/// </remarks>
2030
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
2131
public class VSMEF007DuplicateImportAnalyzer : DiagnosticAnalyzer
@@ -66,20 +76,27 @@ private static void AnalyzeType(SymbolAnalysisContext context)
6676
return;
6777
}
6878

69-
var importContracts = new List<ImportContract>();
79+
var imports = new List<Import>();
7080

7181
// Collect all imports from properties
82+
// Note: We skip NonShared imports because each gets a unique instance, so they're not problematic duplicates
7283
foreach (ISymbol member in namedType.GetMembers())
7384
{
7485
if (member is IPropertySymbol property)
7586
{
7687
AttributeData? importAttribute = Utils.GetImportAttribute(property.GetAttributes());
7788

78-
string? contract = GetImportContract(importAttribute, property.Type);
79-
80-
if (contract is not null)
89+
if (importAttribute is not null)
8190
{
82-
importContracts.Add(new ImportContract(contract, property.Name, property.Locations[0]));
91+
if (Utils.IsNonSharedImport(importAttribute))
92+
{
93+
// Skip NonShared imports, each gets a unique instance.
94+
continue;
95+
}
96+
97+
Contract contract = GetImportContract(importAttribute, property.Type);
98+
99+
imports.Add(new Import(contract, property.Name, property.Locations[0]));
83100
}
84101
}
85102
}
@@ -97,71 +114,91 @@ private static void AnalyzeType(SymbolAnalysisContext context)
97114
{
98115
AttributeData? importAttribute = Utils.GetImportAttribute(parameter.GetAttributes());
99116

100-
string? contract = GetImportContract(importAttribute, parameter.Type);
101-
102-
if (contract is not null)
117+
if (importAttribute is not null && Utils.IsNonSharedImport(importAttribute))
103118
{
104-
importContracts.Add(new ImportContract(contract, parameter.Name, parameter.Locations[0]));
119+
// Skip NonShared imports, each gets a unique instance.
120+
continue;
105121
}
122+
123+
Contract contract = GetImportContract(importAttribute, parameter.Type);
124+
125+
imports.Add(new Import(contract, parameter.Name, parameter.Locations[0]));
106126
}
107127
}
108128
}
109129
}
110130

111131
// Find duplicates
112-
IEnumerable<IGrouping<string, ImportContract>> contractGroups = importContracts.GroupBy(ic => ic.Contract).Where(g => g.Count() > 1);
132+
IEnumerable<IGrouping<Contract, Import>> duplicateImportsByContract = imports.GroupBy(ic => ic.Contract).Where(g => g.Count() > 1);
113133

114-
foreach (IGrouping<string, ImportContract>? group in contractGroups)
134+
foreach ((Contract contract, IEnumerable<Import> duplicateImports) in duplicateImportsByContract)
115135
{
116-
foreach (ImportContract? import in group)
136+
foreach (Import import in duplicateImports)
117137
{
118138
context.ReportDiagnostic(Diagnostic.Create(
119139
Descriptor,
120140
import.Location,
121141
namedType.Name,
122-
import.Contract));
142+
contract));
123143
}
124144
}
125145
}
126146

127-
private static string? GetImportContract(AttributeData? importAttribute, ITypeSymbol importType)
147+
private static Contract GetImportContract(AttributeData? importAttribute, ITypeSymbol importType)
128148
{
129-
// Note that the actual contract name used by MEF is more complex than this. See ContractNameServices
130-
// for the full logic. This approximation suffices for catching duplicates, for the purposes of this analyzer.
149+
string? explicitContractName = null;
150+
ITypeSymbol? explicitContractType = null;
151+
131152
if (importAttribute is not null)
132153
{
133-
// Check for explicit contract name or type in constructor arguments
134-
if (importAttribute.ConstructorArguments is [TypedConstant firstArg, ..])
154+
// Check for explicit contract name or type in constructor arguments.
155+
// Uses patterns from both System.Composition.ImportAttribute and System.ComponentModel.Composition.ImportAttribute.
156+
// An empty or null contract name means "no explicit name".
157+
(explicitContractName, explicitContractType) = importAttribute.ConstructorArguments switch
135158
{
136-
if (firstArg.Value is string { Length: not 0 } contractName)
137-
{
138-
return contractName;
139-
}
140-
141-
if (firstArg.Value is INamedTypeSymbol contractType)
142-
{
143-
return contractType.ToDisplayString();
144-
}
145-
}
159+
[TypedConstant { Value: string { Length: not 0 } contractName }] => (contractName, null),
160+
[TypedConstant { Value: INamedTypeSymbol contractType }] => (null, contractType),
161+
[TypedConstant { Value: string { Length: not 0 } contractName }, TypedConstant { Value: INamedTypeSymbol contractType }] => (contractName, contractType),
162+
[TypedConstant { Value: null or string { Length: 0 } }, TypedConstant { Value: INamedTypeSymbol contractType }] => (null, contractType),
163+
_ => (null, null),
164+
};
146165

147166
// Check for contract name in named arguments
148-
KeyValuePair<string, TypedConstant> contractNameArg = importAttribute.NamedArguments.FirstOrDefault(arg => arg.Key == "ContractName");
149-
if (contractNameArg.Key is not null && contractNameArg.Value.Value is string namedContractName && !string.IsNullOrEmpty(namedContractName))
167+
TypedConstant? nameArg = importAttribute.NamedArguments.FirstOrDefault(arg => arg.Key == "ContractName").Value;
168+
if (nameArg?.Value is string { Length: not 0 } namedContractName)
150169
{
151-
return namedContractName;
170+
explicitContractName = namedContractName;
152171
}
153172

154173
// Check for contract type in named arguments
155-
KeyValuePair<string, TypedConstant> contractTypeArg = importAttribute.NamedArguments.FirstOrDefault(arg => arg.Key == "ContractType");
156-
if (contractTypeArg.Key is not null && contractTypeArg.Value.Value is INamedTypeSymbol namedContractType)
174+
TypedConstant? typeArg = importAttribute.NamedArguments.FirstOrDefault(arg => arg.Key == "ContractType").Value;
175+
if (typeArg?.Value is INamedTypeSymbol namedContractType)
157176
{
158-
return namedContractType.ToDisplayString();
177+
explicitContractType = namedContractType;
159178
}
160179
}
161180

162-
// Default contract is the type name
163-
return importType.ToDisplayString();
181+
// Determine the base type for defaulting contract name and type.
182+
// Note that the actual contract name used by MEF is more complex than this. See ContractNameServices
183+
// for the full logic. This approximation suffices for catching duplicates, for the purposes of this analyzer.
184+
185+
// If contract type is explicitly specified, use it; otherwise use the import parameter type.
186+
ITypeSymbol type = explicitContractType ?? importType;
187+
string typeName = type.ToDisplayString();
188+
189+
// Contract name: use explicit name if provided, otherwise default to type name.
190+
string name = explicitContractName ?? typeName;
191+
192+
return new Contract(typeName, name);
193+
}
194+
195+
// A MEF contract consists of both a contract name and a contract type.
196+
// Both must match for two imports to be considered duplicates.
197+
// See: https://learn.microsoft.com/en-us/dotnet/framework/mef/attributed-programming-model-overview-mef#import-and-export-basics
198+
private readonly record struct Contract(string Type, string? Name)
199+
{
200+
public override string ToString() => this.Name is null || string.Equals(this.Name, this.Type) ? this.Type : $"{this.Type} (\"{this.Name}\")";
164201
}
165202

166-
private record ImportContract(string Contract, string MemberName, Location Location);
203+
private record Import(Contract Contract, string MemberName, Location Location);
167204
}

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
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 System;
5-
using System.Threading.Tasks;
6-
using Xunit;
74
using VerifyCS = CSharpCodeFixVerifier<Microsoft.VisualStudio.Composition.Analyzers.VSMEF001PropertyMustHaveSetter, Microsoft.CodeAnalysis.Testing.EmptyCodeFixProvider>;
85
using VerifyVB = VisualBasicCodeFixVerifier<Microsoft.VisualStudio.Composition.Analyzers.VSMEF001PropertyMustHaveSetter, Microsoft.CodeAnalysis.Testing.EmptyCodeFixProvider>;
96

0 commit comments

Comments
 (0)