From 79b5811805b415bc6855af81ee2089ce7995e4e7 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Thu, 22 Feb 2024 21:50:51 +0100 Subject: [PATCH 01/62] WIP: Add the RegisterSymbolStart wrapper --- ...mpilationStartAnalysisContextExtensions.cs | 48 ++++++++----------- .../SymbolStartAnalysisContext.cs | 47 ++++++++++++++++++ .../RegisterSymbolStartActionWrapperTest.cs | 10 ++-- 3 files changed, 73 insertions(+), 32 deletions(-) create mode 100644 analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs index 0bba5ec874e..ec6a6cf4bbe 100644 --- a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs +++ b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs @@ -25,41 +25,33 @@ namespace SonarAnalyzer.ShimLayer.AnalysisContext; public static class CompilationStartAnalysisContextExtensions { - private static readonly Action, SymbolKind> RegisterSymbolStartActionWrapper = - CreateRegisterSymbolStartAnalysisWrapper(); - - public static void RegisterSymbolStartAction(this CompilationStartAnalysisContext context, Action action, SymbolKind symbolKind) => - RegisterSymbolStartActionWrapper(context, action, symbolKind); - - // Code is executed in static initializers and is not detected by the coverage tool - // See the SonarAnalysisContextTest.SonarCompilationStartAnalysisContext_RegisterSymbolStartAction family of tests to check test coverage manually - [ExcludeFromCodeCoverage] - private static Action, SymbolKind> CreateRegisterSymbolStartAnalysisWrapper() { - if (typeof(CompilationStartAnalysisContext).GetMethod(nameof(RegisterSymbolStartAction)) is not { } registerMethod) { return static (_, _, _) => { }; } - var contextParameter = Parameter(typeof(CompilationStartAnalysisContext)); - var shimmedActionParameter = Parameter(typeof(Action)); - var symbolKindParameter = Parameter(typeof(SymbolKind)); + var contextParameter = Parameter(typeof(CompilationStartAnalysisContext)); + var symbolKindParameter = Parameter(typeof(SymbolKind)); - var roslynSymbolStartAnalysisContextType = typeof(CompilationStartAnalysisContext).Assembly.GetType("Microsoft.CodeAnalysis.Diagnostics.SymbolStartAnalysisContext"); - var roslynSymbolStartAnalysisActionType = typeof(Action<>).MakeGenericType(roslynSymbolStartAnalysisContextType); - var roslynSymbolStartAnalysisContextParameter = Parameter(roslynSymbolStartAnalysisContextType); - var sonarSymbolStartAnalysisContextCtor = typeof(SymbolStartAnalysisContextWrapper).GetConstructors().Single(); - // Action registerAction = roslynSymbolStartAnalysisContextParameter => - // shimmedActionParameter.Invoke(new Sonar.SymbolStartAnalysisContextWrapper(roslynSymbolStartAnalysisContextParameter)) - var registerAction = Lambda( - delegateType: roslynSymbolStartAnalysisActionType, - body: Call(shimmedActionParameter, nameof(Action.Invoke), [], New(sonarSymbolStartAnalysisContextCtor, roslynSymbolStartAnalysisContextParameter)), - parameters: roslynSymbolStartAnalysisContextParameter); - // (contextParameter, shimmedActionParameter, symbolKindParameter) => contextParameter.RegisterSymbolStartAction(registerAction, symbolKindParameter) - return Lambda, SymbolKind>>( - Call(contextParameter, registerMethod, registerAction, symbolKindParameter), - contextParameter, shimmedActionParameter, symbolKindParameter).Compile(); + contextParameter, shimmedActionParameter, symbolKindParameter).Compile(); + } + else + { + return static (_, _, _) => { }; + } + + Expression PassThroughLambda(string registrationMethodName) + { + var registerParameter = Parameter(typeof(Action)); + return Lambda>>(Call(symbolStartAnalysisContextParameter, registrationMethodName, [], registerParameter), registerParameter); + } + + MethodCallExpression DebugPrint(Expression expression) => + Call(typeof(Debug).GetMethod(nameof(Debug.WriteLine), [typeof(object)]), Convert(expression, typeof(object))); } + + public static void RegisterSymbolStartAction(this CompilationStartAnalysisContext context, Action action, SymbolKind symbolKind) => + RegisterSymbolStartAnalysisWrapper(context, action, symbolKind); } diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs new file mode 100644 index 00000000000..e6cfdb83558 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs @@ -0,0 +1,47 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2024 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarAnalyzer.ShimLayer.AnalysisContext; + +public class SymbolStartAnalysisContext +{ + private readonly Action> registerCodeBlockAction; + + public SymbolStartAnalysisContext( + CancellationToken cancellationToken, + Compilation compilation, + AnalyzerOptions options, + ISymbol symbol, + Action> registerCodeBlockAction) + { + CancellationToken = cancellationToken; + Compilation = compilation; + Options = options; + Symbol = symbol; + this.registerCodeBlockAction = registerCodeBlockAction; + } + + public CancellationToken CancellationToken { get; } + public Compilation Compilation { get; } + public AnalyzerOptions Options { get; } + public ISymbol Symbol { get; } + + public void RegisterCodeBlockAction(Action action) => registerCodeBlockAction(action); +} diff --git a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs index 07c8e4ad2be..30009d6a822 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs @@ -115,7 +115,7 @@ public async Task RegisterSymbolStartAction_RegisterCodeBlockStartAction_CS() { var snippet = new SnippetCompiler(""" public class C - { + { int i = 0; public void M() => ToString(); } @@ -123,7 +123,7 @@ public class C var visited = new List(); var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create( new TestDiagnosticAnalyzer(symbolStart => - { + { symbolStart.RegisterCodeBlockStartAction(blockStart => { var node = blockStart.CodeBlock.ToString(); @@ -133,7 +133,9 @@ public class C }, SymbolKind.NamedType))); await compilation.GetAnalyzerDiagnosticsAsync(); visited.Should().BeEquivalentTo("int i = 0;", "public void M() => ToString();", "ToString()"); - } + } + public override ImmutableArray SupportedDiagnostics => + ImmutableArray.Create(new DiagnosticDescriptor("TEST", "Test", "Test", "Test", DiagnosticSeverity.Warning, true)); [TestMethod] public async Task RegisterSymbolStartAction_RegisterCodeBlockStartAction_VB() @@ -156,7 +158,7 @@ End Class var node = blockStart.CodeBlock.ToString(); visited.Add(node); blockStart.RegisterSyntaxNodeAction(nodeContext => visited.Add(nodeContext.Node.ToString()), VB.SyntaxKind.InvocationExpression); - }); + }); }, SymbolKind.NamedType))); await compilation.GetAnalyzerDiagnosticsAsync(); visited.Should().BeEquivalentTo([ From d2c4e6c037968a1dc5253394bed831b5f15b90b0 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Tue, 27 Feb 2024 17:13:47 +0100 Subject: [PATCH 02/62] Clean up --- .../CompilationStartAnalysisContextExtensions.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs index ec6a6cf4bbe..0cbb47681f7 100644 --- a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs +++ b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs @@ -26,14 +26,15 @@ namespace SonarAnalyzer.ShimLayer.AnalysisContext; public static class CompilationStartAnalysisContextExtensions { { + if (typeof(CompilationStartAnalysisContext).GetMethod(nameof(RegisterSymbolStartAction)) is { } registerMethod) { return static (_, _, _) => { }; } var contextParameter = Parameter(typeof(CompilationStartAnalysisContext)); var symbolKindParameter = Parameter(typeof(SymbolKind)); - - + var symbolStartAnalysisContextCtor = typeof(SymbolStartAnalysisContext).GetConstructors().Single(); + PassThroughLambda(nameof(SymbolStartAnalysisContext.RegisterCodeBlockAction))))), symbolStartAnalysisContextParameter); contextParameter, shimmedActionParameter, symbolKindParameter).Compile(); } @@ -42,14 +43,11 @@ public static class CompilationStartAnalysisContextExtensions return static (_, _, _) => { }; } - Expression PassThroughLambda(string registrationMethodName) + static Expression>> PassThroughLambda(ParameterExpression symbolStartAnalysisContextParameter, string registrationMethodName) { var registerParameter = Parameter(typeof(Action)); return Lambda>>(Call(symbolStartAnalysisContextParameter, registrationMethodName, [], registerParameter), registerParameter); } - - MethodCallExpression DebugPrint(Expression expression) => - Call(typeof(Debug).GetMethod(nameof(Debug.WriteLine), [typeof(object)]), Convert(expression, typeof(object))); } public static void RegisterSymbolStartAction(this CompilationStartAnalysisContext context, Action action, SymbolKind symbolKind) => From f09a5817956372c0f6b5361c2e31864d6998f11e Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Tue, 27 Feb 2024 17:15:35 +0100 Subject: [PATCH 03/62] Rename --- .../CompilationStartAnalysisContextExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs index 0cbb47681f7..d9a2d94e168 100644 --- a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs +++ b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs @@ -45,8 +45,8 @@ public static class CompilationStartAnalysisContextExtensions static Expression>> PassThroughLambda(ParameterExpression symbolStartAnalysisContextParameter, string registrationMethodName) { - var registerParameter = Parameter(typeof(Action)); - return Lambda>>(Call(symbolStartAnalysisContextParameter, registrationMethodName, [], registerParameter), registerParameter); + var registerActionParameter = Parameter(typeof(Action)); + return Lambda>>(Call(symbolStartAnalysisContextParameter, registrationMethodName, [], registerActionParameter), registerActionParameter); } } From 15659e6dcaee2ab63e4ddfa40ca52c5b27693267 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Wed, 28 Feb 2024 14:57:55 +0100 Subject: [PATCH 04/62] Add RegisterCodeBlockStartActionCS and RegisterOperationAction --- ...mpilationStartAnalysisContextExtensions.cs | 15 +++-- .../SymbolStartAnalysisContext.cs | 28 ++++++++- .../RegisterSymbolStartActionWrapperTest.cs | 57 +++++++++++++++++++ 3 files changed, 94 insertions(+), 6 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs index d9a2d94e168..f7158b774f7 100644 --- a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs +++ b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs @@ -20,13 +20,14 @@ using System.Diagnostics.CodeAnalysis; using static System.Linq.Expressions.Expression; +using CS = Microsoft.CodeAnalysis.CSharp; namespace SonarAnalyzer.ShimLayer.AnalysisContext; public static class CompilationStartAnalysisContextExtensions { { - if (typeof(CompilationStartAnalysisContext).GetMethod(nameof(RegisterSymbolStartAction)) is { } registerMethod) + if (typeof(CompilationStartAnalysisContext).GetMethod(nameof(RegisterSymbolStartAction)) is not { } registerMethod) { return static (_, _, _) => { }; } @@ -40,13 +41,17 @@ public static class CompilationStartAnalysisContextExtensions } else { - return static (_, _, _) => { }; + var registerActionParameter = Parameter(typeof(Action)); + return Lambda>>(Call(symbolStartAnalysisContextParameter, registrationMethodName, typeArguments, registerActionParameter), registerActionParameter); } - static Expression>> PassThroughLambda(ParameterExpression symbolStartAnalysisContextParameter, string registrationMethodName) + static Expression, TParameter>> RegisterLambdaWithAdditionalParameter( + ParameterExpression symbolStartAnalysisContextParameter, string registrationMethodName, params Type[] typeArguments) { - var registerActionParameter = Parameter(typeof(Action)); - return Lambda>>(Call(symbolStartAnalysisContextParameter, registrationMethodName, [], registerActionParameter), registerActionParameter); + var registerActionParameter = Parameter(typeof(Action)); + var additionalParameter = Parameter(typeof(TParameter)); + return Lambda, TParameter>>( + Call(symbolStartAnalysisContextParameter, registrationMethodName, typeArguments, registerActionParameter, additionalParameter), registerActionParameter, additionalParameter); } } diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs index e6cfdb83558..4ca94457039 100644 --- a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs +++ b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs @@ -18,24 +18,33 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using Microsoft.CodeAnalysis; +using CS = Microsoft.CodeAnalysis.CSharp; namespace SonarAnalyzer.ShimLayer.AnalysisContext; public class SymbolStartAnalysisContext { private readonly Action> registerCodeBlockAction; + private readonly Action>> registerCodeBlockStartActionCS; + private readonly Action, ImmutableArray> registerOperationAction; public SymbolStartAnalysisContext( CancellationToken cancellationToken, Compilation compilation, AnalyzerOptions options, ISymbol symbol, - Action> registerCodeBlockAction) + Action> registerCodeBlockAction, + Action>> registerCodeBlockStartActionCS, + Action, ImmutableArray> registerOperationAction + ) { CancellationToken = cancellationToken; Compilation = compilation; Options = options; Symbol = symbol; this.registerCodeBlockAction = registerCodeBlockAction; + this.registerCodeBlockStartActionCS = registerCodeBlockStartActionCS; + this.registerOperationAction = registerOperationAction; } public CancellationToken CancellationToken { get; } @@ -44,4 +53,21 @@ public SymbolStartAnalysisContext( public ISymbol Symbol { get; } public void RegisterCodeBlockAction(Action action) => registerCodeBlockAction(action); + public void RegisterCodeBlockStartAction(Action> action) where TLanguageKindEnum : struct + { + if (typeof(TLanguageKindEnum) == typeof(CS.SyntaxKind)) + { + var casted = (Action>)action; + registerCodeBlockStartActionCS(casted); + } + else if (typeof(TLanguageKindEnum).FullName == "Microsoft.CodeAnalysis.VisualBasic.SyntaxKind") + { + throw new NotImplementedException("Add a reference to the Microsoft.CodeAnalysis.VisualBasic.Workspaces package."); + } + else + { + throw new ArgumentException("Invalid type parameter.", nameof(TLanguageKindEnum)); + } + } + public void RegisterOperationAction(Action action, ImmutableArray operationKinds) => registerOperationAction(action, operationKinds); } diff --git a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs index 30009d6a822..8cc6234f8fe 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs @@ -341,4 +341,61 @@ public override void Initialize(Microsoft.CodeAnalysis.Diagnostics.AnalysisConte context.RegisterCompilationStartAction(start => CompilationStartAnalysisContextExtensions.RegisterSymbolStartAction(start, Action, SymbolKind)); } + + [TestMethod] + public async Task RegisterSymbolStartAction_RegisterCodeBlockStartAction() + { + var code = """ + public class C + { + int i = 0; + public void M() + { + ToString(); + } + } + """; + var snippet = new SnippetCompiler(code); + var visited = new List(); + var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create( + new TestDiagnosticAnalyzer(symbolStart => + { + symbolStart.RegisterCodeBlockStartAction(blockStart => + { + var node = blockStart.CodeBlock.ToString(); + visited.Add(node.Substring(0, node.IndexOf('\n') is var pos and >= 0 ? pos : node.Length)); + blockStart.RegisterSyntaxNodeAction(nodeContext => visited.Add(nodeContext.Node.ToString()), CS.SyntaxKind.InvocationExpression); + }); + }, SymbolKind.NamedType))); + await compilation.GetAnalyzerDiagnosticsAsync(); + visited.Should().BeEquivalentTo("int i = 0;", "public void M()", "ToString()"); + } + + [TestMethod] + public async Task RegisterSymbolStartAction_RegisterOperationAction() + { + var code = """ + public class C + { + int i = 0; + public void M() + { + ToString(); + } + } + """; + var snippet = new SnippetCompiler(code); + var visited = new List(); + var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create( + new TestDiagnosticAnalyzer(symbolStart => + { + symbolStart.RegisterOperationAction(operationContext => + { + var operation = operationContext.Operation.Syntax.ToString(); + visited.Add(operation); + }, ImmutableArray.Create(OperationKind.Invocation)); + }, SymbolKind.NamedType))); + await compilation.GetAnalyzerDiagnosticsAsync(); + visited.Should().BeEquivalentTo("ToString()"); + } } From 3a727417b0881b65e6cf8a9b905f4d3a791e892b Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Wed, 28 Feb 2024 17:34:45 +0100 Subject: [PATCH 05/62] Add remaining register methods --- .../SymbolStartAnalysisContext.cs | 51 ++++++++- .../RegisterSymbolStartActionWrapperTest.cs | 101 ++++++++++++++++++ 2 files changed, 147 insertions(+), 5 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs index 4ca94457039..79c46337668 100644 --- a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs +++ b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using CS = Microsoft.CodeAnalysis.CSharp; namespace SonarAnalyzer.ShimLayer.AnalysisContext; @@ -27,6 +27,10 @@ public class SymbolStartAnalysisContext private readonly Action> registerCodeBlockAction; private readonly Action>> registerCodeBlockStartActionCS; private readonly Action, ImmutableArray> registerOperationAction; + private readonly Action> registerOperationBlockAction; + private readonly Action> registerOperationBlockStartAction; + private readonly Action> registerSymbolEndAction; + private readonly Action, ImmutableArray> registerSyntaxNodeActionCS; public SymbolStartAnalysisContext( CancellationToken cancellationToken, @@ -35,8 +39,12 @@ public SymbolStartAnalysisContext( ISymbol symbol, Action> registerCodeBlockAction, Action>> registerCodeBlockStartActionCS, - Action, ImmutableArray> registerOperationAction - ) + Action, ImmutableArray> registerOperationAction, + Action> registerOperationBlockAction, + Action> registerOperationBlockStartAction, + Action> registerSymbolEndAction, + Action, ImmutableArray> registerSyntaxNodeActionCS + ) { CancellationToken = cancellationToken; Compilation = compilation; @@ -45,6 +53,10 @@ Action, ImmutableArray> register this.registerCodeBlockAction = registerCodeBlockAction; this.registerCodeBlockStartActionCS = registerCodeBlockStartActionCS; this.registerOperationAction = registerOperationAction; + this.registerOperationBlockAction = registerOperationBlockAction; + this.registerOperationBlockStartAction = registerOperationBlockStartAction; + this.registerSymbolEndAction = registerSymbolEndAction; + this.registerSyntaxNodeActionCS = registerSyntaxNodeActionCS; } public CancellationToken CancellationToken { get; } @@ -52,7 +64,9 @@ Action, ImmutableArray> register public AnalyzerOptions Options { get; } public ISymbol Symbol { get; } - public void RegisterCodeBlockAction(Action action) => registerCodeBlockAction(action); + public void RegisterCodeBlockAction(Action action) => + registerCodeBlockAction(action); + public void RegisterCodeBlockStartAction(Action> action) where TLanguageKindEnum : struct { if (typeof(TLanguageKindEnum) == typeof(CS.SyntaxKind)) @@ -69,5 +83,32 @@ public void RegisterCodeBlockStartAction(Action action, ImmutableArray operationKinds) => registerOperationAction(action, operationKinds); + + public void RegisterOperationAction(Action action, ImmutableArray operationKinds) => + registerOperationAction(action, operationKinds); + + public void RegisterOperationBlockAction(Action action) => + registerOperationBlockAction(action); + + public void RegisterOperationBlockStartAction(Action action) => + registerOperationBlockStartAction(action); + + public void RegisterSymbolEndAction(Action action) => + registerSymbolEndAction(action); + + public void RegisterSyntaxNodeAction(Action action, params TLanguageKindEnum[] syntaxKinds) where TLanguageKindEnum : struct + { + if (typeof(TLanguageKindEnum) == typeof(CS.SyntaxKind)) + { + registerSyntaxNodeActionCS(action, syntaxKinds.Cast().ToImmutableArray()); + } + else if (typeof(TLanguageKindEnum).FullName == "Microsoft.CodeAnalysis.VisualBasic.SyntaxKind") + { + throw new NotImplementedException("Add a reference to the Microsoft.CodeAnalysis.VisualBasic.Workspaces package."); + } + else + { + throw new ArgumentException("Invalid type parameter.", nameof(TLanguageKindEnum)); + } + } } diff --git a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs index 8cc6234f8fe..5daf8708936 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs @@ -398,4 +398,105 @@ public void M() await compilation.GetAnalyzerDiagnosticsAsync(); visited.Should().BeEquivalentTo("ToString()"); } + + [TestMethod] + public async Task RegisterSymbolStartAction_RegisterOperationBlockAction() + { + var code = """ + public class C + { + int i = 0; + public void M() => ToString(); + } + """; + var snippet = new SnippetCompiler(code); + var visited = new List(); + var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create( + new TestDiagnosticAnalyzer(symbolStart => + { + symbolStart.RegisterOperationBlockAction(operationBlockContext => + { + var operation = operationBlockContext.OperationBlocks.First().Syntax.ToString(); + visited.Add(operation); + }); + }, SymbolKind.NamedType))); + await compilation.GetAnalyzerDiagnosticsAsync(); + visited.Should().BeEquivalentTo("= 0", "=> ToString()"); + } + + [TestMethod] + public async Task RegisterSymbolStartAction_RegisterOperationBlockStartAction() + { + var code = """ + public class C + { + int i = 0; + public void M() => ToString(); + } + """; + var snippet = new SnippetCompiler(code); + var visited = new List(); + var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create( + new TestDiagnosticAnalyzer(symbolStart => + { + symbolStart.RegisterOperationBlockStartAction(operationBlockStartContext => + { + var operation = operationBlockStartContext.OperationBlocks.First().Syntax.ToString(); + visited.Add(operation); + operationBlockStartContext.RegisterOperationAction(operationContext => visited.Add(operationContext.Operation.Syntax.ToString()), OperationKind.Invocation); + }); + }, SymbolKind.NamedType))); + await compilation.GetAnalyzerDiagnosticsAsync(); + visited.Should().BeEquivalentTo("= 0", "=> ToString()", "ToString()"); + } + + [TestMethod] + public async Task RegisterSymbolStartAction_RegisterRegisterSymbolEndAction() + { + var code = """ + public class C + { + int i = 0; + public void M() => ToString(); + } + """; + var snippet = new SnippetCompiler(code); + var visited = new List(); + var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create( + new TestDiagnosticAnalyzer(symbolStart => + { + symbolStart.RegisterSymbolEndAction(symbolContext => + { + var symbolName = symbolContext.Symbol.Name; + visited.Add(symbolName); + }); + }, SymbolKind.NamedType))); + await compilation.GetAnalyzerDiagnosticsAsync(); + visited.Should().BeEquivalentTo("C"); + } + + [TestMethod] + public async Task RegisterSymbolStartAction_RegisterSyntaxNodeAction() + { + var code = """ + public class C + { + int i = 0; + public void M() => ToString(); + } + """; + var snippet = new SnippetCompiler(code); + var visited = new List(); + var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create( + new TestDiagnosticAnalyzer(symbolStart => + { + symbolStart.RegisterSyntaxNodeAction(syntaxNodeContext => + { + var symbolName = syntaxNodeContext.Node.ToString(); + visited.Add(symbolName); + }, CS.SyntaxKind.InvocationExpression, CS.SyntaxKind.EqualsValueClause); + }, SymbolKind.NamedType))); + await compilation.GetAnalyzerDiagnosticsAsync(); + visited.Should().BeEquivalentTo("= 0", "ToString()"); + } } From f56b0d27a68884f3952d256247e940a5b8b0aee5 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Thu, 29 Feb 2024 11:39:13 +0100 Subject: [PATCH 06/62] Clean up --- .../CompilationStartAnalysisContextExtensions.cs | 5 ++--- .../AnalysisContext/SymbolStartAnalysisContext.cs | 13 +++++++------ .../RegisterSymbolStartActionWrapperTest.cs | 13 +++++-------- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs index f7158b774f7..b4f2f0ca476 100644 --- a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs +++ b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs @@ -27,6 +27,7 @@ namespace SonarAnalyzer.ShimLayer.AnalysisContext; public static class CompilationStartAnalysisContextExtensions { { +#pragma warning disable S103 // Lines should not be too long if (typeof(CompilationStartAnalysisContext).GetMethod(nameof(RegisterSymbolStartAction)) is not { } registerMethod) { return static (_, _, _) => { }; @@ -53,8 +54,6 @@ static Expression, TParameter>> RegisterLambdaWithAdditi return Lambda, TParameter>>( Call(symbolStartAnalysisContextParameter, registrationMethodName, typeArguments, registerActionParameter, additionalParameter), registerActionParameter, additionalParameter); } +#pragma warning restore S103 // Lines should not be too long } - - public static void RegisterSymbolStartAction(this CompilationStartAnalysisContext context, Action action, SymbolKind symbolKind) => - RegisterSymbolStartAnalysisWrapper(context, action, symbolKind); } diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs index 79c46337668..5fdfae4efa5 100644 --- a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs +++ b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs @@ -43,8 +43,7 @@ public SymbolStartAnalysisContext( Action> registerOperationBlockAction, Action> registerOperationBlockStartAction, Action> registerSymbolEndAction, - Action, ImmutableArray> registerSyntaxNodeActionCS - ) + Action, ImmutableArray> registerSyntaxNodeActionCS) { CancellationToken = cancellationToken; Compilation = compilation; @@ -69,12 +68,13 @@ public void RegisterCodeBlockAction(Action action) => public void RegisterCodeBlockStartAction(Action> action) where TLanguageKindEnum : struct { - if (typeof(TLanguageKindEnum) == typeof(CS.SyntaxKind)) + var languageKindType = typeof(TLanguageKindEnum); + if (languageKindType == typeof(CS.SyntaxKind)) { var casted = (Action>)action; registerCodeBlockStartActionCS(casted); } - else if (typeof(TLanguageKindEnum).FullName == "Microsoft.CodeAnalysis.VisualBasic.SyntaxKind") + else if (languageKindType.FullName == "Microsoft.CodeAnalysis.VisualBasic.SyntaxKind") { throw new NotImplementedException("Add a reference to the Microsoft.CodeAnalysis.VisualBasic.Workspaces package."); } @@ -98,11 +98,12 @@ public void RegisterSymbolEndAction(Action action) => public void RegisterSyntaxNodeAction(Action action, params TLanguageKindEnum[] syntaxKinds) where TLanguageKindEnum : struct { - if (typeof(TLanguageKindEnum) == typeof(CS.SyntaxKind)) + var languageKindType = typeof(TLanguageKindEnum); + if (languageKindType == typeof(CS.SyntaxKind)) { registerSyntaxNodeActionCS(action, syntaxKinds.Cast().ToImmutableArray()); } - else if (typeof(TLanguageKindEnum).FullName == "Microsoft.CodeAnalysis.VisualBasic.SyntaxKind") + else if (languageKindType.FullName == "Microsoft.CodeAnalysis.VisualBasic.SyntaxKind") { throw new NotImplementedException("Add a reference to the Microsoft.CodeAnalysis.VisualBasic.Workspaces package."); } diff --git a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs index 5daf8708936..b1db16fa3ad 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs @@ -349,10 +349,7 @@ public async Task RegisterSymbolStartAction_RegisterCodeBlockStartAction() public class C { int i = 0; - public void M() - { - ToString(); - } + public void M() => ToString(); } """; var snippet = new SnippetCompiler(code); @@ -363,12 +360,12 @@ public void M() symbolStart.RegisterCodeBlockStartAction(blockStart => { var node = blockStart.CodeBlock.ToString(); - visited.Add(node.Substring(0, node.IndexOf('\n') is var pos and >= 0 ? pos : node.Length)); + visited.Add(node); blockStart.RegisterSyntaxNodeAction(nodeContext => visited.Add(nodeContext.Node.ToString()), CS.SyntaxKind.InvocationExpression); }); }, SymbolKind.NamedType))); await compilation.GetAnalyzerDiagnosticsAsync(); - visited.Should().BeEquivalentTo("int i = 0;", "public void M()", "ToString()"); + visited.Should().BeEquivalentTo("int i = 0;", "public void M() => ToString();", "ToString()"); } [TestMethod] @@ -492,8 +489,8 @@ public class C { symbolStart.RegisterSyntaxNodeAction(syntaxNodeContext => { - var symbolName = syntaxNodeContext.Node.ToString(); - visited.Add(symbolName); + var nodeName = syntaxNodeContext.Node.ToString(); + visited.Add(nodeName); }, CS.SyntaxKind.InvocationExpression, CS.SyntaxKind.EqualsValueClause); }, SymbolKind.NamedType))); await compilation.GetAnalyzerDiagnosticsAsync(); From 203a1d000efe2d2bada831790857aa301c984219 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Thu, 29 Feb 2024 14:29:20 +0100 Subject: [PATCH 07/62] Add tests for missing VB support --- .../SymbolStartAnalysisContext.cs | 4 +- .../RegisterSymbolStartActionWrapperTest.cs | 63 ++++++++++++++++++- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs index 5fdfae4efa5..7293a496cca 100644 --- a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs +++ b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs @@ -30,11 +30,11 @@ public class SymbolStartAnalysisContext private readonly Action> registerOperationBlockAction; private readonly Action> registerOperationBlockStartAction; private readonly Action> registerSymbolEndAction; - private readonly Action, ImmutableArray> registerSyntaxNodeActionCS; + private readonly Action, ImmutableArray> registerSyntaxNodeActionCS; public SymbolStartAnalysisContext( CancellationToken cancellationToken, - Compilation compilation, + Compilation compilation, AnalyzerOptions options, ISymbol symbol, Action> registerCodeBlockAction, diff --git a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs index b1db16fa3ad..c19012f41e2 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs @@ -368,6 +368,37 @@ public class C visited.Should().BeEquivalentTo("int i = 0;", "public void M() => ToString();", "ToString()"); } + [TestMethod] + public async Task RegisterSymbolStartAction_RegisterCodeBlockStartAction_VB() + { + var code = """ + Public Class C + Private i As Integer = 0 + + Public Sub M() + Call ToString() + End Sub + End Class + """; + var snippet = new SnippetCompiler(code, ignoreErrors: false, AnalyzerLanguage.VisualBasic); + var visited = new List(); + var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create( + new TestDiagnosticAnalyzer(symbolStart => + { + symbolStart.RegisterCodeBlockStartAction(blockStart => + { + var node = blockStart.CodeBlock.ToString(); + visited.Add(node); + blockStart.RegisterSyntaxNodeAction(nodeContext => visited.Add(nodeContext.Node.ToString()), VB.SyntaxKind.InvocationExpression); + }); + }, SymbolKind.NamedType))); + var diag = await compilation.GetAnalyzerDiagnosticsAsync(); + var ad0001 = diag.Should().ContainSingle().Which; + ad0001.Id.Should().Be("AD0001"); + ad0001.Descriptor.Description.ToString().Should().Contain("System.NotImplementedException: Add a reference to the Microsoft.CodeAnalysis.VisualBasic.Workspaces package"); + visited.Should().BeEmpty(because: "The vb version requires the Microsoft.CodeAnalysis.VisualBasic.Workspaces package to be added. VB.SyntaxKind is not available in the shim layer."); + } + [TestMethod] public async Task RegisterSymbolStartAction_RegisterOperationAction() { @@ -473,7 +504,7 @@ public class C } [TestMethod] - public async Task RegisterSymbolStartAction_RegisterSyntaxNodeAction() + public async Task RegisterSymbolStartAction_RegisterSyntaxNodeAction_CS() { var code = """ public class C @@ -496,4 +527,34 @@ public class C await compilation.GetAnalyzerDiagnosticsAsync(); visited.Should().BeEquivalentTo("= 0", "ToString()"); } + + [TestMethod] + public async Task RegisterSymbolStartAction_RegisterSyntaxNodeAction_VB() + { + var code = """ + Public Class C + Private i As Integer = 0 + + Public Sub M() + Call ToString() + End Sub + End Class + """; + var snippet = new SnippetCompiler(code, ignoreErrors: false, AnalyzerLanguage.VisualBasic); + var visited = new List(); + var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create( + new TestDiagnosticAnalyzer(symbolStart => + { + symbolStart.RegisterSyntaxNodeAction(syntaxNodeContext => + { + var nodeName = syntaxNodeContext.Node.ToString(); + visited.Add(nodeName); + }, VB.SyntaxKind.InvocationExpression); + }, SymbolKind.NamedType))); + var diag = await compilation.GetAnalyzerDiagnosticsAsync(); + var ad0001 = diag.Should().ContainSingle().Which; + ad0001.Id.Should().Be("AD0001"); + ad0001.Descriptor.Description.ToString().Should().Contain("System.NotImplementedException: Add a reference to the Microsoft.CodeAnalysis.VisualBasic.Workspaces package"); + visited.Should().BeEmpty(because: "The vb version requires the Microsoft.CodeAnalysis.VisualBasic.Workspaces package to be added. VB.SyntaxKind is not available in the shim layer."); + } } From aa567ef338ae1ab4108e76d71dee7c8ee689465f Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Thu, 29 Feb 2024 14:38:29 +0100 Subject: [PATCH 08/62] Add comments --- .../AnalysisContext/CompilationStartAnalysisContextExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs index b4f2f0ca476..2302fc5c7a5 100644 --- a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs +++ b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs @@ -46,6 +46,7 @@ public static class CompilationStartAnalysisContextExtensions return Lambda>>(Call(symbolStartAnalysisContextParameter, registrationMethodName, typeArguments, registerActionParameter), registerActionParameter); } + // (registerActionParameter, additionalParameter) => symbolStartAnalysisContextParameter."registrationMethodName"(registerActionParameter, additionalParameter) static Expression, TParameter>> RegisterLambdaWithAdditionalParameter( ParameterExpression symbolStartAnalysisContextParameter, string registrationMethodName, params Type[] typeArguments) { From 02d7aa75e4b63887a623a7088bf0d3ae2a5db9a2 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Thu, 29 Feb 2024 15:48:49 +0100 Subject: [PATCH 09/62] Move TestDiagnosticAnalyzer --- .../RegisterSymbolStartActionWrapperTest.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs index c19012f41e2..fee5ad08c3a 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs @@ -557,4 +557,25 @@ End Class ad0001.Descriptor.Description.ToString().Should().Contain("System.NotImplementedException: Add a reference to the Microsoft.CodeAnalysis.VisualBasic.Workspaces package"); visited.Should().BeEmpty(because: "The vb version requires the Microsoft.CodeAnalysis.VisualBasic.Workspaces package to be added. VB.SyntaxKind is not available in the shim layer."); } + +#pragma warning disable RS1001 // Missing diagnostic analyzer attribute +#pragma warning disable RS1025 // Configure generated code analysis +#pragma warning disable RS1026 // Enable concurrent execution + private class TestDiagnosticAnalyzer : DiagnosticAnalyzer + { + public TestDiagnosticAnalyzer(Action action, SymbolKind symbolKind) + { + Action = action; + SymbolKind = symbolKind; + } + public override ImmutableArray SupportedDiagnostics => + ImmutableArray.Create(new DiagnosticDescriptor("TEST", "Test", "Test", "Test", DiagnosticSeverity.Warning, true)); + + public Action Action { get; } + public SymbolKind SymbolKind { get; } + + public override void Initialize(Microsoft.CodeAnalysis.Diagnostics.AnalysisContext context) => + context.RegisterCompilationStartAction(start => + CompilationStartAnalysisContextExtensions.RegisterSymbolStartAction(start, Action, SymbolKind)); + } } From 7deaaec0b9b07dfe9b00f280bb5d553c272005cc Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Thu, 29 Feb 2024 16:05:30 +0100 Subject: [PATCH 10/62] Cleanup --- .../ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs index 7293a496cca..9b56c24beee 100644 --- a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs +++ b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using Microsoft.CodeAnalysis.CSharp; using CS = Microsoft.CodeAnalysis.CSharp; namespace SonarAnalyzer.ShimLayer.AnalysisContext; @@ -34,7 +33,7 @@ public class SymbolStartAnalysisContext public SymbolStartAnalysisContext( CancellationToken cancellationToken, - Compilation compilation, + Compilation compilation, AnalyzerOptions options, ISymbol symbol, Action> registerCodeBlockAction, From 383b87f610e126fc33a47ae86f70fed5e8b506fd Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 4 Mar 2024 11:33:07 +0100 Subject: [PATCH 11/62] Use property accessor instead of passing per ctor --- .../SymbolStartAnalysisContext.cs | 45 +++++++++++++------ .../RegisterSymbolStartActionWrapperTest.cs | 28 +++++++++++- 2 files changed, 58 insertions(+), 15 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs index 9b56c24beee..1ffc429cf0d 100644 --- a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs +++ b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs @@ -18,11 +18,18 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using static System.Linq.Expressions.Expression; using CS = Microsoft.CodeAnalysis.CSharp; + namespace SonarAnalyzer.ShimLayer.AnalysisContext; public class SymbolStartAnalysisContext { + private static Func cancellationTokenAccessor; + private static Func compilationAccessor; + private static Func optionsAccessor; + private static Func symbolAccessor; + private readonly Action> registerCodeBlockAction; private readonly Action>> registerCodeBlockStartActionCS; private readonly Action, ImmutableArray> registerOperationAction; @@ -31,11 +38,17 @@ public class SymbolStartAnalysisContext private readonly Action> registerSymbolEndAction; private readonly Action, ImmutableArray> registerSyntaxNodeActionCS; + static SymbolStartAnalysisContext() + { + var symbolStartAnalysisContextType = typeof(CompilationStartAnalysisContext).Assembly.GetType("Microsoft.CodeAnalysis.Diagnostics.SymbolStartAnalysisContext"); + cancellationTokenAccessor = CreatePropertyAccessor(symbolStartAnalysisContextType, nameof(CancellationToken)); + compilationAccessor = CreatePropertyAccessor(symbolStartAnalysisContextType, nameof(Compilation)); + optionsAccessor = CreatePropertyAccessor(symbolStartAnalysisContextType, nameof(Options)); + symbolAccessor = CreatePropertyAccessor(symbolStartAnalysisContextType, nameof(Symbol)); + } + public SymbolStartAnalysisContext( - CancellationToken cancellationToken, - Compilation compilation, - AnalyzerOptions options, - ISymbol symbol, + object roslynSymbolStartAnalysisContext, Action> registerCodeBlockAction, Action>> registerCodeBlockStartActionCS, Action, ImmutableArray> registerOperationAction, @@ -44,10 +57,7 @@ public SymbolStartAnalysisContext( Action> registerSymbolEndAction, Action, ImmutableArray> registerSyntaxNodeActionCS) { - CancellationToken = cancellationToken; - Compilation = compilation; - Options = options; - Symbol = symbol; + RoslynSymbolStartAnalysisContext = roslynSymbolStartAnalysisContext; this.registerCodeBlockAction = registerCodeBlockAction; this.registerCodeBlockStartActionCS = registerCodeBlockStartActionCS; this.registerOperationAction = registerOperationAction; @@ -56,11 +66,11 @@ public SymbolStartAnalysisContext( this.registerSymbolEndAction = registerSymbolEndAction; this.registerSyntaxNodeActionCS = registerSyntaxNodeActionCS; } - - public CancellationToken CancellationToken { get; } - public Compilation Compilation { get; } - public AnalyzerOptions Options { get; } - public ISymbol Symbol { get; } + public object RoslynSymbolStartAnalysisContext { get; } + public CancellationToken CancellationToken => cancellationTokenAccessor(RoslynSymbolStartAnalysisContext); + public Compilation Compilation => compilationAccessor(RoslynSymbolStartAnalysisContext); + public AnalyzerOptions Options => optionsAccessor(RoslynSymbolStartAnalysisContext); + public ISymbol Symbol => symbolAccessor(RoslynSymbolStartAnalysisContext); public void RegisterCodeBlockAction(Action action) => registerCodeBlockAction(action); @@ -111,4 +121,13 @@ public void RegisterSyntaxNodeAction(Action CreatePropertyAccessor(Type symbolStartAnalysisContextType, string propertyName) + { + var symbolStartAnalysisContextParameter = Parameter(typeof(object)); + return Lambda>( + Property( + Convert(symbolStartAnalysisContextParameter, symbolStartAnalysisContextType), propertyName), + symbolStartAnalysisContextParameter).Compile(); + } } diff --git a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs index fee5ad08c3a..89eef88dac3 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs @@ -20,6 +20,7 @@ using SonarAnalyzer.ShimLayer.AnalysisContext; using CS = Microsoft.CodeAnalysis.CSharp; +using SonarSymbolStartAnalysisContext = SonarAnalyzer.ShimLayer.AnalysisContext.SymbolStartAnalysisContext; using VB = Microsoft.CodeAnalysis.VisualBasic; namespace SonarAnalyzer.Test.Wrappers; @@ -27,6 +28,28 @@ namespace SonarAnalyzer.Test.Wrappers; [TestClass] public class RegisterSymbolStartActionWrapperTest { + [TestMethod] + public async Task RegisterSymbolStartAction_SymbolStartProperties() + { + var code = """ + public class C + { + int i = 0; + public void M() => ToString(); + } + """; + var snippet = new SnippetCompiler(code); + var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create( + new TestDiagnosticAnalyzer(symbolStart => + { + symbolStart.CancellationToken.IsCancellationRequested.Should().BeFalse(); + symbolStart.Compilation.SyntaxTrees.Should().ContainSingle(); + symbolStart.Options.Should().NotBeNull(); + symbolStart.Symbol.Should().BeAssignableTo().Which.Name.Should().Be("C"); + }, SymbolKind.NamedType))); + await compilation.GetAnalyzerDiagnosticsAsync(); + } + [TestMethod] public async Task RegisterSymbolStartAction_SymbolStartProperties() { @@ -68,6 +91,7 @@ public class C { symbolStart.RegisterCodeBlockAction(block => { + var c = symbolStart.CancellationToken; var node = block.CodeBlock.ToString(); visitedCodeBlocks.Add(node); }); @@ -563,7 +587,7 @@ End Class #pragma warning disable RS1026 // Enable concurrent execution private class TestDiagnosticAnalyzer : DiagnosticAnalyzer { - public TestDiagnosticAnalyzer(Action action, SymbolKind symbolKind) + public TestDiagnosticAnalyzer(Action action, SymbolKind symbolKind) { Action = action; SymbolKind = symbolKind; @@ -571,7 +595,7 @@ public TestDiagnosticAnalyzer(Action SupportedDiagnostics => ImmutableArray.Create(new DiagnosticDescriptor("TEST", "Test", "Test", "Test", DiagnosticSeverity.Warning, true)); - public Action Action { get; } + public Action Action { get; } public SymbolKind SymbolKind { get; } public override void Initialize(Microsoft.CodeAnalysis.Diagnostics.AnalysisContext context) => From 2a2ffaa6ee011cd8690f1713745fb4fe5aefc002 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 4 Mar 2024 12:00:10 +0100 Subject: [PATCH 12/62] Move registerCodeBlockAction and registerCodeBlockStartActionCS to SymbolStartAnalysisContext --- .../SymbolStartAnalysisContext.cs | 23 ++++++++++++------- .../RegisterSymbolStartActionWrapperTest.cs | 3 +-- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs index 1ffc429cf0d..44e6ca9ce57 100644 --- a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs +++ b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs @@ -29,9 +29,9 @@ public class SymbolStartAnalysisContext private static Func compilationAccessor; private static Func optionsAccessor; private static Func symbolAccessor; + private static Action> registerCodeBlockAction; + private static Action>> registerCodeBlockStartActionCS; - private readonly Action> registerCodeBlockAction; - private readonly Action>> registerCodeBlockStartActionCS; private readonly Action, ImmutableArray> registerOperationAction; private readonly Action> registerOperationBlockAction; private readonly Action> registerOperationBlockStartAction; @@ -45,12 +45,21 @@ static SymbolStartAnalysisContext() compilationAccessor = CreatePropertyAccessor(symbolStartAnalysisContextType, nameof(Compilation)); optionsAccessor = CreatePropertyAccessor(symbolStartAnalysisContextType, nameof(Options)); symbolAccessor = CreatePropertyAccessor(symbolStartAnalysisContextType, nameof(Symbol)); + registerCodeBlockAction = CreateRegistrationMethod(symbolStartAnalysisContextType, nameof(RegisterCodeBlockAction)); + registerCodeBlockStartActionCS = CreateRegistrationMethod>(symbolStartAnalysisContextType, nameof(RegisterCodeBlockStartAction), typeof(CS.SyntaxKind)); } + private static Action> CreateRegistrationMethod(Type symbolStartAnalysisContextType, string registrationMethodName, params Type[] typeArguments) + { + var receiver = Parameter(typeof(object)); + var registerActionParameter = Parameter(typeof(Action)); + return Lambda>>( + Call(Convert(receiver, symbolStartAnalysisContextType), registrationMethodName, typeArguments, registerActionParameter), receiver, registerActionParameter).Compile(); + } + + public SymbolStartAnalysisContext( object roslynSymbolStartAnalysisContext, - Action> registerCodeBlockAction, - Action>> registerCodeBlockStartActionCS, Action, ImmutableArray> registerOperationAction, Action> registerOperationBlockAction, Action> registerOperationBlockStartAction, @@ -58,8 +67,6 @@ public SymbolStartAnalysisContext( Action, ImmutableArray> registerSyntaxNodeActionCS) { RoslynSymbolStartAnalysisContext = roslynSymbolStartAnalysisContext; - this.registerCodeBlockAction = registerCodeBlockAction; - this.registerCodeBlockStartActionCS = registerCodeBlockStartActionCS; this.registerOperationAction = registerOperationAction; this.registerOperationBlockAction = registerOperationBlockAction; this.registerOperationBlockStartAction = registerOperationBlockStartAction; @@ -73,7 +80,7 @@ public SymbolStartAnalysisContext( public ISymbol Symbol => symbolAccessor(RoslynSymbolStartAnalysisContext); public void RegisterCodeBlockAction(Action action) => - registerCodeBlockAction(action); + registerCodeBlockAction(RoslynSymbolStartAnalysisContext, action); public void RegisterCodeBlockStartAction(Action> action) where TLanguageKindEnum : struct { @@ -81,7 +88,7 @@ public void RegisterCodeBlockStartAction(Action>)action; - registerCodeBlockStartActionCS(casted); + registerCodeBlockStartActionCS(RoslynSymbolStartAnalysisContext, casted); } else if (languageKindType.FullName == "Microsoft.CodeAnalysis.VisualBasic.SyntaxKind") { diff --git a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs index 89eef88dac3..0fc87514bc0 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs @@ -91,7 +91,6 @@ public class C { symbolStart.RegisterCodeBlockAction(block => { - var c = symbolStart.CancellationToken; var node = block.CodeBlock.ToString(); visitedCodeBlocks.Add(node); }); @@ -498,7 +497,7 @@ public class C operationBlockStartContext.RegisterOperationAction(operationContext => visited.Add(operationContext.Operation.Syntax.ToString()), OperationKind.Invocation); }); }, SymbolKind.NamedType))); - await compilation.GetAnalyzerDiagnosticsAsync(); + var diag = await compilation.GetAnalyzerDiagnosticsAsync(); visited.Should().BeEquivalentTo("= 0", "=> ToString()", "ToString()"); } From 2c60f8cbeb2754b923cae438febba361852a2a34 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 4 Mar 2024 12:09:42 +0100 Subject: [PATCH 13/62] Move registerOperationAction --- .../SymbolStartAnalysisContext.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs index 44e6ca9ce57..95ba3e07c17 100644 --- a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs +++ b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs @@ -31,8 +31,8 @@ public class SymbolStartAnalysisContext private static Func symbolAccessor; private static Action> registerCodeBlockAction; private static Action>> registerCodeBlockStartActionCS; + private static Action, ImmutableArray> registerOperationAction; - private readonly Action, ImmutableArray> registerOperationAction; private readonly Action> registerOperationBlockAction; private readonly Action> registerOperationBlockStartAction; private readonly Action> registerSymbolEndAction; @@ -47,6 +47,16 @@ static SymbolStartAnalysisContext() symbolAccessor = CreatePropertyAccessor(symbolStartAnalysisContextType, nameof(Symbol)); registerCodeBlockAction = CreateRegistrationMethod(symbolStartAnalysisContextType, nameof(RegisterCodeBlockAction)); registerCodeBlockStartActionCS = CreateRegistrationMethod>(symbolStartAnalysisContextType, nameof(RegisterCodeBlockStartAction), typeof(CS.SyntaxKind)); + registerOperationAction = CreateRegistrationMethodWithAdditionalParameter>(symbolStartAnalysisContextType, nameof(RegisterOperationAction)); + } + + private static Action, TParameter> CreateRegistrationMethodWithAdditionalParameter(Type symbolStartAnalysisContextType, string registrationMethodName, params Type[] typeArguments) + { + var receiver = Parameter(typeof(object)); + var registerActionParameter = Parameter(typeof(Action)); + var additionalParameter = Parameter(typeof(TParameter)); + return Lambda, TParameter>>( + Call(Convert(receiver, symbolStartAnalysisContextType), registrationMethodName, typeArguments, registerActionParameter, additionalParameter), receiver, registerActionParameter, additionalParameter).Compile(); } private static Action> CreateRegistrationMethod(Type symbolStartAnalysisContextType, string registrationMethodName, params Type[] typeArguments) @@ -57,17 +67,14 @@ private static Action> CreateRegistrationMethod, ImmutableArray> registerOperationAction, Action> registerOperationBlockAction, Action> registerOperationBlockStartAction, Action> registerSymbolEndAction, Action, ImmutableArray> registerSyntaxNodeActionCS) { RoslynSymbolStartAnalysisContext = roslynSymbolStartAnalysisContext; - this.registerOperationAction = registerOperationAction; this.registerOperationBlockAction = registerOperationBlockAction; this.registerOperationBlockStartAction = registerOperationBlockStartAction; this.registerSymbolEndAction = registerSymbolEndAction; @@ -101,7 +108,7 @@ public void RegisterCodeBlockStartAction(Action action, ImmutableArray operationKinds) => - registerOperationAction(action, operationKinds); + registerOperationAction(RoslynSymbolStartAnalysisContext, action, operationKinds); public void RegisterOperationBlockAction(Action action) => registerOperationBlockAction(action); From 927639a783aaee37104b735f3f7fe747b4bf2d35 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 4 Mar 2024 12:14:05 +0100 Subject: [PATCH 14/62] Move registerOperationBlockAction, registerOperationBlockStartAction, and registerSymbolEndAction --- .../SymbolStartAnalysisContext.cs | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs index 95ba3e07c17..44257fb242d 100644 --- a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs +++ b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs @@ -33,9 +33,9 @@ public class SymbolStartAnalysisContext private static Action>> registerCodeBlockStartActionCS; private static Action, ImmutableArray> registerOperationAction; - private readonly Action> registerOperationBlockAction; - private readonly Action> registerOperationBlockStartAction; - private readonly Action> registerSymbolEndAction; + private static Action> registerOperationBlockAction; + private static Action> registerOperationBlockStartAction; + private static Action> registerSymbolEndAction; private readonly Action, ImmutableArray> registerSyntaxNodeActionCS; static SymbolStartAnalysisContext() @@ -48,6 +48,9 @@ static SymbolStartAnalysisContext() registerCodeBlockAction = CreateRegistrationMethod(symbolStartAnalysisContextType, nameof(RegisterCodeBlockAction)); registerCodeBlockStartActionCS = CreateRegistrationMethod>(symbolStartAnalysisContextType, nameof(RegisterCodeBlockStartAction), typeof(CS.SyntaxKind)); registerOperationAction = CreateRegistrationMethodWithAdditionalParameter>(symbolStartAnalysisContextType, nameof(RegisterOperationAction)); + registerOperationBlockAction = CreateRegistrationMethod(symbolStartAnalysisContextType, nameof(RegisterOperationBlockAction)); + registerOperationBlockStartAction = CreateRegistrationMethod(symbolStartAnalysisContextType, nameof(RegisterOperationBlockStartAction)); + registerSymbolEndAction = CreateRegistrationMethod(symbolStartAnalysisContextType, nameof(RegisterSymbolEndAction)); } private static Action, TParameter> CreateRegistrationMethodWithAdditionalParameter(Type symbolStartAnalysisContextType, string registrationMethodName, params Type[] typeArguments) @@ -69,15 +72,9 @@ private static Action> CreateRegistrationMethod> registerOperationBlockAction, - Action> registerOperationBlockStartAction, - Action> registerSymbolEndAction, Action, ImmutableArray> registerSyntaxNodeActionCS) { RoslynSymbolStartAnalysisContext = roslynSymbolStartAnalysisContext; - this.registerOperationBlockAction = registerOperationBlockAction; - this.registerOperationBlockStartAction = registerOperationBlockStartAction; - this.registerSymbolEndAction = registerSymbolEndAction; this.registerSyntaxNodeActionCS = registerSyntaxNodeActionCS; } public object RoslynSymbolStartAnalysisContext { get; } @@ -111,13 +108,13 @@ public void RegisterOperationAction(Action action, Imm registerOperationAction(RoslynSymbolStartAnalysisContext, action, operationKinds); public void RegisterOperationBlockAction(Action action) => - registerOperationBlockAction(action); + registerOperationBlockAction(RoslynSymbolStartAnalysisContext, action); public void RegisterOperationBlockStartAction(Action action) => - registerOperationBlockStartAction(action); + registerOperationBlockStartAction(RoslynSymbolStartAnalysisContext, action); public void RegisterSymbolEndAction(Action action) => - registerSymbolEndAction(action); + registerSymbolEndAction(RoslynSymbolStartAnalysisContext, action); public void RegisterSyntaxNodeAction(Action action, params TLanguageKindEnum[] syntaxKinds) where TLanguageKindEnum : struct { From d31f523a4e0fb58f5cd5320319bf6e55f18968ed Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 4 Mar 2024 12:19:24 +0100 Subject: [PATCH 15/62] Move registerSyntaxNodeActionCS --- .../AnalysisContext/SymbolStartAnalysisContext.cs | 11 ++++------- .../Wrappers/RegisterSymbolStartActionWrapperTest.cs | 3 ++- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs index 44257fb242d..e0b4434a60b 100644 --- a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs +++ b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs @@ -32,11 +32,10 @@ public class SymbolStartAnalysisContext private static Action> registerCodeBlockAction; private static Action>> registerCodeBlockStartActionCS; private static Action, ImmutableArray> registerOperationAction; - private static Action> registerOperationBlockAction; private static Action> registerOperationBlockStartAction; private static Action> registerSymbolEndAction; - private readonly Action, ImmutableArray> registerSyntaxNodeActionCS; + private static Action, ImmutableArray> registerSyntaxNodeActionCS; static SymbolStartAnalysisContext() { @@ -51,6 +50,7 @@ static SymbolStartAnalysisContext() registerOperationBlockAction = CreateRegistrationMethod(symbolStartAnalysisContextType, nameof(RegisterOperationBlockAction)); registerOperationBlockStartAction = CreateRegistrationMethod(symbolStartAnalysisContextType, nameof(RegisterOperationBlockStartAction)); registerSymbolEndAction = CreateRegistrationMethod(symbolStartAnalysisContextType, nameof(RegisterSymbolEndAction)); + registerSyntaxNodeActionCS = CreateRegistrationMethodWithAdditionalParameter>(symbolStartAnalysisContextType, nameof(RegisterSyntaxNodeAction), typeof(CS.SyntaxKind)); } private static Action, TParameter> CreateRegistrationMethodWithAdditionalParameter(Type symbolStartAnalysisContextType, string registrationMethodName, params Type[] typeArguments) @@ -70,12 +70,9 @@ private static Action> CreateRegistrationMethod, ImmutableArray> registerSyntaxNodeActionCS) + public SymbolStartAnalysisContext(object roslynSymbolStartAnalysisContext) { RoslynSymbolStartAnalysisContext = roslynSymbolStartAnalysisContext; - this.registerSyntaxNodeActionCS = registerSyntaxNodeActionCS; } public object RoslynSymbolStartAnalysisContext { get; } public CancellationToken CancellationToken => cancellationTokenAccessor(RoslynSymbolStartAnalysisContext); @@ -121,7 +118,7 @@ public void RegisterSyntaxNodeAction(Action().ToImmutableArray()); + registerSyntaxNodeActionCS(RoslynSymbolStartAnalysisContext, action, syntaxKinds.Cast().ToImmutableArray()); } else if (languageKindType.FullName == "Microsoft.CodeAnalysis.VisualBasic.SyntaxKind") { diff --git a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs index 0fc87514bc0..defeef7f212 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs @@ -47,7 +47,8 @@ public class C symbolStart.Options.Should().NotBeNull(); symbolStart.Symbol.Should().BeAssignableTo().Which.Name.Should().Be("C"); }, SymbolKind.NamedType))); - await compilation.GetAnalyzerDiagnosticsAsync(); + var diags = await compilation.GetAnalyzerDiagnosticsAsync(); + diags.Should().BeEmpty(); } [TestMethod] From 43ec23609862c75de4da8350bcc4fbbe5a595519 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 4 Mar 2024 12:43:49 +0100 Subject: [PATCH 16/62] Move comments --- ...mpilationStartAnalysisContextExtensions.cs | 2 - .../SymbolStartAnalysisContext.cs | 68 +++++++++++++------ 2 files changed, 48 insertions(+), 22 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs index 2302fc5c7a5..89365b4b1a6 100644 --- a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs +++ b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs @@ -20,14 +20,12 @@ using System.Diagnostics.CodeAnalysis; using static System.Linq.Expressions.Expression; -using CS = Microsoft.CodeAnalysis.CSharp; namespace SonarAnalyzer.ShimLayer.AnalysisContext; public static class CompilationStartAnalysisContextExtensions { { -#pragma warning disable S103 // Lines should not be too long if (typeof(CompilationStartAnalysisContext).GetMethod(nameof(RegisterSymbolStartAction)) is not { } registerMethod) { return static (_, _, _) => { }; diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs index e0b4434a60b..f7dea964254 100644 --- a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs +++ b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs @@ -25,16 +25,35 @@ namespace SonarAnalyzer.ShimLayer.AnalysisContext; public class SymbolStartAnalysisContext { + // symbolStartAnalysisContextParameter => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter).CancellationToken private static Func cancellationTokenAccessor; + // symbolStartAnalysisContextParameter => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter).Compilation private static Func compilationAccessor; + // symbolStartAnalysisContextParameter => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter).Options private static Func optionsAccessor; + // symbolStartAnalysisContextParameter => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter).Symbol private static Func symbolAccessor; + + // (object symbolStartAnalysisContextParameter, Action registerActionParameter) + // => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter).RegisterCodeBlockAction(registerActionParameter), private static Action> registerCodeBlockAction; + // (object symbolStartAnalysisContextParameter, Action> registerActionParameter) + // => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter).CodeBlockStartAction(registerActionParameter), private static Action>> registerCodeBlockStartActionCS; + // (object symbolStartAnalysisContextParameter, Action registerActionParameter, ImmutableArray additionalParameter) + // => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter).RegisterOperationAction(registerActionParameter, additionalParameter), private static Action, ImmutableArray> registerOperationAction; + // (object symbolStartAnalysisContextParameter, Action registerActionParameter) + // => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter).RegisterOperationBlockAction(registerActionParameter), private static Action> registerOperationBlockAction; + // (object symbolStartAnalysisContextParameter, Action registerActionParameter) + // => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter).RegisterOperationBlockStartAction(registerActionParameter), private static Action> registerOperationBlockStartAction; + // (object symbolStartAnalysisContextParameter, Action registerActionParameter) + // => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter).RegisterSymbolEndAction(registerActionParameter), private static Action> registerSymbolEndAction; + // (object symbolStartAnalysisContextParameter, Action registerActionParameter, ImmutableArray additionalParameter) + // => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter).RegisterSyntaxNodeAction(registerActionParameter, additionalParameter)) private static Action, ImmutableArray> registerSyntaxNodeActionCS; static SymbolStartAnalysisContext() @@ -45,29 +64,15 @@ static SymbolStartAnalysisContext() optionsAccessor = CreatePropertyAccessor(symbolStartAnalysisContextType, nameof(Options)); symbolAccessor = CreatePropertyAccessor(symbolStartAnalysisContextType, nameof(Symbol)); registerCodeBlockAction = CreateRegistrationMethod(symbolStartAnalysisContextType, nameof(RegisterCodeBlockAction)); - registerCodeBlockStartActionCS = CreateRegistrationMethod>(symbolStartAnalysisContextType, nameof(RegisterCodeBlockStartAction), typeof(CS.SyntaxKind)); - registerOperationAction = CreateRegistrationMethodWithAdditionalParameter>(symbolStartAnalysisContextType, nameof(RegisterOperationAction)); + registerCodeBlockStartActionCS = + CreateRegistrationMethod>(symbolStartAnalysisContextType, nameof(RegisterCodeBlockStartAction), typeof(CS.SyntaxKind)); + registerOperationAction = + CreateRegistrationMethodWithAdditionalParameter>(symbolStartAnalysisContextType, nameof(RegisterOperationAction)); registerOperationBlockAction = CreateRegistrationMethod(symbolStartAnalysisContextType, nameof(RegisterOperationBlockAction)); registerOperationBlockStartAction = CreateRegistrationMethod(symbolStartAnalysisContextType, nameof(RegisterOperationBlockStartAction)); registerSymbolEndAction = CreateRegistrationMethod(symbolStartAnalysisContextType, nameof(RegisterSymbolEndAction)); - registerSyntaxNodeActionCS = CreateRegistrationMethodWithAdditionalParameter>(symbolStartAnalysisContextType, nameof(RegisterSyntaxNodeAction), typeof(CS.SyntaxKind)); - } - - private static Action, TParameter> CreateRegistrationMethodWithAdditionalParameter(Type symbolStartAnalysisContextType, string registrationMethodName, params Type[] typeArguments) - { - var receiver = Parameter(typeof(object)); - var registerActionParameter = Parameter(typeof(Action)); - var additionalParameter = Parameter(typeof(TParameter)); - return Lambda, TParameter>>( - Call(Convert(receiver, symbolStartAnalysisContextType), registrationMethodName, typeArguments, registerActionParameter, additionalParameter), receiver, registerActionParameter, additionalParameter).Compile(); - } - - private static Action> CreateRegistrationMethod(Type symbolStartAnalysisContextType, string registrationMethodName, params Type[] typeArguments) - { - var receiver = Parameter(typeof(object)); - var registerActionParameter = Parameter(typeof(Action)); - return Lambda>>( - Call(Convert(receiver, symbolStartAnalysisContextType), registrationMethodName, typeArguments, registerActionParameter), receiver, registerActionParameter).Compile(); + registerSyntaxNodeActionCS = CreateRegistrationMethodWithAdditionalParameter>( + symbolStartAnalysisContextType, nameof(RegisterSyntaxNodeAction), typeof(CS.SyntaxKind)); } public SymbolStartAnalysisContext(object roslynSymbolStartAnalysisContext) @@ -130,6 +135,7 @@ public void RegisterSyntaxNodeAction(Action ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter)."propertyName" private static Func CreatePropertyAccessor(Type symbolStartAnalysisContextType, string propertyName) { var symbolStartAnalysisContextParameter = Parameter(typeof(object)); @@ -138,4 +144,26 @@ private static Func CreatePropertyAccessor(Type sy Convert(symbolStartAnalysisContextParameter, symbolStartAnalysisContextType), propertyName), symbolStartAnalysisContextParameter).Compile(); } + + // (object symbolStartAnalysisContextParameter, Action registerActionParameter) + // => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter)."registrationMethodName"(registerActionParameter) + private static Action> CreateRegistrationMethod(Type symbolStartAnalysisContextType, string registrationMethodName, params Type[] typeArguments) + { + var receiver = Parameter(typeof(object)); + var registerActionParameter = Parameter(typeof(Action)); + return Lambda>>( + Call(Convert(receiver, symbolStartAnalysisContextType), registrationMethodName, typeArguments, registerActionParameter), receiver, registerActionParameter).Compile(); + } + + // (object symbolStartAnalysisContextParameter, Action registerActionParameter, TParameter additionalParameter) + // => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter)."registrationMethodName"(registerActionParameter, additionalParameter) + private static Action, TParameter> CreateRegistrationMethodWithAdditionalParameter( + Type symbolStartAnalysisContextType, string registrationMethodName, params Type[] typeArguments) + { + var receiver = Parameter(typeof(object)); + var registerActionParameter = Parameter(typeof(Action)); + var additionalParameter = Parameter(typeof(TParameter)); + return Lambda, TParameter>>(Call(Convert(receiver, symbolStartAnalysisContextType), registrationMethodName, typeArguments, + registerActionParameter, additionalParameter), receiver, registerActionParameter, additionalParameter).Compile(); + } } From 7e8d31dd649372466c051b229cce59f3be47a9f4 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 4 Mar 2024 12:48:03 +0100 Subject: [PATCH 17/62] Make SymbolStartAnalysisContext a readonly struct --- .../ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs index f7dea964254..03198f748a9 100644 --- a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs +++ b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs @@ -23,7 +23,7 @@ namespace SonarAnalyzer.ShimLayer.AnalysisContext; -public class SymbolStartAnalysisContext +public readonly struct SymbolStartAnalysisContext { // symbolStartAnalysisContextParameter => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter).CancellationToken private static Func cancellationTokenAccessor; From 8142b5ccc6df944fa8ad58ae2e90b31537d2e33b Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 4 Mar 2024 21:09:36 +0100 Subject: [PATCH 18/62] Code review. --- .../SymbolStartAnalysisContext.cs | 63 +++++++------------ 1 file changed, 23 insertions(+), 40 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs index 03198f748a9..a7a434451a7 100644 --- a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs +++ b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs @@ -25,37 +25,25 @@ namespace SonarAnalyzer.ShimLayer.AnalysisContext; public readonly struct SymbolStartAnalysisContext { - // symbolStartAnalysisContextParameter => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter).CancellationToken private static Func cancellationTokenAccessor; - // symbolStartAnalysisContextParameter => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter).Compilation private static Func compilationAccessor; - // symbolStartAnalysisContextParameter => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter).Options private static Func optionsAccessor; - // symbolStartAnalysisContextParameter => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter).Symbol private static Func symbolAccessor; - // (object symbolStartAnalysisContextParameter, Action registerActionParameter) - // => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter).RegisterCodeBlockAction(registerActionParameter), private static Action> registerCodeBlockAction; - // (object symbolStartAnalysisContextParameter, Action> registerActionParameter) - // => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter).CodeBlockStartAction(registerActionParameter), private static Action>> registerCodeBlockStartActionCS; - // (object symbolStartAnalysisContextParameter, Action registerActionParameter, ImmutableArray additionalParameter) - // => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter).RegisterOperationAction(registerActionParameter, additionalParameter), private static Action, ImmutableArray> registerOperationAction; - // (object symbolStartAnalysisContextParameter, Action registerActionParameter) - // => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter).RegisterOperationBlockAction(registerActionParameter), private static Action> registerOperationBlockAction; - // (object symbolStartAnalysisContextParameter, Action registerActionParameter) - // => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter).RegisterOperationBlockStartAction(registerActionParameter), private static Action> registerOperationBlockStartAction; - // (object symbolStartAnalysisContextParameter, Action registerActionParameter) - // => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter).RegisterSymbolEndAction(registerActionParameter), private static Action> registerSymbolEndAction; - // (object symbolStartAnalysisContextParameter, Action registerActionParameter, ImmutableArray additionalParameter) - // => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter).RegisterSyntaxNodeAction(registerActionParameter, additionalParameter)) private static Action, ImmutableArray> registerSyntaxNodeActionCS; + private object RoslynSymbolStartAnalysisContext { get; } + public CancellationToken CancellationToken => cancellationTokenAccessor(RoslynSymbolStartAnalysisContext); + public Compilation Compilation => compilationAccessor(RoslynSymbolStartAnalysisContext); + public AnalyzerOptions Options => optionsAccessor(RoslynSymbolStartAnalysisContext); + public ISymbol Symbol => symbolAccessor(RoslynSymbolStartAnalysisContext); + static SymbolStartAnalysisContext() { var symbolStartAnalysisContextType = typeof(CompilationStartAnalysisContext).Assembly.GetType("Microsoft.CodeAnalysis.Diagnostics.SymbolStartAnalysisContext"); @@ -75,15 +63,8 @@ static SymbolStartAnalysisContext() symbolStartAnalysisContextType, nameof(RegisterSyntaxNodeAction), typeof(CS.SyntaxKind)); } - public SymbolStartAnalysisContext(object roslynSymbolStartAnalysisContext) - { + public SymbolStartAnalysisContext(object roslynSymbolStartAnalysisContext) => RoslynSymbolStartAnalysisContext = roslynSymbolStartAnalysisContext; - } - public object RoslynSymbolStartAnalysisContext { get; } - public CancellationToken CancellationToken => cancellationTokenAccessor(RoslynSymbolStartAnalysisContext); - public Compilation Compilation => compilationAccessor(RoslynSymbolStartAnalysisContext); - public AnalyzerOptions Options => optionsAccessor(RoslynSymbolStartAnalysisContext); - public ISymbol Symbol => symbolAccessor(RoslynSymbolStartAnalysisContext); public void RegisterCodeBlockAction(Action action) => registerCodeBlockAction(RoslynSymbolStartAnalysisContext, action); @@ -93,8 +74,8 @@ public void RegisterCodeBlockStartAction(Action>)action; - registerCodeBlockStartActionCS(RoslynSymbolStartAnalysisContext, casted); + var cast = (Action>)action; + registerCodeBlockStartActionCS(RoslynSymbolStartAnalysisContext, cast); } else if (languageKindType.FullName == "Microsoft.CodeAnalysis.VisualBasic.SyntaxKind") { @@ -135,35 +116,37 @@ public void RegisterSyntaxNodeAction(Action ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter)."propertyName" + // symbolStartAnalysisContextParameter => ((symbolStartAnalysisContextType)receiverParameter)."propertyName" private static Func CreatePropertyAccessor(Type symbolStartAnalysisContextType, string propertyName) { - var symbolStartAnalysisContextParameter = Parameter(typeof(object)); + var receiverParameter = Parameter(typeof(object)); return Lambda>( - Property( - Convert(symbolStartAnalysisContextParameter, symbolStartAnalysisContextType), propertyName), - symbolStartAnalysisContextParameter).Compile(); + Property(Convert(receiverParameter, symbolStartAnalysisContextType), propertyName), + receiverParameter).Compile(); } - // (object symbolStartAnalysisContextParameter, Action registerActionParameter) + // (object receiverParameter, Action registerActionParameter) // => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter)."registrationMethodName"(registerActionParameter) private static Action> CreateRegistrationMethod(Type symbolStartAnalysisContextType, string registrationMethodName, params Type[] typeArguments) { - var receiver = Parameter(typeof(object)); + var receiverParameter = Parameter(typeof(object)); var registerActionParameter = Parameter(typeof(Action)); return Lambda>>( - Call(Convert(receiver, symbolStartAnalysisContextType), registrationMethodName, typeArguments, registerActionParameter), receiver, registerActionParameter).Compile(); + Call(Convert(receiverParameter, symbolStartAnalysisContextType), registrationMethodName, typeArguments, registerActionParameter), + receiverParameter, + registerActionParameter).Compile(); } - // (object symbolStartAnalysisContextParameter, Action registerActionParameter, TParameter additionalParameter) + // (object receiverParameter, Action registerActionParameter, TParameter additionalParameter) // => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter)."registrationMethodName"(registerActionParameter, additionalParameter) private static Action, TParameter> CreateRegistrationMethodWithAdditionalParameter( Type symbolStartAnalysisContextType, string registrationMethodName, params Type[] typeArguments) { - var receiver = Parameter(typeof(object)); + var receiverParameter = Parameter(typeof(object)); var registerActionParameter = Parameter(typeof(Action)); var additionalParameter = Parameter(typeof(TParameter)); - return Lambda, TParameter>>(Call(Convert(receiver, symbolStartAnalysisContextType), registrationMethodName, typeArguments, - registerActionParameter, additionalParameter), receiver, registerActionParameter, additionalParameter).Compile(); + return Lambda, TParameter>>( + Call(Convert(receiverParameter, symbolStartAnalysisContextType), registrationMethodName, typeArguments, registerActionParameter, additionalParameter), + receiverParameter, registerActionParameter, additionalParameter).Compile(); } } From 99a523bd95c1e121505af3ec3097753774b1d9c8 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Tue, 5 Mar 2024 09:24:52 +0100 Subject: [PATCH 19/62] Refactorings --- .../SymbolStartAnalysisContext.cs | 91 ++++++++++--------- 1 file changed, 46 insertions(+), 45 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs index a7a434451a7..ac18b307261 100644 --- a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs +++ b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs @@ -47,20 +47,55 @@ public readonly struct SymbolStartAnalysisContext static SymbolStartAnalysisContext() { var symbolStartAnalysisContextType = typeof(CompilationStartAnalysisContext).Assembly.GetType("Microsoft.CodeAnalysis.Diagnostics.SymbolStartAnalysisContext"); - cancellationTokenAccessor = CreatePropertyAccessor(symbolStartAnalysisContextType, nameof(CancellationToken)); - compilationAccessor = CreatePropertyAccessor(symbolStartAnalysisContextType, nameof(Compilation)); - optionsAccessor = CreatePropertyAccessor(symbolStartAnalysisContextType, nameof(Options)); - symbolAccessor = CreatePropertyAccessor(symbolStartAnalysisContextType, nameof(Symbol)); - registerCodeBlockAction = CreateRegistrationMethod(symbolStartAnalysisContextType, nameof(RegisterCodeBlockAction)); + cancellationTokenAccessor = CreatePropertyAccessor(nameof(CancellationToken)); + compilationAccessor = CreatePropertyAccessor(nameof(Compilation)); + optionsAccessor = CreatePropertyAccessor(nameof(Options)); + symbolAccessor = CreatePropertyAccessor(nameof(Symbol)); + registerCodeBlockAction = CreateRegistrationMethod(nameof(RegisterCodeBlockAction)); registerCodeBlockStartActionCS = - CreateRegistrationMethod>(symbolStartAnalysisContextType, nameof(RegisterCodeBlockStartAction), typeof(CS.SyntaxKind)); + CreateRegistrationMethod>(nameof(RegisterCodeBlockStartAction), typeof(CS.SyntaxKind)); registerOperationAction = - CreateRegistrationMethodWithAdditionalParameter>(symbolStartAnalysisContextType, nameof(RegisterOperationAction)); - registerOperationBlockAction = CreateRegistrationMethod(symbolStartAnalysisContextType, nameof(RegisterOperationBlockAction)); - registerOperationBlockStartAction = CreateRegistrationMethod(symbolStartAnalysisContextType, nameof(RegisterOperationBlockStartAction)); - registerSymbolEndAction = CreateRegistrationMethod(symbolStartAnalysisContextType, nameof(RegisterSymbolEndAction)); + CreateRegistrationMethodWithAdditionalParameter>(nameof(RegisterOperationAction)); + registerOperationBlockAction = CreateRegistrationMethod(nameof(RegisterOperationBlockAction)); + registerOperationBlockStartAction = CreateRegistrationMethod(nameof(RegisterOperationBlockStartAction)); + registerSymbolEndAction = CreateRegistrationMethod(nameof(RegisterSymbolEndAction)); registerSyntaxNodeActionCS = CreateRegistrationMethodWithAdditionalParameter>( - symbolStartAnalysisContextType, nameof(RegisterSyntaxNodeAction), typeof(CS.SyntaxKind)); + nameof(RegisterSyntaxNodeAction), typeof(CS.SyntaxKind)); + + // symbolStartAnalysisContextParameter => ((symbolStartAnalysisContextType)receiverParameter)."propertyName" + Func CreatePropertyAccessor(string propertyName) + { + var receiverParameter = Parameter(typeof(object)); + return Lambda>( + Property(Convert(receiverParameter, symbolStartAnalysisContextType), propertyName), + receiverParameter).Compile(); + } + + // (object receiverParameter, Action registerActionParameter) + // => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter)."registrationMethodName"(registerActionParameter) + Action> CreateRegistrationMethod(string registrationMethodName, params Type[] typeArguments) + { + var receiverParameter = Parameter(typeof(object)); + var registerActionParameter = Parameter(typeof(Action)); + return Lambda>>( + Call(Convert(receiverParameter, symbolStartAnalysisContextType), registrationMethodName, typeArguments, registerActionParameter), + receiverParameter, + registerActionParameter).Compile(); + } + + // (object receiverParameter, Action registerActionParameter, TParameter additionalParameter) + // => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter)."registrationMethodName"(registerActionParameter, additionalParameter) + Action, TParameter> CreateRegistrationMethodWithAdditionalParameter(string registrationMethodName, params Type[] typeArguments) + { + var receiverParameter = Parameter(typeof(object)); + var registerActionParameter = Parameter(typeof(Action)); + var additionalParameter = Parameter(typeof(TParameter)); + return Lambda, TParameter>>( + Call(Convert(receiverParameter, symbolStartAnalysisContextType), registrationMethodName, typeArguments, registerActionParameter, additionalParameter), + receiverParameter, + registerActionParameter, + additionalParameter).Compile(); + } } public SymbolStartAnalysisContext(object roslynSymbolStartAnalysisContext) => @@ -115,38 +150,4 @@ public void RegisterSyntaxNodeAction(Action ((symbolStartAnalysisContextType)receiverParameter)."propertyName" - private static Func CreatePropertyAccessor(Type symbolStartAnalysisContextType, string propertyName) - { - var receiverParameter = Parameter(typeof(object)); - return Lambda>( - Property(Convert(receiverParameter, symbolStartAnalysisContextType), propertyName), - receiverParameter).Compile(); - } - - // (object receiverParameter, Action registerActionParameter) - // => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter)."registrationMethodName"(registerActionParameter) - private static Action> CreateRegistrationMethod(Type symbolStartAnalysisContextType, string registrationMethodName, params Type[] typeArguments) - { - var receiverParameter = Parameter(typeof(object)); - var registerActionParameter = Parameter(typeof(Action)); - return Lambda>>( - Call(Convert(receiverParameter, symbolStartAnalysisContextType), registrationMethodName, typeArguments, registerActionParameter), - receiverParameter, - registerActionParameter).Compile(); - } - - // (object receiverParameter, Action registerActionParameter, TParameter additionalParameter) - // => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter)."registrationMethodName"(registerActionParameter, additionalParameter) - private static Action, TParameter> CreateRegistrationMethodWithAdditionalParameter( - Type symbolStartAnalysisContextType, string registrationMethodName, params Type[] typeArguments) - { - var receiverParameter = Parameter(typeof(object)); - var registerActionParameter = Parameter(typeof(Action)); - var additionalParameter = Parameter(typeof(TParameter)); - return Lambda, TParameter>>( - Call(Convert(receiverParameter, symbolStartAnalysisContextType), registrationMethodName, typeArguments, registerActionParameter, additionalParameter), - receiverParameter, registerActionParameter, additionalParameter).Compile(); - } } From 2743a4e0756482dba382fd3a8bcb5dcd2af41923 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Tue, 5 Mar 2024 09:25:17 +0100 Subject: [PATCH 20/62] Improve RegisterSymbolStartAction_SymbolStartProperties --- .../Wrappers/RegisterSymbolStartActionWrapperTest.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs index defeef7f212..b41e2a8eccb 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs @@ -39,6 +39,7 @@ public class C } """; var snippet = new SnippetCompiler(code); + var symbolStartWasCalled = false; var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create( new TestDiagnosticAnalyzer(symbolStart => { @@ -46,8 +47,10 @@ public class C symbolStart.Compilation.SyntaxTrees.Should().ContainSingle(); symbolStart.Options.Should().NotBeNull(); symbolStart.Symbol.Should().BeAssignableTo().Which.Name.Should().Be("C"); + symbolStartWasCalled = true; }, SymbolKind.NamedType))); var diags = await compilation.GetAnalyzerDiagnosticsAsync(); + symbolStartWasCalled.Should().BeTrue(); diags.Should().BeEmpty(); } From 8e64f8e2c544fda71d18c6fff71cd61e76a8cbc8 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Tue, 5 Mar 2024 17:10:58 +0100 Subject: [PATCH 21/62] Add support for VB --- .../SymbolStartAnalysisContext.cs | 16 ++++++++++---- .../RegisterSymbolStartActionWrapperTest.cs | 21 ++++++++++--------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs index ac18b307261..d532a0f5a59 100644 --- a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs +++ b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs @@ -20,6 +20,7 @@ using static System.Linq.Expressions.Expression; using CS = Microsoft.CodeAnalysis.CSharp; +using VB = Microsoft.CodeAnalysis.VisualBasic; namespace SonarAnalyzer.ShimLayer.AnalysisContext; @@ -32,11 +33,13 @@ public readonly struct SymbolStartAnalysisContext private static Action> registerCodeBlockAction; private static Action>> registerCodeBlockStartActionCS; + private static Action>> registerCodeBlockStartActionVB; private static Action, ImmutableArray> registerOperationAction; private static Action> registerOperationBlockAction; private static Action> registerOperationBlockStartAction; private static Action> registerSymbolEndAction; private static Action, ImmutableArray> registerSyntaxNodeActionCS; + private static Action, ImmutableArray> registerSyntaxNodeActionVB; private object RoslynSymbolStartAnalysisContext { get; } public CancellationToken CancellationToken => cancellationTokenAccessor(RoslynSymbolStartAnalysisContext); @@ -54,6 +57,8 @@ static SymbolStartAnalysisContext() registerCodeBlockAction = CreateRegistrationMethod(nameof(RegisterCodeBlockAction)); registerCodeBlockStartActionCS = CreateRegistrationMethod>(nameof(RegisterCodeBlockStartAction), typeof(CS.SyntaxKind)); + registerCodeBlockStartActionVB = + CreateRegistrationMethod>(nameof(RegisterCodeBlockStartAction), typeof(VB.SyntaxKind)); registerOperationAction = CreateRegistrationMethodWithAdditionalParameter>(nameof(RegisterOperationAction)); registerOperationBlockAction = CreateRegistrationMethod(nameof(RegisterOperationBlockAction)); @@ -61,6 +66,8 @@ static SymbolStartAnalysisContext() registerSymbolEndAction = CreateRegistrationMethod(nameof(RegisterSymbolEndAction)); registerSyntaxNodeActionCS = CreateRegistrationMethodWithAdditionalParameter>( nameof(RegisterSyntaxNodeAction), typeof(CS.SyntaxKind)); + registerSyntaxNodeActionVB = CreateRegistrationMethodWithAdditionalParameter>( + nameof(RegisterSyntaxNodeAction), typeof(VB.SyntaxKind)); // symbolStartAnalysisContextParameter => ((symbolStartAnalysisContextType)receiverParameter)."propertyName" Func CreatePropertyAccessor(string propertyName) @@ -112,9 +119,10 @@ public void RegisterCodeBlockStartAction(Action>)action; registerCodeBlockStartActionCS(RoslynSymbolStartAnalysisContext, cast); } - else if (languageKindType.FullName == "Microsoft.CodeAnalysis.VisualBasic.SyntaxKind") + else if (languageKindType == typeof(VB.SyntaxKind)) { - throw new NotImplementedException("Add a reference to the Microsoft.CodeAnalysis.VisualBasic.Workspaces package."); + var cast = (Action>)action; + registerCodeBlockStartActionVB(RoslynSymbolStartAnalysisContext, cast); } else { @@ -141,9 +149,9 @@ public void RegisterSyntaxNodeAction(Action().ToImmutableArray()); } - else if (languageKindType.FullName == "Microsoft.CodeAnalysis.VisualBasic.SyntaxKind") + else if (languageKindType == typeof(VB.SyntaxKind)) { - throw new NotImplementedException("Add a reference to the Microsoft.CodeAnalysis.VisualBasic.Workspaces package."); + registerSyntaxNodeActionVB(RoslynSymbolStartAnalysisContext, action, syntaxKinds.Cast().ToImmutableArray()); } else { diff --git a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs index b41e2a8eccb..bc6d0ac4ce2 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs @@ -419,11 +419,15 @@ End Class blockStart.RegisterSyntaxNodeAction(nodeContext => visited.Add(nodeContext.Node.ToString()), VB.SyntaxKind.InvocationExpression); }); }, SymbolKind.NamedType))); - var diag = await compilation.GetAnalyzerDiagnosticsAsync(); - var ad0001 = diag.Should().ContainSingle().Which; - ad0001.Id.Should().Be("AD0001"); - ad0001.Descriptor.Description.ToString().Should().Contain("System.NotImplementedException: Add a reference to the Microsoft.CodeAnalysis.VisualBasic.Workspaces package"); - visited.Should().BeEmpty(because: "The vb version requires the Microsoft.CodeAnalysis.VisualBasic.Workspaces package to be added. VB.SyntaxKind is not available in the shim layer."); + await compilation.GetAnalyzerDiagnosticsAsync(); + visited.Should().BeEquivalentTo([ + """Private i As Integer = 0""", + """ + Public Sub M() + Call ToString() + End Sub + """, + """ToString()"""]); } [TestMethod] @@ -578,11 +582,8 @@ End Class visited.Add(nodeName); }, VB.SyntaxKind.InvocationExpression); }, SymbolKind.NamedType))); - var diag = await compilation.GetAnalyzerDiagnosticsAsync(); - var ad0001 = diag.Should().ContainSingle().Which; - ad0001.Id.Should().Be("AD0001"); - ad0001.Descriptor.Description.ToString().Should().Contain("System.NotImplementedException: Add a reference to the Microsoft.CodeAnalysis.VisualBasic.Workspaces package"); - visited.Should().BeEmpty(because: "The vb version requires the Microsoft.CodeAnalysis.VisualBasic.Workspaces package to be added. VB.SyntaxKind is not available in the shim layer."); + await compilation.GetAnalyzerDiagnosticsAsync(); + visited.Should().BeEquivalentTo("ToString()"); } #pragma warning disable RS1001 // Missing diagnostic analyzer attribute From 6cd98dc83c8e0914bde4bd026c0ff0bd5180cb65 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Wed, 6 Mar 2024 13:51:51 +0100 Subject: [PATCH 22/62] Add packages.lock.json --- .../src/SonarAnalyzer.RuleDescriptorGenerator/packages.lock.json | 1 + 1 file changed, 1 insertion(+) diff --git a/analyzers/src/SonarAnalyzer.RuleDescriptorGenerator/packages.lock.json b/analyzers/src/SonarAnalyzer.RuleDescriptorGenerator/packages.lock.json index 29e4b65ee7c..135071d6c46 100644 --- a/analyzers/src/SonarAnalyzer.RuleDescriptorGenerator/packages.lock.json +++ b/analyzers/src/SonarAnalyzer.RuleDescriptorGenerator/packages.lock.json @@ -929,6 +929,7 @@ "type": "Project", "dependencies": { "Microsoft.CodeAnalysis.CSharp.Workspaces": "[1.3.2, )", + "Microsoft.CodeAnalysis.VisualBasic.Workspaces": "[1.3.2, )", "Microsoft.Composition": "[1.0.27, )", "System.Collections.Immutable": "[1.1.37, )" } From f5922df9d9b7a728e358c62f15c8650f0b4b6f38 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Wed, 6 Mar 2024 14:00:19 +0100 Subject: [PATCH 23/62] Remove packages.locks.json (VS created it after re-base) --- .../packages.lock.json | 939 ------------------ 1 file changed, 939 deletions(-) delete mode 100644 analyzers/src/SonarAnalyzer.RuleDescriptorGenerator/packages.lock.json diff --git a/analyzers/src/SonarAnalyzer.RuleDescriptorGenerator/packages.lock.json b/analyzers/src/SonarAnalyzer.RuleDescriptorGenerator/packages.lock.json deleted file mode 100644 index 135071d6c46..00000000000 --- a/analyzers/src/SonarAnalyzer.RuleDescriptorGenerator/packages.lock.json +++ /dev/null @@ -1,939 +0,0 @@ -{ - "version": 1, - "dependencies": { - "net6.0": { - "Microsoft.CodeAnalysis.CSharp.Workspaces": { - "type": "Direct", - "requested": "[1.3.2, )", - "resolved": "1.3.2", - "contentHash": "MwGmrrPx3okEJuCogSn4TM3yTtJUDdmTt8RXpnjVo0dPund0YSAq4bHQQ9bxgArbrrapcopJmkb7UOLAvanXkg==", - "dependencies": { - "Microsoft.CodeAnalysis.CSharp": "[1.3.2]", - "Microsoft.CodeAnalysis.Workspaces.Common": "[1.3.2]" - } - }, - "Microsoft.CodeAnalysis.VisualBasic.Workspaces": { - "type": "Direct", - "requested": "[1.3.2, )", - "resolved": "1.3.2", - "contentHash": "I5Z2WBgFsx0G22Na1uVFPDkT6Ob4XI+g91GPN8JWldYUMlmIBcUDBfGmfr8oQPdUipvThpaU1x1xZrnNwRR8JA==", - "dependencies": { - "Microsoft.CodeAnalysis.VisualBasic": "[1.3.2]", - "Microsoft.CodeAnalysis.Workspaces.Common": "[1.3.2]" - } - }, - "StyleCop.Analyzers": { - "type": "Direct", - "requested": "[1.2.0-beta.556, )", - "resolved": "1.2.0-beta.556", - "contentHash": "llRPgmA1fhC0I0QyFLEcjvtM2239QzKr/tcnbsjArLMJxJlu0AA5G7Fft0OI30pHF3MW63Gf4aSSsjc5m82J1Q==", - "dependencies": { - "StyleCop.Analyzers.Unstable": "1.2.0.556" - } - }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==", - "dependencies": { - "NETStandard.Library": "1.6.1" - } - }, - "Google.Protobuf.Tools": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "mNgfZ1A7UtbZUOIA8+UcKOouKnbd2tu9CKctCvGXFunZGrViWk6QbNwSBc268Sle9Gwl+WQB+u6qQezp5f9y3w==" - }, - "Microsoft.CodeAnalysis.Analyzers": { - "type": "Transitive", - "resolved": "1.1.0", - "contentHash": "HS3iRWZKcUw/8eZ/08GXKY2Bn7xNzQPzf8gRPHGSowX7u7XXu9i9YEaBeBNKUXWfI7qjvT2zXtLUvbN0hds8vg==" - }, - "Microsoft.CodeAnalysis.Common": { - "type": "Transitive", - "resolved": "1.3.2", - "contentHash": "lOinFNbjpCvkeYQHutjKi+CfsjoKu88wAFT6hAumSR/XJSJmmVGvmnbzCWW8kUJnDVrw1RrcqS8BzgPMj263og==", - "dependencies": { - "Microsoft.CodeAnalysis.Analyzers": "1.1.0", - "System.AppContext": "4.1.0", - "System.Collections": "4.0.11", - "System.Collections.Concurrent": "4.0.12", - "System.Collections.Immutable": "1.2.0", - "System.Console": "4.0.0", - "System.Diagnostics.Debug": "4.0.11", - "System.Diagnostics.FileVersionInfo": "4.0.0", - "System.Diagnostics.StackTrace": "4.0.1", - "System.Diagnostics.Tools": "4.0.1", - "System.Dynamic.Runtime": "4.0.11", - "System.Globalization": "4.0.11", - "System.IO.FileSystem": "4.0.1", - "System.IO.FileSystem.Primitives": "4.0.1", - "System.Linq": "4.1.0", - "System.Linq.Expressions": "4.1.0", - "System.Reflection": "4.1.0", - "System.Reflection.Metadata": "1.3.0", - "System.Reflection.Primitives": "4.0.1", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.Handles": "4.0.1", - "System.Runtime.InteropServices": "4.1.0", - "System.Runtime.Numerics": "4.0.1", - "System.Security.Cryptography.Algorithms": "4.2.0", - "System.Security.Cryptography.Encoding": "4.0.0", - "System.Security.Cryptography.X509Certificates": "4.1.0", - "System.Text.Encoding": "4.0.11", - "System.Text.Encoding.CodePages": "4.0.1", - "System.Text.Encoding.Extensions": "4.0.11", - "System.Threading": "4.0.11", - "System.Threading.Tasks": "4.0.11", - "System.Threading.Tasks.Parallel": "4.0.1", - "System.Threading.Thread": "4.0.0", - "System.Xml.ReaderWriter": "4.0.11", - "System.Xml.XDocument": "4.0.11", - "System.Xml.XPath.XDocument": "4.0.1", - "System.Xml.XmlDocument": "4.0.1" - } - }, - "Microsoft.CodeAnalysis.CSharp": { - "type": "Transitive", - "resolved": "1.3.2", - "contentHash": "GrYMp6ScZDOMR0fNn/Ce6SegNVFw1G/QRT/8FiKv7lAP+V6lEZx9e42n0FvFUgjjcKgcEJOI4muU6i+3LSvOBA==", - "dependencies": { - "Microsoft.CodeAnalysis.Common": "[1.3.2]" - } - }, - "Microsoft.CodeAnalysis.VisualBasic": { - "type": "Transitive", - "resolved": "1.3.2", - "contentHash": "yllH3rSYEc0bV15CJ2T9Jtx+tSXO5/OVNb+xofuWrACn65Q5VqeFBKgcbgwpyVY/98ypPcGQIWNQL2A/L1seJg==", - "dependencies": { - "Microsoft.CodeAnalysis.Common": "1.3.2" - } - }, - "Microsoft.CodeAnalysis.Workspaces.Common": { - "type": "Transitive", - "resolved": "1.3.2", - "contentHash": "kvdo+rkImlx5MuBgkayl4OV3Mg8/qirUdYgCIfQ9EqN15QasJFlQXmDAtCGqpkK9sYLLO/VK+y+4mvKjfh/FOA==", - "dependencies": { - "Microsoft.CodeAnalysis.Common": "[1.3.2]", - "Microsoft.Composition": "1.0.27" - } - }, - "Microsoft.Composition": { - "type": "Transitive", - "resolved": "1.0.27", - "contentHash": "pwu80Ohe7SBzZ6i69LVdzowp6V+LaVRzd5F7A6QlD42vQkX0oT7KXKWWPlM/S00w1gnMQMRnEdbtOV12z6rXdQ==" - }, - "Microsoft.NETCore.Platforms": { - "type": "Transitive", - "resolved": "1.0.1", - "contentHash": "2G6OjjJzwBfNOO8myRV/nFrbTw5iA+DEm0N+qUqhrOmaVtn4pC77h38I1jsXGw5VH55+dPfQsqHD0We9sCl9FQ==" - }, - "Microsoft.NETCore.Targets": { - "type": "Transitive", - "resolved": "1.0.1", - "contentHash": "rkn+fKobF/cbWfnnfBOQHKVKIOpxMZBvlSHkqDWgBpwGDcLRduvs3D9OLGeV6GWGvVwNlVi2CBbTjuPmtHvyNw==" - }, - "runtime.native.System": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "QfS/nQI7k/BLgmLrw7qm7YBoULEvgWnPI+cYsbfCVFTW8Aj+i8JhccxcFMu1RWms0YZzF+UHguNBK4Qn89e2Sg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1" - } - }, - "runtime.native.System.Net.Http": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "Nh0UPZx2Vifh8r+J+H2jxifZUD3sBrmolgiFWJd2yiNrxO0xTa6bAw3YwRn1VOiSen/tUXMS31ttNItCZ6lKuA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1" - } - }, - "runtime.native.System.Security.Cryptography": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "2CQK0jmO6Eu7ZeMgD+LOFbNJSXHFVQbCJJkEyEwowh1SCgYnrn9W9RykMfpeeVGw7h4IBvYikzpGUlmZTUafJw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1" - } - }, - "StyleCop.Analyzers.Unstable": { - "type": "Transitive", - "resolved": "1.2.0.556", - "contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ==" - }, - "System.AppContext": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "3QjO4jNV7PdKkmQAVp9atA+usVnKRwI3Kx1nMwJ93T0LcQfx7pKAYk0nKz5wn1oP5iqlhZuy6RXOFdhr7rDwow==", - "dependencies": { - "System.Runtime": "4.1.0" - } - }, - "System.Collections": { - "type": "Transitive", - "resolved": "4.0.11", - "contentHash": "YUJGz6eFKqS0V//mLt25vFGrrCvOnsXjlvFQs+KimpwNxug9x0Pzy4PlFMU3Q2IzqAa9G2L4LsK3+9vCBK7oTg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Collections.Concurrent": { - "type": "Transitive", - "resolved": "4.0.12", - "contentHash": "2gBcbb3drMLgxlI0fBfxMA31ec6AEyYCHygGse4vxceJan8mRIWeKJ24BFzN7+bi/NFTgdIgufzb94LWO5EERQ==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Diagnostics.Tracing": "4.1.0", - "System.Globalization": "4.0.11", - "System.Reflection": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Threading": "4.0.11", - "System.Threading.Tasks": "4.0.11" - } - }, - "System.Collections.Immutable": { - "type": "Transitive", - "resolved": "1.2.0", - "contentHash": "Cma8cBW6di16ZLibL8LYQ+cLjGzoKxpOTu/faZfDcx94ZjAGq6Nv5RO7+T1YZXqEXTZP9rt1wLVEONVpURtUqw==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Globalization": "4.0.11", - "System.Linq": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Threading": "4.0.11" - } - }, - "System.Console": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "qSKUSOIiYA/a0g5XXdxFcUFmv1hNICBD7QZ0QhGYVipPIhvpiydY8VZqr1thmCXvmn8aipMg64zuanB4eotK9A==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.IO": "4.1.0", - "System.Runtime": "4.1.0", - "System.Text.Encoding": "4.0.11" - } - }, - "System.Diagnostics.Debug": { - "type": "Transitive", - "resolved": "4.0.11", - "contentHash": "w5U95fVKHY4G8ASs/K5iK3J5LY+/dLFd4vKejsnI/ZhBsWS9hQakfx3Zr7lRWKg4tAw9r4iktyvsTagWkqYCiw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Diagnostics.FileVersionInfo": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "qjF74OTAU+mRhLaL4YSfiWy3vj6T3AOz8AW37l5zCwfbBfj0k7E94XnEsRaf2TnhE/7QaV6Hvqakoy2LoV8MVg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "System.Globalization": "4.0.11", - "System.IO": "4.1.0", - "System.IO.FileSystem": "4.0.1", - "System.IO.FileSystem.Primitives": "4.0.1", - "System.Reflection.Metadata": "1.3.0", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.InteropServices": "4.1.0" - } - }, - "System.Diagnostics.StackTrace": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "6i2EbRq0lgGfiZ+FDf0gVaw9qeEU+7IS2+wbZJmFVpvVzVOgZEt0ScZtyenuBvs6iDYbGiF51bMAa0oDP/tujQ==", - "dependencies": { - "System.Collections.Immutable": "1.2.0", - "System.IO.FileSystem": "4.0.1", - "System.Reflection": "4.1.0", - "System.Reflection.Metadata": "1.3.0", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0" - } - }, - "System.Diagnostics.Tools": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "xBfJ8pnd4C17dWaC9FM6aShzbJcRNMChUMD42I6772KGGrqaFdumwhn9OdM68erj1ueNo3xdQ1EwiFjK5k8p0g==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Diagnostics.Tracing": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "vDN1PoMZCkkdNjvZLql592oYJZgS7URcJzJ7bxeBgGtx5UtR5leNm49VmfHGqIffX4FKacHbI3H6UyNSHQknBg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Dynamic.Runtime": { - "type": "Transitive", - "resolved": "4.0.11", - "contentHash": "db34f6LHYM0U0JpE+sOmjar27BnqTVkbLJhgfwMpTdgTigG/Hna3m2MYVwnFzGGKnEJk2UXFuoVTr8WUbU91/A==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Globalization": "4.0.11", - "System.Linq": "4.1.0", - "System.Linq.Expressions": "4.1.0", - "System.ObjectModel": "4.0.12", - "System.Reflection": "4.1.0", - "System.Reflection.Emit": "4.0.1", - "System.Reflection.Emit.ILGeneration": "4.0.1", - "System.Reflection.Primitives": "4.0.1", - "System.Reflection.TypeExtensions": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Threading": "4.0.11" - } - }, - "System.Globalization": { - "type": "Transitive", - "resolved": "4.0.11", - "contentHash": "B95h0YLEL2oSnwF/XjqSWKnwKOy/01VWkNlsCeMTFJLLabflpGV26nK164eRs5GiaRSBGpOxQ3pKoSnnyZN5pg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Globalization.Calendars": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "L1c6IqeQ88vuzC1P81JeHmHA8mxq8a18NUBNXnIY/BVb+TCyAaGIFbhpZt60h9FJNmisymoQkHEFSE9Vslja1Q==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Globalization": "4.0.11", - "System.Runtime": "4.1.0" - } - }, - "System.IO": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "3KlTJceQc3gnGIaHZ7UBZO26SHL1SHE4ddrmiwumFnId+CEHP+O8r386tZKaE6zlk5/mF8vifMBzHj9SaXN+mQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0", - "System.Text.Encoding": "4.0.11", - "System.Threading.Tasks": "4.0.11" - } - }, - "System.IO.FileSystem": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "IBErlVq5jOggAD69bg1t0pJcHaDbJbWNUZTPI96fkYWzwYbN6D9wRHMULLDd9dHsl7C2YsxXL31LMfPI1SWt8w==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.IO": "4.1.0", - "System.IO.FileSystem.Primitives": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Handles": "4.0.1", - "System.Text.Encoding": "4.0.11", - "System.Threading.Tasks": "4.0.11" - } - }, - "System.IO.FileSystem.Primitives": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "kWkKD203JJKxJeE74p8aF8y4Qc9r9WQx4C0cHzHPrY3fv/L/IhWnyCHaFJ3H1QPOH6A93whlQ2vG5nHlBDvzWQ==", - "dependencies": { - "System.Runtime": "4.1.0" - } - }, - "System.Linq": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "bQ0iYFOQI0nuTnt+NQADns6ucV4DUvMdwN6CbkB1yj8i7arTGiTN5eok1kQwdnnNWSDZfIUySQY+J3d5KjWn0g==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0" - } - }, - "System.Linq.Expressions": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "I+y02iqkgmCAyfbqOmSDOgqdZQ5tTj80Akm5BPSS8EeB0VGWdy6X1KCoYe8Pk6pwDoAKZUOdLVxnTJcExiv5zw==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Globalization": "4.0.11", - "System.IO": "4.1.0", - "System.Linq": "4.1.0", - "System.ObjectModel": "4.0.12", - "System.Reflection": "4.1.0", - "System.Reflection.Emit": "4.0.1", - "System.Reflection.Emit.ILGeneration": "4.0.1", - "System.Reflection.Emit.Lightweight": "4.0.1", - "System.Reflection.Extensions": "4.0.1", - "System.Reflection.Primitives": "4.0.1", - "System.Reflection.TypeExtensions": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Threading": "4.0.11" - } - }, - "System.ObjectModel": { - "type": "Transitive", - "resolved": "4.0.12", - "contentHash": "tAgJM1xt3ytyMoW4qn4wIqgJYm7L7TShRZG4+Q4Qsi2PCcj96pXN7nRywS9KkB3p/xDUjc2HSwP9SROyPYDYKQ==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Threading": "4.0.11" - } - }, - "System.Reflection": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "JCKANJ0TI7kzoQzuwB/OoJANy1Lg338B6+JVacPl4TpUwi3cReg3nMLplMq2uqYfHFQpKIlHAUVAJlImZz/4ng==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.IO": "4.1.0", - "System.Reflection.Primitives": "4.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Reflection.Emit": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "P2wqAj72fFjpP6wb9nSfDqNBMab+2ovzSDzUZK7MVIm54tBJEPr9jWfSjjoTpPwj1LeKcmX3vr0ttyjSSFM47g==", - "dependencies": { - "System.IO": "4.1.0", - "System.Reflection": "4.1.0", - "System.Reflection.Emit.ILGeneration": "4.0.1", - "System.Reflection.Primitives": "4.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Reflection.Emit.ILGeneration": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "Ov6dU8Bu15Bc7zuqttgHF12J5lwSWyTf1S+FJouUXVMSqImLZzYaQ+vRr1rQ0OZ0HqsrwWl4dsKHELckQkVpgA==", - "dependencies": { - "System.Reflection": "4.1.0", - "System.Reflection.Primitives": "4.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Reflection.Emit.Lightweight": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "sSzHHXueZ5Uh0OLpUQprhr+ZYJrLPA2Cmr4gn0wj9+FftNKXx8RIMKvO9qnjk2ebPYUjZ+F2ulGdPOsvj+MEjA==", - "dependencies": { - "System.Reflection": "4.1.0", - "System.Reflection.Emit.ILGeneration": "4.0.1", - "System.Reflection.Primitives": "4.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Reflection.Extensions": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "GYrtRsZcMuHF3sbmRHfMYpvxZoIN2bQGrYGerUiWLEkqdEUQZhH3TRSaC/oI4wO0II1RKBPlpIa1TOMxIcOOzQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Reflection": "4.1.0", - "System.Runtime": "4.1.0" - } - }, - "System.Reflection.Metadata": { - "type": "Transitive", - "resolved": "1.3.0", - "contentHash": "jMSCxA4LSyKBGRDm/WtfkO03FkcgRzHxwvQRib1bm2GZ8ifKM1MX1al6breGCEQK280mdl9uQS7JNPXRYk90jw==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Collections.Immutable": "1.2.0", - "System.Diagnostics.Debug": "4.0.11", - "System.IO": "4.1.0", - "System.Linq": "4.1.0", - "System.Reflection": "4.1.0", - "System.Reflection.Extensions": "4.0.1", - "System.Reflection.Primitives": "4.0.1", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.InteropServices": "4.1.0", - "System.Text.Encoding": "4.0.11", - "System.Text.Encoding.Extensions": "4.0.11", - "System.Threading": "4.0.11" - } - }, - "System.Reflection.Primitives": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "4inTox4wTBaDhB7V3mPvp9XlCbeGYWVEM9/fXALd52vNEAVisc1BoVWQPuUuD0Ga//dNbA/WeMy9u9mzLxGTHQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Reflection.TypeExtensions": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "tsQ/ptQ3H5FYfON8lL4MxRk/8kFyE0A+tGPXmVP967cT/gzLHYxIejIYSxp4JmIeFHVP78g/F2FE1mUUTbDtrg==", - "dependencies": { - "System.Reflection": "4.1.0", - "System.Runtime": "4.1.0" - } - }, - "System.Resources.ResourceManager": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "TxwVeUNoTgUOdQ09gfTjvW411MF+w9MBYL7AtNVc+HtBCFlutPLhUCdZjNkjbhj3bNQWMdHboF0KIWEOjJssbA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Globalization": "4.0.11", - "System.Reflection": "4.1.0", - "System.Runtime": "4.1.0" - } - }, - "System.Runtime": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "v6c/4Yaa9uWsq+JMhnOFewrYkgdNHNG2eMKuNqRn8P733rNXeRCGvV5FkkjBXn2dbVkPXOsO0xjsEeM1q2zC0g==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1" - } - }, - "System.Runtime.Extensions": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "CUOHjTT/vgP0qGW22U4/hDlOqXmcPq5YicBaXdUR2UiUoLwBT+olO6we4DVbq57jeX5uXH2uerVZhf0qGj+sVQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Runtime.Handles": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "nCJvEKguXEvk2ymk1gqj625vVnlK3/xdGzx0vOKicQkoquaTBJTP13AIYkocSUwHCLNBwUbXTqTWGDxBTWpt7g==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Runtime.InteropServices": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "16eu3kjHS633yYdkjwShDHZLRNMKVi/s0bY8ODiqJ2RfMhDMAwxZaUaWVnZ2P71kr/or+X9o/xFWtNqz8ivieQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Reflection": "4.1.0", - "System.Reflection.Primitives": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Handles": "4.0.1" - } - }, - "System.Runtime.Numerics": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "+XbKFuzdmLP3d1o9pdHu2nxjNr2OEPqGzKeegPLCUMM71a0t50A/rOcIRmGs9wR7a8KuHX6hYs/7/TymIGLNqg==", - "dependencies": { - "System.Globalization": "4.0.11", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0" - } - }, - "System.Security.Cryptography.Algorithms": { - "type": "Transitive", - "resolved": "4.2.0", - "contentHash": "8JQFxbLVdrtIOKMDN38Fn0GWnqYZw/oMlwOUG/qz1jqChvyZlnUmu+0s7wLx7JYua/nAXoESpHA3iw11QFWhXg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "System.Collections": "4.0.11", - "System.IO": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.Handles": "4.0.1", - "System.Runtime.InteropServices": "4.1.0", - "System.Runtime.Numerics": "4.0.1", - "System.Security.Cryptography.Encoding": "4.0.0", - "System.Security.Cryptography.Primitives": "4.0.0", - "System.Text.Encoding": "4.0.11", - "runtime.native.System.Security.Cryptography": "4.0.0" - } - }, - "System.Security.Cryptography.Cng": { - "type": "Transitive", - "resolved": "4.2.0", - "contentHash": "cUJ2h+ZvONDe28Szw3st5dOHdjndhJzQ2WObDEXAWRPEQBtVItVoxbXM/OEsTthl3cNn2dk2k0I3y45igCQcLw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "System.IO": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.Handles": "4.0.1", - "System.Runtime.InteropServices": "4.1.0", - "System.Security.Cryptography.Algorithms": "4.2.0", - "System.Security.Cryptography.Encoding": "4.0.0", - "System.Security.Cryptography.Primitives": "4.0.0", - "System.Text.Encoding": "4.0.11" - } - }, - "System.Security.Cryptography.Csp": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "/i1Usuo4PgAqgbPNC0NjbO3jPW//BoBlTpcWFD1EHVbidH21y4c1ap5bbEMSGAXjAShhMH4abi/K8fILrnu4BQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "System.IO": "4.1.0", - "System.Reflection": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.Handles": "4.0.1", - "System.Runtime.InteropServices": "4.1.0", - "System.Security.Cryptography.Algorithms": "4.2.0", - "System.Security.Cryptography.Encoding": "4.0.0", - "System.Security.Cryptography.Primitives": "4.0.0", - "System.Text.Encoding": "4.0.11", - "System.Threading": "4.0.11" - } - }, - "System.Security.Cryptography.Encoding": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "FbKgE5MbxSQMPcSVRgwM6bXN3GtyAh04NkV8E5zKCBE26X0vYW0UtTa2FIgkH33WVqBVxRgxljlVYumWtU+HcQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "System.Collections": "4.0.11", - "System.Collections.Concurrent": "4.0.12", - "System.Linq": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.Handles": "4.0.1", - "System.Runtime.InteropServices": "4.1.0", - "System.Security.Cryptography.Primitives": "4.0.0", - "System.Text.Encoding": "4.0.11", - "runtime.native.System.Security.Cryptography": "4.0.0" - } - }, - "System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "HUG/zNUJwEiLkoURDixzkzZdB5yGA5pQhDP93ArOpDPQMteURIGERRNzzoJlmTreLBWr5lkFSjjMSk8ySEpQMw==", - "dependencies": { - "System.Collections": "4.0.11", - "System.IO": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.Handles": "4.0.1", - "System.Runtime.InteropServices": "4.1.0", - "System.Runtime.Numerics": "4.0.1", - "System.Security.Cryptography.Algorithms": "4.2.0", - "System.Security.Cryptography.Encoding": "4.0.0", - "System.Security.Cryptography.Primitives": "4.0.0", - "System.Text.Encoding": "4.0.11", - "runtime.native.System.Security.Cryptography": "4.0.0" - } - }, - "System.Security.Cryptography.Primitives": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "Wkd7QryWYjkQclX0bngpntW5HSlMzeJU24UaLJQ7YTfI8ydAVAaU2J+HXLLABOVJlKTVvAeL0Aj39VeTe7L+oA==", - "dependencies": { - "System.Diagnostics.Debug": "4.0.11", - "System.Globalization": "4.0.11", - "System.IO": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Threading": "4.0.11", - "System.Threading.Tasks": "4.0.11" - } - }, - "System.Security.Cryptography.X509Certificates": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "4HEfsQIKAhA1+ApNn729Gi09zh+lYWwyIuViihoMDWp1vQnEkL2ct7mAbhBlLYm+x/L4Rr/pyGge1lIY635e0w==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Globalization": "4.0.11", - "System.Globalization.Calendars": "4.0.1", - "System.IO": "4.1.0", - "System.IO.FileSystem": "4.0.1", - "System.IO.FileSystem.Primitives": "4.0.1", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.Handles": "4.0.1", - "System.Runtime.InteropServices": "4.1.0", - "System.Runtime.Numerics": "4.0.1", - "System.Security.Cryptography.Algorithms": "4.2.0", - "System.Security.Cryptography.Cng": "4.2.0", - "System.Security.Cryptography.Csp": "4.0.0", - "System.Security.Cryptography.Encoding": "4.0.0", - "System.Security.Cryptography.OpenSsl": "4.0.0", - "System.Security.Cryptography.Primitives": "4.0.0", - "System.Text.Encoding": "4.0.11", - "System.Threading": "4.0.11", - "runtime.native.System": "4.0.0", - "runtime.native.System.Net.Http": "4.0.1", - "runtime.native.System.Security.Cryptography": "4.0.0" - } - }, - "System.Text.Encoding": { - "type": "Transitive", - "resolved": "4.0.11", - "contentHash": "U3gGeMlDZXxCEiY4DwVLSacg+DFWCvoiX+JThA/rvw37Sqrku7sEFeVBBBMBnfB6FeZHsyDx85HlKL19x0HtZA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Text.Encoding.CodePages": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "h4z6rrA/hxWf4655D18IIZ0eaLRa3tQC/j+e26W+VinIHY0l07iEXaAvO0YSYq3MvCjMYy8Zs5AdC1sxNQOB7Q==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "System.Collections": "4.0.11", - "System.Globalization": "4.0.11", - "System.IO": "4.1.0", - "System.Reflection": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.Handles": "4.0.1", - "System.Runtime.InteropServices": "4.1.0", - "System.Text.Encoding": "4.0.11", - "System.Threading": "4.0.11" - } - }, - "System.Text.Encoding.Extensions": { - "type": "Transitive", - "resolved": "4.0.11", - "contentHash": "jtbiTDtvfLYgXn8PTfWI+SiBs51rrmO4AAckx4KR6vFK9Wzf6tI8kcRdsYQNwriUeQ1+CtQbM1W4cMbLXnj/OQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0", - "System.Text.Encoding": "4.0.11" - } - }, - "System.Text.RegularExpressions": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "i88YCXpRTjCnoSQZtdlHkAOx4KNNik4hMy83n0+Ftlb7jvV6ZiZWMpnEZHhjBp6hQVh8gWd/iKNPzlPF7iyA2g==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Globalization": "4.0.11", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Threading": "4.0.11" - } - }, - "System.Threading": { - "type": "Transitive", - "resolved": "4.0.11", - "contentHash": "N+3xqIcg3VDKyjwwCGaZ9HawG9aC6cSDI+s7ROma310GQo8vilFZa86hqKppwTHleR/G0sfOzhvgnUxWCR/DrQ==", - "dependencies": { - "System.Runtime": "4.1.0", - "System.Threading.Tasks": "4.0.11" - } - }, - "System.Threading.Tasks": { - "type": "Transitive", - "resolved": "4.0.11", - "contentHash": "k1S4Gc6IGwtHGT8188RSeGaX86Qw/wnrgNLshJvsdNUOPP9etMmo8S07c+UlOAx4K/xLuN9ivA1bD0LVurtIxQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Threading.Tasks.Extensions": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "pH4FZDsZQ/WmgJtN4LWYmRdJAEeVkyriSwrv2Teoe5FOU0Yxlb6II6GL8dBPOfRmutHGATduj3ooMt7dJ2+i+w==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Runtime": "4.1.0", - "System.Threading.Tasks": "4.0.11" - } - }, - "System.Threading.Tasks.Parallel": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "7Pc9t25bcynT9FpMvkUw4ZjYwUiGup/5cJFW72/5MgCG+np2cfVUMdh29u8d7onxX7d8PS3J+wL73zQRqkdrSA==", - "dependencies": { - "System.Collections.Concurrent": "4.0.12", - "System.Diagnostics.Debug": "4.0.11", - "System.Diagnostics.Tracing": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Threading": "4.0.11", - "System.Threading.Tasks": "4.0.11" - } - }, - "System.Threading.Thread": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "gIdJqDXlOr5W9zeqFErLw3dsOsiShSCYtF9SEHitACycmvNvY8odf9kiKvp6V7aibc8C4HzzNBkWXjyfn7plbQ==", - "dependencies": { - "System.Runtime": "4.1.0" - } - }, - "System.Xml.ReaderWriter": { - "type": "Transitive", - "resolved": "4.0.11", - "contentHash": "ZIiLPsf67YZ9zgr31vzrFaYQqxRPX9cVHjtPSnmx4eN6lbS/yEyYNr2vs1doGDEscF0tjCZFsk9yUg1sC9e8tg==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Globalization": "4.0.11", - "System.IO": "4.1.0", - "System.IO.FileSystem": "4.0.1", - "System.IO.FileSystem.Primitives": "4.0.1", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.InteropServices": "4.1.0", - "System.Text.Encoding": "4.0.11", - "System.Text.Encoding.Extensions": "4.0.11", - "System.Text.RegularExpressions": "4.1.0", - "System.Threading.Tasks": "4.0.11", - "System.Threading.Tasks.Extensions": "4.0.0" - } - }, - "System.Xml.XDocument": { - "type": "Transitive", - "resolved": "4.0.11", - "contentHash": "Mk2mKmPi0nWaoiYeotq1dgeNK1fqWh61+EK+w4Wu8SWuTYLzpUnschb59bJtGywaPq7SmTuPf44wrXRwbIrukg==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Diagnostics.Tools": "4.0.1", - "System.Globalization": "4.0.11", - "System.IO": "4.1.0", - "System.Reflection": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Text.Encoding": "4.0.11", - "System.Threading": "4.0.11", - "System.Xml.ReaderWriter": "4.0.11" - } - }, - "System.Xml.XmlDocument": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "2eZu6IP+etFVBBFUFzw2w6J21DqIN5eL9Y8r8JfJWUmV28Z5P0SNU01oCisVHQgHsDhHPnmq2s1hJrJCFZWloQ==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Globalization": "4.0.11", - "System.IO": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Text.Encoding": "4.0.11", - "System.Threading": "4.0.11", - "System.Xml.ReaderWriter": "4.0.11" - } - }, - "System.Xml.XPath": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "UWd1H+1IJ9Wlq5nognZ/XJdyj8qPE4XufBUkAW59ijsCPjZkZe0MUzKKJFBr+ZWBe5Wq1u1d5f2CYgE93uH7DA==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Globalization": "4.0.11", - "System.IO": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Threading": "4.0.11", - "System.Xml.ReaderWriter": "4.0.11" - } - }, - "System.Xml.XPath.XDocument": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "FLhdYJx4331oGovQypQ8JIw2kEmNzCsjVOVYY/16kZTUoquZG85oVn7yUhBE2OZt1yGPSXAL0HTEfzjlbNpM7Q==", - "dependencies": { - "System.Diagnostics.Debug": "4.0.11", - "System.Linq": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Threading": "4.0.11", - "System.Xml.ReaderWriter": "4.0.11", - "System.Xml.XDocument": "4.0.11", - "System.Xml.XPath": "4.0.1" - } - }, - "SonarAnalyzer": { - "type": "Project", - "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Google.Protobuf.Tools": "[3.6.1, )", - "Microsoft.CodeAnalysis.Workspaces.Common": "[1.3.2, )", - "Microsoft.Composition": "[1.0.27, )", - "SonarAnalyzer.CFG": "[1.0.0, )", - "System.Collections.Immutable": "[1.1.37, )" - } - }, - "sonaranalyzer.cfg": { - "type": "Project", - "dependencies": { - "Microsoft.CodeAnalysis.CSharp.Workspaces": "[1.3.2, )", - "Microsoft.CodeAnalysis.VisualBasic.Workspaces": "[1.3.2, )", - "Microsoft.Composition": "[1.0.27, )", - "System.Collections.Immutable": "[1.1.37, )" - } - } - } - } -} \ No newline at end of file From f83cb2c16b8201e9ec390a31156f45ba3deee377 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 11 Mar 2024 10:12:36 +0100 Subject: [PATCH 24/62] Rename SymbolStartAnalysisContext to SymbolStartAnalysisContextWrapper --- .../SymbolStartAnalysisContext.cs | 161 ------------------ .../SymbolStartAnalysisContextWrapper.cs | 4 + .../RegisterSymbolStartActionWrapperTest.cs | 2 +- 3 files changed, 5 insertions(+), 162 deletions(-) delete mode 100644 analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs deleted file mode 100644 index d532a0f5a59..00000000000 --- a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContext.cs +++ /dev/null @@ -1,161 +0,0 @@ -/* - * SonarAnalyzer for .NET - * Copyright (C) 2015-2024 SonarSource SA - * mailto: contact AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using static System.Linq.Expressions.Expression; -using CS = Microsoft.CodeAnalysis.CSharp; -using VB = Microsoft.CodeAnalysis.VisualBasic; - -namespace SonarAnalyzer.ShimLayer.AnalysisContext; - -public readonly struct SymbolStartAnalysisContext -{ - private static Func cancellationTokenAccessor; - private static Func compilationAccessor; - private static Func optionsAccessor; - private static Func symbolAccessor; - - private static Action> registerCodeBlockAction; - private static Action>> registerCodeBlockStartActionCS; - private static Action>> registerCodeBlockStartActionVB; - private static Action, ImmutableArray> registerOperationAction; - private static Action> registerOperationBlockAction; - private static Action> registerOperationBlockStartAction; - private static Action> registerSymbolEndAction; - private static Action, ImmutableArray> registerSyntaxNodeActionCS; - private static Action, ImmutableArray> registerSyntaxNodeActionVB; - - private object RoslynSymbolStartAnalysisContext { get; } - public CancellationToken CancellationToken => cancellationTokenAccessor(RoslynSymbolStartAnalysisContext); - public Compilation Compilation => compilationAccessor(RoslynSymbolStartAnalysisContext); - public AnalyzerOptions Options => optionsAccessor(RoslynSymbolStartAnalysisContext); - public ISymbol Symbol => symbolAccessor(RoslynSymbolStartAnalysisContext); - - static SymbolStartAnalysisContext() - { - var symbolStartAnalysisContextType = typeof(CompilationStartAnalysisContext).Assembly.GetType("Microsoft.CodeAnalysis.Diagnostics.SymbolStartAnalysisContext"); - cancellationTokenAccessor = CreatePropertyAccessor(nameof(CancellationToken)); - compilationAccessor = CreatePropertyAccessor(nameof(Compilation)); - optionsAccessor = CreatePropertyAccessor(nameof(Options)); - symbolAccessor = CreatePropertyAccessor(nameof(Symbol)); - registerCodeBlockAction = CreateRegistrationMethod(nameof(RegisterCodeBlockAction)); - registerCodeBlockStartActionCS = - CreateRegistrationMethod>(nameof(RegisterCodeBlockStartAction), typeof(CS.SyntaxKind)); - registerCodeBlockStartActionVB = - CreateRegistrationMethod>(nameof(RegisterCodeBlockStartAction), typeof(VB.SyntaxKind)); - registerOperationAction = - CreateRegistrationMethodWithAdditionalParameter>(nameof(RegisterOperationAction)); - registerOperationBlockAction = CreateRegistrationMethod(nameof(RegisterOperationBlockAction)); - registerOperationBlockStartAction = CreateRegistrationMethod(nameof(RegisterOperationBlockStartAction)); - registerSymbolEndAction = CreateRegistrationMethod(nameof(RegisterSymbolEndAction)); - registerSyntaxNodeActionCS = CreateRegistrationMethodWithAdditionalParameter>( - nameof(RegisterSyntaxNodeAction), typeof(CS.SyntaxKind)); - registerSyntaxNodeActionVB = CreateRegistrationMethodWithAdditionalParameter>( - nameof(RegisterSyntaxNodeAction), typeof(VB.SyntaxKind)); - - // symbolStartAnalysisContextParameter => ((symbolStartAnalysisContextType)receiverParameter)."propertyName" - Func CreatePropertyAccessor(string propertyName) - { - var receiverParameter = Parameter(typeof(object)); - return Lambda>( - Property(Convert(receiverParameter, symbolStartAnalysisContextType), propertyName), - receiverParameter).Compile(); - } - - // (object receiverParameter, Action registerActionParameter) - // => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter)."registrationMethodName"(registerActionParameter) - Action> CreateRegistrationMethod(string registrationMethodName, params Type[] typeArguments) - { - var receiverParameter = Parameter(typeof(object)); - var registerActionParameter = Parameter(typeof(Action)); - return Lambda>>( - Call(Convert(receiverParameter, symbolStartAnalysisContextType), registrationMethodName, typeArguments, registerActionParameter), - receiverParameter, - registerActionParameter).Compile(); - } - - // (object receiverParameter, Action registerActionParameter, TParameter additionalParameter) - // => ((symbolStartAnalysisContextType)symbolStartAnalysisContextParameter)."registrationMethodName"(registerActionParameter, additionalParameter) - Action, TParameter> CreateRegistrationMethodWithAdditionalParameter(string registrationMethodName, params Type[] typeArguments) - { - var receiverParameter = Parameter(typeof(object)); - var registerActionParameter = Parameter(typeof(Action)); - var additionalParameter = Parameter(typeof(TParameter)); - return Lambda, TParameter>>( - Call(Convert(receiverParameter, symbolStartAnalysisContextType), registrationMethodName, typeArguments, registerActionParameter, additionalParameter), - receiverParameter, - registerActionParameter, - additionalParameter).Compile(); - } - } - - public SymbolStartAnalysisContext(object roslynSymbolStartAnalysisContext) => - RoslynSymbolStartAnalysisContext = roslynSymbolStartAnalysisContext; - - public void RegisterCodeBlockAction(Action action) => - registerCodeBlockAction(RoslynSymbolStartAnalysisContext, action); - - public void RegisterCodeBlockStartAction(Action> action) where TLanguageKindEnum : struct - { - var languageKindType = typeof(TLanguageKindEnum); - if (languageKindType == typeof(CS.SyntaxKind)) - { - var cast = (Action>)action; - registerCodeBlockStartActionCS(RoslynSymbolStartAnalysisContext, cast); - } - else if (languageKindType == typeof(VB.SyntaxKind)) - { - var cast = (Action>)action; - registerCodeBlockStartActionVB(RoslynSymbolStartAnalysisContext, cast); - } - else - { - throw new ArgumentException("Invalid type parameter.", nameof(TLanguageKindEnum)); - } - } - - public void RegisterOperationAction(Action action, ImmutableArray operationKinds) => - registerOperationAction(RoslynSymbolStartAnalysisContext, action, operationKinds); - - public void RegisterOperationBlockAction(Action action) => - registerOperationBlockAction(RoslynSymbolStartAnalysisContext, action); - - public void RegisterOperationBlockStartAction(Action action) => - registerOperationBlockStartAction(RoslynSymbolStartAnalysisContext, action); - - public void RegisterSymbolEndAction(Action action) => - registerSymbolEndAction(RoslynSymbolStartAnalysisContext, action); - - public void RegisterSyntaxNodeAction(Action action, params TLanguageKindEnum[] syntaxKinds) where TLanguageKindEnum : struct - { - var languageKindType = typeof(TLanguageKindEnum); - if (languageKindType == typeof(CS.SyntaxKind)) - { - registerSyntaxNodeActionCS(RoslynSymbolStartAnalysisContext, action, syntaxKinds.Cast().ToImmutableArray()); - } - else if (languageKindType == typeof(VB.SyntaxKind)) - { - registerSyntaxNodeActionVB(RoslynSymbolStartAnalysisContext, action, syntaxKinds.Cast().ToImmutableArray()); - } - else - { - throw new ArgumentException("Invalid type parameter.", nameof(TLanguageKindEnum)); - } - } -} diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContextWrapper.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContextWrapper.cs index 9a52b150fc6..a809a92c6c3 100644 --- a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContextWrapper.cs +++ b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContextWrapper.cs @@ -47,6 +47,10 @@ public readonly struct SymbolStartAnalysisContextWrapper public AnalyzerOptions Options => OptionsAccessor(RoslynSymbolStartAnalysisContext); public ISymbol Symbol => SymbolAccessor(RoslynSymbolStartAnalysisContext); private object RoslynSymbolStartAnalysisContext { get; } + public CancellationToken CancellationToken => cancellationTokenAccessor(RoslynSymbolStartAnalysisContext); + public Compilation Compilation => compilationAccessor(RoslynSymbolStartAnalysisContext); + public AnalyzerOptions Options => optionsAccessor(RoslynSymbolStartAnalysisContext); + public ISymbol Symbol => symbolAccessor(RoslynSymbolStartAnalysisContext); // Code is executed in static initializers and is not detected by the coverage tool // See the RegisterSymbolStartActionWrapperTest family of tests to check test coverage manually diff --git a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs index bc6d0ac4ce2..f4f48c8abc7 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs @@ -20,7 +20,7 @@ using SonarAnalyzer.ShimLayer.AnalysisContext; using CS = Microsoft.CodeAnalysis.CSharp; -using SonarSymbolStartAnalysisContext = SonarAnalyzer.ShimLayer.AnalysisContext.SymbolStartAnalysisContext; +using SonarSymbolStartAnalysisContext = SonarAnalyzer.ShimLayer.AnalysisContext.SymbolStartAnalysisContextWrapper; using VB = Microsoft.CodeAnalysis.VisualBasic; namespace SonarAnalyzer.Test.Wrappers; From dd1a605006bfc73146cbfc6e4f18128b8f45a50b Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 11 Mar 2024 10:20:27 +0100 Subject: [PATCH 25/62] Make fields readonly and rename --- .../AnalysisContext/SymbolStartAnalysisContextWrapper.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContextWrapper.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContextWrapper.cs index a809a92c6c3..87b713ac0b3 100644 --- a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContextWrapper.cs +++ b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContextWrapper.cs @@ -47,10 +47,10 @@ public readonly struct SymbolStartAnalysisContextWrapper public AnalyzerOptions Options => OptionsAccessor(RoslynSymbolStartAnalysisContext); public ISymbol Symbol => SymbolAccessor(RoslynSymbolStartAnalysisContext); private object RoslynSymbolStartAnalysisContext { get; } - public CancellationToken CancellationToken => cancellationTokenAccessor(RoslynSymbolStartAnalysisContext); - public Compilation Compilation => compilationAccessor(RoslynSymbolStartAnalysisContext); - public AnalyzerOptions Options => optionsAccessor(RoslynSymbolStartAnalysisContext); - public ISymbol Symbol => symbolAccessor(RoslynSymbolStartAnalysisContext); + public CancellationToken CancellationToken => CancellationTokenAccessor(RoslynSymbolStartAnalysisContext); + public Compilation Compilation => CompilationAccessor(RoslynSymbolStartAnalysisContext); + public AnalyzerOptions Options => OptionsAccessor(RoslynSymbolStartAnalysisContext); + public ISymbol Symbol => SymbolAccessor(RoslynSymbolStartAnalysisContext); // Code is executed in static initializers and is not detected by the coverage tool // See the RegisterSymbolStartActionWrapperTest family of tests to check test coverage manually From 9c9f0c52232e89b90b32deb0ae1d467f43a473c9 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 11 Mar 2024 10:26:25 +0100 Subject: [PATCH 26/62] Move property --- .../AnalysisContext/SymbolStartAnalysisContextWrapper.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContextWrapper.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContextWrapper.cs index 87b713ac0b3..c6317a1216d 100644 --- a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContextWrapper.cs +++ b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContextWrapper.cs @@ -51,6 +51,7 @@ public readonly struct SymbolStartAnalysisContextWrapper public Compilation Compilation => CompilationAccessor(RoslynSymbolStartAnalysisContext); public AnalyzerOptions Options => OptionsAccessor(RoslynSymbolStartAnalysisContext); public ISymbol Symbol => SymbolAccessor(RoslynSymbolStartAnalysisContext); + private object RoslynSymbolStartAnalysisContext { get; } // Code is executed in static initializers and is not detected by the coverage tool // See the RegisterSymbolStartActionWrapperTest family of tests to check test coverage manually From 0c1ff8a6b93792018666f97981e9c5a7b2e2aa5b Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 11 Mar 2024 10:26:42 +0100 Subject: [PATCH 27/62] Remove alias --- .../Wrappers/RegisterSymbolStartActionWrapperTest.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs index f4f48c8abc7..6fc4b9dc39c 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs @@ -20,7 +20,6 @@ using SonarAnalyzer.ShimLayer.AnalysisContext; using CS = Microsoft.CodeAnalysis.CSharp; -using SonarSymbolStartAnalysisContext = SonarAnalyzer.ShimLayer.AnalysisContext.SymbolStartAnalysisContextWrapper; using VB = Microsoft.CodeAnalysis.VisualBasic; namespace SonarAnalyzer.Test.Wrappers; @@ -591,7 +590,7 @@ End Class #pragma warning disable RS1026 // Enable concurrent execution private class TestDiagnosticAnalyzer : DiagnosticAnalyzer { - public TestDiagnosticAnalyzer(Action action, SymbolKind symbolKind) + public TestDiagnosticAnalyzer(Action action, SymbolKind symbolKind) { Action = action; SymbolKind = symbolKind; @@ -599,7 +598,7 @@ public TestDiagnosticAnalyzer(Action action, Sy public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(new DiagnosticDescriptor("TEST", "Test", "Test", "Test", DiagnosticSeverity.Warning, true)); - public Action Action { get; } + public Action Action { get; } public SymbolKind SymbolKind { get; } public override void Initialize(Microsoft.CodeAnalysis.Diagnostics.AnalysisContext context) => From f259d3ee1f4fc0a2f42ba9690a2b41bdbc91a261 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 11 Mar 2024 10:57:18 +0100 Subject: [PATCH 28/62] Inline "code" variable --- .../RegisterSymbolStartActionWrapperTest.cs | 43 ++++++++----------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs index 6fc4b9dc39c..b6527b06c9e 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs @@ -30,14 +30,13 @@ public class RegisterSymbolStartActionWrapperTest [TestMethod] public async Task RegisterSymbolStartAction_SymbolStartProperties() { - var code = """ + var snippet = new SnippetCompiler(""" public class C { int i = 0; public void M() => ToString(); } - """; - var snippet = new SnippetCompiler(code); + """); var symbolStartWasCalled = false; var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create( new TestDiagnosticAnalyzer(symbolStart => @@ -377,8 +376,7 @@ public class C int i = 0; public void M() => ToString(); } - """; - var snippet = new SnippetCompiler(code); + """); var visited = new List(); var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create( new TestDiagnosticAnalyzer(symbolStart => @@ -397,7 +395,7 @@ public class C [TestMethod] public async Task RegisterSymbolStartAction_RegisterCodeBlockStartAction_VB() { - var code = """ + var snippet = new SnippetCompiler(""" Public Class C Private i As Integer = 0 @@ -405,8 +403,7 @@ Public Sub M() Call ToString() End Sub End Class - """; - var snippet = new SnippetCompiler(code, ignoreErrors: false, AnalyzerLanguage.VisualBasic); + """, ignoreErrors: false, AnalyzerLanguage.VisualBasic); var visited = new List(); var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create( new TestDiagnosticAnalyzer(symbolStart => @@ -432,7 +429,7 @@ End Sub [TestMethod] public async Task RegisterSymbolStartAction_RegisterOperationAction() { - var code = """ + var snippet = new SnippetCompiler(""" public class C { int i = 0; @@ -441,8 +438,7 @@ public void M() ToString(); } } - """; - var snippet = new SnippetCompiler(code); + """); var visited = new List(); var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create( new TestDiagnosticAnalyzer(symbolStart => @@ -460,14 +456,13 @@ public void M() [TestMethod] public async Task RegisterSymbolStartAction_RegisterOperationBlockAction() { - var code = """ + var snippet = new SnippetCompiler(""" public class C { int i = 0; public void M() => ToString(); } - """; - var snippet = new SnippetCompiler(code); + """); var visited = new List(); var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create( new TestDiagnosticAnalyzer(symbolStart => @@ -485,14 +480,13 @@ public class C [TestMethod] public async Task RegisterSymbolStartAction_RegisterOperationBlockStartAction() { - var code = """ + var snippet = new SnippetCompiler(""" public class C { int i = 0; public void M() => ToString(); } - """; - var snippet = new SnippetCompiler(code); + """); var visited = new List(); var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create( new TestDiagnosticAnalyzer(symbolStart => @@ -511,14 +505,13 @@ public class C [TestMethod] public async Task RegisterSymbolStartAction_RegisterRegisterSymbolEndAction() { - var code = """ + var snippet = new SnippetCompiler(""" public class C { int i = 0; public void M() => ToString(); } - """; - var snippet = new SnippetCompiler(code); + """); var visited = new List(); var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create( new TestDiagnosticAnalyzer(symbolStart => @@ -536,14 +529,13 @@ public class C [TestMethod] public async Task RegisterSymbolStartAction_RegisterSyntaxNodeAction_CS() { - var code = """ + var snippet = new SnippetCompiler(""" public class C { int i = 0; public void M() => ToString(); } - """; - var snippet = new SnippetCompiler(code); + """); var visited = new List(); var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create( new TestDiagnosticAnalyzer(symbolStart => @@ -561,7 +553,7 @@ public class C [TestMethod] public async Task RegisterSymbolStartAction_RegisterSyntaxNodeAction_VB() { - var code = """ + var snippet = new SnippetCompiler(""" Public Class C Private i As Integer = 0 @@ -569,8 +561,7 @@ Public Sub M() Call ToString() End Sub End Class - """; - var snippet = new SnippetCompiler(code, ignoreErrors: false, AnalyzerLanguage.VisualBasic); + """, ignoreErrors: false, AnalyzerLanguage.VisualBasic); var visited = new List(); var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create( new TestDiagnosticAnalyzer(symbolStart => From a07b70b0e77fa0e88c03387cad444313c6891f6f Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 11 Mar 2024 11:00:16 +0100 Subject: [PATCH 29/62] Add RegisterSymbolStartAction_RegisterCodeBlockAction_ConditionalRegistration --- .../Wrappers/RegisterSymbolStartActionWrapperTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs index b6527b06c9e..df7f94e32ef 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs @@ -47,9 +47,9 @@ public class C symbolStart.Symbol.Should().BeAssignableTo().Which.Name.Should().Be("C"); symbolStartWasCalled = true; }, SymbolKind.NamedType))); - var diags = await compilation.GetAnalyzerDiagnosticsAsync(); + var diagnostics = await compilation.GetAnalyzerDiagnosticsAsync(); symbolStartWasCalled.Should().BeTrue(); - diags.Should().BeEmpty(); + diagnostics.Should().BeEmpty(); } [TestMethod] From d5f16fbcc09a4e55c12447768ce9e968618bba15 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 11 Mar 2024 12:40:23 +0100 Subject: [PATCH 30/62] Improve test coverage --- .../CompilationStartAnalysisContextExtensions.cs | 3 +++ .../AnalysisContext/SymbolStartAnalysisContextWrapper.cs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs index 89365b4b1a6..7b3a25a5cfb 100644 --- a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs +++ b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs @@ -23,6 +23,9 @@ namespace SonarAnalyzer.ShimLayer.AnalysisContext; +// Code is executed in static initializers and is not detected by the coverage tool +// See the SonarAnalysisContextTest.SonarCompilationStartAnalysisContext_RegisterSymbolStartAction family of tests to check test coverage manually +[ExcludeFromCodeCoverage] public static class CompilationStartAnalysisContextExtensions { { diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContextWrapper.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContextWrapper.cs index c6317a1216d..d71da9b4a95 100644 --- a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContextWrapper.cs +++ b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContextWrapper.cs @@ -25,6 +25,9 @@ namespace SonarAnalyzer.ShimLayer.AnalysisContext; +// Code is executed in static initializers and is not detected by the coverage tool +// See the RegisterSymbolStartActionWrapperTest family of tests to check test coverage manually +[ExcludeFromCodeCoverage] public readonly struct SymbolStartAnalysisContextWrapper { private static readonly Func CancellationTokenAccessor; From 93c9cb5a492e9734fb0c076e7263d1ae4a3395a7 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 11 Mar 2024 16:03:48 +0100 Subject: [PATCH 31/62] Scaffold rule with ./scripts/rspec/rspec -language cs -ruleKey S6932 -rspecBranch "rule/add-RSPEC-S6932" ClassName --- analyzers/rspec/cs/S6932.html | 336 ++++++++++++++++++ analyzers/rspec/cs/S6932.json | 25 ++ .../SonarAnalyzer.CSharp/Rules/ClassName.cs | 43 +++ .../PackagingTests/RuleTypeMappingCS.cs | 2 +- .../SonarAnalyzer.Test/Rules/ClassNameTest.cs | 33 ++ .../SonarAnalyzer.Test/TestCases/ClassName.cs | 5 + 6 files changed, 443 insertions(+), 1 deletion(-) create mode 100644 analyzers/rspec/cs/S6932.html create mode 100644 analyzers/rspec/cs/S6932.json create mode 100644 analyzers/src/SonarAnalyzer.CSharp/Rules/ClassName.cs create mode 100644 analyzers/tests/SonarAnalyzer.Test/Rules/ClassNameTest.cs create mode 100644 analyzers/tests/SonarAnalyzer.Test/TestCases/ClassName.cs diff --git a/analyzers/rspec/cs/S6932.html b/analyzers/rspec/cs/S6932.html new file mode 100644 index 00000000000..7f0cef39296 --- /dev/null +++ b/analyzers/rspec/cs/S6932.html @@ -0,0 +1,336 @@ +

The HttpRequest class provides access to the raw request data through the QueryString, Headers, and +Forms properties. However, whenever possible it is recommended to use model binding instead of directly accessing the input data.

+

Why is this an issue?

+

Both ASP.Net MVC implementations - Core and Framework - support model binding in a comparable fashion. Model binding streamlines the +process by automatically aligning data from HTTP requests with action method parameters, providing numerous benefits compared to manually parsing raw +incoming request data:

+
+
+ Simplicity +
+
+

Model binding simplifies the code by automatically mapping data from HTTP requests to action method parameters. You don’t need to write any + code to manually extract values from the request.

+
+
+ Type Safety +
+
+

Model binding provides type safety by automatically converting the incoming data into the appropriate .NET types. If the conversion fails, the + model state becomes invalid, which you can easily check using ModelState.IsValid.

+
+
+ Validation +
+
+

With model binding, you can easily apply validation rules to your models using data annotations. If the incoming data doesn’t comply with these + rules, the model state becomes invalid.

+
+
+ Security +
+
+

Model binding helps protect against over-posting attacks by only including properties in the model that you explicitly bind using the + [Bind] attribute or by using view models that only contain the properties you want to update.

+
+
+ Maintainability +
+
+

By using model binding, your code becomes cleaner, easier to read, and maintain. It promotes the use of strongly typed views, which can provide + compile-time checking of your views.

+
+
+

How to fix it in ASP.NET Core

+

Request.Form, Request.Form.Files, Request.Headers, Request.Query and Request.RouteValues are keyed +collections that expose data from the incoming HTTP request:

+ +

Model binding can bind these keyed collections to

+
    +
  • action method parameters by matching the key to the parameter name or
  • +
  • the property of a complex type by matching the key to the property name.
  • +
+

To replace the keyed collection access, you can:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Replacewith parameter bindingor complex type bindingor route binding

Request.Form["id"]

optional [FromForm] + attribute on the parameter or a FormCollection parameter

optional [FromForm] + attribute on the property

Request.Form.Files

IFormFile, IFormFileCollection, or + IEnumerable<IFormFile> parameter

Request.Headers["id"]

[FromHeader] + attribute on the parameter

[FromHeader] + attribute on the property

Request.Query["id"]

optional [FromQuery] + attribute on the parameter

optional [FromQuery] + attribute on the property

Request.RouteValues["id"]

optional [FromRoute] + attribute on the parameter

optional [Route("{id}")]attribute on the + action method/controller or via conventional routing

+

The Model Binding in ASP.NET Core article describes the +mechanisms, conventions, and customization options for model binding in more detail. Route-based binding is described in the Routing to controller actions in ASP.NET Core document.

+

Code examples

+

Noncompliant code example

+
+public IActionResult Post()
+{
+    var name = Request.Form["name"];                           // Noncompliant: Request.Form
+    var birthdate = DateTime.Parse(Request.Form["Birthdate"]); // Noncompliant: Request.Form
+
+    var origin = Request.Headers[HeaderNames.Origin];          // Noncompliant: Request.Headers
+    var locale = Request.Query.TryGetValue("locale", out var locales)
+        ? locales.ToString()
+        : "en-US";                                             // Noncompliant: Request.Query
+    // ..
+}
+
+

Compliant solution

+
+public record User
+{
+    [Required, StringLength(100)]
+    public required string Name { get; init; }
+    [DataType(DataType.Date)]
+    public DateTime? Birthdate { get; init; }
+}
+
+public IActionResult Post(User user, [FromHeader] string origin, [FromQuery] string locale = "en-US")
+{
+    if (ModelState.IsValid)
+    {
+        // ...
+    }
+}
+
+

How does this work?

+

Model binding in ASP.NET Core MVC and ASP.NET MVC 4.x works by automatically mapping data from HTTP requests to action method parameters. Here’s a +step-by-step breakdown of how it works:

+
    +
  1. Request Data When a user submits a form or sends a request to an ASP.NET application, the request data might include form + data, query string parameters, request body, and HTTP headers.
  2. +
  3. Model Binder The model binder’s job is to create .NET objects from the request data. It looks at each parameter in the action + method and attempts to populate it with the incoming data.
  4. +
  5. Value Providers The model binder uses Value Providers to get data from various parts of the request, such as the query string, + form data, or route data. Each value provider tells the model binder where to find values in the request.
  6. +
  7. Binding The model binder tries to match the keys from the incoming data with the properties of the action method’s parameters. + If a match is found, it attempts to convert the incoming data into the appropriate .NET type and assigns it to the parameter.
  8. +
  9. Validation If the model binder can’t convert the value or if the converted value doesn’t pass any specified validation rules, + it adds an error to the ModelState.Errors collection. You can check ModelState.IsValid in your action method to see if any + errors occurred during model binding.
  10. +
  11. Action Method Execution The action method is executed with the bound parameters. If ModelState.IsValid is + false, you can handle the errors in your action method and return an appropriate response.
  12. +
+

See the links in the Resources section for more information.

+

How to fix it in ASP.NET MVC 4.x

+

Request.Form and Request.QueryString are keyed collections +that expose data from the incoming HTTP request:

+ +

Model binding can bind these keyed collections to

+
    +
  • action method parameters by matching the key to the parameter name or
  • +
  • the property of a complex type by matching the key to the property name.
  • +
+

To replace the keyed collection access, you can:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Replacewith parameter bindingor complex type binding

Request.Form["id"]

optional [Bind] attribute on the + parameter or a FormCollection + parameter

optional [Bind] attribute on the + parameter or type

Request.QueryString["id"]

optional [Bind] attribute on the + parameter

property name must match query parameter key

+

Code examples

+

Noncompliant code example

+
+public ActionResult Post()
+{
+    var name = Request.Form["name"];                            // Noncompliant: Request.Form
+    Debug.WriteLine(Request.Form[0]);                           // Compliant: Binding by index is not supported.
+    var birthdate = DateTime.Parse(Request.Form["Birthdate"]);  // Noncompliant: Request.Form
+
+    var cultureName = Request.QueryString["locale"] ?? "en-US"; // Noncompliant: Request.QueryString
+    // ..
+}
+
+

Compliant solution

+
+public class User
+{
+    [Required, StringLength(100)]
+    public string Name { get; set; }
+    [DataType(DataType.Date)]
+    public DateTime? Birthdate { get; set; }
+}
+
+public ActionResult Post(User user, [Bind(Prefix = "locale")] string cultureName = "en-US")
+{
+    if (ModelState.IsValid)
+    {
+        // ...
+    }
+}
+
+

How does this work?

+

Model binding in ASP.NET Core MVC and ASP.NET MVC 4.x works by automatically mapping data from HTTP requests to action method parameters. Here’s a +step-by-step breakdown of how it works:

+
    +
  1. Request Data When a user submits a form or sends a request to an ASP.NET application, the request data might include form + data, query string parameters, request body, and HTTP headers.
  2. +
  3. Model Binder The model binder’s job is to create .NET objects from the request data. It looks at each parameter in the action + method and attempts to populate it with the incoming data.
  4. +
  5. Value Providers The model binder uses Value Providers to get data from various parts of the request, such as the query string, + form data, or route data. Each value provider tells the model binder where to find values in the request.
  6. +
  7. Binding The model binder tries to match the keys from the incoming data with the properties of the action method’s parameters. + If a match is found, it attempts to convert the incoming data into the appropriate .NET type and assigns it to the parameter.
  8. +
  9. Validation If the model binder can’t convert the value or if the converted value doesn’t pass any specified validation rules, + it adds an error to the ModelState.Errors collection. You can check ModelState.IsValid in your action method to see if any + errors occurred during model binding.
  10. +
  11. Action Method Execution The action method is executed with the bound parameters. If ModelState.IsValid is + false, you can handle the errors in your action method and return an appropriate response.
  12. +
+

See the links in the Resources section for more information.

+

Resources

+

Documentation

+ + diff --git a/analyzers/rspec/cs/S6932.json b/analyzers/rspec/cs/S6932.json new file mode 100644 index 00000000000..24c4c6e7f45 --- /dev/null +++ b/analyzers/rspec/cs/S6932.json @@ -0,0 +1,25 @@ +{ + "title": "Use model binding instead of reading raw request data", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5min" + }, + "tags": [ + "asp.net" + ], + "defaultSeverity": "Major", + "ruleSpecification": "RSPEC-6932", + "sqKey": "S6932", + "scope": "Main", + "quickfix": "infeasible", + "code": { + "impacts": { + "MAINTAINABILITY": "HIGH", + "RELIABILITY": "MEDIUM", + "SECURITY": "MEDIUM" + }, + "attribute": "FOCUSED" + } +} diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/ClassName.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/ClassName.cs new file mode 100644 index 00000000000..68f4927565c --- /dev/null +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/ClassName.cs @@ -0,0 +1,43 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2024 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarAnalyzer.Rules.CSharp; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class ClassName : SonarDiagnosticAnalyzer +{ + private const string DiagnosticId = "S6932"; + private const string MessageFormat = "FIXME"; + + private static readonly DiagnosticDescriptor Rule = DescriptorFactory.Create(DiagnosticId, MessageFormat); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + protected override void Initialize(SonarAnalysisContext context) => + context.RegisterNodeAction(c => + { + var node = c.Node; + if (true) + { + c.ReportIssue(Diagnostic.Create(Rule, node.GetLocation())); + } + }, + SyntaxKind.InvocationExpression); +} diff --git a/analyzers/tests/SonarAnalyzer.Test/PackagingTests/RuleTypeMappingCS.cs b/analyzers/tests/SonarAnalyzer.Test/PackagingTests/RuleTypeMappingCS.cs index b09c62fd2f3..cc4034c6cdc 100644 --- a/analyzers/tests/SonarAnalyzer.Test/PackagingTests/RuleTypeMappingCS.cs +++ b/analyzers/tests/SonarAnalyzer.Test/PackagingTests/RuleTypeMappingCS.cs @@ -6856,7 +6856,7 @@ internal static class RuleTypeMappingCS // ["S6929"], ["S6930"] = "BUG", // ["S6931"], - // ["S6932"], + ["S6932"] = "CODE_SMELL", // ["S6933"], // ["S6934"], // ["S6935"], diff --git a/analyzers/tests/SonarAnalyzer.Test/Rules/ClassNameTest.cs b/analyzers/tests/SonarAnalyzer.Test/Rules/ClassNameTest.cs new file mode 100644 index 00000000000..50b40a4b935 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.Test/Rules/ClassNameTest.cs @@ -0,0 +1,33 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2024 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarAnalyzer.Rules.CSharp; + +namespace SonarAnalyzer.Test.Rules; + +[TestClass] +public class ClassNameTest +{ + private readonly VerifierBuilder builder = new VerifierBuilder(); + + [TestMethod] + public void ClassName_CS() => + builder.AddPaths("ClassName.cs").Verify(); +} diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/ClassName.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/ClassName.cs new file mode 100644 index 00000000000..a790d6d1f92 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/ClassName.cs @@ -0,0 +1,5 @@ +using System; + +public class Program +{ +} From 19d418f1c38e29e736a6390cecb3fb6847955923 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 11 Mar 2024 18:22:23 +0100 Subject: [PATCH 32/62] Add registrations for asp.net core controller --- .../SonarAnalyzer.CSharp/Rules/ClassName.cs | 24 ++++++++++++------- .../Helpers/AspNetMvcHelper.cs | 2 +- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/ClassName.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/ClassName.cs index 68f4927565c..ade9be63e45 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Rules/ClassName.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/ClassName.cs @@ -30,14 +30,22 @@ public sealed class ClassName : SonarDiagnosticAnalyzer public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); - protected override void Initialize(SonarAnalysisContext context) => - context.RegisterNodeAction(c => + protected override void Initialize(SonarAnalysisContext context) + { + context.RegisterCompilationStartAction(compilationStartContext => + { + if (compilationStartContext.Compilation.GetTypeByMetadataName(KnownType.Microsoft_AspNetCore_Mvc_ControllerAttribute) is { } controllerAttribute) { - var node = c.Node; - if (true) + // ASP.Net core + compilationStartContext.RegisterSymbolStartAction(symbolStartContext => { - c.ReportIssue(Diagnostic.Create(Rule, node.GetLocation())); - } - }, - SyntaxKind.InvocationExpression); + if (symbolStartContext.Symbol is INamedTypeSymbol namedType + && namedType.IsControllerType()) + { + + } + }, SymbolKind.NamedType); + } + }); + } } diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/AspNetMvcHelper.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/AspNetMvcHelper.cs index 108b5694749..47d9d55a8a3 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/AspNetMvcHelper.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/AspNetMvcHelper.cs @@ -52,7 +52,7 @@ public static bool IsControllerMethod(this IMethodSymbol methodSymbol) => /// Returns a value indicating whether the provided type symbol is a ASP.NET MVC /// controller. /// - private static bool IsControllerType(this INamedTypeSymbol containingType) => + public static bool IsControllerType(this INamedTypeSymbol containingType) => containingType != null && (containingType.DerivesFromAny(ControllerTypes) || containingType.GetAttributes(ControllerAttributeTypes).Any()) From 6204f53917bafb9d687b346ef6199a5f19227d7a Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 11 Mar 2024 19:03:40 +0100 Subject: [PATCH 33/62] Pick changes from Martin/S2077_PetaPoco2 branch --- .../ExpressionSyntaxExtensions.Roslyn.cs | 288 ++++ .../Extensions/ExpressionSyntaxExtensions.cs | 2 +- .../Extensions/SyntaxNodeExtensions.cs | 55 +- .../Facade/CSharpSyntaxFacade.cs | 3 + .../Facade/CSharpTrackerFacade.cs | 1 + .../Helpers/CSharpAttributeParameterLookup.cs | 3 + .../Helpers/CSharpMethodParameterLookup.cs | 7 +- .../Trackers/CSharpArgumentTracker.cs | 97 ++ .../Facade/ITrackerFacade.cs | 1 + .../Facade/SyntaxFacade.cs | 1 + .../Helpers/ArgumentDescriptor.cs | 161 +++ .../SonarAnalyzer.Common/Helpers/KnownType.cs | 1 + .../Helpers/MethodParameterLookupBase.cs | 11 + .../Helpers/SyntaxNodeExtensions.Roslyn.cs | 82 ++ .../Helpers/SyntaxNodeExtensions.cs | 2 +- .../Trackers/ArgumentContext.cs | 30 + .../Trackers/ArgumentTracker.cs | 85 ++ .../Trackers/InvocationTracker.cs | 12 - .../Trackers/ObjectCreationTracker.cs | 12 - .../Trackers/PropertyAccessTracker.cs | 12 - .../Trackers/SyntaxTrackerBase.cs | 12 + .../ExpressionSyntaxExtensions.Roslyn.cs | 175 +++ .../Extensions/SyntaxNodeExtensions.Roslyn.cs | 34 + .../Extensions/SyntaxNodeExtensions.cs | 2 + .../Facade/VisualBasicFacade.cs | 13 +- .../Facade/VisualBasicSyntaxFacade.cs | 3 + .../Facade/VisualBasicTrackerFacade.cs | 1 + .../VisualBasicAttributeParameterLookup.cs | 39 + .../VisualBasicMethodParameterLookup.cs | 3 + .../Trackers/VisualBasicArgumentTracker.cs | 54 + .../Trackers/ArgumentTrackerTest.cs | 1249 +++++++++++++++++ 31 files changed, 2395 insertions(+), 56 deletions(-) create mode 100644 analyzers/src/SonarAnalyzer.CSharp/Extensions/ExpressionSyntaxExtensions.Roslyn.cs create mode 100644 analyzers/src/SonarAnalyzer.CSharp/Trackers/CSharpArgumentTracker.cs create mode 100644 analyzers/src/SonarAnalyzer.Common/Helpers/ArgumentDescriptor.cs create mode 100644 analyzers/src/SonarAnalyzer.Common/Helpers/SyntaxNodeExtensions.Roslyn.cs create mode 100644 analyzers/src/SonarAnalyzer.Common/Trackers/ArgumentContext.cs create mode 100644 analyzers/src/SonarAnalyzer.Common/Trackers/ArgumentTracker.cs create mode 100644 analyzers/src/SonarAnalyzer.VisualBasic/Extensions/ExpressionSyntaxExtensions.Roslyn.cs create mode 100644 analyzers/src/SonarAnalyzer.VisualBasic/Extensions/SyntaxNodeExtensions.Roslyn.cs create mode 100644 analyzers/src/SonarAnalyzer.VisualBasic/Helpers/VisualBasicAttributeParameterLookup.cs create mode 100644 analyzers/src/SonarAnalyzer.VisualBasic/Trackers/VisualBasicArgumentTracker.cs create mode 100644 analyzers/tests/SonarAnalyzer.Test/Trackers/ArgumentTrackerTest.cs diff --git a/analyzers/src/SonarAnalyzer.CSharp/Extensions/ExpressionSyntaxExtensions.Roslyn.cs b/analyzers/src/SonarAnalyzer.CSharp/Extensions/ExpressionSyntaxExtensions.Roslyn.cs new file mode 100644 index 00000000000..871a8663981 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.CSharp/Extensions/ExpressionSyntaxExtensions.Roslyn.cs @@ -0,0 +1,288 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.CodeDom.Compiler; + +namespace SonarAnalyzer.Extensions; + +[GeneratedCode("Copied From Roslyn", "575bc42589145ba18b4f1cc2267d02695f861d8f")] +public partial class ExpressionSyntaxExtensions +{ + // Copied from + // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ExpressionSyntaxExtensions.cs#L319 + public static bool IsWrittenTo( + this ExpressionSyntax expression, + SemanticModel semanticModel, + CancellationToken cancellationToken) + { + if (expression == null) + return false; + + expression = GetExpressionToAnalyzeForWrites(expression); + + if (expression.IsOnlyWrittenTo()) + return true; + + if (expression.IsInRefContext(out var refParent)) + { + // most cases of `ref x` will count as a potential write of `x`. An important exception is: + // `ref readonly y = ref x`. In that case, because 'y' can't be written to, this would not + // be a write of 'x'. + if (refParent.Parent is EqualsValueClauseSyntax { Parent: VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Type: { } variableDeclarationType } } }) + { + if (ScopedTypeSyntaxWrapper.IsInstance(variableDeclarationType) && (ScopedTypeSyntaxWrapper)variableDeclarationType is { } scopedType) + { + variableDeclarationType = scopedType.Type; + } + + if (RefTypeSyntaxWrapper.IsInstance(variableDeclarationType) && ((RefTypeSyntaxWrapper)variableDeclarationType).ReadOnlyKeyword != default) + { + return false; + } + } + + return true; + } + + // Similar to `ref x`, `&x` allows reads and write of the value, meaning `x` may be (but is not definitely) + // written to. + if (expression.Parent.IsKind(SyntaxKind.AddressOfExpression)) + return true; + + // We're written if we're used in a ++, or -- expression. + if (expression.IsOperandOfIncrementOrDecrementExpression()) + return true; + + if (expression.IsLeftSideOfAnyAssignExpression()) + return true; + + // An extension method invocation with a ref-this parameter can write to an expression. + if (expression.Parent is MemberAccessExpressionSyntax memberAccess && + expression == memberAccess.Expression) + { + var symbol = semanticModel.GetSymbolInfo(memberAccess, cancellationToken).Symbol; + if (symbol is IMethodSymbol + { + MethodKind: MethodKind.ReducedExtension, + ReducedFrom.Parameters: { Length: > 0 } parameters, + } && parameters[0].RefKind == RefKind.Ref) + { + return true; + } + } + + return false; + } + + // Copy of + // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ExpressionSyntaxExtensions.cs#L221 + private static ExpressionSyntax GetExpressionToAnalyzeForWrites(ExpressionSyntax? expression) + { + if (expression.IsRightSideOfDotOrArrow()) + { + expression = (ExpressionSyntax)expression.Parent; + } + + expression = (ExpressionSyntax)expression.WalkUpParentheses(); + + return expression; + } + + // Copy of + // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ExpressionSyntaxExtensions.cs#L63 + public static bool IsRightSideOfDotOrArrow(this ExpressionSyntax name) + => IsAnyMemberAccessExpressionName(name) || IsRightSideOfQualifiedName(name); + + // Copy of + // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ExpressionSyntaxExtensions.cs#L41 + public static bool IsAnyMemberAccessExpressionName(this ExpressionSyntax expression) + { + if (expression == null) + return false; + + return expression == (expression.Parent as MemberAccessExpressionSyntax)?.Name || + expression.IsMemberBindingExpressionName(); + } + + // Copy of + // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ExpressionSyntaxExtensions.cs#L50 + public static bool IsMemberBindingExpressionName(this ExpressionSyntax expression) + => expression?.Parent is MemberBindingExpressionSyntax memberBinding && + memberBinding.Name == expression; + + // Copy of + // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ExpressionSyntaxExtensions.cs#L54 + public static bool IsRightSideOfQualifiedName(this ExpressionSyntax expression) + => expression?.Parent is QualifiedNameSyntax qualifiedName && qualifiedName.Right == expression; + + // Copy of + // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ExpressionSyntaxExtensions.cs#L233 + public static bool IsOnlyWrittenTo(this ExpressionSyntax expression) + { + expression = GetExpressionToAnalyzeForWrites(expression); + + if (expression != null) + { + if (expression.IsInOutContext()) + { + return true; + } + + if (expression.Parent != null) + { + if (expression.IsLeftSideOfAssignExpression()) + { + return true; + } + + if (expression.IsAttributeNamedArgumentIdentifier()) + { + return true; + } + } + + if (IsExpressionOfArgumentInDeconstruction(expression)) + { + return true; + } + } + + return false; + } + + /// + /// If this declaration or identifier is part of a deconstruction, find the deconstruction. + /// If found, returns either an assignment expression or a foreach variable statement. + /// Returns null otherwise. + /// + /// copied from SyntaxExtensions.GetContainingDeconstruction. + /// + // Copy of + // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ExpressionSyntaxExtensions.cs#L273 + private static bool IsExpressionOfArgumentInDeconstruction(ExpressionSyntax expr) + { + if (!expr.IsParentKind(SyntaxKind.Argument)) + { + return false; + } + + while (true) + { + var parent = expr.Parent; + if (parent == null) + { + return false; + } + + switch (parent.Kind()) + { + case SyntaxKind.Argument: + if (parent.Parent?.Kind() == SyntaxKindEx.TupleExpression) + { + expr = (ExpressionSyntax)parent.Parent; + continue; + } + + return false; + case SyntaxKind.SimpleAssignmentExpression: + if (((AssignmentExpressionSyntax)parent).Left == expr) + { + return true; + } + + return false; + case SyntaxKindEx.ForEachVariableStatement: + if (((ForEachVariableStatementSyntaxWrapper)parent).Variable == expr) + { + return true; + } + + return false; + + default: + return false; + } + } + } + + // Copy of + // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ExpressionSyntaxExtensions.cs#L190 + public static bool IsInOutContext(this ExpressionSyntax expression) + => expression?.Parent is ArgumentSyntax { RefOrOutKeyword: SyntaxToken { RawKind: (int)SyntaxKind.OutKeyword } } argument && + argument.Expression == expression; + + // Copy of + // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ExpressionSyntaxExtensions.cs#L383 + public static bool IsAttributeNamedArgumentIdentifier(this ExpressionSyntax expression) + { + var nameEquals = expression?.Parent as NameEqualsSyntax; + return nameEquals.IsParentKind(SyntaxKind.AttributeArgument); + } + + // Copy of + // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ExpressionSyntaxExtensions.cs#L194 + public static bool IsInRefContext(this ExpressionSyntax expression) + => IsInRefContext(expression, out _); + + /// + /// Returns true if this expression is in some ref keyword context. If then + /// will be the node containing the keyword. + /// + // Copy of + // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ExpressionSyntaxExtensions.cs#L201 + public static bool IsInRefContext(this ExpressionSyntax expression, out SyntaxNode refParent) + { + while (expression?.Parent is ParenthesizedExpressionSyntax or PostfixUnaryExpressionSyntax { RawKind: (int)SyntaxKindEx.SuppressNullableWarningExpression }) + expression = (ExpressionSyntax)expression.Parent; + + if (expression?.Parent switch + { + ArgumentSyntax { RefOrOutKeyword.RawKind: (int)SyntaxKind.RefKeyword } => true, + var x when RefExpressionSyntaxWrapper.IsInstance(x) => true, + _ => false, + }) + { + refParent = expression.Parent; + return true; + } + + refParent = null; + return false; + } + + // Copy of + // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ExpressionSyntaxExtensions.cs#L389 + public static bool IsOperandOfIncrementOrDecrementExpression(this ExpressionSyntax expression) + { + if (expression?.Parent is SyntaxNode parent) + { + switch (parent.Kind()) + { + case SyntaxKind.PostIncrementExpression: + case SyntaxKind.PreIncrementExpression: + case SyntaxKind.PostDecrementExpression: + case SyntaxKind.PreDecrementExpression: + return true; + } + } + + return false; + } +} diff --git a/analyzers/src/SonarAnalyzer.CSharp/Extensions/ExpressionSyntaxExtensions.cs b/analyzers/src/SonarAnalyzer.CSharp/Extensions/ExpressionSyntaxExtensions.cs index 2f75c0e0210..d60923bb424 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Extensions/ExpressionSyntaxExtensions.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Extensions/ExpressionSyntaxExtensions.cs @@ -20,7 +20,7 @@ namespace SonarAnalyzer.Extensions { - public static class ExpressionSyntaxExtensions + public static partial class ExpressionSyntaxExtensions { private static readonly ISet EqualsOrNotEquals = new HashSet { diff --git a/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.cs b/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.cs index 4dfe05a601d..7522d51d9c0 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.cs @@ -179,7 +179,6 @@ public static SyntaxNode WalkUpParentheses(this SyntaxNode node) DestructorDeclarationSyntax { Identifier: var identifier } => identifier, EnumMemberDeclarationSyntax { Identifier: var identifier } => identifier, EventDeclarationSyntax { Identifier: var identifier } => identifier, - IdentifierNameSyntax { Identifier: var identifier } => identifier, IndexerDeclarationSyntax { ThisKeyword: var thisKeyword } => thisKeyword, InvocationExpressionSyntax { @@ -199,6 +198,7 @@ public static SyntaxNode WalkUpParentheses(this SyntaxNode node) PointerTypeSyntax { ElementType: { } elementType } => GetIdentifier(elementType), PredefinedTypeSyntax { Keyword: var keyword } => keyword, QualifiedNameSyntax { Right.Identifier: var identifier } => identifier, + SimpleBaseTypeSyntax { Type: { } type } => GetIdentifier(type), SimpleNameSyntax { Identifier: var identifier } => identifier, TypeParameterConstraintClauseSyntax { Name.Identifier: var identifier } => identifier, TypeParameterSyntax { Identifier: var identifier } => identifier, @@ -363,23 +363,14 @@ static bool TakesExpressionTree(SymbolInfo info) } } - public static bool IsParentKind(this SyntaxNode node, SyntaxKind kind, out T result) where T : SyntaxNode - { - if (node?.Parent?.IsKind(kind) is true && node.Parent is T t) - { - result = t; - return true; - } - result = null; - return false; - } - // based on Type="ArgumentListSyntax" in https://github.com/dotnet/roslyn/blob/main/src/Compilers/CSharp/Portable/Syntax/Syntax.xml - public static ArgumentListSyntax ArgumentList(this SyntaxNode node) => + public static BaseArgumentListSyntax ArgumentList(this SyntaxNode node) => node switch { ObjectCreationExpressionSyntax creation => creation.ArgumentList, InvocationExpressionSyntax invocation => invocation.ArgumentList, + ElementAccessExpressionSyntax x => x.ArgumentList, + ElementBindingExpressionSyntax x => x.ArgumentList, ConstructorInitializerSyntax constructorInitializer => constructorInitializer.ArgumentList, null => null, _ when PrimaryConstructorBaseTypeSyntaxWrapper.IsInstance(node) => ((PrimaryConstructorBaseTypeSyntaxWrapper)node).ArgumentList, @@ -503,6 +494,44 @@ public static ConditionalAccessExpressionSyntax GetRootConditionalAccessExpressi return current; } + // Copy of + // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/SyntaxNodeExtensions.cs#L347 + public static bool IsLeftSideOfAssignExpression(this SyntaxNode node) + => node?.Parent is AssignmentExpressionSyntax { RawKind: (int)SyntaxKind.SimpleAssignmentExpression } assignment && + assignment.Left == node; + + // Copy of + // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/SyntaxNodeExtensions.cs#L43C1-L45C1 + public static bool IsParentKind(this SyntaxNode node, SyntaxKind kind) + => Microsoft.CodeAnalysis.CSharpExtensions.IsKind(node?.Parent, kind); + + // Copy of + // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/SyntaxNodeExtensions.cs#L46 + public static bool IsParentKind(this SyntaxNode node, SyntaxKind kind, out T result) where T : SyntaxNode + { + if (node?.Parent?.IsKind(kind) is true && node.Parent is T t) + { + result = t; + return true; + } + result = null; + return false; + } + + // Copy of + // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/SyntaxNodeExtensions.cs#L351 + public static bool IsLeftSideOfAnyAssignExpression(this SyntaxNode node) + { + return node?.Parent != null && + node.Parent.IsAnyAssignExpression() && + ((AssignmentExpressionSyntax)node.Parent).Left == node; + } + + // Copy of + // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/SyntaxNodeExtensions.cs#L323 + public static bool IsAnyAssignExpression(this SyntaxNode node) + => SyntaxFacts.IsAssignmentExpression(node.Kind()); + private static string GetUnknownType(SyntaxKind kind) => #if DEBUG diff --git a/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxFacade.cs b/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxFacade.cs index 25023b750ba..2a5257beb0c 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxFacade.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxFacade.cs @@ -158,4 +158,7 @@ public override bool TryGetInterpolatedTextValue(SyntaxNode node, SemanticModel public override bool TryGetOperands(SyntaxNode invocation, out SyntaxNode left, out SyntaxNode right) => Cast(invocation).TryGetOperands(out left, out right); + + public override bool IsWrittenTo(SyntaxNode expression, SemanticModel semanticModel, CancellationToken cancellationToken) => + expression is ExpressionSyntax ex && ex.IsWrittenTo(semanticModel, cancellationToken); } diff --git a/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpTrackerFacade.cs b/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpTrackerFacade.cs index 5355c39d22e..6c214fd204e 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpTrackerFacade.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpTrackerFacade.cs @@ -31,5 +31,6 @@ internal sealed class CSharpTrackerFacade : ITrackerFacade public MethodDeclarationTracker MethodDeclaration { get; } = new CSharpMethodDeclarationTracker(); public ObjectCreationTracker ObjectCreation { get; } = new CSharpObjectCreationTracker(); public PropertyAccessTracker PropertyAccess { get; } = new CSharpPropertyAccessTracker(); + public ArgumentTracker Argument => new CSharpArgumentTracker(); } } diff --git a/analyzers/src/SonarAnalyzer.CSharp/Helpers/CSharpAttributeParameterLookup.cs b/analyzers/src/SonarAnalyzer.CSharp/Helpers/CSharpAttributeParameterLookup.cs index f49481b7871..7554163392c 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Helpers/CSharpAttributeParameterLookup.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Helpers/CSharpAttributeParameterLookup.cs @@ -30,4 +30,7 @@ protected override SyntaxNode Expression(AttributeArgumentSyntax argument) => protected override SyntaxToken? GetNameColonArgumentIdentifier(AttributeArgumentSyntax argument) => argument.NameColon?.Name.Identifier; + + protected override SyntaxToken? GetNameEqualsArgumentIdentifier(AttributeArgumentSyntax argument) => + argument.NameEquals?.Name.Identifier; } diff --git a/analyzers/src/SonarAnalyzer.CSharp/Helpers/CSharpMethodParameterLookup.cs b/analyzers/src/SonarAnalyzer.CSharp/Helpers/CSharpMethodParameterLookup.cs index 7c85b5a6e13..ffefb350e07 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Helpers/CSharpMethodParameterLookup.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Helpers/CSharpMethodParameterLookup.cs @@ -28,10 +28,10 @@ public CSharpMethodParameterLookup(InvocationExpressionSyntax invocation, Semant public CSharpMethodParameterLookup(InvocationExpressionSyntax invocation, IMethodSymbol methodSymbol) : this(invocation.ArgumentList, methodSymbol) { } - public CSharpMethodParameterLookup(ArgumentListSyntax argumentList, SemanticModel semanticModel) + public CSharpMethodParameterLookup(BaseArgumentListSyntax argumentList, SemanticModel semanticModel) : base(argumentList.Arguments, semanticModel.GetSymbolInfo(argumentList.Parent)) { } - public CSharpMethodParameterLookup(ArgumentListSyntax argumentList, IMethodSymbol methodSymbol) + public CSharpMethodParameterLookup(BaseArgumentListSyntax argumentList, IMethodSymbol methodSymbol) : base(argumentList.Arguments, methodSymbol) { } protected override SyntaxNode Expression(ArgumentSyntax argument) => @@ -39,4 +39,7 @@ protected override SyntaxNode Expression(ArgumentSyntax argument) => protected override SyntaxToken? GetNameColonArgumentIdentifier(ArgumentSyntax argument) => argument.NameColon?.Name.Identifier; + + protected override SyntaxToken? GetNameEqualsArgumentIdentifier(ArgumentSyntax argument) => + null; } diff --git a/analyzers/src/SonarAnalyzer.CSharp/Trackers/CSharpArgumentTracker.cs b/analyzers/src/SonarAnalyzer.CSharp/Trackers/CSharpArgumentTracker.cs new file mode 100644 index 00000000000..48880ca3f73 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.CSharp/Trackers/CSharpArgumentTracker.cs @@ -0,0 +1,97 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarAnalyzer.Helpers.Trackers; + +internal class CSharpArgumentTracker : ArgumentTracker +{ + protected override SyntaxKind[] TrackedSyntaxKinds => new[] + { + SyntaxKind.AttributeArgument, + SyntaxKind.Argument, + }; + + protected override ILanguageFacade Language => CSharpFacade.Instance; + + protected override IReadOnlyCollection ArgumentList(SyntaxNode argumentNode) => + argumentNode switch + { + AttributeArgumentSyntax { Parent: AttributeArgumentListSyntax { Arguments: { } list } } => list, + ArgumentSyntax { Parent: BaseArgumentListSyntax { Arguments: { } list } } => list, + _ => null, + }; + + protected override int? Position(SyntaxNode argumentNode) => + argumentNode is ArgumentSyntax { NameColon: not null } or AttributeArgumentSyntax { NameColon: not null } or AttributeArgumentSyntax { NameEquals: not null } + ? null + : ArgumentList(argumentNode).IndexOf(x => x == argumentNode); + + protected override RefKind? ArgumentRefKind(SyntaxNode argumentNode) => + argumentNode switch + { + AttributeArgumentSyntax => null, + ArgumentSyntax { RefOrOutKeyword: { } refOrOut } => refOrOut.Kind() switch { SyntaxKind.OutKeyword => RefKind.Out, SyntaxKind.RefKeyword => RefKind.Ref, _ => RefKind.None }, + _ => null, + }; + + protected override bool InvocationFitsMemberKind(SyntaxNode invokedExpression, InvokedMemberKind memberKind) => + memberKind switch + { + InvokedMemberKind.Method => invokedExpression is InvocationExpressionSyntax, + InvokedMemberKind.Constructor => invokedExpression is ObjectCreationExpressionSyntax + or ConstructorInitializerSyntax + || ImplicitObjectCreationExpressionSyntaxWrapper.IsInstance(invokedExpression), + InvokedMemberKind.Indexer => invokedExpression is ElementAccessExpressionSyntax or ElementBindingExpressionSyntax, + InvokedMemberKind.Attribute => invokedExpression is AttributeSyntax, + _ => false, + }; + + protected override bool InvokedMemberFits(SemanticModel model, SyntaxNode invokedExpression, InvokedMemberKind memberKind, Func invokedMemberNameConstraint) => + memberKind switch + { + InvokedMemberKind.Method => invokedMemberNameConstraint(invokedExpression.GetName()), + InvokedMemberKind.Constructor => invokedExpression switch + { + ObjectCreationExpressionSyntax { Type: { } typeName } => invokedMemberNameConstraint(typeName.GetName()), + ConstructorInitializerSyntax x => FindClassNameFromConstructorInitializerSyntax(x) is not string name || invokedMemberNameConstraint(name), + { } ex when ImplicitObjectCreationExpressionSyntaxWrapper.IsInstance(ex) => invokedMemberNameConstraint(model.GetSymbolInfo(ex).Symbol?.ContainingType?.Name), + _ => false, + }, + InvokedMemberKind.Indexer => invokedExpression switch + { + ElementAccessExpressionSyntax { Expression: { } accessedExpression } => invokedMemberNameConstraint(accessedExpression.GetName()), + ElementBindingExpressionSyntax { } binding => binding.GetParentConditionalAccessExpression() is + { Expression: { } accessedExpression } && invokedMemberNameConstraint(accessedExpression.GetName()), + _ => false, + }, + InvokedMemberKind.Attribute => invokedExpression is AttributeSyntax { Name: { } typeName } && invokedMemberNameConstraint(typeName.GetName()), + _ => false, + }; + + private string FindClassNameFromConstructorInitializerSyntax(ConstructorInitializerSyntax initializerSyntax) => + initializerSyntax.ThisOrBaseKeyword.Kind() switch + { + SyntaxKind.ThisKeyword => initializerSyntax is { Parent: ConstructorDeclarationSyntax { Identifier.ValueText: { } typeName } } ? typeName : null, + SyntaxKind.BaseKeyword => initializerSyntax is { Parent: ConstructorDeclarationSyntax { Parent: BaseTypeDeclarationSyntax { BaseList.Types: { Count: > 0 } baseListTypes } } } + ? baseListTypes.First().GetName() // Get the class name of the called constructor from the base types list of the type declaration + : null, + _ => null, + }; +} diff --git a/analyzers/src/SonarAnalyzer.Common/Facade/ITrackerFacade.cs b/analyzers/src/SonarAnalyzer.Common/Facade/ITrackerFacade.cs index 226b7ae5ebe..e496056ce31 100644 --- a/analyzers/src/SonarAnalyzer.Common/Facade/ITrackerFacade.cs +++ b/analyzers/src/SonarAnalyzer.Common/Facade/ITrackerFacade.cs @@ -32,5 +32,6 @@ public interface ITrackerFacade MethodDeclarationTracker MethodDeclaration { get; } ObjectCreationTracker ObjectCreation { get; } PropertyAccessTracker PropertyAccess { get; } + ArgumentTracker Argument { get; } } } diff --git a/analyzers/src/SonarAnalyzer.Common/Facade/SyntaxFacade.cs b/analyzers/src/SonarAnalyzer.Common/Facade/SyntaxFacade.cs index 48efed1287d..118e1778c85 100644 --- a/analyzers/src/SonarAnalyzer.Common/Facade/SyntaxFacade.cs +++ b/analyzers/src/SonarAnalyzer.Common/Facade/SyntaxFacade.cs @@ -44,6 +44,7 @@ public abstract class SyntaxFacade public abstract bool IsAnyKind(SyntaxNode node, params TSyntaxKind[] syntaxKinds); public abstract bool IsAnyKind(SyntaxTrivia trivia, params TSyntaxKind[] syntaxKinds); public abstract bool IsInExpressionTree(SemanticModel model, SyntaxNode node); + public abstract bool IsWrittenTo(SyntaxNode expression, SemanticModel semanticModel, CancellationToken cancellationToken); public abstract bool IsKind(SyntaxNode node, TSyntaxKind kind); public abstract bool IsKind(SyntaxToken token, TSyntaxKind kind); public abstract bool IsKind(SyntaxTrivia trivia, TSyntaxKind kind); diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/ArgumentDescriptor.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/ArgumentDescriptor.cs new file mode 100644 index 00000000000..b745deac974 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/ArgumentDescriptor.cs @@ -0,0 +1,161 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarAnalyzer.Helpers; + +public enum InvokedMemberKind +{ + Method, + Constructor, + Indexer, + Attribute +} + +public class ArgumentDescriptor +{ + private ArgumentDescriptor(InvokedMemberKind memberKind, Func invokedMemberConstraint, Func invokedMemberNameConstraint, + Func, int?, bool> argumentListConstraint, Func parameterConstraint, RefKind? refKind) + { + MemberKind = memberKind; + ArgumentListConstraint = argumentListConstraint; + RefKind = refKind; + ParameterConstraint = parameterConstraint; + InvokedMemberNameConstraint = invokedMemberNameConstraint; + InvokedMemberConstraint = invokedMemberConstraint; + } + + public static ArgumentDescriptor MethodInvocation(KnownType invokedType, string methodName, string parameterName, int argumentPosition) => + MethodInvocation(invokedType, methodName, parameterName, x => x == argumentPosition); + + public static ArgumentDescriptor MethodInvocation(KnownType invokedType, string methodName, string parameterName, Func argumentPosition) => + MethodInvocation(invokedType, methodName, p => p.Name == parameterName, argumentPosition, null); + + public static ArgumentDescriptor MethodInvocation(KnownType invokedType, string methodName, string parameterName, Func argumentPosition, RefKind refKind) => + MethodInvocation(invokedType, methodName, p => p.Name == parameterName, argumentPosition, refKind); + + public static ArgumentDescriptor MethodInvocation(KnownType invokedType, string methodName, Func parameterConstraint, Func argumentPosition, RefKind? refKind) => + MethodInvocation(invokedType, (n, c) => n.Equals(methodName, c), parameterConstraint, argumentPosition, refKind); + + public static ArgumentDescriptor MethodInvocation(KnownType invokedType, Func invokedMemberNameConstraint, Func parameterConstraint, + Func argumentPosition, RefKind? refKind) => + MethodInvocation(s => invokedType.Matches(s.ContainingType), invokedMemberNameConstraint, parameterConstraint, argumentPosition, refKind); + + public static ArgumentDescriptor MethodInvocation(Func invokedMethodSymbol, Func invokedMemberNameConstraint, + Func parameterConstraint, Func argumentPosition, RefKind? refKind) => + MethodInvocation(invokedMethodSymbol, + invokedMemberNameConstraint, + parameterConstraint, + (_, position) => position is null || argumentPosition is null || argumentPosition(position.Value), + refKind); + + public static ArgumentDescriptor MethodInvocation(Func invokedMethodSymbol, Func invokedMemberNameConstraint, + Func parameterConstraint, Func, int?, bool> argumentListConstraint, RefKind? refKind) => + new(InvokedMemberKind.Method, + invokedMemberConstraint: invokedMethodSymbol, + invokedMemberNameConstraint: invokedMemberNameConstraint, + argumentListConstraint: argumentListConstraint, + parameterConstraint: parameterConstraint, + refKind: refKind); + + public static ArgumentDescriptor ConstructorInvocation(KnownType constructedType, string parameterName, int argumentPosition) => + ConstructorInvocation( + x => constructedType.Matches(x.ContainingType), + (x, c) => x.Equals(constructedType.TypeName, c), + x => x.Name == parameterName, + (_, x) => x is null || x == argumentPosition, + null); + + public static ArgumentDescriptor ConstructorInvocation(Func invokedMethodSymbol, Func invokedMemberNameConstraint, + Func parameterConstraint, Func, int?, bool> argumentListConstraint, RefKind? refKind) => + new(InvokedMemberKind.Constructor, + invokedMemberConstraint: invokedMethodSymbol, + invokedMemberNameConstraint: invokedMemberNameConstraint, + argumentListConstraint: argumentListConstraint, + parameterConstraint: parameterConstraint, + refKind: refKind); + + public static ArgumentDescriptor ElementAccess(KnownType invokedIndexerContainer, Func parameterConstraint, int argumentPosition) => + ElementAccess( + invokedIndexerContainer, + null, + parameterConstraint, + x => x == argumentPosition); + + public static ArgumentDescriptor ElementAccess(KnownType invokedIndexerContainer, string invokedIndexerExpression, Func parameterConstraint, int argumentPosition) => + ElementAccess(invokedIndexerContainer, invokedIndexerExpression, parameterConstraint, x => x == argumentPosition); + + public static ArgumentDescriptor ElementAccess(KnownType invokedIndexerContainer, + Func parameterConstraint, Func argumentPositionConstraint) => + ElementAccess( + invokedIndexerContainer, + null, + parameterConstraint, + argumentPositionConstraint); + + public static ArgumentDescriptor ElementAccess(KnownType invokedIndexerContainer, string invokedIndexerExpression, + Func parameterConstraint, Func argumentPositionConstraint) => + ElementAccess( + x => x is { ContainingSymbol: INamedTypeSymbol { } container } && invokedIndexerContainer.Matches(container), + (s, c) => invokedIndexerExpression is null || s.Equals(invokedIndexerExpression, c), + argumentListConstraint: (_, p) => argumentPositionConstraint is null || p is null || argumentPositionConstraint(p.Value), + parameterConstraint: parameterConstraint); + + public static ArgumentDescriptor ElementAccess(Func invokedIndexerPropertyMethod, Func invokedIndexerExpression, + Func parameterConstraint, Func, int?, bool> argumentListConstraint) => + new(InvokedMemberKind.Indexer, + invokedMemberConstraint: invokedIndexerPropertyMethod, + invokedMemberNameConstraint: invokedIndexerExpression, + argumentListConstraint: argumentListConstraint, + parameterConstraint: parameterConstraint, + refKind: null); + + public static ArgumentDescriptor AttributeArgument(string attributeName, string parameterName, int argumentPosition) => + AttributeArgument( + x => x is { MethodKind: MethodKind.Constructor, ContainingType.Name: { } name } && (name == attributeName || name == $"{attributeName}Attribute"), + (x, c) => AttributeClassNameConstraint(attributeName, x, c), + p => p.Name == parameterName, + (_, i) => i is null || i.Value == argumentPosition); + + public static ArgumentDescriptor AttributeArgument(Func attributeConstructorConstraint, Func attributeNameConstraint, + Func parameterConstraint, Func, int?, bool> argumentListConstraint) => + new(InvokedMemberKind.Attribute, + invokedMemberConstraint: attributeConstructorConstraint, + invokedMemberNameConstraint: attributeNameConstraint, + argumentListConstraint: argumentListConstraint, + parameterConstraint: parameterConstraint, + refKind: null); + + public static ArgumentDescriptor AttributeProperty(string attributeName, string propertyName) => + AttributeArgument( + attributeConstructorConstraint: x => x is { MethodKind: MethodKind.PropertySet, AssociatedSymbol.Name: { } name } && name == propertyName, + attributeNameConstraint: (s, c) => AttributeClassNameConstraint(attributeName, s, c), + parameterConstraint: p => true, + argumentListConstraint: (_, _) => true); + + private static bool AttributeClassNameConstraint(string expectedAttributeName, string nodeClassName, StringComparison c) => + nodeClassName.Equals(expectedAttributeName, c) || nodeClassName.Equals($"{expectedAttributeName}Attribute"); + + public InvokedMemberKind MemberKind { get; } + public Func, int?, bool> ArgumentListConstraint { get; } + public RefKind? RefKind { get; } + public Func ParameterConstraint { get; } + public Func InvokedMemberNameConstraint { get; } + public Func InvokedMemberConstraint { get; } +} diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs index aee81d18351..0562a371acd 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs @@ -214,6 +214,7 @@ public sealed partial class KnownType public static readonly KnownType System_Collections_DictionaryBase = new("System.Collections.DictionaryBase"); public static readonly KnownType System_Collections_Frozen_FrozenDictionary_TKey_TValue = new("System.Collections.Frozen.FrozenDictionary", "TKey", "TValue"); public static readonly KnownType System_Collections_Frozen_FrozenSet_T = new("System.Collections.Frozen.FrozenSet", "T"); + public static readonly KnownType System_Collections_Generic_Comparer_T = new("System.Collections.Generic.Comparer", "T"); public static readonly KnownType System_Collections_Generic_Dictionary_TKey_TValue = new("System.Collections.Generic.Dictionary", "TKey", "TValue"); public static readonly KnownType System_Collections_Generic_HashSet_T = new("System.Collections.Generic.HashSet", "T"); public static readonly KnownType System_Collections_Generic_IAsyncEnumerable_T = new("System.Collections.Generic.IAsyncEnumerable", "T"); diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/MethodParameterLookupBase.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/MethodParameterLookupBase.cs index 43249717989..b2f83dba712 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/MethodParameterLookupBase.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/MethodParameterLookupBase.cs @@ -36,6 +36,7 @@ internal abstract class MethodParameterLookupBase : IMethodPara private readonly SeparatedSyntaxList argumentList; protected abstract SyntaxToken? GetNameColonArgumentIdentifier(TArgumentSyntax argument); + protected abstract SyntaxToken? GetNameEqualsArgumentIdentifier(TArgumentSyntax argument); protected abstract SyntaxNode Expression(TArgumentSyntax argument); public IMethodSymbol MethodSymbol { get; } @@ -75,6 +76,16 @@ private bool TryGetSymbol(SyntaxNode argument, IMethodSymbol methodSymbol, out I return parameter != null; } + if (GetNameEqualsArgumentIdentifier(arg) is { } nameEqualsArgumentIdentifier + && methodSymbol.ContainingType.GetMembers(nameEqualsArgumentIdentifier.ValueText) is { Length: 1 } properties + && properties[0] is IPropertySymbol { SetMethod: { } setter } property + && property.Name == nameEqualsArgumentIdentifier.ValueText + && setter.Parameters is { Length: 1 } parameters) + { + parameter = parameters[0]; + return parameter != null; + } + var index = argumentList.IndexOf(arg); if (index >= methodSymbol.Parameters.Length) { diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/SyntaxNodeExtensions.Roslyn.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/SyntaxNodeExtensions.Roslyn.cs new file mode 100644 index 00000000000..691fccd214d --- /dev/null +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/SyntaxNodeExtensions.Roslyn.cs @@ -0,0 +1,82 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.CodeDom.Compiler; + +namespace SonarAnalyzer.Helpers; + +[GeneratedCode("Copied from Roslyn", "5a1cc5f83e4baba57f0355a685a5d1f487bfac66")] +internal static partial class SyntaxNodeExtensions +{ + /// + /// Returns true if is a given token is a child token of a certain type of parent node. + /// + /// The type of the parent node. + /// The node that we are testing. + /// A function that, when given the parent node, returns the child token we are interested in. + // Copy of + // https://github.com/dotnet/roslyn/blob/5a1cc5f83e4baba57f0355a685a5d1f487bfac66/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/SyntaxNodeExtensions.cs#L142 + public static bool IsChildNode(this SyntaxNode node, Func childGetter) where TParent : SyntaxNode + { + var ancestor = node.GetAncestor(); + if (ancestor == null) + { + return false; + } + + var ancestorNode = childGetter(ancestor); + + return node == ancestorNode; + } + + // Copy of + // https://github.com/dotnet/roslyn/blob/5a1cc5f83e4baba57f0355a685a5d1f487bfac66/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/SyntaxNodeExtensions.cs#L56 + public static TNode GetAncestor(this SyntaxNode node) where TNode : SyntaxNode + { + var current = node.Parent; + while (current != null) + { + if (current is TNode tNode) + { + return tNode; + } + + current = current.GetParent(ascendOutOfTrivia: true); + } + + return null; + } + + // Copy of + // https://github.com/dotnet/roslyn/blob/5a1cc5f83e4baba57f0355a685a5d1f487bfac66/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/SyntaxNodeExtensions.cs#L811 + public static SyntaxNode GetParent(this SyntaxNode node, bool ascendOutOfTrivia) + { + var parent = node.Parent; + if (parent == null && ascendOutOfTrivia) + { + if (node is IStructuredTriviaSyntax structuredTrivia) + { + parent = structuredTrivia.ParentTrivia.Token.Parent; + } + } + + return parent; + } +} diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/SyntaxNodeExtensions.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/SyntaxNodeExtensions.cs index 51209ee6981..057e1e00a5f 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/SyntaxNodeExtensions.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/SyntaxNodeExtensions.cs @@ -20,7 +20,7 @@ namespace SonarAnalyzer.Helpers; -internal static class SyntaxNodeExtensions +internal static partial class SyntaxNodeExtensions { public static SemanticModel EnsureCorrectSemanticModelOrDefault(this SyntaxNode node, SemanticModel semanticModel) => node.SyntaxTree.GetSemanticModelOrDefault(semanticModel); diff --git a/analyzers/src/SonarAnalyzer.Common/Trackers/ArgumentContext.cs b/analyzers/src/SonarAnalyzer.Common/Trackers/ArgumentContext.cs new file mode 100644 index 00000000000..da2e9502ba3 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.Common/Trackers/ArgumentContext.cs @@ -0,0 +1,30 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarAnalyzer.Helpers; + +public class ArgumentContext : SyntaxBaseContext +{ + public IParameterSymbol Parameter { get; internal set; } + + public ArgumentContext(SonarSyntaxNodeReportingContext context) : base(context) { } + + public ArgumentContext(SyntaxNode node, SemanticModel semanticModel) : base(node, semanticModel) { } +} diff --git a/analyzers/src/SonarAnalyzer.Common/Trackers/ArgumentTracker.cs b/analyzers/src/SonarAnalyzer.Common/Trackers/ArgumentTracker.cs new file mode 100644 index 00000000000..a2ddc7435e2 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.Common/Trackers/ArgumentTracker.cs @@ -0,0 +1,85 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarAnalyzer.Helpers.Trackers; + +public abstract class ArgumentTracker : SyntaxTrackerBase + where TSyntaxKind : struct +{ + protected abstract RefKind? ArgumentRefKind(SyntaxNode argumentNode); + protected abstract IReadOnlyCollection ArgumentList(SyntaxNode argumentNode); + protected abstract int? Position(SyntaxNode argumentNode); + protected abstract bool InvocationFitsMemberKind(SyntaxNode invokedExpression, InvokedMemberKind memberKind); + protected abstract bool InvokedMemberFits(SemanticModel model, SyntaxNode invokedExpression, InvokedMemberKind memberKind, Func invokedMemberNameConstraint); + + protected override ArgumentContext CreateContext(SonarSyntaxNodeReportingContext context) => + new(context); + + public Condition MatchArgument(ArgumentDescriptor descriptor) => + context => + { + if (context.Node is { } argumentNode + && argumentNode is { Parent.Parent: { } invoked } + && SyntacticChecks(context.SemanticModel, descriptor, argumentNode, invoked) + && MethodSymbol(context.SemanticModel, invoked) is { } methodSymbol + && Language.MethodParameterLookup(invoked, methodSymbol).TryGetSymbol(argumentNode, out var parameter) + && ParameterFits(parameter, descriptor.ParameterConstraint, descriptor.InvokedMemberConstraint)) + { + context.Parameter = parameter; + return true; + } + return false; + }; + + private IMethodSymbol MethodSymbol(SemanticModel model, SyntaxNode invoked) => + model.GetSymbolInfo(invoked).Symbol switch + { + IMethodSymbol x => x, + IPropertySymbol propertySymbol => Language.Syntax.IsWrittenTo(invoked, model, CancellationToken.None) + ? propertySymbol.SetMethod + : propertySymbol.GetMethod, + _ => null, + }; + + private bool SyntacticChecks(SemanticModel model, ArgumentDescriptor descriptor, SyntaxNode argumentNode, SyntaxNode invokedExpression) => + InvocationFitsMemberKind(invokedExpression, descriptor.MemberKind) + && (descriptor.RefKind is not { } expectedRefKind || ArgumentRefKind(argumentNode) is not { } actualRefKind || actualRefKind == expectedRefKind) + && (descriptor.ArgumentListConstraint == null + || (ArgumentList(argumentNode) is { } argList && descriptor.ArgumentListConstraint(argList, Position(argumentNode)))) + && (descriptor.InvokedMemberNameConstraint == null + || InvokedMemberFits(model, invokedExpression, descriptor.MemberKind, x => descriptor.InvokedMemberNameConstraint(x, Language.NameComparison))); + + private static bool ParameterFits(IParameterSymbol parameter, Func parameterConstraint, Func invokedMemberConstraint) + { + if (parameter.ContainingSymbol is IMethodSymbol method + && method.Parameters.IndexOf(parameter) is >= 0 and int position) + { + do + { + if (invokedMemberConstraint?.Invoke(method) is null or true && parameterConstraint?.Invoke(method.Parameters[position]) is null or true) + { + return true; + } + } + while ((method = method.OverriddenMethod) != null); + } + return false; + } +} diff --git a/analyzers/src/SonarAnalyzer.Common/Trackers/InvocationTracker.cs b/analyzers/src/SonarAnalyzer.Common/Trackers/InvocationTracker.cs index d95f82a9f3e..f59f4198967 100644 --- a/analyzers/src/SonarAnalyzer.Common/Trackers/InvocationTracker.cs +++ b/analyzers/src/SonarAnalyzer.Common/Trackers/InvocationTracker.cs @@ -51,18 +51,6 @@ public Condition MethodHasParameters(int count) => public Condition IsInvalidBuilderInitialization(BuilderPatternCondition condition) where TInvocationSyntax : SyntaxNode => condition.IsInvalidBuilderInitialization; - public Condition ExceptWhen(Condition condition) => - value => !condition(value); - - public Condition And(Condition condition1, Condition condition2) => - value => condition1(value) && condition2(value); - - public Condition Or(Condition condition1, Condition condition2) => - value => condition1(value) || condition2(value); - - public Condition Or(Condition condition1, Condition condition2, Condition condition3) => - value => condition1(value) || condition2(value) || condition3(value); - internal Condition MethodReturnTypeIs(KnownType returnType) => context => context.MethodSymbol.Value != null && context.MethodSymbol.Value.ReturnType.DerivesFrom(returnType); diff --git a/analyzers/src/SonarAnalyzer.Common/Trackers/ObjectCreationTracker.cs b/analyzers/src/SonarAnalyzer.Common/Trackers/ObjectCreationTracker.cs index acbe8a90b4e..5592eb03377 100644 --- a/analyzers/src/SonarAnalyzer.Common/Trackers/ObjectCreationTracker.cs +++ b/analyzers/src/SonarAnalyzer.Common/Trackers/ObjectCreationTracker.cs @@ -28,18 +28,6 @@ public abstract class ObjectCreationTracker : SyntaxTrackerBase - value => !condition(value); - - public Condition And(Condition condition1, Condition condition2) => - value => condition1(value) && condition2(value); - - public Condition Or(Condition condition1, Condition condition2) => - value => condition1(value) || condition2(value); - - public Condition Or(Condition condition1, Condition condition2, Condition condition3) => - value => condition1(value) || condition2(value) || condition3(value); - internal Condition ArgumentIsBoolConstant(string parameterName, bool expectedValue) => context => ConstArgumentForParameter(context, parameterName) is bool boolValue && boolValue == expectedValue; diff --git a/analyzers/src/SonarAnalyzer.Common/Trackers/PropertyAccessTracker.cs b/analyzers/src/SonarAnalyzer.Common/Trackers/PropertyAccessTracker.cs index 60c72d2c12a..a04c1af3b5c 100644 --- a/analyzers/src/SonarAnalyzer.Common/Trackers/PropertyAccessTracker.cs +++ b/analyzers/src/SonarAnalyzer.Common/Trackers/PropertyAccessTracker.cs @@ -35,18 +35,6 @@ public Condition MatchProperty(params MemberDescriptor[] properties) => public Condition MatchProperty(bool checkOverridenProperties, params MemberDescriptor[] properties) => context => MemberDescriptor.MatchesAny(context.PropertyName, context.PropertySymbol, checkOverridenProperties, Language.NameComparison, properties); - public Condition ExceptWhen(Condition condition) => - value => !condition(value); - - public Condition And(Condition condition1, Condition condition2) => - value => condition1(value) && condition2(value); - - public Condition Or(Condition condition1, Condition condition2) => - value => condition1(value) || condition2(value); - - public Condition Or(Condition condition1, Condition condition2, Condition condition3) => - value => condition1(value) || condition2(value) || condition3(value); - protected override PropertyAccessContext CreateContext(SonarSyntaxNodeReportingContext context) { // We register for both MemberAccess and IdentifierName and we want to diff --git a/analyzers/src/SonarAnalyzer.Common/Trackers/SyntaxTrackerBase.cs b/analyzers/src/SonarAnalyzer.Common/Trackers/SyntaxTrackerBase.cs index 4d64f815aab..b7f63edf513 100644 --- a/analyzers/src/SonarAnalyzer.Common/Trackers/SyntaxTrackerBase.cs +++ b/analyzers/src/SonarAnalyzer.Common/Trackers/SyntaxTrackerBase.cs @@ -59,5 +59,17 @@ void TrackAndReportIfNecessary(SonarSyntaxNodeReportingContext c) } } } + + public Condition ExceptWhen(Condition condition) => + value => !condition(value); + + public Condition And(Condition condition1, Condition condition2) => + value => condition1(value) && condition2(value); + + public Condition Or(Condition condition1, Condition condition2) => + value => condition1(value) || condition2(value); + + public Condition Or(Condition condition1, Condition condition2, Condition condition3) => + value => condition1(value) || condition2(value) || condition3(value); } } diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Extensions/ExpressionSyntaxExtensions.Roslyn.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Extensions/ExpressionSyntaxExtensions.Roslyn.cs new file mode 100644 index 00000000000..3d1668222e7 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Extensions/ExpressionSyntaxExtensions.Roslyn.cs @@ -0,0 +1,175 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.CodeDom.Compiler; + +namespace SonarAnalyzer.Extensions +{ + [GeneratedCode("Copied and converted from Roslyn", "5a1cc5f83e4baba57f0355a685a5d1f487bfac66")] + internal static class ExpressionSyntaxExtensions + { + // Copied and converted from + // https://github.com/dotnet/roslyn/blob/5a1cc5f83e4baba57f0355a685a5d1f487bfac66/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Extensions/ExpressionSyntaxExtensions.vb#L362 + public static bool IsWrittenTo(this ExpressionSyntax expression, SemanticModel semanticModel, CancellationToken cancellationToken) + { + if (expression == null) + return false; + + if (expression.IsOnlyWrittenTo()) + return true; + + if (expression.IsRightSideOfDot()) + expression = expression.Parent as ExpressionSyntax; + + if (expression != null) + { + if (expression.IsInRefContext(semanticModel, cancellationToken)) + return true; + + if (expression.Parent is AssignmentStatementSyntax) + { + var assignmentStatement = (AssignmentStatementSyntax)expression.Parent; + if (expression == assignmentStatement.Left) + return true; + } + + if (expression.IsChildNode(n => n.Name)) + return true; + + // Extension method with a 'ref' parameter can write to the value it is called on. + if (expression.Parent is MemberAccessExpressionSyntax) + { + var memberAccess = (MemberAccessExpressionSyntax)expression.Parent; + if (memberAccess.Expression == expression) + { + var method = semanticModel.GetSymbolInfo(memberAccess, cancellationToken).Symbol as IMethodSymbol; + if (method != null) + { + if (method.MethodKind == MethodKind.ReducedExtension && method.ReducedFrom.Parameters.Length > 0 && method.ReducedFrom.Parameters.First().RefKind == RefKind.Ref) + return true; + } + } + } + + return false; + } + + return false; + } + + // copied and converted from + // https://github.com/dotnet/roslyn/blob/5a1cc5f83e4baba57f0355a685a5d1f487bfac66/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Extensions/ExpressionSyntaxExtensions.vb#L325 + public static bool IsOnlyWrittenTo(this ExpressionSyntax expression) + { + if (expression.IsRightSideOfDot()) + expression = expression.Parent as ExpressionSyntax; + + if (expression != null) + { + // Sonar: IsInOutContext deleted because not relevant for VB + if (expression.IsParentKind(SyntaxKind.SimpleAssignmentStatement)) + { + var assignmentStatement = (AssignmentStatementSyntax)expression.Parent; + if (expression == assignmentStatement.Left) + return true; + } + + if (expression.IsParentKind(SyntaxKind.NameColonEquals) && expression.Parent.IsParentKind(SyntaxKind.SimpleArgument)) + + // + // this is only a write to Prop + return true; + + if (expression.IsChildNode(n => n.Name)) + return true; + + return false; + } + + return false; + } + + // Copied and converted from + // https://github.com/dotnet/roslyn/blob/5a1cc5f83e4baba57f0355a685a5d1f487bfac66/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Extensions/ExpressionSyntaxExtensions.vb#L73 + public static bool IsRightSideOfDot(this ExpressionSyntax expression) + { + return expression.IsSimpleMemberAccessExpressionName() || expression.IsRightSideOfQualifiedName(); + } + + // Copied and converted from + // https://github.com/dotnet/roslyn/blob/5a1cc5f83e4baba57f0355a685a5d1f487bfac66/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Extensions/ExpressionSyntaxExtensions.vb#L56 + public static bool IsSimpleMemberAccessExpressionName(this ExpressionSyntax expression) + { + return expression.IsParentKind(SyntaxKind.SimpleMemberAccessExpression) && ((MemberAccessExpressionSyntax)expression.Parent).Name == expression; + } + + // Copied and converted from + // https://github.com/dotnet/roslyn/blob/5a1cc5f83e4baba57f0355a685a5d1f487bfac66/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Extensions/ExpressionSyntaxExtensions.vb#L78 + public static bool IsRightSideOfQualifiedName(this ExpressionSyntax expression) + { + return expression.IsParentKind(SyntaxKind.QualifiedName) && ((QualifiedNameSyntax)expression.Parent).Right == expression; + } + + // Copied and converted from + // https://github.com/dotnet/roslyn/blob/5a1cc5f83e4baba57f0355a685a5d1f487bfac66/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Extensions/ExpressionSyntaxExtensions.vb#L277 + public static bool IsInRefContext(this ExpressionSyntax expression, SemanticModel semanticModel, CancellationToken cancellationToken) + { + var simpleArgument = expression?.Parent as SimpleArgumentSyntax; + + if (simpleArgument == null) + return false; + else if (simpleArgument.IsNamed) + { + var info = semanticModel.GetSymbolInfo(simpleArgument.NameColonEquals.Name, cancellationToken); + + var parameter = info.Symbol as IParameterSymbol; + return parameter != null && parameter.RefKind != RefKind.None; + } + else + { + var argumentList = simpleArgument.Parent as ArgumentListSyntax; + + if (argumentList != null) + { + var parent = argumentList.Parent; + var index = argumentList.Arguments.IndexOf(simpleArgument); + + var info = semanticModel.GetSymbolInfo(parent, cancellationToken); + var symbol = info.Symbol; + + if (symbol is IMethodSymbol) + { + var method = (IMethodSymbol)symbol; + if (index < method.Parameters.Length) + return method.Parameters[index].RefKind != RefKind.None; + } + else if (symbol is IPropertySymbol) + { + var prop = (IPropertySymbol)symbol; + if (index < prop.Parameters.Length) + return prop.Parameters[index].RefKind != RefKind.None; + } + } + } + + return false; + } + } +} diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Extensions/SyntaxNodeExtensions.Roslyn.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Extensions/SyntaxNodeExtensions.Roslyn.cs new file mode 100644 index 00000000000..d11a30a5a72 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Extensions/SyntaxNodeExtensions.Roslyn.cs @@ -0,0 +1,34 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.CodeDom.Compiler; + +namespace SonarAnalyzer.Extensions; + +[GeneratedCode("Copied and converted from Roslyn", "5a1cc5f83e4baba57f0355a685a5d1f487bfac66")] +internal static partial class SyntaxNodeExtensions +{ + // Copied and converted from + // https://github.com/dotnet/roslyn/blob/5a1cc5f83e4baba57f0355a685a5d1f487bfac66/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Extensions/SyntaxNodeExtensions.vb#L16 + public static bool IsParentKind(this SyntaxNode node, SyntaxKind kind) + { + return node != null && node.Parent.IsKind(kind); + } +} diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Extensions/SyntaxNodeExtensions.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Extensions/SyntaxNodeExtensions.cs index 0dc09b66547..3e555ee89ff 100644 --- a/analyzers/src/SonarAnalyzer.VisualBasic/Extensions/SyntaxNodeExtensions.cs +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Extensions/SyntaxNodeExtensions.cs @@ -89,6 +89,7 @@ static bool TakesExpressionTree(SymbolInfo info) EnumMemberDeclarationSyntax x => x.Identifier, InvocationExpressionSyntax x => x.Expression?.GetIdentifier(), ModifiedIdentifierSyntax x => x.Identifier, + ObjectCreationExpressionSyntax x=> x.Type?.GetIdentifier(), PredefinedTypeSyntax x => x.Keyword, ParameterSyntax x => x.Identifier?.GetIdentifier(), PropertyStatementSyntax x => x.Identifier, @@ -104,6 +105,7 @@ static bool TakesExpressionTree(SymbolInfo info) public static ArgumentListSyntax ArgumentList(this SyntaxNode node) => node switch { + ArgumentListSyntax argumentList => argumentList, ArrayCreationExpressionSyntax arrayCreation => arrayCreation.ArrayBounds, AttributeSyntax attribute => attribute.ArgumentList, InvocationExpressionSyntax invocation => invocation.ArgumentList, diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicFacade.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicFacade.cs index b280347b9c8..3f7bb4d33d5 100644 --- a/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicFacade.cs +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicFacade.cs @@ -51,9 +51,16 @@ public object FindConstantValue(SemanticModel model, SyntaxNode node) => node.FindConstantValue(model); public IMethodParameterLookup MethodParameterLookup(SyntaxNode invocation, IMethodSymbol methodSymbol) => - invocation?.ArgumentList() is { } argumentList - ? new VisualBasicMethodParameterLookup(argumentList, methodSymbol) - : null; + invocation switch + { + null => null, + AttributeSyntax x => new VisualBasicAttributeParameterLookup(x.ArgumentList.Arguments, methodSymbol), + IdentifierNameSyntax + { + Parent: NameColonEqualsSyntax { Parent: SimpleArgumentSyntax { IsNamed: true, Parent.Parent: AttributeSyntax attribute } } + } => new VisualBasicAttributeParameterLookup(attribute.ArgumentList.Arguments, methodSymbol), + _ => new VisualBasicMethodParameterLookup(invocation.ArgumentList(), methodSymbol), + }; public IMethodParameterLookup MethodParameterLookup(SyntaxNode invocation, SemanticModel semanticModel) => invocation?.ArgumentList() is { } argumentList diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicSyntaxFacade.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicSyntaxFacade.cs index 0cb42fd1178..d7a3764030a 100644 --- a/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicSyntaxFacade.cs +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicSyntaxFacade.cs @@ -108,6 +108,9 @@ public override bool IsMemberAccessOnKnownType(SyntaxNode memberAccess, string n public override bool IsStatic(SyntaxNode node) => Cast(node).IsShared(); + public override bool IsWrittenTo(SyntaxNode expression, SemanticModel semanticModel, CancellationToken cancellationToken) => + expression is ExpressionSyntax ex && ex.IsWrittenTo(semanticModel, cancellationToken); + public override SyntaxKind Kind(SyntaxNode node) => node.Kind(); public override string LiteralText(SyntaxNode literal) => diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicTrackerFacade.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicTrackerFacade.cs index d6912a40147..f573cc99a33 100644 --- a/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicTrackerFacade.cs +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicTrackerFacade.cs @@ -31,5 +31,6 @@ internal sealed class VisualBasicTrackerFacade : ITrackerFacade public MethodDeclarationTracker MethodDeclaration { get; } = new VisualBasicMethodDeclarationTracker(); public ObjectCreationTracker ObjectCreation { get; } = new VisualBasicObjectCreationTracker(); public PropertyAccessTracker PropertyAccess { get; } = new VisualBasicPropertyAccessTracker(); + public ArgumentTracker Argument => new VisualBasicArgumentTracker(); } } diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Helpers/VisualBasicAttributeParameterLookup.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Helpers/VisualBasicAttributeParameterLookup.cs new file mode 100644 index 00000000000..223bbbd7402 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Helpers/VisualBasicAttributeParameterLookup.cs @@ -0,0 +1,39 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarAnalyzer.Helpers; + +internal class VisualBasicAttributeParameterLookup : MethodParameterLookupBase +{ + public VisualBasicAttributeParameterLookup(SeparatedSyntaxList argumentList, IMethodSymbol methodSymbol) : base(argumentList, methodSymbol) + { + } + + protected override SyntaxNode Expression(ArgumentSyntax argument) => + argument.GetExpression(); + + protected override SyntaxToken? GetNameColonArgumentIdentifier(ArgumentSyntax argument) => + null; + + protected override SyntaxToken? GetNameEqualsArgumentIdentifier(ArgumentSyntax argument) => + argument is SimpleArgumentSyntax { NameColonEquals.Name.Identifier: var identifier } + ? identifier + : null; +} diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Helpers/VisualBasicMethodParameterLookup.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Helpers/VisualBasicMethodParameterLookup.cs index 47be470b2fe..c1acde9d473 100644 --- a/analyzers/src/SonarAnalyzer.VisualBasic/Helpers/VisualBasicMethodParameterLookup.cs +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Helpers/VisualBasicMethodParameterLookup.cs @@ -39,4 +39,7 @@ public VisualBasicMethodParameterLookup(ArgumentListSyntax argumentList, IMethod protected override SyntaxNode Expression(ArgumentSyntax argument) => argument.GetExpression(); + + protected override SyntaxToken? GetNameEqualsArgumentIdentifier(ArgumentSyntax argument) => + null; } diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Trackers/VisualBasicArgumentTracker.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Trackers/VisualBasicArgumentTracker.cs new file mode 100644 index 00000000000..7871103aebc --- /dev/null +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Trackers/VisualBasicArgumentTracker.cs @@ -0,0 +1,54 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarAnalyzer.Helpers.Trackers; + +public class VisualBasicArgumentTracker : ArgumentTracker +{ + protected override SyntaxKind[] TrackedSyntaxKinds => new[] { SyntaxKind.SimpleArgument }; + + protected override ILanguageFacade Language => VisualBasicFacade.Instance; + + protected override IReadOnlyCollection ArgumentList(SyntaxNode argumentNode) => + argumentNode is ArgumentSyntax { Parent: ArgumentListSyntax { Arguments: { } list } } + ? list + : null; + + protected override int? Position(SyntaxNode argumentNode) => + argumentNode is ArgumentSyntax { IsNamed: true } + ? null + : ArgumentList(argumentNode).IndexOf(x => x == argumentNode); + + protected override RefKind? ArgumentRefKind(SyntaxNode argumentNode) => + null; + + protected override bool InvocationFitsMemberKind(SyntaxNode invokedExpression, InvokedMemberKind memberKind) => + memberKind switch + { + InvokedMemberKind.Method => invokedExpression is InvocationExpressionSyntax, + InvokedMemberKind.Constructor => invokedExpression is ObjectCreationExpressionSyntax, + InvokedMemberKind.Indexer => invokedExpression is InvocationExpressionSyntax, + InvokedMemberKind.Attribute => invokedExpression is AttributeSyntax, + _ => false, + }; + + protected override bool InvokedMemberFits(SemanticModel model, SyntaxNode invokedExpression, InvokedMemberKind memberKind, Func invokedMemberNameConstraint) => + invokedMemberNameConstraint(invokedExpression.GetName()); +} diff --git a/analyzers/tests/SonarAnalyzer.Test/Trackers/ArgumentTrackerTest.cs b/analyzers/tests/SonarAnalyzer.Test/Trackers/ArgumentTrackerTest.cs new file mode 100644 index 00000000000..85a773d2c8d --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.Test/Trackers/ArgumentTrackerTest.cs @@ -0,0 +1,1249 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Microsoft.CodeAnalysis.Text; +using SonarAnalyzer.Helpers.Trackers; +using CS = Microsoft.CodeAnalysis.CSharp.Syntax; +using VB = Microsoft.CodeAnalysis.VisualBasic.Syntax; + +namespace SonarAnalyzer.UnitTest.Trackers; + +[TestClass] +public class ArgumentTrackerTest +{ + [TestMethod] + public void Method_SimpleArgument() + { + var snippet = """ + System.IFormatProvider provider = null; + 1.ToString($$provider); + """; + var (node, model) = ArgumentAndModelCS(WrapInMethodCS(snippet)); + + var argument = ArgumentDescriptor.MethodInvocation(KnownType.System_Int32, "ToString", "provider", 0); + var (result, context) = MatchArgumentCS(model, node, argument); + result.Should().BeTrue(); + context.Parameter.Name.Should().Be("provider"); + context.Parameter.Type.Name.Should().Be("IFormatProvider"); + } + + [TestMethod] + public void Method_SimpleArgument_VB() + { + var snippet = """ + Dim provider As System.IFormatProvider = Nothing + Dim i As Integer + i.ToString($$provider) + """; + var (node, model) = ArgumentAndModelVB(WrapInMethodVB(snippet)); + + var argument = ArgumentDescriptor.MethodInvocation(KnownType.System_Int32, "ToString", "provider", 0); + var (result, context) = MatchArgumentVB(model, node, argument); + result.Should().BeTrue(); + context.Parameter.Name.Should().Be("provider"); + context.Parameter.Type.Name.Should().Be("IFormatProvider"); + } + + [DataTestMethod] + [DataRow("""M( $$ , , 1)""", "i", true)] + [DataRow("""M( $$ , , 1)""", "j", false)] + [DataRow("""M( , $$ , 1)""", "j", true)] + [DataRow("""M( , , $$1)""", "k", true)] + [DataRow("""M( , , $$1)""", "i", false)] + public void Method_OmittedArgument_VB(string invocation, string parameterName, bool expected) + { + var snippet = $$""" + Public Class C + Public Sub M(Optional i As Integer = 0, Optional j As Integer = 0, Optional k As Integer = 0) + {{invocation}} + End Sub + End Class + """; + var (node, model) = ArgumentAndModelVB(snippet); + + var argument = ArgumentDescriptor.MethodInvocation(m => m.Name == "M", (s, c) => s.Equals("M", c), p => p.Name == parameterName, _ => true, null); + var (result, _) = MatchArgumentVB(model, node, argument); + result.Should().Be(expected); + } + + [DataTestMethod] + [DataRow("""1.ToString($$provider);""", 0, true)] + [DataRow("""1.ToString($$provider);""", 1, false)] + [DataRow("""1.ToString("", $$provider);""", 1, true)] + [DataRow("""1.ToString("", $$provider);""", 0, false)] + [DataRow("""1.ToString("", $$provider: provider);""", 1, true)] + [DataRow("""1.ToString("", $$provider: provider);""", 0, true)] + [DataRow("""1.ToString($$provider: provider, format: "");""", 1, true)] + [DataRow("""1.ToString($$provider: provider, format: "");""", 0, true)] + public void Method_Position(string invocation, int position, bool expected) + { + var snippet = $$""" + System.IFormatProvider provider = null; + {{invocation}} + """; + var (node, model) = ArgumentAndModelCS(WrapInMethodCS(snippet)); + + var argument = ArgumentDescriptor.MethodInvocation(KnownType.System_Int32, "ToString", "provider", position); + var (result, _) = MatchArgumentCS(model, node, argument); + result.Should().Be(expected); + } + + [DataTestMethod] + [DataRow("""i.ToString($$provider)""", 0, true)] + [DataRow("""i.ToString($$provider)""", 1, false)] + [DataRow("""i.ToString("", $$provider)""", 1, true)] + [DataRow("""i.ToString("", $$provider)""", 0, false)] + [DataRow("""i.ToString("", $$provider:= provider)""", 1, true)] + [DataRow("""i.ToString("", $$provider:= provider)""", 0, true)] + [DataRow("""i.ToString($$provider:= provider, format:= "")""", 1, true)] + [DataRow("""i.ToString($$provider:= provider, format:= "")""", 0, true)] + public void Method_Position_VB(string invocation, int position, bool expected) + { + var snippet = $$""" + Dim provider As System.IFormatProvider = Nothing + Dim i As Integer + {{invocation}} + """; + var (node, model) = ArgumentAndModelVB(WrapInMethodVB(snippet)); + + var argument = ArgumentDescriptor.MethodInvocation(KnownType.System_Int32, "ToString", "provider", position); + var (result, _) = MatchArgumentVB(model, node, argument); + result.Should().Be(expected); + } + + [DataTestMethod] + [DataRow("""int.TryParse("", $$out var result);""")] + [DataRow("""int.TryParse("", System.Globalization.NumberStyles.HexNumber, null, $$out var result);""")] + public void Method_RefOut_True(string invocation) + { + var snippet = $$""" + System.IFormatProvider provider = null; + {{invocation}} + """; + var (node, model) = ArgumentAndModelCS(WrapInMethodCS(snippet)); + + var argument = ArgumentDescriptor.MethodInvocation(KnownType.System_Int32, "TryParse", "result", x => true, RefKind.Out); + var (result, context) = MatchArgumentCS(model, node, argument); + result.Should().BeTrue(); + context.Parameter.RefKind.Should().Be(RefKind.Out); + } + + [DataTestMethod] + [DataRow("""Integer.TryParse("", $$result)""")] + [DataRow("""Integer.TryParse("", System.Globalization.NumberStyles.HexNumber, Nothing, $$result)""")] + public void Method_RefOut_True_VB(string invocation) + { + var snippet = $$""" + Dim provider As System.IFormatProvider = Nothing + Dim result As Integer + {{invocation}} + """; + var (node, model) = ArgumentAndModelVB(WrapInMethodVB(snippet)); + + var argument = ArgumentDescriptor.MethodInvocation(KnownType.System_Int32, "TryParse", "result", x => true, RefKind.Out); + var (result, _) = MatchArgumentVB(model, node, argument); + result.Should().BeTrue(); + } + + [DataTestMethod] + [DataRow("""int.TryParse("", $$out var result);""", RefKind.Ref)] + [DataRow("""int.TryParse($$"", out var result);""", RefKind.Out)] + [DataRow("""int.TryParse("", System.Globalization.NumberStyles.HexNumber, null, $$out var result);""", RefKind.Ref)] + [DataRow("""int.TryParse("", $$System.Globalization.NumberStyles.HexNumber, null, out var result);""", RefKind.Out)] + public void Method_RefOut_False(string invocation, RefKind refKind) + { + var snippet = $$""" + System.IFormatProvider provider = null; + {{invocation}} + """; + var (node, model) = ArgumentAndModelCS(WrapInMethodCS(snippet)); + + var argument = ArgumentDescriptor.MethodInvocation(KnownType.System_Int32, "TryParse", "result", x => true, refKind); + var (result, context) = MatchArgumentCS(model, node, argument); + result.Should().BeFalse(); + context.Parameter.Should().BeNull(); + } + + [DataTestMethod] + [DataRow("""int.TryParse("", $$out var result);""")] + [DataRow("""int.TryParse("", System.Globalization.NumberStyles.HexNumber, null, $$out var result);""")] + public void Method_RefOut_Unspecified(string invocation) + { + var snippet = $$""" + System.IFormatProvider provider = null; + {{invocation}} + """; + var (node, model) = ArgumentAndModelCS(WrapInMethodCS(snippet)); + + var argument = ArgumentDescriptor.MethodInvocation(KnownType.System_Int32, "TryParse", "result", x => true); + var (result, context) = MatchArgumentCS(model, node, argument); + result.Should().BeTrue(); + context.Parameter.RefKind.Should().Be(RefKind.Out); + } + + [DataTestMethod] + [DataRow("""new Direct().M($$1);""", true)] + [DataRow("""new DirectDifferentParameterName().M($$1);""", false)] // FN. This would require ExplicitOrImplicitInterfaceImplementations from the internal ISymbolExtensions in Roslyn. + [DataRow("""(new Explicit() as I).M($$1);""", true)] + [DataRow("""(new ExplicitDifferentParameterName() as I).M($$1);""", true)] + public void Method_Inheritance_Interface(string invocation, bool expected) + { + var snippet = $$""" + interface I + { + void M(int parameter); + } + public class Direct: I + { + public void M(int parameter) { } + } + public class DirectDifferentParameterName: I + { + public void M(int renamed) { } + } + public class Explicit: I + { + void I.M(int parameter) { } + } + public class ExplicitDifferentParameterName: I + { + void I.M(int renamed) { } + } + public class Test + { + void M() + { + {{invocation}} + } + } + """; + var (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.MethodInvocation(m => true, (m, c) => m.Equals("M", c), p => p.Name == "parameter", x => true, null); + var (result, _) = MatchArgumentCS(model, node, argument); + result.Should().Be(expected); + } + + [DataTestMethod] + [DataRow("""Dim a = New Direct().M($$1)""", true)] + [DataRow("""Dim a = New DirectDifferentParameterName().M($$1)""", false)] // FN. This would require ExplicitOrImplicitInterfaceImplementations from the internal ISymbolExtensions in Roslyn. + [DataRow(""" + Dim i As I = New Explicit() + i.M($$1) + """, true)] + [DataRow(""" + Dim i As I = New ExplicitDifferentParameterName() + i.M($$1) + """, true)] + public void Method_Inheritance_Interface_VB(string invocation, bool expected) + { + var snippet = $$""" + Interface I + Function M(ByVal parameter As Integer) As Boolean + End Interface + + Public Class Direct + Implements I + + Public Function M(parameter As Integer) As Boolean Implements I.M + End Function + End Class + + Public Class DirectDifferentParameterName + Implements I + + Public Function M(ByVal renamed As Integer) As Boolean Implements I.M + End Function + End Class + + Public Class Explicit + Implements I + + Private Function M(ByVal parameter As Integer) As Boolean Implements I.M + End Function + End Class + + Public Class ExplicitDifferentParameterName + Implements I + + Private Function M(ByVal renamed As Integer) As Boolean Implements I.M + End Function + End Class + + Public Class Test + Private Sub M() + {{invocation}} + End Sub + End Class + """; + var (node, model) = ArgumentAndModelVB(snippet); + + var argument = ArgumentDescriptor.MethodInvocation(m => true, (m, c) => m.Equals("M", c), p => p.Name == "parameter", x => true, null); + var (result, _) = MatchArgumentVB(model, node, argument); + result.Should().Be(expected); + } + + [DataTestMethod] + [DataRow("""comparer.Compare($$default, default);""")] + [DataRow("""new MyComparer().Compare($$1, 2);""")] + public void Method_Inheritance_BaseClasses_Generics(string invocation) + { + var snippet = $$""" + using System.Collections.Generic; + public class MyComparer : Comparer + { + public MyComparer() { } + public override int Compare(T a, T b) => 1; // The original definition uses x and y: int Compare(T? x, T? y) + } + public class Test + { + void M(MyComparer comparer) + { + {{invocation}} + } + } + """; + var (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.MethodInvocation(KnownType.System_Collections_Generic_Comparer_T, "Compare", "x", 0); + var (result, _) = MatchArgumentCS(model, node, argument); + result.Should().BeTrue(); + } + + [DataTestMethod] + [DataRow("""comparer.Compare($$Nothing, Nothing)""")] + [DataRow("""Call New MyComparer(Of Integer)().Compare($$1, 2)""")] + public void Method_Inheritance_BaseClasses_Generics_VB(string invocation) + { + var snippet = $$""" + Imports System.Collections.Generic + + Public Class MyComparer(Of T) + Inherits Comparer(Of T) + + Public Sub New() + End Sub + + Public Overrides Function Compare(ByVal a As T, ByVal b As T) As Integer ' The original definition uses x and y + Return 1 + End Function + End Class + + Public Class Test + Private Sub M(Of T)(ByVal comparer As MyComparer(Of T)) + {{invocation}} + End Sub + End Class + """; + var (node, model) = ArgumentAndModelVB(snippet); + + var argument = ArgumentDescriptor.MethodInvocation(KnownType.System_Collections_Generic_Comparer_T, "Compare", "x", 0); + var (result, _) = MatchArgumentVB(model, node, argument); + result.Should().BeTrue(); + } + + [DataTestMethod] + [DataRow("""OnInsert($$1, null);""")] + [DataRow("""OnInsert(position: $$1, null);""")] + public void Method_Inheritance_BaseClasses_Overrides(string invocation) + { + var snippet = $$""" + using System.Collections; + public class Collection : CollectionBase + { + protected override void OnInsert(int position, object value) { } + + void M(T arg) + { + {{invocation}} + } + } + """; + var (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.MethodInvocation(KnownType.System_Collections_CollectionBase, "OnInsert", "index", 0); + var (result, _) = MatchArgumentCS(model, node, argument); + result.Should().BeTrue(); + } + + [DataTestMethod] + [DataRow("""OnInsert($$1, Nothing)""")] + [DataRow("""OnInsert(position:= $$1, Nothing)""")] + public void Method_Inheritance_BaseClasses_Overrides_VB(string invocation) + { + var snippet = $$""" + Imports System.Collections + + Public Class Collection(Of T) + Inherits CollectionBase + + Protected Overrides Sub OnInsert(ByVal position As Integer, ByVal value As Object) + End Sub + + Private Sub M(ByVal arg As T) + {{invocation}} + End Sub + End Class + """; + var (node, model) = ArgumentAndModelVB(snippet); + + var argument = ArgumentDescriptor.MethodInvocation(KnownType.System_Collections_CollectionBase, "OnInsert", "index", 0); + var (result, _) = MatchArgumentVB(model, node, argument); + result.Should().BeTrue(); + } + + [DataTestMethod] + // learn.microsoft.com/en-us/dotnet/api/system.string.format + [DataRow("""string.Format("format", $$0)""", "arg0")] + [DataRow("""string.Format("format", 0, $$1)""", "arg1")] + [DataRow("""string.Format("format", 0, 1, $$2)""", "arg2")] + [DataRow("""string.Format("format", 0, 1, 2, $$3)""", "args")] + [DataRow("""string.Format("format", arg2: 2, arg1: 1, $$arg0:0)""", "arg0")] + [DataRow("""string.Format("format", $$new object[0])""", "args")] + public void Method_ParamsArray(string invocation, string parameterName) + { + var snippet = $$""" + _ = {{invocation}}; + """; + var (node, model) = ArgumentAndModelCS(WrapInMethodCS(snippet)); + + var argument = ArgumentDescriptor.MethodInvocation(KnownType.System_String, "Format", parameterName, i => i >= 1); + var (result, _) = MatchArgumentCS(model, node, argument); + result.Should().BeTrue(); + } + + [DataTestMethod] + // learn.microsoft.com/en-us/dotnet/api/system.string.format + [DataRow("""String.Format("format", $$0)""", "arg0")] + [DataRow("""String.Format("format", 0, $$1)""", "arg1")] + [DataRow("""String.Format("format", 0, 1, $$2)""", "arg2")] + [DataRow("""String.Format("format", 0, 1, 2, $$3)""", "args")] + [DataRow("""String.Format("format", arg2:=2, arg1:=1, $$arg0:=0)""", "arg0")] + [DataRow("""String.Format("format", $$New Object(){ })""", "args")] + public void Method_ParamsArray_VB(string invocation, string parameterName) + { + var snippet = $$""" + Dim a = {{invocation}} + """; + var (node, model) = ArgumentAndModelVB(WrapInMethodVB(snippet)); + + var argument = ArgumentDescriptor.MethodInvocation(KnownType.System_String, "Format", parameterName, i => i >= 1); + var (result, _) = MatchArgumentVB(model, node, argument); + result.Should().BeTrue(); + } + + [TestMethod] + public void Method_NamelessMethod() + { + var snippet = $$""" + using System; + class C + { + Action ActionReturning() => null; + + void M() + { + ActionReturning()($$1); + } + } + """; + var (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.MethodInvocation(KnownType.System_Action_T, methodName: string.Empty, "obj", 0); + var (result, context) = MatchArgumentCS(model, node, argument); + result.Should().BeTrue(); + context.Parameter.Name.Should().Be("obj"); + context.Parameter.ContainingSymbol.Name.Should().Be("Invoke"); + context.Parameter.ContainingType.Name.Should().Be("Action"); + } + + [DataTestMethod] + [DataRow("""ProcessStartInfo($$"fileName")""", "fileName", 0, true)] + [DataRow("""ProcessStartInfo($$"fileName")""", "arguments", 1, false)] + [DataRow("""ProcessStartInfo("fileName", $$"arguments")""", "arguments", 1, true)] + [DataRow("""ProcessStartInfo("fileName", $$"arguments")""", "arguments", 0, false)] + [DataRow("""ProcessStartInfo($$"fileName", "arguments")""", "arguments", 1, false)] + [DataRow("""ProcessStartInfo(arguments: $$"arguments", fileName: "fileName")""", "arguments", 1, true)] + [DataRow("""ProcessStartInfo(arguments: $$"arguments", fileName: "fileName")""", "fileName", 0, false)] + [DataRow("""ProcessStartInfo(arguments: "arguments", $$fileName: "fileName")""", "fileName", 0, true)] + [DataRow("""ProcessStartInfo(arguments: "arguments", $$fileName: "fileName")""", "arguments", 1, false)] + public void Constructor_SimpleArgument(string constructor, string parameterName, int argumentPosition, bool expected) + { + var snippet = $$""" + _ = new System.Diagnostics.{{constructor}}; + """; + var (node, model) = ArgumentAndModelCS(WrapInMethodCS(snippet)); + + var argument = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Diagnostics_ProcessStartInfo, parameterName, argumentPosition); + var (result, context) = MatchArgumentCS(model, node, argument); + result.Should().Be(expected); + if (expected) + { + context.Parameter.Name.Should().Be(parameterName); + context.Parameter.ContainingSymbol.Name.Should().Be(".ctor"); + context.Parameter.ContainingType.Name.Should().Be("ProcessStartInfo"); + } + } + + [DataTestMethod] + [DataRow("""ProcessStartInfo($$"fileName")""", "fileName", 0, true)] + [DataRow("""ProcessStartInfo($$"fileName")""", "arguments", 1, false)] + [DataRow("""ProcessStartInfo("fileName", $$"arguments")""", "arguments", 1, true)] + [DataRow("""ProcessStartInfo("fileName", $$"arguments")""", "arguments", 0, false)] + [DataRow("""ProcessStartInfo($$"fileName", "arguments")""", "arguments", 1, false)] + [DataRow("""ProcessStartInfo(arguments:= $$"arguments", fileName:= "fileName")""", "arguments", 1, true)] + [DataRow("""ProcessStartInfo(arguments:= $$"arguments", fileName:= "fileName")""", "fileName", 0, false)] + [DataRow("""ProcessStartInfo(arguments:= "arguments", $$fileName:= "fileName")""", "fileName", 0, true)] + [DataRow("""ProcessStartInfo(arguments:= "arguments", $$fileName:= "fileName")""", "arguments", 1, false)] + public void Constructor_SimpleArgument_VB(string constructor, string parameterName, int argumentPosition, bool expected) + { + var snippet = $$""" + Dim a = New System.Diagnostics.{{constructor}} + """; + var (node, model) = ArgumentAndModelVB(WrapInMethodVB(snippet)); + + var argument = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Diagnostics_ProcessStartInfo, parameterName, argumentPosition); + var (result, _) = MatchArgumentVB(model, node, argument); + result.Should().Be(expected); + } + + [DataTestMethod] + [DataRow("""($$"fileName")""", "fileName", 0, true)] + [DataRow("""($$"fileName")""", "arguments", 1, false)] + [DataRow("""("fileName", $$"arguments")""", "arguments", 1, true)] + [DataRow("""("fileName", $$"arguments")""", "arguments", 0, false)] + [DataRow("""($$"fileName", "arguments")""", "arguments", 1, false)] + [DataRow("""(arguments: $$"arguments", fileName: "fileName")""", "arguments", 1, true)] + [DataRow("""(arguments: $$"arguments", fileName: "fileName")""", "fileName", 0, false)] + [DataRow("""(arguments: "arguments", $$fileName: "fileName")""", "fileName", 0, true)] + [DataRow("""(arguments: "arguments", $$fileName: "fileName")""", "arguments", 1, false)] + public void Constructor_TargetTyped(string constructor, string parameterName, int argumentPosition, bool expected) + { + var snippet = $$""" + System.Diagnostics.ProcessStartInfo psi = new{{constructor}}; + """; + var (node, model) = ArgumentAndModelCS(WrapInMethodCS(snippet)); + + var argument = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Diagnostics_ProcessStartInfo, parameterName, argumentPosition); + var (result, _) = MatchArgumentCS(model, node, argument); + result.Should().Be(expected); + } + + [DataTestMethod] + [DataRow("""new Dictionary($$1)""", "capacity", 0, true)] + [DataRow("""new Dictionary($$1)""", "capacity", 0, true)] + [DataRow("""new Dictionary($$1)""", "capacity", 0, true)] + [DataRow("""new Dictionary($$1, EqualityComparer.Default)""", "capacity", 0, true)] + [DataRow("""new Dictionary(1, $$EqualityComparer.Default)""", "comparer", 1, true)] + public void Constructor_Generic(string constructor, string parameterName, int argumentPosition, bool expected) + { + var snippet = $$""" + using System.Collections.Generic; + class C + { + public void M() where TKey : notnull + { + _ = {{constructor}}; + } + } + """; + var (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Collections_Generic_Dictionary_TKey_TValue, parameterName, argumentPosition); + var (result, _) = MatchArgumentCS(model, node, argument); + result.Should().Be(expected); + } + + [DataTestMethod] + [DataRow("""Dim a = new Dictionary(Of TKey, TValue)($$1)""", "capacity", 0, true)] + [DataRow("""Dim a = new Dictionary(Of Integer, TValue)($$1)""", "capacity", 0, true)] + [DataRow("""Dim a = new Dictionary(Of Integer, String)($$1)""", "capacity", 0, true)] + [DataRow("""Dim a = New Dictionary(Of TKey, TValue)($$1, EqualityComparer(Of TKey).Default)""", "capacity", 0, true)] + [DataRow("""Dim a = new Dictionary(Of TKey, TValue)(1, $$EqualityComparer(Of TKey).Default)""", "comparer", 1, true)] + public void Constructor_Generic_VB(string constructor, string parameterName, int argumentPosition, bool expected) + { + var snippet = $$""" + Imports System.Collections.Generic + + Class C + Public Sub M(Of TKey, TValue)() + {{constructor}} + End Sub + End Class + """; + var (node, model) = ArgumentAndModelVB(snippet); + + var argument = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Collections_Generic_Dictionary_TKey_TValue, parameterName, argumentPosition); + var (result, _) = MatchArgumentVB(model, node, argument); + result.Should().Be(expected); + } + + [TestMethod] + public void Constructor_BaseCall() + { + var snippet = $$""" + using System.Collections.Generic; + class MyList: List + { + public MyList(int capacity) : base(capacity) // Unsupported + { + } + } + public class Test + { + public void M() + { + _ = new MyList($$1); // Requires tracking of the parameter to the base constructor + } + } + """; + var (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Collections_Generic_List_T, "capacity", 0); + var (result, _) = MatchArgumentCS(model, node, argument); + result.Should().BeFalse(); + } + + [TestMethod] + public void Constructor_BaseCall_VB() + { + var snippet = $$""" + Imports System.Collections.Generic + + Class MyList + Inherits List(Of Integer) + + Public Sub New(ByVal capacity As Integer) + MyBase.New(capacity) ' Passing of the parameter to the base constructor is not followed + End Sub + End Class + + Public Class Test + Public Sub M() + Dim a = New MyList($$1) ' Requires tracking of the parameter to the base constructor + End Sub + End Class + """; + var (node, model) = ArgumentAndModelVB(snippet); + + var argument = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Collections_Generic_List_T, "capacity", 0); + var (result, _) = MatchArgumentVB(model, node, argument); + result.Should().BeFalse(); + } + + [DataTestMethod] + [DataRow("""new NumberList($$1)""", "capacity", 0, false)] // FN. Syntactic checks bail out before the semantic model can resolve the alias + [DataRow("""new($$1)""", "capacity", 0, true)] // Target typed new resolves the alias + public void Constructor_TypeAlias(string constructor, string parameterName, int argumentPosition, bool expected) + { + var snippet = $$""" + using NumberList = System.Collections.Generic.List; + class C + { + public void M() + { + NumberList nl = {{constructor}}; + } + } + """; + var (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Collections_Generic_List_T, parameterName, argumentPosition); + var (result, _) = MatchArgumentCS(model, node, argument); + result.Should().Be(expected); + } + + [TestMethod] + public void Constructor_TypeAlias_VB() + { + var snippet = $$""" + Imports NumberList = System.Collections.Generic.List(Of Integer) + + Class C + Public Sub M() + Dim nl As NumberList = New NumberList($$1) + End Sub + End Class + """; + var (node, model) = ArgumentAndModelVB(snippet); + + var argument = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Collections_Generic_List_T, "capacity", 0); + var (result, _) = MatchArgumentVB(model, node, argument); + result.Should().BeFalse("FN. Syntactic check does not respect aliases."); + } + + [DataTestMethod] + [DataRow("""new($$1, 2)""", true)] + [DataRow("""new C(1, $$2)""", true)] + [DataRow("""new CAlias(1, $$2)""", true)] + [DataRow("""new C($$1)""", false)] // Count constraint fails + [DataRow("""new C(1, 2, $$3)""", false)] // Parameter name constraint fails + [DataRow("""new C($$k: 1, j:2, i:3)""", false)] // Parameter name constraint fails + public void Constructor_CustomLogic(string constructor, bool expected) + { + var snippet = $$""" + using CAlias = C; + class C + { + public C(int i) { } + public C(int j, int i) { } + public C(int j, int i, int k) { } + + public void M() + { + C c = {{constructor}}; + } + } + """; + var (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.ConstructorInvocation(invokedMethodSymbol: x => x is { MethodKind: MethodKind.Constructor, ContainingSymbol.Name: "C" }, + invokedMemberNameConstraint: (c, n) => c.Equals("C", n) || c.Equals("CAlias"), + parameterConstraint: p => p.Name is "i" or "j", + argumentListConstraint: (n, i) => i is null or 0 or 1 && n.Count > 1, + refKind: null); + var (result, _) = MatchArgumentCS(model, node, argument); + result.Should().Be(expected); + } + + [TestMethod] + public void Constructor_InitializerCalls_This() + { + var snippet = $$""" + class Base + { + public Base(int i) : this($$i, 1) { } + public Base(int i, int j) { } + } + """; + var (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.ConstructorInvocation(invokedMethodSymbol: x => x is { MethodKind: MethodKind.Constructor, ContainingSymbol.Name: "Base" }, + invokedMemberNameConstraint: (c, n) => c.Equals("Base", n), + parameterConstraint: p => p.Name is "i", + argumentListConstraint: (_, _) => true, + refKind: null); + var (result, _) = MatchArgumentCS(model, node, argument); + result.Should().BeTrue(); + } + + [TestMethod] + public void Constructor_InitializerCalls_Base() + { + var snippet = $$""" + class Base + { + public Base(int i) { } + } + class Derived: Base + { + public Derived() : base($$1) { } + } + """; + var (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.ConstructorInvocation(invokedMethodSymbol: x => x is { MethodKind: MethodKind.Constructor, ContainingSymbol.Name: "Base" }, + invokedMemberNameConstraint: (c, n) => c.Equals("Base", n), + parameterConstraint: p => p.Name is "i", + argumentListConstraint: (_, _) => true, + refKind: null); + var (result, _) = MatchArgumentCS(model, node, argument); + result.Should().BeTrue(); + } + + [TestMethod] + public void Constructor_InitializerCalls_Base_MyException() + { + var snippet = """ + using System; + + class MyException: Exception + { + public MyException(string message) : base($$message) + { } + } + """; + var (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Exception, "message", 0); + var (result, _) = MatchArgumentCS(model, node, argument); + result.Should().BeTrue(); + } + + [TestMethod] + public void Constructor_InitializerCalls_Base_MyException_VB() + { + var snippet = $$""" + Imports System + + Public Class MyException + Inherits Exception + + Public Sub New(ByVal message As String) + MyBase.New($$message) + End Sub + End Class + """; + var (node, model) = ArgumentAndModelVB(snippet); + + var argument = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Exception, "message", 0); + var (result, _) = MatchArgumentVB(model, node, argument); + result.Should().BeFalse("FN. MyBase.New and Me.New are not supported."); + } + + [TestMethod] + public void Indexer_List_Get() + { + var snippet = $$""" + var list = new System.Collections.Generic.List(); + _ = list[$$1]; + """; + var (node, model) = ArgumentAndModelCS(WrapInMethodCS(snippet)); + + var argument = ArgumentDescriptor.ElementAccess(KnownType.System_Collections_Generic_List_T, "list", + p => p is { Name: "index", Type.SpecialType: SpecialType.System_Int32, ContainingSymbol: IMethodSymbol { MethodKind: MethodKind.PropertyGet } }, 0); + var (result, context) = MatchArgumentCS(model, node, argument); + result.Should().BeTrue(); + context.Parameter.Name.Should().Be("index"); + var associatedSymbol = context.Parameter.ContainingSymbol.Should().BeAssignableTo().Which.AssociatedSymbol.Should().BeAssignableTo().Which; + associatedSymbol.IsIndexer.Should().BeTrue(); + associatedSymbol.Name.Should().Be("this[]"); + } + + [TestMethod] + public void Indexer_List_Get_VB() + { + var snippet = $$""" + Dim list = New System.Collections.Generic.List(Of Integer)() + Dim a = list($$1) + """; + var (node, model) = ArgumentAndModelVB(WrapInMethodVB(snippet)); + + var argument = ArgumentDescriptor.ElementAccess(KnownType.System_Collections_Generic_List_T, "list", + p => p is { Name: "index", Type.SpecialType: SpecialType.System_Int32, ContainingSymbol: IMethodSymbol { MethodKind: MethodKind.PropertyGet } }, 0); + var (result, context) = MatchArgumentVB(model, node, argument); + result.Should().BeTrue(); + context.Parameter.Name.Should().Be("index"); + var associatedSymbol = context.Parameter.ContainingSymbol.Should().BeAssignableTo().Which.AssociatedSymbol.Should().BeAssignableTo().Which; + associatedSymbol.IsIndexer.Should().BeTrue(); + associatedSymbol.Name.Should().Be("Item"); + } + + [DataTestMethod] + [DataRow("list[$$1] = 1;")] + [DataRow("(list[$$1], list[2]) = (1, 2);")] + [DataRow("list[$$1]++;")] + [DataRow("list[$$1]--;")] + public void Indexer_List_Set(string writeExpression) + { + var snippet = $$""" + var list = new System.Collections.Generic.List(); + {{writeExpression}}; + """; + var (node, model) = ArgumentAndModelCS(WrapInMethodCS(snippet)); + + var argument = ArgumentDescriptor.ElementAccess(KnownType.System_Collections_Generic_List_T, + p => p is { Name: "index", ContainingSymbol: IMethodSymbol { MethodKind: MethodKind.PropertySet } }, 0); + var (result, context) = MatchArgumentCS(model, node, argument); + result.Should().BeTrue(); + context.Parameter.ContainingSymbol.Should().BeAssignableTo().Which.MethodKind.Should().Be(MethodKind.PropertySet); + } + + [DataTestMethod] + [DataRow("list($$1) = 1")] + [DataRow("list($$1) += 1")] + [DataRow("list($$1) -= 1")] + public void Indexer_List_Set_VB(string writeExpression) + { + var snippet = $$""" + Dim list = New System.Collections.Generic.List(Of Integer)() + {{writeExpression}} + """; + var (node, model) = ArgumentAndModelVB(WrapInMethodVB(snippet)); + + var argument = ArgumentDescriptor.ElementAccess(KnownType.System_Collections_Generic_List_T, + p => p is { Name: "index", ContainingSymbol: IMethodSymbol { MethodKind: MethodKind.PropertySet } }, 0); + var (result, context) = MatchArgumentVB(model, node, argument); + result.Should().BeTrue(); + context.Parameter.ContainingSymbol.Should().BeAssignableTo().Which.MethodKind.Should().Be(MethodKind.PropertySet); + } + + [DataTestMethod] + [DataRow("""Environment.GetEnvironmentVariables()[$$"TEMP"]""")] + [DataRow("""Environment.GetEnvironmentVariables()?[$$"TEMP"]""")] + public void Indexer_DictionaryGet(string environmentVariableAccess) + { + var snippet = $$""" + _ = {{environmentVariableAccess}}; + """; + var (node, model) = ArgumentAndModelCS(WrapInMethodCS(snippet)); + + var argument = ArgumentDescriptor.ElementAccess(m => m is { MethodKind: MethodKind.PropertyGet, ContainingType: { } type } && type.Name == "IDictionary", + (n, c) => n.Equals("GetEnvironmentVariables", c), p => p.Name == "key", (_, p) => p is null or 0); + var (result, _) = MatchArgumentCS(model, node, argument); + result.Should().BeTrue(); + } + + [TestMethod] + public void Indexer_DictionaryGet_VB() + { + var snippet = """ + Dim a = Environment.GetEnvironmentVariables()($$"TEMP") + """; + var (node, model) = ArgumentAndModelVB(WrapInMethodVB(snippet)); + + var argument = ArgumentDescriptor.ElementAccess(m => m is { MethodKind: MethodKind.PropertyGet, ContainingType: { } type } && type.Name == "IDictionary", + (n, c) => n.Equals("GetEnvironmentVariables", c), p => p.Name == "key", (_, p) => p is null or 0); + var (result, _) = MatchArgumentVB(model, node, argument); + result.Should().BeTrue(); + } + + [DataTestMethod] + [DataRow("""_ = this[$$0,0];""", "x", true)] + [DataRow("""_ = this[0,$$0];""", "y", true)] + [DataRow("""_ = this[$$y: 0,x: 0];""", "y", true)] + [DataRow("""_ = this[y: 0,$$x: 0];""", "x", true)] + [DataRow("""this[$$0, 0] = 1;""", "x", false)] + [DataRow("""this[0, $$0] = 1;""", "y", false)] + [DataRow("""this[y: $$0, x: 0] = 1;""", "y", false)] + [DataRow("""this[y: 0, $$x: 0] = 1;""", "x", false)] + public void Indexer_MultiDimensional(string access, string parameterName, bool isGetter) + { + var snippet = $$""" + public class C { + public int this[int x, int y] + { + get => 1; + set { } + } + + public void M() { + {{access}} + } + } + """; + var (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.ElementAccess( + m => m is { MethodKind: var kind, ContainingType: { } type } && type.Name == "C" && (isGetter ? kind == MethodKind.PropertyGet : kind == MethodKind.PropertySet), + (n, c) => true, + p => p.Name == parameterName, (_, _) => true); + var (result, _) = MatchArgumentCS(model, node, argument); + result.Should().BeTrue(); + } + + [DataTestMethod] + [DataRow("""Dim a = Me($$0, 0)""", "x", true)] + [DataRow("""Dim a = Me(0, $$0)""", "y", true)] + [DataRow("""Dim a = Me(y := $$0, x := 0)""", "y", true)] + [DataRow("""Dim a = Me(y := 0, $$x := 0)""", "x", true)] + [DataRow("""Me($$0, 0) = 1""", "x", false)] + [DataRow("""Me(0, $$0) = 1""", "y", false)] + [DataRow("""Me(y := $$0, x := 0) = 1""", "y", false)] + [DataRow("""Me(y := 0, $$x := 0) = 1""", "x", false)] + public void Indexer_MultiDimensional_VB(string access, string parameterName, bool isGetter) + { + var snippet = $$""" + Public Class C + Default Public Property Item(ByVal x As Integer, ByVal y As Integer) As Integer + Get + Return 1 + End Get + Set(ByVal value As Integer) + End Set + End Property + + Public Sub M() + {{access}} + End Sub + End Class + """; + var (node, model) = ArgumentAndModelVB(snippet); + + var argument = ArgumentDescriptor.ElementAccess( + m => m is { MethodKind: var kind, ContainingType: { } type } && type.Name == "C" && (isGetter ? kind == MethodKind.PropertyGet : kind == MethodKind.PropertySet), + (n, c) => true, + p => p.Name == parameterName, (_, _) => true); + var (result, _) = MatchArgumentVB(model, node, argument); + result.Should().BeTrue(); + } + + [DataTestMethod] + [DataRow("""process.Modules[$$0]""")] + [DataRow("""process?.Modules[$$0]""")] + [DataRow("""process.Modules?[$$0]""")] + [DataRow("""process?.Modules?[$$0]""")] + [DataRow("""process.Modules[index: $$0]""")] + [DataRow("""process?.Modules?[index: $$0]""")] + public void Indexer_ModulesAccess(string modulesAccess) + { + var snippet = $$""" + public class Test + { + public void M(System.Diagnostics.Process process) + { + _ = {{modulesAccess}}; + } + } + """; + var (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.ElementAccess(m => m is { MethodKind: MethodKind.PropertyGet, ContainingType: { } type } && type.Name == "ProcessModuleCollection", + (n, c) => n.Equals("Modules", c), p => p.Name == "index", (_, p) => p is null or 0); + var (result, _) = MatchArgumentCS(model, node, argument); + result.Should().BeTrue(); + } + + [DataTestMethod] + [DataRow("""processStartInfo.Environment[$$"TEMP"]""")] + [DataRow("""processStartInfo?.Environment[$$"TEMP"]""")] + [DataRow("""processStartInfo.Environment?[$$"TEMP"]""")] + [DataRow("""processStartInfo?.Environment?[$$"TEMP"]""")] + [DataRow("""processStartInfo.Environment[key: $$"TEMP"]""")] + [DataRow("""processStartInfo?.Environment?[key: $$"TEMP"]""")] + public void Indexer_Environment(string environmentAccess) + { + var snippet = $$""" + public class Test + { + public void M(System.Diagnostics.ProcessStartInfo processStartInfo) + { + _ = {{environmentAccess}}; + } + } + """; + var (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.ElementAccess(KnownType.System_Collections_Generic_IDictionary_TKey_TValue, "Environment", p => p.Name == "key", 0); + var (result, _) = MatchArgumentCS(model, node, argument); + result.Should().BeTrue(); + } + +#if NET5_0_OR_GREATER + + [TestMethod] + public void Attribute_Obsolete() + { + var snippet = $$""" + using System; + public class Test + { + [Obsolete($$"message", UrlFormat = "")] + public void M() + { + } + } + """; + var (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.AttributeArgument(x => x is { MethodKind: MethodKind.Constructor, ContainingType.Name: "ObsoleteAttribute" }, + (s, c) => s.StartsWith("Obsolete", c), p => p.Name == "message", (_, i) => i is 0); + var (result, _) = MatchArgumentCS(model, node, argument); + result.Should().BeTrue(); + } + + [TestMethod] + public void Attribute_Obsolete_VB() + { + var snippet = $$""" + Imports System + + Public Class Test + + Public Sub M() + End Sub + End Class + """; + var (node, model) = ArgumentAndModelVB(snippet); + + var argument = ArgumentDescriptor.AttributeArgument(x => x is { MethodKind: MethodKind.Constructor, ContainingType.Name: "ObsoleteAttribute" }, + (s, c) => s.StartsWith("Obsolete", c), p => p.Name == "message", (_, i) => i is 0); + var (result, _) = MatchArgumentVB(model, node, argument); + result.Should().BeTrue(); + } + +#endif + + [DataTestMethod] + [DataRow("""[Designer($$"designerTypeName")]""", "designerTypeName", 0)] + [DataRow("""[DesignerAttribute($$"designerTypeName")]""", "designerTypeName", 0)] + [DataRow("""[DesignerAttribute($$"designerTypeName", "designerBaseTypeName")]""", "designerTypeName", 0)] + [DataRow("""[DesignerAttribute("designerTypeName", $$"designerBaseTypeName")]""", "designerBaseTypeName", 1)] + [DataRow("""[Designer($$designerBaseTypeName: "designerBaseTypeName", designerTypeName: "designerTypeName")]""", "designerBaseTypeName", 1)] + [DataRow("""[Designer(designerBaseTypeName: "designerBaseTypeName", $$designerTypeName: "designerTypeName")]""", "designerTypeName", 0)] + [DataRow("""[Designer(designerBaseTypeName: "designerBaseTypeName", $$designerTypeName: "designerTypeName")]""", "designerTypeName", 1)] + public void Attribute_Designer(string attribute, string parameterName, int argumentPosition) + { + var snippet = $$""" + using System.ComponentModel; + + {{attribute}} + public class Test + { + public void M() + { + } + } + """; + var (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.AttributeArgument("Designer", parameterName, argumentPosition); + var (result, _) = MatchArgumentCS(model, node, argument); + result.Should().BeTrue(); + } + + [DataTestMethod] + [DataRow("""""", "designerTypeName", 0)] + [DataRow("""""", "designerTypeName", 0)] + [DataRow("""""", "designerTypeName", 0)] + [DataRow("""""", "designerBaseTypeName", 1)] + public void Attribute_Designer_VB(string attribute, string parameterName, int argumentPosition) + { + var snippet = $$""" + Imports System.ComponentModel + + {{attribute}} + Public Class Test + End Class + """; + var (node, model) = ArgumentAndModelVB(snippet); + + var argument = ArgumentDescriptor.AttributeArgument("Designer", parameterName, argumentPosition); + var (result, _) = MatchArgumentVB(model, node, argument); + result.Should().BeTrue(); + } + + [DataTestMethod] + [DataRow("""[AttributeUsage(AttributeTargets.All, $$AllowMultiple = true)]""", "AllowMultiple", true)] + [DataRow("""[AttributeUsage(AttributeTargets.All, $$AllowMultiple = true, Inherited = true)]""", "AllowMultiple", true)] + [DataRow("""[AttributeUsage(AttributeTargets.All, $$AllowMultiple = true, Inherited = true)]""", "Inherited", false)] + [DataRow("""[AttributeUsage(AttributeTargets.All, AllowMultiple = true, $$Inherited = true)]""", "Inherited", true)] + public void Attribute_Property(string attribute, string propertyName, bool expected) + { + var snippet = $$""" + using System; + + {{attribute}} + public sealed class TestAttribute: Attribute + { + } + """; + var (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.AttributeProperty("AttributeUsage", propertyName); + var (result, context) = MatchArgumentCS(model, node, argument); + result.Should().Be(expected); + if (result) + { + // The mapped parameter is the "value" parameter of the property set method. + context.Parameter.Name.Should().Be("value"); + var method = context.Parameter.ContainingSymbol.Should().BeAssignableTo().Which; + method.MethodKind.Should().Be(MethodKind.PropertySet); + method.AssociatedSymbol.Should().BeAssignableTo().Which.Name.Should().Be(propertyName); + } + } + + [DataTestMethod] + [DataRow("""""", "AllowMultiple", true)] + [DataRow("""""", "AllowMultiple", true)] + [DataRow("""""", "Inherited", false)] + [DataRow("""""", "Inherited", true)] + public void Attribute_Property_VB(string attribute, string propertyName, bool expected) + { + var snippet = $$""" + Imports System + + {{attribute}} + Public NotInheritable Class TestAttribute + Inherits Attribute + End Class + """; + var (node, model) = ArgumentAndModelVB(snippet); + + var argument = ArgumentDescriptor.AttributeProperty("AttributeUsage", propertyName); + var (result, context) = MatchArgumentVB(model, node, argument); + result.Should().Be(expected); + if (result) + { + // The mapped parameter is the "value" parameter of the property set method. + context.Parameter.Name.Should().Be("value"); + var method = context.Parameter.ContainingSymbol.Should().BeAssignableTo().Which; + method.MethodKind.Should().Be(MethodKind.PropertySet); + method.AssociatedSymbol.Should().BeAssignableTo().Which.Name.Should().Be(propertyName); + } + } + + private static string WrapInMethodCS(string snippet) => + $$""" + using System; + class C + { + public void M() + { + {{snippet}} + } + } + """; + + private static string WrapInMethodVB(string snippet) => + $$""" + Imports System + Class C + Public Sub M() + {{snippet}} + End Sub + End Class + """; + + private static (SyntaxNode Node, SemanticModel Model) ArgumentAndModel(string snippet, + Func compile, params Type[] argumentNodeTypes) + { + var pos = snippet.IndexOf("$$"); + if (pos == -1) + { + throw new InvalidOperationException("The $$ maker was not found"); + } + snippet = snippet.Replace("$$", string.Empty); + var (tree, model) = compile(snippet, MetadataReferenceFacade.SystemCollections.Concat(MetadataReferenceFacade.SystemDiagnosticsProcess).ToArray()); + var node = tree.GetRoot().DescendantNodesAndSelf(new TextSpan(pos, 1)).Reverse().First(x => argumentNodeTypes.Any(t => t.IsInstanceOfType(x))); // root.Find does not work with OmittedArgument + return (node, model); + } + + private static (SyntaxNode Node, SemanticModel Model) ArgumentAndModelCS(string snippet) => + ArgumentAndModel(snippet, TestHelper.CompileCS, typeof(CS.ArgumentSyntax), typeof(CS.AttributeArgumentSyntax)); + + private static (SyntaxNode Node, SemanticModel Model) ArgumentAndModelVB(string snippet) => + ArgumentAndModel(snippet, TestHelper.CompileVB, typeof(VB.ArgumentSyntax)); + + private static (bool Match, ArgumentContext Context) MatchArgumentCS(SemanticModel model, SyntaxNode node, ArgumentDescriptor descriptor) => + MatchArgument(model, node, descriptor); + + private static (bool Match, ArgumentContext Context) MatchArgumentVB(SemanticModel model, SyntaxNode node, ArgumentDescriptor descriptor) => + MatchArgument(model, node, descriptor); + + private static (bool Match, ArgumentContext Context) MatchArgument(SemanticModel model, SyntaxNode node, ArgumentDescriptor descriptor) + where TTracker : ArgumentTracker, new() + where TSyntaxKind : struct + { + var context = new ArgumentContext(node, model); + var result = new TTracker().MatchArgument(descriptor)(context); + return (result, context); + } +} From b06707675a3ed4f6928c26986c7b015860263f3f Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 11 Mar 2024 19:10:28 +0100 Subject: [PATCH 34/62] Use tracker --- analyzers/src/SonarAnalyzer.CSharp/Rules/ClassName.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/ClassName.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/ClassName.cs index ade9be63e45..e5c02dd11f9 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Rules/ClassName.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/ClassName.cs @@ -18,6 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using SonarAnalyzer.Helpers.Trackers; + namespace SonarAnalyzer.Rules.CSharp; [DiagnosticAnalyzer(LanguageNames.CSharp)] @@ -42,7 +44,11 @@ protected override void Initialize(SonarAnalysisContext context) if (symbolStartContext.Symbol is INamedTypeSymbol namedType && namedType.IsControllerType()) { - + symbolStartContext.RegisterSyntaxNodeAction(nodeAction => + { + var tracker = new CSharpArgumentTracker(); + + }, SyntaxKind.Argument); } }, SymbolKind.NamedType); } From e3de32e773b34559de8a5f1090f887470c3b6e8a Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 11 Mar 2024 20:44:20 +0100 Subject: [PATCH 35/62] Add support for Form access --- .../SonarAnalyzer.CSharp/Rules/ClassName.cs | 57 ---- .../Rules/UseModelBinding.cs | 90 ++++++ .../SonarAnalyzer.Common/Helpers/KnownType.cs | 2 + ...lassNameTest.cs => UseModelBindingTest.cs} | 19 +- .../SonarAnalyzer.Test/TestCases/ClassName.cs | 5 - .../TestCases/UseModelBinding_AspNetCore.cs | 259 ++++++++++++++++++ .../AspNetCoreMetadataReference.cs | 1 + 7 files changed, 367 insertions(+), 66 deletions(-) delete mode 100644 analyzers/src/SonarAnalyzer.CSharp/Rules/ClassName.cs create mode 100644 analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs rename analyzers/tests/SonarAnalyzer.Test/Rules/{ClassNameTest.cs => UseModelBindingTest.cs} (54%) delete mode 100644 analyzers/tests/SonarAnalyzer.Test/TestCases/ClassName.cs create mode 100644 analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/ClassName.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/ClassName.cs deleted file mode 100644 index e5c02dd11f9..00000000000 --- a/analyzers/src/SonarAnalyzer.CSharp/Rules/ClassName.cs +++ /dev/null @@ -1,57 +0,0 @@ -/* - * SonarAnalyzer for .NET - * Copyright (C) 2015-2024 SonarSource SA - * mailto: contact AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarAnalyzer.Helpers.Trackers; - -namespace SonarAnalyzer.Rules.CSharp; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class ClassName : SonarDiagnosticAnalyzer -{ - private const string DiagnosticId = "S6932"; - private const string MessageFormat = "FIXME"; - - private static readonly DiagnosticDescriptor Rule = DescriptorFactory.Create(DiagnosticId, MessageFormat); - - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); - - protected override void Initialize(SonarAnalysisContext context) - { - context.RegisterCompilationStartAction(compilationStartContext => - { - if (compilationStartContext.Compilation.GetTypeByMetadataName(KnownType.Microsoft_AspNetCore_Mvc_ControllerAttribute) is { } controllerAttribute) - { - // ASP.Net core - compilationStartContext.RegisterSymbolStartAction(symbolStartContext => - { - if (symbolStartContext.Symbol is INamedTypeSymbol namedType - && namedType.IsControllerType()) - { - symbolStartContext.RegisterSyntaxNodeAction(nodeAction => - { - var tracker = new CSharpArgumentTracker(); - - }, SyntaxKind.Argument); - } - }, SymbolKind.NamedType); - } - }); - } -} diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs new file mode 100644 index 00000000000..207a50e13eb --- /dev/null +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs @@ -0,0 +1,90 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2024 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarAnalyzer.Helpers.Trackers; + +namespace SonarAnalyzer.Rules.CSharp; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UseModelBinding : SonarDiagnosticAnalyzer +{ + private const string DiagnosticId = "S6932"; + private const string MessageFormat = "Use model binding instead of accessing the raw request data"; + + private static readonly DiagnosticDescriptor Rule = DescriptorFactory.Create(DiagnosticId, MessageFormat); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + protected override void Initialize(SonarAnalysisContext context) + { + context.RegisterCompilationStartAction(compilationStartContext => + { + var tracker = new CSharpArgumentTracker(); + var argumentDescriptors = new List(); + if (compilationStartContext.Compilation.GetTypeByMetadataName(KnownType.Microsoft_AspNetCore_Mvc_ControllerAttribute) is { } controllerAttribute) + { + // ASP.Net core + argumentDescriptors.Add(ArgumentDescriptor.ElementAccess( + invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Http_IFormCollection, + invokedIndexerExpression: "Form", + parameterConstraint: parameter => parameter.IsType(KnownType.System_String) && parameter.ContainingSymbol is IMethodSymbol { MethodKind: MethodKind.PropertyGet }, + argumentPosition: 0)); + argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation( + invokedType: KnownType.Microsoft_AspNetCore_Http_IFormCollection, + methodName: "TryGetValue", + parameterName: "key", + argumentPosition: 0)); + argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation( + invokedType: KnownType.Microsoft_AspNetCore_Http_IFormCollection, + methodName: "ContainsKey", + parameterName: "key", + argumentPosition: 0)); + } + if (argumentDescriptors.Any()) + { + compilationStartContext.RegisterSymbolStartAction(symbolStartContext => + { + if (symbolStartContext.Symbol is INamedTypeSymbol namedType + && namedType.IsControllerType()) + { + symbolStartContext.RegisterSyntaxNodeAction(nodeContext => + { + var argument = (ArgumentSyntax)nodeContext.Node; + var context = new ArgumentContext(argument, nodeContext.SemanticModel); + if (argumentDescriptors.Any(x => tracker.MatchArgument(x)(context)) + && nodeContext.SemanticModel.GetConstantValue(argument.Expression) is { HasValue: true, Value: string }) + { + nodeContext.ReportIssue(Diagnostic.Create(Rule, GetPrimaryLocation(argument))); + } + }, SyntaxKind.Argument); + } + }, SymbolKind.NamedType); + } + }); + } + + private Location GetPrimaryLocation(ArgumentSyntax argument) => + argument switch + { + { Parent: BracketedArgumentListSyntax { Parent: ElementAccessExpressionSyntax { Expression: { } expression } } } => expression.GetLocation(), + { Parent: ArgumentListSyntax { Parent: InvocationExpressionSyntax { Expression: { } expression } } } => expression.GetLocation(), + _ => argument.GetLocation(), + }; +} diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs index 0562a371acd..0c897b69d19 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs @@ -55,6 +55,8 @@ public sealed partial class KnownType public static readonly KnownType Microsoft_AspNet_SignalR_Hub = new("Microsoft.AspNet.SignalR.Hub"); public static readonly KnownType Microsoft_AspNetCore_Builder_DeveloperExceptionPageExtensions = new("Microsoft.AspNetCore.Builder.DeveloperExceptionPageExtensions"); public static readonly KnownType Microsoft_AspNetCore_Builder_DatabaseErrorPageExtensions = new("Microsoft.AspNetCore.Builder.DatabaseErrorPageExtensions"); + public static readonly KnownType Microsoft_AspNetCore_Http_HttpRequest = new("Microsoft.AspNetCore.Http.HttpRequest"); + public static readonly KnownType Microsoft_AspNetCore_Http_IFormCollection = new("Microsoft.AspNetCore.Http.IFormCollection"); public static readonly KnownType Microsoft_AspNetCore_Components_ParameterAttribute = new("Microsoft.AspNetCore.Components.ParameterAttribute"); public static readonly KnownType Microsoft_AspNetCore_Components_Rendering_RenderTreeBuilder = new("Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder"); public static readonly KnownType Microsoft_AspNetCore_Components_RouteAttribute = new("Microsoft.AspNetCore.Components.RouteAttribute"); diff --git a/analyzers/tests/SonarAnalyzer.Test/Rules/ClassNameTest.cs b/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs similarity index 54% rename from analyzers/tests/SonarAnalyzer.Test/Rules/ClassNameTest.cs rename to analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs index 50b40a4b935..eecef364ca4 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Rules/ClassNameTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs @@ -23,11 +23,22 @@ namespace SonarAnalyzer.Test.Rules; [TestClass] -public class ClassNameTest +public class UseModelBindingTest { - private readonly VerifierBuilder builder = new VerifierBuilder(); +#if NET + private readonly VerifierBuilder builderAspNetCore = new VerifierBuilder() + .WithOptions(ParseOptionsHelper.FromCSharp12) + .AddReferences([ + AspNetCoreMetadataReference.MicrosoftAspNetCoreMvcCore, + AspNetCoreMetadataReference.MicrosoftAspNetCoreHttpAbstractions, + AspNetCoreMetadataReference.MicrosoftAspNetCoreMvcViewFeatures, + AspNetCoreMetadataReference.MicrosoftAspNetCoreMvcAbstractions, + AspNetCoreMetadataReference.MicrosoftAspNetCoreHttpFeatures, + AspNetCoreMetadataReference.MicrosoftExtensionsPrimitives, + ]); [TestMethod] - public void ClassName_CS() => - builder.AddPaths("ClassName.cs").Verify(); + public void UseModelBinding_AspNetCore_CS() => + builderAspNetCore.AddPaths("UseModelBinding_AspNetCore.cs").Verify(); +#endif } diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/ClassName.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/ClassName.cs deleted file mode 100644 index a790d6d1f92..00000000000 --- a/analyzers/tests/SonarAnalyzer.Test/TestCases/ClassName.cs +++ /dev/null @@ -1,5 +0,0 @@ -using System; - -public class Program -{ -} diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs new file mode 100644 index 00000000000..7d852e25a01 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs @@ -0,0 +1,259 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using System; +using System.Linq; +using System.Threading.Tasks; + +public class TestController : Controller +{ + private readonly string Key = "id"; + + public IActionResult Post() + { + _ = Request.Form["id"]; // Noncompliant {{Use model binding instead of accessing the raw request data}} + // ^^^^^^^^^^^^ + _ = Request.Form.TryGetValue("id", out _); // Noncompliant {{Use model binding instead of accessing the raw request data}} + // ^^^^^^^^^^^^^^^^^^^^^^^^ + _ = Request.Form.ContainsKey("id"); // Noncompliant {{Use model binding instead of accessing the raw request data}} + // ^^^^ + _ = Request.Headers["id"]; // Noncompliant {{Use model binding instead of accessing the raw request data}} + // ^^^^^^^ + _ = Request.Headers.TryGetValue("id", out _); // Noncompliant {{Use model binding instead of accessing the raw request data}} + // ^^^^^^^ + _ = Request.Headers.ContainsKey("id"); // Noncompliant {{Use model binding instead of accessing the raw request data}} + // ^^^^^^^ + _ = Request.Query["id"]; // Noncompliant {{Use model binding instead of accessing the raw request data}} + // ^^^^^ + _ = Request.Query.TryGetValue("id", out _); // Noncompliant {{Use model binding instead of accessing the raw request data}} + // ^^^^^ + _ = Request.RouteValues["id"]; // Noncompliant {{Use model binding instead of accessing the raw request data}} + // ^^^^^^^^^^^ + _ = Request.RouteValues.TryGetValue("id", out _); // Noncompliant {{Use model binding instead of accessing the raw request data}} + // ^^^^^^^^^^^ + _ = Request.Form.Files; // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}} + // ^^^^^ + _ = Request.Form.Files["file"]; // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}} + // ^^^^^ + _ = Request.Form.Files[0]; // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}} + // ^^^^^ + _ = Request.Form.Files.Any(); // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}} + // ^^^^^ + _ = Request.Form.Files.Count; // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}} + // ^^^^^ + _ = Request.Form.Files.GetFile("file"); // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}} + // ^^^^^ + _ = Request.Form.Files.GetFiles("file"); // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}} + // ^^^^^ + return default; + } + + // Parameterized for "Form", "Query", "RouteValues", "Headers" + void NoncompliantKeyVariations() + { + _ = Request.Form[@"key"]; // Noncompliant + _ = Request.Form.TryGetValue(@"key", out _); // Noncompliant + _ = Request.Form["""key"""]; // Noncompliant + _ = Request.Form.TryGetValue("""key""", out _); // Noncompliant + + _ = Request.Form[Key]; // FN: Key is a readonly field with a constant initializer (Requires cross procedure SE) + const string key = "id"; + _ = Request.Form[key]; // Noncompliant + _ = Request.Form.TryGetValue(key, out _); // Noncompliant + _ = Request.Form[$"prefix.{key}"]; // Noncompliant + _ = Request.Form.TryGetValue($"prefix.{key}", out _); // Noncompliant + _ = Request.Form[$"""prefix.{key}"""]; // Noncompliant + _ = Request.Form.TryGetValue($"""prefix.{key}""", out _); // Noncompliant + string localKey = "id"; + _ = Request.Form[localKey]; // FN (Requires SE) + + _ = Request.Form[key: "id"]; // Noncompliant + _ = Request.Form.TryGetValue(value: out _, key: "id"); // Noncompliant + } + + // Parameterized for Form, Headers, Query, RouteValues / Request, this.Request, ControllerContext.HttpContext.Request / [FromForm], [FromQuery], [FromRoute], [FromHeader] + // Implementation: Consider adding a CombinatorialDataAttribute https://stackoverflow.com/a/75531690 + async Task Compliant([FromForm] string key) + { + _ = Request.Form.Keys; + _ = Request.Form.Count; + foreach (var kvp in Request.Form) + { } + _ = Request.Form.Select(x => x); + _ = Request.Form[key]; // Compliant: The accessed key is not a compile time constant + _ = Request.Cookies["cookie"]; // Compliant: Cookies are not bound by default + _ = Request.QueryString; // Compliant: Accessing the whole raw string is fine. + _ = await Request.ReadFormAsync(); // Compliant: This might be used for optimization purposes e.g. conditional form value access. + } + + // parameterized test: parameters are the different forbidden Request accesses (see above) + private static void HandleRequest(HttpRequest request) + { + _ = request.Form["id"]; // Noncompliant: Containing type is a controller + void LocalFunction() + { + _ = request.Form["id"]; // Noncompliant: Containing type is a controller + } + static void StaticLocalFunction(HttpRequest request) + { + _ = request.Form["id"]; // Noncompliant: Containing type is a controller + } + } +} + +public class CodeBlocksController : Controller +{ + public CodeBlocksController() + { + _ = Request.Form["id"]; // Noncompliant + } + + public CodeBlocksController(object o) => _ = Request.Form["id"]; // Noncompliant + + HttpRequest ValidRequest => Request; + IFormCollection Form => Request.Form; + + string P1 => Request.Form["id"]; // Noncompliant + string P2 + { + get => Request.Form["id"]; // Noncompliant + } + string P3 + { + get + { + return Request.Form["id"]; // Noncompliant + } + } + void M1() => _ = Request.Form["id"]; // Noncompliant + void M2() + { + Func f1 = () => Request.Form["id"]; // Noncompliant + Func f2 = x => Request.Form["id"]; // Noncompliant + Func f3 = delegate (object x) { return Request.Form["id"]; }; // Noncompliant + } + void M3() + { + _ = (true ? Request : Request).Form["id"]; // Noncompliant + _ = ValidatedRequest().Form["id"]; // Noncompliant + _ = ValidRequest.Form["id"]; + _ = Form["id"]; // FN: requires cross method SE + _ = this.Form["id"]; // FN: requires cross method SE + _ = new CodeBlocksController().Form["id"]; // Compliant + + HttpRequest ValidatedRequest() => Request; + } + + void M4() + { + _ = this.Request.Form["id"]; // Noncompliant + _ = Request?.Form?["id"]; // Noncompliant + _ = Request?.Form?.TryGetValue("id", out _); // Noncompliant + _ = Request.Form?.TryGetValue("id", out _); // Noncompliant + _ = Request.Form?.TryGetValue("id", out _).ToString(); // Noncompliant + _ = HttpContext.Request.Form["id"]; // Noncompliant + _ = Request.HttpContext.Request.Form["id"]; // Noncompliant + _ = this.ControllerContext.HttpContext.Request.Form["id"]; // Noncompliant + var r1 = HttpContext.Request; + _ = r1.Form["id"]; // Noncompliant + var r2 = ControllerContext; + _ = r2.HttpContext.Request.Form["id"]; // Noncompliant + } + ~CodeBlocksController() => _ = Request.Form["id"]; // Noncompliant +} + + +// parameterized test: Repeat for Controller, ControllerBase, MyBaseController, MyBaseBaseController base classes +// consider adding "PageModel" to the parametrized test but functional tests and updates to the RSpec are needed. +public class MyBaseController : ControllerBase { } +public class MyBaseBaseController : MyBaseController { } +public class MyTestController : MyBaseBaseController +{ + public void Action() + { + _ = Request.Form["id"]; // Noncompliant + } +} + +public class OverridesController : Controller +{ + public void Action() + { + _ = Request.Form["id"]; // Noncompliant + } + private void Undecidable(HttpContext context) + { + // Implementation: It might be difficult to distinguish between access to "Request" that originate from overrides vs. "Request" access that originate from action methods. + // This is especially true for "Request" which originate from parameters like here. We may need to redeclare such cases as FNs (see e.g HandleRequest above). + _ = context.Request.Form["id"]; // Undecidable: request may originate from an action method (which supports binding), or from one of the following overrides (which don't). + } + private void Undecidable(HttpRequest request) + { + _ = request.Form["id"]; // Undecidable: request may originate from an action method (which supports binding), or from one of the following overloads (which don't). + } + public override void OnActionExecuted(ActionExecutedContext context) + { + _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here + } + public override void OnActionExecuting(ActionExecutingContext context) + { + _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here + } + public override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here + return base.OnActionExecutionAsync(context, next); + } +} + +// parameterized test for PocoController, [Controller]Poco +// consider adding "PageModel" to the parametrized test but functional tests and updates to the RSpec are needed. +public class PocoController : IActionFilter, IAsyncActionFilter +{ + public void OnActionExecuted(ActionExecutedContext context) + { + _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here + } + void IActionFilter.OnActionExecuted(Microsoft.AspNetCore.Mvc.Filters.ActionExecutedContext context) + { + _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here + } + public void OnActionExecuting(ActionExecutingContext context) + { + _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here + } + void IActionFilter.OnActionExecuting(ActionExecutingContext context) + { + _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here + } + public Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here + return Task.CompletedTask; + } + Task IAsyncActionFilter.OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here + return Task.CompletedTask; + } +} + +static class HttpRequestExtensions +{ + // parameterized test: parameters are the different forbidden Request accesses (see above) + public static void Ext(this HttpRequest request) + { + _ = request.Form["id"]; // Compliant: Not in a controller + } +} + +class RequestService +{ + public HttpRequest Request { get; } + // parameterized test: parameters are the different forbidden Request accesses (see above) + public void HandleRequest(HttpRequest request) + { + _ = Request.Form["id"]; // Compliant: Not in a controller + _ = request.Form["id"]; // Compliant: Not in a controller + } +} diff --git a/analyzers/tests/SonarAnalyzer.TestFramework/MetadataReferences/AspNetCoreMetadataReference.cs b/analyzers/tests/SonarAnalyzer.TestFramework/MetadataReferences/AspNetCoreMetadataReference.cs index cbe770ec542..1e09a226b4d 100644 --- a/analyzers/tests/SonarAnalyzer.TestFramework/MetadataReferences/AspNetCoreMetadataReference.cs +++ b/analyzers/tests/SonarAnalyzer.TestFramework/MetadataReferences/AspNetCoreMetadataReference.cs @@ -42,5 +42,6 @@ public static class AspNetCoreMetadataReference public static MetadataReference MicrosoftAspNetCoreRouting { get; } = Create(typeof(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder)); public static MetadataReference MicrosoftAspNetCoreWebHost { get; } = Create(typeof(Microsoft.AspNetCore.WebHost)); public static MetadataReference MicrosoftExtensionsHostingAbstractions { get; } = Create(typeof(Microsoft.Extensions.Hosting.IHost)); + public static MetadataReference MicrosoftExtensionsPrimitives { get; } = Create(typeof(Microsoft.Extensions.Primitives.CancellationChangeToken)); } #endif From af82bbc82115fab53dff32fbe08ea6979c73aa07 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 11 Mar 2024 22:00:43 +0100 Subject: [PATCH 36/62] Comments --- .../src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs index 207a50e13eb..0a0d7b83899 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs @@ -41,22 +41,23 @@ protected override void Initialize(SonarAnalysisContext context) if (compilationStartContext.Compilation.GetTypeByMetadataName(KnownType.Microsoft_AspNetCore_Mvc_ControllerAttribute) is { } controllerAttribute) { // ASP.Net core - argumentDescriptors.Add(ArgumentDescriptor.ElementAccess( + argumentDescriptors.Add(ArgumentDescriptor.ElementAccess(// Request.Form["id"] invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Http_IFormCollection, invokedIndexerExpression: "Form", parameterConstraint: parameter => parameter.IsType(KnownType.System_String) && parameter.ContainingSymbol is IMethodSymbol { MethodKind: MethodKind.PropertyGet }, argumentPosition: 0)); - argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation( + argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation(// Request.Form.TryGetValue("id", out _) invokedType: KnownType.Microsoft_AspNetCore_Http_IFormCollection, methodName: "TryGetValue", parameterName: "key", argumentPosition: 0)); - argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation( + argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation(// Request.Form.ContainsKey("id") invokedType: KnownType.Microsoft_AspNetCore_Http_IFormCollection, methodName: "ContainsKey", parameterName: "key", argumentPosition: 0)); } + // TODO: Add descriptors for Asp.Net MVC 4.x if (argumentDescriptors.Any()) { compilationStartContext.RegisterSymbolStartAction(symbolStartContext => From 942252f68c5a3b6e6119544d5c529215f2336e4f Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Tue, 12 Mar 2024 11:34:50 +0100 Subject: [PATCH 37/62] Support all NonCompliant cases --- .../Rules/UseModelBinding.cs | 96 ++++++++++++++++--- .../SonarAnalyzer.Common/Helpers/KnownType.cs | 6 +- .../Trackers/PropertyAccessContext.cs | 6 ++ .../Rules/UseModelBindingTest.cs | 24 +++++ .../TestCases/UseModelBinding_AspNetCore.cs | 43 +++++---- 5 files changed, 140 insertions(+), 35 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs index 0a0d7b83899..a4271250c5c 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs @@ -26,9 +26,10 @@ namespace SonarAnalyzer.Rules.CSharp; public sealed class UseModelBinding : SonarDiagnosticAnalyzer { private const string DiagnosticId = "S6932"; - private const string MessageFormat = "Use model binding instead of accessing the raw request data"; + private const string UseModelBindingMessage = "Use model binding instead of accessing the raw request data"; + private const string UseIFormFileBindingMessage = "Use IFormFile or IFormFileCollection binding instead"; - private static readonly DiagnosticDescriptor Rule = DescriptorFactory.Create(DiagnosticId, MessageFormat); + private static readonly DiagnosticDescriptor Rule = DescriptorFactory.Create(DiagnosticId, "{0}"); public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); @@ -36,7 +37,8 @@ protected override void Initialize(SonarAnalysisContext context) { context.RegisterCompilationStartAction(compilationStartContext => { - var tracker = new CSharpArgumentTracker(); + var argumentTracker = new CSharpArgumentTracker(); + var propertyTracker = new CSharpPropertyAccessTracker(); var argumentDescriptors = new List(); if (compilationStartContext.Compilation.GetTypeByMetadataName(KnownType.Microsoft_AspNetCore_Mvc_ControllerAttribute) is { } controllerAttribute) { @@ -44,7 +46,7 @@ protected override void Initialize(SonarAnalysisContext context) argumentDescriptors.Add(ArgumentDescriptor.ElementAccess(// Request.Form["id"] invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Http_IFormCollection, invokedIndexerExpression: "Form", - parameterConstraint: parameter => parameter.IsType(KnownType.System_String) && parameter.ContainingSymbol is IMethodSymbol { MethodKind: MethodKind.PropertyGet }, + parameterConstraint: parameter => parameter.IsType(KnownType.System_String) && IsGetterParameter(parameter), argumentPosition: 0)); argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation(// Request.Form.TryGetValue("id", out _) invokedType: KnownType.Microsoft_AspNetCore_Http_IFormCollection, @@ -56,36 +58,102 @@ protected override void Initialize(SonarAnalysisContext context) methodName: "ContainsKey", parameterName: "key", argumentPosition: 0)); + argumentDescriptors.Add(ArgumentDescriptor.ElementAccess(// Request.Headers["id"] + invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Http_IHeaderDictionary, + invokedIndexerExpression: "Headers", + parameterConstraint: parameter => parameter.IsType(KnownType.System_String) && IsGetterParameter(parameter), + argumentPosition: 0)); + argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation(// Request.Headers.TryGetValue("id", out _) + invokedMethodSymbol: x => IsIDictionaryStringStringValuesInvocation(x, "TryGetValue"), + invokedMemberNameConstraint: (name, comparison) => string.Equals(name, "TryGetValue", comparison), + parameterConstraint: x => string.Equals(x.Name, "key", StringComparison.Ordinal), + argumentPosition: x => x == 0, + refKind: null)); + argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation(// Request.Headers.ContainsKey("id") + invokedMethodSymbol: x => IsIDictionaryStringStringValuesInvocation(x, "ContainsKey"), + invokedMemberNameConstraint: (name, comparison) => string.Equals(name, "ContainsKey", comparison), + parameterConstraint: x => string.Equals(x.Name, "key", StringComparison.Ordinal), + argumentPosition: x => x == 0, + refKind: null)); + argumentDescriptors.Add(ArgumentDescriptor.ElementAccess(// Request.Query["id"] + invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Http_IQueryCollection, + invokedIndexerExpression: "Query", + parameterConstraint: parameter => parameter.IsType(KnownType.System_String) && IsGetterParameter(parameter), + argumentPosition: 0)); + argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation(// Request.Query.TryGetValue("id", out _) + invokedType: KnownType.Microsoft_AspNetCore_Http_IQueryCollection, + methodName: "TryGetValue", + parameterName: "key", + argumentPosition: 0)); + argumentDescriptors.Add(ArgumentDescriptor.ElementAccess(// Request.RouteValues["id"] + invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Routing_RouteValueDictionary, + invokedIndexerExpression: "RouteValues", + parameterConstraint: parameter => parameter.IsType(KnownType.System_String) && IsGetterParameter(parameter), + argumentPosition: 0)); + argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation(// Request.RouteValues.TryGetValue("id", out _) + invokedType: KnownType.Microsoft_AspNetCore_Routing_RouteValueDictionary, + methodName: "TryGetValue", + parameterName: "key", + argumentPosition: 0)); } + + var propertyAccessDescriptors = new List + { + new(KnownType.Microsoft_AspNetCore_Http_IFormCollection, "Files"), // Request.Form.Files... + }; // TODO: Add descriptors for Asp.Net MVC 4.x - if (argumentDescriptors.Any()) + if (argumentDescriptors.Any() || propertyAccessDescriptors.Any()) { compilationStartContext.RegisterSymbolStartAction(symbolStartContext => { if (symbolStartContext.Symbol is INamedTypeSymbol namedType && namedType.IsControllerType()) { - symbolStartContext.RegisterSyntaxNodeAction(nodeContext => + if (argumentDescriptors.Any()) { - var argument = (ArgumentSyntax)nodeContext.Node; - var context = new ArgumentContext(argument, nodeContext.SemanticModel); - if (argumentDescriptors.Any(x => tracker.MatchArgument(x)(context)) - && nodeContext.SemanticModel.GetConstantValue(argument.Expression) is { HasValue: true, Value: string }) + symbolStartContext.RegisterSyntaxNodeAction(nodeContext => { - nodeContext.ReportIssue(Diagnostic.Create(Rule, GetPrimaryLocation(argument))); - } - }, SyntaxKind.Argument); + var argument = (ArgumentSyntax)nodeContext.Node; + var context = new ArgumentContext(argument, nodeContext.SemanticModel); + if (argumentDescriptors.Any(x => argumentTracker.MatchArgument(x)(context)) + && nodeContext.SemanticModel.GetConstantValue(argument.Expression) is { HasValue: true, Value: string }) + { + nodeContext.ReportIssue(Diagnostic.Create(Rule, GetPrimaryLocation(argument), UseModelBindingMessage)); + } + }, SyntaxKind.Argument); + } + if (propertyAccessDescriptors.Any()) + { + symbolStartContext.RegisterSyntaxNodeAction(nodeContext => + { + var memberAccess = (MemberAccessExpressionSyntax)nodeContext.Node; + var context = new PropertyAccessContext(memberAccess, nodeContext.SemanticModel, memberAccess.Name.Identifier.ValueText); + if (propertyTracker.MatchProperty([.. propertyAccessDescriptors])(context)) + { + nodeContext.ReportIssue(Diagnostic.Create(Rule, memberAccess.GetLocation(), UseIFormFileBindingMessage)); + } + }, SyntaxKind.SimpleMemberAccessExpression); + } } }, SymbolKind.NamedType); } }); } - private Location GetPrimaryLocation(ArgumentSyntax argument) => + private static Location GetPrimaryLocation(ArgumentSyntax argument) => argument switch { { Parent: BracketedArgumentListSyntax { Parent: ElementAccessExpressionSyntax { Expression: { } expression } } } => expression.GetLocation(), { Parent: ArgumentListSyntax { Parent: InvocationExpressionSyntax { Expression: { } expression } } } => expression.GetLocation(), _ => argument.GetLocation(), }; + + private static bool IsGetterParameter(IParameterSymbol parameter) => + parameter.ContainingSymbol is IMethodSymbol { MethodKind: MethodKind.PropertyGet }; + + private static bool IsIDictionaryStringStringValuesInvocation(IMethodSymbol method, string name) => + method.Is(KnownType.System_Collections_Generic_IDictionary_TKey_TValue, name) + && method.ContainingType.TypeArguments is { Length: 2 } typeArguments + && typeArguments[0].Is(KnownType.System_String) + && typeArguments[1].Is(KnownType.Microsoft_Extensions_Primitives_StringValues); } diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs index 0c897b69d19..98127937c7f 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs @@ -55,8 +55,6 @@ public sealed partial class KnownType public static readonly KnownType Microsoft_AspNet_SignalR_Hub = new("Microsoft.AspNet.SignalR.Hub"); public static readonly KnownType Microsoft_AspNetCore_Builder_DeveloperExceptionPageExtensions = new("Microsoft.AspNetCore.Builder.DeveloperExceptionPageExtensions"); public static readonly KnownType Microsoft_AspNetCore_Builder_DatabaseErrorPageExtensions = new("Microsoft.AspNetCore.Builder.DatabaseErrorPageExtensions"); - public static readonly KnownType Microsoft_AspNetCore_Http_HttpRequest = new("Microsoft.AspNetCore.Http.HttpRequest"); - public static readonly KnownType Microsoft_AspNetCore_Http_IFormCollection = new("Microsoft.AspNetCore.Http.IFormCollection"); public static readonly KnownType Microsoft_AspNetCore_Components_ParameterAttribute = new("Microsoft.AspNetCore.Components.ParameterAttribute"); public static readonly KnownType Microsoft_AspNetCore_Components_Rendering_RenderTreeBuilder = new("Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder"); public static readonly KnownType Microsoft_AspNetCore_Components_RouteAttribute = new("Microsoft.AspNetCore.Components.RouteAttribute"); @@ -66,7 +64,10 @@ public sealed partial class KnownType public static readonly KnownType Microsoft_AspNetCore_Hosting_WebHostBuilderExtensions = new("Microsoft.AspNetCore.Hosting.WebHostBuilderExtensions"); public static readonly KnownType Microsoft_AspNetCore_Http_CookieOptions = new("Microsoft.AspNetCore.Http.CookieOptions"); public static readonly KnownType Microsoft_AspNetCore_Http_HeaderDictionaryExtensions = new("Microsoft.AspNetCore.Http.HeaderDictionaryExtensions"); + public static readonly KnownType Microsoft_AspNetCore_Http_HttpRequest = new("Microsoft.AspNetCore.Http.HttpRequest"); + public static readonly KnownType Microsoft_AspNetCore_Http_IFormCollection = new("Microsoft.AspNetCore.Http.IFormCollection"); public static readonly KnownType Microsoft_AspNetCore_Http_IHeaderDictionary = new("Microsoft.AspNetCore.Http.IHeaderDictionary"); + public static readonly KnownType Microsoft_AspNetCore_Http_IQueryCollection = new("Microsoft.AspNetCore.Http.IQueryCollection"); public static readonly KnownType Microsoft_AspNetCore_Http_IRequestCookieCollection = new("Microsoft.AspNetCore.Http.IRequestCookieCollection"); public static readonly KnownType Microsoft_AspNetCore_Http_IResponseCookies = new("Microsoft.AspNetCore.Http.IResponseCookies"); public static readonly KnownType Microsoft_AspNetCore_Mvc_Controller = new("Microsoft.AspNetCore.Mvc.Controller"); @@ -84,6 +85,7 @@ public sealed partial class KnownType public static readonly KnownType Microsoft_AspNetCore_Mvc_RouteAttribute = new("Microsoft.AspNetCore.Mvc.RouteAttribute"); public static readonly KnownType Microsoft_AspNetCore_Mvc_Routing_HttpMethodAttribute = new("Microsoft.AspNetCore.Mvc.Routing.HttpMethodAttribute"); public static readonly KnownType Microsoft_AspNetCore_Razor_Hosting_RazorCompiledItemAttribute = new("Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemAttribute"); + public static readonly KnownType Microsoft_AspNetCore_Routing_RouteValueDictionary = new("Microsoft.AspNetCore.Routing.RouteValueDictionary"); public static readonly KnownType Microsoft_Azure_Cosmos_CosmosClient = new("Microsoft.Azure.Cosmos.CosmosClient"); public static readonly KnownType Microsoft_Azure_Documents_Client_DocumentClient = new("Microsoft.Azure.Documents.Client.DocumentClient"); public static readonly KnownType Microsoft_Azure_ServiceBus_Management_ManagementClient = new("Microsoft.Azure.ServiceBus.Management.ManagementClient"); diff --git a/analyzers/src/SonarAnalyzer.Common/Trackers/PropertyAccessContext.cs b/analyzers/src/SonarAnalyzer.Common/Trackers/PropertyAccessContext.cs index a31699a9d90..c331e3d7f1d 100644 --- a/analyzers/src/SonarAnalyzer.Common/Trackers/PropertyAccessContext.cs +++ b/analyzers/src/SonarAnalyzer.Common/Trackers/PropertyAccessContext.cs @@ -30,5 +30,11 @@ public PropertyAccessContext(SonarSyntaxNodeReportingContext context, string pro PropertyName = propertyName; PropertySymbol = new Lazy(() => context.SemanticModel.GetSymbolInfo(context.Node).Symbol as IPropertySymbol); } + + public PropertyAccessContext(SyntaxNode node, SemanticModel semanticModel, string propertyName) : base(node, semanticModel) + { + PropertyName = propertyName; + PropertySymbol = new Lazy(() => semanticModel.GetSymbolInfo(node).Symbol as IPropertySymbol); + } } } diff --git a/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs b/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs index eecef364ca4..6cafd06b904 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs @@ -40,5 +40,29 @@ public class UseModelBindingTest [TestMethod] public void UseModelBinding_AspNetCore_CS() => builderAspNetCore.AddPaths("UseModelBinding_AspNetCore.cs").Verify(); + + [TestMethod] + public void UseModelBinding_AspNetCore_CS_Debug() => + builderAspNetCore + .WithConcurrentAnalysis(false) + .AddSnippet(""" + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Filters; + using System; + using System.Linq; + using System.Threading.Tasks; + + public class TestController : Controller + { + private readonly string Key = "id"; + + public IActionResult Post() + { + _ = Request.Headers.TryGetValue("id", out _); // Noncompliant {{Use model binding instead of accessing the raw request data}} + return null; + } + } + """).Verify(); #endif } diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs index 7d852e25a01..234e31a8f52 100644 --- a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs +++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs @@ -16,35 +16,35 @@ public IActionResult Post() _ = Request.Form.TryGetValue("id", out _); // Noncompliant {{Use model binding instead of accessing the raw request data}} // ^^^^^^^^^^^^^^^^^^^^^^^^ _ = Request.Form.ContainsKey("id"); // Noncompliant {{Use model binding instead of accessing the raw request data}} - // ^^^^ + // ^^^^^^^^^^^^^^^^^^^^^^^^ _ = Request.Headers["id"]; // Noncompliant {{Use model binding instead of accessing the raw request data}} - // ^^^^^^^ + // ^^^^^^^^^^^^^^^ _ = Request.Headers.TryGetValue("id", out _); // Noncompliant {{Use model binding instead of accessing the raw request data}} - // ^^^^^^^ + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^ _ = Request.Headers.ContainsKey("id"); // Noncompliant {{Use model binding instead of accessing the raw request data}} - // ^^^^^^^ + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^ _ = Request.Query["id"]; // Noncompliant {{Use model binding instead of accessing the raw request data}} - // ^^^^^ + // ^^^^^^^^^^^^^ _ = Request.Query.TryGetValue("id", out _); // Noncompliant {{Use model binding instead of accessing the raw request data}} - // ^^^^^ + // ^^^^^^^^^^^^^^^^^^^^^^^^^ _ = Request.RouteValues["id"]; // Noncompliant {{Use model binding instead of accessing the raw request data}} - // ^^^^^^^^^^^ + // ^^^^^^^^^^^^^^^^^^^ _ = Request.RouteValues.TryGetValue("id", out _); // Noncompliant {{Use model binding instead of accessing the raw request data}} - // ^^^^^^^^^^^ + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ _ = Request.Form.Files; // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}} - // ^^^^^ + // ^^^^^^^^^^^^^^^^^^ _ = Request.Form.Files["file"]; // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}} - // ^^^^^ + // ^^^^^^^^^^^^^^^^^^ _ = Request.Form.Files[0]; // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}} - // ^^^^^ + // ^^^^^^^^^^^^^^^^^^ _ = Request.Form.Files.Any(); // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}} - // ^^^^^ + // ^^^^^^^^^^^^^^^^^^ _ = Request.Form.Files.Count; // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}} - // ^^^^^ + // ^^^^^^^^^^^^^^^^^^ _ = Request.Form.Files.GetFile("file"); // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}} - // ^^^^^ + // ^^^^^^^^^^^^^^^^^^ _ = Request.Form.Files.GetFiles("file"); // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}} - // ^^^^^ + // ^^^^^^^^^^^^^^^^^^ return default; } @@ -71,6 +71,11 @@ void NoncompliantKeyVariations() _ = Request.Form.TryGetValue(value: out _, key: "id"); // Noncompliant } + void HeaderAccess() + { + Request.Headers["id"] = "Assignment"; // Compliant + } + // Parameterized for Form, Headers, Query, RouteValues / Request, this.Request, ControllerContext.HttpContext.Request / [FromForm], [FromQuery], [FromRoute], [FromHeader] // Implementation: Consider adding a CombinatorialDataAttribute https://stackoverflow.com/a/75531690 async Task Compliant([FromForm] string key) @@ -136,10 +141,10 @@ void M3() { _ = (true ? Request : Request).Form["id"]; // Noncompliant _ = ValidatedRequest().Form["id"]; // Noncompliant - _ = ValidRequest.Form["id"]; - _ = Form["id"]; // FN: requires cross method SE - _ = this.Form["id"]; // FN: requires cross method SE - _ = new CodeBlocksController().Form["id"]; // Compliant + _ = ValidRequest.Form["id"]; // Noncompliant + _ = Form["id"]; // Noncompliant + _ = this.Form["id"]; // Noncompliant + _ = new CodeBlocksController().Form["id"]; // Noncompliant HttpRequest ValidatedRequest() => Request; } From f35eb46b8552f647b0216935898b21c02be18ef9 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Tue, 12 Mar 2024 20:47:00 +0100 Subject: [PATCH 38/62] Add support for overrides --- .../Rules/UseModelBinding.cs | 84 ++++++++++++++++--- .../SonarAnalyzer.Common/Helpers/KnownType.cs | 2 + .../Rules/UseModelBindingTest.cs | 34 ++++++-- .../TestCases/UseModelBinding_AspNetCore.cs | 16 +++- 4 files changed, 115 insertions(+), 21 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs index a4271250c5c..26ef8284529 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs @@ -106,21 +106,47 @@ protected override void Initialize(SonarAnalysisContext context) { compilationStartContext.RegisterSymbolStartAction(symbolStartContext => { + var hasOverrides = false; + var controllerCandidates = new List(); if (symbolStartContext.Symbol is INamedTypeSymbol namedType && namedType.IsControllerType()) { if (argumentDescriptors.Any()) { - symbolStartContext.RegisterSyntaxNodeAction(nodeContext => + symbolStartContext.RegisterCodeBlockStartAction(codeBlockStart => { - var argument = (ArgumentSyntax)nodeContext.Node; - var context = new ArgumentContext(argument, nodeContext.SemanticModel); - if (argumentDescriptors.Any(x => argumentTracker.MatchArgument(x)(context)) - && nodeContext.SemanticModel.GetConstantValue(argument.Expression) is { HasValue: true, Value: string }) + var isOverride = codeBlockStart.OwningSymbol is IMethodSymbol method + && method.ExplicitOrImplicitInterfaceImplementations().Any(x => x is IMethodSymbol { ContainingType: { } container } && container.IsAny( + KnownType.Microsoft_AspNetCore_Mvc_Filters_IActionFilter, + KnownType.Microsoft_AspNetCore_Mvc_Filters_IAsyncActionFilter)); + hasOverrides |= isOverride; + var allConstantAccess = true; + var codeBlockCandidates = new List(); + if (!isOverride) { - nodeContext.ReportIssue(Diagnostic.Create(Rule, GetPrimaryLocation(argument), UseModelBindingMessage)); + codeBlockStart.RegisterNodeAction(nodeContext => + { + var argument = (ArgumentSyntax)nodeContext.Node; + var context = new ArgumentContext(argument, nodeContext.SemanticModel); + if (argumentDescriptors.Any(x => argumentTracker.MatchArgument(x)(context))) + { + allConstantAccess &= nodeContext.SemanticModel.GetConstantValue(argument.Expression) is { HasValue: true, Value: string }; + if (allConstantAccess) + { + var originatesFromParameter = OriginatesFromParameter(nodeContext.SemanticModel, argument); + codeBlockCandidates.Add(new(UseModelBindingMessage, GetPrimaryLocation(argument), originatesFromParameter)); + } + } + }, SyntaxKind.Argument); + codeBlockStart.RegisterCodeBlockEndAction(codeBlockEnd => + { + if (allConstantAccess) + { + controllerCandidates.AddRange(codeBlockCandidates); + } + }); } - }, SyntaxKind.Argument); + }); } if (propertyAccessDescriptors.Any()) { @@ -135,19 +161,53 @@ protected override void Initialize(SonarAnalysisContext context) }, SyntaxKind.SimpleMemberAccessExpression); } } + symbolStartContext.RegisterSymbolEndAction(symbolEnd => + { + foreach (var candidate in controllerCandidates) + { + if (hasOverrides && candidate.OriginatesFromParameter) + { + continue; + } + symbolEnd.ReportIssue(Diagnostic.Create(Rule, candidate.Location, candidate.Message)); + } + }); }, SymbolKind.NamedType); } }); } - private static Location GetPrimaryLocation(ArgumentSyntax argument) => + private static bool OriginatesFromParameter(SemanticModel semanticModel, ArgumentSyntax argument) => + GetExpressionOfArgumentParent(argument) is { } parentExpression + && MostLeftOfDottedChain(parentExpression) is { } mostLeft + && semanticModel.GetSymbolInfo(mostLeft).Symbol is IParameterSymbol; + + private static ExpressionSyntax MostLeftOfDottedChain(ExpressionSyntax root) + { + var current = root.GetRootConditionalAccessExpression() ?? root; + while (current.Kind() is SyntaxKind.SimpleMemberAccessExpression or SyntaxKind.ElementAccessExpression) + { + current = current switch + { + MemberAccessExpressionSyntax { Expression: { } left } => left, + ElementAccessExpressionSyntax { Expression: { } left } => left, + _ => throw new InvalidOperationException("Unreachable"), + }; + } + return current; + } + private static ExpressionSyntax GetExpressionOfArgumentParent(ArgumentSyntax argument) => argument switch { - { Parent: BracketedArgumentListSyntax { Parent: ElementAccessExpressionSyntax { Expression: { } expression } } } => expression.GetLocation(), - { Parent: ArgumentListSyntax { Parent: InvocationExpressionSyntax { Expression: { } expression } } } => expression.GetLocation(), - _ => argument.GetLocation(), + { Parent: BracketedArgumentListSyntax { Parent: ElementBindingExpressionSyntax { Parent: ConditionalAccessExpressionSyntax { Expression: { } expression } } } } => expression, + { Parent: BracketedArgumentListSyntax { Parent: ElementAccessExpressionSyntax { Expression: { } expression } } } => expression, + { Parent: ArgumentListSyntax { Parent: InvocationExpressionSyntax { Expression: { } expression } } } => expression, + _ => null, }; + private static Location GetPrimaryLocation(ArgumentSyntax argument) => + ((SyntaxNode)GetExpressionOfArgumentParent(argument) ?? argument).GetLocation(); + private static bool IsGetterParameter(IParameterSymbol parameter) => parameter.ContainingSymbol is IMethodSymbol { MethodKind: MethodKind.PropertyGet }; @@ -156,4 +216,6 @@ private static bool IsIDictionaryStringStringValuesInvocation(IMethodSymbol meth && method.ContainingType.TypeArguments is { Length: 2 } typeArguments && typeArguments[0].Is(KnownType.System_String) && typeArguments[1].Is(KnownType.Microsoft_Extensions_Primitives_StringValues); + + private readonly record struct ReportCandidate(string Message, Location Location, bool OriginatesFromParameter = false); } diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs index 98127937c7f..ab37fc5b4f1 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs @@ -74,6 +74,8 @@ public sealed partial class KnownType public static readonly KnownType Microsoft_AspNetCore_Mvc_ControllerBase = new("Microsoft.AspNetCore.Mvc.ControllerBase"); public static readonly KnownType Microsoft_AspNetCore_Mvc_ControllerAttribute = new("Microsoft.AspNetCore.Mvc.ControllerAttribute"); public static readonly KnownType Microsoft_AspNetCore_Mvc_DisableRequestSizeLimitAttribute = new("Microsoft.AspNetCore.Mvc.DisableRequestSizeLimitAttribute"); + public static readonly KnownType Microsoft_AspNetCore_Mvc_Filters_IActionFilter = new("Microsoft.AspNetCore.Mvc.Filters.IActionFilter"); + public static readonly KnownType Microsoft_AspNetCore_Mvc_Filters_IAsyncActionFilter = new("Microsoft.AspNetCore.Mvc.Filters.IAsyncActionFilter"); public static readonly KnownType Microsoft_AspNetCore_Mvc_FromServicesAttribute = new("Microsoft.AspNetCore.Mvc.FromServicesAttribute"); public static readonly KnownType Microsoft_AspNetCore_Mvc_IActionResult = new("Microsoft.AspNetCore.Mvc.IActionResult"); public static readonly KnownType Microsoft_AspNetCore_Mvc_IgnoreAntiforgeryTokenAttribute = new("Microsoft.AspNetCore.Mvc.IgnoreAntiforgeryTokenAttribute"); diff --git a/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs b/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs index 6cafd06b904..9a67c3be954 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs @@ -53,16 +53,36 @@ public void UseModelBinding_AspNetCore_CS_Debug() => using System.Linq; using System.Threading.Tasks; - public class TestController : Controller + public class OverridesController : Controller { - private readonly string Key = "id"; - - public IActionResult Post() + public void Action() + { + _ = Request.Form["id"]; // Noncompliant + } + private void Undecidable(HttpContext context) + { + // Implementation: It might be difficult to distinguish between access to "Request" that originate from overrides vs. "Request" access that originate from action methods. + // This is especially true for "Request" which originate from parameters like here. We may need to redeclare such cases as FNs (see e.g HandleRequest above). + _ = context.Request.Form["id"]; // Undecidable: request may originate from an action method (which supports binding), or from one of the following overrides (which don't). + } + private void Undecidable(HttpRequest request) + { + _ = request.Form["id"]; // Undecidable: request may originate from an action method (which supports binding), or from one of the following overloads (which don't). + } + public override void OnActionExecuted(ActionExecutedContext context) + { + _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here + } + public override void OnActionExecuting(ActionExecutingContext context) + { + _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here + } + public override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { - _ = Request.Headers.TryGetValue("id", out _); // Noncompliant {{Use model binding instead of accessing the raw request data}} - return null; + _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here + return base.OnActionExecutionAsync(context, next); } } - """).Verify(); + """).Verify(); #endif } diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs index 234e31a8f52..687dc808541 100644 --- a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs +++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs @@ -56,7 +56,6 @@ void NoncompliantKeyVariations() _ = Request.Form["""key"""]; // Noncompliant _ = Request.Form.TryGetValue("""key""", out _); // Noncompliant - _ = Request.Form[Key]; // FN: Key is a readonly field with a constant initializer (Requires cross procedure SE) const string key = "id"; _ = Request.Form[key]; // Noncompliant _ = Request.Form.TryGetValue(key, out _); // Noncompliant @@ -64,13 +63,24 @@ void NoncompliantKeyVariations() _ = Request.Form.TryGetValue($"prefix.{key}", out _); // Noncompliant _ = Request.Form[$"""prefix.{key}"""]; // Noncompliant _ = Request.Form.TryGetValue($"""prefix.{key}""", out _); // Noncompliant - string localKey = "id"; - _ = Request.Form[localKey]; // FN (Requires SE) _ = Request.Form[key: "id"]; // Noncompliant _ = Request.Form.TryGetValue(value: out _, key: "id"); // Noncompliant } + void MixedAccess(string key) + { + _ = Request.Form["id"]; // Compliant (a mixed access with constant and non-constant keys is compliant) + _ = Request.Form[key]; // Compliant + } + + void FalseNegatives() + { + string localKey = "id"; + _ = Request.Form[localKey]; // FN (Requires SE) + _ = Request.Form[Key]; // FN: Key is a readonly field with a constant initializer (Requires cross procedure SE) + } + void HeaderAccess() { Request.Headers["id"] = "Assignment"; // Compliant From c1726a3e8303f927ccb8746083d8c0291404a1f2 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Wed, 13 Mar 2024 11:57:26 +0100 Subject: [PATCH 39/62] Add IsOverridingFilterMethods and use ConcurrentStack --- .../Rules/UseModelBinding.cs | 31 ++++++++++++------- .../TestCases/UseModelBinding_AspNetCore.cs | 8 ++++- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs index 26ef8284529..cae32df379f 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using System.Collections.Concurrent; using SonarAnalyzer.Helpers.Trackers; namespace SonarAnalyzer.Rules.CSharp; @@ -107,7 +108,7 @@ protected override void Initialize(SonarAnalysisContext context) compilationStartContext.RegisterSymbolStartAction(symbolStartContext => { var hasOverrides = false; - var controllerCandidates = new List(); + var controllerCandidates = new ConcurrentStack(); if (symbolStartContext.Symbol is INamedTypeSymbol namedType && namedType.IsControllerType()) { @@ -115,13 +116,10 @@ protected override void Initialize(SonarAnalysisContext context) { symbolStartContext.RegisterCodeBlockStartAction(codeBlockStart => { - var isOverride = codeBlockStart.OwningSymbol is IMethodSymbol method - && method.ExplicitOrImplicitInterfaceImplementations().Any(x => x is IMethodSymbol { ContainingType: { } container } && container.IsAny( - KnownType.Microsoft_AspNetCore_Mvc_Filters_IActionFilter, - KnownType.Microsoft_AspNetCore_Mvc_Filters_IAsyncActionFilter)); + var isOverride = codeBlockStart.OwningSymbol is IMethodSymbol method && IsOverridingFilterMethods(method); hasOverrides |= isOverride; var allConstantAccess = true; - var codeBlockCandidates = new List(); + var codeBlockCandidates = new ConcurrentStack(); if (!isOverride) { codeBlockStart.RegisterNodeAction(nodeContext => @@ -133,8 +131,7 @@ protected override void Initialize(SonarAnalysisContext context) allConstantAccess &= nodeContext.SemanticModel.GetConstantValue(argument.Expression) is { HasValue: true, Value: string }; if (allConstantAccess) { - var originatesFromParameter = OriginatesFromParameter(nodeContext.SemanticModel, argument); - codeBlockCandidates.Add(new(UseModelBindingMessage, GetPrimaryLocation(argument), originatesFromParameter)); + codeBlockCandidates.Push(new(UseModelBindingMessage, GetPrimaryLocation(argument), OriginatesFromParameter(nodeContext.SemanticModel, argument))); } } }, SyntaxKind.Argument); @@ -142,7 +139,7 @@ protected override void Initialize(SonarAnalysisContext context) { if (allConstantAccess) { - controllerCandidates.AddRange(codeBlockCandidates); + controllerCandidates.PushRange([.. codeBlockCandidates]); } }); } @@ -156,7 +153,7 @@ protected override void Initialize(SonarAnalysisContext context) var context = new PropertyAccessContext(memberAccess, nodeContext.SemanticModel, memberAccess.Name.Identifier.ValueText); if (propertyTracker.MatchProperty([.. propertyAccessDescriptors])(context)) { - nodeContext.ReportIssue(Diagnostic.Create(Rule, memberAccess.GetLocation(), UseIFormFileBindingMessage)); + controllerCandidates.Push(new(UseIFormFileBindingMessage, memberAccess.GetLocation(), OriginatesFromParameter(nodeContext.SemanticModel, memberAccess))); } }, SyntaxKind.SimpleMemberAccessExpression); } @@ -177,10 +174,19 @@ protected override void Initialize(SonarAnalysisContext context) }); } + private static bool IsOverridingFilterMethods(IMethodSymbol method) => + (method.GetOverriddenMember() ?? method).ExplicitOrImplicitInterfaceImplementations().Any(x => x is IMethodSymbol { ContainingType: { } container } + && container.IsAny( + KnownType.Microsoft_AspNetCore_Mvc_Filters_IActionFilter, + KnownType.Microsoft_AspNetCore_Mvc_Filters_IAsyncActionFilter)); + private static bool OriginatesFromParameter(SemanticModel semanticModel, ArgumentSyntax argument) => GetExpressionOfArgumentParent(argument) is { } parentExpression - && MostLeftOfDottedChain(parentExpression) is { } mostLeft - && semanticModel.GetSymbolInfo(mostLeft).Symbol is IParameterSymbol; + && OriginatesFromParameter(semanticModel, parentExpression); + + private static bool OriginatesFromParameter(SemanticModel semanticModel, ExpressionSyntax expression) => + MostLeftOfDottedChain(expression) is { } mostLeft + && semanticModel.GetSymbolInfo(mostLeft).Symbol is IParameterSymbol; private static ExpressionSyntax MostLeftOfDottedChain(ExpressionSyntax root) { @@ -196,6 +202,7 @@ private static ExpressionSyntax MostLeftOfDottedChain(ExpressionSyntax root) } return current; } + private static ExpressionSyntax GetExpressionOfArgumentParent(ArgumentSyntax argument) => argument switch { diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs index 687dc808541..0650cae7d7f 100644 --- a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs +++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs @@ -68,12 +68,18 @@ void NoncompliantKeyVariations() _ = Request.Form.TryGetValue(value: out _, key: "id"); // Noncompliant } - void MixedAccess(string key) + void MixedAccess_Form(string key) { _ = Request.Form["id"]; // Compliant (a mixed access with constant and non-constant keys is compliant) _ = Request.Form[key]; // Compliant } + void MixedAccess_Form_Query(string key) + { + _ = Request.Form["id"]; // Compliant (a mixed access with constant and non-constant keys is compliant) + _ = Request.Query[key]; // Compliant + } + void FalseNegatives() { string localKey = "id"; From a0847196acb56f3323e94087e359aba668809476 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Wed, 13 Mar 2024 12:01:20 +0100 Subject: [PATCH 40/62] Extract descriptors --- .../Rules/UseModelBinding.cs | 118 +++++++++--------- 1 file changed, 60 insertions(+), 58 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs index cae32df379f..dc3d2f1230a 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs @@ -41,67 +41,11 @@ protected override void Initialize(SonarAnalysisContext context) var argumentTracker = new CSharpArgumentTracker(); var propertyTracker = new CSharpPropertyAccessTracker(); var argumentDescriptors = new List(); + var propertyAccessDescriptors = new List(); if (compilationStartContext.Compilation.GetTypeByMetadataName(KnownType.Microsoft_AspNetCore_Mvc_ControllerAttribute) is { } controllerAttribute) { - // ASP.Net core - argumentDescriptors.Add(ArgumentDescriptor.ElementAccess(// Request.Form["id"] - invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Http_IFormCollection, - invokedIndexerExpression: "Form", - parameterConstraint: parameter => parameter.IsType(KnownType.System_String) && IsGetterParameter(parameter), - argumentPosition: 0)); - argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation(// Request.Form.TryGetValue("id", out _) - invokedType: KnownType.Microsoft_AspNetCore_Http_IFormCollection, - methodName: "TryGetValue", - parameterName: "key", - argumentPosition: 0)); - argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation(// Request.Form.ContainsKey("id") - invokedType: KnownType.Microsoft_AspNetCore_Http_IFormCollection, - methodName: "ContainsKey", - parameterName: "key", - argumentPosition: 0)); - argumentDescriptors.Add(ArgumentDescriptor.ElementAccess(// Request.Headers["id"] - invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Http_IHeaderDictionary, - invokedIndexerExpression: "Headers", - parameterConstraint: parameter => parameter.IsType(KnownType.System_String) && IsGetterParameter(parameter), - argumentPosition: 0)); - argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation(// Request.Headers.TryGetValue("id", out _) - invokedMethodSymbol: x => IsIDictionaryStringStringValuesInvocation(x, "TryGetValue"), - invokedMemberNameConstraint: (name, comparison) => string.Equals(name, "TryGetValue", comparison), - parameterConstraint: x => string.Equals(x.Name, "key", StringComparison.Ordinal), - argumentPosition: x => x == 0, - refKind: null)); - argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation(// Request.Headers.ContainsKey("id") - invokedMethodSymbol: x => IsIDictionaryStringStringValuesInvocation(x, "ContainsKey"), - invokedMemberNameConstraint: (name, comparison) => string.Equals(name, "ContainsKey", comparison), - parameterConstraint: x => string.Equals(x.Name, "key", StringComparison.Ordinal), - argumentPosition: x => x == 0, - refKind: null)); - argumentDescriptors.Add(ArgumentDescriptor.ElementAccess(// Request.Query["id"] - invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Http_IQueryCollection, - invokedIndexerExpression: "Query", - parameterConstraint: parameter => parameter.IsType(KnownType.System_String) && IsGetterParameter(parameter), - argumentPosition: 0)); - argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation(// Request.Query.TryGetValue("id", out _) - invokedType: KnownType.Microsoft_AspNetCore_Http_IQueryCollection, - methodName: "TryGetValue", - parameterName: "key", - argumentPosition: 0)); - argumentDescriptors.Add(ArgumentDescriptor.ElementAccess(// Request.RouteValues["id"] - invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Routing_RouteValueDictionary, - invokedIndexerExpression: "RouteValues", - parameterConstraint: parameter => parameter.IsType(KnownType.System_String) && IsGetterParameter(parameter), - argumentPosition: 0)); - argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation(// Request.RouteValues.TryGetValue("id", out _) - invokedType: KnownType.Microsoft_AspNetCore_Routing_RouteValueDictionary, - methodName: "TryGetValue", - parameterName: "key", - argumentPosition: 0)); + AddAspNetCoreDescriptors(argumentDescriptors, propertyAccessDescriptors); } - - var propertyAccessDescriptors = new List - { - new(KnownType.Microsoft_AspNetCore_Http_IFormCollection, "Files"), // Request.Form.Files... - }; // TODO: Add descriptors for Asp.Net MVC 4.x if (argumentDescriptors.Any() || propertyAccessDescriptors.Any()) { @@ -174,6 +118,64 @@ protected override void Initialize(SonarAnalysisContext context) }); } + private static void AddAspNetCoreDescriptors(List argumentDescriptors, List propertyAccessDescriptors) + { + argumentDescriptors.Add(ArgumentDescriptor.ElementAccess(// Request.Form["id"] + invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Http_IFormCollection, + invokedIndexerExpression: "Form", + parameterConstraint: parameter => parameter.IsType(KnownType.System_String) && IsGetterParameter(parameter), + argumentPosition: 0)); + argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation(// Request.Form.TryGetValue("id", out _) + invokedType: KnownType.Microsoft_AspNetCore_Http_IFormCollection, + methodName: "TryGetValue", + parameterName: "key", + argumentPosition: 0)); + argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation(// Request.Form.ContainsKey("id") + invokedType: KnownType.Microsoft_AspNetCore_Http_IFormCollection, + methodName: "ContainsKey", + parameterName: "key", + argumentPosition: 0)); + argumentDescriptors.Add(ArgumentDescriptor.ElementAccess(// Request.Headers["id"] + invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Http_IHeaderDictionary, + invokedIndexerExpression: "Headers", + parameterConstraint: parameter => parameter.IsType(KnownType.System_String) && IsGetterParameter(parameter), + argumentPosition: 0)); + argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation(// Request.Headers.TryGetValue("id", out _) + invokedMethodSymbol: x => IsIDictionaryStringStringValuesInvocation(x, "TryGetValue"), + invokedMemberNameConstraint: (name, comparison) => string.Equals(name, "TryGetValue", comparison), + parameterConstraint: x => string.Equals(x.Name, "key", StringComparison.Ordinal), + argumentPosition: x => x == 0, + refKind: null)); + argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation(// Request.Headers.ContainsKey("id") + invokedMethodSymbol: x => IsIDictionaryStringStringValuesInvocation(x, "ContainsKey"), + invokedMemberNameConstraint: (name, comparison) => string.Equals(name, "ContainsKey", comparison), + parameterConstraint: x => string.Equals(x.Name, "key", StringComparison.Ordinal), + argumentPosition: x => x == 0, + refKind: null)); + argumentDescriptors.Add(ArgumentDescriptor.ElementAccess(// Request.Query["id"] + invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Http_IQueryCollection, + invokedIndexerExpression: "Query", + parameterConstraint: parameter => parameter.IsType(KnownType.System_String) && IsGetterParameter(parameter), + argumentPosition: 0)); + argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation(// Request.Query.TryGetValue("id", out _) + invokedType: KnownType.Microsoft_AspNetCore_Http_IQueryCollection, + methodName: "TryGetValue", + parameterName: "key", + argumentPosition: 0)); + argumentDescriptors.Add(ArgumentDescriptor.ElementAccess(// Request.RouteValues["id"] + invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Routing_RouteValueDictionary, + invokedIndexerExpression: "RouteValues", + parameterConstraint: parameter => parameter.IsType(KnownType.System_String) && IsGetterParameter(parameter), + argumentPosition: 0)); + argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation(// Request.RouteValues.TryGetValue("id", out _) + invokedType: KnownType.Microsoft_AspNetCore_Routing_RouteValueDictionary, + methodName: "TryGetValue", + parameterName: "key", + argumentPosition: 0)); + + propertyAccessDescriptors.Add(new(KnownType.Microsoft_AspNetCore_Http_IFormCollection, "Files")); // Request.Form.Files... + } + private static bool IsOverridingFilterMethods(IMethodSymbol method) => (method.GetOverriddenMember() ?? method).ExplicitOrImplicitInterfaceImplementations().Any(x => x is IMethodSymbol { ContainingType: { } container } && container.IsAny( From 1dfab22b61dcebc4dac9bc9c0d1f0355d8ec0b6d Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Wed, 13 Mar 2024 12:38:49 +0100 Subject: [PATCH 41/62] Re-factor --- .../Rules/UseModelBinding.cs | 77 ++++++++++--------- 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs index dc3d2f1230a..3608daf279c 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs @@ -51,56 +51,57 @@ protected override void Initialize(SonarAnalysisContext context) { compilationStartContext.RegisterSymbolStartAction(symbolStartContext => { + // If the user overrides any action filters, model binding may not be working as expected. We do not want to raise on expressions that originate from parameters. var hasOverrides = false; - var controllerCandidates = new ConcurrentStack(); + var controllerCandidates = new ConcurrentStack(); // In SymbolEnd, we filter the candidates based on the overriding we learn on the go. if (symbolStartContext.Symbol is INamedTypeSymbol namedType && namedType.IsControllerType()) { - if (argumentDescriptors.Any()) + symbolStartContext.RegisterCodeBlockStartAction(codeBlockStart => { - symbolStartContext.RegisterCodeBlockStartAction(codeBlockStart => + var isOverride = codeBlockStart.OwningSymbol is IMethodSymbol method && IsOverridingFilterMethods(method); + hasOverrides |= isOverride; + if (isOverride) { - var isOverride = codeBlockStart.OwningSymbol is IMethodSymbol method && IsOverridingFilterMethods(method); - hasOverrides |= isOverride; - var allConstantAccess = true; - var codeBlockCandidates = new ConcurrentStack(); - if (!isOverride) + return; + } + // Within a single codeblock, access via constant and variable keys could be mixed + // We only want to raise, if all access were done via constants + var allConstantAccess = true; + var codeBlockCandidates = new ConcurrentStack(); + if (argumentDescriptors.Any()) + { + codeBlockStart.RegisterNodeAction(nodeContext => { - codeBlockStart.RegisterNodeAction(nodeContext => + var argument = (ArgumentSyntax)nodeContext.Node; + var context = new ArgumentContext(argument, nodeContext.SemanticModel); + if (allConstantAccess && argumentDescriptors.Exists(x => argumentTracker.MatchArgument(x)(context))) { - var argument = (ArgumentSyntax)nodeContext.Node; - var context = new ArgumentContext(argument, nodeContext.SemanticModel); - if (argumentDescriptors.Any(x => argumentTracker.MatchArgument(x)(context))) - { - allConstantAccess &= nodeContext.SemanticModel.GetConstantValue(argument.Expression) is { HasValue: true, Value: string }; - if (allConstantAccess) - { - codeBlockCandidates.Push(new(UseModelBindingMessage, GetPrimaryLocation(argument), OriginatesFromParameter(nodeContext.SemanticModel, argument))); - } - } - }, SyntaxKind.Argument); - codeBlockStart.RegisterCodeBlockEndAction(codeBlockEnd => + allConstantAccess &= nodeContext.SemanticModel.GetConstantValue(argument.Expression) is { HasValue: true, Value: string }; + codeBlockCandidates.Push(new(UseModelBindingMessage, GetPrimaryLocation(argument), OriginatesFromParameter(nodeContext.SemanticModel, argument))); + } + }, SyntaxKind.Argument); + } + if (propertyAccessDescriptors.Any()) + { + codeBlockStart.RegisterNodeAction(nodeContext => + { + var memberAccess = (MemberAccessExpressionSyntax)nodeContext.Node; + var context = new PropertyAccessContext(memberAccess, nodeContext.SemanticModel, memberAccess.Name.Identifier.ValueText); + if (propertyTracker.MatchProperty([.. propertyAccessDescriptors])(context)) { - if (allConstantAccess) - { - controllerCandidates.PushRange([.. codeBlockCandidates]); - } - }); - } - }); - } - if (propertyAccessDescriptors.Any()) - { - symbolStartContext.RegisterSyntaxNodeAction(nodeContext => + codeBlockCandidates.Push(new(UseIFormFileBindingMessage, memberAccess.GetLocation(), OriginatesFromParameter(nodeContext.SemanticModel, memberAccess))); + } + }, SyntaxKind.SimpleMemberAccessExpression); + } + codeBlockStart.RegisterCodeBlockEndAction(codeBlockEnd => { - var memberAccess = (MemberAccessExpressionSyntax)nodeContext.Node; - var context = new PropertyAccessContext(memberAccess, nodeContext.SemanticModel, memberAccess.Name.Identifier.ValueText); - if (propertyTracker.MatchProperty([.. propertyAccessDescriptors])(context)) + if (allConstantAccess) { - controllerCandidates.Push(new(UseIFormFileBindingMessage, memberAccess.GetLocation(), OriginatesFromParameter(nodeContext.SemanticModel, memberAccess))); + controllerCandidates.PushRange([.. codeBlockCandidates]); } - }, SyntaxKind.SimpleMemberAccessExpression); - } + }); + }); } symbolStartContext.RegisterSymbolEndAction(symbolEnd => { From af98074a44948655e552519482f9ced9eb4105e0 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Wed, 13 Mar 2024 13:04:58 +0100 Subject: [PATCH 42/62] Styling --- .../Rules/UseModelBinding.cs | 105 +++++++++--------- 1 file changed, 53 insertions(+), 52 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs index 3608daf279c..e14a2b37dc8 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs @@ -51,66 +51,18 @@ protected override void Initialize(SonarAnalysisContext context) { compilationStartContext.RegisterSymbolStartAction(symbolStartContext => { - // If the user overrides any action filters, model binding may not be working as expected. We do not want to raise on expressions that originate from parameters. + // If the user overrides any action filters, model binding may not be working as expected. Then we do not want to raise on expressions that originate from parameters. var hasOverrides = false; var controllerCandidates = new ConcurrentStack(); // In SymbolEnd, we filter the candidates based on the overriding we learn on the go. - if (symbolStartContext.Symbol is INamedTypeSymbol namedType - && namedType.IsControllerType()) + if (symbolStartContext.Symbol is INamedTypeSymbol namedType && namedType.IsControllerType()) { symbolStartContext.RegisterCodeBlockStartAction(codeBlockStart => - { - var isOverride = codeBlockStart.OwningSymbol is IMethodSymbol method && IsOverridingFilterMethods(method); - hasOverrides |= isOverride; - if (isOverride) - { - return; - } - // Within a single codeblock, access via constant and variable keys could be mixed - // We only want to raise, if all access were done via constants - var allConstantAccess = true; - var codeBlockCandidates = new ConcurrentStack(); - if (argumentDescriptors.Any()) - { - codeBlockStart.RegisterNodeAction(nodeContext => - { - var argument = (ArgumentSyntax)nodeContext.Node; - var context = new ArgumentContext(argument, nodeContext.SemanticModel); - if (allConstantAccess && argumentDescriptors.Exists(x => argumentTracker.MatchArgument(x)(context))) - { - allConstantAccess &= nodeContext.SemanticModel.GetConstantValue(argument.Expression) is { HasValue: true, Value: string }; - codeBlockCandidates.Push(new(UseModelBindingMessage, GetPrimaryLocation(argument), OriginatesFromParameter(nodeContext.SemanticModel, argument))); - } - }, SyntaxKind.Argument); - } - if (propertyAccessDescriptors.Any()) - { - codeBlockStart.RegisterNodeAction(nodeContext => - { - var memberAccess = (MemberAccessExpressionSyntax)nodeContext.Node; - var context = new PropertyAccessContext(memberAccess, nodeContext.SemanticModel, memberAccess.Name.Identifier.ValueText); - if (propertyTracker.MatchProperty([.. propertyAccessDescriptors])(context)) - { - codeBlockCandidates.Push(new(UseIFormFileBindingMessage, memberAccess.GetLocation(), OriginatesFromParameter(nodeContext.SemanticModel, memberAccess))); - } - }, SyntaxKind.SimpleMemberAccessExpression); - } - codeBlockStart.RegisterCodeBlockEndAction(codeBlockEnd => - { - if (allConstantAccess) - { - controllerCandidates.PushRange([.. codeBlockCandidates]); - } - }); - }); + hasOverrides |= CheckCodeBlock(codeBlockStart, argumentTracker, propertyTracker, argumentDescriptors, propertyAccessDescriptors, controllerCandidates)); } symbolStartContext.RegisterSymbolEndAction(symbolEnd => { - foreach (var candidate in controllerCandidates) + foreach (var candidate in controllerCandidates.Where(x => !(hasOverrides && x.OriginatesFromParameter))) { - if (hasOverrides && candidate.OriginatesFromParameter) - { - continue; - } symbolEnd.ReportIssue(Diagnostic.Create(Rule, candidate.Location, candidate.Message)); } }); @@ -119,6 +71,55 @@ protected override void Initialize(SonarAnalysisContext context) }); } + private static bool CheckCodeBlock(SonarCodeBlockStartAnalysisContext codeBlockStart, + CSharpArgumentTracker argumentTracker, CSharpPropertyAccessTracker propertyTracker, + IReadOnlyList argumentDescriptors, IReadOnlyList propertyAccessDescriptors, + ConcurrentStack controllerCandidates) + { + var isOverride = codeBlockStart.OwningSymbol is IMethodSymbol method && IsOverridingFilterMethods(method); + if (isOverride) + { + return true; + } + // Within a single code block, access via constant and variable keys could be mixed + // We only want to raise, if all access were done via constants + var allConstantAccess = true; + var codeBlockCandidates = new ConcurrentStack(); + if (argumentDescriptors.Any()) + { + codeBlockStart.RegisterNodeAction(nodeContext => + { + var argument = (ArgumentSyntax)nodeContext.Node; + var context = new ArgumentContext(argument, nodeContext.SemanticModel); + if (allConstantAccess && argumentDescriptors.Any(x => argumentTracker.MatchArgument(x)(context))) + { + allConstantAccess &= nodeContext.SemanticModel.GetConstantValue(argument.Expression) is { HasValue: true, Value: string }; + codeBlockCandidates.Push(new(UseModelBindingMessage, GetPrimaryLocation(argument), OriginatesFromParameter(nodeContext.SemanticModel, argument))); + } + }, SyntaxKind.Argument); + } + if (propertyAccessDescriptors.Any()) + { + codeBlockStart.RegisterNodeAction(nodeContext => + { + var memberAccess = (MemberAccessExpressionSyntax)nodeContext.Node; + var context = new PropertyAccessContext(memberAccess, nodeContext.SemanticModel, memberAccess.Name.Identifier.ValueText); + if (propertyTracker.MatchProperty([.. propertyAccessDescriptors])(context)) + { + codeBlockCandidates.Push(new(UseIFormFileBindingMessage, memberAccess.GetLocation(), OriginatesFromParameter(nodeContext.SemanticModel, memberAccess))); + } + }, SyntaxKind.SimpleMemberAccessExpression); + } + codeBlockStart.RegisterCodeBlockEndAction(codeBlockEnd => + { + if (allConstantAccess) + { + controllerCandidates.PushRange([.. codeBlockCandidates]); + } + }); + return false; + } + private static void AddAspNetCoreDescriptors(List argumentDescriptors, List propertyAccessDescriptors) { argumentDescriptors.Add(ArgumentDescriptor.ElementAccess(// Request.Form["id"] From 3ca6b46e9405e5b3933ecc030d240e1c69c7b098 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Wed, 13 Mar 2024 21:16:27 +0100 Subject: [PATCH 43/62] Add invokedMemberNodeConstraint to ArgumentDescriptor --- .../Rules/UseModelBinding.cs | 15 +++++- .../Helpers/ArgumentDescriptor.cs | 26 ++++++++-- .../Trackers/ArgumentTracker.cs | 1 + .../Rules/UseModelBindingTest.cs | 27 +--------- .../TestCases/UseModelBinding_AspNetCore.cs | 5 ++ .../Trackers/ArgumentTrackerTest.cs | 49 +++++++++++++++++-- 6 files changed, 86 insertions(+), 37 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs index e14a2b37dc8..73be91396ac 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs @@ -42,7 +42,7 @@ protected override void Initialize(SonarAnalysisContext context) var propertyTracker = new CSharpPropertyAccessTracker(); var argumentDescriptors = new List(); var propertyAccessDescriptors = new List(); - if (compilationStartContext.Compilation.GetTypeByMetadataName(KnownType.Microsoft_AspNetCore_Mvc_ControllerAttribute) is { } controllerAttribute) + if (compilationStartContext.Compilation.GetTypeByMetadataName(KnownType.Microsoft_AspNetCore_Mvc_ControllerAttribute) is { }) { AddAspNetCoreDescriptors(argumentDescriptors, propertyAccessDescriptors); } @@ -145,8 +145,11 @@ private static void AddAspNetCoreDescriptors(List argumentDe argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation(// Request.Headers.TryGetValue("id", out _) invokedMethodSymbol: x => IsIDictionaryStringStringValuesInvocation(x, "TryGetValue"), invokedMemberNameConstraint: (name, comparison) => string.Equals(name, "TryGetValue", comparison), + invokedMemberNodeConstraint: (model, language, invocation) => invocation is InvocationExpressionSyntax { Expression: { } expression } + && GetLeftOfDot(expression) is var left + && (left is null || (model.GetTypeInfo(left) is { Type: { } typeSymbol } && typeSymbol.Is(KnownType.Microsoft_AspNetCore_Http_IHeaderDictionary))), parameterConstraint: x => string.Equals(x.Name, "key", StringComparison.Ordinal), - argumentPosition: x => x == 0, + argumentListConstraint: (list, position) => list.Count == 2 && position == 0, refKind: null)); argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation(// Request.Headers.ContainsKey("id") invokedMethodSymbol: x => IsIDictionaryStringStringValuesInvocation(x, "ContainsKey"), @@ -192,6 +195,14 @@ private static bool OriginatesFromParameter(SemanticModel semanticModel, Express MostLeftOfDottedChain(expression) is { } mostLeft && semanticModel.GetSymbolInfo(mostLeft).Symbol is IParameterSymbol; + private static ExpressionSyntax GetLeftOfDot(ExpressionSyntax expression) => + expression switch + { + MemberAccessExpressionSyntax memberAccessExpression => memberAccessExpression.Expression, + MemberBindingExpressionSyntax memberBindingExpression => memberBindingExpression.GetParentConditionalAccessExpression()?.Expression, + _ => null, + }; + private static ExpressionSyntax MostLeftOfDottedChain(ExpressionSyntax root) { var current = root.GetRootConditionalAccessExpression() ?? root; diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/ArgumentDescriptor.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/ArgumentDescriptor.cs index b745deac974..b94d821f311 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/ArgumentDescriptor.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/ArgumentDescriptor.cs @@ -31,13 +31,15 @@ public enum InvokedMemberKind public class ArgumentDescriptor { private ArgumentDescriptor(InvokedMemberKind memberKind, Func invokedMemberConstraint, Func invokedMemberNameConstraint, - Func, int?, bool> argumentListConstraint, Func parameterConstraint, RefKind? refKind) + Func invokedMemberNodeConstraint, Func, int?, bool> argumentListConstraint, + Func parameterConstraint, RefKind? refKind) { MemberKind = memberKind; ArgumentListConstraint = argumentListConstraint; RefKind = refKind; ParameterConstraint = parameterConstraint; InvokedMemberNameConstraint = invokedMemberNameConstraint; + InvokedMemberNodeConstraint = invokedMemberNodeConstraint; InvokedMemberConstraint = invokedMemberConstraint; } @@ -61,15 +63,18 @@ public static ArgumentDescriptor MethodInvocation(Func invo Func parameterConstraint, Func argumentPosition, RefKind? refKind) => MethodInvocation(invokedMethodSymbol, invokedMemberNameConstraint, + (_, _, _) => true, parameterConstraint, (_, position) => position is null || argumentPosition is null || argumentPosition(position.Value), refKind); public static ArgumentDescriptor MethodInvocation(Func invokedMethodSymbol, Func invokedMemberNameConstraint, - Func parameterConstraint, Func, int?, bool> argumentListConstraint, RefKind? refKind) => + Func invokedMemberNodeConstraint, Func parameterConstraint, + Func, int?, bool> argumentListConstraint, RefKind? refKind) => new(InvokedMemberKind.Method, invokedMemberConstraint: invokedMethodSymbol, invokedMemberNameConstraint: invokedMemberNameConstraint, + invokedMemberNodeConstraint: invokedMemberNodeConstraint, argumentListConstraint: argumentListConstraint, parameterConstraint: parameterConstraint, refKind: refKind); @@ -78,15 +83,18 @@ public static ArgumentDescriptor ConstructorInvocation(KnownType constructedType ConstructorInvocation( x => constructedType.Matches(x.ContainingType), (x, c) => x.Equals(constructedType.TypeName, c), + static (_, _, _) => true, x => x.Name == parameterName, (_, x) => x is null || x == argumentPosition, null); public static ArgumentDescriptor ConstructorInvocation(Func invokedMethodSymbol, Func invokedMemberNameConstraint, - Func parameterConstraint, Func, int?, bool> argumentListConstraint, RefKind? refKind) => + Func invokedMemberNodeConstraint, Func parameterConstraint, + Func, int?, bool> argumentListConstraint, RefKind? refKind) => new(InvokedMemberKind.Constructor, invokedMemberConstraint: invokedMethodSymbol, invokedMemberNameConstraint: invokedMemberNameConstraint, + invokedMemberNodeConstraint: invokedMemberNodeConstraint, argumentListConstraint: argumentListConstraint, parameterConstraint: parameterConstraint, refKind: refKind); @@ -114,14 +122,17 @@ public static ArgumentDescriptor ElementAccess(KnownType invokedIndexerContainer ElementAccess( x => x is { ContainingSymbol: INamedTypeSymbol { } container } && invokedIndexerContainer.Matches(container), (s, c) => invokedIndexerExpression is null || s.Equals(invokedIndexerExpression, c), + (_, _, _) => true, argumentListConstraint: (_, p) => argumentPositionConstraint is null || p is null || argumentPositionConstraint(p.Value), parameterConstraint: parameterConstraint); public static ArgumentDescriptor ElementAccess(Func invokedIndexerPropertyMethod, Func invokedIndexerExpression, - Func parameterConstraint, Func, int?, bool> argumentListConstraint) => + Func invokedIndexerExpressionNodeConstraint, Func parameterConstraint, + Func, int?, bool> argumentListConstraint) => new(InvokedMemberKind.Indexer, invokedMemberConstraint: invokedIndexerPropertyMethod, invokedMemberNameConstraint: invokedIndexerExpression, + invokedMemberNodeConstraint: invokedIndexerExpressionNodeConstraint, argumentListConstraint: argumentListConstraint, parameterConstraint: parameterConstraint, refKind: null); @@ -130,14 +141,17 @@ public static ArgumentDescriptor AttributeArgument(string attributeName, string AttributeArgument( x => x is { MethodKind: MethodKind.Constructor, ContainingType.Name: { } name } && (name == attributeName || name == $"{attributeName}Attribute"), (x, c) => AttributeClassNameConstraint(attributeName, x, c), + (_, _, _) => true, p => p.Name == parameterName, (_, i) => i is null || i.Value == argumentPosition); public static ArgumentDescriptor AttributeArgument(Func attributeConstructorConstraint, Func attributeNameConstraint, - Func parameterConstraint, Func, int?, bool> argumentListConstraint) => + Func attributeNodeConstraint, Func parameterConstraint, + Func, int?, bool> argumentListConstraint) => new(InvokedMemberKind.Attribute, invokedMemberConstraint: attributeConstructorConstraint, invokedMemberNameConstraint: attributeNameConstraint, + invokedMemberNodeConstraint: attributeNodeConstraint, argumentListConstraint: argumentListConstraint, parameterConstraint: parameterConstraint, refKind: null); @@ -146,6 +160,7 @@ public static ArgumentDescriptor AttributeProperty(string attributeName, string AttributeArgument( attributeConstructorConstraint: x => x is { MethodKind: MethodKind.PropertySet, AssociatedSymbol.Name: { } name } && name == propertyName, attributeNameConstraint: (s, c) => AttributeClassNameConstraint(attributeName, s, c), + (_, _, _) => true, parameterConstraint: p => true, argumentListConstraint: (_, _) => true); @@ -157,5 +172,6 @@ private static bool AttributeClassNameConstraint(string expectedAttributeName, s public RefKind? RefKind { get; } public Func ParameterConstraint { get; } public Func InvokedMemberNameConstraint { get; } + public Func InvokedMemberNodeConstraint { get; } public Func InvokedMemberConstraint { get; } } diff --git a/analyzers/src/SonarAnalyzer.Common/Trackers/ArgumentTracker.cs b/analyzers/src/SonarAnalyzer.Common/Trackers/ArgumentTracker.cs index a2ddc7435e2..e48d7e578c4 100644 --- a/analyzers/src/SonarAnalyzer.Common/Trackers/ArgumentTracker.cs +++ b/analyzers/src/SonarAnalyzer.Common/Trackers/ArgumentTracker.cs @@ -38,6 +38,7 @@ public Condition MatchArgument(ArgumentDescriptor descriptor) => if (context.Node is { } argumentNode && argumentNode is { Parent.Parent: { } invoked } && SyntacticChecks(context.SemanticModel, descriptor, argumentNode, invoked) + && (descriptor.InvokedMemberNodeConstraint?.Invoke(context.SemanticModel, Language, invoked) ?? true) && MethodSymbol(context.SemanticModel, invoked) is { } methodSymbol && Language.MethodParameterLookup(invoked, methodSymbol).TryGetSymbol(argumentNode, out var parameter) && ParameterFits(parameter, descriptor.ParameterConstraint, descriptor.InvokedMemberConstraint)) diff --git a/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs b/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs index 9a67c3be954..ce9b3470988 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs @@ -55,32 +55,9 @@ public void UseModelBinding_AspNetCore_CS_Debug() => public class OverridesController : Controller { - public void Action() + public void Action(IFormCollection form) { - _ = Request.Form["id"]; // Noncompliant - } - private void Undecidable(HttpContext context) - { - // Implementation: It might be difficult to distinguish between access to "Request" that originate from overrides vs. "Request" access that originate from action methods. - // This is especially true for "Request" which originate from parameters like here. We may need to redeclare such cases as FNs (see e.g HandleRequest above). - _ = context.Request.Form["id"]; // Undecidable: request may originate from an action method (which supports binding), or from one of the following overrides (which don't). - } - private void Undecidable(HttpRequest request) - { - _ = request.Form["id"]; // Undecidable: request may originate from an action method (which supports binding), or from one of the following overloads (which don't). - } - public override void OnActionExecuted(ActionExecutedContext context) - { - _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here - } - public override void OnActionExecuting(ActionExecutingContext context) - { - _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here - } - public override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) - { - _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here - return base.OnActionExecutionAsync(context, next); + _ = form["id"]; // Compliant. Using IFormCollection is model binding } } """).Verify(); diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs index 0650cae7d7f..e1428adee5f 100644 --- a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs +++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs @@ -87,6 +87,11 @@ void FalseNegatives() _ = Request.Form[Key]; // FN: Key is a readonly field with a constant initializer (Requires cross procedure SE) } + void FormCollection(IFormCollection form) + { + _ = form["id"]; // Compliant. Using IFormCollection is model binding + } + void HeaderAccess() { Request.Headers["id"] = "Assignment"; // Compliant diff --git a/analyzers/tests/SonarAnalyzer.Test/Trackers/ArgumentTrackerTest.cs b/analyzers/tests/SonarAnalyzer.Test/Trackers/ArgumentTrackerTest.cs index 85a773d2c8d..f2848434231 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Trackers/ArgumentTrackerTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Trackers/ArgumentTrackerTest.cs @@ -474,6 +474,40 @@ void M() context.Parameter.ContainingType.Name.Should().Be("Action"); } + [TestMethod] + public void Method_InvocationOnProperty() + { + var snippet = $$""" + using System.Collections.Generic; + class C + { + public IList List { get; } = new List(); + void M() + { + List.Add($$1); // Add is defined on ICollection while the List property is of type IList, invokedMemberNodeConstraint can figure this out + } + } + """; + var (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.MethodInvocation( + invokedMethodSymbol: x => x.Is(KnownType.System_Collections_Generic_ICollection_T, "Add"), + invokedMemberNameConstraint: (x, c) => string.Equals(x, "Add", c), + invokedMemberNodeConstraint: (model, language, node) => + node is CS.InvocationExpressionSyntax { Expression: CS.MemberAccessExpressionSyntax { Expression: CS.IdentifierNameSyntax { Identifier.ValueText: { } leftName } left } } + && language.NameComparer.Equals(leftName, "List") + && model.GetSymbolInfo(left).Symbol is IPropertySymbol property + && property.Type.Is(KnownType.System_Collections_Generic_IList_T), + parameterConstraint: _ => true, + argumentListConstraint: (_, _) => true, + refKind: null); + var (result, context) = MatchArgumentCS(model, node, argument); + result.Should().BeTrue(); + context.Parameter.Name.Should().Be("item"); + context.Parameter.ContainingSymbol.Name.Should().Be("Add"); + context.Parameter.ContainingType.Name.Should().Be("ICollection"); + } + [DataTestMethod] [DataRow("""ProcessStartInfo($$"fileName")""", "fileName", 0, true)] [DataRow("""ProcessStartInfo($$"fileName")""", "arguments", 1, false)] @@ -716,6 +750,7 @@ public void M() var argument = ArgumentDescriptor.ConstructorInvocation(invokedMethodSymbol: x => x is { MethodKind: MethodKind.Constructor, ContainingSymbol.Name: "C" }, invokedMemberNameConstraint: (c, n) => c.Equals("C", n) || c.Equals("CAlias"), + invokedMemberNodeConstraint: (_, _, _) => true, parameterConstraint: p => p.Name is "i" or "j", argumentListConstraint: (n, i) => i is null or 0 or 1 && n.Count > 1, refKind: null); @@ -737,6 +772,7 @@ public Base(int i, int j) { } var argument = ArgumentDescriptor.ConstructorInvocation(invokedMethodSymbol: x => x is { MethodKind: MethodKind.Constructor, ContainingSymbol.Name: "Base" }, invokedMemberNameConstraint: (c, n) => c.Equals("Base", n), + invokedMemberNodeConstraint: (_, _, _) => true, parameterConstraint: p => p.Name is "i", argumentListConstraint: (_, _) => true, refKind: null); @@ -761,6 +797,7 @@ public Derived() : base($$1) { } var argument = ArgumentDescriptor.ConstructorInvocation(invokedMethodSymbol: x => x is { MethodKind: MethodKind.Constructor, ContainingSymbol.Name: "Base" }, invokedMemberNameConstraint: (c, n) => c.Equals("Base", n), + invokedMemberNodeConstraint: (_, _, _) => true, parameterConstraint: p => p.Name is "i", argumentListConstraint: (_, _) => true, refKind: null); @@ -896,7 +933,7 @@ public void Indexer_DictionaryGet(string environmentVariableAccess) var (node, model) = ArgumentAndModelCS(WrapInMethodCS(snippet)); var argument = ArgumentDescriptor.ElementAccess(m => m is { MethodKind: MethodKind.PropertyGet, ContainingType: { } type } && type.Name == "IDictionary", - (n, c) => n.Equals("GetEnvironmentVariables", c), p => p.Name == "key", (_, p) => p is null or 0); + (n, c) => n.Equals("GetEnvironmentVariables", c), (_, _, _) => true, p => p.Name == "key", (_, p) => p is null or 0); var (result, _) = MatchArgumentCS(model, node, argument); result.Should().BeTrue(); } @@ -910,7 +947,7 @@ public void Indexer_DictionaryGet_VB() var (node, model) = ArgumentAndModelVB(WrapInMethodVB(snippet)); var argument = ArgumentDescriptor.ElementAccess(m => m is { MethodKind: MethodKind.PropertyGet, ContainingType: { } type } && type.Name == "IDictionary", - (n, c) => n.Equals("GetEnvironmentVariables", c), p => p.Name == "key", (_, p) => p is null or 0); + (n, c) => n.Equals("GetEnvironmentVariables", c), (_, _, _) => true, p => p.Name == "key", (_, p) => p is null or 0); var (result, _) = MatchArgumentVB(model, node, argument); result.Should().BeTrue(); } @@ -944,6 +981,7 @@ public void M() { var argument = ArgumentDescriptor.ElementAccess( m => m is { MethodKind: var kind, ContainingType: { } type } && type.Name == "C" && (isGetter ? kind == MethodKind.PropertyGet : kind == MethodKind.PropertySet), (n, c) => true, + (_, _, _) => true, p => p.Name == parameterName, (_, _) => true); var (result, _) = MatchArgumentCS(model, node, argument); result.Should().BeTrue(); @@ -980,6 +1018,7 @@ End Class var argument = ArgumentDescriptor.ElementAccess( m => m is { MethodKind: var kind, ContainingType: { } type } && type.Name == "C" && (isGetter ? kind == MethodKind.PropertyGet : kind == MethodKind.PropertySet), (n, c) => true, + (_, _, _) => true, p => p.Name == parameterName, (_, _) => true); var (result, _) = MatchArgumentVB(model, node, argument); result.Should().BeTrue(); @@ -1006,7 +1045,7 @@ public void M(System.Diagnostics.Process process) var (node, model) = ArgumentAndModelCS(snippet); var argument = ArgumentDescriptor.ElementAccess(m => m is { MethodKind: MethodKind.PropertyGet, ContainingType: { } type } && type.Name == "ProcessModuleCollection", - (n, c) => n.Equals("Modules", c), p => p.Name == "index", (_, p) => p is null or 0); + (n, c) => n.Equals("Modules", c), (_, _, _) => true, p => p.Name == "index", (_, p) => p is null or 0); var (result, _) = MatchArgumentCS(model, node, argument); result.Should().BeTrue(); } @@ -1054,7 +1093,7 @@ public void M() var (node, model) = ArgumentAndModelCS(snippet); var argument = ArgumentDescriptor.AttributeArgument(x => x is { MethodKind: MethodKind.Constructor, ContainingType.Name: "ObsoleteAttribute" }, - (s, c) => s.StartsWith("Obsolete", c), p => p.Name == "message", (_, i) => i is 0); + (s, c) => s.StartsWith("Obsolete", c), (_, _, _) => true, p => p.Name == "message", (_, i) => i is 0); var (result, _) = MatchArgumentCS(model, node, argument); result.Should().BeTrue(); } @@ -1074,7 +1113,7 @@ End Class var (node, model) = ArgumentAndModelVB(snippet); var argument = ArgumentDescriptor.AttributeArgument(x => x is { MethodKind: MethodKind.Constructor, ContainingType.Name: "ObsoleteAttribute" }, - (s, c) => s.StartsWith("Obsolete", c), p => p.Name == "message", (_, i) => i is 0); + (s, c) => s.StartsWith("Obsolete", c), (_, _, _) => true, p => p.Name == "message", (_, i) => i is 0); var (result, _) = MatchArgumentVB(model, node, argument); result.Should().BeTrue(); } From 892d211f5302a8d29e0bf76bda2e13673903fa86 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Thu, 14 Mar 2024 10:09:23 +0100 Subject: [PATCH 44/62] Refactor and comments --- .../Rules/UseModelBinding.cs | 45 ++++++++++++------- .../Rules/UseModelBindingTest.cs | 21 --------- 2 files changed, 29 insertions(+), 37 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs index 73be91396ac..f8a7d336595 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs @@ -40,24 +40,19 @@ protected override void Initialize(SonarAnalysisContext context) { var argumentTracker = new CSharpArgumentTracker(); var propertyTracker = new CSharpPropertyAccessTracker(); - var argumentDescriptors = new List(); - var propertyAccessDescriptors = new List(); - if (compilationStartContext.Compilation.GetTypeByMetadataName(KnownType.Microsoft_AspNetCore_Mvc_ControllerAttribute) is { }) - { - AddAspNetCoreDescriptors(argumentDescriptors, propertyAccessDescriptors); - } - // TODO: Add descriptors for Asp.Net MVC 4.x + var (argumentDescriptors, propertyAccessDescriptors) = GetDescriptors(compilationStartContext.Compilation); if (argumentDescriptors.Any() || propertyAccessDescriptors.Any()) { compilationStartContext.RegisterSymbolStartAction(symbolStartContext => { // If the user overrides any action filters, model binding may not be working as expected. Then we do not want to raise on expressions that originate from parameters. + // See the OverridesController.Undecidable test cases for details. var hasOverrides = false; var controllerCandidates = new ConcurrentStack(); // In SymbolEnd, we filter the candidates based on the overriding we learn on the go. if (symbolStartContext.Symbol is INamedTypeSymbol namedType && namedType.IsControllerType()) { symbolStartContext.RegisterCodeBlockStartAction(codeBlockStart => - hasOverrides |= CheckCodeBlock(codeBlockStart, argumentTracker, propertyTracker, argumentDescriptors, propertyAccessDescriptors, controllerCandidates)); + hasOverrides |= RegisterCodeBlockActions(codeBlockStart, argumentTracker, propertyTracker, argumentDescriptors, propertyAccessDescriptors, controllerCandidates)); } symbolStartContext.RegisterSymbolEndAction(symbolEnd => { @@ -71,14 +66,15 @@ protected override void Initialize(SonarAnalysisContext context) }); } - private static bool CheckCodeBlock(SonarCodeBlockStartAnalysisContext codeBlockStart, + private static bool RegisterCodeBlockActions(SonarCodeBlockStartAnalysisContext codeBlockStart, CSharpArgumentTracker argumentTracker, CSharpPropertyAccessTracker propertyTracker, IReadOnlyList argumentDescriptors, IReadOnlyList propertyAccessDescriptors, ConcurrentStack controllerCandidates) { - var isOverride = codeBlockStart.OwningSymbol is IMethodSymbol method && IsOverridingFilterMethods(method); - if (isOverride) + if (codeBlockStart.OwningSymbol is IMethodSymbol method && IsOverridingFilterMethods(method)) { + // We do not want to raise in ActionFilter overrides. The SymbolEndAction needs to be made aware, that there are + // ActionFilter overrides, so it can filter out some candidates. return true; } // Within a single code block, access via constant and variable keys could be mixed @@ -120,6 +116,18 @@ private static bool CheckCodeBlock(SonarCodeBlockStartAnalysisContext ArgumentDescriptors, List PropertyAccessDescriptors) GetDescriptors(Compilation compilation) + { + var argumentDescriptors = new List(); + var propertyAccessDescriptors = new List(); + if (compilation.GetTypeByMetadataName(KnownType.Microsoft_AspNetCore_Mvc_ControllerAttribute) is { }) + { + AddAspNetCoreDescriptors(argumentDescriptors, propertyAccessDescriptors); + } + // TODO: Add descriptors for Asp.Net MVC 4.x + return (argumentDescriptors, propertyAccessDescriptors); + } + private static void AddAspNetCoreDescriptors(List argumentDescriptors, List propertyAccessDescriptors) { argumentDescriptors.Add(ArgumentDescriptor.ElementAccess(// Request.Form["id"] @@ -143,19 +151,18 @@ private static void AddAspNetCoreDescriptors(List argumentDe parameterConstraint: parameter => parameter.IsType(KnownType.System_String) && IsGetterParameter(parameter), argumentPosition: 0)); argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation(// Request.Headers.TryGetValue("id", out _) - invokedMethodSymbol: x => IsIDictionaryStringStringValuesInvocation(x, "TryGetValue"), + invokedMethodSymbol: x => IsIDictionaryStringStringValuesInvocation(x, "TryGetValue"), // TryGetValue is from IDictionary here. We check the type arguments. invokedMemberNameConstraint: (name, comparison) => string.Equals(name, "TryGetValue", comparison), - invokedMemberNodeConstraint: (model, language, invocation) => invocation is InvocationExpressionSyntax { Expression: { } expression } - && GetLeftOfDot(expression) is var left - && (left is null || (model.GetTypeInfo(left) is { Type: { } typeSymbol } && typeSymbol.Is(KnownType.Microsoft_AspNetCore_Http_IHeaderDictionary))), + invokedMemberNodeConstraint: IsAccessedViaHeaderDictionary, parameterConstraint: x => string.Equals(x.Name, "key", StringComparison.Ordinal), argumentListConstraint: (list, position) => list.Count == 2 && position == 0, refKind: null)); argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation(// Request.Headers.ContainsKey("id") invokedMethodSymbol: x => IsIDictionaryStringStringValuesInvocation(x, "ContainsKey"), invokedMemberNameConstraint: (name, comparison) => string.Equals(name, "ContainsKey", comparison), + invokedMemberNodeConstraint: IsAccessedViaHeaderDictionary, parameterConstraint: x => string.Equals(x.Name, "key", StringComparison.Ordinal), - argumentPosition: x => x == 0, + argumentListConstraint: (list, position) => list.Count == 1, refKind: null)); argumentDescriptors.Add(ArgumentDescriptor.ElementAccess(// Request.Query["id"] invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Http_IQueryCollection, @@ -181,6 +188,12 @@ private static void AddAspNetCoreDescriptors(List argumentDe propertyAccessDescriptors.Add(new(KnownType.Microsoft_AspNetCore_Http_IFormCollection, "Files")); // Request.Form.Files... } + // Check that the "Headers" expression in the Headers.TryGetValue("id", out _) invocation is of type IHeaderDictionary + private static bool IsAccessedViaHeaderDictionary(SemanticModel model, ILanguageFacade language, SyntaxNode invocation) => + invocation is InvocationExpressionSyntax { Expression: { } expression } + && GetLeftOfDot(expression) is { } left + && model.GetTypeInfo(left) is { Type: { } typeSymbol } && typeSymbol.Is(KnownType.Microsoft_AspNetCore_Http_IHeaderDictionary); + private static bool IsOverridingFilterMethods(IMethodSymbol method) => (method.GetOverriddenMember() ?? method).ExplicitOrImplicitInterfaceImplementations().Any(x => x is IMethodSymbol { ContainingType: { } container } && container.IsAny( diff --git a/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs b/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs index ce9b3470988..eecef364ca4 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs @@ -40,26 +40,5 @@ public class UseModelBindingTest [TestMethod] public void UseModelBinding_AspNetCore_CS() => builderAspNetCore.AddPaths("UseModelBinding_AspNetCore.cs").Verify(); - - [TestMethod] - public void UseModelBinding_AspNetCore_CS_Debug() => - builderAspNetCore - .WithConcurrentAnalysis(false) - .AddSnippet(""" - using Microsoft.AspNetCore.Http; - using Microsoft.AspNetCore.Mvc; - using Microsoft.AspNetCore.Mvc.Filters; - using System; - using System.Linq; - using System.Threading.Tasks; - - public class OverridesController : Controller - { - public void Action(IFormCollection form) - { - _ = form["id"]; // Compliant. Using IFormCollection is model binding - } - } - """).Verify(); #endif } From 460d2ca5921f379f138ec90e012eac6fc7504ba4 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Thu, 14 Mar 2024 10:49:54 +0100 Subject: [PATCH 45/62] Fix UT --- .../tests/SonarAnalyzer.Test/Facade/VisualBasicFacadeTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/analyzers/tests/SonarAnalyzer.Test/Facade/VisualBasicFacadeTest.cs b/analyzers/tests/SonarAnalyzer.Test/Facade/VisualBasicFacadeTest.cs index 7f765890eec..555296b2908 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Facade/VisualBasicFacadeTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Facade/VisualBasicFacadeTest.cs @@ -97,8 +97,8 @@ End Class var root = tree.GetRoot(); var argumentList = root.DescendantNodes().OfType().First(); var method = model.GetDeclaredSymbol(root.DescendantNodes().OfType().First()); - var actual = () => sut.MethodParameterLookup(argumentList, method); - actual.Should().Throw(); + var actual = sut.MethodParameterLookup(argumentList, method); + actual.Should().NotBeNull().And.BeOfType(); } [TestMethod] From 1b61e74dcc02f8bddc3d57ac2710afdb33e1d66c Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Thu, 14 Mar 2024 12:32:18 +0100 Subject: [PATCH 46/62] Use LanguageFacade --- .../Rules/UseModelBinding.cs | 25 +++++++++---------- .../Trackers/CSharpArgumentTracker.cs | 10 ++++---- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs index f8a7d336595..bbf83b8bcc6 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs @@ -24,22 +24,22 @@ namespace SonarAnalyzer.Rules.CSharp; [DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class UseModelBinding : SonarDiagnosticAnalyzer +public sealed class UseModelBinding : SonarDiagnosticAnalyzer { private const string DiagnosticId = "S6932"; private const string UseModelBindingMessage = "Use model binding instead of accessing the raw request data"; private const string UseIFormFileBindingMessage = "Use IFormFile or IFormFileCollection binding instead"; - private static readonly DiagnosticDescriptor Rule = DescriptorFactory.Create(DiagnosticId, "{0}"); + protected override string MessageFormat => "{0}"; - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + protected override ILanguageFacade Language => CSharpFacade.Instance; + + public UseModelBinding() : base(DiagnosticId) { } protected override void Initialize(SonarAnalysisContext context) { context.RegisterCompilationStartAction(compilationStartContext => { - var argumentTracker = new CSharpArgumentTracker(); - var propertyTracker = new CSharpPropertyAccessTracker(); var (argumentDescriptors, propertyAccessDescriptors) = GetDescriptors(compilationStartContext.Compilation); if (argumentDescriptors.Any() || propertyAccessDescriptors.Any()) { @@ -52,7 +52,7 @@ protected override void Initialize(SonarAnalysisContext context) if (symbolStartContext.Symbol is INamedTypeSymbol namedType && namedType.IsControllerType()) { symbolStartContext.RegisterCodeBlockStartAction(codeBlockStart => - hasOverrides |= RegisterCodeBlockActions(codeBlockStart, argumentTracker, propertyTracker, argumentDescriptors, propertyAccessDescriptors, controllerCandidates)); + hasOverrides |= RegisterCodeBlockActions(codeBlockStart, argumentDescriptors, propertyAccessDescriptors, controllerCandidates)); } symbolStartContext.RegisterSymbolEndAction(symbolEnd => { @@ -66,8 +66,7 @@ protected override void Initialize(SonarAnalysisContext context) }); } - private static bool RegisterCodeBlockActions(SonarCodeBlockStartAnalysisContext codeBlockStart, - CSharpArgumentTracker argumentTracker, CSharpPropertyAccessTracker propertyTracker, + private bool RegisterCodeBlockActions(SonarCodeBlockStartAnalysisContext codeBlockStart, IReadOnlyList argumentDescriptors, IReadOnlyList propertyAccessDescriptors, ConcurrentStack controllerCandidates) { @@ -87,7 +86,7 @@ private static bool RegisterCodeBlockActions(SonarCodeBlockStartAnalysisContext< { var argument = (ArgumentSyntax)nodeContext.Node; var context = new ArgumentContext(argument, nodeContext.SemanticModel); - if (allConstantAccess && argumentDescriptors.Any(x => argumentTracker.MatchArgument(x)(context))) + if (allConstantAccess && argumentDescriptors.Any(x => Language.Tracker.Argument.MatchArgument(x)(context))) { allConstantAccess &= nodeContext.SemanticModel.GetConstantValue(argument.Expression) is { HasValue: true, Value: string }; codeBlockCandidates.Push(new(UseModelBindingMessage, GetPrimaryLocation(argument), OriginatesFromParameter(nodeContext.SemanticModel, argument))); @@ -100,7 +99,7 @@ private static bool RegisterCodeBlockActions(SonarCodeBlockStartAnalysisContext< { var memberAccess = (MemberAccessExpressionSyntax)nodeContext.Node; var context = new PropertyAccessContext(memberAccess, nodeContext.SemanticModel, memberAccess.Name.Identifier.ValueText); - if (propertyTracker.MatchProperty([.. propertyAccessDescriptors])(context)) + if (Language.Tracker.PropertyAccess.MatchProperty([.. propertyAccessDescriptors])(context)) { codeBlockCandidates.Push(new(UseIFormFileBindingMessage, memberAccess.GetLocation(), OriginatesFromParameter(nodeContext.SemanticModel, memberAccess))); } @@ -133,7 +132,7 @@ private static void AddAspNetCoreDescriptors(List argumentDe argumentDescriptors.Add(ArgumentDescriptor.ElementAccess(// Request.Form["id"] invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Http_IFormCollection, invokedIndexerExpression: "Form", - parameterConstraint: parameter => parameter.IsType(KnownType.System_String) && IsGetterParameter(parameter), + parameterConstraint: _ => true, argumentPosition: 0)); argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation(// Request.Form.TryGetValue("id", out _) invokedType: KnownType.Microsoft_AspNetCore_Http_IFormCollection, @@ -162,12 +161,12 @@ private static void AddAspNetCoreDescriptors(List argumentDe invokedMemberNameConstraint: (name, comparison) => string.Equals(name, "ContainsKey", comparison), invokedMemberNodeConstraint: IsAccessedViaHeaderDictionary, parameterConstraint: x => string.Equals(x.Name, "key", StringComparison.Ordinal), - argumentListConstraint: (list, position) => list.Count == 1, + argumentListConstraint: (list, _) => list.Count == 1, refKind: null)); argumentDescriptors.Add(ArgumentDescriptor.ElementAccess(// Request.Query["id"] invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Http_IQueryCollection, invokedIndexerExpression: "Query", - parameterConstraint: parameter => parameter.IsType(KnownType.System_String) && IsGetterParameter(parameter), + parameterConstraint: _ => true, argumentPosition: 0)); argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation(// Request.Query.TryGetValue("id", out _) invokedType: KnownType.Microsoft_AspNetCore_Http_IQueryCollection, diff --git a/analyzers/src/SonarAnalyzer.CSharp/Trackers/CSharpArgumentTracker.cs b/analyzers/src/SonarAnalyzer.CSharp/Trackers/CSharpArgumentTracker.cs index 48880ca3f73..0ad20f6c12f 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Trackers/CSharpArgumentTracker.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Trackers/CSharpArgumentTracker.cs @@ -20,13 +20,13 @@ namespace SonarAnalyzer.Helpers.Trackers; -internal class CSharpArgumentTracker : ArgumentTracker +internal sealed class CSharpArgumentTracker : ArgumentTracker { - protected override SyntaxKind[] TrackedSyntaxKinds => new[] - { + protected override SyntaxKind[] TrackedSyntaxKinds => + [ SyntaxKind.AttributeArgument, SyntaxKind.Argument, - }; + ]; protected override ILanguageFacade Language => CSharpFacade.Instance; @@ -71,7 +71,7 @@ protected override bool InvokedMemberFits(SemanticModel model, SyntaxNode invoke { ObjectCreationExpressionSyntax { Type: { } typeName } => invokedMemberNameConstraint(typeName.GetName()), ConstructorInitializerSyntax x => FindClassNameFromConstructorInitializerSyntax(x) is not string name || invokedMemberNameConstraint(name), - { } ex when ImplicitObjectCreationExpressionSyntaxWrapper.IsInstance(ex) => invokedMemberNameConstraint(model.GetSymbolInfo(ex).Symbol?.ContainingType?.Name), + { } x when ImplicitObjectCreationExpressionSyntaxWrapper.IsInstance(x) => invokedMemberNameConstraint(model.GetSymbolInfo(x).Symbol?.ContainingType?.Name), _ => false, }, InvokedMemberKind.Indexer => invokedExpression switch From 259c7cf043d0f5dc0efad853d1fca82faebdfe3f Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Thu, 14 Mar 2024 12:38:44 +0100 Subject: [PATCH 47/62] Collection expression --- .../Trackers/VisualBasicArgumentTracker.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Trackers/VisualBasicArgumentTracker.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Trackers/VisualBasicArgumentTracker.cs index 7871103aebc..3365ae7ac76 100644 --- a/analyzers/src/SonarAnalyzer.VisualBasic/Trackers/VisualBasicArgumentTracker.cs +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Trackers/VisualBasicArgumentTracker.cs @@ -22,7 +22,7 @@ namespace SonarAnalyzer.Helpers.Trackers; public class VisualBasicArgumentTracker : ArgumentTracker { - protected override SyntaxKind[] TrackedSyntaxKinds => new[] { SyntaxKind.SimpleArgument }; + protected override SyntaxKind[] TrackedSyntaxKinds => [SyntaxKind.SimpleArgument]; protected override ILanguageFacade Language => VisualBasicFacade.Instance; From 6131b53448cdd7af981a3236f570fcb3af20c11d Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Thu, 14 Mar 2024 12:53:17 +0100 Subject: [PATCH 48/62] Add Request.RouteValues write test case and simplify descriptors. Pass descriptors as array to avoid re-allocations --- .../Rules/UseModelBinding.cs | 118 +++++++++--------- .../TestCases/UseModelBinding_AspNetCore.cs | 3 +- 2 files changed, 61 insertions(+), 60 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs index bbf83b8bcc6..5f8e614c9db 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs @@ -19,7 +19,6 @@ */ using System.Collections.Concurrent; -using SonarAnalyzer.Helpers.Trackers; namespace SonarAnalyzer.Rules.CSharp; @@ -67,7 +66,7 @@ protected override void Initialize(SonarAnalysisContext context) } private bool RegisterCodeBlockActions(SonarCodeBlockStartAnalysisContext codeBlockStart, - IReadOnlyList argumentDescriptors, IReadOnlyList propertyAccessDescriptors, + ArgumentDescriptor[] argumentDescriptors, MemberDescriptor[] propertyAccessDescriptors, ConcurrentStack controllerCandidates) { if (codeBlockStart.OwningSymbol is IMethodSymbol method && IsOverridingFilterMethods(method)) @@ -99,7 +98,7 @@ private bool RegisterCodeBlockActions(SonarCodeBlockStartAnalysisContext ArgumentDescriptors, List PropertyAccessDescriptors) GetDescriptors(Compilation compilation) + private static (ArgumentDescriptor[] ArgumentDescriptors, MemberDescriptor[] PropertyAccessDescriptors) GetDescriptors(Compilation compilation) { var argumentDescriptors = new List(); var propertyAccessDescriptors = new List(); @@ -124,65 +123,66 @@ private static (List ArgumentDescriptors, List argumentDescriptors, List propertyAccessDescriptors) { - argumentDescriptors.Add(ArgumentDescriptor.ElementAccess(// Request.Form["id"] - invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Http_IFormCollection, - invokedIndexerExpression: "Form", - parameterConstraint: _ => true, - argumentPosition: 0)); - argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation(// Request.Form.TryGetValue("id", out _) - invokedType: KnownType.Microsoft_AspNetCore_Http_IFormCollection, - methodName: "TryGetValue", - parameterName: "key", - argumentPosition: 0)); - argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation(// Request.Form.ContainsKey("id") - invokedType: KnownType.Microsoft_AspNetCore_Http_IFormCollection, - methodName: "ContainsKey", - parameterName: "key", - argumentPosition: 0)); - argumentDescriptors.Add(ArgumentDescriptor.ElementAccess(// Request.Headers["id"] - invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Http_IHeaderDictionary, - invokedIndexerExpression: "Headers", - parameterConstraint: parameter => parameter.IsType(KnownType.System_String) && IsGetterParameter(parameter), - argumentPosition: 0)); - argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation(// Request.Headers.TryGetValue("id", out _) - invokedMethodSymbol: x => IsIDictionaryStringStringValuesInvocation(x, "TryGetValue"), // TryGetValue is from IDictionary here. We check the type arguments. - invokedMemberNameConstraint: (name, comparison) => string.Equals(name, "TryGetValue", comparison), - invokedMemberNodeConstraint: IsAccessedViaHeaderDictionary, - parameterConstraint: x => string.Equals(x.Name, "key", StringComparison.Ordinal), - argumentListConstraint: (list, position) => list.Count == 2 && position == 0, - refKind: null)); - argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation(// Request.Headers.ContainsKey("id") - invokedMethodSymbol: x => IsIDictionaryStringStringValuesInvocation(x, "ContainsKey"), - invokedMemberNameConstraint: (name, comparison) => string.Equals(name, "ContainsKey", comparison), - invokedMemberNodeConstraint: IsAccessedViaHeaderDictionary, - parameterConstraint: x => string.Equals(x.Name, "key", StringComparison.Ordinal), - argumentListConstraint: (list, _) => list.Count == 1, - refKind: null)); - argumentDescriptors.Add(ArgumentDescriptor.ElementAccess(// Request.Query["id"] - invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Http_IQueryCollection, - invokedIndexerExpression: "Query", - parameterConstraint: _ => true, - argumentPosition: 0)); - argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation(// Request.Query.TryGetValue("id", out _) - invokedType: KnownType.Microsoft_AspNetCore_Http_IQueryCollection, - methodName: "TryGetValue", - parameterName: "key", - argumentPosition: 0)); - argumentDescriptors.Add(ArgumentDescriptor.ElementAccess(// Request.RouteValues["id"] - invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Routing_RouteValueDictionary, - invokedIndexerExpression: "RouteValues", - parameterConstraint: parameter => parameter.IsType(KnownType.System_String) && IsGetterParameter(parameter), - argumentPosition: 0)); - argumentDescriptors.Add(ArgumentDescriptor.MethodInvocation(// Request.RouteValues.TryGetValue("id", out _) - invokedType: KnownType.Microsoft_AspNetCore_Routing_RouteValueDictionary, - methodName: "TryGetValue", - parameterName: "key", - argumentPosition: 0)); + argumentDescriptors.AddRange([ + ArgumentDescriptor.ElementAccess(// Request.Form["id"] + invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Http_IFormCollection, + invokedIndexerExpression: "Form", + parameterConstraint: _ => true, // There is only a single overload and it is getter only + argumentPosition: 0), + ArgumentDescriptor.MethodInvocation(// Request.Form.TryGetValue("id", out _) + invokedType: KnownType.Microsoft_AspNetCore_Http_IFormCollection, + methodName: "TryGetValue", + parameterName: "key", + argumentPosition: 0), + ArgumentDescriptor.MethodInvocation(// Request.Form.ContainsKey("id") + invokedType: KnownType.Microsoft_AspNetCore_Http_IFormCollection, + methodName: "ContainsKey", + parameterName: "key", + argumentPosition: 0), + ArgumentDescriptor.ElementAccess(// Request.Headers["id"] + invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Http_IHeaderDictionary, + invokedIndexerExpression: "Headers", + parameterConstraint: IsGetterParameter, // Headers are read/write + argumentPosition: 0), + ArgumentDescriptor.MethodInvocation(// Request.Headers.TryGetValue("id", out _) + invokedMethodSymbol: x => IsIDictionaryStringStringValuesInvocation(x, "TryGetValue"), // TryGetValue is from IDictionary here. We check the type arguments. + invokedMemberNameConstraint: (name, comparison) => string.Equals(name, "TryGetValue", comparison), + invokedMemberNodeConstraint: IsAccessedViaHeaderDictionary, + parameterConstraint: x => string.Equals(x.Name, "key", StringComparison.Ordinal), + argumentListConstraint: (list, position) => list.Count == 2 && position == 0, + refKind: RefKind.None), + ArgumentDescriptor.MethodInvocation(// Request.Headers.ContainsKey("id") + invokedMethodSymbol: x => IsIDictionaryStringStringValuesInvocation(x, "ContainsKey"), + invokedMemberNameConstraint: (name, comparison) => string.Equals(name, "ContainsKey", comparison), + invokedMemberNodeConstraint: IsAccessedViaHeaderDictionary, + parameterConstraint: x => string.Equals(x.Name, "key", StringComparison.Ordinal), + argumentListConstraint: (list, _) => list.Count == 1, + refKind: RefKind.None), + ArgumentDescriptor.ElementAccess(// Request.Query["id"] + invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Http_IQueryCollection, + invokedIndexerExpression: "Query", + parameterConstraint: _ => true, // There is only a single overload and it is getter only + argumentPosition: 0), + ArgumentDescriptor.MethodInvocation(// Request.Query.TryGetValue("id", out _) + invokedType: KnownType.Microsoft_AspNetCore_Http_IQueryCollection, + methodName: "TryGetValue", + parameterName: "key", + argumentPosition: 0), + ArgumentDescriptor.ElementAccess(// Request.RouteValues["id"] + invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Routing_RouteValueDictionary, + invokedIndexerExpression: "RouteValues", + parameterConstraint: IsGetterParameter, // RouteValues are read/write + argumentPosition: 0), + ArgumentDescriptor.MethodInvocation(// Request.RouteValues.TryGetValue("id", out _) + invokedType: KnownType.Microsoft_AspNetCore_Routing_RouteValueDictionary, + methodName: "TryGetValue", + parameterName: "key", + argumentPosition: 0)]); propertyAccessDescriptors.Add(new(KnownType.Microsoft_AspNetCore_Http_IFormCollection, "Files")); // Request.Form.Files... } diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs index e1428adee5f..1c80c47738a 100644 --- a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs +++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs @@ -92,9 +92,10 @@ void FormCollection(IFormCollection form) _ = form["id"]; // Compliant. Using IFormCollection is model binding } - void HeaderAccess() + void WriteAccess() { Request.Headers["id"] = "Assignment"; // Compliant + Request.RouteValues["id"] = "Assignment"; // Compliant } // Parameterized for Form, Headers, Query, RouteValues / Request, this.Request, ControllerContext.HttpContext.Request / [FromForm], [FromQuery], [FromRoute], [FromHeader] From ae757708d0eb5a4bd954e2bcac827e4bf509f862 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Thu, 14 Mar 2024 15:55:57 +0100 Subject: [PATCH 49/62] Move Compliant() and NoncompliantKeyVariations to parameterized tests. --- .../Rules/UseModelBinding.cs | 2 +- .../Rules/UseModelBindingTest.cs | 66 +++++++++++++++++++ .../TestCases/UseModelBinding_AspNetCore.cs | 37 ++--------- 3 files changed, 74 insertions(+), 31 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs index 5f8e614c9db..c8b177f646e 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs @@ -154,7 +154,7 @@ private static void AddAspNetCoreDescriptors(List argumentDe invokedMemberNameConstraint: (name, comparison) => string.Equals(name, "TryGetValue", comparison), invokedMemberNodeConstraint: IsAccessedViaHeaderDictionary, parameterConstraint: x => string.Equals(x.Name, "key", StringComparison.Ordinal), - argumentListConstraint: (list, position) => list.Count == 2 && position == 0, + argumentListConstraint: (list, position) => list.Count == 2 && position is 0 or null, refKind: RefKind.None), ArgumentDescriptor.MethodInvocation(// Request.Headers.ContainsKey("id") invokedMethodSymbol: x => IsIDictionaryStringStringValuesInvocation(x, "ContainsKey"), diff --git a/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs b/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs index eecef364ca4..dba5fe0a171 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs @@ -40,5 +40,71 @@ public class UseModelBindingTest [TestMethod] public void UseModelBinding_AspNetCore_CS() => builderAspNetCore.AddPaths("UseModelBinding_AspNetCore.cs").Verify(); + + [DataTestMethod] + [DataRow("Form")] + [DataRow("Query")] + [DataRow("RouteValues")] + [DataRow("Headers")] + public void NonCompliantAccess(string property) => + builderAspNetCore.AddSnippet($$"""" + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Filters; + using System; + using System.Linq; + using System.Threading.Tasks; + + public class TestController : Controller + { + async Task NoncompliantKeyVariations() + { + _ = Request.{{property}}[@"key"]; // Noncompliant + _ = Request.{{property}}.TryGetValue(@"key", out _); // Noncompliant + _ = Request.{{property}}["""key"""]; // Noncompliant + _ = Request.{{property}}.TryGetValue("""key""", out _); // Noncompliant + + const string key = "id"; + _ = Request.{{property}}[key]; // Noncompliant + _ = Request.{{property}}.TryGetValue(key, out _); // Noncompliant + _ = Request.{{property}}[$"prefix.{key}"]; // Noncompliant + _ = Request.{{property}}.TryGetValue($"prefix.{key}", out _); // Noncompliant + _ = Request.{{property}}[$"""prefix.{key}"""]; // Noncompliant + _ = Request.{{property}}.TryGetValue($"""prefix.{key}""", out _); // Noncompliant + + _ = Request.{{property}}[key: "id"]; // Noncompliant + _ = Request.{{property}}.TryGetValue(value: out _, key: "id"); // Noncompliant + } + } + """").Verify(); + + [TestMethod] + [CombinatorialData] + public void CompliantAccess( + [DataValues( + "_ = {0}.Keys", + "_ = {0}.Count", + "foreach (var kvp in {0}) {{ }}", + "_ = {0}.Select(x => x);", + "_ = {0}[key]; // Compliant: The accessed key is not a compile time constant")] string statementFormat, + [DataValues("Request", "this.Request", "ControllerContext.HttpContext.Request", "request")] string request, + [DataValues("Form", "Headers", "Query", "RouteValues")] string property, + [DataValues("[FromForm]", "[FromQuery]", "[FromRoute]", "[FromHeader]")] string attribute) => + builderAspNetCore.AddSnippet($$""" + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Filters; + using System; + using System.Linq; + using System.Threading.Tasks; + + public class TestController : Controller + { + async Task Compliant({{attribute}} string key, HttpRequest request) + { + {{string.Format(statementFormat, $"{request}.{property}")}}; + } + } + """).Verify(); #endif } diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs index 1c80c47738a..94ffb2b3443 100644 --- a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs +++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs @@ -48,26 +48,6 @@ public IActionResult Post() return default; } - // Parameterized for "Form", "Query", "RouteValues", "Headers" - void NoncompliantKeyVariations() - { - _ = Request.Form[@"key"]; // Noncompliant - _ = Request.Form.TryGetValue(@"key", out _); // Noncompliant - _ = Request.Form["""key"""]; // Noncompliant - _ = Request.Form.TryGetValue("""key""", out _); // Noncompliant - - const string key = "id"; - _ = Request.Form[key]; // Noncompliant - _ = Request.Form.TryGetValue(key, out _); // Noncompliant - _ = Request.Form[$"prefix.{key}"]; // Noncompliant - _ = Request.Form.TryGetValue($"prefix.{key}", out _); // Noncompliant - _ = Request.Form[$"""prefix.{key}"""]; // Noncompliant - _ = Request.Form.TryGetValue($"""prefix.{key}""", out _); // Noncompliant - - _ = Request.Form[key: "id"]; // Noncompliant - _ = Request.Form.TryGetValue(value: out _, key: "id"); // Noncompliant - } - void MixedAccess_Form(string key) { _ = Request.Form["id"]; // Compliant (a mixed access with constant and non-constant keys is compliant) @@ -98,19 +78,16 @@ void WriteAccess() Request.RouteValues["id"] = "Assignment"; // Compliant } - // Parameterized for Form, Headers, Query, RouteValues / Request, this.Request, ControllerContext.HttpContext.Request / [FromForm], [FromQuery], [FromRoute], [FromHeader] - // Implementation: Consider adding a CombinatorialDataAttribute https://stackoverflow.com/a/75531690 - async Task Compliant([FromForm] string key) + async Task Compliant() { - _ = Request.Form.Keys; - _ = Request.Form.Count; - foreach (var kvp in Request.Form) - { } - _ = Request.Form.Select(x => x); - _ = Request.Form[key]; // Compliant: The accessed key is not a compile time constant _ = Request.Cookies["cookie"]; // Compliant: Cookies are not bound by default _ = Request.QueryString; // Compliant: Accessing the whole raw string is fine. - _ = await Request.ReadFormAsync(); // Compliant: This might be used for optimization purposes e.g. conditional form value access. + } + + async Task CompliantFormAccess() + { + var form = await Request.ReadFormAsync(); // Compliant: This might be used for optimization purposes e.g. conditional form value access. + _ = form["id"]; } // parameterized test: parameters are the different forbidden Request accesses (see above) From 173fb53041f79b1a01ce1bcf49ff702d65870c33 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Thu, 14 Mar 2024 15:56:06 +0100 Subject: [PATCH 50/62] Add CombinatorialDataAttribute --- .../Common/CombinatorialDataAttributeTest.cs | 128 ++++++++++++++++++ .../Common/CombinatorialDataAttribute.cs | 87 ++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 analyzers/tests/SonarAnalyzer.TestFramework.Test/Common/CombinatorialDataAttributeTest.cs create mode 100644 analyzers/tests/SonarAnalyzer.TestFramework/Common/CombinatorialDataAttribute.cs diff --git a/analyzers/tests/SonarAnalyzer.TestFramework.Test/Common/CombinatorialDataAttributeTest.cs b/analyzers/tests/SonarAnalyzer.TestFramework.Test/Common/CombinatorialDataAttributeTest.cs new file mode 100644 index 00000000000..7d7d87d9a75 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.TestFramework.Test/Common/CombinatorialDataAttributeTest.cs @@ -0,0 +1,128 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2024 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarAnalyzer.Test.TestFramework.Tests.Common; + +[TestClass] + +public class CombinatorialDataAttributeTest_TwoDimensions +{ + private static List<(int X, int Y)> combinations; + + [ClassInitialize] + public static void Initialize(TestContext context) + { + combinations = new(); + } + + [TestMethod] + [CombinatorialData] +#pragma warning disable S2699 // Tests should include assertions. Assertion happens in cleanup + public void Combinatorial([DataValues(1, 2, 3)] int x, [DataValues(-1, -2, -3)] int y) +#pragma warning restore S2699 + { + combinations.Add((x, y)); + } + + [ClassCleanup] + public static void Cleanup() + { + combinations.Should().BeEquivalentTo([ + (1, -1), + (1, -2), + (1, -3), + (2, -1), + (2, -2), + (2, -3), + (3, -1), + (3, -2), + (3, -3), + ]); + } +} + +[TestClass] +public class CombinatorialDataAttributeTest_ThreeDimensions +{ + private static List<(int X, string Y, bool Z)> combinations; + + [ClassInitialize] + public static void Initialize(TestContext context) + { + combinations = new(); + } + + [TestMethod] + [CombinatorialData] +#pragma warning disable S2699 // Tests should include assertions. Assertion happens in cleanup + public void Combinatorial([DataValues(1, 2, 3)] int x, [DataValues("A", "B")] string y, [DataValues(true, false)] bool z) +#pragma warning restore S2699 + { + combinations.Add((x, y, z)); + } + + [ClassCleanup] + public static void Cleanup() + { + combinations.Should().BeEquivalentTo([ + (1, "A", true), + (1, "B", true), + (1, "A", false), + (1, "B", false), + (2, "A", true), + (2, "B", true), + (2, "A", false), + (2, "B", false), + (3, "A", true), + (3, "B", true), + (3, "A", false), + (3, "B", false), + ]); + } +} + +[TestClass] +public class CombinatorialDataAttributeTest_AttributeTest +{ + [TestMethod] + public void CombinatorialData() + { + var attribute = new CombinatorialDataAttribute(); + var data = attribute.GetData(typeof(CombinatorialDataAttributeTest_AttributeTest).GetMethod(nameof(Combinatorial))); + data.Should().BeEquivalentTo([ + [1, "A", true], + [1, "B", true], + [1, "A", false], + [1, "B", false], + [2, "A", true], + [2, "B", true], + [2, "A", false], + [2, "B", false], + [3, "A", true], + [3, "B", true], + [3, "A", false], + [3, "B", false], + ]); + } + public void Combinatorial([DataValues(1, 2, 3)] int x, [DataValues("A", "B")] string y, [DataValues(true, false)] bool z) + { + + } +} diff --git a/analyzers/tests/SonarAnalyzer.TestFramework/Common/CombinatorialDataAttribute.cs b/analyzers/tests/SonarAnalyzer.TestFramework/Common/CombinatorialDataAttribute.cs new file mode 100644 index 00000000000..704c28e61e1 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.TestFramework/Common/CombinatorialDataAttribute.cs @@ -0,0 +1,87 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2024 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Globalization; +using System.Reflection; + +namespace SonarAnalyzer.TestFramework.Common; + +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] +public sealed class DataValuesAttribute : Attribute +{ + public object[] Values { get; } + + public DataValuesAttribute(params object[] values) + { + Values = values; + } +} + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +public sealed class CombinatorialDataAttribute : Attribute, ITestDataSource +{ + public IEnumerable GetData(MethodInfo methodInfo) + { + var valuesPerParameter = methodInfo.GetParameters().Select(p => p.GetCustomAttribute()?.Values + ?? throw new InvalidOperationException("Combinatorial test requires all parameters to have the [DataValues] attribute set")).ToArray(); + var parameterIndices = new int[valuesPerParameter.Length]; + + while (true) + { + // Create new arguments + var arg = new object[parameterIndices.Length]; + for (var i = 0; i < parameterIndices.Length; i++) + { + arg[i] = valuesPerParameter[i][parameterIndices[i]]; + } + + yield return arg; + + // Increment indices + for (int i = parameterIndices.Length - 1; i >= 0; i--) + { + parameterIndices[i]++; + if (parameterIndices[i] >= valuesPerParameter[i].Length) + { + parameterIndices[i] = 0; + + if (i == 0) + yield break; + } + else + break; + } + } + } + + public string GetDisplayName(MethodInfo methodInfo, object[] data) + { + if (data != null) + { + return string.Format(CultureInfo.CurrentCulture, "{0} ({1})", new object[2] + { + methodInfo.Name, + string.Join(",", data) + }); + } + + return null!; + } +} From 7efce244bd86cf6f952004e373ab721ef0d9db19 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Thu, 14 Mar 2024 17:41:19 +0100 Subject: [PATCH 51/62] Move remaining parameterized tests --- .../Rules/UseModelBindingTest.cs | 126 ++++++++++++++++++ .../TestCases/UseModelBinding_AspNetCore.cs | 49 +------ 2 files changed, 127 insertions(+), 48 deletions(-) diff --git a/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs b/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs index dba5fe0a171..1eff86b6d5e 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs @@ -75,6 +75,19 @@ async Task NoncompliantKeyVariations() _ = Request.{{property}}[key: "id"]; // Noncompliant _ = Request.{{property}}.TryGetValue(value: out _, key: "id"); // Noncompliant } + + private static void HandleRequest(HttpRequest request) + { + _ = request.{{property}}["id"]; // Noncompliant: Containing type is a controller + void LocalFunction() + { + _ = request.{{property}}["id"]; // Noncompliant: Containing type is a controller + } + static void StaticLocalFunction(HttpRequest request) + { + _ = request.{{property}}["id"]; // Noncompliant: Containing type is a controller + } + } } """").Verify(); @@ -106,5 +119,118 @@ async Task Compliant({{attribute}} string key, HttpRequest request) } } """).Verify(); + + [DataTestMethod] + [DataRow("public class MyController: Controller")] + [DataRow("public class MyController: ControllerBase")] + [DataRow("[Controller] public class My: Controller")] + // [DataRow("public class MyController")] FN: Poco controller are not detected + public void PocoController(string classDeclaration) => + builderAspNetCore.AddSnippet($$"""" + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Filters; + using System; + using System.Linq; + using System.Threading.Tasks; + + {{classDeclaration}} + { + public async Task Action([FromServices]IHttpContextAccessor httpContextAccessor) + { + _ = httpContextAccessor.HttpContext.Request.Form["id"]; // Noncompliant + } + } + """").Verify(); + + [DataTestMethod] + [DataRow("public class My")] + [DataRow("[NonController] public class My: Controller")] + [DataRow("[NonController] public class MyController: Controller")] + public void NoController(string classDeclaration) => + builderAspNetCore.AddSnippet($$"""" + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Filters; + using System; + using System.Linq; + using System.Threading.Tasks; + + {{classDeclaration}} + { + public async Task Action([FromServices]IHttpContextAccessor httpContextAccessor) + { + _ = httpContextAccessor.HttpContext.Request.Form["id"]; // Compliant + } + } + """").Verify(); + + [DataTestMethod] + [DataRow("Form")] + [DataRow("Headers")] + [DataRow("Query")] + [DataRow("RouteValues")] + public void NoControllerHelpers(string property) => + builderAspNetCore.AddSnippet($$"""" + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Filters; + using System; + using System.Linq; + using System.Threading.Tasks; + + static class HttpRequestExtensions + { + public static void Ext(this HttpRequest request) + { + _ = request.{{property}}["id"]; // Compliant: Not in a controller + } + } + + class RequestService + { + public HttpRequest Request { get; } + + public void HandleRequest(HttpRequest request) + { + _ = Request.{{property}}["id"]; // Compliant: Not in a controller + _ = request.{{property}}["id"]; // Compliant: Not in a controller + } + } + """").Verify(); + + [TestMethod] + [CombinatorialData] + public void InheritanceAccess( + [DataValues( + ": Controller", + ": ControllerBase", + ": MyBaseController", + ": MyBaseBaseController")]string baseList, + [DataValues( + """_ = Request.Form["id"]""", + """_ = Request.Form.TryGetValue("id", out var _)""", + """_ = Request.Headers["id"]""", + """_ = Request.Query["id"]""", + """_ = Request.RouteValues["id"]""")]string nonCompliantStatement) => + builderAspNetCore.AddSnippet($$"""" + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Filters; + using System; + using System.Linq; + using System.Threading.Tasks; + + public class MyBaseController : ControllerBase { } + public class MyBaseBaseController : MyBaseController { } + + public class MyTestController {{baseList}} + { + public void Action() + { + {{nonCompliantStatement}}; // Noncompliant + } + } + """").Verify(); #endif } diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs index 94ffb2b3443..6857dbc8074 100644 --- a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs +++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs @@ -89,20 +89,6 @@ async Task CompliantFormAccess() var form = await Request.ReadFormAsync(); // Compliant: This might be used for optimization purposes e.g. conditional form value access. _ = form["id"]; } - - // parameterized test: parameters are the different forbidden Request accesses (see above) - private static void HandleRequest(HttpRequest request) - { - _ = request.Form["id"]; // Noncompliant: Containing type is a controller - void LocalFunction() - { - _ = request.Form["id"]; // Noncompliant: Containing type is a controller - } - static void StaticLocalFunction(HttpRequest request) - { - _ = request.Form["id"]; // Noncompliant: Containing type is a controller - } - } } public class CodeBlocksController : Controller @@ -167,18 +153,6 @@ void M4() } -// parameterized test: Repeat for Controller, ControllerBase, MyBaseController, MyBaseBaseController base classes -// consider adding "PageModel" to the parametrized test but functional tests and updates to the RSpec are needed. -public class MyBaseController : ControllerBase { } -public class MyBaseBaseController : MyBaseController { } -public class MyTestController : MyBaseBaseController -{ - public void Action() - { - _ = Request.Form["id"]; // Noncompliant - } -} - public class OverridesController : Controller { public void Action() @@ -210,8 +184,7 @@ public override Task OnActionExecutionAsync(ActionExecutingContext context, Acti } } -// parameterized test for PocoController, [Controller]Poco -// consider adding "PageModel" to the parametrized test but functional tests and updates to the RSpec are needed. +[Controller] public class PocoController : IActionFilter, IAsyncActionFilter { public void OnActionExecuted(ActionExecutedContext context) @@ -241,23 +214,3 @@ Task IAsyncActionFilter.OnActionExecutionAsync(ActionExecutingContext context, A return Task.CompletedTask; } } - -static class HttpRequestExtensions -{ - // parameterized test: parameters are the different forbidden Request accesses (see above) - public static void Ext(this HttpRequest request) - { - _ = request.Form["id"]; // Compliant: Not in a controller - } -} - -class RequestService -{ - public HttpRequest Request { get; } - // parameterized test: parameters are the different forbidden Request accesses (see above) - public void HandleRequest(HttpRequest request) - { - _ = Request.Form["id"]; // Compliant: Not in a controller - _ = request.Form["id"]; // Compliant: Not in a controller - } -} From c1d3ad06b89d6ec28750e3367f027e52ab9cc023 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Thu, 14 Mar 2024 18:21:48 +0100 Subject: [PATCH 52/62] Fix after re-base --- ...mpilationStartAnalysisContextExtensions.cs | 52 ++-- .../SymbolStartAnalysisContextWrapper.cs | 8 - .../RegisterSymbolStartActionWrapperTest.cs | 259 +----------------- 3 files changed, 29 insertions(+), 290 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs index 7b3a25a5cfb..0bba5ec874e 100644 --- a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs +++ b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/CompilationStartAnalysisContextExtensions.cs @@ -23,39 +23,43 @@ namespace SonarAnalyzer.ShimLayer.AnalysisContext; -// Code is executed in static initializers and is not detected by the coverage tool -// See the SonarAnalysisContextTest.SonarCompilationStartAnalysisContext_RegisterSymbolStartAction family of tests to check test coverage manually -[ExcludeFromCodeCoverage] public static class CompilationStartAnalysisContextExtensions { + private static readonly Action, SymbolKind> RegisterSymbolStartActionWrapper = + CreateRegisterSymbolStartAnalysisWrapper(); + + public static void RegisterSymbolStartAction(this CompilationStartAnalysisContext context, Action action, SymbolKind symbolKind) => + RegisterSymbolStartActionWrapper(context, action, symbolKind); + + // Code is executed in static initializers and is not detected by the coverage tool + // See the SonarAnalysisContextTest.SonarCompilationStartAnalysisContext_RegisterSymbolStartAction family of tests to check test coverage manually + [ExcludeFromCodeCoverage] + private static Action, SymbolKind> CreateRegisterSymbolStartAnalysisWrapper() { if (typeof(CompilationStartAnalysisContext).GetMethod(nameof(RegisterSymbolStartAction)) is not { } registerMethod) { return static (_, _, _) => { }; } - var contextParameter = Parameter(typeof(CompilationStartAnalysisContext)); - var symbolKindParameter = Parameter(typeof(SymbolKind)); - var symbolStartAnalysisContextCtor = typeof(SymbolStartAnalysisContext).GetConstructors().Single(); - PassThroughLambda(nameof(SymbolStartAnalysisContext.RegisterCodeBlockAction))))), symbolStartAnalysisContextParameter); + var contextParameter = Parameter(typeof(CompilationStartAnalysisContext)); + var shimmedActionParameter = Parameter(typeof(Action)); + var symbolKindParameter = Parameter(typeof(SymbolKind)); - contextParameter, shimmedActionParameter, symbolKindParameter).Compile(); - } - else - { - var registerActionParameter = Parameter(typeof(Action)); - return Lambda>>(Call(symbolStartAnalysisContextParameter, registrationMethodName, typeArguments, registerActionParameter), registerActionParameter); - } + var roslynSymbolStartAnalysisContextType = typeof(CompilationStartAnalysisContext).Assembly.GetType("Microsoft.CodeAnalysis.Diagnostics.SymbolStartAnalysisContext"); + var roslynSymbolStartAnalysisActionType = typeof(Action<>).MakeGenericType(roslynSymbolStartAnalysisContextType); + var roslynSymbolStartAnalysisContextParameter = Parameter(roslynSymbolStartAnalysisContextType); + var sonarSymbolStartAnalysisContextCtor = typeof(SymbolStartAnalysisContextWrapper).GetConstructors().Single(); - // (registerActionParameter, additionalParameter) => symbolStartAnalysisContextParameter."registrationMethodName"(registerActionParameter, additionalParameter) - static Expression, TParameter>> RegisterLambdaWithAdditionalParameter( - ParameterExpression symbolStartAnalysisContextParameter, string registrationMethodName, params Type[] typeArguments) - { - var registerActionParameter = Parameter(typeof(Action)); - var additionalParameter = Parameter(typeof(TParameter)); - return Lambda, TParameter>>( - Call(symbolStartAnalysisContextParameter, registrationMethodName, typeArguments, registerActionParameter, additionalParameter), registerActionParameter, additionalParameter); - } -#pragma warning restore S103 // Lines should not be too long + // Action registerAction = roslynSymbolStartAnalysisContextParameter => + // shimmedActionParameter.Invoke(new Sonar.SymbolStartAnalysisContextWrapper(roslynSymbolStartAnalysisContextParameter)) + var registerAction = Lambda( + delegateType: roslynSymbolStartAnalysisActionType, + body: Call(shimmedActionParameter, nameof(Action.Invoke), [], New(sonarSymbolStartAnalysisContextCtor, roslynSymbolStartAnalysisContextParameter)), + parameters: roslynSymbolStartAnalysisContextParameter); + + // (contextParameter, shimmedActionParameter, symbolKindParameter) => contextParameter.RegisterSymbolStartAction(registerAction, symbolKindParameter) + return Lambda, SymbolKind>>( + Call(contextParameter, registerMethod, registerAction, symbolKindParameter), + contextParameter, shimmedActionParameter, symbolKindParameter).Compile(); } } diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContextWrapper.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContextWrapper.cs index d71da9b4a95..9a52b150fc6 100644 --- a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContextWrapper.cs +++ b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/AnalysisContext/SymbolStartAnalysisContextWrapper.cs @@ -25,9 +25,6 @@ namespace SonarAnalyzer.ShimLayer.AnalysisContext; -// Code is executed in static initializers and is not detected by the coverage tool -// See the RegisterSymbolStartActionWrapperTest family of tests to check test coverage manually -[ExcludeFromCodeCoverage] public readonly struct SymbolStartAnalysisContextWrapper { private static readonly Func CancellationTokenAccessor; @@ -45,11 +42,6 @@ public readonly struct SymbolStartAnalysisContextWrapper private static readonly Action, ImmutableArray> RegisterSyntaxNodeActionCS; private static readonly Action, ImmutableArray> RegisterSyntaxNodeActionVB; - public CancellationToken CancellationToken => CancellationTokenAccessor(RoslynSymbolStartAnalysisContext); - public Compilation Compilation => CompilationAccessor(RoslynSymbolStartAnalysisContext); - public AnalyzerOptions Options => OptionsAccessor(RoslynSymbolStartAnalysisContext); - public ISymbol Symbol => SymbolAccessor(RoslynSymbolStartAnalysisContext); - private object RoslynSymbolStartAnalysisContext { get; } public CancellationToken CancellationToken => CancellationTokenAccessor(RoslynSymbolStartAnalysisContext); public Compilation Compilation => CompilationAccessor(RoslynSymbolStartAnalysisContext); public AnalyzerOptions Options => OptionsAccessor(RoslynSymbolStartAnalysisContext); diff --git a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs index df7f94e32ef..4b9e0b4c5c1 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs @@ -52,31 +52,6 @@ public class C diagnostics.Should().BeEmpty(); } - [TestMethod] - public async Task RegisterSymbolStartAction_SymbolStartProperties() - { - var snippet = new SnippetCompiler(""" - public class C - { - int i = 0; - public void M() => ToString(); - } - """); - var symbolStartWasCalled = false; - var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create( - new TestDiagnosticAnalyzer(symbolStart => - { - symbolStart.CancellationToken.IsCancellationRequested.Should().BeFalse(); - symbolStart.Compilation.SyntaxTrees.Should().ContainSingle(); - symbolStart.Options.Should().NotBeNull(); - symbolStart.Symbol.Should().BeAssignableTo().Which.Name.Should().Be("C"); - symbolStartWasCalled = true; - }, SymbolKind.NamedType))); - var diagnostics = await compilation.GetAnalyzerDiagnosticsAsync(); - symbolStartWasCalled.Should().BeTrue(); - diagnostics.Should().BeEmpty(); - } - [TestMethod] public async Task RegisterSymbolStartAction_RegisterCodeBlockAction() { @@ -140,238 +115,6 @@ public async Task RegisterSymbolStartAction_RegisterCodeBlockStartAction_CS() { var snippet = new SnippetCompiler(""" public class C - { - int i = 0; - public void M() => ToString(); - } - """); - var visited = new List(); - var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create( - new TestDiagnosticAnalyzer(symbolStart => - { - symbolStart.RegisterCodeBlockStartAction(blockStart => - { - var node = blockStart.CodeBlock.ToString(); - visited.Add(node); - blockStart.RegisterSyntaxNodeAction(nodeContext => visited.Add(nodeContext.Node.ToString()), CS.SyntaxKind.InvocationExpression); - }); - }, SymbolKind.NamedType))); - await compilation.GetAnalyzerDiagnosticsAsync(); - visited.Should().BeEquivalentTo("int i = 0;", "public void M() => ToString();", "ToString()"); - } - public override ImmutableArray SupportedDiagnostics => - ImmutableArray.Create(new DiagnosticDescriptor("TEST", "Test", "Test", "Test", DiagnosticSeverity.Warning, true)); - - [TestMethod] - public async Task RegisterSymbolStartAction_RegisterCodeBlockStartAction_VB() - { - var snippet = new SnippetCompiler(""" - Public Class C - Private i As Integer = 0 - - Public Sub M() - Call ToString() - End Sub - End Class - """, ignoreErrors: false, AnalyzerLanguage.VisualBasic); - var visited = new List(); - var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create( - new TestDiagnosticAnalyzer(symbolStart => - { - symbolStart.RegisterCodeBlockStartAction(blockStart => - { - var node = blockStart.CodeBlock.ToString(); - visited.Add(node); - blockStart.RegisterSyntaxNodeAction(nodeContext => visited.Add(nodeContext.Node.ToString()), VB.SyntaxKind.InvocationExpression); - }); - }, SymbolKind.NamedType))); - await compilation.GetAnalyzerDiagnosticsAsync(); - visited.Should().BeEquivalentTo([ - """Private i As Integer = 0""", - """ - Public Sub M() - Call ToString() - End Sub - """, - """ToString()"""]); - } - - [TestMethod] - public async Task RegisterSymbolStartAction_RegisterOperationAction() - { - var snippet = new SnippetCompiler(""" - public class C - { - int i = 0; - public void M() - { - ToString(); - } - } - """); - var visited = new List(); - var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create( - new TestDiagnosticAnalyzer(symbolStart => - { - symbolStart.RegisterOperationAction(operationContext => - { - var operation = operationContext.Operation.Syntax.ToString(); - visited.Add(operation); - }, ImmutableArray.Create(OperationKind.Invocation)); - }, SymbolKind.NamedType))); - await compilation.GetAnalyzerDiagnosticsAsync(); - visited.Should().BeEquivalentTo("ToString()"); - } - - [TestMethod] - public async Task RegisterSymbolStartAction_RegisterOperationBlockAction() - { - var snippet = new SnippetCompiler(""" - public class C - { - int i = 0; - public void M() => ToString(); - } - """); - var visited = new List(); - var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create( - new TestDiagnosticAnalyzer(symbolStart => - { - symbolStart.RegisterOperationBlockAction(operationBlockContext => - { - var operation = operationBlockContext.OperationBlocks.First().Syntax.ToString(); - visited.Add(operation); - }); - }, SymbolKind.NamedType))); - await compilation.GetAnalyzerDiagnosticsAsync(); - visited.Should().BeEquivalentTo("= 0", "=> ToString()"); - } - - [TestMethod] - public async Task RegisterSymbolStartAction_RegisterOperationBlockStartAction() - { - var snippet = new SnippetCompiler(""" - public class C - { - int i = 0; - public void M() => ToString(); - } - """); - var visited = new List(); - var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create( - new TestDiagnosticAnalyzer(symbolStart => - { - symbolStart.RegisterOperationBlockStartAction(operationBlockStartContext => - { - var operation = operationBlockStartContext.OperationBlocks.First().Syntax.ToString(); - visited.Add(operation); - operationBlockStartContext.RegisterOperationAction(operationContext => visited.Add(operationContext.Operation.Syntax.ToString()), OperationKind.Invocation); - }); - }, SymbolKind.NamedType))); - var diag = await compilation.GetAnalyzerDiagnosticsAsync(); - visited.Should().BeEquivalentTo("= 0", "=> ToString()", "ToString()"); - } - - [TestMethod] - public async Task RegisterSymbolStartAction_RegisterRegisterSymbolEndAction() - { - var snippet = new SnippetCompiler(""" - public class C - { - int i = 0; - public void M() => ToString(); - } - """); - var visited = new List(); - var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create( - new TestDiagnosticAnalyzer(symbolStart => - { - symbolStart.RegisterSymbolEndAction(symbolContext => - { - var symbolName = symbolContext.Symbol.Name; - visited.Add(symbolName); - }); - }, SymbolKind.NamedType))); - await compilation.GetAnalyzerDiagnosticsAsync(); - visited.Should().BeEquivalentTo("C"); - } - - [TestMethod] - public async Task RegisterSymbolStartAction_RegisterSyntaxNodeAction_CS() - { - var snippet = new SnippetCompiler(""" - public class C - { - int i = 0; - public void M() => ToString(); - } - """); - var visited = new List(); - var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create( - new TestDiagnosticAnalyzer(symbolStart => - { - symbolStart.RegisterSyntaxNodeAction(syntaxNodeContext => - { - var nodeName = syntaxNodeContext.Node.ToString(); - visited.Add(nodeName); - }, CS.SyntaxKind.InvocationExpression, CS.SyntaxKind.EqualsValueClause); - }, SymbolKind.NamedType))); - await compilation.GetAnalyzerDiagnosticsAsync(); - visited.Should().BeEquivalentTo("= 0", "ToString()"); - } - - [TestMethod] - public async Task RegisterSymbolStartAction_RegisterSyntaxNodeAction_VB() - { - var snippet = new SnippetCompiler(""" - Public Class C - Private i As Integer = 0 - - Public Sub M() - Call ToString() - End Sub - End Class - """, ignoreErrors: false, AnalyzerLanguage.VisualBasic); - var visited = new List(); - var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create( - new TestDiagnosticAnalyzer(symbolStart => - { - symbolStart.RegisterSyntaxNodeAction(syntaxNodeContext => - { - var nodeName = syntaxNodeContext.Node.ToString(); - visited.Add(nodeName); - }, VB.SyntaxKind.InvocationExpression); - }, SymbolKind.NamedType))); - await compilation.GetAnalyzerDiagnosticsAsync(); - visited.Should().BeEquivalentTo("ToString()"); - } - -#pragma warning disable RS1001 // Missing diagnostic analyzer attribute -#pragma warning disable RS1025 // Configure generated code analysis -#pragma warning disable RS1026 // Enable concurrent execution - private class TestDiagnosticAnalyzer : DiagnosticAnalyzer - { - public TestDiagnosticAnalyzer(Action action, SymbolKind symbolKind) - { - Action = action; - SymbolKind = symbolKind; - } - public override ImmutableArray SupportedDiagnostics => - ImmutableArray.Create(new DiagnosticDescriptor("TEST", "Test", "Test", "Test", DiagnosticSeverity.Warning, true)); - - public Action Action { get; } - public SymbolKind SymbolKind { get; } - - public override void Initialize(Microsoft.CodeAnalysis.Diagnostics.AnalysisContext context) => - context.RegisterCompilationStartAction(start => - CompilationStartAnalysisContextExtensions.RegisterSymbolStartAction(start, Action, SymbolKind)); - } - - [TestMethod] - public async Task RegisterSymbolStartAction_RegisterCodeBlockStartAction() - { - var code = """ - public class C { int i = 0; public void M() => ToString(); @@ -596,4 +339,4 @@ public override void Initialize(Microsoft.CodeAnalysis.Diagnostics.AnalysisConte context.RegisterCompilationStartAction(start => CompilationStartAnalysisContextExtensions.RegisterSymbolStartAction(start, Action, SymbolKind)); } -} +} \ No newline at end of file From 383e5a583c62f8537958f50ed709eca484544b4d Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Thu, 14 Mar 2024 18:26:07 +0100 Subject: [PATCH 53/62] EOF --- .../Wrappers/RegisterSymbolStartActionWrapperTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs index 4b9e0b4c5c1..07c8e4ad2be 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Wrappers/RegisterSymbolStartActionWrapperTest.cs @@ -339,4 +339,4 @@ public override void Initialize(Microsoft.CodeAnalysis.Diagnostics.AnalysisConte context.RegisterCompilationStartAction(start => CompilationStartAnalysisContextExtensions.RegisterSymbolStartAction(start, Action, SymbolKind)); } -} \ No newline at end of file +} From 016edfa2c48bd49fbe94e28ee3c3e83a56f5c8c7 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Thu, 14 Mar 2024 20:36:56 +0100 Subject: [PATCH 54/62] Code smells --- .../Rules/UseModelBinding.cs | 6 ++--- .../Trackers/CSharpArgumentTracker.cs | 2 +- .../Helpers/ArgumentDescriptor.cs | 2 +- .../Trackers/ArgumentContext.cs | 2 +- .../Trackers/ArgumentTracker.cs | 2 +- .../VisualBasicAttributeParameterLookup.cs | 2 +- .../Trackers/VisualBasicArgumentTracker.cs | 2 +- .../TestCases/UseModelBinding_AspNetCore.cs | 1 - .../Common/CombinatorialDataAttributeTest.cs | 18 +++++--------- .../Common/CombinatorialDataAttribute.cs | 24 ++++++++----------- 10 files changed, 25 insertions(+), 36 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs index c8b177f646e..e6743c63edd 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs @@ -85,7 +85,7 @@ private bool RegisterCodeBlockActions(SonarCodeBlockStartAnalysisContext Language.Tracker.Argument.MatchArgument(x)(context))) + if (allConstantAccess && Array.Exists(argumentDescriptors, x => Language.Tracker.Argument.MatchArgument(x)(context))) { allConstantAccess &= nodeContext.SemanticModel.GetConstantValue(argument.Expression) is { HasValue: true, Value: string }; codeBlockCandidates.Push(new(UseModelBindingMessage, GetPrimaryLocation(argument), OriginatesFromParameter(nodeContext.SemanticModel, argument))); @@ -196,8 +196,8 @@ private static bool IsAccessedViaHeaderDictionary(SemanticModel model, ILanguage private static bool IsOverridingFilterMethods(IMethodSymbol method) => (method.GetOverriddenMember() ?? method).ExplicitOrImplicitInterfaceImplementations().Any(x => x is IMethodSymbol { ContainingType: { } container } && container.IsAny( - KnownType.Microsoft_AspNetCore_Mvc_Filters_IActionFilter, - KnownType.Microsoft_AspNetCore_Mvc_Filters_IAsyncActionFilter)); + KnownType.Microsoft_AspNetCore_Mvc_Filters_IActionFilter, + KnownType.Microsoft_AspNetCore_Mvc_Filters_IAsyncActionFilter)); private static bool OriginatesFromParameter(SemanticModel semanticModel, ArgumentSyntax argument) => GetExpressionOfArgumentParent(argument) is { } parentExpression diff --git a/analyzers/src/SonarAnalyzer.CSharp/Trackers/CSharpArgumentTracker.cs b/analyzers/src/SonarAnalyzer.CSharp/Trackers/CSharpArgumentTracker.cs index 0ad20f6c12f..3e03bad14e3 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Trackers/CSharpArgumentTracker.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Trackers/CSharpArgumentTracker.cs @@ -1,6 +1,6 @@ /* * SonarAnalyzer for .NET - * Copyright (C) 2015-2023 SonarSource SA + * Copyright (C) 2015-2024 SonarSource SA * mailto: contact AT sonarsource DOT com * * This program is free software; you can redistribute it and/or diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/ArgumentDescriptor.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/ArgumentDescriptor.cs index b94d821f311..7edf1f8bfd0 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/ArgumentDescriptor.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/ArgumentDescriptor.cs @@ -1,6 +1,6 @@ /* * SonarAnalyzer for .NET - * Copyright (C) 2015-2023 SonarSource SA + * Copyright (C) 2015-2024 SonarSource SA * mailto: contact AT sonarsource DOT com * * This program is free software; you can redistribute it and/or diff --git a/analyzers/src/SonarAnalyzer.Common/Trackers/ArgumentContext.cs b/analyzers/src/SonarAnalyzer.Common/Trackers/ArgumentContext.cs index da2e9502ba3..3befd88f6fd 100644 --- a/analyzers/src/SonarAnalyzer.Common/Trackers/ArgumentContext.cs +++ b/analyzers/src/SonarAnalyzer.Common/Trackers/ArgumentContext.cs @@ -1,6 +1,6 @@ /* * SonarAnalyzer for .NET - * Copyright (C) 2015-2023 SonarSource SA + * Copyright (C) 2015-2024 SonarSource SA * mailto: contact AT sonarsource DOT com * * This program is free software; you can redistribute it and/or diff --git a/analyzers/src/SonarAnalyzer.Common/Trackers/ArgumentTracker.cs b/analyzers/src/SonarAnalyzer.Common/Trackers/ArgumentTracker.cs index e48d7e578c4..180d4211e22 100644 --- a/analyzers/src/SonarAnalyzer.Common/Trackers/ArgumentTracker.cs +++ b/analyzers/src/SonarAnalyzer.Common/Trackers/ArgumentTracker.cs @@ -1,6 +1,6 @@ /* * SonarAnalyzer for .NET - * Copyright (C) 2015-2023 SonarSource SA + * Copyright (C) 2015-2024 SonarSource SA * mailto: contact AT sonarsource DOT com * * This program is free software; you can redistribute it and/or diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Helpers/VisualBasicAttributeParameterLookup.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Helpers/VisualBasicAttributeParameterLookup.cs index 223bbbd7402..60979bf8a55 100644 --- a/analyzers/src/SonarAnalyzer.VisualBasic/Helpers/VisualBasicAttributeParameterLookup.cs +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Helpers/VisualBasicAttributeParameterLookup.cs @@ -1,6 +1,6 @@ /* * SonarAnalyzer for .NET - * Copyright (C) 2015-2023 SonarSource SA + * Copyright (C) 2015-2024 SonarSource SA * mailto: contact AT sonarsource DOT com * * This program is free software; you can redistribute it and/or diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Trackers/VisualBasicArgumentTracker.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Trackers/VisualBasicArgumentTracker.cs index 3365ae7ac76..dabc2664fe6 100644 --- a/analyzers/src/SonarAnalyzer.VisualBasic/Trackers/VisualBasicArgumentTracker.cs +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Trackers/VisualBasicArgumentTracker.cs @@ -1,6 +1,6 @@ /* * SonarAnalyzer for .NET - * Copyright (C) 2015-2023 SonarSource SA + * Copyright (C) 2015-2024 SonarSource SA * mailto: contact AT sonarsource DOT com * * This program is free software; you can redistribute it and/or diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs index 6857dbc8074..36dc65f3ff2 100644 --- a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs +++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs @@ -152,7 +152,6 @@ void M4() ~CodeBlocksController() => _ = Request.Form["id"]; // Noncompliant } - public class OverridesController : Controller { public void Action() diff --git a/analyzers/tests/SonarAnalyzer.TestFramework.Test/Common/CombinatorialDataAttributeTest.cs b/analyzers/tests/SonarAnalyzer.TestFramework.Test/Common/CombinatorialDataAttributeTest.cs index 7d7d87d9a75..81c5f65b075 100644 --- a/analyzers/tests/SonarAnalyzer.TestFramework.Test/Common/CombinatorialDataAttributeTest.cs +++ b/analyzers/tests/SonarAnalyzer.TestFramework.Test/Common/CombinatorialDataAttributeTest.cs @@ -27,23 +27,18 @@ public class CombinatorialDataAttributeTest_TwoDimensions private static List<(int X, int Y)> combinations; [ClassInitialize] - public static void Initialize(TestContext context) - { + public static void Initialize(TestContext context) => combinations = new(); - } [TestMethod] [CombinatorialData] #pragma warning disable S2699 // Tests should include assertions. Assertion happens in cleanup - public void Combinatorial([DataValues(1, 2, 3)] int x, [DataValues(-1, -2, -3)] int y) -#pragma warning restore S2699 - { + public void Combinatorial([DataValues(1, 2, 3)] int x, [DataValues(-1, -2, -3)] int y) => combinations.Add((x, y)); - } +#pragma warning restore S2699 [ClassCleanup] - public static void Cleanup() - { + public static void Cleanup() => combinations.Should().BeEquivalentTo([ (1, -1), (1, -2), @@ -55,7 +50,6 @@ public static void Cleanup() (3, -2), (3, -3), ]); - } } [TestClass] @@ -121,8 +115,8 @@ public void CombinatorialData() [3, "B", false], ]); } - public void Combinatorial([DataValues(1, 2, 3)] int x, [DataValues("A", "B")] string y, [DataValues(true, false)] bool z) + public static void Combinatorial([DataValues(1, 2, 3)] int x, [DataValues("A", "B")] string y, [DataValues(true, false)] bool z) { - + // For Attribute reflection only } } diff --git a/analyzers/tests/SonarAnalyzer.TestFramework/Common/CombinatorialDataAttribute.cs b/analyzers/tests/SonarAnalyzer.TestFramework/Common/CombinatorialDataAttribute.cs index 704c28e61e1..38b4da2b813 100644 --- a/analyzers/tests/SonarAnalyzer.TestFramework/Common/CombinatorialDataAttribute.cs +++ b/analyzers/tests/SonarAnalyzer.TestFramework/Common/CombinatorialDataAttribute.cs @@ -37,6 +37,7 @@ public DataValuesAttribute(params object[] values) [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] public sealed class CombinatorialDataAttribute : Attribute, ITestDataSource { + // Based on https://stackoverflow.com/a/75531690 public IEnumerable GetData(MethodInfo methodInfo) { var valuesPerParameter = methodInfo.GetParameters().Select(p => p.GetCustomAttribute()?.Values @@ -55,7 +56,7 @@ public IEnumerable GetData(MethodInfo methodInfo) yield return arg; // Increment indices - for (int i = parameterIndices.Length - 1; i >= 0; i--) + for (var i = parameterIndices.Length - 1; i >= 0; i--) { parameterIndices[i]++; if (parameterIndices[i] >= valuesPerParameter[i].Length) @@ -63,25 +64,20 @@ public IEnumerable GetData(MethodInfo methodInfo) parameterIndices[i] = 0; if (i == 0) + { yield break; + } } else + { break; + } } } } - public string GetDisplayName(MethodInfo methodInfo, object[] data) - { - if (data != null) - { - return string.Format(CultureInfo.CurrentCulture, "{0} ({1})", new object[2] - { - methodInfo.Name, - string.Join(",", data) - }); - } - - return null!; - } + public string GetDisplayName(MethodInfo methodInfo, object[] data) => + data == null + ? null + : $"{methodInfo.Name} ({string.Join(",", data)})"; } From bbe60e81074b1533c4e6b9e4de218b78460ef999 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Thu, 14 Mar 2024 21:04:28 +0100 Subject: [PATCH 55/62] Add DottedExpressions tests and ElementAccess/Binding --- .../Rules/UseModelBinding.cs | 16 +++--- .../Rules/UseModelBindingTest.cs | 54 +++++++++++++++++++ .../TestCases/UseModelBinding_AspNetCore.cs | 33 ++++-------- 3 files changed, 70 insertions(+), 33 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs index e6743c63edd..ca9ac02dfcd 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs @@ -190,22 +190,20 @@ private static void AddAspNetCoreDescriptors(List argumentDe // Check that the "Headers" expression in the Headers.TryGetValue("id", out _) invocation is of type IHeaderDictionary private static bool IsAccessedViaHeaderDictionary(SemanticModel model, ILanguageFacade language, SyntaxNode invocation) => invocation is InvocationExpressionSyntax { Expression: { } expression } - && GetLeftOfDot(expression) is { } left - && model.GetTypeInfo(left) is { Type: { } typeSymbol } && typeSymbol.Is(KnownType.Microsoft_AspNetCore_Http_IHeaderDictionary); + && GetLeftOfDot(expression) is { } left + && model.GetTypeInfo(left) is { Type: { } typeSymbol } && typeSymbol.Is(KnownType.Microsoft_AspNetCore_Http_IHeaderDictionary); private static bool IsOverridingFilterMethods(IMethodSymbol method) => (method.GetOverriddenMember() ?? method).ExplicitOrImplicitInterfaceImplementations().Any(x => x is IMethodSymbol { ContainingType: { } container } - && container.IsAny( - KnownType.Microsoft_AspNetCore_Mvc_Filters_IActionFilter, - KnownType.Microsoft_AspNetCore_Mvc_Filters_IAsyncActionFilter)); + && container.IsAny( + KnownType.Microsoft_AspNetCore_Mvc_Filters_IActionFilter, + KnownType.Microsoft_AspNetCore_Mvc_Filters_IAsyncActionFilter)); private static bool OriginatesFromParameter(SemanticModel semanticModel, ArgumentSyntax argument) => - GetExpressionOfArgumentParent(argument) is { } parentExpression - && OriginatesFromParameter(semanticModel, parentExpression); + GetExpressionOfArgumentParent(argument) is { } parentExpression && OriginatesFromParameter(semanticModel, parentExpression); private static bool OriginatesFromParameter(SemanticModel semanticModel, ExpressionSyntax expression) => - MostLeftOfDottedChain(expression) is { } mostLeft - && semanticModel.GetSymbolInfo(mostLeft).Symbol is IParameterSymbol; + MostLeftOfDottedChain(expression) is { } mostLeft && semanticModel.GetSymbolInfo(mostLeft).Symbol is IParameterSymbol; private static ExpressionSyntax GetLeftOfDot(ExpressionSyntax expression) => expression switch diff --git a/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs b/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs index 1eff86b6d5e..a564847a902 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs @@ -37,6 +37,10 @@ public class UseModelBindingTest AspNetCoreMetadataReference.MicrosoftExtensionsPrimitives, ]); + [TestMethod] + public void UseModelBinding_NoRegistrationIfNotAspNet() => + new VerifierBuilder().AddSnippet(string.Empty).Verify(); + [TestMethod] public void UseModelBinding_AspNetCore_CS() => builderAspNetCore.AddPaths("UseModelBinding_AspNetCore.cs").Verify(); @@ -120,6 +124,56 @@ async Task Compliant({{attribute}} string key, HttpRequest request) } """).Verify(); + [DataTestMethod] + [DataRow("Form")] + [DataRow("Headers")] + [DataRow("Query")] + [DataRow("RouteValues")] + public void DottedExpressions(string property) => + builderAspNetCore.AddSnippet($$""" + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Filters; + using Microsoft.AspNetCore.Routing; + using System; + using System.Linq; + using System.Threading.Tasks; + + public class TestController : Controller + { + HttpRequest ValidRequest => Request; + IFormCollection Form => Request.Form; + IHeaderDictionary Headers => Request.Headers; + IQueryCollection Query => Request.Query; + RouteValueDictionary RouteValues => Request.RouteValues; + + async Task DottedExpressions() + { + _ = (true ? Request : Request).{{property}}["id"]; // Noncompliant + _ = ValidatedRequest().{{property}}["id"]; // Noncompliant + _ = ValidRequest.{{property}}["id"]; // Noncompliant + _ = {{property}}["id"]; // Noncompliant + _ = this.{{property}}["id"]; // Noncompliant + _ = new TestController().{{property}}["id"]; // Noncompliant + + _ = this.Request.{{property}}["id"]; // Noncompliant + _ = Request?.{{property}}?["id"]; // Noncompliant + _ = Request?.{{property}}?.TryGetValue("id", out _); // Noncompliant + _ = Request.{{property}}?.TryGetValue("id", out _); // Noncompliant + _ = Request.{{property}}?.TryGetValue("id", out _).ToString(); // Noncompliant + _ = HttpContext.Request.{{property}}["id"]; // Noncompliant + _ = Request.HttpContext.Request.{{property}}["id"]; // Noncompliant + _ = this.ControllerContext.HttpContext.Request.{{property}}["id"]; // Noncompliant + var r1 = HttpContext.Request; + _ = r1.{{property}}["id"]; // Noncompliant + var r2 = ControllerContext; + _ = r2.HttpContext.Request.{{property}}["id"]; // Noncompliant + + HttpRequest ValidatedRequest() => Request; + } + } + """).Verify(); + [DataTestMethod] [DataRow("public class MyController: Controller")] [DataRow("public class MyController: ControllerBase")] diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs index 36dc65f3ff2..b4ebe4bdc64 100644 --- a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs +++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs @@ -124,30 +124,15 @@ void M2() } void M3() { - _ = (true ? Request : Request).Form["id"]; // Noncompliant - _ = ValidatedRequest().Form["id"]; // Noncompliant - _ = ValidRequest.Form["id"]; // Noncompliant - _ = Form["id"]; // Noncompliant - _ = this.Form["id"]; // Noncompliant - _ = new CodeBlocksController().Form["id"]; // Noncompliant - - HttpRequest ValidatedRequest() => Request; - } - - void M4() - { - _ = this.Request.Form["id"]; // Noncompliant - _ = Request?.Form?["id"]; // Noncompliant - _ = Request?.Form?.TryGetValue("id", out _); // Noncompliant - _ = Request.Form?.TryGetValue("id", out _); // Noncompliant - _ = Request.Form?.TryGetValue("id", out _).ToString(); // Noncompliant - _ = HttpContext.Request.Form["id"]; // Noncompliant - _ = Request.HttpContext.Request.Form["id"]; // Noncompliant - _ = this.ControllerContext.HttpContext.Request.Form["id"]; // Noncompliant - var r1 = HttpContext.Request; - _ = r1.Form["id"]; // Noncompliant - var r2 = ControllerContext; - _ = r2.HttpContext.Request.Form["id"]; // Noncompliant + // see also parameterized test "DottedExpressions" + _ = Request.Form["id"][0]; // Noncompliant + _ = Request?.Form["id"][0]; // Noncompliant + _ = Request.Form?["id"][0]; // Noncompliant + _ = Request?.Form?["id"][0]; // Noncompliant + _ = Request.Form?["id"][0]; // Noncompliant + + _ = Request.Form?["id"][0][0]; // Noncompliant + _ = Request.Form?["id"][0]?[0]; // Noncompliant } ~CodeBlocksController() => _ = Request.Form["id"]; // Noncompliant } From cd149e15308072ba2d4cac2003a5e0d64adcca73 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Thu, 14 Mar 2024 21:20:31 +0100 Subject: [PATCH 56/62] Fix CAE handling --- .../Rules/UseModelBinding.cs | 4 ++-- .../Rules/UseModelBindingTest.cs | 1 + .../TestCases/UseModelBinding_AspNetCore.cs | 24 +++++++++++++------ 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs index ca9ac02dfcd..93323c5be29 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs @@ -215,7 +215,7 @@ private static ExpressionSyntax GetLeftOfDot(ExpressionSyntax expression) => private static ExpressionSyntax MostLeftOfDottedChain(ExpressionSyntax root) { - var current = root.GetRootConditionalAccessExpression() ?? root; + var current = root.GetRootConditionalAccessExpression()?.Expression ?? root; while (current.Kind() is SyntaxKind.SimpleMemberAccessExpression or SyntaxKind.ElementAccessExpression) { current = current switch @@ -231,7 +231,7 @@ private static ExpressionSyntax MostLeftOfDottedChain(ExpressionSyntax root) private static ExpressionSyntax GetExpressionOfArgumentParent(ArgumentSyntax argument) => argument switch { - { Parent: BracketedArgumentListSyntax { Parent: ElementBindingExpressionSyntax { Parent: ConditionalAccessExpressionSyntax { Expression: { } expression } } } } => expression, + { Parent: BracketedArgumentListSyntax { Parent: ElementBindingExpressionSyntax expression } } => expression.GetParentConditionalAccessExpression(), { Parent: BracketedArgumentListSyntax { Parent: ElementAccessExpressionSyntax { Expression: { } expression } } } => expression, { Parent: ArgumentListSyntax { Parent: InvocationExpressionSyntax { Expression: { } expression } } } => expression, _ => null, diff --git a/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs b/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs index a564847a902..cdc57a17e0a 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using Microsoft.VisualStudio.TestTools.UnitTesting; using SonarAnalyzer.Rules.CSharp; namespace SonarAnalyzer.Test.Rules; diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs index b4ebe4bdc64..b2867205f47 100644 --- a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs +++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs @@ -125,14 +125,15 @@ void M2() void M3() { // see also parameterized test "DottedExpressions" - _ = Request.Form["id"][0]; // Noncompliant - _ = Request?.Form["id"][0]; // Noncompliant - _ = Request.Form?["id"][0]; // Noncompliant - _ = Request?.Form?["id"][0]; // Noncompliant - _ = Request.Form?["id"][0]; // Noncompliant + _ = Request.Form["id"][0]; // Noncompliant + _ = Request?.Form["id"][0]; // Noncompliant + _ = Request.Form?["id"][0]; // Noncompliant + _ = Request?.Form?["id"][0]; // Noncompliant + _ = Request.Form?["id"][0]; // Noncompliant - _ = Request.Form?["id"][0][0]; // Noncompliant - _ = Request.Form?["id"][0]?[0]; // Noncompliant + _ = Request.Form?["id"][0][0]; // Noncompliant + _ = Request.Form?["id"][0]?[0]; // Noncompliant + _ = Request.Form["id"][0]?[0]; // Noncompliant } ~CodeBlocksController() => _ = Request.Form["id"]; // Noncompliant } @@ -148,6 +149,15 @@ private void Undecidable(HttpContext context) // Implementation: It might be difficult to distinguish between access to "Request" that originate from overrides vs. "Request" access that originate from action methods. // This is especially true for "Request" which originate from parameters like here. We may need to redeclare such cases as FNs (see e.g HandleRequest above). _ = context.Request.Form["id"]; // Undecidable: request may originate from an action method (which supports binding), or from one of the following overrides (which don't). + _ = context.Request.Form["id"][0]; + _ = context.Request?.Form["id"][0]; + _ = context.Request.Form?["id"][0]; + _ = context.Request?.Form?["id"][0]; + _ = context.Request.Form?["id"][0]; + + _ = context.Request.Form?["id"][0][0]; + _ = context.Request.Form?["id"][0]?[0]; + _ = context.Request.Form["id"][0]?[0]; } private void Undecidable(HttpRequest request) { From e8ec464affbfadcfe619405eaa8f1dc904ff3caf Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 18 Mar 2024 12:45:26 +0100 Subject: [PATCH 57/62] Review --- .../SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.cs | 2 +- .../src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxFacade.cs | 6 +++--- .../src/SonarAnalyzer.CSharp/Facade/CSharpTrackerFacade.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.cs b/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.cs index 7522d51d9c0..cbfa91cec3b 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.cs @@ -363,7 +363,7 @@ static bool TakesExpressionTree(SymbolInfo info) } } - // based on Type="ArgumentListSyntax" in https://github.com/dotnet/roslyn/blob/main/src/Compilers/CSharp/Portable/Syntax/Syntax.xml + // based on Type="BaseArgumentListSyntax" in https://github.com/dotnet/roslyn/blob/main/src/Compilers/CSharp/Portable/Syntax/Syntax.xml public static BaseArgumentListSyntax ArgumentList(this SyntaxNode node) => node switch { diff --git a/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxFacade.cs b/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxFacade.cs index 2a5257beb0c..eefe6074bd6 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxFacade.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxFacade.cs @@ -108,6 +108,9 @@ public override bool IsMemberAccessOnKnownType(SyntaxNode memberAccess, string n public override bool IsStatic(SyntaxNode node) => Cast(node).IsStatic(); + public override bool IsWrittenTo(SyntaxNode expression, SemanticModel semanticModel, CancellationToken cancellationToken) => + Cast(expression).IsWrittenTo(semanticModel, cancellationToken); + public override SyntaxKind Kind(SyntaxNode node) => node.Kind(); public override string LiteralText(SyntaxNode literal) => @@ -158,7 +161,4 @@ public override bool TryGetInterpolatedTextValue(SyntaxNode node, SemanticModel public override bool TryGetOperands(SyntaxNode invocation, out SyntaxNode left, out SyntaxNode right) => Cast(invocation).TryGetOperands(out left, out right); - - public override bool IsWrittenTo(SyntaxNode expression, SemanticModel semanticModel, CancellationToken cancellationToken) => - expression is ExpressionSyntax ex && ex.IsWrittenTo(semanticModel, cancellationToken); } diff --git a/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpTrackerFacade.cs b/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpTrackerFacade.cs index 6c214fd204e..87983c5909f 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpTrackerFacade.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpTrackerFacade.cs @@ -24,6 +24,7 @@ namespace SonarAnalyzer.Helpers.Facade { internal sealed class CSharpTrackerFacade : ITrackerFacade { + public ArgumentTracker Argument { get; } = new CSharpArgumentTracker(); public BaseTypeTracker BaseType { get; } = new CSharpBaseTypeTracker(); public ElementAccessTracker ElementAccess { get; } = new CSharpElementAccessTracker(); public FieldAccessTracker FieldAccess { get; } = new CSharpFieldAccessTracker(); @@ -31,6 +32,5 @@ internal sealed class CSharpTrackerFacade : ITrackerFacade public MethodDeclarationTracker MethodDeclaration { get; } = new CSharpMethodDeclarationTracker(); public ObjectCreationTracker ObjectCreation { get; } = new CSharpObjectCreationTracker(); public PropertyAccessTracker PropertyAccess { get; } = new CSharpPropertyAccessTracker(); - public ArgumentTracker Argument => new CSharpArgumentTracker(); } } From a15734683c6f24bb9b3a6706e7326d4f1f86ca6b Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 18 Mar 2024 12:47:12 +0100 Subject: [PATCH 58/62] Rename rule. --- .../{UseModelBinding.cs => UseAspNetModelBinding.cs} | 8 ++++---- ...elBindingTest.cs => UseAspNetModelBindingTest.cs} | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) rename analyzers/src/SonarAnalyzer.CSharp/Rules/{UseModelBinding.cs => UseAspNetModelBinding.cs} (97%) rename analyzers/tests/SonarAnalyzer.Test/Rules/{UseModelBindingTest.cs => UseAspNetModelBindingTest.cs} (96%) diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseAspNetModelBinding.cs similarity index 97% rename from analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs rename to analyzers/src/SonarAnalyzer.CSharp/Rules/UseAspNetModelBinding.cs index 93323c5be29..a4906cb847b 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseModelBinding.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseAspNetModelBinding.cs @@ -23,17 +23,17 @@ namespace SonarAnalyzer.Rules.CSharp; [DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class UseModelBinding : SonarDiagnosticAnalyzer +public sealed class UseAspNetModelBinding : SonarDiagnosticAnalyzer { private const string DiagnosticId = "S6932"; - private const string UseModelBindingMessage = "Use model binding instead of accessing the raw request data"; + private const string UseAspNetModelBindingMessage = "Use model binding instead of accessing the raw request data"; private const string UseIFormFileBindingMessage = "Use IFormFile or IFormFileCollection binding instead"; protected override string MessageFormat => "{0}"; protected override ILanguageFacade Language => CSharpFacade.Instance; - public UseModelBinding() : base(DiagnosticId) { } + public UseAspNetModelBinding() : base(DiagnosticId) { } protected override void Initialize(SonarAnalysisContext context) { @@ -88,7 +88,7 @@ private bool RegisterCodeBlockActions(SonarCodeBlockStartAnalysisContext Language.Tracker.Argument.MatchArgument(x)(context))) { allConstantAccess &= nodeContext.SemanticModel.GetConstantValue(argument.Expression) is { HasValue: true, Value: string }; - codeBlockCandidates.Push(new(UseModelBindingMessage, GetPrimaryLocation(argument), OriginatesFromParameter(nodeContext.SemanticModel, argument))); + codeBlockCandidates.Push(new(UseAspNetModelBindingMessage, GetPrimaryLocation(argument), OriginatesFromParameter(nodeContext.SemanticModel, argument))); } }, SyntaxKind.Argument); } diff --git a/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs b/analyzers/tests/SonarAnalyzer.Test/Rules/UseAspNetModelBindingTest.cs similarity index 96% rename from analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs rename to analyzers/tests/SonarAnalyzer.Test/Rules/UseAspNetModelBindingTest.cs index cdc57a17e0a..b9f147b435d 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Rules/UseModelBindingTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Rules/UseAspNetModelBindingTest.cs @@ -24,10 +24,10 @@ namespace SonarAnalyzer.Test.Rules; [TestClass] -public class UseModelBindingTest +public class UseAspNetModelBindingTest { #if NET - private readonly VerifierBuilder builderAspNetCore = new VerifierBuilder() + private readonly VerifierBuilder builderAspNetCore = new VerifierBuilder() .WithOptions(ParseOptionsHelper.FromCSharp12) .AddReferences([ AspNetCoreMetadataReference.MicrosoftAspNetCoreMvcCore, @@ -39,12 +39,12 @@ public class UseModelBindingTest ]); [TestMethod] - public void UseModelBinding_NoRegistrationIfNotAspNet() => - new VerifierBuilder().AddSnippet(string.Empty).Verify(); + public void UseAspNetModelBinding_NoRegistrationIfNotAspNet() => + new VerifierBuilder().AddSnippet(string.Empty).Verify(); [TestMethod] - public void UseModelBinding_AspNetCore_CS() => - builderAspNetCore.AddPaths("UseModelBinding_AspNetCore.cs").Verify(); + public void UseAspNetModelBinding_AspNetCore_CS() => + builderAspNetCore.AddPaths("UseAspNetModelBinding_AspNetCore.cs").Verify(); [DataTestMethod] [DataRow("Form")] From 053e04fcb67bdd57cc22134f3b48a882d073b6aa Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 18 Mar 2024 13:03:37 +0100 Subject: [PATCH 59/62] Rename test file --- ...lBinding_AspNetCore.cs => UseAspNetModelBinding_AspNetCore.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename analyzers/tests/SonarAnalyzer.Test/TestCases/{UseModelBinding_AspNetCore.cs => UseAspNetModelBinding_AspNetCore.cs} (100%) diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseAspNetModelBinding_AspNetCore.cs similarity index 100% rename from analyzers/tests/SonarAnalyzer.Test/TestCases/UseModelBinding_AspNetCore.cs rename to analyzers/tests/SonarAnalyzer.Test/TestCases/UseAspNetModelBinding_AspNetCore.cs From 26b0d760cf11d2e2b4dc2a0c8a3a27cd9a7c3f12 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 18 Mar 2024 13:04:21 +0100 Subject: [PATCH 60/62] Move IsOverridingFilterMethods logic --- .../Rules/UseAspNetModelBinding.cs | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseAspNetModelBinding.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseAspNetModelBinding.cs index a4906cb847b..be449ee1eb3 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseAspNetModelBinding.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseAspNetModelBinding.cs @@ -42,20 +42,33 @@ protected override void Initialize(SonarAnalysisContext context) var (argumentDescriptors, propertyAccessDescriptors) = GetDescriptors(compilationStartContext.Compilation); if (argumentDescriptors.Any() || propertyAccessDescriptors.Any()) { - compilationStartContext.RegisterSymbolStartAction(symbolStartContext => + compilationStartContext.RegisterSymbolStartAction(symbolStart => { - // If the user overrides any action filters, model binding may not be working as expected. Then we do not want to raise on expressions that originate from parameters. + // If the user overrides any action filters, model binding may not be working as expected. + // Then we do not want to raise on expressions that originate from parameters. // See the OverridesController.Undecidable test cases for details. - var hasOverrides = false; - var controllerCandidates = new ConcurrentStack(); // In SymbolEnd, we filter the candidates based on the overriding we learn on the go. - if (symbolStartContext.Symbol is INamedTypeSymbol namedType && namedType.IsControllerType()) + var hasActionFiltersOverrides = false; + var candidates = new ConcurrentStack(); // In SymbolEnd, we filter the candidates based on the overriding we learn on the go. + if (symbolStart.Symbol is INamedTypeSymbol namedType && namedType.IsControllerType()) { - symbolStartContext.RegisterCodeBlockStartAction(codeBlockStart => - hasOverrides |= RegisterCodeBlockActions(codeBlockStart, argumentDescriptors, propertyAccessDescriptors, controllerCandidates)); + symbolStart.RegisterCodeBlockStartAction(codeBlockStart => + { + if (codeBlockStart.OwningSymbol is IMethodSymbol method && IsOverridingFilterMethods(method)) + { + // We do not want to raise in ActionFilter overrides and so we do not register. + // The SymbolEndAction needs to be made aware, that there are + // ActionFilter overrides, so it can filter out some candidates. + hasActionFiltersOverrides = true; + } + else + { + RegisterCodeBlockActions(codeBlockStart, argumentDescriptors, propertyAccessDescriptors, candidates); + } + }); } - symbolStartContext.RegisterSymbolEndAction(symbolEnd => + symbolStart.RegisterSymbolEndAction(symbolEnd => { - foreach (var candidate in controllerCandidates.Where(x => !(hasOverrides && x.OriginatesFromParameter))) + foreach (var candidate in candidates.Where(x => !(hasActionFiltersOverrides && x.OriginatesFromParameter))) { symbolEnd.ReportIssue(Diagnostic.Create(Rule, candidate.Location, candidate.Message)); } @@ -65,16 +78,10 @@ protected override void Initialize(SonarAnalysisContext context) }); } - private bool RegisterCodeBlockActions(SonarCodeBlockStartAnalysisContext codeBlockStart, + private void RegisterCodeBlockActions(SonarCodeBlockStartAnalysisContext codeBlockStart, ArgumentDescriptor[] argumentDescriptors, MemberDescriptor[] propertyAccessDescriptors, ConcurrentStack controllerCandidates) { - if (codeBlockStart.OwningSymbol is IMethodSymbol method && IsOverridingFilterMethods(method)) - { - // We do not want to raise in ActionFilter overrides. The SymbolEndAction needs to be made aware, that there are - // ActionFilter overrides, so it can filter out some candidates. - return true; - } // Within a single code block, access via constant and variable keys could be mixed // We only want to raise, if all access were done via constants var allConstantAccess = true; @@ -111,7 +118,6 @@ private bool RegisterCodeBlockActions(SonarCodeBlockStartAnalysisContext Date: Mon, 18 Mar 2024 16:53:03 +0100 Subject: [PATCH 61/62] Review --- .../Rules/UseAspNetModelBinding.cs | 32 ++++++++++--------- .../Trackers/CSharpArgumentTracker.cs | 6 +--- .../Facade/ITrackerFacade.cs | 2 +- .../Facade/SyntaxFacade.cs | 2 +- .../UseAspNetModelBinding_AspNetCore.cs | 2 ++ 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseAspNetModelBinding.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseAspNetModelBinding.cs index be449ee1eb3..36d58d5889b 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseAspNetModelBinding.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseAspNetModelBinding.cs @@ -53,7 +53,7 @@ protected override void Initialize(SonarAnalysisContext context) { symbolStart.RegisterCodeBlockStartAction(codeBlockStart => { - if (codeBlockStart.OwningSymbol is IMethodSymbol method && IsOverridingFilterMethods(method)) + if (IsOverridingFilterMethods(codeBlockStart.OwningSymbol)) { // We do not want to raise in ActionFilter overrides and so we do not register. // The SymbolEndAction needs to be made aware, that there are @@ -82,9 +82,9 @@ private void RegisterCodeBlockActions(SonarCodeBlockStartAnalysisContext controllerCandidates) { - // Within a single code block, access via constant and variable keys could be mixed - // We only want to raise, if all access were done via constants - var allConstantAccess = true; + // Within a single code block, access via constant and variable keys could be mixed. + // We only want to raise, if all access were done via constants. + var allConstantAccesses = true; var codeBlockCandidates = new ConcurrentStack(); if (argumentDescriptors.Any()) { @@ -92,10 +92,10 @@ private void RegisterCodeBlockActions(SonarCodeBlockStartAnalysisContext Language.Tracker.Argument.MatchArgument(x)(context))) + if (allConstantAccesses && Array.Exists(argumentDescriptors, x => Language.Tracker.Argument.MatchArgument(x)(context))) { - allConstantAccess &= nodeContext.SemanticModel.GetConstantValue(argument.Expression) is { HasValue: true, Value: string }; - codeBlockCandidates.Push(new(UseAspNetModelBindingMessage, GetPrimaryLocation(argument), OriginatesFromParameter(nodeContext.SemanticModel, argument))); + allConstantAccesses &= nodeContext.SemanticModel.GetConstantValue(argument.Expression) is { HasValue: true, Value: string }; + codeBlockCandidates.Push(new(UseAspNetModelBindingMessage, GetPrimaryLocation(argument), IsOriginatingFromParameter(nodeContext.SemanticModel, argument))); } }, SyntaxKind.Argument); } @@ -103,17 +103,19 @@ private void RegisterCodeBlockActions(SonarCodeBlockStartAnalysisContext { + // The property access of Request.Form.Files can be replaced by an IFormFile binding. + // Any access to a "Files" property is therefore noncompliant. This is different from the Argument handling above. var memberAccess = (MemberAccessExpressionSyntax)nodeContext.Node; var context = new PropertyAccessContext(memberAccess, nodeContext.SemanticModel, memberAccess.Name.Identifier.ValueText); if (Language.Tracker.PropertyAccess.MatchProperty(propertyAccessDescriptors)(context)) { - codeBlockCandidates.Push(new(UseIFormFileBindingMessage, memberAccess.GetLocation(), OriginatesFromParameter(nodeContext.SemanticModel, memberAccess))); + codeBlockCandidates.Push(new(UseIFormFileBindingMessage, memberAccess.GetLocation(), IsOriginatingFromParameter(nodeContext.SemanticModel, memberAccess))); } }, SyntaxKind.SimpleMemberAccessExpression); } codeBlockStart.RegisterCodeBlockEndAction(codeBlockEnd => { - if (allConstantAccess) + if (allConstantAccesses) { controllerCandidates.PushRange([.. codeBlockCandidates]); } @@ -199,16 +201,16 @@ private static bool IsAccessedViaHeaderDictionary(SemanticModel model, ILanguage && GetLeftOfDot(expression) is { } left && model.GetTypeInfo(left) is { Type: { } typeSymbol } && typeSymbol.Is(KnownType.Microsoft_AspNetCore_Http_IHeaderDictionary); - private static bool IsOverridingFilterMethods(IMethodSymbol method) => - (method.GetOverriddenMember() ?? method).ExplicitOrImplicitInterfaceImplementations().Any(x => x is IMethodSymbol { ContainingType: { } container } + private static bool IsOverridingFilterMethods(ISymbol owningSymbol) => + (owningSymbol.GetOverriddenMember() ?? owningSymbol).ExplicitOrImplicitInterfaceImplementations().Any(x => x is IMethodSymbol { ContainingType: { } container } && container.IsAny( KnownType.Microsoft_AspNetCore_Mvc_Filters_IActionFilter, KnownType.Microsoft_AspNetCore_Mvc_Filters_IAsyncActionFilter)); - private static bool OriginatesFromParameter(SemanticModel semanticModel, ArgumentSyntax argument) => - GetExpressionOfArgumentParent(argument) is { } parentExpression && OriginatesFromParameter(semanticModel, parentExpression); + private static bool IsOriginatingFromParameter(SemanticModel semanticModel, ArgumentSyntax argument) => + GetExpressionOfArgumentParent(argument) is { } parentExpression && IsOriginatingFromParameter(semanticModel, parentExpression); - private static bool OriginatesFromParameter(SemanticModel semanticModel, ExpressionSyntax expression) => + private static bool IsOriginatingFromParameter(SemanticModel semanticModel, ExpressionSyntax expression) => MostLeftOfDottedChain(expression) is { } mostLeft && semanticModel.GetSymbolInfo(mostLeft).Symbol is IParameterSymbol; private static ExpressionSyntax GetLeftOfDot(ExpressionSyntax expression) => @@ -255,5 +257,5 @@ private static bool IsIDictionaryStringStringValuesInvocation(IMethodSymbol meth && typeArguments[0].Is(KnownType.System_String) && typeArguments[1].Is(KnownType.Microsoft_Extensions_Primitives_StringValues); - private readonly record struct ReportCandidate(string Message, Location Location, bool OriginatesFromParameter = false); + private readonly record struct ReportCandidate(string Message, Location Location, bool OriginatesFromParameter); } diff --git a/analyzers/src/SonarAnalyzer.CSharp/Trackers/CSharpArgumentTracker.cs b/analyzers/src/SonarAnalyzer.CSharp/Trackers/CSharpArgumentTracker.cs index 3e03bad14e3..488ef59ad02 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Trackers/CSharpArgumentTracker.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Trackers/CSharpArgumentTracker.cs @@ -22,11 +22,7 @@ namespace SonarAnalyzer.Helpers.Trackers; internal sealed class CSharpArgumentTracker : ArgumentTracker { - protected override SyntaxKind[] TrackedSyntaxKinds => - [ - SyntaxKind.AttributeArgument, - SyntaxKind.Argument, - ]; + protected override SyntaxKind[] TrackedSyntaxKinds => [SyntaxKind.AttributeArgument, SyntaxKind.Argument,]; protected override ILanguageFacade Language => CSharpFacade.Instance; diff --git a/analyzers/src/SonarAnalyzer.Common/Facade/ITrackerFacade.cs b/analyzers/src/SonarAnalyzer.Common/Facade/ITrackerFacade.cs index e496056ce31..869789f9aca 100644 --- a/analyzers/src/SonarAnalyzer.Common/Facade/ITrackerFacade.cs +++ b/analyzers/src/SonarAnalyzer.Common/Facade/ITrackerFacade.cs @@ -25,6 +25,7 @@ namespace SonarAnalyzer.Helpers.Facade public interface ITrackerFacade where TSyntaxKind : struct { + ArgumentTracker Argument { get; } BaseTypeTracker BaseType { get; } ElementAccessTracker ElementAccess { get; } FieldAccessTracker FieldAccess { get; } @@ -32,6 +33,5 @@ public interface ITrackerFacade MethodDeclarationTracker MethodDeclaration { get; } ObjectCreationTracker ObjectCreation { get; } PropertyAccessTracker PropertyAccess { get; } - ArgumentTracker Argument { get; } } } diff --git a/analyzers/src/SonarAnalyzer.Common/Facade/SyntaxFacade.cs b/analyzers/src/SonarAnalyzer.Common/Facade/SyntaxFacade.cs index 118e1778c85..b95fbbae1d0 100644 --- a/analyzers/src/SonarAnalyzer.Common/Facade/SyntaxFacade.cs +++ b/analyzers/src/SonarAnalyzer.Common/Facade/SyntaxFacade.cs @@ -44,7 +44,6 @@ public abstract class SyntaxFacade public abstract bool IsAnyKind(SyntaxNode node, params TSyntaxKind[] syntaxKinds); public abstract bool IsAnyKind(SyntaxTrivia trivia, params TSyntaxKind[] syntaxKinds); public abstract bool IsInExpressionTree(SemanticModel model, SyntaxNode node); - public abstract bool IsWrittenTo(SyntaxNode expression, SemanticModel semanticModel, CancellationToken cancellationToken); public abstract bool IsKind(SyntaxNode node, TSyntaxKind kind); public abstract bool IsKind(SyntaxToken token, TSyntaxKind kind); public abstract bool IsKind(SyntaxTrivia trivia, TSyntaxKind kind); @@ -52,6 +51,7 @@ public abstract class SyntaxFacade public abstract bool IsMemberAccessOnKnownType(SyntaxNode memberAccess, string name, KnownType knownType, SemanticModel semanticModel); public abstract bool IsNullLiteral(SyntaxNode node); public abstract bool IsStatic(SyntaxNode node); + public abstract bool IsWrittenTo(SyntaxNode expression, SemanticModel semanticModel, CancellationToken cancellationToken); public abstract TSyntaxKind Kind(SyntaxNode node); public abstract string LiteralText(SyntaxNode literal); public abstract ImmutableArray LocalDeclarationIdentifiers(SyntaxNode node); diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseAspNetModelBinding_AspNetCore.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseAspNetModelBinding_AspNetCore.cs index b2867205f47..283335150ab 100644 --- a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseAspNetModelBinding_AspNetCore.cs +++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseAspNetModelBinding_AspNetCore.cs @@ -33,6 +33,8 @@ public IActionResult Post() // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ _ = Request.Form.Files; // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}} // ^^^^^^^^^^^^^^^^^^ + _ = Request.Form.File\u0073; // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}} + // ^^^^^^^^^^^^^^^^^^^^^^^ _ = Request.Form.Files["file"]; // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}} // ^^^^^^^^^^^^^^^^^^ _ = Request.Form.Files[0]; // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}} From 1d9591689503f0366fc12d3e2990ca3d01047d33 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 18 Mar 2024 18:01:06 +0100 Subject: [PATCH 62/62] Review --- .../Rules/UseAspNetModelBindingTest.cs | 16 ++++++++-------- .../UseAspNetModelBinding_AspNetCore.cs | 4 +++- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/analyzers/tests/SonarAnalyzer.Test/Rules/UseAspNetModelBindingTest.cs b/analyzers/tests/SonarAnalyzer.Test/Rules/UseAspNetModelBindingTest.cs index b9f147b435d..b9bc1410de5 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Rules/UseAspNetModelBindingTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Rules/UseAspNetModelBindingTest.cs @@ -43,7 +43,7 @@ public void UseAspNetModelBinding_NoRegistrationIfNotAspNet() => new VerifierBuilder().AddSnippet(string.Empty).Verify(); [TestMethod] - public void UseAspNetModelBinding_AspNetCore_CS() => + public void UseAspNetModelBinding_AspNetCore_CSharp12() => builderAspNetCore.AddPaths("UseAspNetModelBinding_AspNetCore.cs").Verify(); [DataTestMethod] @@ -51,7 +51,7 @@ public void UseAspNetModelBinding_AspNetCore_CS() => [DataRow("Query")] [DataRow("RouteValues")] [DataRow("Headers")] - public void NonCompliantAccess(string property) => + public void UseAspNetModelBinding_NonCompliantAccess(string property) => builderAspNetCore.AddSnippet($$"""" using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -98,7 +98,7 @@ static void StaticLocalFunction(HttpRequest request) [TestMethod] [CombinatorialData] - public void CompliantAccess( + public void UseAspNetModelBinding_CompliantAccess( [DataValues( "_ = {0}.Keys", "_ = {0}.Count", @@ -130,7 +130,7 @@ async Task Compliant({{attribute}} string key, HttpRequest request) [DataRow("Headers")] [DataRow("Query")] [DataRow("RouteValues")] - public void DottedExpressions(string property) => + public void UseAspNetModelBinding_DottedExpressions(string property) => builderAspNetCore.AddSnippet($$""" using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -180,7 +180,7 @@ async Task DottedExpressions() [DataRow("public class MyController: ControllerBase")] [DataRow("[Controller] public class My: Controller")] // [DataRow("public class MyController")] FN: Poco controller are not detected - public void PocoController(string classDeclaration) => + public void UseAspNetModelBinding_PocoController(string classDeclaration) => builderAspNetCore.AddSnippet($$"""" using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -202,7 +202,7 @@ public async Task Action([FromServices]IHttpContextAccessor httpContextAccessor) [DataRow("public class My")] [DataRow("[NonController] public class My: Controller")] [DataRow("[NonController] public class MyController: Controller")] - public void NoController(string classDeclaration) => + public void UseAspNetModelBinding_NoController(string classDeclaration) => builderAspNetCore.AddSnippet($$"""" using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -225,7 +225,7 @@ public async Task Action([FromServices]IHttpContextAccessor httpContextAccessor) [DataRow("Headers")] [DataRow("Query")] [DataRow("RouteValues")] - public void NoControllerHelpers(string property) => + public void UseAspNetModelBinding_NoControllerHelpers(string property) => builderAspNetCore.AddSnippet($$"""" using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -256,7 +256,7 @@ public void HandleRequest(HttpRequest request) [TestMethod] [CombinatorialData] - public void InheritanceAccess( + public void UseAspNetModelBinding_InheritanceAccess( [DataValues( ": Controller", ": ControllerBase", diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseAspNetModelBinding_AspNetCore.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseAspNetModelBinding_AspNetCore.cs index 283335150ab..5e47dd444f9 100644 --- a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseAspNetModelBinding_AspNetCore.cs +++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseAspNetModelBinding_AspNetCore.cs @@ -9,7 +9,7 @@ public class TestController : Controller { private readonly string Key = "id"; - public IActionResult Post() + public IActionResult Post(string key) { _ = Request.Form["id"]; // Noncompliant {{Use model binding instead of accessing the raw request data}} // ^^^^^^^^^^^^ @@ -37,6 +37,8 @@ public IActionResult Post() // ^^^^^^^^^^^^^^^^^^^^^^^ _ = Request.Form.Files["file"]; // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}} // ^^^^^^^^^^^^^^^^^^ + _ = Request.Form.Files[key]; // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}} + // ^^^^^^^^^^^^^^^^^^ _ = Request.Form.Files[0]; // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}} // ^^^^^^^^^^^^^^^^^^ _ = Request.Form.Files.Any(); // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}}