Skip to content

Commit d98ad1f

Browse files
bcollamoreajbargaclaude
authored
feat: Detect Implicit Object Creation (Mock<T> myMock = new()) (#1090)
Co-authored-by: Alex Barga <99690046+ajbarga@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent ac252d9 commit d98ad1f

10 files changed

Lines changed: 470 additions & 125 deletions

File tree

Philips.CodeAnalysis.MaintainabilityAnalyzers/Maintainability/NoRegionsInMethodAnalyzer.cs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
// © 2023 Koninklijke Philips N.V. See License.md in the project root for license information.
22

3+
using System.Collections.Generic;
34
using System.Linq;
45
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CSharp;
57
using Microsoft.CodeAnalysis.CSharp.Syntax;
68
using Microsoft.CodeAnalysis.Diagnostics;
79
using Philips.CodeAnalysis.Common;
@@ -25,18 +27,18 @@ public class NoRegionsInMethodSyntaxNodeAction : SyntaxNodeAction<MethodDeclarat
2527
{
2628
public override void Analyze()
2729
{
28-
// Specifying Span instead of FullSpan correctly excludes trivia before or after the method
29-
System.Collections.Generic.IEnumerable<DirectiveTriviaSyntax> descendants = Node.DescendantNodes(Node.Span, null, descendIntoTrivia: true).OfType<DirectiveTriviaSyntax>();
30-
foreach (RegionDirectiveTriviaSyntax regionDirective in descendants.OfType<RegionDirectiveTriviaSyntax>())
31-
{
32-
Location location = regionDirective.GetLocation();
33-
ReportDiagnostic(location);
34-
}
30+
IEnumerable<DirectiveTriviaSyntax> directives = Node
31+
.DescendantTrivia(Node.Span, descendIntoTrivia: true)
32+
.Select(trivia => trivia.GetStructure())
33+
.OfType<DirectiveTriviaSyntax>();
3534

36-
foreach (EndRegionDirectiveTriviaSyntax endRegionDirective in descendants.OfType<EndRegionDirectiveTriviaSyntax>())
35+
foreach (DirectiveTriviaSyntax directive in directives)
3736
{
38-
Location location = endRegionDirective.GetLocation();
39-
ReportDiagnostic(location);
37+
if (directive.IsKind(SyntaxKind.RegionDirectiveTrivia) ||
38+
directive.IsKind(SyntaxKind.EndRegionDirectiveTrivia))
39+
{
40+
ReportDiagnostic(directive.GetLocation());
41+
}
4042
}
4143
}
4244
}

Philips.CodeAnalysis.MaintainabilityAnalyzers/Maintainability/ProhibitDynamicKeywordAnalyzer.cs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ namespace Philips.CodeAnalysis.MaintainabilityAnalyzers.Maintainability
1313
public class ProhibitDynamicKeywordAnalyzer : SingleDiagnosticAnalyzer
1414
{
1515
private const string Title = @"Prohibit the ""dynamic"" Keyword";
16-
private const string MessageFormat = @"Do not use the ""dynamic"" keyword. It it not compile time type safe.";
16+
private const string MessageFormat = @"Do not use the ""dynamic"" keyword. It is not compile time type safe.";
1717
private const string Description = @"The ""dynamic"" keyword is not checked for type safety at compile time.";
18-
private const string DynamicIdentifier = "dynamic";
18+
private const string DynamicIdentifier = @"dynamic";
1919

2020
public ProhibitDynamicKeywordAnalyzer()
2121
: base(DiagnosticId.DynamicKeywordProhibited, Title, MessageFormat, Description, Categories.Maintainability)
@@ -70,17 +70,14 @@ private void AnalyzeVariable(SyntaxNodeAnalysisContext context)
7070

7171
private static bool IsDynamicType(SyntaxNodeAnalysisContext context, TypeSyntax typeSyntax)
7272
{
73-
if (typeSyntax is SimpleNameSyntax { Identifier.ValueText: DynamicIdentifier })
73+
if (typeSyntax is IdentifierNameSyntax { Identifier.ValueText: DynamicIdentifier })
7474
{
75-
// Double check the semantic model (to distinguish a variable named 'dynamic').
76-
SymbolInfo symbol = context.SemanticModel.GetSymbolInfo(typeSyntax);
77-
if (symbol.Symbol is IDynamicTypeSymbol)
78-
{
79-
return true;
80-
}
75+
TypeInfo typeInfo = context.SemanticModel.GetTypeInfo(typeSyntax);
76+
return typeInfo.Type is IDynamicTypeSymbol;
8177
}
8278

83-
if (typeSyntax is GenericNameSyntax generic && generic.TypeArgumentList.Arguments.Any(gen => IsDynamicType(context, gen)))
79+
if (typeSyntax is GenericNameSyntax generic &&
80+
generic.TypeArgumentList.Arguments.Any(gen => IsDynamicType(context, gen)))
8481
{
8582
return true;
8683
}

Philips.CodeAnalysis.MoqAnalyzers/MockDisposableClassesShouldSetupDisposeAnalyzer.cs

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,12 @@ protected override void InitializeCompilation(CompilationStartAnalysisContext co
3030
return;
3131
}
3232

33-
context.RegisterSyntaxNodeAction(AnalyzeObjectCreation, SyntaxKind.ObjectCreationExpression);
33+
context.RegisterSyntaxNodeAction(AnalyzeObjectCreation, SyntaxKind.ObjectCreationExpression, SyntaxKind.ImplicitObjectCreationExpression);
3434
}
3535

3636
private void AnalyzeObjectCreation(SyntaxNodeAnalysisContext context)
3737
{
38-
var objectCreationExpressionSyntax = (ObjectCreationExpressionSyntax)context.Node;
39-
40-
if (objectCreationExpressionSyntax.Type is not GenericNameSyntax genericNameSyntax)
41-
{
42-
return;
43-
}
44-
45-
if (genericNameSyntax.Identifier.ValueText != MockName)
38+
if (context.Node is not ExpressionSyntax objectCreationExpressionSyntax)
4639
{
4740
return;
4841
}
@@ -84,9 +77,8 @@ private void AnalyzeObjectCreation(SyntaxNodeAnalysisContext context)
8477
ReportDiagnostic(context, objectCreationExpressionSyntax, mockedType);
8578
}
8679

87-
private void ReportDiagnostic(SyntaxNodeAnalysisContext context, ObjectCreationExpressionSyntax objectCreationExpressionSyntax, ITypeSymbol mockedType)
80+
private void ReportDiagnostic(SyntaxNodeAnalysisContext context, ExpressionSyntax objectCreationExpressionSyntax, ITypeSymbol mockedType)
8881
{
89-
// Pass the preferred disposable mock type from editorconfig as additional property, so that code fix can use it to determine which type to suggest in the code fix message and when applying the code fix.
9082
var preferredDisposableMockType = Helper.ForAdditionalFiles.GetValueFromEditorConfig(Rule.Id, "preferred_disposable_mock_type");
9183

9284
ImmutableDictionary<string, string> properties = ImmutableDictionary<string, string>.Empty;
@@ -98,7 +90,7 @@ private void ReportDiagnostic(SyntaxNodeAnalysisContext context, ObjectCreationE
9890
context.ReportDiagnostic(Diagnostic.Create(Rule, objectCreationExpressionSyntax.GetLocation(), properties, mockedType.Name));
9991
}
10092

101-
private bool TryGetMockedType(SyntaxNodeAnalysisContext context, ObjectCreationExpressionSyntax objectCreationExpressionSyntax, out ITypeSymbol mockedType)
93+
private bool TryGetMockedType(SyntaxNodeAnalysisContext context, ExpressionSyntax objectCreationExpressionSyntax, out ITypeSymbol mockedType)
10294
{
10395
mockedType = null;
10496

@@ -180,7 +172,7 @@ Accessibility.ProtectedOrInternal or
180172
};
181173
}
182174

183-
private bool HasDisposeSetupInSameContainingMember(SyntaxNodeAnalysisContext context, ObjectCreationExpressionSyntax objectCreationExpressionSyntax)
175+
private bool HasDisposeSetupInSameContainingMember(SyntaxNodeAnalysisContext context, ExpressionSyntax objectCreationExpressionSyntax)
184176
{
185177
ISymbol mockSymbol = GetAssignedMockSymbol(context, objectCreationExpressionSyntax);
186178
if (mockSymbol == null)
@@ -229,16 +221,14 @@ private static SyntaxNode GetContainingMember(SyntaxNode node)
229221
return containingMember;
230222
}
231223

232-
private ISymbol GetAssignedMockSymbol(SyntaxNodeAnalysisContext context, ObjectCreationExpressionSyntax objectCreationExpressionSyntax)
224+
private ISymbol GetAssignedMockSymbol(SyntaxNodeAnalysisContext context, ExpressionSyntax objectCreationExpressionSyntax)
233225
{
234-
// Local variable, including "using var mock = ..."
235226
var equalsValueClauseSyntax = objectCreationExpressionSyntax.Parent as EqualsValueClauseSyntax;
236227
if (equalsValueClauseSyntax?.Parent is VariableDeclaratorSyntax variableDeclaratorSyntax)
237228
{
238229
return context.SemanticModel.GetDeclaredSymbol(variableDeclaratorSyntax);
239230
}
240231

241-
// Assignment to a field/property/local: mock = new Mock<T>();
242232
if (objectCreationExpressionSyntax.Parent is AssignmentExpressionSyntax assignmentExpressionSyntax)
243233
{
244234
SymbolInfo symbolInfo = context.SemanticModel.GetSymbolInfo(assignmentExpressionSyntax.Left);
@@ -250,10 +240,6 @@ private ISymbol GetAssignedMockSymbol(SyntaxNodeAnalysisContext context, ObjectC
250240

251241
private bool IsDisposeSetupInvocation(SyntaxNodeAnalysisContext context, InvocationExpressionSyntax invocationExpressionSyntax, ISymbol expectedMockSymbol)
252242
{
253-
// Looking for:
254-
// mock.Protected().Setup(...Dispose...)
255-
// We intentionally do not require CallBase() in v1.
256-
257243
if (invocationExpressionSyntax.Expression is not MemberAccessExpressionSyntax memberAccessExpressionSyntax)
258244
{
259245
return false;

Philips.CodeAnalysis.MoqAnalyzers/MockDisposableClassesShouldSetupDisposeCodeFixProvider.cs

Lines changed: 68 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@
1414
namespace Philips.CodeAnalysis.MoqAnalyzers
1515
{
1616
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MockDisposableClassesShouldSetupDisposeCodeFixProvider)), Shared]
17-
public class MockDisposableClassesShouldSetupDisposeCodeFixProvider : SingleDiagnosticCodeFixProvider<ObjectCreationExpressionSyntax>
17+
public class MockDisposableClassesShouldSetupDisposeCodeFixProvider : SingleDiagnosticCodeFixProvider<ExpressionSyntax>
1818
{
1919
protected override string Title => "Use configured disposable mock type";
2020

2121
protected override DiagnosticId DiagnosticId => DiagnosticId.MockDisposableObjectsShouldSetupDispose;
2222

23-
protected override async Task<Document> ApplyFix(Document document, ObjectCreationExpressionSyntax node, ImmutableDictionary<string, string> properties, CancellationToken cancellationToken)
23+
protected override async Task<Document> ApplyFix(Document document, ExpressionSyntax node, ImmutableDictionary<string, string> properties, CancellationToken cancellationToken)
2424
{
2525
var configuredTypeName = GetPreferredDisposableMockType(properties);
2626
if (string.IsNullOrWhiteSpace(configuredTypeName))
@@ -34,55 +34,98 @@ protected override async Task<Document> ApplyFix(Document document, ObjectCreati
3434
return document;
3535
}
3636

37-
if (node.Type is not GenericNameSyntax objectCreationGenericName)
37+
ExpressionSyntax currentNode = FindCurrentNode<ExpressionSyntax>(rootNode, node);
38+
if (currentNode == null)
3839
{
3940
return document;
4041
}
4142

42-
TypeSyntax replacementTypeSyntax = CreateReplacementTypeSyntax(configuredTypeName, objectCreationGenericName.TypeArgumentList);
43+
TypeSyntax declaredTypeToReplace = GetDeclaredTypeFromContext(currentNode);
4344

44-
TypeSyntax replacementType = replacementTypeSyntax.WithAdditionalAnnotations(Formatter.Annotation);
45+
if (currentNode is ImplicitObjectCreationExpressionSyntax)
46+
{
47+
if (declaredTypeToReplace is not GenericNameSyntax declaredGenericName)
48+
{
49+
return document;
50+
}
51+
52+
TypeSyntax replacementType = CreateReplacementTypeSyntax(configuredTypeName, declaredGenericName.TypeArgumentList)
53+
.WithAdditionalAnnotations(Formatter.Annotation);
4554

46-
SyntaxNode newRoot;
47-
TypeSyntax declaredTypeToReplace = GetDeclaredTypeToReplace(node, objectCreationGenericName);
55+
SyntaxNode newRoot = rootNode.ReplaceNode(
56+
declaredTypeToReplace,
57+
replacementType.WithTriviaFrom(declaredTypeToReplace));
58+
59+
return document.WithSyntaxRoot(newRoot);
60+
}
4861

49-
if (declaredTypeToReplace != null)
62+
if (currentNode is ObjectCreationExpressionSyntax explicitObjectCreation &&
63+
explicitObjectCreation.Type is GenericNameSyntax explicitGenericName)
5064
{
51-
newRoot = rootNode.ReplaceNodes(
52-
new SyntaxNode[] { node.Type, declaredTypeToReplace },
53-
(original, _) => replacementType.WithTriviaFrom(original));
65+
TypeSyntax replacementType = CreateReplacementTypeSyntax(configuredTypeName, explicitGenericName.TypeArgumentList)
66+
.WithAdditionalAnnotations(Formatter.Annotation);
67+
68+
SyntaxNode newRoot;
69+
if (IsMatchingMockDeclaredType(declaredTypeToReplace, explicitGenericName))
70+
{
71+
newRoot = rootNode.ReplaceNodes(
72+
new SyntaxNode[] { explicitObjectCreation.Type, declaredTypeToReplace },
73+
(original, _) => replacementType.WithTriviaFrom(original));
74+
}
75+
else
76+
{
77+
newRoot = rootNode.ReplaceNode(
78+
explicitObjectCreation.Type,
79+
replacementType.WithTriviaFrom(explicitObjectCreation.Type));
80+
}
81+
82+
return document.WithSyntaxRoot(newRoot);
5483
}
55-
else
84+
85+
return document;
86+
}
87+
88+
private static TNode FindCurrentNode<TNode>(SyntaxNode rootNode, SyntaxNode originalNode)
89+
where TNode : SyntaxNode
90+
{
91+
if (rootNode == null || originalNode == null)
5692
{
57-
newRoot = rootNode.ReplaceNode(
58-
node.Type,
59-
replacementType.WithTriviaFrom(node.Type));
93+
return null;
6094
}
6195

62-
return document.WithSyntaxRoot(newRoot);
96+
SyntaxNode currentNode = rootNode.FindNode(originalNode.Span, getInnermostNodeForTie: true);
97+
return currentNode as TNode;
6398
}
6499

65-
private static TypeSyntax GetDeclaredTypeToReplace(ObjectCreationExpressionSyntax objectCreationSyntax, GenericNameSyntax originalObjectCreationType)
100+
private static TypeSyntax GetDeclaredTypeFromContext(SyntaxNode node)
66101
{
67-
VariableDeclarationSyntax variableDeclarationSyntax = objectCreationSyntax.FirstAncestorOrSelf<VariableDeclarationSyntax>();
68-
if (variableDeclarationSyntax != null &&
69-
variableDeclarationSyntax.Type is GenericNameSyntax declaredGenericName &&
70-
declaredGenericName.Identifier.ValueText == originalObjectCreationType.Identifier.ValueText)
102+
VariableDeclarationSyntax variableDeclarationSyntax = node.FirstAncestorOrSelf<VariableDeclarationSyntax>();
103+
if (variableDeclarationSyntax != null)
71104
{
72105
return variableDeclarationSyntax.Type;
73106
}
74107

75-
PropertyDeclarationSyntax propertyDeclarationSyntax = objectCreationSyntax.FirstAncestorOrSelf<PropertyDeclarationSyntax>();
76-
if (propertyDeclarationSyntax != null &&
77-
propertyDeclarationSyntax.Type is GenericNameSyntax propertyGenericName &&
78-
propertyGenericName.Identifier.ValueText == originalObjectCreationType.Identifier.ValueText)
108+
PropertyDeclarationSyntax propertyDeclarationSyntax = node.FirstAncestorOrSelf<PropertyDeclarationSyntax>();
109+
if (propertyDeclarationSyntax != null)
79110
{
80111
return propertyDeclarationSyntax.Type;
81112
}
82113

114+
FieldDeclarationSyntax fieldDeclarationSyntax = node.FirstAncestorOrSelf<FieldDeclarationSyntax>();
115+
if (fieldDeclarationSyntax != null)
116+
{
117+
return fieldDeclarationSyntax.Declaration?.Type;
118+
}
119+
83120
return null;
84121
}
85122

123+
private static bool IsMatchingMockDeclaredType(TypeSyntax declaredTypeSyntax, GenericNameSyntax originalObjectCreationType)
124+
{
125+
return declaredTypeSyntax is GenericNameSyntax declaredGenericName &&
126+
declaredGenericName.Identifier.ValueText == originalObjectCreationType.Identifier.ValueText;
127+
}
128+
86129
private static string GetPreferredDisposableMockType(ImmutableDictionary<string, string> properties)
87130
{
88131
if (properties != null &&

Philips.CodeAnalysis.MsTestAnalyzers/TestMethodNameCodeFixProvider.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ protected override async Task<Solution> ApplyFix(Document document, MethodDeclar
4141

4242
// Get the symbol representing the method to be renamed.
4343
SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken);
44-
IMethodSymbol typeSymbol = semanticModel.GetDeclaredSymbol(node, cancellationToken);
44+
IMethodSymbol methodSymbol = semanticModel.GetDeclaredSymbol(node, cancellationToken);
4545

4646
SymbolRenameOptions renameOptions = new()
4747
{
@@ -53,12 +53,11 @@ protected override async Task<Solution> ApplyFix(Document document, MethodDeclar
5353

5454
Solution newSolution = await Renamer.RenameSymbolAsync(
5555
document.Project.Solution,
56-
typeSymbol,
56+
methodSymbol,
5757
renameOptions,
5858
name,
5959
cancellationToken).ConfigureAwait(false);
6060

61-
// Return the new solution with the now-uppercase type name.
6261
return newSolution;
6362
}
6463
}

Philips.CodeAnalysis.Test/Maintainability/Documentation/XmlDocumentationShouldAddValueAnalyzerTest.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ public DoubleList(int capacity)
8181
}}
8282
}}
8383
";
84-
8584
await VerifyDiagnostic(content, DiagnosticId.EmptyXmlComments).ConfigureAwait(false);
8685
await VerifyFix(content, newContent);
8786
}

Philips.CodeAnalysis.Test/Maintainability/Maintainability/NoRegionsInMethodAnalyzerTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ public async Task RegionCoversMultipleMethodsTestAsync()
8585
[TestCategory(TestDefinitions.UnitTests)]
8686
public async Task RegionBeforeClassTestAsync()
8787
{
88-
await VerifySuccessfulCompilation(@" #region testRegion #endregion Class C{ public void foo(){ return; } public void bar(){ } }").ConfigureAwait(false);
88+
await VerifySuccessfulCompilation(@" #region testRegion #endregion class C{ public void foo(){ return; } public void bar(){ } }").ConfigureAwait(false);
8989
}
9090

9191
[TestMethod]

0 commit comments

Comments
 (0)