Skip to content

Commit d721631

Browse files
authored
Merge pull request #1874 from shimat/claude/exciting-jackson-eaf946
Fix Row()/At<T> misuse and add Span-based pixel access APIs (fixes #1775)
2 parents ec06732 + 9757c5c commit d721631

21 files changed

Lines changed: 1236 additions & 16 deletions
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: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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
8+
OCVS002 | Performance | Warning | Mat property (Rows/Cols/Dims/Width/Height) in loop condition
9+
OCVS003 | Performance | Warning | Mat.Row() / Mat.Col() called inside a loop body
10+
OCVS004 | Reliability | Warning | Mat submatrix (Row/Col/RowRange/ColRange) not disposed
11+
OCVS005 | Reliability | Warning | Intermediate MatExpr not disposed in chained Mat arithmetic
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
using System.Collections.Immutable;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CSharp;
4+
using Microsoft.CodeAnalysis.CSharp.Syntax;
5+
using Microsoft.CodeAnalysis.Diagnostics;
6+
7+
namespace OpenCvSharp.Analyzers;
8+
9+
/// <summary>
10+
/// Detects chained arithmetic on <c>Mat</c> (e.g. <c>a + b + c</c>) where intermediate
11+
/// <c>MatExpr</c> objects are never disposed.
12+
/// </summary>
13+
/// <remarks>
14+
/// <para>
15+
/// <c>Mat</c> arithmetic operators return <c>MatExpr</c> (an <see cref="System.IDisposable"/> wrapper
16+
/// around a native lazy-evaluation expression). In a chain such as <c>a + b + c</c>, the
17+
/// <c>MatExpr</c> produced by <c>a + b</c> is consumed by the second <c>+</c> and never disposed,
18+
/// leaving the native object to the GC finalizer.
19+
/// </para>
20+
/// <para>
21+
/// Prefer <c>Cv2.Add</c> / <c>Cv2.Subtract</c> etc., or assign each step to a <c>using</c>
22+
/// variable.
23+
/// </para>
24+
/// </remarks>
25+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
26+
public sealed class MatExprChainAnalyzer : DiagnosticAnalyzer
27+
{
28+
public const string DiagnosticId = "OCVS005";
29+
30+
private static readonly DiagnosticDescriptor Rule = new(
31+
id: DiagnosticId,
32+
title: "Intermediate MatExpr not disposed in chained Mat arithmetic",
33+
messageFormat: "The intermediate MatExpr from '{0}' is never disposed. " +
34+
"Use Cv2.Add/Subtract/Multiply/Divide, or assign each step to a 'using' variable.",
35+
category: "Reliability",
36+
defaultSeverity: DiagnosticSeverity.Warning,
37+
isEnabledByDefault: true,
38+
description: "Mat arithmetic operators return MatExpr, an IDisposable wrapping a native " +
39+
"lazy-evaluation object. In a chain like a + b + c, the MatExpr for a + b " +
40+
"is consumed without being disposed, leaking the native resource until " +
41+
"the GC finalizer runs.",
42+
helpLinkUri: "https://github.com/shimat/opencvsharp/issues/1775");
43+
44+
private const string MatExprFullName = "OpenCvSharp.MatExpr";
45+
46+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
47+
=> ImmutableArray.Create(Rule);
48+
49+
public override void Initialize(AnalysisContext context)
50+
{
51+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
52+
context.EnableConcurrentExecution();
53+
context.RegisterSyntaxNodeAction(AnalyzeBinaryExpression, SyntaxKind.AddExpression,
54+
SyntaxKind.SubtractExpression, SyntaxKind.MultiplyExpression, SyntaxKind.DivideExpression);
55+
}
56+
57+
private static void AnalyzeBinaryExpression(SyntaxNodeAnalysisContext context)
58+
{
59+
var binary = (BinaryExpressionSyntax)context.Node;
60+
61+
// The result of this expression must be MatExpr
62+
var resultType = context.SemanticModel.GetTypeInfo(binary).Type;
63+
if (resultType?.ToDisplayString() != MatExprFullName)
64+
return;
65+
66+
// Flag only when this MatExpr is consumed immediately by another binary expression
67+
// without being assigned to a variable — that's the intermediate-temporary case.
68+
// Unwrap any parentheses: (a + b) + c has a ParenthesizedExpressionSyntax between the
69+
// inner binary and the outer one, but the semantics are identical.
70+
var parent = binary.Parent;
71+
while (parent is ParenthesizedExpressionSyntax)
72+
parent = parent.Parent;
73+
if (parent is BinaryExpressionSyntax or AssignmentExpressionSyntax { Left: not IdentifierNameSyntax })
74+
{
75+
var operatorToken = binary.OperatorToken.ToString();
76+
context.ReportDiagnostic(Diagnostic.Create(Rule, binary.GetLocation(), operatorToken));
77+
}
78+
}
79+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
using System;
2+
using System.Collections.Immutable;
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 <c>mat.Rows</c>, <c>mat.Cols</c>, <c>mat.Dims</c>, <c>mat.Width</c>, or <c>mat.Height</c>
12+
/// used in loop conditions, where each access is a P/Invoke call evaluated on every iteration.
13+
/// </summary>
14+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
15+
public sealed class MatPropertyInLoopConditionAnalyzer : DiagnosticAnalyzer
16+
{
17+
public const string DiagnosticId = "OCVS002";
18+
19+
private static readonly DiagnosticDescriptor Rule = new(
20+
id: DiagnosticId,
21+
title: "Mat property accessed in loop condition",
22+
messageFormat: "'{0}' is a P/Invoke call on every iteration; cache it before the loop as 'int {1} = mat.{0};'",
23+
category: "Performance",
24+
defaultSeverity: DiagnosticSeverity.Warning,
25+
isEnabledByDefault: true,
26+
description: "Mat.Rows, Mat.Cols, Mat.Dims, Mat.Width, and Mat.Height each invoke a native " +
27+
"P/Invoke call. Placing them in a loop condition causes one P/Invoke call per " +
28+
"iteration. Cache the value in a local variable before the loop.",
29+
helpLinkUri: "https://github.com/shimat/opencvsharp/issues/1775");
30+
31+
// Properties on OpenCvSharp.Mat that are P/Invoke calls
32+
private static readonly ImmutableHashSet<string> ExpensiveProperties =
33+
ImmutableHashSet.Create(StringComparer.Ordinal, "Rows", "Cols", "Dims", "Width", "Height");
34+
35+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
36+
=> ImmutableArray.Create(Rule);
37+
38+
public override void Initialize(AnalysisContext context)
39+
{
40+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
41+
context.EnableConcurrentExecution();
42+
context.RegisterSyntaxNodeAction(AnalyzeFor, SyntaxKind.ForStatement);
43+
context.RegisterSyntaxNodeAction(AnalyzeWhile, SyntaxKind.WhileStatement);
44+
context.RegisterSyntaxNodeAction(AnalyzeDo, SyntaxKind.DoStatement);
45+
}
46+
47+
private static void AnalyzeFor(SyntaxNodeAnalysisContext context)
48+
{
49+
var forLoop = (ForStatementSyntax)context.Node;
50+
if (forLoop.Condition is not null)
51+
CheckExpression(forLoop.Condition, context);
52+
}
53+
54+
private static void AnalyzeWhile(SyntaxNodeAnalysisContext context)
55+
{
56+
var whileLoop = (WhileStatementSyntax)context.Node;
57+
CheckExpression(whileLoop.Condition, context);
58+
}
59+
60+
private static void AnalyzeDo(SyntaxNodeAnalysisContext context)
61+
{
62+
var doLoop = (DoStatementSyntax)context.Node;
63+
CheckExpression(doLoop.Condition, context);
64+
}
65+
66+
private static void CheckExpression(ExpressionSyntax condition, SyntaxNodeAnalysisContext context)
67+
{
68+
foreach (var node in condition.DescendantNodesAndSelf())
69+
{
70+
if (node is not MemberAccessExpressionSyntax ma)
71+
continue;
72+
73+
var propName = ma.Name.Identifier.ValueText;
74+
if (!ExpensiveProperties.Contains(propName))
75+
continue;
76+
77+
var symbol = context.SemanticModel.GetSymbolInfo(ma).Symbol as IPropertySymbol;
78+
if (symbol?.ContainingType?.ToDisplayString() != "OpenCvSharp.Mat")
79+
continue;
80+
81+
var suggestedName = char.ToLowerInvariant(propName[0]) + propName.Substring(1);
82+
context.ReportDiagnostic(Diagnostic.Create(Rule, ma.GetLocation(), propName, suggestedName));
83+
}
84+
}
85+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
<!-- AssemblyVersion must be a valid major.minor.build.revision (each ≤ 65535).
10+
The repo uses date-based Version (e.g. 4.13.0.20260524) which exceeds that limit,
11+
so we pin the assembly version independently. -->
12+
<AssemblyVersion>1.0.0.0</AssemblyVersion>
13+
<FileVersion>1.0.0.0</FileVersion>
14+
<IsRoslynComponent>true</IsRoslynComponent>
15+
<!-- EnforceExtendedAnalyzerRules ensures the analyzer follows Roslyn analyzer best practices -->
16+
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
17+
<!-- Suppress build output from the package; it goes into analyzers/ folder only -->
18+
<IncludeBuildOutput>false</IncludeBuildOutput>
19+
<GenerateDocumentationFile>true</GenerateDocumentationFile>
20+
<!-- DiagnosticId, SupportedDiagnostics, Initialize are required overrides; XML comments not needed -->
21+
<NoWarn>CS1591</NoWarn>
22+
</PropertyGroup>
23+
24+
<ItemGroup>
25+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />
26+
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
27+
</ItemGroup>
28+
29+
<ItemGroup>
30+
<AdditionalFiles Include="AnalyzerReleases.Shipped.md" />
31+
<AdditionalFiles Include="AnalyzerReleases.Unshipped.md" />
32+
</ItemGroup>
33+
34+
</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+
}

0 commit comments

Comments
 (0)