diff --git a/src/EFCore.Analyzers/StringsUsageInRawQueriesDiagnosticAnalyzer.cs b/src/EFCore.Analyzers/StringsUsageInRawQueriesDiagnosticAnalyzer.cs index faa7e1f7990..4fed2d0adfb 100644 --- a/src/EFCore.Analyzers/StringsUsageInRawQueriesDiagnosticAnalyzer.cs +++ b/src/EFCore.Analyzers/StringsUsageInRawQueriesDiagnosticAnalyzer.cs @@ -186,9 +186,65 @@ IInterpolatedStringOperation interpolatedString when AnalyzeInterpolatedString(i } concatenation when AnalyzeConcatenation(concatenation) => StringConcatenationDescriptor, + // ...an explicit call to string.Format(...) or string.Concat(...) + IInvocationOperation argInvocation + => AnalyzeStringMethodInvocation(argInvocation), + _ => null, }; + private static DiagnosticDescriptor? AnalyzeStringMethodInvocation(IInvocationOperation invocation) + { + if (invocation.TargetMethod.ContainingType.SpecialType != SpecialType.System_String + || invocation.TargetMethod.Name is not (nameof(string.Format) or nameof(string.Concat))) + { + return null; + } + + return HasNonConstantArgument(invocation) ? StringConcatenationDescriptor : null; + } + + private static bool HasNonConstantArgument(IInvocationOperation invocation) + { + foreach (var argument in invocation.Arguments) + { + var value = Unwrap(argument.Value); + + // Implicit params arrays — inspect each element rather than the array itself. + if (argument.ArgumentKind == ArgumentKind.ParamArray + && value is IArrayCreationOperation { Initializer.ElementValues: var elements }) + { + foreach (var element in elements) + { + if (!Unwrap(element).ConstantValue.HasValue) + { + return true; + } + } + + continue; + } + + if (!value.ConstantValue.HasValue) + { + return true; + } + } + + return false; + + // Strip implicit conversions (e.g. boxing to object) so we see the original constant. + static IOperation Unwrap(IOperation operation) + { + while (operation is IConversionOperation { IsImplicit: true } conversion) + { + operation = conversion.Operand; + } + + return operation; + } + } + private static bool AnalyzeInterpolatedString(IInterpolatedStringOperation interpolatedString) { if (interpolatedString.ConstantValue.HasValue) diff --git a/test/EFCore.Analyzers.Tests/StringConcatenationInRawQueriesAnalyzerTests.cs b/test/EFCore.Analyzers.Tests/StringConcatenationInRawQueriesAnalyzerTests.cs index 7f603b10de5..36891201878 100644 --- a/test/EFCore.Analyzers.Tests/StringConcatenationInRawQueriesAnalyzerTests.cs +++ b/test/EFCore.Analyzers.Tests/StringConcatenationInRawQueriesAnalyzerTests.cs @@ -162,6 +162,110 @@ void M(MyDbContext db) db.{{call}}("FooBar WHERE Id = " + Id); } } +""", + DiagnosticResult.CompilerWarning(EFDiagnostics.StringConcatenationUsageInRawQueries).WithLocation(0)); + + [Theory] + [MemberData(nameof(DoNotReportData))] + public Task Constant_string_format_do_not_report(string call) + => Verify.VerifyAnalyzerAsync( + $$""" +{{MyDbContext}} + +class C +{ + void M(MyDbContext db) + { + db.{{call}}(string.Format("FooBar WHERE Id = {0}", "1")); + } +} +"""); + + [Theory] + [MemberData(nameof(ShouldReportData))] + public Task String_format_with_argument_should_report(string call) + => Verify.VerifyAnalyzerAsync( + $$""" +{{MyDbContext}} + +class C +{ + void M(MyDbContext db, string id) + { + db.{{call}}(string.Format("FooBar WHERE Id = {0}", id)); + } +} +""", + DiagnosticResult.CompilerWarning(EFDiagnostics.StringConcatenationUsageInRawQueries).WithLocation(0)); + + [Theory] + [MemberData(nameof(ShouldReportData))] + public Task String_format_with_method_call_should_report(string call) + => Verify.VerifyAnalyzerAsync( + $$""" +{{MyDbContext}} + +class C +{ + void M(MyDbContext db) + { + db.{{call}}(string.Format("FooBar WHERE Id = {0}", GetId())); + } + + string GetId() => "1"; +} +""", + DiagnosticResult.CompilerWarning(EFDiagnostics.StringConcatenationUsageInRawQueries).WithLocation(0)); + + [Theory] + [MemberData(nameof(DoNotReportData))] + public Task Constant_string_concat_do_not_report(string call) + => Verify.VerifyAnalyzerAsync( + $$""" +{{MyDbContext}} + +class C +{ + void M(MyDbContext db) + { + db.{{call}}(string.Concat("FooBar", " WHERE Id = ", "1")); + } +} +"""); + + [Theory] + [MemberData(nameof(ShouldReportData))] + public Task String_concat_with_argument_should_report(string call) + => Verify.VerifyAnalyzerAsync( + $$""" +{{MyDbContext}} + +class C +{ + void M(MyDbContext db, string id) + { + db.{{call}}(string.Concat("FooBar WHERE Id = ", id)); + } +} +""", + DiagnosticResult.CompilerWarning(EFDiagnostics.StringConcatenationUsageInRawQueries).WithLocation(0)); + + [Theory] + [MemberData(nameof(ShouldReportData))] + public Task String_concat_with_method_call_should_report(string call) + => Verify.VerifyAnalyzerAsync( + $$""" +{{MyDbContext}} + +class C +{ + void M(MyDbContext db) + { + db.{{call}}(string.Concat("FooBar WHERE Id = ", GetId())); + } + + string GetId() => "1"; +} """, DiagnosticResult.CompilerWarning(EFDiagnostics.StringConcatenationUsageInRawQueries).WithLocation(0)); }