diff --git a/analyzers/src/SonarAnalyzer.Common/SymbolicExecution/Roslyn/OperationProcessors/Invocation.Enumerable.cs b/analyzers/src/SonarAnalyzer.Common/SymbolicExecution/Roslyn/OperationProcessors/Invocation.Enumerable.cs index 9da7d5cf504..0976e07bf42 100644 --- a/analyzers/src/SonarAnalyzer.Common/SymbolicExecution/Roslyn/OperationProcessors/Invocation.Enumerable.cs +++ b/analyzers/src/SonarAnalyzer.Common/SymbolicExecution/Roslyn/OperationProcessors/Invocation.Enumerable.cs @@ -87,18 +87,34 @@ private static ProgramState[] ProcessLinqEnumerableAndQueryable(ProgramState sta return states.Select(x => x.SetOperationConstraint(invocation, ObjectConstraint.NotNull)).ToArray(); } // ElementAtOrDefault is intentionally not supported. It's causing many FPs - else if (name is nameof(Enumerable.FirstOrDefault) or nameof(Enumerable.LastOrDefault) or nameof(Enumerable.SingleOrDefault)) + else if (name is nameof(Enumerable.FirstOrDefault) or nameof(Enumerable.LastOrDefault) or nameof(Enumerable.SingleOrDefault) + && invocation.TryGetInstance(state) is { } instance + && instance.TrackedSymbol(state) is { } instanceSymbol + && GetElementType(instanceSymbol) is { } elementType) { - return states.SelectMany(x => new List - { - x.SetOperationConstraint(invocation, ObjectConstraint.Null), - x.SetOperationConstraint(invocation, ObjectConstraint.NotNull) - }).ToArray(); + return states.SelectMany(x => ProcessElementOrDefaultMethods(x, invocation, elementType, instanceSymbol)).ToArray(); } else { return states; } + + static IEnumerable ProcessElementOrDefaultMethods(ProgramState state, IInvocationOperationWrapper invocation, ITypeSymbol elementType, ISymbol instanceSymbol) => + state[instanceSymbol]?.Constraint() switch + { + CollectionConstraint constraint when constraint == CollectionConstraint.Empty && elementType.IsReferenceType => [state.SetOperationConstraint(invocation, ObjectConstraint.Null)], + CollectionConstraint constraint when constraint == CollectionConstraint.NotEmpty && elementType.IsReferenceType => + [ + state, + state.SetOperationConstraint(invocation, ObjectConstraint.NotNull) + ], + _ when elementType.IsReferenceType => + [ + state.SetOperationConstraint(invocation, ObjectConstraint.Null), + state.SetOperationConstraint(invocation, ObjectConstraint.NotNull) + ], + _ => state.SetOperationConstraint(invocation, ObjectConstraint.NotNull).ToArray() + }; } private static ProgramState[] ProcessBoolCollectionMethods(ProgramState state, IInvocationOperationWrapper invocation) @@ -129,4 +145,12 @@ static bool HasNoParameters(IMethodSymbol symbol) => (symbol.IsExtensionMethod && symbol.Parameters.Length == 1) || symbol.Parameters.IsEmpty; } + + private static ITypeSymbol GetElementType(ISymbol instance) => + instance.GetSymbolType() switch + { + INamedTypeSymbol { TypeArguments: { Length: 1 } typeArguments } => typeArguments.First(), + IArrayTypeSymbol arrayType => arrayType.ElementType, + _ => null + }; } diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/SymbolicExecution/Roslyn/NullPointerDereference.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/SymbolicExecution/Roslyn/NullPointerDereference.cs index 715bb12caa6..f13762a6b5e 100644 --- a/analyzers/tests/SonarAnalyzer.Test/TestCases/SymbolicExecution/Roslyn/NullPointerDereference.cs +++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/SymbolicExecution/Roslyn/NullPointerDereference.cs @@ -2085,7 +2085,7 @@ void Method(Exception[] array) if (array.Any()) { Exception exception = array.FirstOrDefault(); - Console.WriteLine(exception.Message); // Noncompliant - FP + Console.WriteLine(exception.Message); // Compliant - we know that array is not empty } } }