Skip to content

Commit f136955

Browse files
shimatclaude
andcommitted
Add Roslyn analyzer OCVS001: detect Row().At<T>(col) misuse at compile time
Introduces OpenCvSharp.Analyzers project with OCVS001 that warns at compile time when At<T>(int) is called on a Mat returned by Row(), catching the misuse pattern that causes silent out-of-bounds memory access (#1775). The analyzer detects both forms: mat.Row(i).At<Vec3b>(c) // direct chain var row = mat.Row(i); row.At<Vec3b>(c) // local variable The analyzer DLL is included in the OpenCvSharp4 NuGet package under analyzers/dotnet/cs/ via ProjectReference with OutputItemType=Analyzer, so all NuGet consumers receive compile-time warnings automatically. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f491bcb commit f136955

7 files changed

Lines changed: 297 additions & 0 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
; Shipped analyzer releases
2+
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
; Unshipped analyzer releases
2+
3+
### New Rules
4+
5+
Rule ID | Category | Severity | Notes
6+
--------|----------|----------|-------
7+
OCVS001 | Correctness | Warning | At&lt;T&gt;(int) called on a Mat row submatrix
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
<LangVersion>12</LangVersion>
6+
<Nullable>enable</Nullable>
7+
<RootNamespace>OpenCvSharp.Analyzers</RootNamespace>
8+
<AssemblyName>OpenCvSharp.Analyzers</AssemblyName>
9+
<IsRoslynComponent>true</IsRoslynComponent>
10+
<!-- EnforceExtendedAnalyzerRules ensures the analyzer follows Roslyn analyzer best practices -->
11+
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
12+
<!-- Suppress build output from the package; it goes into analyzers/ folder only -->
13+
<IncludeBuildOutput>false</IncludeBuildOutput>
14+
<GenerateDocumentationFile>true</GenerateDocumentationFile>
15+
<!-- DiagnosticId, SupportedDiagnostics, Initialize are required overrides; XML comments not needed -->
16+
<NoWarn>CS1591</NoWarn>
17+
</PropertyGroup>
18+
19+
<ItemGroup>
20+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />
21+
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
22+
</ItemGroup>
23+
24+
<ItemGroup>
25+
<AdditionalFiles Include="AnalyzerReleases.Shipped.md" />
26+
<AdditionalFiles Include="AnalyzerReleases.Unshipped.md" />
27+
</ItemGroup>
28+
29+
</Project>
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
using System.Collections.Immutable;
2+
using System.Linq;
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.CSharp;
5+
using Microsoft.CodeAnalysis.CSharp.Syntax;
6+
using Microsoft.CodeAnalysis.Diagnostics;
7+
8+
namespace OpenCvSharp.Analyzers;
9+
10+
/// <summary>
11+
/// Detects the misuse pattern <c>mat.Row(i).At&lt;T&gt;(col)</c> where the column index is
12+
/// silently interpreted as a row index, causing out-of-bounds memory access.
13+
/// </summary>
14+
/// <remarks>
15+
/// <para>
16+
/// <c>Mat.Row(i)</c> returns a 1×N submatrix. <c>At&lt;T&gt;(int i0)</c> on a 2D matrix
17+
/// treats <c>i0</c> as the row index (dimension 0), not a column index. The resulting
18+
/// address is <c>data + step * col</c> — jumping entire rows past the end of the submatrix.
19+
/// </para>
20+
/// <para>
21+
/// Use <c>mat.At&lt;T&gt;(row, col)</c> or <c>mat.AsRows&lt;T&gt;()</c> instead.
22+
/// See https://github.com/shimat/opencvsharp/issues/1775
23+
/// </para>
24+
/// </remarks>
25+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
26+
public sealed class RowAtAnalyzer : DiagnosticAnalyzer
27+
{
28+
public const string DiagnosticId = "OCVS001";
29+
30+
private static readonly DiagnosticDescriptor Rule = new(
31+
id: DiagnosticId,
32+
title: "At<T>(int) called on a Mat row submatrix",
33+
messageFormat: "At<T>(int) on the 1-row Mat returned by Row() treats the argument as a row index, " +
34+
"not a column index, silently producing wrong results or out-of-bounds access. " +
35+
"Use mat.At<T>(row, col) or mat.AsRows<T>() instead.",
36+
category: "Correctness",
37+
defaultSeverity: DiagnosticSeverity.Warning,
38+
isEnabledByDefault: true,
39+
description: "Mat.Row(i) returns a 2D 1×N submatrix. At<T>(int i0) interprets i0 as a row " +
40+
"index (dimension 0), not a column index. Access the parent matrix directly: " +
41+
"mat.At<T>(row, col), or use mat.AsRows<T>() for high-performance loops.",
42+
helpLinkUri: "https://github.com/shimat/opencvsharp/issues/1775");
43+
44+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
45+
=> ImmutableArray.Create(Rule);
46+
47+
public override void Initialize(AnalysisContext context)
48+
{
49+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
50+
context.EnableConcurrentExecution();
51+
context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression);
52+
}
53+
54+
private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context)
55+
{
56+
var atInvocation = (InvocationExpressionSyntax)context.Node;
57+
58+
if (!IsMatAtIntCall(atInvocation, context.SemanticModel))
59+
return;
60+
61+
if (atInvocation.Expression is not MemberAccessExpressionSyntax memberAccess)
62+
return;
63+
64+
var receiver = memberAccess.Expression;
65+
66+
// Case 1: direct chain — mat.Row(i).At<T>(c)
67+
if (IsMatRowCall(receiver, context.SemanticModel))
68+
{
69+
context.ReportDiagnostic(Diagnostic.Create(Rule, atInvocation.GetLocation()));
70+
return;
71+
}
72+
73+
// Case 2: local variable — var row = mat.Row(i); row.At<T>(c)
74+
if (receiver is IdentifierNameSyntax identifier &&
75+
context.SemanticModel.GetSymbolInfo(identifier).Symbol is ILocalSymbol local &&
76+
IsLocalDeclaredFromRowCall(local, context.SemanticModel))
77+
{
78+
context.ReportDiagnostic(Diagnostic.Create(Rule, atInvocation.GetLocation()));
79+
}
80+
}
81+
82+
/// <summary>
83+
/// Returns true when <paramref name="expr"/> is a call to <c>OpenCvSharp.Mat.At&lt;T&gt;(int)</c>.
84+
/// </summary>
85+
private static bool IsMatAtIntCall(InvocationExpressionSyntax expr, SemanticModel model)
86+
{
87+
if (expr.Expression is not MemberAccessExpressionSyntax ma)
88+
return false;
89+
if (ma.Name.Identifier.ValueText != "At")
90+
return false;
91+
if (expr.ArgumentList.Arguments.Count != 1)
92+
return false;
93+
94+
var sym = model.GetSymbolInfo(expr).Symbol as IMethodSymbol;
95+
return sym is { Parameters.Length: 1 } &&
96+
sym.ContainingType?.ToDisplayString() == "OpenCvSharp.Mat" &&
97+
sym.Parameters[0].Type.SpecialType == SpecialType.System_Int32;
98+
}
99+
100+
/// <summary>
101+
/// Returns true when <paramref name="expr"/> is a call to <c>OpenCvSharp.Mat.Row(int)</c>.
102+
/// </summary>
103+
private static bool IsMatRowCall(ExpressionSyntax expr, SemanticModel model)
104+
{
105+
if (expr is not InvocationExpressionSyntax inv)
106+
return false;
107+
if (inv.Expression is not MemberAccessExpressionSyntax ma)
108+
return false;
109+
if (ma.Name.Identifier.ValueText != "Row")
110+
return false;
111+
112+
var sym = model.GetSymbolInfo(inv).Symbol as IMethodSymbol;
113+
return sym?.ContainingType?.ToDisplayString() == "OpenCvSharp.Mat";
114+
}
115+
116+
/// <summary>
117+
/// Returns true when the local variable's declaration initializer is a call to <c>Mat.Row(int)</c>.
118+
/// Covers: <c>var row = mat.Row(i);</c>
119+
/// </summary>
120+
private static bool IsLocalDeclaredFromRowCall(ILocalSymbol local, SemanticModel model)
121+
{
122+
var syntaxRef = local.DeclaringSyntaxReferences.FirstOrDefault();
123+
if (syntaxRef?.GetSyntax() is not VariableDeclaratorSyntax { Initializer.Value: { } initializer })
124+
return false;
125+
126+
return IsMatRowCall(initializer, model);
127+
}
128+
}

src/OpenCvSharp/OpenCvSharp.csproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@
4444
<ItemGroup>
4545
<PackageReference Include="System.Memory" Version="4.6.3" />
4646
</ItemGroup>
47+
48+
<ItemGroup>
49+
<!-- Include the analyzer in this package under analyzers/dotnet/cs/ -->
50+
<ProjectReference Include="..\OpenCvSharp.Analyzers\OpenCvSharp.Analyzers.csproj"
51+
ReferenceOutputAssembly="false"
52+
OutputItemType="Analyzer"
53+
SetTargetFramework="TargetFramework=netstandard2.0" />
54+
</ItemGroup>
4755
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.1'">
4856
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.1.2" />
4957
</ItemGroup>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<LangVersion>12</LangVersion>
6+
<Nullable>enable</Nullable>
7+
<ImplicitUsings>enable</ImplicitUsings>
8+
<IsPackable>false</IsPackable>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
13+
<PackageReference Include="xunit" Version="2.7.0" />
14+
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7" PrivateAssets="all" />
15+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit" Version="1.1.2" />
16+
<!-- Override the old CodeAnalysis pulled in transitively by the testing framework -->
17+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
18+
</ItemGroup>
19+
20+
<ItemGroup>
21+
<ProjectReference Include="..\..\src\OpenCvSharp.Analyzers\OpenCvSharp.Analyzers.csproj" />
22+
</ItemGroup>
23+
24+
</Project>
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
using Microsoft.CodeAnalysis.CSharp.Testing;
2+
using Microsoft.CodeAnalysis.Testing;
3+
using Microsoft.CodeAnalysis.Testing.Verifiers;
4+
using OpenCvSharp.Analyzers;
5+
using Xunit;
6+
7+
namespace OpenCvSharp.Analyzers.Tests;
8+
9+
public class RowAtAnalyzerTests
10+
{
11+
// Minimal stub of OpenCvSharp.Mat — lets the analyzer resolve types
12+
// without requiring the actual OpenCvSharp assembly.
13+
private const string MatStub = """
14+
namespace OpenCvSharp
15+
{
16+
public class Mat
17+
{
18+
public Mat Row(int y) => this;
19+
public ref T At<T>(int i0) where T : unmanaged => throw null!;
20+
public ref T At<T>(int i0, int i1) where T : unmanaged => throw null!;
21+
}
22+
public struct Vec3b { }
23+
}
24+
""";
25+
26+
private static Task Verify(string source, params DiagnosticResult[] expected)
27+
{
28+
var test = new CSharpAnalyzerTest<RowAtAnalyzer, XUnitVerifier>
29+
{
30+
TestCode = MatStub + source,
31+
};
32+
test.ExpectedDiagnostics.AddRange(expected);
33+
return test.RunAsync();
34+
}
35+
36+
[Fact]
37+
public Task DirectChain_ReportsWarning() => Verify(
38+
"""
39+
class Test
40+
{
41+
void M(OpenCvSharp.Mat mat)
42+
{
43+
var _ = {|#0:mat.Row(1).At<OpenCvSharp.Vec3b>(2)|};
44+
}
45+
}
46+
""",
47+
DiagnosticResult.CompilerWarning(RowAtAnalyzer.DiagnosticId).WithLocation(0));
48+
49+
[Fact]
50+
public Task LocalVariable_ReportsWarning() => Verify(
51+
"""
52+
class Test
53+
{
54+
void M(OpenCvSharp.Mat mat)
55+
{
56+
var row = mat.Row(1);
57+
var _ = {|#0:row.At<OpenCvSharp.Vec3b>(2)|};
58+
}
59+
}
60+
""",
61+
DiagnosticResult.CompilerWarning(RowAtAnalyzer.DiagnosticId).WithLocation(0));
62+
63+
[Fact]
64+
public Task TwoArgAt_NoWarning() => Verify(
65+
"""
66+
class Test
67+
{
68+
void M(OpenCvSharp.Mat mat)
69+
{
70+
var _ = mat.At<OpenCvSharp.Vec3b>(1, 2);
71+
}
72+
}
73+
""");
74+
75+
[Fact]
76+
public Task AtOnFullMatrix_NoWarning() => Verify(
77+
"""
78+
class Test
79+
{
80+
void M(OpenCvSharp.Mat mat)
81+
{
82+
var _ = mat.At<OpenCvSharp.Vec3b>(1);
83+
}
84+
}
85+
""");
86+
87+
[Fact]
88+
public Task LocalAssignedFromNonRowMethod_NoWarning() => Verify(
89+
"""
90+
class Test
91+
{
92+
void M(OpenCvSharp.Mat mat, OpenCvSharp.Mat other)
93+
{
94+
var row = other;
95+
var _ = row.At<OpenCvSharp.Vec3b>(2);
96+
}
97+
}
98+
""");
99+
}

0 commit comments

Comments
 (0)