diff --git a/docs/RULE-REFERENCE-TEMPLATE.md b/docs/RULE-REFERENCE-TEMPLATE.md index 8933b37..1e4aa0d 100644 --- a/docs/RULE-REFERENCE-TEMPLATE.md +++ b/docs/RULE-REFERENCE-TEMPLATE.md @@ -10,6 +10,23 @@ This rule is described in detail in [Effective C#: 50 Specific Ways to Improve y ## When to suppress warnings +### Suppress a warning + +If you just want to suppress a single violation, add preprocessor directives to your source file to disable and then re-enable the rule. + +```csharp +#pragma warning disable RULEID +// The code that's violating the rule +#pragma warning restore RULEID +``` + +To disable the rule for a file, folder, or project, set its severity to none in the [configuration file](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/configuration-files). + +```ini +[*.cs] +dotnet_diagnostic.RULEID.severity = none +``` + ## Example of a violation ### Description diff --git a/docs/rules/ECS0008.md b/docs/rules/ECS0008.md new file mode 100644 index 0000000..44719f9 --- /dev/null +++ b/docs/rules/ECS0008.md @@ -0,0 +1,81 @@ +# ECS0008: Use the Null-Conditional Operator for Event Invocations + +This rule is described in detail in [Effective C#: 50 Specific Ways to Improve your C#](https://www.oreilly.com/library/view/effective-c-50/9780134579290/). + +## Cause + +This rule is triggered when an event handler is invoked without using the null-conditional operator (`?.`), which can potentially lead to a `NullReferenceException` if there are no subscribers to the event. + + +## Rule description + +When invoking events in C#, it is recommended to use the null-conditional operator to ensure that the event is only invoked if it has subscribers. This prevents potential runtime errors and makes the code more robust. This rule checks for patterns where the event handler is invoked directly or after a null check and suggests replacing them with the null-conditional operator. + +## How to fix violations + +Replace any `if` statement that checks if an event handler is `null` and then invokes the handler with the null-conditional operator. + +If the code uses an intermediate variable (e.g., `var handler = Updated;`), remove the variable and replace the `if` statement with the null-conditional operator directly on the event. + +## When to suppress warnings + +You can suppress warnings from this rule if you're confident that the event will always have subscribers at the time of invocation, or if you have special logic that must be executed before the event is invoked. + +## Example of a violation + +### Description + +Directly invoking the event handler or checking for null before invoking without using the null-conditional operator. + +### Code + +```csharp +public class EventSource +{ + private EventHandler Updated; + private int counter; + + public void RaiseUpdates() + { + counter++; + if (Updated != null) + Updated(this, counter); + } +} + +public class EventSource +{ + private EventHandler Updated; + private int counter; + + public void RaiseUpdates() + { + counter++; + var handler = Updated; + if (handler != null) + handler(this, counter); + } +} +``` + +## Example of how to fix + +### Description + +Replace the direct invocation or the null check with the null-conditional operator. + +### Code + +```csharp +public class EventSource +{ + private EventHandler Updated; + private int counter; + + public void RaiseUpdates() + { + counter++; + Updated?.Invoke(this, counter); + } +} +``` \ No newline at end of file diff --git a/src/EffectiveCSharp.Analyzers/AnalyzerReleases.Unshipped.md b/src/EffectiveCSharp.Analyzers/AnalyzerReleases.Unshipped.md index 493e6bd..3d45752 100644 --- a/src/EffectiveCSharp.Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/EffectiveCSharp.Analyzers/AnalyzerReleases.Unshipped.md @@ -4,10 +4,11 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------- -ECS0001 | Style | Info | PreferImplicitlyTypedLocalVariablesAnalyzer, [Documentation](https://github.com/rjmurillo/EffectiveCSharp.Analyzers/blob/e7c151c721c3039011356d6012838f46e4b60a21/docs/ECS0001.md) -ECS0002 | Maintainability | Info | PreferReadonlyOverConstAnalyzer, [Documentation](https://github.com/rjmurillo/EffectiveCSharp.Analyzers/blob/10c2d53afd688efe5a59097f76cb4edf33f6a474/docs/ECS0002.md) -ECS0004 | Style | Info | ReplaceStringFormatAnalyzer, [Documentation](https://github.com/rjmurillo/EffectiveCSharp.Analyzers5da647e447fad4eb0a9e3db287e1d16cce316114/docs/ECS0004.md) -ECS0006 | Refactoring | Info | AvoidStringlyTypedApisAnalyzer, [Documentation](https://github.com/rjmurillo/EffectiveCSharp.Analyzers6213cba8473dac61d6132e205550884eae1c94bf/docs/ECS0006.md) -ECS0007 | Design | Info | ExpressCallbacksWithDelegatesAnalyzer, [Documentation](https://github.com/rjmurillo/EffectiveCSharp.Analyzers/blob/dc194bbde030e9c40d8d9cdb1e0b5ff8919fe5a8/docs/ECS0007.md) -ECS0009 | Performance | Info | MinimizeBoxingUnboxingAnalyzer, [Documentation](https://github.com/rjmurillo/EffectiveCSharp.Analyzers/blob/6213cba8473dac61d6132e205550884eae1c94bf/docs/ECS0009.md) -ECS1000 | Performance | Info | SpanAnalyzer, [Documentation](https://github.com/rjmurillo/EffectiveCSharp.Analyzers/blob/d00a4cc9f61e7d5b392894aad859e46c43a5611c/docs/ECS1000.md) \ No newline at end of file +ECS0001 | Style | Info | PreferImplicitlyTypedLocalVariablesAnalyzer, [Documentation](https://github.com/rjmurillo/EffectiveCSharp.Analyzers/blob/e7c151c721c3039011356d6012838f46e4b60a21/docs/rules/ECS0001.md) +ECS0002 | Maintainability | Info | PreferReadonlyOverConstAnalyzer, [Documentation](https://github.com/rjmurillo/EffectiveCSharp.Analyzers/blob/10c2d53afd688efe5a59097f76cb4edf33f6a474/docs/rules/ECS0002.md) +ECS0004 | Style | Info | ReplaceStringFormatAnalyzer, [Documentation](https://github.com/rjmurillo/EffectiveCSharp.Analyzers5da647e447fad4eb0a9e3db287e1d16cce316114/docs/rules/ECS0004.md) +ECS0006 | Refactoring | Info | AvoidStringlyTypedApisAnalyzer, [Documentation](https://github.com/rjmurillo/EffectiveCSharp.Analyzers6213cba8473dac61d6132e205550884eae1c94bf/docs/rules/ECS0006.md) +ECS0007 | Design | Info | ExpressCallbacksWithDelegatesAnalyzer, [Documentation](https://github.com/rjmurillo/EffectiveCSharp.Analyzers/blob/dc194bbde030e9c40d8d9cdb1e0b5ff8919fe5a8/docs/rules/ECS0007.md) +ECS0008 | Usage | Info | EventInvocationAnalyzer, [Documentation](https://github.com/rjmurillo/EffectiveCSharp.Analyzers/blob/cc7d91eb81f6781851c09732db1268be7dab402b/docs/rules/ECS0008.md) +ECS0009 | Performance | Info | MinimizeBoxingUnboxingAnalyzer, [Documentation](https://github.com/rjmurillo/EffectiveCSharp.Analyzers/blob/6213cba8473dac61d6132e205550884eae1c94bf/docs/rules/ECS0009.md) +ECS1000 | Performance | Info | SpanAnalyzer, [Documentation](https://github.com/rjmurillo/EffectiveCSharp.Analyzers/blob/d00a4cc9f61e7d5b392894aad859e46c43a5611c/docs/rules/ECS1000.md) \ No newline at end of file diff --git a/src/EffectiveCSharp.Analyzers/Common/Categories.cs b/src/EffectiveCSharp.Analyzers/Common/Categories.cs index 8556dac..3ce334e 100644 --- a/src/EffectiveCSharp.Analyzers/Common/Categories.cs +++ b/src/EffectiveCSharp.Analyzers/Common/Categories.cs @@ -5,4 +5,5 @@ internal static class Categories internal static readonly string Refactoring = nameof(Refactoring); internal static readonly string Performance = nameof(Performance); internal static readonly string Style = nameof(Style); + internal static readonly string Usage = nameof(Usage); } diff --git a/src/EffectiveCSharp.Analyzers/DiagnosticIds.cs b/src/EffectiveCSharp.Analyzers/Common/DiagnosticIds.cs similarity index 90% rename from src/EffectiveCSharp.Analyzers/DiagnosticIds.cs rename to src/EffectiveCSharp.Analyzers/Common/DiagnosticIds.cs index 1521a23..d1e9665 100644 --- a/src/EffectiveCSharp.Analyzers/DiagnosticIds.cs +++ b/src/EffectiveCSharp.Analyzers/Common/DiagnosticIds.cs @@ -10,6 +10,7 @@ internal static class DiagnosticIds internal const string ReplaceStringFormatWithInterpolatedString = "ECS0004"; internal const string AvoidStringlyTypedApis = "ECS0006"; internal const string ExpressCallbacksWithDelegates = "ECS0007"; + internal const string UseNullConditionalOperatorForEventInvocations = "ECS0008"; internal const string MinimizeBoxingUnboxing = "ECS0009"; internal const string BeAwareOfValueTypeCopyInReferenceTypes = "ECS0009"; internal const string UseSpanInstead = "ECS1000"; diff --git a/src/EffectiveCSharp.Analyzers/EventInvocationAnalyzer.cs b/src/EffectiveCSharp.Analyzers/EventInvocationAnalyzer.cs new file mode 100644 index 0000000..465870b --- /dev/null +++ b/src/EffectiveCSharp.Analyzers/EventInvocationAnalyzer.cs @@ -0,0 +1,120 @@ +namespace EffectiveCSharp.Analyzers; + +/// +/// A for Effective C# Item #8 - Use the Null Conditional Operator for Event Invocations. +/// +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class EventInvocationAnalyzer : DiagnosticAnalyzer +{ + private static readonly string Title = "Use the Null Conditional Operator for Event Invocations"; + private static readonly string MessageFormat = "Use the null-conditional operator to invoke the event '{0}'"; + private static readonly string Description = "Event invocation should use the null-conditional operator to avoid race conditions and improve readability."; + + private static readonly DiagnosticDescriptor Rule = new( + DiagnosticIds.UseNullConditionalOperatorForEventInvocations, + Title, + MessageFormat, + Categories.Usage, + DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: Description, + helpLinkUri: $"https://github.com/rjmurillo/EffectiveCSharp.Analyzers/blob/{ThisAssembly.GitCommitId}/docs/rules/{DiagnosticIds.UseNullConditionalOperatorForEventInvocations}.md"); + + /// + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.IfStatement, SyntaxKind.InvocationExpression); + } + + private static void AnalyzeNode(SyntaxNodeAnalysisContext context) + { + switch (context.Node) + { + case IfStatementSyntax ifStatement: + AnalyzeIfStatement(context, ifStatement); + break; + case InvocationExpressionSyntax invocationExpression: + AnalyzeInvocationExpression(context, invocationExpression); + break; + default: + Debug.Fail("Unknown node type"); + break; + } + } + + private static void AnalyzeInvocationExpression(SyntaxNodeAnalysisContext context, InvocationExpressionSyntax invocationExpression) + { + // Check if the invocation is on an event handler directly + if (invocationExpression.Expression is not IdentifierNameSyntax identifierName) + { + return; + } + + ISymbol? symbol = context.SemanticModel.GetSymbolInfo(identifierName, context.CancellationToken).Symbol; + + if (symbol == null || !IsEventSymbol(symbol)) + { + return; + } + + // Check if the invocation is not within an if statement or null-conditional access + SyntaxNode? parent = invocationExpression.Parent; + while (parent != null) + { + if (parent is IfStatementSyntax { Condition: BinaryExpressionSyntax binaryExpression } + && binaryExpression.IsKind(SyntaxKind.NotEqualsExpression) + && binaryExpression.Left is IdentifierNameSyntax binaryIdentifier + && string.Equals(binaryIdentifier.Identifier.Text, identifierName.Identifier.Text, StringComparison.Ordinal) + && binaryExpression.Right.IsKind(SyntaxKind.NullLiteralExpression)) + { + return; // Safe pattern, exit + } + + if (parent is ConditionalAccessExpressionSyntax) + { + return; // Safe pattern, exit + } + + parent = parent.Parent; + } + + // Report a diagnostic for direct event handler invocation + Diagnostic diagnostic = invocationExpression.GetLocation().CreateDiagnostic(Rule, identifierName.Identifier.Text); + context.ReportDiagnostic(diagnostic); + } + + private static bool IsEventSymbol(ISymbol symbol) + { + return symbol is IFieldSymbol fieldSymbol && fieldSymbol.Type.Name.StartsWith("EventHandler", StringComparison.Ordinal); + } + +#pragma warning disable S125 // Sections of code should not be commented out + private static void AnalyzeIfStatement(SyntaxNodeAnalysisContext context, IfStatementSyntax ifStatement) + { + // Check for patterns like: if (handler != null) handler(args); + if (ifStatement.Condition is not BinaryExpressionSyntax binaryExpression + || !binaryExpression.IsKind(SyntaxKind.NotEqualsExpression) + || binaryExpression.Left is not IdentifierNameSyntax identifierName + || !binaryExpression.Right.IsKind(SyntaxKind.NullLiteralExpression)) + { + return; + } + + StatementSyntax statement = ifStatement.Statement; + if (statement is not ExpressionStatementSyntax { Expression: InvocationExpressionSyntax { Expression: IdentifierNameSyntax invocationIdentifier } } + || !string.Equals(invocationIdentifier.Identifier.Text, identifierName.Identifier.Text, StringComparison.Ordinal)) + { + return; + } + + Diagnostic diagnostic = ifStatement.GetLocation().CreateDiagnostic(Rule, identifierName.Identifier.Text); + context.ReportDiagnostic(diagnostic); + } +#pragma warning restore S125 // Sections of code should not be commented out +} diff --git a/src/EffectiveCSharp.Analyzers/EventInvocationCodeFixProvider.cs b/src/EffectiveCSharp.Analyzers/EventInvocationCodeFixProvider.cs new file mode 100644 index 0000000..e903717 --- /dev/null +++ b/src/EffectiveCSharp.Analyzers/EventInvocationCodeFixProvider.cs @@ -0,0 +1,160 @@ +namespace EffectiveCSharp.Analyzers; + +/// +/// A that provides a code fix for the . +/// +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(EventInvocationCodeFixProvider))] +[Shared] +public class EventInvocationCodeFixProvider : CodeFixProvider +{ + private static readonly string Title = "Use null-conditional operator"; + + /// + public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(DiagnosticIds.UseNullConditionalOperatorForEventInvocations); + + /// + public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + /// + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + Diagnostic diagnostic = context.Diagnostics[0]; + TextSpan diagnosticSpan = diagnostic.Location.SourceSpan; + SyntaxNode? node = root?.FindNode(diagnosticSpan); + InvocationExpressionSyntax? invocationExpression = node?.FirstAncestorOrSelf(); + IfStatementSyntax? ifStatement = node?.FirstAncestorOrSelf(); + + if (invocationExpression != null) + { + context.RegisterCodeFix( + CodeAction.Create( + title: Title, + createChangedDocument: c => UseNullConditionalOperatorAsync(context.Document, invocationExpression, c), + equivalenceKey: Title), + diagnostic); + } + else if (ifStatement != null) + { + context.RegisterCodeFix( + CodeAction.Create( + title: Title, + createChangedDocument: c => UseNullConditionalOperatorAsync(context.Document, ifStatement, c), + equivalenceKey: Title), + diagnostic); + } + } + + private static async Task UseNullConditionalOperatorAsync(Document document, InvocationExpressionSyntax invocationExpression, CancellationToken cancellationToken) + { + DocumentEditor? editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + IdentifierNameSyntax identifierName = (IdentifierNameSyntax)invocationExpression.Expression; + ConditionalAccessExpressionSyntax newInvocation = SyntaxFactory.ConditionalAccessExpression( + identifierName, + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberBindingExpression(SyntaxFactory.IdentifierName("Invoke")), + invocationExpression.ArgumentList)); + + editor.ReplaceNode(invocationExpression, newInvocation); + return editor.GetChangedDocument(); + } + +#pragma warning disable MA0051 // Method is too long +#pragma warning disable S125 // Remove commented out code + private static async Task UseNullConditionalOperatorAsync(Document document, IfStatementSyntax ifStatement, CancellationToken cancellationToken) + { + DocumentEditor? editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + + // Check if the if statement follows the pattern: if (handler != null) handler(this, counter); + ExpressionStatementSyntax? expressionStatement = ifStatement.Statement as ExpressionStatementSyntax; + + if (expressionStatement?.Expression is not InvocationExpressionSyntax invocationExpression) + { + return document; + } + + // Identify the event handler variable (handler in this case) + IdentifierNameSyntax? identifierName = invocationExpression.Expression as IdentifierNameSyntax + ?? (invocationExpression.Expression as MemberAccessExpressionSyntax)?.Expression as IdentifierNameSyntax; + + if (identifierName == null) + { + return document; + } + + // Attempt to find a preceding variable declaration if it exists + BlockSyntax? blockSyntax = ifStatement.Parent as BlockSyntax; + LocalDeclarationStatementSyntax? variableDeclaration = null; + + if (blockSyntax?.Statements != null) + { + for (int i = 0; i < blockSyntax.Statements.Count; i++) + { + StatementSyntax statement = blockSyntax.Statements[i]; + if (statement is LocalDeclarationStatementSyntax localDeclaration) + { + for (int j = 0; j < localDeclaration.Declaration.Variables.Count; j++) + { + VariableDeclaratorSyntax variable = localDeclaration.Declaration.Variables[j]; + if (string.Equals( + variable.Identifier.Text, + identifierName.Identifier.Text, + StringComparison.Ordinal)) + { + variableDeclaration = localDeclaration; + break; + } + } + } + + if (variableDeclaration != null) + { + break; + } + } + } + + if (variableDeclaration == null) + { + // If there is no preceding variable declaration, replace the if statement directly + ExpressionStatementSyntax newInvocation = SyntaxFactory.ExpressionStatement( + SyntaxFactory.ConditionalAccessExpression( + identifierName, + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberBindingExpression(SyntaxFactory.IdentifierName("Invoke")), + invocationExpression.ArgumentList))) + .WithLeadingTrivia(ifStatement.GetLeadingTrivia()) + .WithTrailingTrivia(ifStatement.GetTrailingTrivia()); + + editor.ReplaceNode(ifStatement, newInvocation); + return editor.GetChangedDocument(); + } + + // Get the actual event handler (e.g., Updated) + if (variableDeclaration.Declaration.Variables.First().Initializer?.Value is IdentifierNameSyntax eventHandlerIdentifier) + { + // Create the null-conditional invocation using the event handler directly + ExpressionStatementSyntax newInvocation = SyntaxFactory.ExpressionStatement( + SyntaxFactory.ConditionalAccessExpression( + eventHandlerIdentifier, + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberBindingExpression(SyntaxFactory.IdentifierName("Invoke")), + invocationExpression.ArgumentList))) + .WithLeadingTrivia(ifStatement.GetLeadingTrivia()) + .WithTrailingTrivia(ifStatement.GetTrailingTrivia()); + + // Remove the variable declaration + editor.RemoveNode(variableDeclaration); + + // Replace the if statement with the new invocation + editor.ReplaceNode(ifStatement, newInvocation); + + return editor.GetChangedDocument(); + } + + return document; + } +#pragma warning restore S125 // Removed commented out code +#pragma warning restore MA0051 // Method is too long +} diff --git a/src/EffectiveCSharp.Analyzers/GlobalUsings.cs b/src/EffectiveCSharp.Analyzers/GlobalUsings.cs index 10e9b57..a43bd92 100644 --- a/src/EffectiveCSharp.Analyzers/GlobalUsings.cs +++ b/src/EffectiveCSharp.Analyzers/GlobalUsings.cs @@ -8,5 +8,6 @@ global using Microsoft.CodeAnalysis.CSharp; global using Microsoft.CodeAnalysis.CSharp.Syntax; global using Microsoft.CodeAnalysis.Diagnostics; +global using Microsoft.CodeAnalysis.Editing; global using Microsoft.CodeAnalysis.Operations; global using Microsoft.CodeAnalysis.Text; diff --git a/src/EffectiveCSharp.Analyzers/MinimizeBoxingUnboxingAnalyzer.cs b/src/EffectiveCSharp.Analyzers/MinimizeBoxingUnboxingAnalyzer.cs index b6bcaee..2198460 100644 --- a/src/EffectiveCSharp.Analyzers/MinimizeBoxingUnboxingAnalyzer.cs +++ b/src/EffectiveCSharp.Analyzers/MinimizeBoxingUnboxingAnalyzer.cs @@ -123,7 +123,7 @@ private static void AnalyzeOperation(OperationAnalysisContext context) private static void AnalyzeConversionOperation(IConversionOperation conversionOperation, OperationAnalysisContext context) { - // We need to detect when a conversion operation should indeed considered to be problematic: + // We need to detect when a conversion operation should indeed be considered to be problematic: // // 1. Excluding safe operations: Some conversions might be misidentified as boxing when they are safe or optimized // away by the compiler, such as converting between numeric types or appending integers to strings which only involves @@ -131,7 +131,7 @@ private static void AnalyzeConversionOperation(IConversionOperation conversionOp // 2. Context-Sensitive: Depending on the context of the conversion (like within a string concatenation), it might be // considered boxing. Analyzing the parent operations or the usage context is needed to decide to trigger a boxing warning // 3. Specific type checks: before reporting boxing, verify the types involved in the conversion are not special cases - // that are handled differently, like enum to string conversions in switch statements or using `int` in `string` concatentation + // that are handled differently, like enum to string conversions in switch statements or using `int` in `string` concatenation // Check if the conversion explicitly involves boxing or unboxing if (!conversionOperation.IsBoxingOrUnboxingOperation()) diff --git a/src/tools/Dogfood/SquiggleCop.Baseline.yaml b/src/tools/Dogfood/SquiggleCop.Baseline.yaml index 3e0a9a6..0af4a27 100644 --- a/src/tools/Dogfood/SquiggleCop.Baseline.yaml +++ b/src/tools/Dogfood/SquiggleCop.Baseline.yaml @@ -303,6 +303,7 @@ - {Id: ECS0004, Title: Replace string.Format with interpolated string, Category: Style, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: ECS0006, Title: Avoid stringly-typed APIs, Category: Refactoring, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: ECS0007, Title: Express callbacks with delegates, Category: Design, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} +- {Id: ECS0008, Title: Use the Null Conditional Operator for Event Invocations, Category: Usage, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: ECS0009, Title: Minimize boxing and unboxing, Category: Performance, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: ECS1000, Title: Use Span for performance, Category: Performance, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} - {Id: EM0001, Title: Switch on Enum Not Exhaustive, Category: Logic, DefaultSeverity: Error, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} diff --git a/tests/.editorconfig b/tests/.editorconfig index 2f43152..6b85056 100644 --- a/tests/.editorconfig +++ b/tests/.editorconfig @@ -16,3 +16,23 @@ dotnet_diagnostic.CS1712.severity = suggestion # VSTHRD200: Use "Async" suffix for async methods # Just about every test method is async, doesn't provide any real value and clustters up test window dotnet_diagnostic.VSTHRD200.severity = none + +# Static members are grouped with their Theory +dotnet_diagnostic.SA1204.severity = none + +# The harness has literal code as a string +dotnet_diagnostic.SA1001.severity = none + +dotnet_diagnostic.SA1113.severity = none + +# There's code in comments as examples +dotnet_diagnostic.S125.severity = none + +# Some test methods are "too long" +dotnet_diagnostic.MA0051.severity = none + +# Multiple types of tests are defined in TheoryData +dotnet_diagnostic.MA0007.severity = none + +# The harness uses async and await +dotnet_diagnostic.AsyncFixer01.severity = none \ No newline at end of file diff --git a/tests/EffectiveCSharp.Analyzers.Benchmarks/SquiggleCop.Baseline.yaml b/tests/EffectiveCSharp.Analyzers.Benchmarks/SquiggleCop.Baseline.yaml index 2ae0ea5..558190c 100644 --- a/tests/EffectiveCSharp.Analyzers.Benchmarks/SquiggleCop.Baseline.yaml +++ b/tests/EffectiveCSharp.Analyzers.Benchmarks/SquiggleCop.Baseline.yaml @@ -426,7 +426,7 @@ - {Id: MA0004, Title: Use Task.ConfigureAwait, Category: Usage, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: MA0005, Title: Use Array.Empty(), Category: Performance, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: MA0006, Title: Use String.Equals instead of equality operator, Category: Usage, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} -- {Id: MA0007, Title: Add a comma after the last value, Category: Style, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} +- {Id: MA0007, Title: Add a comma after the last value, Category: Style, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note, None], IsEverSuppressed: true} - {Id: MA0008, Title: Add StructLayoutAttribute, Category: Performance, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: MA0009, Title: Add regex evaluation timeout, Category: Security, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: MA0010, Title: Mark attributes with AttributeUsageAttribute, Category: Design, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} @@ -469,7 +469,7 @@ - {Id: MA0048, Title: File name must match type name, Category: Design, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: MA0049, Title: Type name should not match containing namespace, Category: Design, DefaultSeverity: Error, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: MA0050, Title: Validate arguments correctly in iterator methods, Category: Design, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} -- {Id: MA0051, Title: Method is too long, Category: Design, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} +- {Id: MA0051, Title: Method is too long, Category: Design, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error, None], IsEverSuppressed: true} - {Id: MA0052, Title: Replace constant Enum.ToString with nameof, Category: Performance, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} - {Id: MA0053, Title: Make class sealed, Category: Design, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} - {Id: MA0054, Title: Embed the caught exception as innerException, Category: Design, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} @@ -921,7 +921,7 @@ - {Id: S1226, Title: "Method parameters, caught exceptions and foreach variables' initial values should not be ignored", Category: Minor Bug, DefaultSeverity: Warning, IsEnabledByDefault: false, EffectiveSeverities: [None], IsEverSuppressed: true} - {Id: S1227, Title: break statements should not be used except for switch cases, Category: Minor Code Smell, DefaultSeverity: Warning, IsEnabledByDefault: false, EffectiveSeverities: [None], IsEverSuppressed: true} - {Id: S1244, Title: Floating point numbers should not be tested for equality, Category: Major Bug, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} -- {Id: S125, Title: Sections of code should not be commented out, Category: Major Code Smell, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} +- {Id: S125, Title: Sections of code should not be commented out, Category: Major Code Smell, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error, None], IsEverSuppressed: true} - {Id: S126, Title: '"if ... else if" constructs should end with "else" clauses', Category: Critical Code Smell, DefaultSeverity: Warning, IsEnabledByDefault: false, EffectiveSeverities: [None], IsEverSuppressed: true} - {Id: S1264, Title: A "while" loop should be used instead of a "for" loop, Category: Minor Code Smell, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: S127, Title: '"for" loop stop conditions should be invariant', Category: Major Code Smell, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} @@ -1343,7 +1343,7 @@ - {Id: SA0001, Title: XML comment analysis disabled, Category: StyleCop.CSharp.SpecialRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: SA0002, Title: Invalid settings file, Category: StyleCop.CSharp.SpecialRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: SA1000, Title: Keywords should be spaced correctly, Category: StyleCop.CSharp.SpacingRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} -- {Id: SA1001, Title: Commas should be spaced correctly, Category: StyleCop.CSharp.SpacingRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} +- {Id: SA1001, Title: Commas should be spaced correctly, Category: StyleCop.CSharp.SpacingRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error, None], IsEverSuppressed: true} - {Id: SA1002, Title: Semicolons should be spaced correctly, Category: StyleCop.CSharp.SpacingRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: SA1003, Title: Symbols should be spaced correctly, Category: StyleCop.CSharp.SpacingRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: SA1004, Title: Documentation lines should begin with single space, Category: StyleCop.CSharp.SpacingRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} @@ -1384,7 +1384,7 @@ - {Id: SA1110, Title: Opening parenthesis or bracket should be on declaration line, Category: StyleCop.CSharp.ReadabilityRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: SA1111, Title: Closing parenthesis should be on line of last parameter, Category: StyleCop.CSharp.ReadabilityRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: SA1112, Title: Closing parenthesis should be on line of opening parenthesis, Category: StyleCop.CSharp.ReadabilityRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} -- {Id: SA1113, Title: Comma should be on the same line as previous parameter, Category: StyleCop.CSharp.ReadabilityRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} +- {Id: SA1113, Title: Comma should be on the same line as previous parameter, Category: StyleCop.CSharp.ReadabilityRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error, None], IsEverSuppressed: true} - {Id: SA1114, Title: Parameter list should follow declaration, Category: StyleCop.CSharp.ReadabilityRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: SA1115, Title: Parameter should follow comma, Category: StyleCop.CSharp.ReadabilityRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: SA1116, Title: Split parameters should start on line after declaration, Category: StyleCop.CSharp.ReadabilityRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} @@ -1417,7 +1417,7 @@ - {Id: SA1201, Title: Elements should appear in the correct order, Category: StyleCop.CSharp.OrderingRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {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} +- {Id: SA1204, Title: Static elements should appear before instance elements, Category: StyleCop.CSharp.OrderingRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error, None], IsEverSuppressed: true} - {Id: SA1205, Title: Partial elements should declare access, Category: StyleCop.CSharp.OrderingRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: SA1206, Title: Declaration keywords should follow order, Category: StyleCop.CSharp.OrderingRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: SA1207, Title: Protected should come before internal, Category: StyleCop.CSharp.OrderingRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} diff --git a/tests/EffectiveCSharp.Analyzers.Tests/EventInvocationTests.cs b/tests/EffectiveCSharp.Analyzers.Tests/EventInvocationTests.cs new file mode 100644 index 0000000..c9d0c8b --- /dev/null +++ b/tests/EffectiveCSharp.Analyzers.Tests/EventInvocationTests.cs @@ -0,0 +1,183 @@ +using CodeFixVerifier = EffectiveCSharp.Analyzers.Tests.Helpers.AnalyzerAndCodeFixVerifier; +using Verifier = EffectiveCSharp.Analyzers.Tests.Helpers.AnalyzerVerifier; + +namespace EffectiveCSharp.Analyzers.Tests; + +#pragma warning disable SA1204 // Static members are grouped with their Theory +#pragma warning disable SA1001 // The harness has literal code as a string, which can be weirdly formatted +#pragma warning disable SA1113 +#pragma warning disable S125 // There's code in comments as examples +#pragma warning disable MA0051 // Some test methods are "too long" +#pragma warning disable MA0007 // There are multiple types of tests defined in theory data +#pragma warning disable IDE0028 // We cannot simply object creation on TheoryData because we need to convert from object[] to string, the way it is now is cleaner +#pragma warning disable AsyncFixer01 + +public class EventInvocationTests +{ + public static TheoryData TestData() + { + TheoryData data = new() + { + // When no event handlers have been attached to the Updated event + // the code will throw NRE + """ + {|ECS0008:Updated(this, counter)|}; + """, + + // Developers need to wrap any event invocation in a check + // This code has a bug: the null check can pass but the event + // can be unsubscribed by another thread, and you still get NRE + """ + {|ECS0008:if (Updated != null) + Updated(this, counter);|} + """, + + // This code is correct in earlier versions of C# + """ + var handler = Updated; + {|ECS0008:if (handler != null) + handler(this, counter);|} + """, + + // Can be simplified to a single line with Null conditional operators reduce cognitive overhead + "Updated?.Invoke(this, counter);", + }; + +#pragma warning disable MA0002 // IEqualityComparer or IComparer is missing + return data.WithReferenceAssemblyGroups(p => ReferenceAssemblyCatalog.DotNetCore.Contains(p)); +#pragma warning restore MA0002 // IEqualityComparer or IComparer is missing + } + + [Theory] + [MemberData(nameof(TestData))] + public async Task Analyzer(string referenceAssemblyGroup, string code) + { + await Verifier.VerifyAnalyzerAsync( + $$""" + public class EventSource + { + private EventHandler Updated; + private int counter; + + public void RaiseUpdates() + { + counter++; + {{code}} + } + } + """, + referenceAssemblyGroup); + } + + // The test cases are broken out as Facts instead of a Theory because the white space is super fiddly + [Fact] + public async Task Existing_Known_Good_Pattern() + { + const string testCode = + """ + public class EventSource + { + private EventHandler Updated; + private int counter; + + public void RaiseUpdates() + { + counter++; + var handler = Updated; + {|ECS0008:if (handler != null) + handler(this, counter);|} + } + } + """; + + const string fixedCode = + """ + public class EventSource + { + private EventHandler Updated; + private int counter; + + public void RaiseUpdates() + { + counter++; + Updated?.Invoke(this, counter); + } + } + """; + + await CodeFixVerifier.VerifyCodeFixAsync(testCode, fixedCode, ReferenceAssemblyCatalog.Latest); + } + + [Fact] + public async Task Checking_for_null_against_the_EventHandler_directly() + { + const string testCode = + """ + public class EventSource + { + private EventHandler Updated; + private int counter; + + public void RaiseUpdates() + { + counter++; + {|ECS0008:if (Updated != null) + Updated(this, counter);|} + } + } + """; + + const string fixedCode = + """ + public class EventSource + { + private EventHandler Updated; + private int counter; + + public void RaiseUpdates() + { + counter++; + Updated?.Invoke(this, counter); + } + } + """; + + await CodeFixVerifier.VerifyCodeFixAsync(testCode, fixedCode, ReferenceAssemblyCatalog.Latest); + } + + [Fact] + public async Task Calling_the_event_handler_directly() + { + const string testCode = + """ + public class EventSource + { + private EventHandler Updated; + private int counter; + + public void RaiseUpdates() + { + counter++; + {|ECS0008:Updated(this, counter)|}; + } + } + """; + + const string fixedCode = + """ + public class EventSource + { + private EventHandler Updated; + private int counter; + + public void RaiseUpdates() + { + counter++; + Updated?.Invoke(this, counter); + } + } + """; + + await CodeFixVerifier.VerifyCodeFixAsync(testCode, fixedCode, ReferenceAssemblyCatalog.Latest); + } +} diff --git a/tests/EffectiveCSharp.Analyzers.Tests/SquiggleCop.Baseline.yaml b/tests/EffectiveCSharp.Analyzers.Tests/SquiggleCop.Baseline.yaml index 7f5640c..2cc4f16 100644 --- a/tests/EffectiveCSharp.Analyzers.Tests/SquiggleCop.Baseline.yaml +++ b/tests/EffectiveCSharp.Analyzers.Tests/SquiggleCop.Baseline.yaml @@ -426,7 +426,7 @@ - {Id: MA0004, Title: Use Task.ConfigureAwait, Category: Usage, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: MA0005, Title: Use Array.Empty(), Category: Performance, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: MA0006, Title: Use String.Equals instead of equality operator, Category: Usage, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} -- {Id: MA0007, Title: Add a comma after the last value, Category: Style, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} +- {Id: MA0007, Title: Add a comma after the last value, Category: Style, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note, None], IsEverSuppressed: true} - {Id: MA0008, Title: Add StructLayoutAttribute, Category: Performance, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: MA0009, Title: Add regex evaluation timeout, Category: Security, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: MA0010, Title: Mark attributes with AttributeUsageAttribute, Category: Design, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} @@ -469,7 +469,7 @@ - {Id: MA0048, Title: File name must match type name, Category: Design, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: true} - {Id: MA0049, Title: Type name should not match containing namespace, Category: Design, DefaultSeverity: Error, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: MA0050, Title: Validate arguments correctly in iterator methods, Category: Design, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} -- {Id: MA0051, Title: Method is too long, Category: Design, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: true} +- {Id: MA0051, Title: Method is too long, Category: Design, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error, None], IsEverSuppressed: true} - {Id: MA0052, Title: Replace constant Enum.ToString with nameof, Category: Performance, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} - {Id: MA0053, Title: Make class sealed, Category: Design, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false} - {Id: MA0054, Title: Embed the caught exception as innerException, Category: Design, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} @@ -921,7 +921,7 @@ - {Id: S1226, Title: "Method parameters, caught exceptions and foreach variables' initial values should not be ignored", Category: Minor Bug, DefaultSeverity: Warning, IsEnabledByDefault: false, EffectiveSeverities: [None], IsEverSuppressed: true} - {Id: S1227, Title: break statements should not be used except for switch cases, Category: Minor Code Smell, DefaultSeverity: Warning, IsEnabledByDefault: false, EffectiveSeverities: [None], IsEverSuppressed: true} - {Id: S1244, Title: Floating point numbers should not be tested for equality, Category: Major Bug, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} -- {Id: S125, Title: Sections of code should not be commented out, Category: Major Code Smell, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: true} +- {Id: S125, Title: Sections of code should not be commented out, Category: Major Code Smell, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error, None], IsEverSuppressed: true} - {Id: S126, Title: '"if ... else if" constructs should end with "else" clauses', Category: Critical Code Smell, DefaultSeverity: Warning, IsEnabledByDefault: false, EffectiveSeverities: [None], IsEverSuppressed: true} - {Id: S1264, Title: A "while" loop should be used instead of a "for" loop, Category: Minor Code Smell, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: S127, Title: '"for" loop stop conditions should be invariant', Category: Major Code Smell, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} @@ -1343,7 +1343,7 @@ - {Id: SA0001, Title: XML comment analysis disabled, Category: StyleCop.CSharp.SpecialRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: SA0002, Title: Invalid settings file, Category: StyleCop.CSharp.SpecialRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: SA1000, Title: Keywords should be spaced correctly, Category: StyleCop.CSharp.SpacingRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} -- {Id: SA1001, Title: Commas should be spaced correctly, Category: StyleCop.CSharp.SpacingRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: true} +- {Id: SA1001, Title: Commas should be spaced correctly, Category: StyleCop.CSharp.SpacingRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error, None], IsEverSuppressed: true} - {Id: SA1002, Title: Semicolons should be spaced correctly, Category: StyleCop.CSharp.SpacingRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: SA1003, Title: Symbols should be spaced correctly, Category: StyleCop.CSharp.SpacingRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: SA1004, Title: Documentation lines should begin with single space, Category: StyleCop.CSharp.SpacingRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} @@ -1384,7 +1384,7 @@ - {Id: SA1110, Title: Opening parenthesis or bracket should be on declaration line, Category: StyleCop.CSharp.ReadabilityRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: SA1111, Title: Closing parenthesis should be on line of last parameter, Category: StyleCop.CSharp.ReadabilityRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: SA1112, Title: Closing parenthesis should be on line of opening parenthesis, Category: StyleCop.CSharp.ReadabilityRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} -- {Id: SA1113, Title: Comma should be on the same line as previous parameter, Category: StyleCop.CSharp.ReadabilityRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: true} +- {Id: SA1113, Title: Comma should be on the same line as previous parameter, Category: StyleCop.CSharp.ReadabilityRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error, None], IsEverSuppressed: true} - {Id: SA1114, Title: Parameter list should follow declaration, Category: StyleCop.CSharp.ReadabilityRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: SA1115, Title: Parameter should follow comma, Category: StyleCop.CSharp.ReadabilityRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: true} - {Id: SA1116, Title: Split parameters should start on line after declaration, Category: StyleCop.CSharp.ReadabilityRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} @@ -1417,7 +1417,7 @@ - {Id: SA1201, Title: Elements should appear in the correct order, Category: StyleCop.CSharp.OrderingRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {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: true} +- {Id: SA1204, Title: Static elements should appear before instance elements, Category: StyleCop.CSharp.OrderingRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error, None], IsEverSuppressed: true} - {Id: SA1205, Title: Partial elements should declare access, Category: StyleCop.CSharp.OrderingRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: SA1206, Title: Declaration keywords should follow order, Category: StyleCop.CSharp.OrderingRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false} - {Id: SA1207, Title: Protected should come before internal, Category: StyleCop.CSharp.OrderingRules, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false}