Skip to content

Add Item 14: Minimize initialization logic #70

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Sep 28, 2024
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- https://github.com/dotnet/roslyn/blob/main/docs/wiki/NuGet-packages.md
- https://learn.microsoft.com/en-us/visualstudio/productinfo/vs-servicing
-->
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="1.1.1" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.3.1" />
</ItemGroup>
<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
ECS1400 | Initialization | Info | MinimizeDuplicateInitializationLogicAnalyzer, [Documentation](https://github.com/rjmurillo/EffectiveCSharp.Analyzers/blob/4f481355aaf72846c9ff201c6ccd8551bf7874f5/docs/rules/ECS1400.md)
1 change: 1 addition & 0 deletions src/EffectiveCSharp.Analyzers/Common/DiagnosticIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ internal static class DiagnosticIds
internal const string UseSpanInstead = "ECS1000";
internal const string PreferDeclarationInitializersToAssignmentStatement = "ECS1200";
internal const string StaticClassMemberInitialization = "ECS1300";
internal const string MinimizeDuplicateInitializationLogic = "ECS1400";
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Bcl.HashCode" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" PrivateAssets="all" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
namespace EffectiveCSharp.Analyzers;

Check failure on line 1 in src/EffectiveCSharp.Analyzers/MinimizeDuplicateInitializationLogicAnalyzer.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/EffectiveCSharp.Analyzers/MinimizeDuplicateInitializationLogicAnalyzer.cs#L1

Provide an 'AssemblyVersion' attribute for assembly 'srcassembly.dll'.

/// <summary>
/// A <see cref="DiagnosticAnalyzer"/> for Effective C# Item #14 - Minimize duplicate initialization logic.
/// </summary>
/// <seealso cref="Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer" />
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class MinimizeDuplicateInitializationLogicAnalyzer : DiagnosticAnalyzer
{
private static readonly LocalizableString Title = "Minimize duplicate initialization logic";

private static readonly LocalizableString MessageFormat =
"Constructor '{0}' contains duplicate initialization logic";

private static readonly DiagnosticDescriptor Rule = new(
DiagnosticIds.MinimizeDuplicateInitializationLogic,
Title,
MessageFormat,
Categories.Initialization,
DiagnosticSeverity.Info,
isEnabledByDefault: true,
helpLinkUri: $"https://github.com/rjmurillo/EffectiveCSharp.Analyzers/blob/{ThisAssembly.GitCommitId}/docs/rules/{DiagnosticIds.MinimizeDuplicateInitializationLogic}.md");

/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

/// <inheritdoc/>
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSymbolAction(AnalyzeNamedType, SymbolKind.NamedType);
}

private static void AnalyzeNamedType(SymbolAnalysisContext context)
{
INamedTypeSymbol namedTypeSymbol = (INamedTypeSymbol)context.Symbol;

// Collect constructors that are instance constructors and do not use chaining
var constructors = namedTypeSymbol.Constructors
.Where(c => !c.IsStatic && c.DeclaringSyntaxReferences.Length > 0)
.Select(c => new
{
ConstructorSymbol = c,
Declaration = c.DeclaringSyntaxReferences[0].GetSyntax(context.CancellationToken) as ConstructorDeclarationSyntax,
})
.Where(c => c.Declaration is { Initializer: null })
.ToList();

if (constructors.Count < 2)
{
return;
}

SyntaxTree? syntaxTree = constructors[0].Declaration?.SyntaxTree;
if (syntaxTree == null)
{
return;
}

#pragma warning disable RS1030
SemanticModel semanticModel = context.Compilation.GetSemanticModel(syntaxTree);
#pragma warning restore RS1030

// Compute initialization statements for all constructors once
Dictionary<IMethodSymbol, List<InitializationStatement>> constructorInitStatements = new(SymbolEqualityComparer.IncludeNullability);

for (int i = 0; i < constructors.Count; i++)
{
var ctor = constructors[i];
List<InitializationStatement> initStatements = GetInitializationStatements(
ctor.Declaration,
semanticModel,
context.CancellationToken);

if (initStatements.Count > 0)
{
constructorInitStatements[ctor.ConstructorSymbol] = initStatements;
}
}

// Compare initialization statements between constructors
CompareConstructors(context, constructorInitStatements);
}

private static void CompareConstructors(SymbolAnalysisContext context, Dictionary<IMethodSymbol, List<InitializationStatement>> constructorInitStatements)
{
foreach (KeyValuePair<IMethodSymbol, List<InitializationStatement>> ctor in constructorInitStatements)
{
foreach (KeyValuePair<IMethodSymbol, List<InitializationStatement>> otherCtor in constructorInitStatements)
{
if (ctor.Key.Equals(otherCtor.Key, SymbolEqualityComparer.Default))
{
continue;
}

if (!InitializationStatementsAreEqual(ctor.Value, otherCtor.Value))
{
continue;
}

Diagnostic diagnostic = ctor.Key.Locations[0].CreateDiagnostic(Rule, ctor.Key.Name);
context.ReportDiagnostic(diagnostic);
break;
}
}
}

#pragma warning disable MA0051
private static List<InitializationStatement> GetInitializationStatements(

Check failure on line 110 in src/EffectiveCSharp.Analyzers/MinimizeDuplicateInitializationLogicAnalyzer.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/EffectiveCSharp.Analyzers/MinimizeDuplicateInitializationLogicAnalyzer.cs#L110

Refactor this method to reduce its Cognitive Complexity from 24 to the 15 allowed.

Check failure on line 110 in src/EffectiveCSharp.Analyzers/MinimizeDuplicateInitializationLogicAnalyzer.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/EffectiveCSharp.Analyzers/MinimizeDuplicateInitializationLogicAnalyzer.cs#L110

The Cyclomatic Complexity of this method is 13 which is greater than 10 authorized.
ConstructorDeclarationSyntax? constructor,
SemanticModel semanticModel,
CancellationToken cancellationToken)
{
List<InitializationStatement> statements = [];

if (constructor?.Body == null)
{
return statements;
}

for (int i = 0; i < constructor.Body.Statements.Count; i++)
{
switch (constructor.Body.Statements[i])

Check failure on line 124 in src/EffectiveCSharp.Analyzers/MinimizeDuplicateInitializationLogicAnalyzer.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/EffectiveCSharp.Analyzers/MinimizeDuplicateInitializationLogicAnalyzer.cs#L124

Add a 'default' clause to this 'switch' statement.
{
// Handle assignments and method calls
case ExpressionStatementSyntax { Expression: AssignmentExpressionSyntax assignment }:
{
ISymbol? leftSymbol = semanticModel.GetSymbolInfo(assignment.Left, cancellationToken).Symbol;
ISymbol? rightSymbol = semanticModel.GetSymbolInfo(assignment.Right, cancellationToken).Symbol ??
semanticModel.GetTypeInfo(assignment.Right, cancellationToken).Type;

if (leftSymbol != null && rightSymbol != null)
{
statements.Add(
new InitializationStatement(
kind: InitializationKind.Assignment,
leftSymbol: leftSymbol,
rightSymbol: rightSymbol));
}

break;
}

case ExpressionStatementSyntax exprStmt:
{
if (exprStmt.Expression is InvocationExpressionSyntax invocation &&
semanticModel.GetSymbolInfo(invocation, cancellationToken).Symbol is IMethodSymbol methodSymbol)
{
statements.Add(
new InitializationStatement(
kind: InitializationKind.MethodCall,
methodSymbol: methodSymbol));
}

break;
}

case LocalDeclarationStatementSyntax localDecl:
{
foreach (VariableDeclaratorSyntax variable in localDecl.Declaration.Variables)
{
if (variable.Initializer == null)

Check failure on line 163 in src/EffectiveCSharp.Analyzers/MinimizeDuplicateInitializationLogicAnalyzer.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/EffectiveCSharp.Analyzers/MinimizeDuplicateInitializationLogicAnalyzer.cs#L163

Refactor this code to not nest more than 3 control flow statements.
{
continue;
}

ISymbol? symbol = semanticModel.GetDeclaredSymbol(variable, cancellationToken);
ITypeSymbol? initializerType = semanticModel.GetTypeInfo(
variable.Initializer.Value,
cancellationToken).Type;
if (symbol != null && initializerType != null)

Check failure on line 172 in src/EffectiveCSharp.Analyzers/MinimizeDuplicateInitializationLogicAnalyzer.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/EffectiveCSharp.Analyzers/MinimizeDuplicateInitializationLogicAnalyzer.cs#L172

Refactor this code to not nest more than 3 control flow statements.
{
statements.Add(
new InitializationStatement(
kind: InitializationKind.VariableDeclaration,
leftSymbol: symbol,
rightSymbol: initializerType));
}
}

break;
}
}
}

return statements;
}
#pragma warning restore MA0051

private static bool InitializationStatementsAreEqual(
List<InitializationStatement> first,
List<InitializationStatement> second)
{
if (first.Count != second.Count || first.Count == 0)
{
return false;
}

HashSet<InitializationStatement> firstSet = [.. first];
HashSet<InitializationStatement> secondSet = [.. second];

return firstSet.SetEquals(secondSet) && firstSet.Count > 0;
}

#pragma warning disable SA1201
private enum InitializationKind
#pragma warning restore SA1201
{
Assignment,
MethodCall,
VariableDeclaration,
}

private sealed class InitializationStatement : IEquatable<InitializationStatement>

Check notice on line 215 in src/EffectiveCSharp.Analyzers/MinimizeDuplicateInitializationLogicAnalyzer.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/EffectiveCSharp.Analyzers/MinimizeDuplicateInitializationLogicAnalyzer.cs#L215

Implement 'IEquatable<InitializationStatement>'.
{
public InitializationStatement(InitializationKind kind, ISymbol leftSymbol, ISymbol rightSymbol)
: this(kind)
{
LeftSymbol = leftSymbol;
RightSymbol = rightSymbol;
}

public InitializationStatement(InitializationKind kind, IMethodSymbol methodSymbol)
: this(kind)
{
MethodSymbol = methodSymbol;
}

private InitializationStatement(InitializationKind kind)
{
Kind = kind;
}

public InitializationKind Kind { get; }

public ISymbol? LeftSymbol { get; }

public ISymbol? RightSymbol { get; }

public IMethodSymbol? MethodSymbol { get; }

public bool Equals(InitializationStatement? other)
{
if (other == null || Kind != other.Kind)
{
return false;
}

SymbolEqualityComparer comparer = SymbolEqualityComparer.Default;

return Kind switch
{
InitializationKind.Assignment => comparer.Equals(LeftSymbol, other.LeftSymbol) &&
comparer.Equals(RightSymbol, other.RightSymbol),
InitializationKind.MethodCall => comparer.Equals(MethodSymbol, other.MethodSymbol),
InitializationKind.VariableDeclaration => comparer.Equals(LeftSymbol, other.LeftSymbol) &&
comparer.Equals(RightSymbol, other.RightSymbol),
_ => false,
};
}

public override bool Equals(object? obj)
{
return Equals(obj as InitializationStatement);
}

public override int GetHashCode()
{
HashCode hashCode = default;
hashCode.Add(Kind);
SymbolEqualityComparer comparer = SymbolEqualityComparer.IncludeNullability;

switch (Kind)

Check failure on line 274 in src/EffectiveCSharp.Analyzers/MinimizeDuplicateInitializationLogicAnalyzer.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/EffectiveCSharp.Analyzers/MinimizeDuplicateInitializationLogicAnalyzer.cs#L274

Add a 'default' clause to this 'switch' statement.
{
case InitializationKind.Assignment:
case InitializationKind.VariableDeclaration:
hashCode.Add(comparer.GetHashCode(LeftSymbol));
hashCode.Add(comparer.GetHashCode(RightSymbol));
break;
case InitializationKind.MethodCall:
hashCode.Add(comparer.GetHashCode(MethodSymbol));
break;
}

return hashCode.ToHashCode();
}
}
}
10 changes: 5 additions & 5 deletions src/EffectiveCSharp.Analyzers/SquiggleCop.Baseline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -604,8 +604,8 @@
- {Id: RCS1020, Title: 'Simplify Nullable<T> to T?', Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false}
- {Id: RCS1021, Title: Convert lambda expression body to expression body, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false}
- {Id: RCS1021FadeOut, Title: Convert lambda expression body to expression body, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false}
- {Id: RCS1031, Title: Remove unnecessary braces in switch section, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false}
- {Id: RCS1031FadeOut, Title: Remove unnecessary braces in switch section, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false}
- {Id: RCS1031, Title: Remove unnecessary braces in switch section, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: true}
- {Id: RCS1031FadeOut, Title: Remove unnecessary braces in switch section, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: true}
- {Id: RCS1032, Title: Remove redundant parentheses, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false}
- {Id: RCS1032FadeOut, Title: Remove redundant parentheses, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false}
- {Id: RCS1033, Title: Remove redundant boolean literal, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false}
Expand Down Expand Up @@ -853,7 +853,7 @@
- {Id: RS1027, Title: Types marked with DiagnosticAnalyzerAttribute(s) should inherit from DiagnosticAnalyzer, Category: MicrosoftCodeAnalysisCorrectness, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false}
- {Id: RS1028, Title: Provide non-null 'customTags' value to diagnostic descriptor constructor, Category: MicrosoftCodeAnalysisDocumentation, DefaultSeverity: Warning, IsEnabledByDefault: false, EffectiveSeverities: [None], IsEverSuppressed: true}
- {Id: RS1029, Title: Do not use reserved diagnostic IDs, Category: MicrosoftCodeAnalysisDesign, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false}
- {Id: RS1030, Title: Do not invoke Compilation.GetSemanticModel() method within a diagnostic analyzer, Category: MicrosoftCodeAnalysisCorrectness, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false}
- {Id: RS1030, Title: Do not invoke Compilation.GetSemanticModel() method within a diagnostic analyzer, Category: MicrosoftCodeAnalysisCorrectness, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: true}
- {Id: RS1031, Title: Define diagnostic title correctly, Category: MicrosoftCodeAnalysisDesign, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false}
- {Id: RS1032, Title: Define diagnostic message correctly, Category: MicrosoftCodeAnalysisDesign, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false}
- {Id: RS1033, Title: Define diagnostic description correctly, Category: MicrosoftCodeAnalysisDesign, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false}
Expand Down Expand Up @@ -1371,7 +1371,7 @@
- {Id: SA1027, Title: Use tabs correctly, Category: StyleCop.CSharp.SpacingRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false}
- {Id: SA1028, Title: Code should not contain trailing whitespace, Category: StyleCop.CSharp.SpacingRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false}
- {Id: SA1100, Title: Do not prefix calls with base unless local implementation exists, Category: StyleCop.CSharp.ReadabilityRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false}
- {Id: SA1101, Title: Prefix local calls with this, Category: StyleCop.CSharp.ReadabilityRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false}
- {Id: SA1101, Title: Prefix local calls with this, Category: StyleCop.CSharp.ReadabilityRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: true}
- {Id: SA1102, Title: Query clause should follow previous clause, Category: StyleCop.CSharp.ReadabilityRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false}
- {Id: SA1103, Title: Query clauses should be on separate lines or all on one line, Category: StyleCop.CSharp.ReadabilityRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false}
- {Id: SA1104, Title: Query clause should begin on new line when previous clause spans multiple lines, Category: StyleCop.CSharp.ReadabilityRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false}
Expand Down Expand Up @@ -1413,7 +1413,7 @@
- {Id: SA1141, Title: Use tuple syntax, Category: StyleCop.CSharp.ReadabilityRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false}
- {Id: SA1142, Title: Refer to tuple fields by name, Category: StyleCop.CSharp.ReadabilityRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false}
- {Id: SA1200, Title: Using directives should be placed correctly, Category: StyleCop.CSharp.OrderingRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false}
- {Id: SA1201, Title: Elements should appear in the correct order, Category: StyleCop.CSharp.OrderingRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false}
- {Id: SA1201, Title: Elements should appear in the correct order, Category: StyleCop.CSharp.OrderingRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: true}
- {Id: SA1202, Title: Elements should be ordered by access, Category: StyleCop.CSharp.OrderingRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false}
- {Id: SA1203, Title: Constants should appear before fields, Category: StyleCop.CSharp.OrderingRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false}
- {Id: SA1204, Title: Static elements should appear before instance elements, Category: StyleCop.CSharp.OrderingRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false}
Expand Down
1 change: 1 addition & 0 deletions src/tools/Dogfood/Dogfood.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Bcl.HashCode" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" PrivateAssets="all" />
</ItemGroup>
</Project>
Loading