diff --git a/src/Meziantou.Analyzer.CodeFixers/Meziantou.Analyzer.CodeFixers.csproj b/src/Meziantou.Analyzer.CodeFixers/Meziantou.Analyzer.CodeFixers.csproj index e83932e03..f390fdf5f 100644 --- a/src/Meziantou.Analyzer.CodeFixers/Meziantou.Analyzer.CodeFixers.csproj +++ b/src/Meziantou.Analyzer.CodeFixers/Meziantou.Analyzer.CodeFixers.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/NamedParameterFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/NamedParameterFixer.cs index 3d7b32b79..7f5f3d965 100644 --- a/src/Meziantou.Analyzer.CodeFixers/Rules/NamedParameterFixer.cs +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/NamedParameterFixer.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Collections.Immutable; +using System.Collections.Immutable; using System.Composition; using System.Threading; using System.Threading.Tasks; @@ -49,12 +48,11 @@ private static async Task AddParameterName(Document document, SyntaxNo if (argument is null || argument.NameColon is not null) return document; - var parameters = FindParameters(semanticModel, argument, cancellationToken); - if (parameters is null) + if (FindParameters(semanticModel, argument, cancellationToken) is not { } parameters) return document; var index = NamedParameterAnalyzerCommon.ArgumentIndex(argument); - if (index < 0 || index >= parameters.Count) + if (index < 0 || index >= parameters.Length) return document; var parameter = parameters[index]; @@ -64,7 +62,7 @@ private static async Task AddParameterName(Document document, SyntaxNo return editor.GetChangedDocument(); } - private static IReadOnlyList? FindParameters(SemanticModel semanticModel, SyntaxNode? node, CancellationToken cancellationToken) + private static ImmutableArray? FindParameters(SemanticModel semanticModel, SyntaxNode? node, CancellationToken cancellationToken) { while (node is not null) { diff --git a/src/Meziantou.Analyzer/Internals/EnumerableExtensions.cs b/src/Meziantou.Analyzer/Internals/EnumerableExtensions.cs index 4bcdcd19f..292119c22 100644 --- a/src/Meziantou.Analyzer/Internals/EnumerableExtensions.cs +++ b/src/Meziantou.Analyzer/Internals/EnumerableExtensions.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; -using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Meziantou.Analyzer; diff --git a/src/Meziantou.Analyzer/Internals/ObjectPool.cs b/src/Meziantou.Analyzer/Internals/ObjectPool.cs new file mode 100644 index 000000000..a61d5b5a0 --- /dev/null +++ b/src/Meziantou.Analyzer/Internals/ObjectPool.cs @@ -0,0 +1,352 @@ +#pragma warning disable MA0048 // File name must match type name +#pragma warning disable RS1035 // Do not use APIs banned for analyzers +using System.Collections.Concurrent; +using System.Threading; +using System; +using System.Text; + +namespace Meziantou.Analyzer.Internals; + + +/// +/// A pool of objects. +/// +/// The type of objects to pool. +internal abstract class ObjectPool where T : class +{ + /// + /// Gets an object from the pool if one is available, otherwise creates one. + /// + /// A . + public abstract T Get(); + + /// + /// Return an object to the pool. + /// + /// The object to add to the pool. + public abstract void Return(T obj); +} + +/// +/// Methods for creating instances. +/// +internal static class ObjectPool +{ + public static ObjectPool SharedStringBuilderPool { get; } = CreateStringBuilderPool(); + + public static ObjectPool Create(IPooledObjectPolicy? policy = null) where T : class, new() + { + var provider = new DefaultObjectPoolProvider(); + return provider.Create(policy ?? new DefaultPooledObjectPolicy()); + } + + public static ObjectPool CreateStringBuilderPool() + { + var provider = new DefaultObjectPoolProvider(); + return provider.Create(new StringBuilderPooledObjectPolicy()); + } +} + +/// +/// Represents a policy for managing pooled objects. +/// +/// The type of object which is being pooled. +internal interface IPooledObjectPolicy where T : notnull +{ + /// + /// Create a . + /// + /// The which was created. + T Create(); + + /// + /// Runs some processing when an object was returned to the pool. Can be used to reset the state of an object and indicate if the object should be returned to the pool. + /// + /// The object to return to the pool. + /// if the object should be returned to the pool. if it's not possible/desirable for the pool to keep the object. + bool Return(T obj); +} + +/// +/// A provider of instances. +/// +internal abstract class ObjectPoolProvider +{ + /// + /// Creates an . + /// + /// The type to create a pool for. + public ObjectPool Create() where T : class, new() + { + return Create(new DefaultPooledObjectPolicy()); + } + + /// + /// Creates an with the given . + /// + /// The type to create a pool for. + public abstract ObjectPool Create(IPooledObjectPolicy policy) where T : class; +} + +/// +/// Default implementation of . +/// +/// The type to pool objects for. +/// This implementation keeps a cache of retained objects. This means that if objects are returned when the pool has already reached "maximumRetained" objects they will be available to be Garbage Collected. +internal class DefaultObjectPool : ObjectPool where T : class +{ + private readonly Func _createFunc; + private readonly Func _returnFunc; + private readonly int _maxCapacity; + private int _numItems; + + private protected readonly ConcurrentQueue Items = new(); + private protected T? FastItem; + + /// + /// Creates an instance of . + /// + /// The pooling policy to use. + public DefaultObjectPool(IPooledObjectPolicy policy) + : this(policy, Environment.ProcessorCount * 2) + { + } + + /// + /// Creates an instance of . + /// + /// The pooling policy to use. + /// The maximum number of objects to retain in the pool. + public DefaultObjectPool(IPooledObjectPolicy policy, int maximumRetained) + { + // cache the target interface methods, to avoid interface lookup overhead + _createFunc = policy.Create; + _returnFunc = policy.Return; + _maxCapacity = maximumRetained - 1; // -1 to account for _fastItem + } + + /// + public override T Get() + { + var item = FastItem; + if (item == null || Interlocked.CompareExchange(ref FastItem, null, item) != item) + { + if (Items.TryDequeue(out item)) + { + Interlocked.Decrement(ref _numItems); + return item; + } + + // no object available, so go get a brand new one + return _createFunc(); + } + + return item; + } + + /// + public override void Return(T obj) + { + ReturnCore(obj); + } + + /// + /// Returns an object to the pool. + /// + /// true if the object was returned to the pool + private protected bool ReturnCore(T obj) + { + if (!_returnFunc(obj)) + { + // policy says to drop this object + return false; + } + + if (FastItem != null || Interlocked.CompareExchange(ref FastItem, obj, null) != null) + { + if (Interlocked.Increment(ref _numItems) <= _maxCapacity) + { + Items.Enqueue(obj); + return true; + } + + // no room, clean up the count and drop the object on the floor + Interlocked.Decrement(ref _numItems); + return false; + } + + return true; + } +} + +/// +/// The default . +/// +internal sealed class DefaultObjectPoolProvider : ObjectPoolProvider +{ + /// + /// The maximum number of objects to retain in the pool. + /// + public int MaximumRetained { get; set; } = Environment.ProcessorCount * 2; + + /// + public override ObjectPool Create(IPooledObjectPolicy policy) + { + if (typeof(IDisposable).IsAssignableFrom(typeof(T))) + { + return new DisposableObjectPool(policy, MaximumRetained); + } + + return new DefaultObjectPool(policy, MaximumRetained); + } +} + +/// +/// Default implementation for . +/// +/// The type of object which is being pooled. +internal sealed class DefaultPooledObjectPolicy : PooledObjectPolicy where T : class, new() +{ + /// + public override T Create() + { + return new T(); + } + + /// + public override bool Return(T obj) + { + if (obj is IResettable resettable) + { + return resettable.TryReset(); + } + + return true; + } +} + +/// +/// A base type for . +/// +/// The type of object which is being pooled. +internal abstract class PooledObjectPolicy : IPooledObjectPolicy where T : notnull +{ + /// + public abstract T Create(); + + /// + public abstract bool Return(T obj); +} + +/// +/// Defines a method to reset an object to its initial state. +/// +internal interface IResettable +{ + /// + /// Reset the object to a neutral state, semantically similar to when the object was first constructed. + /// + /// if the object was able to reset itself, otherwise . + /// + /// In general, this method is not expected to be thread-safe. + /// + bool TryReset(); +} + +internal sealed class DisposableObjectPool : DefaultObjectPool, IDisposable where T : class +{ + private volatile bool _isDisposed; + + public DisposableObjectPool(IPooledObjectPolicy policy) + : base(policy) + { + } + + public DisposableObjectPool(IPooledObjectPolicy policy, int maximumRetained) + : base(policy, maximumRetained) + { + } + + public override T Get() + { + if (_isDisposed) + { + ThrowObjectDisposedException(); + } + + return base.Get(); + + void ThrowObjectDisposedException() + { + throw new ObjectDisposedException(GetType().Name); + } + } + + public override void Return(T obj) + { + // When the pool is disposed or the obj is not returned to the pool, dispose it + if (_isDisposed || !ReturnCore(obj)) + { + DisposeItem(obj); + } + } + + public void Dispose() + { + _isDisposed = true; + + DisposeItem(FastItem); + FastItem = null; + + while (Items.TryDequeue(out var item)) + { + DisposeItem(item); + } + } + + private static void DisposeItem(T? item) + { + if (item is IDisposable disposable) + { + disposable.Dispose(); + } + } +} + +/// +/// A policy for pooling instances. +/// +internal sealed class StringBuilderPooledObjectPolicy : PooledObjectPolicy +{ + /// + /// Gets or sets the initial capacity of pooled instances. + /// + /// Defaults to 100. + public int InitialCapacity { get; set; } = 100; + + /// + /// Gets or sets the maximum value for that is allowed to be + /// retained, when is invoked. + /// + /// Defaults to 4096. + public int MaximumRetainedCapacity { get; set; } = 4 * 1024; + + /// + public override StringBuilder Create() + { + return new StringBuilder(InitialCapacity); + } + + /// + public override bool Return(StringBuilder obj) + { + if (obj.Capacity > MaximumRetainedCapacity) + { + // Too big. Discard this one. + return false; + } + + obj.Clear(); + return true; + } +} \ No newline at end of file diff --git a/src/Meziantou.Analyzer/Rules/EqualityShouldBeCorrectlyImplementedAnalyzer.cs b/src/Meziantou.Analyzer/Rules/EqualityShouldBeCorrectlyImplementedAnalyzer.cs index 372f18f2c..1ee8a08c7 100644 --- a/src/Meziantou.Analyzer/Rules/EqualityShouldBeCorrectlyImplementedAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/EqualityShouldBeCorrectlyImplementedAnalyzer.cs @@ -162,18 +162,18 @@ private static bool HasMethod(INamedTypeSymbol parentType, Func(6) - { - "op_LessThan", - "op_LessThanOrEqual", - "op_GreaterThan", - "op_GreaterThanOrEqual", - "op_Equality", - "op_Inequality", - }; + { + "op_LessThan", + "op_LessThanOrEqual", + "op_GreaterThan", + "op_GreaterThanOrEqual", + "op_Equality", + "op_Inequality", + }; foreach (var member in parentType.GetAllMembers().OfType()) { - if (member.MethodKind == MethodKind.UserDefinedOperator) + if (member.MethodKind is MethodKind.UserDefinedOperator) { operatorNames.Remove(member.Name); } diff --git a/src/Meziantou.Analyzer/Rules/LoggerParameterTypeAnalyzer.cs b/src/Meziantou.Analyzer/Rules/LoggerParameterTypeAnalyzer.cs index 2a31bd149..a87d84b8f 100644 --- a/src/Meziantou.Analyzer/Rules/LoggerParameterTypeAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/LoggerParameterTypeAnalyzer.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; -using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; @@ -13,6 +12,7 @@ using Microsoft.CodeAnalysis.Text; using Meziantou.Analyzer.Configurations; using Microsoft.CodeAnalysis.CSharp; +using Meziantou.Analyzer.Internals; namespace Meziantou.Analyzer.Rules; @@ -619,7 +619,7 @@ private sealed class LogValuesFormatter public LogValuesFormatter(string format) { - var sb = new StringBuilder(); + var sb = ObjectPool.SharedStringBuilderPool.Get(); var scanIndex = 0; var endIndex = format.Length; @@ -646,6 +646,8 @@ public LogValuesFormatter(string format) scanIndex = closeBraceIndex + 1; } } + + ObjectPool.SharedStringBuilderPool.Return(sb); } public List ValueNames { get; } = []; diff --git a/src/Meziantou.Analyzer/Rules/OptimizeStringBuilderUsageAnalyzer.cs b/src/Meziantou.Analyzer/Rules/OptimizeStringBuilderUsageAnalyzer.cs index f868b139e..d941fc60a 100644 --- a/src/Meziantou.Analyzer/Rules/OptimizeStringBuilderUsageAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/OptimizeStringBuilderUsageAnalyzer.cs @@ -1,6 +1,7 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Text; +using Meziantou.Analyzer.Internals; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; @@ -253,10 +254,11 @@ private static bool IsConstString(IOperation operation) private static bool TryGetConstStringValue(IOperation operation, [NotNullWhen(true)] out string? value) { - var sb = new StringBuilder(); + var sb = ObjectPool.SharedStringBuilderPool.Get(); if (OptimizeStringBuilderUsageAnalyzerCommon.TryGetConstStringValue(operation, sb)) { value = sb.ToString(); + ObjectPool.SharedStringBuilderPool.Return(sb); return true; } diff --git a/src/Meziantou.Analyzer/Rules/OptimizeStringBuilderUsageAnalyzerCommon.cs b/src/Meziantou.Analyzer/Rules/OptimizeStringBuilderUsageAnalyzerCommon.cs index dd6af3f66..4e0cb48db 100644 --- a/src/Meziantou.Analyzer/Rules/OptimizeStringBuilderUsageAnalyzerCommon.cs +++ b/src/Meziantou.Analyzer/Rules/OptimizeStringBuilderUsageAnalyzerCommon.cs @@ -1,4 +1,5 @@ using System.Text; +using Meziantou.Analyzer.Internals; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Operations; @@ -8,9 +9,13 @@ internal static class OptimizeStringBuilderUsageAnalyzerCommon { public static string? GetConstStringValue(IOperation operation) { - var sb = new StringBuilder(); + var sb = ObjectPool.SharedStringBuilderPool.Get(); if (TryGetConstStringValue(operation, sb)) - return sb.ToString(); + { + var result = sb.ToString(); + ObjectPool.SharedStringBuilderPool.Return(sb); + return result; + } return null; } diff --git a/src/Meziantou.Analyzer/Rules/PrimaryConstructorParameterShouldBeReadOnlyAnalyzer.cs b/src/Meziantou.Analyzer/Rules/PrimaryConstructorParameterShouldBeReadOnlyAnalyzer.cs index 3a23cf7fa..decee4588 100644 --- a/src/Meziantou.Analyzer/Rules/PrimaryConstructorParameterShouldBeReadOnlyAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/PrimaryConstructorParameterShouldBeReadOnlyAnalyzer.cs @@ -1,6 +1,7 @@ #if CSHARP12_OR_GREATER using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -86,38 +87,39 @@ private void AnalyzerAssignment(OperationAnalysisContext context) var target = operation.Target; if (target is ITupleOperation) { - foreach (var innerTarget in GetAllAssignmentTargets(target)) + foreach (var innerTarget in GetAllPrimaryCtorAssignmentTargets(target, context.CancellationToken)) { - if (IsPrimaryConstructorParameter(innerTarget, context.CancellationToken)) - { - context.ReportDiagnostic(Rule, innerTarget); - } + context.ReportDiagnostic(Rule, innerTarget); } } else if (IsPrimaryConstructorParameter(target, context.CancellationToken)) { context.ReportDiagnostic(Rule, target); } - } - - private static List GetAllAssignmentTargets(IOperation operation) - { - var result = new List(); - GetAllAssignmentTargets(result, operation); - return result; - static void GetAllAssignmentTargets(List operations, IOperation operation) + static IEnumerable GetAllPrimaryCtorAssignmentTargets(IOperation operation, CancellationToken cancellationToken) { - if (operation is ITupleOperation tuple) + List? result = null; + GetAllAssignmentTargets(ref result, operation, cancellationToken); + return result ?? Enumerable.Empty(); + + static void GetAllAssignmentTargets(ref List? operations, IOperation operation, CancellationToken cancellationToken) { - foreach (var element in tuple.Elements) + if (operation is ITupleOperation tuple) { - GetAllAssignmentTargets(operations, element); + foreach (var element in tuple.Elements) + { + GetAllAssignmentTargets(ref operations, element, cancellationToken); + } + } + else + { + if (IsPrimaryConstructorParameter(operation, cancellationToken)) + { + operations ??= []; + operations.Add(operation); + } } - } - else - { - operations.Add(operation); } } } diff --git a/src/Meziantou.Analyzer/Rules/UseLangwordInXmlCommentAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseLangwordInXmlCommentAnalyzer.cs index 71860da4e..15147b4db 100644 --- a/src/Meziantou.Analyzer/Rules/UseLangwordInXmlCommentAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/UseLangwordInXmlCommentAnalyzer.cs @@ -4,16 +4,16 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.CSharp.Syntax; using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading; -using Microsoft.CodeAnalysis.Text; +using Meziantou.Analyzer.Internals; +using System.Linq.Expressions; namespace Meziantou.Analyzer.Rules; [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class UseLangwordInXmlCommentAnalyzer : DiagnosticAnalyzer { + private static readonly ObjectPool> NodeQueuePool = ObjectPool.Create>(); + private static readonly HashSet CSharpKeywords = new(StringComparer.Ordinal) { "abstract", @@ -142,7 +142,13 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context) // Detect the following patterns // {keyword} // {keyword} - var queue = new Queue(documentation.ChildNodes()); + + var queue = NodeQueuePool.Get(); + foreach (var item in documentation.ChildNodes()) + { + queue.Enqueue(item); + } + while (queue.TryDequeue(out var childNode)) { if (childNode is XmlElementSyntax elementSyntax) @@ -165,7 +171,40 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context) } } } + + NodeQueuePool.Return(queue); } } } + + private sealed class NodeQueuePoolPolicy : IPooledObjectPolicy> + { + private readonly Func, int> _func; + + public NodeQueuePoolPolicy() + { + var field = typeof(Queue).GetField("_array"); + if (field is not null) + { + var param = Expression.Parameter(typeof(Queue)); + var lambda = Expression.Lambda(Expression.Property(Expression.Field(param, field), "Length"), param); + _func = (Func, int>)lambda.Compile(); + } + else + { + _func = item => 0; + } + } + + public Queue Create() => new Queue(capacity: 30); + + public bool Return(Queue obj) + { + if (_func(obj) > 100) + return false; + + obj.Clear(); + return true; + } + } } diff --git a/src/Meziantou.Analyzer/Rules/ValidateArgumentsCorrectlyAnalyzer.cs b/src/Meziantou.Analyzer/Rules/ValidateArgumentsCorrectlyAnalyzer.cs index 6926338ac..d083ee70d 100644 --- a/src/Meziantou.Analyzer/Rules/ValidateArgumentsCorrectlyAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/ValidateArgumentsCorrectlyAnalyzer.cs @@ -41,7 +41,7 @@ public override void Initialize(AnalysisContext context) private sealed class AnalyzerContext { - private readonly List _symbols; + private readonly HashSet _symbols; private readonly INamedTypeSymbol? _argumentExceptionSymbol; public AnalyzerContext(Compilation compilation) @@ -53,7 +53,7 @@ public AnalyzerContext(Compilation compilation) symbols.AddIfNotNull(compilation.GetBestTypeByMetadataName("System.Collections.IEnumerator")); symbols.AddIfNotNull(compilation.GetBestTypeByMetadataName("System.Collections.Generic.IEnumerator`1")); symbols.AddIfNotNull(compilation.GetBestTypeByMetadataName("System.Collections.Generic.IAsyncEnumerator`1")); - _symbols = symbols; + _symbols = new HashSet(symbols, SymbolEqualityComparer.Default); _argumentExceptionSymbol = compilation.GetBestTypeByMetadataName("System.ArgumentException"); }