diff --git a/CHANGELOG.md b/CHANGELOG.md index 9739439..df895f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.3.2 +- Added alternative CodeFix for `CI0008` (ArrayContainsToHashSet) allowing replacement of arrays with `FrozenSet` (`System.Collections.Frozen`) when supported by the target .NET version (.NET 8+). +- Added support for C# 12+ Collection Expressions (`[item1, item2, ...]`) across analyzers and code fixes: + - `CI0008` (ArrayContainsToHashSet): Support array declarations initialized with collection expressions when detecting `Contains()` calls, and preserving collection expression syntax in code fixes. + - `CI0006` (ListInitializer): Updated collection element counting and initializer handling. + - `ExpressionExtensions`: Added `FindCollectionInitializer` and `GetCollectionElementCount` to support modern syntax. + ## 0.3.1 - Fixed `ArgumentException` (An item with the same key has already been added) in `AnalyzerConfigHelper` due to race condition when accessing configuration cache from multiple threads. diff --git a/Collections.Analyzer.CodeFixes/ArrayContainsToFrozenSetCodeFix.cs b/Collections.Analyzer.CodeFixes/ArrayContainsToFrozenSetCodeFix.cs new file mode 100644 index 0000000..0264d7b --- /dev/null +++ b/Collections.Analyzer.CodeFixes/ArrayContainsToFrozenSetCodeFix.cs @@ -0,0 +1,273 @@ +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Collections.Analyzer.Diagnostics; +using Collections.Analyzer.Diagnostics.CI0008; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Formatting; + +namespace Collections.Analyzer.CodeFixes; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ArrayContainsToFrozenSetCodeFix)), Shared] +public class ArrayContainsToFrozenSetCodeFix : CodeFixProvider +{ + public sealed override ImmutableArray FixableDiagnosticIds => [ArrayContainsToHashSetDiagnostic.Rule.Id]; + + public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); + if (semanticModel == null || !semanticModel.Compilation.SupportsFrozenSet()) + return; + + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var diagnostic = context.Diagnostics.First(); + var diagnosticSpan = diagnostic.Location.SourceSpan; + + var token = root?.FindToken(diagnosticSpan.Start); + if (!token.HasValue) + return; + + // fields and local variables + var variableDeclarator = token.Value.Parent?.FirstAncestorOrSelf(); + if (variableDeclarator != null) + { + context.RegisterCodeFix( + CodeAction.Create( + title: Resources.CI0008_FrozenSetCodeFixTitle, + createChangedDocument: c => ReplaceArrayWithFrozenSetInVariable(context.Document, variableDeclarator, c), + equivalenceKey: nameof(ArrayContainsToFrozenSetCodeFix)), + diagnostic); + return; + } + + // properties + var propertyDeclaration = token.Value.Parent?.FirstAncestorOrSelf(); + if (propertyDeclaration != null) + { + context.RegisterCodeFix( + CodeAction.Create( + title: Resources.CI0008_FrozenSetCodeFixTitle, + createChangedDocument: c => ReplaceArrayWithFrozenSetInProperty(context.Document, propertyDeclaration, c), + equivalenceKey: nameof(ArrayContainsToFrozenSetCodeFix)), + diagnostic); + } + } + + private static async Task ReplaceArrayWithFrozenSetInVariable( + Document document, + VariableDeclaratorSyntax variableDeclarator, + CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + if (root == null || semanticModel == null) + return document; + + var variableDeclaration = variableDeclarator.Parent as VariableDeclarationSyntax; + if (variableDeclaration == null) + return document; + + var typeInfo = semanticModel.GetTypeInfo(variableDeclaration.Type, cancellationToken); + if (typeInfo.Type is not IArrayTypeSymbol arrayType) + return document; + + var elementType = arrayType.ElementType; + var elementTypeName = elementType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); + + // get new type + TypeSyntax newType; + var isFieldDeclaration = variableDeclaration.Parent is FieldDeclarationSyntax; + + if (variableDeclaration.Type.IsVar && !isFieldDeclaration && variableDeclarator.Initializer?.Value is not CollectionExpressionSyntax) + { + // user var + newType = variableDeclaration.Type; + } + else + { + newType = CreateFrozenSetType(elementTypeName); + } + + var newInitializer = CreateFrozenSetInitializer(variableDeclarator.Initializer, elementTypeName); + if (newInitializer == null) + return document; + + var newVariableDeclarator = variableDeclarator.WithInitializer(newInitializer); + + var newVariables = variableDeclaration.Variables.Replace(variableDeclarator, newVariableDeclarator); + + var newVariableDeclaration = variableDeclaration + .WithType(newType) + .WithVariables(newVariables) + .WithAdditionalAnnotations(Formatter.Annotation); + + var newRoot = root.ReplaceNode(variableDeclaration, newVariableDeclaration); + + newRoot = AddUsingDirective(newRoot); + + return document.WithSyntaxRoot(newRoot); + } + + private static async Task ReplaceArrayWithFrozenSetInProperty( + Document document, + PropertyDeclarationSyntax propertyDeclaration, + CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + if (root == null || semanticModel == null) + return document; + + // Get the array element type + var typeInfo = semanticModel.GetTypeInfo(propertyDeclaration.Type, cancellationToken); + if (typeInfo.Type is not IArrayTypeSymbol arrayType) + return document; + + var elementType = arrayType.ElementType; + var elementTypeName = elementType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); + + var newType = CreateFrozenSetType(elementTypeName); + + // Create a new initializer + var newInitializer = CreateFrozenSetInitializer(propertyDeclaration.Initializer, elementTypeName); + if (newInitializer == null) + return document; + + // Create a new property declaration + var newPropertyDeclaration = propertyDeclaration + .WithType(newType) + .WithInitializer(newInitializer) + .WithAdditionalAnnotations(Formatter.Annotation); + + var newRoot = root.ReplaceNode(propertyDeclaration, newPropertyDeclaration); + + // Add using System.Collections.Frozen + newRoot = AddUsingDirective(newRoot); + + return document.WithSyntaxRoot(newRoot); + } + + private static EqualsValueClauseSyntax? CreateFrozenSetInitializer( + EqualsValueClauseSyntax? originalInitializer, + string elementTypeName) + { + if (originalInitializer?.Value == null) + return null; + + ExpressionSyntax frozenSetCreation; + + if (originalInitializer.Value is InitializerExpressionSyntax initializerExpression) + { + // Case: int[] arr = { 1, 2, 3 }; + // Transform to: FrozenSet arr = new[] { 1, 2, 3 }.ToFrozenSet(); + var implicitArrayCreation = SyntaxFactory.ImplicitArrayCreationExpression(initializerExpression); + var memberAccess = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + implicitArrayCreation, + SyntaxFactory.IdentifierName("ToFrozenSet") + ); + frozenSetCreation = SyntaxFactory.InvocationExpression(memberAccess) + .WithAdditionalAnnotations(Formatter.Annotation); + } + else if (originalInitializer.Value is CollectionExpressionSyntax collectionExpression) + { + if (collectionExpression.Elements.Any(e => e is SpreadElementSyntax)) + { + // Case: int[] arr = [..other, 1, 2, 3]; + // Transform to: FrozenSet arr = ((int[])[..other, 1, 2, 3]).ToFrozenSet(); + var arrayType = SyntaxFactory.ParseTypeName($"{elementTypeName}[]"); + var castExpression = SyntaxFactory.CastExpression(arrayType, collectionExpression); + var parenthesized = SyntaxFactory.ParenthesizedExpression(castExpression); + var memberAccess = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + parenthesized, + SyntaxFactory.IdentifierName("ToFrozenSet") + ); + frozenSetCreation = SyntaxFactory.InvocationExpression(memberAccess) + .WithAdditionalAnnotations(Formatter.Annotation); + } + else + { + // Case: int[] arr = [ 1, 2, 3 ]; + // Transform to: FrozenSet arr = new[] { 1, 2, 3 }.ToFrozenSet(); + var expressions = collectionExpression.Elements + .OfType() + .Select(e => e.Expression); + var initializer = SyntaxFactory.InitializerExpression( + SyntaxKind.ArrayInitializerExpression, + SyntaxFactory.SeparatedList(expressions) + ); + var implicitArrayCreation = SyntaxFactory.ImplicitArrayCreationExpression(initializer); + var memberAccess = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + implicitArrayCreation, + SyntaxFactory.IdentifierName("ToFrozenSet") + ); + frozenSetCreation = SyntaxFactory.InvocationExpression(memberAccess) + .WithAdditionalAnnotations(Formatter.Annotation); + } + } + else + { + // Case: int[] arr = new int[] { 1, 2, 3 }; or var arr = new[] { 1, 2, 3 }; + // Transform to: FrozenSet arr = new int[] { 1, 2, 3 }.ToFrozenSet(); + var memberAccess = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + originalInitializer.Value, + SyntaxFactory.IdentifierName("ToFrozenSet") + ); + frozenSetCreation = SyntaxFactory.InvocationExpression(memberAccess) + .WithAdditionalAnnotations(Formatter.Annotation); + } + + return SyntaxFactory.EqualsValueClause(frozenSetCreation) + .WithAdditionalAnnotations(Formatter.Annotation); + } + + private static GenericNameSyntax CreateFrozenSetType(string elementTypeName) + { + return SyntaxFactory.GenericName( + SyntaxFactory.Identifier("FrozenSet"), + SyntaxFactory.TypeArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.ParseTypeName(elementTypeName) + ) + ) + ); + } + + private static SyntaxNode AddUsingDirective(SyntaxNode root) + { + if (root is not CompilationUnitSyntax compilationUnit) + return root; + + var hasUsing = compilationUnit.DescendantNodesAndSelf() + .OfType() + .Any(u => u.Name?.ToString() == "System.Collections.Frozen"); + + if (hasUsing) + return root; + + var systemCollectionsFrozen = SyntaxFactory.UsingDirective( + SyntaxFactory.QualifiedName( + SyntaxFactory.QualifiedName( + SyntaxFactory.IdentifierName("System"), + SyntaxFactory.IdentifierName("Collections") + ), + SyntaxFactory.IdentifierName("Frozen") + ) + ); + + return compilationUnit.AddUsings(systemCollectionsFrozen); + } +} diff --git a/Collections.Analyzer.CodeFixes/ArrayContainsToHashSetCodeFix.cs b/Collections.Analyzer.CodeFixes/ArrayContainsToHashSetCodeFix.cs index 5833314..896679c 100644 --- a/Collections.Analyzer.CodeFixes/ArrayContainsToHashSetCodeFix.cs +++ b/Collections.Analyzer.CodeFixes/ArrayContainsToHashSetCodeFix.cs @@ -16,8 +16,7 @@ namespace Collections.Analyzer.CodeFixes; [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ArrayContainsToHashSetCodeFix)), Shared] public class ArrayContainsToHashSetCodeFix : CodeFixProvider { - public sealed override ImmutableArray FixableDiagnosticIds => - ImmutableArray.Create(ArrayContainsToHashSetDiagnostic.Rule.Id); + public sealed override ImmutableArray FixableDiagnosticIds => [ArrayContainsToHashSetDiagnostic.Rule.Id]; public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; @@ -99,11 +98,11 @@ private static async Task ReplaceArrayWithHashSetInVariable( var newVariableDeclarator = variableDeclarator.WithInitializer(newInitializer); + var newVariables = variableDeclaration.Variables.Replace(variableDeclarator, newVariableDeclarator); + var newVariableDeclaration = variableDeclaration .WithType(newType) - .WithVariables( - SyntaxFactory.SingletonSeparatedList(newVariableDeclarator) - ) + .WithVariables(newVariables) .WithAdditionalAnnotations(Formatter.Annotation); var newRoot = root.ReplaceNode(variableDeclaration, newVariableDeclaration); @@ -168,10 +167,20 @@ private static async Task ReplaceArrayWithHashSetInProperty( { // Case: int[] arr = { 1, 2, 3 }; // Transform to: HashSet arr = new HashSet { 1, 2, 3 }; + var collectionInitializer = SyntaxFactory.InitializerExpression( + SyntaxKind.CollectionInitializerExpression, + initializerExpression.Expressions + ); hashSetCreation = SyntaxFactory.ObjectCreationExpression(hashSetType) - .WithInitializer(initializerExpression) + .WithInitializer(collectionInitializer) .WithAdditionalAnnotations(Formatter.Annotation); } + else if (originalInitializer.Value is CollectionExpressionSyntax) + { + // Case: int[] arr = [ 1, 2, 3 ]; + // Transform to: HashSet arr = [ 1, 2, 3 ]; + return originalInitializer; + } else { // Case: int[] arr = new int[] { 1, 2, 3 }; or var arr = new[] { 1, 2, 3 }; @@ -208,8 +217,9 @@ private static SyntaxNode AddUsingDirective(SyntaxNode root) if (root is not CompilationUnitSyntax compilationUnit) return root; - var hasUsing = compilationUnit.Usings.Any(u => - u.Name?.ToString() == "System.Collections.Generic"); + var hasUsing = compilationUnit.DescendantNodesAndSelf() + .OfType() + .Any(u => u.Name?.ToString() == "System.Collections.Generic"); if (hasUsing) return root; diff --git a/Collections.Analyzer.CodeFixes/Collections.Analyzer.CodeFixes.csproj b/Collections.Analyzer.CodeFixes/Collections.Analyzer.CodeFixes.csproj index 3cbb1f5..fbe6a7c 100644 --- a/Collections.Analyzer.CodeFixes/Collections.Analyzer.CodeFixes.csproj +++ b/Collections.Analyzer.CodeFixes/Collections.Analyzer.CodeFixes.csproj @@ -2,7 +2,7 @@ netstandard2.0 false - 10 + latest enable diff --git a/Collections.Analyzer.CodeFixes/SetListCapacityCodeFix.cs b/Collections.Analyzer.CodeFixes/SetListCapacityCodeFix.cs index 9fdbd7c..944e858 100644 --- a/Collections.Analyzer.CodeFixes/SetListCapacityCodeFix.cs +++ b/Collections.Analyzer.CodeFixes/SetListCapacityCodeFix.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Collections.Analyzer.Diagnostics; using Collections.Analyzer.Diagnostics.CI0006; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; @@ -18,8 +19,10 @@ namespace Collections.Analyzer.CodeFixes; [Shared] public class SetListCapacityCodeFix : CodeFixProvider { - public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create( - ListInitializerDiagnostic.InitializeListWithCapacityRule.Id); + public override ImmutableArray FixableDiagnosticIds => + [ + ListInitializerDiagnostic.InitializeListWithCapacityRule.Id + ]; public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; @@ -50,7 +53,7 @@ private static async Task FixAsync(Document document, { var oldRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var count = objectCreation.Initializer?.Expressions.Count ?? 0; + var count = ExpressionExtensions.GetCollectionElementCount(objectCreation.Initializer); var newArgumentList = ArgumentList( SingletonSeparatedList( diff --git a/Collections.Analyzer/Collections.Analyzer.csproj b/Collections.Analyzer/Collections.Analyzer.csproj index c0afd69..ff4855c 100644 --- a/Collections.Analyzer/Collections.Analyzer.csproj +++ b/Collections.Analyzer/Collections.Analyzer.csproj @@ -1,12 +1,12 @@ netstandard2.0 - 10 + latest false true true true - 0.3.1 + 0.3.2 Collections.Analyzer Rogatnev Sergey Collections.Analyzer is a set of roslyn-based diagnostics for C#-projects that detect potential problems with operating different collections. diff --git a/Collections.Analyzer/Diagnostics/CI0006/ListInitializerDiagnostic.cs b/Collections.Analyzer/Diagnostics/CI0006/ListInitializerDiagnostic.cs index 7767bcd..bd368e3 100644 --- a/Collections.Analyzer/Diagnostics/CI0006/ListInitializerDiagnostic.cs +++ b/Collections.Analyzer/Diagnostics/CI0006/ListInitializerDiagnostic.cs @@ -18,8 +18,7 @@ public sealed class ListInitializerDiagnostic : DiagnosticAnalyzer true ); - public override ImmutableArray SupportedDiagnostics => - ImmutableArray.Create(InitializeListWithCapacityRule); + public override ImmutableArray SupportedDiagnostics => [InitializeListWithCapacityRule]; public override void Initialize(AnalysisContext context) { @@ -35,7 +34,8 @@ private static void Analyze(SyntaxNodeAnalysisContext context) { var objectCreation = (BaseObjectCreationExpressionSyntax)context.Node; - if (objectCreation.Initializer == null || objectCreation.Initializer.Expressions.Count == 0) + var count = ExpressionExtensions.GetCollectionElementCount(objectCreation.Initializer); + if (count == 0) { return; } @@ -82,6 +82,6 @@ private static bool HasInsufficientCapacity(SyntaxNodeAnalysisContext context, return false; } - return capacity < objectCreation.Initializer!.Expressions.Count; + return capacity < ExpressionExtensions.GetCollectionElementCount(objectCreation.Initializer); } } \ No newline at end of file diff --git a/Collections.Analyzer/Diagnostics/CI0008/ArrayContainsToHashSetDiagnostic.cs b/Collections.Analyzer/Diagnostics/CI0008/ArrayContainsToHashSetDiagnostic.cs index 4c24a13..cffb107 100644 --- a/Collections.Analyzer/Diagnostics/CI0008/ArrayContainsToHashSetDiagnostic.cs +++ b/Collections.Analyzer/Diagnostics/CI0008/ArrayContainsToHashSetDiagnostic.cs @@ -23,8 +23,7 @@ public class ArrayContainsToHashSetDiagnostic : DiagnosticAnalyzer Resources.CI0008_Description ); - public override ImmutableArray SupportedDiagnostics => - ImmutableArray.Create(Rule); + public override ImmutableArray SupportedDiagnostics => [Rule]; public override void Initialize(AnalysisContext context) { @@ -111,21 +110,14 @@ private static bool IsLinqContains(IMethodSymbol? methodSymbol, Compilation comp return SymbolEqualityComparer.Default.Equals(methodSymbol.ContainingType, enumerableType); } - private static InitializerExpressionSyntax? FindArrayInitializer(EqualsValueClauseSyntax? equalsValue) + private static SyntaxNode? FindArrayInitializer(EqualsValueClauseSyntax? equalsValue) { - if (equalsValue == null) - return null; - - if (equalsValue.Value is ImplicitArrayCreationExpressionSyntax implicitArray) - return implicitArray.Initializer; - - if (equalsValue.Value is ArrayCreationExpressionSyntax arrayCreation) - return arrayCreation.Initializer; - - if (equalsValue.Value is InitializerExpressionSyntax initializerExpression) - return initializerExpression; + return ExpressionExtensions.FindCollectionInitializer(equalsValue); + } - return null; + private static int CountArrayElements(SyntaxNode initializer) + { + return ExpressionExtensions.GetCollectionElementCount(initializer); } private static void AnalyzeVariable(SyntaxNodeAnalysisContext context, @@ -243,11 +235,6 @@ argument.Parent is ArgumentListSyntax argumentList && return false; } - private static int CountArrayElements(InitializerExpressionSyntax initializer) - { - return initializer.Expressions.Count; - } - private class UsageAnalysis { public bool HasContainsCall { get; set; } diff --git a/Collections.Analyzer/Diagnostics/ExpressionExtensions.cs b/Collections.Analyzer/Diagnostics/ExpressionExtensions.cs index dfd0411..cb4809a 100644 --- a/Collections.Analyzer/Diagnostics/ExpressionExtensions.cs +++ b/Collections.Analyzer/Diagnostics/ExpressionExtensions.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Frozen; +using System.Collections.Generic; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -9,8 +10,7 @@ namespace Collections.Analyzer.Diagnostics; public static class ExpressionExtensions { - private static readonly IReadOnlyCollection Methods = - new HashSet(new[] {nameof(Enumerable.ToArray), nameof(Enumerable.ToList)}); + private static readonly IReadOnlyCollection Methods = new [] {nameof(Enumerable.ToArray), nameof(Enumerable.ToList)}.ToFrozenSet(); public static bool IsRedundantMethod(SyntaxNodeAnalysisContext context, ExpressionSyntax invocationExpression) @@ -30,7 +30,46 @@ public static bool IsRedundantMethod(SyntaxNodeAnalysisContext context, { ILocalSymbol localSymbol => localSymbol.Type, IMethodSymbol methodSymbol => methodSymbol.ReturnType, - _ => null + IPropertySymbol propertySymbol => propertySymbol.Type, + IFieldSymbol fieldSymbol => fieldSymbol.Type, + IParameterSymbol parameterSymbol => parameterSymbol.Type, + _ => context.SemanticModel.GetTypeInfo(memberAccessExpression.Expression).Type }; } + + public static SyntaxNode? FindCollectionInitializer(EqualsValueClauseSyntax? equalsValue) + { + if (equalsValue == null) + return null; + + if (equalsValue.Value is ImplicitArrayCreationExpressionSyntax implicitArray) + return implicitArray.Initializer; + + if (equalsValue.Value is ArrayCreationExpressionSyntax arrayCreation) + return arrayCreation.Initializer; + + if (equalsValue.Value is InitializerExpressionSyntax initializerExpression) + return initializerExpression; + + if (equalsValue.Value is CollectionExpressionSyntax collectionExpression) + return collectionExpression; + + return null; + } + + public static int GetCollectionElementCount(SyntaxNode? initializer) + { + return initializer switch + { + InitializerExpressionSyntax initExpr => initExpr.Expressions.Count, + CollectionExpressionSyntax collExpr => collExpr.Elements.Count, + _ => 0 + }; + } + + public static bool SupportsFrozenSet(this Compilation compilation) + { + return compilation.GetTypeByMetadataName("System.Collections.Frozen.FrozenSet`1") != null + && compilation.GetTypeByMetadataName("System.Collections.Frozen.FrozenSet") != null; + } } \ No newline at end of file diff --git a/Collections.Analyzer/Resources.Designer.cs b/Collections.Analyzer/Resources.Designer.cs index 5ce44fb..a43ab8b 100644 --- a/Collections.Analyzer/Resources.Designer.cs +++ b/Collections.Analyzer/Resources.Designer.cs @@ -153,6 +153,12 @@ internal static string CI0008_CodeFixTitle { } } + internal static string CI0008_FrozenSetCodeFixTitle { + get { + return ResourceManager.GetString("CI0008_FrozenSetCodeFixTitle", resourceCulture); + } + } + internal static string CI0009_Title { get { return ResourceManager.GetString("CI0009_Title", resourceCulture); diff --git a/Collections.Analyzer/Resources.resx b/Collections.Analyzer/Resources.resx index 079d5f0..9cc72b6 100644 --- a/Collections.Analyzer/Resources.resx +++ b/Collections.Analyzer/Resources.resx @@ -77,6 +77,9 @@ Replace array with HashSet + + Replace array with FrozenSet + List capacity should be set based on the source collection diff --git a/Collections.Analyzer/Resources.ru.resx b/Collections.Analyzer/Resources.ru.resx index 0519e48..5a3fed7 100644 --- a/Collections.Analyzer/Resources.ru.resx +++ b/Collections.Analyzer/Resources.ru.resx @@ -69,6 +69,9 @@ Заменить массив на HashSet + + Заменить массив на FrozenSet + Емкость списка следует установить исходя из размера коллекции источника. diff --git a/Documentation/CI0006.md b/Documentation/CI0006.md index b1aa879..4d5c09b 100644 --- a/Documentation/CI0006.md +++ b/Documentation/CI0006.md @@ -1,10 +1,30 @@ -# CI0006: Collection initializer is used without capacity -Using collection initializer without capacity harms performance and cause additional memory allocation. +# CI0006: Collection initializer is used without capacity + +Using a collection initializer on `List` without specifying an initial capacity (or specifying a capacity smaller than the count of initialized items) harms performance by causing repeated memory allocations and array resizes as elements are added. + +## Code Fixes +- **Set initial capacity**: Automatically calculates the number of elements in the collection initializer and passes it to the `List` constructor. ## Examples + +### Violation: capacity is not specified +```csharp +var list = new List { 1, 2, 3, 4 }; +``` + +#### Fix ```csharp -var list = new List {1, 2, 3, 4}; +var list = new List(4) { 1, 2, 3, 4 }; ``` + +### Violation: capacity is smaller than number of initialized elements +```csharp +var list = new List(2) { "1", "2", "3", "4" }; +``` + +#### Fix ```csharp -var list = new List(2) {"1", "2", "3", "4"}; -``` \ No newline at end of file +var list = new List(4) { "1", "2", "3", "4" }; +``` + +> **Note**: For C# 12+ collection expressions (`List list = [1, 2, 3, 4];`), the C# compiler automatically allocates the exact required capacity, so no warning is emitted. diff --git a/Documentation/CI0008.md b/Documentation/CI0008.md index e4fb5a8..8f004a9 100644 --- a/Documentation/CI0008.md +++ b/Documentation/CI0008.md @@ -1,16 +1,31 @@ # CI0008: Consider using HashSet instead of an array for Contains operations -An array is used with the `Contains()` method for lookup operations. + +An array is used with the LINQ `Contains()` method for lookup operations. Arrays perform linear search with $O(N)$ time complexity, which can become a performance bottleneck when the array contains multiple elements or when lookups are executed frequently. + +Replacing the array with `HashSet` or `FrozenSet` provides $O(1)$ constant-time lookup performance. ## Configuration -You can configure the minimum number of items in the array to trigger the analyzer. Default value is 1. -``` +You can configure the minimum number of items in the array required to trigger the diagnostic. Default value is `1`. + +In `.editorconfig`: +```ini dotnet_diagnostic.CI0008.min_items_count = 5 ``` +## Code Fixes +- **Replace array with HashSet**: Replaces array declaration with `HashSet` for $O(1)$ lookups. Supports array creation expressions (`new[] { ... }`, `new int[] { ... }`), array initializers (`{ ... }`), and C# 12 collection expressions (`[ ... ]`). +- **Replace array with FrozenSet**: (Available on .NET 8+ when `System.Collections.Frozen` is referenced) Replaces array with immutable `FrozenSet` (`ToFrozenSet()`). `FrozenSet` is optimized for read-only lookups, offering faster lookup speeds and smaller memory footprint than `HashSet`. Supports spread elements (`[..other, ...]`) as well. + ## Examples + +### Examples of Violations + ```csharp public class ArrayContains { + // C# 12+ collection expression syntax + public int[] ListCollectionExpr { get; } = [1, 2, 3, 4]; + public int[] List3 { get; } = new[] { 1, 2, 3, 4 }; private readonly int[] _list2 = { 1, 2, 3, 4 }; @@ -18,7 +33,6 @@ public class ArrayContains public bool Method1() { var list1 = new[] { 1, 2, 3, 4 }; - return list1.Contains(5); } @@ -35,13 +49,67 @@ public class ArrayContains public bool Method4() { var list = new int[] { 1, 2, 3, 4 }; + return list.Contains(2); + } + + public bool Method5() + { + int[] numbers = [1, 2, 3, 4, 5]; + return numbers.Contains(3); + } +} +``` + +### Code Fix: Replace with HashSet + +```csharp +using System.Collections.Generic; - if (list.Contains(2)) - { - return true; - } +public class ArrayContains +{ + public HashSet ListCollectionExpr { get; } = [1, 2, 3, 4]; + + public HashSet List3 { get; } = new HashSet { 1, 2, 3, 4 }; - return false; + private readonly HashSet _list2 = new HashSet { 1, 2, 3, 4 }; + + public bool Method1() + { + var list1 = new HashSet { 1, 2, 3, 4 }; + return list1.Contains(5); + } + + public bool Method5() + { + HashSet numbers = [1, 2, 3, 4, 5]; + return numbers.Contains(3); } } -``` \ No newline at end of file +``` + +### Code Fix: Replace with FrozenSet (.NET 8+) + +```csharp +using System.Collections.Frozen; + +public class ArrayContains +{ + public FrozenSet ListCollectionExpr { get; } = new[] { 1, 2, 3, 4 }.ToFrozenSet(); + + public FrozenSet List3 { get; } = new[] { 1, 2, 3, 4 }.ToFrozenSet(); + + private readonly FrozenSet _list2 = new[] { 1, 2, 3, 4 }.ToFrozenSet(); + + public bool Method1() + { + var list1 = new[] { 1, 2, 3, 4 }.ToFrozenSet(); + return list1.Contains(5); + } + + public bool Method5() + { + FrozenSet numbers = new[] { 1, 2, 3, 4, 5 }.ToFrozenSet(); + return numbers.Contains(3); + } +} +``` diff --git a/Examples/Examples.csproj b/Examples/Examples.csproj index 7803c80..d2da2ad 100644 --- a/Examples/Examples.csproj +++ b/Examples/Examples.csproj @@ -1,8 +1,8 @@ - netstandard2.0 - 10 + net10.0 + latest enable diff --git a/Tests/ArrayContainsToHashSetTests/ArrayContainsToFrozenSetTests.cs b/Tests/ArrayContainsToHashSetTests/ArrayContainsToFrozenSetTests.cs new file mode 100644 index 0000000..a580d89 --- /dev/null +++ b/Tests/ArrayContainsToHashSetTests/ArrayContainsToFrozenSetTests.cs @@ -0,0 +1,60 @@ +using System.Threading.Tasks; +using Collections.Analyzer.CodeFixes; +using Collections.Analyzer.Diagnostics.CI0008; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; +using NUnit.Framework; + +namespace Tests.ArrayContainsToHashSetTests; + +[TestFixture] +public class ArrayContainsToFrozenSetTests : CSharpCodeFixTest< + ArrayContainsToHashSetDiagnostic, ArrayContainsToFrozenSetCodeFix, DefaultVerifier> +{ + [Test] + [TestCase("ArrayContainsBefore1.cs", "ArrayContainsFrozenSetAfter1.cs")] + [TestCase("ArrayContainsBefore2.cs", "ArrayContainsFrozenSetAfter2.cs")] + [TestCase("ArrayContainsBefore3.cs", "ArrayContainsFrozenSetAfter3.cs")] + [TestCase("ArrayContainsBefore4.cs", "ArrayContainsFrozenSetAfter4.cs")] + [TestCase("ArrayContainsBefore5.cs", "ArrayContainsFrozenSetAfter5.cs")] + [TestCase("ArrayContainsBefore6.cs", "ArrayContainsFrozenSetAfter6.cs")] + [TestCase("ArrayContainsBefore7.cs", "ArrayContainsFrozenSetAfter7.cs")] + [TestCase("ArrayContainsBefore10.cs", "ArrayContainsFrozenSetAfter10.cs")] + [TestCase("ArrayContainsBefore11.cs", "ArrayContainsFrozenSetAfter11.cs")] + public Task CodeFixesTest(string before, string after) + { + var code = ResourceReader.ReadFromFile(before); + var fixedCode = ResourceReader.ReadFromFile(after); + + var test = new CSharpCodeFixTest + { + TestCode = code, + FixedCode = fixedCode, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80 + }; + + test.SolutionTransforms.Add((solution, projectId) => + { + var project = solution.GetProject(projectId)!; + var parseOptions = ((Microsoft.CodeAnalysis.CSharp.CSharpParseOptions)project.ParseOptions!).WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.CSharp12); + return solution.WithProjectParseOptions(projectId, parseOptions); + }); + + return test.RunAsync(); + } + + [Test] + public Task WhenFrozenSetNotSupported_CodeFixIsNotOffered() + { + var code = ResourceReader.ReadFromFile("ArrayContainsBefore1.cs"); + + var test = new CSharpCodeFixTest + { + TestCode = code, + FixedCode = code, + ReferenceAssemblies = ReferenceAssemblies.NetStandard.NetStandard20 + }; + + return test.RunAsync(); + } +} diff --git a/Tests/ArrayContainsToHashSetTests/ArrayContainsToFrozenSetVerifier.cs b/Tests/ArrayContainsToHashSetTests/ArrayContainsToFrozenSetVerifier.cs new file mode 100644 index 0000000..9d6804d --- /dev/null +++ b/Tests/ArrayContainsToHashSetTests/ArrayContainsToFrozenSetVerifier.cs @@ -0,0 +1,13 @@ +using Collections.Analyzer.CodeFixes; +using Collections.Analyzer.Diagnostics.CI0008; +using Microsoft.CodeAnalysis.Testing; + +namespace Tests.ArrayContainsToHashSetTests; + +public class ArrayContainsToFrozenSetVerifier : CodeFixVerifier< + ArrayContainsToHashSetDiagnostic, + ArrayContainsToFrozenSetCodeFix, + ArrayContainsToFrozenSetTests, + DefaultVerifier> +{ +} diff --git a/Tests/ArrayContainsToHashSetTests/ArrayContainsToHashSetTests.cs b/Tests/ArrayContainsToHashSetTests/ArrayContainsToHashSetTests.cs index 0eb3882..65dcc02 100644 --- a/Tests/ArrayContainsToHashSetTests/ArrayContainsToHashSetTests.cs +++ b/Tests/ArrayContainsToHashSetTests/ArrayContainsToHashSetTests.cs @@ -113,11 +113,26 @@ public Task ArrayWithoutContains_ShouldNotWarn() .VerifyAnalyzerAsync(code, DiagnosticResult.EmptyDiagnosticResults); } + [Test] + public Task LocalVariableCollectionExpression_ShouldWarn() + { + var code = ResourceReader.ReadFromFile("ArrayContains8.cs"); + + return ArrayContainsVerifier + .VerifyAnalyzerAsync(code, DiagnosticResult.CompilerWarning("CI0008").WithSpan(7, 15, 7, 22)); + } + [Test] [TestCase("ArrayContainsBefore1.cs", "ArrayContainsAfter1.cs")] [TestCase("ArrayContainsBefore2.cs", "ArrayContainsAfter2.cs")] - [TestCase("ArrayContainsBefore3.cs", "ArrayContainsAfter3.cs", Ignore = "Different syntax tree in result")] + [TestCase("ArrayContainsBefore3.cs", "ArrayContainsAfter3.cs")] [TestCase("ArrayContainsBefore4.cs", "ArrayContainsAfter4.cs")] + [TestCase("ArrayContainsBefore5.cs", "ArrayContainsAfter5.cs")] + [TestCase("ArrayContainsBefore6.cs", "ArrayContainsAfter6.cs")] + [TestCase("ArrayContainsBefore7.cs", "ArrayContainsAfter7.cs")] + [TestCase("ArrayContainsBefore8.cs", "ArrayContainsAfter8.cs")] + [TestCase("ArrayContainsBefore9.cs", "ArrayContainsAfter9.cs")] + [TestCase("ArrayContainsBefore11.cs", "ArrayContainsAfter11.cs")] public Task CodeFixesTest(string before, string after) { var code = ResourceReader.ReadFromFile(before); diff --git a/Tests/ExpressionExtensionsTests.cs b/Tests/ExpressionExtensionsTests.cs new file mode 100644 index 0000000..7f68049 --- /dev/null +++ b/Tests/ExpressionExtensionsTests.cs @@ -0,0 +1,76 @@ +using System.Linq; +using Collections.Analyzer.Diagnostics; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using NUnit.Framework; + +namespace Tests; + +[TestFixture] +public class ExpressionExtensionsTests +{ + [Test] + public void FindCollectionInitializer_CollectionExpression_ShouldReturnCollectionExpression() + { + var tree = CSharpSyntaxTree.ParseText("class C { int[] arr = [1, 2, 3]; }"); + var root = tree.GetRoot(); + var variable = root.DescendantNodes().OfType().First(); + + var initializer = ExpressionExtensions.FindCollectionInitializer(variable.Initializer); + + Assert.That(initializer, Is.InstanceOf()); + Assert.That(ExpressionExtensions.GetCollectionElementCount(initializer), Is.EqualTo(3)); + } + + [Test] + public void FindCollectionInitializer_ImplicitArray_ShouldReturnInitializer() + { + var tree = CSharpSyntaxTree.ParseText("class C { int[] arr = new[] { 1, 2, 3, 4 }; }"); + var root = tree.GetRoot(); + var variable = root.DescendantNodes().OfType().First(); + + var initializer = ExpressionExtensions.FindCollectionInitializer(variable.Initializer); + + Assert.That(initializer, Is.InstanceOf()); + Assert.That(ExpressionExtensions.GetCollectionElementCount(initializer), Is.EqualTo(4)); + } + + [Test] + public void FindCollectionInitializer_ExplicitArray_ShouldReturnInitializer() + { + var tree = CSharpSyntaxTree.ParseText("class C { int[] arr = new int[] { 1, 2 }; }"); + var root = tree.GetRoot(); + var variable = root.DescendantNodes().OfType().First(); + + var initializer = ExpressionExtensions.FindCollectionInitializer(variable.Initializer); + + Assert.That(initializer, Is.InstanceOf()); + Assert.That(ExpressionExtensions.GetCollectionElementCount(initializer), Is.EqualTo(2)); + } + + [Test] + public void FindCollectionInitializer_DirectInitializer_ShouldReturnInitializer() + { + var tree = CSharpSyntaxTree.ParseText("class C { int[] arr = { 1, 2, 3, 4, 5 }; }"); + var root = tree.GetRoot(); + var variable = root.DescendantNodes().OfType().First(); + + var initializer = ExpressionExtensions.FindCollectionInitializer(variable.Initializer); + + Assert.That(initializer, Is.InstanceOf()); + Assert.That(ExpressionExtensions.GetCollectionElementCount(initializer), Is.EqualTo(5)); + } + + [Test] + public void FindCollectionInitializer_NullOrNonCollection_ShouldReturnNullAndZero() + { + var tree = CSharpSyntaxTree.ParseText("class C { int x = 42; }"); + var root = tree.GetRoot(); + var variable = root.DescendantNodes().OfType().First(); + + var initializer = ExpressionExtensions.FindCollectionInitializer(variable.Initializer); + + Assert.That(initializer, Is.Null); + Assert.That(ExpressionExtensions.GetCollectionElementCount(initializer), Is.EqualTo(0)); + } +} diff --git a/Tests/ListTests/ListCapacityTests.cs b/Tests/ListTests/ListCapacityTests.cs index d94744b..a4b88a2 100644 --- a/Tests/ListTests/ListCapacityTests.cs +++ b/Tests/ListTests/ListCapacityTests.cs @@ -47,6 +47,15 @@ public Task ListObjectCreationLessCapacityTest() .VerifyAnalyzerAsync(code, DiagnosticResult.CompilerWarning("CI0006").WithSpan(9, 31, 9, 50)); } + [Test] + public Task ListCollectionExpression_ShouldNotWarn() + { + var code = ResourceReader.ReadFromFile("ListInitializer5.cs"); + + return ListCapacityVerifier + .VerifyAnalyzerAsync(code, DiagnosticResult.EmptyDiagnosticResults); + } + [Test] [TestCase("ListInitializerBefore1.cs", "ListInitializerAfter1.cs")] [TestCase("ListInitializerBefore2.cs", "ListInitializerAfter2.cs")] diff --git a/Tests/Resources/ArrayContainsToHashSetTests/ArrayContains8.cs b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContains8.cs new file mode 100644 index 0000000..ddcdd65 --- /dev/null +++ b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContains8.cs @@ -0,0 +1,12 @@ +using System.Linq; + +class TestClass +{ + void TestMethod() + { + int[] numbers = [1, 2, 3, 4, 5]; + if (numbers.Contains(3)) + { + } + } +} diff --git a/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsAfter11.cs b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsAfter11.cs new file mode 100644 index 0000000..d8fffdc --- /dev/null +++ b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsAfter11.cs @@ -0,0 +1,12 @@ +using System.Linq; +using System.Collections.Generic; + +class TestClass +{ + private static readonly HashSet Values = [1, 2, 3, 4]; + + public bool Check(int val) + { + return Values.Contains(val); + } +} diff --git a/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsAfter5.cs b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsAfter5.cs new file mode 100644 index 0000000..d2e867b --- /dev/null +++ b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsAfter5.cs @@ -0,0 +1,13 @@ +using System.Linq; +using System.Collections.Generic; + +class TestClass +{ + void TestMethod() + { + HashSet numbers = [1, 2, 3, 4, 5]; + if (numbers.Contains(3)) + { + } + } +} diff --git a/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsAfter6.cs b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsAfter6.cs new file mode 100644 index 0000000..b65512d --- /dev/null +++ b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsAfter6.cs @@ -0,0 +1,12 @@ +using System.Linq; +using System.Collections.Generic; + +class TestClass +{ + public HashSet List3 { get; } = [1, 2, 3, 4]; + + public bool Check(int value) + { + return List3.Contains(value); + } +} diff --git a/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsAfter7.cs b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsAfter7.cs new file mode 100644 index 0000000..0e07b13 --- /dev/null +++ b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsAfter7.cs @@ -0,0 +1,14 @@ +using System.Linq; +using System.Collections.Generic; + +class TestClass +{ + void TestMethod() + { + int[] other = [1, 2]; + HashSet numbers = [.. other, 3, 4, 5]; + if (numbers.Contains(3)) + { + } + } +} diff --git a/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsAfter8.cs b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsAfter8.cs new file mode 100644 index 0000000..0aeeac4 --- /dev/null +++ b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsAfter8.cs @@ -0,0 +1,13 @@ +using System.Linq; +using System.Collections.Generic; + +class TestClass +{ + void TestMethod() + { + HashSet numbers1 = [1, 2, 3], numbers2 = [4, 5, 6]; + if (numbers1.Contains(3)) + { + } + } +} diff --git a/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsAfter9.cs b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsAfter9.cs new file mode 100644 index 0000000..a7eda08 --- /dev/null +++ b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsAfter9.cs @@ -0,0 +1,16 @@ +namespace TestNamespace +{ + using System.Linq; + using System.Collections.Generic; + + class TestClass + { + void TestMethod() + { + HashSet numbers = [1, 2, 3, 4, 5]; + if (numbers.Contains(3)) + { + } + } + } +} diff --git a/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsBefore10.cs b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsBefore10.cs new file mode 100644 index 0000000..4b37b74 --- /dev/null +++ b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsBefore10.cs @@ -0,0 +1,16 @@ +namespace TestNamespace +{ + using System.Linq; + using System.Collections.Frozen; + + class TestClass + { + void TestMethod() + { + int[] {|CI0008:numbers|} = [1, 2, 3, 4, 5]; + if (numbers.Contains(3)) + { + } + } + } +} diff --git a/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsBefore11.cs b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsBefore11.cs new file mode 100644 index 0000000..4b6bdae --- /dev/null +++ b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsBefore11.cs @@ -0,0 +1,11 @@ +using System.Linq; + +class TestClass +{ + private static readonly int[] {|CI0008:Values|} = [1, 2, 3, 4]; + + public bool Check(int val) + { + return Values.Contains(val); + } +} diff --git a/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsBefore5.cs b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsBefore5.cs new file mode 100644 index 0000000..8e8b6a6 --- /dev/null +++ b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsBefore5.cs @@ -0,0 +1,12 @@ +using System.Linq; + +class TestClass +{ + void TestMethod() + { + int[] {|CI0008:numbers|} = [1, 2, 3, 4, 5]; + if (numbers.Contains(3)) + { + } + } +} diff --git a/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsBefore6.cs b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsBefore6.cs new file mode 100644 index 0000000..39a0a7f --- /dev/null +++ b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsBefore6.cs @@ -0,0 +1,11 @@ +using System.Linq; + +class TestClass +{ + public int[] {|CI0008:List3|} { get; } = [1, 2, 3, 4]; + + public bool Check(int value) + { + return List3.Contains(value); + } +} diff --git a/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsBefore7.cs b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsBefore7.cs new file mode 100644 index 0000000..0db1d14 --- /dev/null +++ b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsBefore7.cs @@ -0,0 +1,13 @@ +using System.Linq; + +class TestClass +{ + void TestMethod() + { + int[] other = [1, 2]; + int[] {|CI0008:numbers|} = [.. other, 3, 4, 5]; + if (numbers.Contains(3)) + { + } + } +} diff --git a/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsBefore8.cs b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsBefore8.cs new file mode 100644 index 0000000..13b6044 --- /dev/null +++ b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsBefore8.cs @@ -0,0 +1,12 @@ +using System.Linq; + +class TestClass +{ + void TestMethod() + { + int[] {|CI0008:numbers1|} = [1, 2, 3], numbers2 = [4, 5, 6]; + if (numbers1.Contains(3)) + { + } + } +} diff --git a/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsBefore9.cs b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsBefore9.cs new file mode 100644 index 0000000..88dbc4b --- /dev/null +++ b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsBefore9.cs @@ -0,0 +1,16 @@ +namespace TestNamespace +{ + using System.Linq; + using System.Collections.Generic; + + class TestClass + { + void TestMethod() + { + int[] {|CI0008:numbers|} = [1, 2, 3, 4, 5]; + if (numbers.Contains(3)) + { + } + } + } +} diff --git a/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsFrozenSetAfter1.cs b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsFrozenSetAfter1.cs new file mode 100644 index 0000000..0e75f8f --- /dev/null +++ b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsFrozenSetAfter1.cs @@ -0,0 +1,13 @@ +using System.Linq; +using System.Collections.Frozen; + +class TestClass +{ + void TestMethod() + { + var numbers = new[] { 1, 2, 3, 4, 5 }.ToFrozenSet(); + if (numbers.Contains(3)) + { + } + } +} diff --git a/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsFrozenSetAfter10.cs b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsFrozenSetAfter10.cs new file mode 100644 index 0000000..945d611 --- /dev/null +++ b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsFrozenSetAfter10.cs @@ -0,0 +1,16 @@ +namespace TestNamespace +{ + using System.Linq; + using System.Collections.Frozen; + + class TestClass + { + void TestMethod() + { + FrozenSet numbers = new[] { 1, 2, 3, 4, 5 }.ToFrozenSet(); + if (numbers.Contains(3)) + { + } + } + } +} diff --git a/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsFrozenSetAfter11.cs b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsFrozenSetAfter11.cs new file mode 100644 index 0000000..28f5350 --- /dev/null +++ b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsFrozenSetAfter11.cs @@ -0,0 +1,12 @@ +using System.Linq; +using System.Collections.Frozen; + +class TestClass +{ + private static readonly FrozenSet Values = new[] { 1, 2, 3, 4 }.ToFrozenSet(); + + public bool Check(int val) + { + return Values.Contains(val); + } +} diff --git a/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsFrozenSetAfter2.cs b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsFrozenSetAfter2.cs new file mode 100644 index 0000000..c9aefee --- /dev/null +++ b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsFrozenSetAfter2.cs @@ -0,0 +1,12 @@ +using System.Linq; +using System.Collections.Frozen; + +class TestClass +{ + private readonly FrozenSet _list2 = new int[] { 1, 2, 3, 4 }.ToFrozenSet(); + + public bool Method() + { + return _list2.Contains(5); + } +} diff --git a/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsFrozenSetAfter3.cs b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsFrozenSetAfter3.cs new file mode 100644 index 0000000..4f81ea8 --- /dev/null +++ b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsFrozenSetAfter3.cs @@ -0,0 +1,12 @@ +using System.Linq; +using System.Collections.Frozen; + +class TestClass +{ + private readonly FrozenSet _list2 = new[] { 1, 2, 3, 4 }.ToFrozenSet(); + + public bool Method() + { + return _list2.Contains(5); + } +} diff --git a/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsFrozenSetAfter4.cs b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsFrozenSetAfter4.cs new file mode 100644 index 0000000..213ac37 --- /dev/null +++ b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsFrozenSetAfter4.cs @@ -0,0 +1,12 @@ +using System.Linq; +using System.Collections.Frozen; + +class TestClass +{ + public FrozenSet List3 { get; } = new[] { 1, 2, 3, 4 }.ToFrozenSet(); + + public bool Check(int value) + { + return List3.Contains(value); + } +} diff --git a/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsFrozenSetAfter5.cs b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsFrozenSetAfter5.cs new file mode 100644 index 0000000..b733773 --- /dev/null +++ b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsFrozenSetAfter5.cs @@ -0,0 +1,13 @@ +using System.Linq; +using System.Collections.Frozen; + +class TestClass +{ + void TestMethod() + { + FrozenSet numbers = new[] { 1, 2, 3, 4, 5 }.ToFrozenSet(); + if (numbers.Contains(3)) + { + } + } +} diff --git a/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsFrozenSetAfter6.cs b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsFrozenSetAfter6.cs new file mode 100644 index 0000000..213ac37 --- /dev/null +++ b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsFrozenSetAfter6.cs @@ -0,0 +1,12 @@ +using System.Linq; +using System.Collections.Frozen; + +class TestClass +{ + public FrozenSet List3 { get; } = new[] { 1, 2, 3, 4 }.ToFrozenSet(); + + public bool Check(int value) + { + return List3.Contains(value); + } +} diff --git a/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsFrozenSetAfter7.cs b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsFrozenSetAfter7.cs new file mode 100644 index 0000000..c1e354d --- /dev/null +++ b/Tests/Resources/ArrayContainsToHashSetTests/ArrayContainsFrozenSetAfter7.cs @@ -0,0 +1,14 @@ +using System.Linq; +using System.Collections.Frozen; + +class TestClass +{ + void TestMethod() + { + int[] other = [1, 2]; + FrozenSet numbers = ((int[])[.. other, 3, 4, 5]).ToFrozenSet(); + if (numbers.Contains(3)) + { + } + } +} diff --git a/Tests/Resources/ListTests/ListInitializer5.cs b/Tests/Resources/ListTests/ListInitializer5.cs new file mode 100644 index 0000000..40a11c2 --- /dev/null +++ b/Tests/Resources/ListTests/ListInitializer5.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Examples.Lists; + +public class ListInitializer +{ + public void Method() + { + List list1 = [1, 2, 3, 4]; + } +} diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 54bc157..4f05fb0 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -2,13 +2,13 @@ net10.0 - 10 + latest enable - - + +