@@ -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 ) ]
2131public 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}
0 commit comments