Skip to content

Commit c3dfe2d

Browse files
authored
Merge pull request #658 from drewnoakes/vsmef004-and-recomposition
VSMEF004 support for recomposition/PartNotDiscoverable
2 parents b4f2b81 + 4843c4e commit c3dfe2d

File tree

7 files changed

+334
-6
lines changed

7 files changed

+334
-6
lines changed

docfx/analyzers/VSMEF004.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,34 @@ class Foo
7070
}
7171
```
7272

73+
### Option 4: Add [PartNotDiscoverable] for manually constructed parts
74+
75+
If your part is intended to be manually constructed and inserted into the composition (e.g., for recomposition or mocking scenarios), you can mark it with `[PartNotDiscoverable]`:
76+
77+
```cs
78+
[Export]
79+
[PartNotDiscoverable]
80+
class Foo
81+
{
82+
public Foo(string parameter)
83+
{
84+
// This part will be manually constructed and composed
85+
}
86+
}
87+
```
88+
89+
This tells MEF not to discover and instantiate this part automatically. You must then manually create instances and add them to the composition container.
90+
91+
## When to suppress warnings
92+
93+
You can suppress this diagnostic if:
94+
95+
1. **Manual construction scenarios**: Your part is intentionally constructed outside of MEF and inserted into the composition using methods like `CompositionContainer.ComposeParts()` or `CompositionContainer.SatisfyImportsOnce()`. Consider using `[PartNotDiscoverable]` instead of suppressing, as it makes the intent explicit.
96+
97+
2. **Mocking/testing scenarios**: The type is used for testing purposes where you manually construct mock instances. Again, `[PartNotDiscoverable]` may be a better choice.
98+
99+
3. **Very advanced recomposition scenarios**: You're using MEF v1's recomposition feature where parts can be dynamically added to or removed from the composition at runtime.
100+
73101
## Examples that trigger this rule
74102

75103
### Class-level export with non-default constructor

src/Microsoft.VisualStudio.Composition.Analyzers.CodeFixes/VSMEF004ExportWithoutImportingConstructorCodeFixProvider.cs

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,19 +74,27 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
7474

7575
// Primary fix: Add [ImportingConstructor] attribute
7676
var addAttributeAction = CodeAction.Create(
77-
title: "Add [ImportingConstructor] attribute",
77+
title: Strings.VSMEF004_CodeFix_AddImportingConstructorAttribute,
7878
createChangedDocument: c => AddImportingConstructorAttributeAsync(context.Document, constructorDeclaration, mefVersion, c),
7979
equivalenceKey: "AddImportingConstructorAttribute");
8080

8181
context.RegisterCodeFix(addAttributeAction, diagnostic);
8282

8383
// Secondary fix: Add parameterless constructor
8484
var addConstructorAction = CodeAction.Create(
85-
title: "Add parameterless constructor",
85+
title: Strings.VSMEF004_CodeFix_AddParameterlessConstructor,
8686
createChangedDocument: c => AddParameterlessConstructorAsync(context.Document, classDeclaration, c),
8787
equivalenceKey: "AddParameterlessConstructor");
8888

8989
context.RegisterCodeFix(addConstructorAction, diagnostic);
90+
91+
// Tertiary fix: Add [PartNotDiscoverable] attribute (for manually constructed parts)
92+
var addPartNotDiscoverableAction = CodeAction.Create(
93+
title: Strings.VSMEF004_CodeFix_AddPartNotDiscoverableAttribute,
94+
createChangedDocument: c => AddPartNotDiscoverableAttributeAsync(context.Document, classDeclaration, mefVersion, c),
95+
equivalenceKey: "AddPartNotDiscoverableAttribute");
96+
97+
context.RegisterCodeFix(addPartNotDiscoverableAction, diagnostic);
9098
}
9199

92100
private static async Task<Document> AddImportingConstructorAttributeAsync(
@@ -173,6 +181,43 @@ private static async Task<Document> AddParameterlessConstructorAsync(
173181
return newDocument;
174182
}
175183

184+
private static async Task<Document> AddPartNotDiscoverableAttributeAsync(
185+
Document document,
186+
ClassDeclarationSyntax classDeclaration,
187+
MefVersion mefVersion,
188+
CancellationToken cancellationToken)
189+
{
190+
SyntaxNode? root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
191+
if (root is null)
192+
{
193+
return document;
194+
}
195+
196+
// Create the PartNotDiscoverable attribute with proper annotations for simplification
197+
string attributeName = mefVersion == MefVersion.V1
198+
? "System.ComponentModel.Composition.PartNotDiscoverable"
199+
: "System.Composition.PartNotDiscoverable";
200+
201+
AttributeSyntax attribute = SyntaxFactory.Attribute(
202+
SyntaxFactory.ParseName(attributeName)
203+
.WithAdditionalAnnotations(Simplifier.AddImportsAnnotation, Simplifier.Annotation));
204+
AttributeListSyntax attributeList = SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(attribute));
205+
206+
// Add the attribute to the class
207+
ClassDeclarationSyntax newClass = classDeclaration.WithAttributeLists(
208+
classDeclaration.AttributeLists.Add(attributeList));
209+
210+
SyntaxNode newRoot = root.ReplaceNode(classDeclaration, newClass);
211+
212+
// Apply simplification and formatting
213+
document = document.WithSyntaxRoot(newRoot);
214+
document = await ImportAdder.AddImportsAsync(document, Simplifier.AddImportsAnnotation, cancellationToken: cancellationToken).ConfigureAwait(false);
215+
document = await Simplifier.ReduceAsync(document, cancellationToken: cancellationToken).ConfigureAwait(false);
216+
document = await Formatter.FormatAsync(document, Formatter.Annotation, cancellationToken: cancellationToken).ConfigureAwait(false);
217+
218+
return document;
219+
}
220+
176221
private static MefVersion DetermineMefVersion(ClassDeclarationSyntax classDeclaration, SemanticModel semanticModel)
177222
{
178223
// Check the class and its members for MEF attributes to determine which version to use

src/Microsoft.VisualStudio.Composition.Analyzers.CodeFixes/VSMEF006ImportNullabilityCodeFixProvider.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,15 +151,15 @@ private static void RegisterFixes(CodeFixContext context, SyntaxNode root, Diagn
151151
// Fix 1: Add AllowDefault = true
152152
context.RegisterCodeFix(
153153
CodeAction.Create(
154-
title: "Add AllowDefault = true",
154+
title: Strings.VSMEF006_CodeFix_AddAllowDefault,
155155
createChangedDocument: cancellationToken => AddAllowDefaultAsync(context.Document, root, targetNode, attributeLists, cancellationToken),
156156
equivalenceKey: "AddAllowDefault"),
157157
diagnostic);
158158

159159
// Fix 2: Make type non-nullable
160160
context.RegisterCodeFix(
161161
CodeAction.Create(
162-
title: "Make type non-nullable",
162+
title: Strings.VSMEF006_CodeFix_MakeTypeNonNullable,
163163
createChangedDocument: cancellationToken => MakeTypeNonNullableAsync(context.Document, root, targetNode, typeSyntax, cancellationToken),
164164
equivalenceKey: "MakeNonNullable"),
165165
diagnostic);
@@ -170,15 +170,15 @@ private static void RegisterFixes(CodeFixContext context, SyntaxNode root, Diagn
170170
// Fix 1: Make type nullable
171171
context.RegisterCodeFix(
172172
CodeAction.Create(
173-
title: "Make type nullable",
173+
title: Strings.VSMEF006_CodeFix_MakeTypeNullable,
174174
createChangedDocument: cancellationToken => MakeTypeNullableAsync(context.Document, root, targetNode, typeSyntax, cancellationToken),
175175
equivalenceKey: "MakeNullable"),
176176
diagnostic);
177177

178178
// Fix 2: Remove AllowDefault = true
179179
context.RegisterCodeFix(
180180
CodeAction.Create(
181-
title: "Remove AllowDefault = true",
181+
title: Strings.VSMEF006_CodeFix_RemoveAllowDefault,
182182
createChangedDocument: cancellationToken => RemoveAllowDefaultAsync(context.Document, root, targetNode, attributeLists, cancellationToken),
183183
equivalenceKey: "RemoveAllowDefault"),
184184
diagnostic);

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,15 +122,18 @@
122122
</resheader>
123123
<data name="VSMEF001_MessageFormat" xml:space="preserve">
124124
<value>A property with an [ImportAttribute] must define a setter so the export can be set on the property.</value>
125+
<comment>{Locked="[ImportAttribute]"} The attribute name in brackets should not be localized.</comment>
125126
</data>
126127
<data name="VSMEF001_Title" xml:space="preserve">
127128
<value>Importing property must have setter</value>
128129
</data>
129130
<data name="VSMEF002_MessageFormat" xml:space="preserve">
130131
<value>The type "{0}" contains a mix of attributes from MEFv1 and MEFv2. Consolidate to just one variety of attributes.</value>
132+
<comment>{Locked="MEFv1"} {Locked="MEFv2"} The framework version names should not be localized.</comment>
131133
</data>
132134
<data name="VSMEF002_Title" xml:space="preserve">
133135
<value>Avoid mixing MEF attribute varieties</value>
136+
<comment>{Locked="MEF"} The framework name should not be localized.</comment>
134137
</data>
135138
<data name="VSMEF003_MessageFormat" xml:space="preserve">
136139
<value>The type "{0}" does not implement the exported type "{1}". This may be an authoring mistake.</value>
@@ -140,29 +143,59 @@
140143
</data>
141144
<data name="VSMEF004_MessageFormat" xml:space="preserve">
142145
<value>The type "{0}" defines exports but has non-default constructors that are not annotated with [ImportingConstructor]. Add a parameterless constructor, or annotate a constructor with [ImportingConstructor].</value>
146+
<comment>{Locked="[ImportingConstructor]"} The attribute name in brackets should not be localized.</comment>
143147
</data>
144148
<data name="VSMEF004_Title" xml:space="preserve">
145149
<value>Exported type missing importing constructor</value>
146150
</data>
147151
<data name="VSMEF005_MessageFormat" xml:space="preserve">
148152
<value>The type "{0}" has multiple constructors annotated with [ImportingConstructor]. Only one constructor should be marked as importing.</value>
153+
<comment>{Locked="[ImportingConstructor]"} The attribute name in brackets should not be localized.</comment>
149154
</data>
150155
<data name="VSMEF005_Title" xml:space="preserve">
151156
<value>Multiple importing constructors</value>
152157
</data>
153158
<data name="VSMEF006_AllowDefaultWithoutNullable_MessageFormat" xml:space="preserve">
154159
<value>The import "{0}" has AllowDefault = true but is not nullable. Consider making the type nullable or removing AllowDefault.</value>
160+
<comment>{Locked="AllowDefault"} {Locked="true"} The property name and value should not be localized.</comment>
155161
</data>
156162
<data name="VSMEF006_NullableWithoutAllowDefault_MessageFormat" xml:space="preserve">
157163
<value>The import "{0}" is nullable but does not have AllowDefault = true. Consider adding AllowDefault = true or making the type non-nullable (and either add a 'required' modifier or an '= null!' initializer).</value>
164+
<comment>{Locked="AllowDefault"} {Locked="true"} {Locked="required"} {Locked="= null!"} The property name, value, and C# keywords/syntax should not be localized.</comment>
158165
</data>
159166
<data name="VSMEF006_Title" xml:space="preserve">
160167
<value>Import nullability and AllowDefault mismatch</value>
168+
<comment>{Locked="AllowDefault"} The property name should not be localized.</comment>
161169
</data>
162170
<data name="VSMEF007_MessageFormat" xml:space="preserve">
163171
<value>The type "{0}" imports the same contract "{1}" multiple times. Each contract should only be imported once per type.</value>
164172
</data>
165173
<data name="VSMEF007_Title" xml:space="preserve">
166174
<value>Duplicate import contract</value>
167175
</data>
176+
<data name="VSMEF004_CodeFix_AddImportingConstructorAttribute" xml:space="preserve">
177+
<value>Add [ImportingConstructor] attribute</value>
178+
<comment>{Locked="[ImportingConstructor]"} The attribute name in brackets should not be localized.</comment>
179+
</data>
180+
<data name="VSMEF004_CodeFix_AddParameterlessConstructor" xml:space="preserve">
181+
<value>Add parameterless constructor</value>
182+
</data>
183+
<data name="VSMEF004_CodeFix_AddPartNotDiscoverableAttribute" xml:space="preserve">
184+
<value>Add [PartNotDiscoverable] attribute</value>
185+
<comment>{Locked="[PartNotDiscoverable]"} The attribute name in brackets should not be localized.</comment>
186+
</data>
187+
<data name="VSMEF006_CodeFix_AddAllowDefault" xml:space="preserve">
188+
<value>Add AllowDefault = true</value>
189+
<comment>{Locked="AllowDefault"} {Locked="true"} The property name and value should not be localized.</comment>
190+
</data>
191+
<data name="VSMEF006_CodeFix_MakeTypeNonNullable" xml:space="preserve">
192+
<value>Make type non-nullable</value>
193+
</data>
194+
<data name="VSMEF006_CodeFix_MakeTypeNullable" xml:space="preserve">
195+
<value>Make type nullable</value>
196+
</data>
197+
<data name="VSMEF006_CodeFix_RemoveAllowDefault" xml:space="preserve">
198+
<value>Remove AllowDefault = true</value>
199+
<comment>{Locked="AllowDefault"} {Locked="true"} The property name and value should not be localized.</comment>
200+
</data>
168201
</root>

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,26 @@ internal static bool IsImportingConstructorAttribute(INamedTypeSymbol? attribute
9292
IsAttributeOfType(attributeType, "ImportingConstructorAttribute", MefV2AttributeNamespace.AsSpan());
9393
}
9494

95+
/// <summary>
96+
/// Determines whether the specified attribute type is a MEF PartNotDiscoverable attribute.
97+
/// </summary>
98+
/// <param name="attributeType">The attribute type to check.</param>
99+
/// <returns><see langword="true"/> if the attribute type is a PartNotDiscoverable attribute; otherwise, <see langword="false"/>.</returns>
100+
/// <remarks>
101+
/// This method checks for both MEF v1 and MEF v2 PartNotDiscoverable attributes.
102+
/// </remarks>
103+
internal static bool IsPartNotDiscoverableAttribute(INamedTypeSymbol? attributeType)
104+
{
105+
if (attributeType is null)
106+
{
107+
return false;
108+
}
109+
110+
// Check if it's a PartNotDiscoverable attribute (not typically subclassed, but check inheritance for consistency)
111+
return IsAttributeOfType(attributeType, "PartNotDiscoverableAttribute", MefV1AttributeNamespace.AsSpan()) ||
112+
IsAttributeOfType(attributeType, "PartNotDiscoverableAttribute", MefV2AttributeNamespace.AsSpan());
113+
}
114+
95115
/// <summary>
96116
/// Determines whether the specified attribute type matches the expected type and namespace, including inheritance hierarchy.
97117
/// </summary>
@@ -215,6 +235,24 @@ internal static bool HasImportingConstructorAttribute(IMethodSymbol constructor)
215235
return false;
216236
}
217237

238+
/// <summary>
239+
/// Determines whether the specified type has the PartNotDiscoverable attribute.
240+
/// </summary>
241+
/// <param name="symbol">The type symbol to check.</param>
242+
/// <returns><see langword="true"/> if the type has the PartNotDiscoverable attribute; otherwise, <see langword="false"/>.</returns>
243+
internal static bool HasPartNotDiscoverableAttribute(INamedTypeSymbol symbol)
244+
{
245+
foreach (AttributeData attribute in symbol.GetAttributes())
246+
{
247+
if (IsPartNotDiscoverableAttribute(attribute.AttributeClass))
248+
{
249+
return true;
250+
}
251+
}
252+
253+
return false;
254+
}
255+
218256
/// <summary>
219257
/// Determines whether the specified namespace matches the expected namespace components.
220258
/// </summary>

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ static void AnalyzeSymbol(SymbolAnalysisContext context)
6666
return;
6767
}
6868

69+
// Skip types marked with [PartNotDiscoverable] as they are intended for manual construction
70+
if (Utils.HasPartNotDiscoverableAttribute(symbol))
71+
{
72+
return;
73+
}
74+
6975
// Get all constructors
7076
var constructors = symbol.Constructors.Where(c => !c.IsStatic && !c.IsImplicitlyDeclared).ToList();
7177

0 commit comments

Comments
 (0)