Skip to content

Commit e5b599b

Browse files
committed
Implement naive variable matching
1 parent f158491 commit e5b599b

11 files changed

Lines changed: 328 additions & 58 deletions
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using Microsoft.CodeAnalysis;
2+
using Microsoft.CodeAnalysis.Diagnostics;
3+
using Microsoft.VisualStudio.TestTools.UnitTesting;
4+
5+
using Sql.Analyzer.Test.Helpers;
6+
7+
using TestHelper;
8+
9+
namespace Sql.Analyzer.Test
10+
{
11+
[TestClass]
12+
public class DapperParametersMatchingAnalyzerTests : DiagnosticVerifier
13+
{
14+
[TestMethod]
15+
public void InlineSqlUnmatchedParameters_AnalyzerTriggered()
16+
{
17+
var code = EmbeddedResourceHelper.ReadTestData("InlineSqlUnmatchedParameters.cs");
18+
19+
var expected = new DiagnosticResult
20+
{
21+
Id = DapperParametersMatchingAnalyzer.DiagnosticId,
22+
Message = string.Format(DapperParametersMatchingAnalyzer.MessageFormatCsharpArgumentNotFound, "not_found"),
23+
Severity = DiagnosticSeverity.Warning,
24+
Locations = new[] { new DiagnosticResultLocation("Test0.cs", 13, 13) }
25+
};
26+
27+
VerifyCSharpDiagnostic(code, expected);
28+
}
29+
30+
[TestMethod]
31+
public void InlineSqlWithVariableUnmatchedParameters_AnalyzerTriggered()
32+
{
33+
var code = EmbeddedResourceHelper.ReadTestData("InlineSqlWithConstUnmatchedParameters.cs");
34+
35+
var expected = new DiagnosticResult
36+
{
37+
Id = DapperParametersMatchingAnalyzer.DiagnosticId,
38+
Message = string.Format(DapperParametersMatchingAnalyzer.MessageFormatCsharpArgumentNotFound, "not_found"),
39+
Severity = DiagnosticSeverity.Warning,
40+
Locations = new[] { new DiagnosticResultLocation("Test0.cs", 14, 13) }
41+
};
42+
43+
VerifyCSharpDiagnostic(code, expected);
44+
}
45+
46+
47+
[TestMethod]
48+
public void InlineSqlUnmatchedSqlVariable_AnalyzerTriggered()
49+
{
50+
var code = EmbeddedResourceHelper.ReadTestData("InlineSqlUnmatchedSqlVariable.cs");
51+
52+
var expected = new DiagnosticResult
53+
{
54+
Id = DapperParametersMatchingAnalyzer.DiagnosticId,
55+
Message = string.Format(DapperParametersMatchingAnalyzer.MessageFormatSqlVariableNotFound, "param"),
56+
Severity = DiagnosticSeverity.Warning,
57+
Locations = new[] { new DiagnosticResultLocation("Test0.cs", 13, 13) }
58+
};
59+
60+
VerifyCSharpDiagnostic(code, expected);
61+
}
62+
63+
protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
64+
{
65+
return new DapperParametersMatchingAnalyzer();
66+
}
67+
}
68+
}

src/Sql.Analyzer/Sql.Analyzer.Test/DapperStringParameterAnalyzerTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public void InlineSqlWithStringArgument_AnalyzerTriggered()
2727
var expected = new DiagnosticResult
2828
{
2929
Id = DapperStringParameterAnalyzer.DiagnosticId,
30-
Message = DapperStringParameterAnalyzer.MessageFormat,
30+
Message = string.Format(DapperStringParameterAnalyzer.MessageFormat, "id"),
3131
Severity = DiagnosticSeverity.Warning,
3232
Locations = new[] { new DiagnosticResultLocation("Test0.cs", 13, 46) }
3333
};

src/Sql.Analyzer/Sql.Analyzer.Test/Sql.Analyzer.Test.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,19 @@
55
</PropertyGroup>
66

77
<ItemGroup>
8+
<Compile Remove="TestData\InlineSqlUnmatchedParameters.cs" />
9+
<Compile Remove="TestData\InlineSqlUnmatchedSqlVariable.cs" />
10+
<Compile Remove="TestData\InlineSqlWithConstUnmatchedParameters.cs" />
811
<Compile Remove="TestData\InlineSqlWithDbStringArgumentTestData.cs" />
912
<Compile Remove="TestData\InlineSqlWithStringArgumentTestData.cs" />
1013
<Compile Remove="TestData\StoredProcedureWithStringArgumentTestData.cs" />
1114
</ItemGroup>
1215

1316
<ItemGroup>
17+
<EmbeddedResource Include="TestData\InlineSqlUnmatchedSqlVariable.cs" />
18+
<EmbeddedResource Include="TestData\InlineSqlWithConstUnmatchedParameters.cs" />
1419
<EmbeddedResource Include="TestData\InlineSqlWithDbStringArgumentTestData.cs" />
20+
<EmbeddedResource Include="TestData\InlineSqlUnmatchedParameters.cs" />
1521
<EmbeddedResource Include="TestData\InlineSqlWithStringArgumentTestData.cs" />
1622
<EmbeddedResource Include="TestData\StoredProcedureWithStringArgumentTestData.cs" />
1723
</ItemGroup>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System.Data.SqlClient;
2+
using System.Threading.Tasks;
3+
4+
using Dapper;
5+
6+
namespace Sql.Analyzer.Test.TestData
7+
{
8+
public class InlineSqlWithStringParameterTestData
9+
{
10+
private static async Task Main(string[] args)
11+
{
12+
var sql = new SqlConnection();
13+
sql.Execute("inline sql @param, @not_found", new { param = new DbString() { IsAnsi = true, Value = "some_string" } });
14+
}
15+
}
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System.Data.SqlClient;
2+
using System.Threading.Tasks;
3+
4+
using Dapper;
5+
6+
namespace Sql.Analyzer.Test.TestData
7+
{
8+
public class InlineSqlWithStringParameterTestData
9+
{
10+
private static async Task Main(string[] args)
11+
{
12+
var sql = new SqlConnection();
13+
sql.Execute("inline sql", new { param = new DbString() { IsAnsi = true, Value = "some_string" } });
14+
}
15+
}
16+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System.Data.SqlClient;
2+
using System.Threading.Tasks;
3+
4+
using Dapper;
5+
6+
namespace Sql.Analyzer.Test.TestData
7+
{
8+
public class InlineSqlWithStringParameterTestData
9+
{
10+
private static async Task Main(string[] args)
11+
{
12+
var sql = new SqlConnection();
13+
const string sqlConst = "inline sql @param, @not_found";
14+
sql.Execute(sqlConst, new { param = new DbString() { IsAnsi = true, Value = "some_string" } });
15+
}
16+
}
17+
}

src/Sql.Analyzer/Sql.Analyzer.Test/TestData/InlineSqlWithStringArgumentTestData.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public class InlineSqlWithStringParameterTestData
1010
private static async Task Main(string[] args)
1111
{
1212
var sql = new SqlConnection();
13-
sql.Execute("inline sql @param", new { param = "some_string" });
13+
sql.Execute("inline sql @param", new { id = "some_id" });
1414
}
1515
}
1616
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
using System.Collections.Generic;
2+
using System.Collections.Immutable;
3+
using System.Linq;
4+
using System.Text.RegularExpressions;
5+
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.CSharp;
8+
using Microsoft.CodeAnalysis.CSharp.Syntax;
9+
using Microsoft.CodeAnalysis.Diagnostics;
10+
11+
using Sql.Analyzer.Extensions;
12+
13+
namespace Sql.Analyzer
14+
{
15+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
16+
public class DapperParametersMatchingAnalyzer : DiagnosticAnalyzer
17+
{
18+
public const string DiagnosticId = "SQL002";
19+
20+
public const string MessageFormatSqlVariableNotFound = "SQL variable not found for argument '{0}'";
21+
22+
public const string MessageFormatCsharpArgumentNotFound = "Argument not found for SQL variable '{0}'";
23+
24+
private const string Category = "API Guidance";
25+
26+
private static readonly string Description = "Parameters mismatching";
27+
28+
private static readonly string Title = "Parameters mismatching";
29+
30+
private static readonly Regex SqlParameterRegex = new Regex(@"@(?<variable>\w+)", RegexOptions.Compiled);
31+
32+
private static readonly DiagnosticDescriptor CsharpArgumentNotFoundRule = new DiagnosticDescriptor(
33+
DiagnosticId,
34+
Title,
35+
MessageFormatCsharpArgumentNotFound,
36+
Category,
37+
DiagnosticSeverity.Warning,
38+
true,
39+
Description);
40+
41+
private static readonly DiagnosticDescriptor SqlParameterNotFoundRule = new DiagnosticDescriptor(
42+
DiagnosticId,
43+
Title,
44+
MessageFormatSqlVariableNotFound,
45+
Category,
46+
DiagnosticSeverity.Warning,
47+
true,
48+
Description);
49+
50+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
51+
ImmutableArray.Create(CsharpArgumentNotFoundRule, SqlParameterNotFoundRule);
52+
53+
public override void Initialize(AnalysisContext context)
54+
{
55+
context.RegisterSyntaxNodeAction(AnalyzeInvocationExpression, SyntaxKind.InvocationExpression);
56+
}
57+
58+
private void AnalyzeInvocationExpression(SyntaxNodeAnalysisContext context)
59+
{
60+
var invocationExpressionSyntax = (InvocationExpressionSyntax)context.Node;
61+
62+
if (!invocationExpressionSyntax.IsDapperInlineSqlMethod(context.SemanticModel))
63+
{
64+
return;
65+
}
66+
67+
ReportDiagnostics(context, invocationExpressionSyntax);
68+
}
69+
70+
private void ReportDiagnostics(
71+
SyntaxNodeAnalysisContext context,
72+
InvocationExpressionSyntax invocationExpressionSyntax)
73+
{
74+
List<string> sqlVariables = new List<string>();
75+
List<string> sharpParameters = null;
76+
foreach (var argument in invocationExpressionSyntax.ArgumentList.Arguments)
77+
{
78+
var parameter = argument.DetermineParameter(context.SemanticModel);
79+
if (string.Equals(parameter.Name, "sql"))
80+
{
81+
var sourceText = argument.TryGetArgumentStringValue(context.SemanticModel);
82+
83+
// If SQL code is not constant, return
84+
if (sourceText == null)
85+
{
86+
return;
87+
}
88+
89+
var matches = SqlParameterRegex.Matches(sourceText);
90+
foreach (Match match in matches)
91+
{
92+
sqlVariables.Add(match.Groups["variable"].Value);
93+
}
94+
95+
continue;
96+
}
97+
98+
if (string.Equals(parameter.Name, "param")
99+
&& context.SemanticModel.GetSymbolInfo(argument.Expression).Symbol is IMethodSymbol methodSymbol)
100+
{
101+
sharpParameters = methodSymbol.Parameters.Select(p => p.Name.Trim('@')).ToList();
102+
}
103+
}
104+
105+
if (sharpParameters == null)
106+
{
107+
return;
108+
}
109+
110+
foreach (var notFoundArgument in sqlVariables.Except(sharpParameters))
111+
{
112+
context.ReportDiagnostic(
113+
Diagnostic.Create(
114+
CsharpArgumentNotFoundRule,
115+
invocationExpressionSyntax.GetLocation(),
116+
notFoundArgument));
117+
}
118+
119+
foreach (var notFoundVariable in sharpParameters.Except(sqlVariables))
120+
{
121+
context.ReportDiagnostic(
122+
Diagnostic.Create(
123+
SqlParameterNotFoundRule,
124+
invocationExpressionSyntax.GetLocation(),
125+
notFoundVariable));
126+
}
127+
}
128+
}
129+
}

src/Sql.Analyzer/Sql.Analyzer/DapperStringParameterAnalyzer.cs

Lines changed: 9 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public class DapperStringParameterAnalyzer : DiagnosticAnalyzer
1515
{
1616
public const string DiagnosticId = "SQL001";
1717

18-
public const string MessageFormat = "Argument with unspecified string type";
18+
public const string MessageFormat = "SQL type is not specified for '{0}' argument";
1919

2020
private const string Category = "API Guidance";
2121

@@ -43,62 +43,15 @@ private void AnalyzeInvocationExpression(SyntaxNodeAnalysisContext context)
4343
{
4444
var invocationExpressionSyntax = (InvocationExpressionSyntax)context.Node;
4545

46-
var methodSymbol = context.SemanticModel.GetSymbolInfo(invocationExpressionSyntax).Symbol as IMethodSymbol;
47-
if (methodSymbol == null)
46+
if (!invocationExpressionSyntax.IsDapperInlineSqlMethod(context.SemanticModel))
4847
{
4948
return;
5049
}
5150

52-
var dapperClass = context.SemanticModel.Compilation.GetTypeByMetadataName("Dapper.SqlMapper");
53-
if (methodSymbol.ContainingType != dapperClass)
54-
{
55-
return;
56-
}
57-
58-
if (!CheckIfInlineSqlCommand(context, invocationExpressionSyntax))
59-
{
60-
return;
61-
}
62-
63-
var badStringArgument = FindBadStringArgument(context, invocationExpressionSyntax);
64-
if (badStringArgument == null)
65-
{
66-
return;
67-
}
68-
69-
context.ReportDiagnostic(Diagnostic.Create(Rule, badStringArgument.GetLocation()));
70-
}
71-
72-
private bool CheckIfInlineSqlCommand(
73-
SyntaxNodeAnalysisContext context,
74-
InvocationExpressionSyntax invocationExpressionSyntax)
75-
{
76-
foreach (var argument in invocationExpressionSyntax.ArgumentList.Arguments)
77-
{
78-
var parameter = argument.DetermineParameter(context.SemanticModel);
79-
if (!string.Equals(parameter.Name, "commandType"))
80-
{
81-
continue;
82-
}
83-
84-
var symbolInfo = context.SemanticModel.GetSymbolInfo(argument.Expression).Symbol;
85-
if (symbolInfo == null)
86-
{
87-
continue;
88-
}
89-
90-
if (string.Equals(symbolInfo.Name, "Text"))
91-
{
92-
break;
93-
}
94-
95-
return false;
96-
}
97-
98-
return true;
51+
ReportStringArgument(context, invocationExpressionSyntax);
9952
}
10053

101-
private ArgumentSyntax FindBadStringArgument(
54+
private void ReportStringArgument(
10255
SyntaxNodeAnalysisContext context,
10356
InvocationExpressionSyntax invocationExpressionSyntax)
10457
{
@@ -117,15 +70,16 @@ private ArgumentSyntax FindBadStringArgument(
11770
break;
11871
}
11972

120-
if (symbolInfo.Parameters.Any(p => p.Type.Equals(stringType)))
73+
foreach (var property in symbolInfo.Parameters)
12174
{
122-
return argument;
75+
if (property.Type.Equals(stringType))
76+
{
77+
context.ReportDiagnostic(Diagnostic.Create(Rule, argument.GetLocation(), property.Name));
78+
}
12379
}
12480

12581
break;
12682
}
127-
128-
return null;
12983
}
13084
}
13185
}

0 commit comments

Comments
 (0)