Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,18 @@

<MicroBuildVersion>2.0.226</MicroBuildVersion>
<CodeAnalysisVersion>5.3.0</CodeAnalysisVersion>
<CodefixTestingVersion>1.1.2</CodefixTestingVersion>
<CodefixTestingVersion>1.1.3</CodefixTestingVersion>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="DiffPlex" Version="1.9.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" version="$(CodeAnalysisVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.XUnit" Version="$(CodefixTestingVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" version="$(CodeAnalysisVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing" Version="$(CodefixTestingVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="$(CodeAnalysisVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="$(CodeAnalysisVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.VisualBasic" version="$(CodeAnalysisVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.VisualBasic.CodeFix.Testing.XUnit" Version="$(CodefixTestingVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.VisualBasic.Workspaces" version="$(CodeAnalysisVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.VisualBasic.CodeFix.Testing" Version="$(CodefixTestingVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.VisualBasic.Workspaces" Version="$(CodeAnalysisVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis" Version="$(CodeAnalysisVersion)" />
<PackageVersion Include="Microsoft.CSharp" Version="4.7.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.VisualStudio.Composition.Analyzers;

/// <summary>
/// Suppresses IDE0044 ("Make field readonly") for fields decorated with MEF
/// <c>[Import]</c> or <c>[ImportMany]</c> attributes,
/// since such fields are assigned at runtime via reflection and cannot be made readonly.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public class IDE0044ImportFieldSuppressor : DiagnosticSuppressor
{
Comment thread
AArnott marked this conversation as resolved.
/// <summary>
/// The suppressor ID.
/// </summary>
public const string Id = "VSMEF013";

private const string SuppressedDiagnosticId = "IDE0044";

/// <summary>
/// The descriptor for this suppressor.
/// </summary>
internal static readonly SuppressionDescriptor Descriptor = new(
id: Id,
suppressedDiagnosticId: SuppressedDiagnosticId,
justification: Strings.VSMEF013_Justification);
Comment thread
AArnott marked this conversation as resolved.

/// <inheritdoc/>
public override ImmutableArray<SuppressionDescriptor> SupportedSuppressions =>
ImmutableArray.Create(Descriptor);

/// <inheritdoc/>
public override void ReportSuppressions(SuppressionAnalysisContext context)
{
foreach (Diagnostic diagnostic in context.ReportedDiagnostics)
{
if (diagnostic.Id != SuppressedDiagnosticId)
{
continue;
}

SyntaxTree? syntaxTree = diagnostic.Location.SourceTree;
if (syntaxTree is null)
{
continue;
}
Comment thread
AArnott marked this conversation as resolved.

SemanticModel semanticModel = context.GetSemanticModel(syntaxTree);
SyntaxNode root = syntaxTree.GetRoot(context.CancellationToken);
SyntaxNode? node = root.FindNode(diagnostic.Location.SourceSpan);

IFieldSymbol? field = null;
while (node is not null)
{
ISymbol? symbol = semanticModel.GetDeclaredSymbol(node, context.CancellationToken);
if (symbol is IFieldSymbol f)
{
field = f;
break;
}

node = node.Parent;
}

if (field is null)
{
continue;
}

foreach (AttributeData attribute in field.GetAttributes())
{
if (Utils.IsFieldImportAttribute(attribute.AttributeClass))
{
context.ReportSuppression(Suppression.Create(Descriptor, diagnostic));
break;
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<PackageId>Microsoft.VisualStudio.Composition.Analyzers.Only</PackageId>
<IsAnalyzerProject>true</IsAnalyzerProject>
<IsPackable>false</IsPackable>
<NoWarn>$(NoWarn);RS2008</NoWarn>
Comment thread
AArnott marked this conversation as resolved.
</PropertyGroup>

<ItemGroup>
Expand Down
6 changes: 6 additions & 0 deletions src/Microsoft.VisualStudio.Composition.Analyzers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@ VSMEF004 | Ensures exported types have a parameterless constructor or importing
VSMEF005 | Detects multiple constructors marked with `[ImportingConstructor]`.
VSMEF006 | Ensures import nullability matches `AllowDefault` setting.
VSMEF007 | Detects when a type imports the same contract multiple times.

## Diagnostic Suppressors

Suppressor ID | Suppressed Diagnostic | Description
--|--|--
VSMEF013 | IDE0044 | Suppresses "Make field readonly" for fields decorated with MEF `[Import]` or `[ImportMany]` attributes, since such fields are assigned at runtime via reflection.
4 changes: 4 additions & 0 deletions src/Microsoft.VisualStudio.Composition.Analyzers/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -274,4 +274,8 @@
<value>MEFv2 attribute "{0}" is not allowed. Use the MEFv1 equivalent from System.ComponentModel.Composition.</value>
<comment>{Locked="MEFv1","MEFv2","System.ComponentModel.Composition"}</comment>
</data>
<data name="VSMEF013_Justification" xml:space="preserve">
<value>Fields with [Import] or [ImportMany] attributes are assigned at runtime via reflection and cannot be made readonly.</value>
Comment thread
AArnott marked this conversation as resolved.
<comment>{Locked="[Import]"} {Locked="[ImportMany]"} The attribute names in brackets should not be localized.</comment>
</data>
</root>
12 changes: 12 additions & 0 deletions src/Microsoft.VisualStudio.Composition.Analyzers/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,18 @@ internal static bool IsNamespaceMatch(INamespaceSymbol? actual, ReadOnlySpan<str
return attributes.FirstOrDefault(attr => IsImportAttribute(attr.AttributeClass));
}

internal static bool IsFieldImportAttribute(INamedTypeSymbol? attributeType)
{
if (attributeType is null)
{
return false;
}

// MEFv2 attributes don't support field imports, so only check MEFv1 Import and ImportMany attributes.
return IsAttributeOfType(attributeType, "ImportAttribute", MefV1AttributeNamespace.AsSpan()) ||
IsAttributeOfType(attributeType, "ImportManyAttribute", MefV1AttributeNamespace.AsSpan());
Comment thread
AArnott marked this conversation as resolved.
}
Comment thread
AArnott marked this conversation as resolved.

internal static bool IsImportAttribute(INamedTypeSymbol? attributeType)
{
if (attributeType is null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ protected override IEnumerable<DiagnosticAnalyzer> GetDiagnosticAnalyzers()
new VSMEF010ImportManyParameterCollectionTypeAnalyzer(),
new VSMEF011BothImportAndImportManyAnalyzer(),
new VSMEF012DisallowMefAttributeVersionAnalyzer(),
new IDE0044ImportFieldSuppressor(),
];
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.VisualStudio.Composition.Analyzers;

public class IDE0044ImportFieldSuppressorTests
{
[Fact]
public async Task FieldWithMefV1ImportAttribute_SuppressesIDE0044()
{
string test = """
using System.ComponentModel.Composition;

class Foo
{
[Import]
private object someField;
}
""";

await new Test
{
TestCode = test,
ExpectedDiagnostics = { Ide0044Diagnostic(6, 20, 6, 29, isSuppressed: true) },
}.RunAsync();
}

[Fact]
public async Task FieldWithMefV1ImportManyAttribute_SuppressesIDE0044()
{
string test = """
using System.Collections.Generic;
using System.ComponentModel.Composition;

class Foo
{
[ImportMany]
private IEnumerable<object> someField;
}
""";

await new Test
{
TestCode = test,
ExpectedDiagnostics = { Ide0044Diagnostic(7, 33, 7, 42, isSuppressed: true) },
}.RunAsync();
}

[Fact]
Comment thread
AArnott marked this conversation as resolved.
public async Task FieldWithoutMefAttribute_DoesNotSuppressIDE0044()
{
string test = """
class Foo
{
private object {|IDE0044:someField|};
}
""";

await new Test { TestCode = test }.RunAsync();
}

[Fact]
public async Task ReadonlyField_NoDiagnostic()
{
string test = """
using System.ComponentModel.Composition;

class Foo
{
private readonly object someField = null;
}
""";

await new Test { TestCode = test }.RunAsync();
}

[Fact]
public async Task ClassWithMixedFields_OnlyNonMefFieldsGetDiagnostic()
{
string test = """
using System.ComponentModel.Composition;

class Foo
{
[Import]
private object mefField;

private object {|IDE0044:regularField|};
}
""";

await new Test
{
TestCode = test,
ExpectedDiagnostics = { Ide0044Diagnostic(6, 20, 6, 28, isSuppressed: true) },
}.RunAsync();
}

private static DiagnosticResult Ide0044Diagnostic(int startLine, int startColumn, int endLine, int endColumn, bool isSuppressed = false)
{
DiagnosticResult result = new DiagnosticResult(FakeIDE0044Analyzer.Descriptor).WithSpan(startLine, startColumn, endLine, endColumn);
return isSuppressed ? result.WithIsSuppressed(true) : result;
}

/// <summary>
/// A fake analyzer that produces IDE0044 for non-readonly, non-const fields,
/// simulating IDE behavior for suppressor testing.
/// </summary>
#pragma warning disable RS1001 // Missing DiagnosticAnalyzerAttribute - intentionally omitted for test-only class
private sealed class FakeIDE0044Analyzer : DiagnosticAnalyzer
#pragma warning restore RS1001
{
#pragma warning disable RS2008 // Enable analyzer release tracking - this is a test-only fake analyzer
internal static readonly DiagnosticDescriptor Descriptor = new(
id: "IDE0044",
title: "Make field readonly",
messageFormat: "Make field readonly",
category: "Style",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
#pragma warning restore RS2008

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
ImmutableArray.Create(Descriptor);

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterSymbolAction(AnalyzeField, SymbolKind.Field);
}

private static void AnalyzeField(SymbolAnalysisContext context)
{
var field = (IFieldSymbol)context.Symbol;
if (!field.IsReadOnly && !field.IsConst)
{
Location? location = field.Locations.FirstOrDefault();
if (location is not null)
{
context.ReportDiagnostic(Diagnostic.Create(Descriptor, location));
}
}
}
}

private sealed class Test : CSharpCodeFixTest<FakeIDE0044Analyzer, EmptyCodeFixProvider, DefaultVerifier>
{
public Test()
{
this.ReferenceAssemblies = ReferencesHelper.DefaultReferences;
}

protected override IEnumerable<DiagnosticAnalyzer> GetDiagnosticAnalyzers()
{
yield return new FakeIDE0044Analyzer();
yield return new IDE0044ImportFieldSuppressor();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.XUnit" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" />
<PackageReference Include="Microsoft.CodeAnalysis.VisualBasic.CodeFix.Testing.XUnit" />
<PackageReference Include="Microsoft.CodeAnalysis.VisualBasic.CodeFix.Testing" />
<PackageReference Include="Microsoft.CodeAnalysis.VisualBasic.Workspaces" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit.runner.visualstudio" />
Expand Down
Loading