Skip to content

Commit 809b19e

Browse files
rolfbjarneCopilot
andauthored
[src] Transient strings must always be disposed. (#24894)
Fix a couple of places where transient strings weren't disposed. And to make sure this doesn't happen again, add a new Roslyn analyzer (RBI0042) that reports an error when transient strings are declared as local variables without the 'using' keyword. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent cd5bbb0 commit 809b19e

9 files changed

Lines changed: 321 additions & 3 deletions

File tree

src/CoreGraphics/CGFont.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ protected internal override void Release ()
125125
// and have a unit tests to make sure this behavior does not change over time
126126
if (name is null)
127127
return null;
128-
var nameHandle = new TransientCFString (name);
128+
using var nameHandle = new TransientCFString (name);
129129
return Create (CGFontCreateWithFontName (nameHandle));
130130
}
131131

src/Network/NWEthernetChannel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ public void Send (ReadOnlySpan<byte> content, ushort vlanTag, string remoteAddre
113113
unsafe {
114114
delegate* unmanaged<IntPtr, IntPtr, void> trampoline = &TrampolineSendCompletion;
115115
using var block = new BlockLiteral (trampoline, callback, typeof (NWEthernetChannel), nameof (TrampolineSendCompletion));
116-
var remoteAddressStr = new TransientString (remoteAddress);
116+
using var remoteAddressStr = new TransientString (remoteAddress);
117117
nw_ethernet_channel_send (GetCheckedHandle (), dispatchData.GetHandle (), vlanTag, remoteAddressStr, &block);
118118
}
119119
}

src/ObjCRuntime/Protocol.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public static IntPtr GetHandle (string name)
8989

9090
internal static IntPtr objc_getProtocol (string? name)
9191
{
92-
var namePtr = new TransientString (name);
92+
using var namePtr = new TransientString (name);
9393
return objc_getProtocol (namePtr);
9494
}
9595

src/rgen/Microsoft.Macios.Bindings.Analyzer/AnalyzerReleases.Unshipped.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@
2323
| RBI0017 | Usage | Error | Field used with the wrong flag. |
2424
| RBI0018 | Usage | Error | The export attribute must have a nonnull selector. |
2525
| RBI0019 | Usage | Error | The selector string cannot contain any whitespace characters. |
26+
| RBI0042 | Usage | Error | Transient disposable type not declared with 'using'. |

src/rgen/Microsoft.Macios.Bindings.Analyzer/Resources.Designer.cs

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/rgen/Microsoft.Macios.Bindings.Analyzer/Resources.resx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,4 +560,17 @@
560560
<value>Protocol constructor overlap:</value>
561561
</data>
562562

563+
<!-- RBI0042 -->
564+
565+
<data name="RBI0042Description" xml:space="preserve">
566+
<value>Transient types (TransientString, TransientCFString, TransientCFObject) allocate native memory and must be disposed. Use the 'using' keyword to ensure proper cleanup.</value>
567+
</data>
568+
<data name="RBI0042MessageFormat" xml:space="preserve">
569+
<value>Variable '{0}' of type '{1}' must be declared with the 'using' keyword to ensure proper disposal of native resources</value>
570+
<comment>{0} is the name of the variable, {1} is the type name.</comment>
571+
</data>
572+
<data name="RBI0042Title" xml:space="preserve">
573+
<value>Transient disposable type not declared with 'using'</value>
574+
</data>
575+
563576
</root>

src/rgen/Microsoft.Macios.Bindings.Analyzer/RgenDiagnostics.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,4 +631,20 @@ public static class RgenDiagnostics {
631631
description: new LocalizableResourceString (nameof (Resources.RBI0041Description), Resources.ResourceManager,
632632
typeof (Resources))
633633
);
634+
635+
/// <summary>
636+
/// Diagnostic descriptor for when a transient disposable type (TransientString, TransientCFString, TransientCFObject)
637+
/// is not declared with the 'using' keyword.
638+
/// </summary>
639+
internal static readonly DiagnosticDescriptor RBI0042 = new (
640+
"RBI0042",
641+
new LocalizableResourceString (nameof (Resources.RBI0042Title), Resources.ResourceManager, typeof (Resources)),
642+
new LocalizableResourceString (nameof (Resources.RBI0042MessageFormat), Resources.ResourceManager,
643+
typeof (Resources)),
644+
"Usage",
645+
DiagnosticSeverity.Error,
646+
isEnabledByDefault: true,
647+
description: new LocalizableResourceString (nameof (Resources.RBI0042Description), Resources.ResourceManager,
648+
typeof (Resources))
649+
);
634650
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Collections.Immutable;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CSharp;
7+
using Microsoft.CodeAnalysis.CSharp.Syntax;
8+
using Microsoft.CodeAnalysis.Diagnostics;
9+
using static Microsoft.Macios.Generator.RgenDiagnostics;
10+
11+
namespace Microsoft.Macios.Bindings.Analyzer;
12+
13+
/// <summary>
14+
/// Analyzer to ensure that transient disposable types (TransientString, TransientCFString, TransientCFObject)
15+
/// are always declared with the 'using' keyword to guarantee proper disposal of native resources.
16+
/// </summary>
17+
[DiagnosticAnalyzer (LanguageNames.CSharp)]
18+
public class TransientDisposableAnalyzer : DiagnosticAnalyzer {
19+
20+
static readonly ImmutableHashSet<string> transientTypeFullNames = ImmutableHashSet.Create (
21+
"ObjCRuntime.TransientString",
22+
"ObjCRuntime.TransientCFString",
23+
"ObjCRuntime.TransientCFObject"
24+
);
25+
26+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create (RBI0042);
27+
28+
public override void Initialize (AnalysisContext context)
29+
{
30+
context.ConfigureGeneratedCodeAnalysis (GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
31+
context.EnableConcurrentExecution ();
32+
context.RegisterSyntaxNodeAction (AnalyzeNode, SyntaxKind.LocalDeclarationStatement);
33+
}
34+
35+
void AnalyzeNode (SyntaxNodeAnalysisContext context)
36+
{
37+
if (context.Node is not LocalDeclarationStatementSyntax localDeclaration)
38+
return;
39+
40+
// If the declaration already has 'using', it's fine.
41+
if (localDeclaration.UsingKeyword != default)
42+
return;
43+
44+
foreach (var variable in localDeclaration.Declaration.Variables) {
45+
var symbol = context.SemanticModel.GetDeclaredSymbol (variable);
46+
if (symbol is not ILocalSymbol localSymbol)
47+
continue;
48+
49+
var typeDisplayName = localSymbol.Type.ToDisplayString ();
50+
if (!transientTypeFullNames.Contains (typeDisplayName))
51+
continue;
52+
53+
var diagnostic = Diagnostic.Create (RBI0042, variable.GetLocation (), variable.Identifier.Text, localSymbol.Type.Name);
54+
context.ReportDiagnostic (diagnostic);
55+
}
56+
}
57+
}
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
using System.Collections;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Threading.Tasks;
7+
using Microsoft.CodeAnalysis;
8+
using Xamarin.Tests;
9+
using Xamarin.Utils;
10+
using Xunit;
11+
12+
namespace Microsoft.Macios.Bindings.Analyzer.Tests;
13+
14+
public class TransientDisposableAnalyzerTests : BaseGeneratorWithAnalyzerTestClass {
15+
class ErrorTestCases : IEnumerable<object []> {
16+
public IEnumerator<object []> GetEnumerator ()
17+
{
18+
// TransientString without using
19+
yield return [
20+
"""
21+
using System;
22+
23+
namespace ObjCRuntime {
24+
struct TransientString : IDisposable {
25+
public void Dispose () { }
26+
}
27+
}
28+
29+
class Test
30+
{
31+
void Method ()
32+
{
33+
var s = new ObjCRuntime.TransientString ();
34+
}
35+
}
36+
"""];
37+
38+
// TransientCFString without using
39+
yield return [
40+
"""
41+
using System;
42+
43+
namespace ObjCRuntime {
44+
ref struct TransientCFString {
45+
public void Dispose () { }
46+
}
47+
}
48+
49+
class Test
50+
{
51+
void Method ()
52+
{
53+
ObjCRuntime.TransientCFString s = new ObjCRuntime.TransientCFString ();
54+
}
55+
}
56+
"""];
57+
58+
// TransientCFObject without using
59+
yield return [
60+
"""
61+
using System;
62+
63+
namespace ObjCRuntime {
64+
ref struct TransientCFObject {
65+
public void Dispose () { }
66+
}
67+
}
68+
69+
class Test
70+
{
71+
void Method ()
72+
{
73+
var obj = new ObjCRuntime.TransientCFObject ();
74+
}
75+
}
76+
"""];
77+
}
78+
79+
IEnumerator IEnumerable.GetEnumerator () => GetEnumerator ();
80+
}
81+
82+
class NoErrorTestCases : IEnumerable<object []> {
83+
public IEnumerator<object []> GetEnumerator ()
84+
{
85+
// TransientString with using var
86+
yield return [
87+
"""
88+
using System;
89+
90+
namespace ObjCRuntime {
91+
struct TransientString : IDisposable {
92+
public void Dispose () { }
93+
}
94+
}
95+
96+
class Test
97+
{
98+
void Method ()
99+
{
100+
using var s = new ObjCRuntime.TransientString ();
101+
}
102+
}
103+
"""];
104+
105+
// TransientCFString with using var
106+
yield return [
107+
"""
108+
using System;
109+
110+
namespace ObjCRuntime {
111+
ref struct TransientCFString {
112+
public void Dispose () { }
113+
}
114+
}
115+
116+
class Test
117+
{
118+
void Method ()
119+
{
120+
using var s = new ObjCRuntime.TransientCFString ();
121+
}
122+
}
123+
"""];
124+
125+
// TransientCFObject with using var
126+
yield return [
127+
"""
128+
using System;
129+
130+
namespace ObjCRuntime {
131+
ref struct TransientCFObject {
132+
public void Dispose () { }
133+
}
134+
}
135+
136+
class Test
137+
{
138+
void Method ()
139+
{
140+
using var obj = new ObjCRuntime.TransientCFObject ();
141+
}
142+
}
143+
"""];
144+
145+
// Non-transient type without using is fine
146+
yield return [
147+
"""
148+
using System;
149+
150+
struct SomeOtherStruct : IDisposable {
151+
public void Dispose () { }
152+
}
153+
154+
class Test
155+
{
156+
void Method ()
157+
{
158+
var s = new SomeOtherStruct ();
159+
}
160+
}
161+
"""];
162+
163+
// User-defined TransientString outside ObjCRuntime namespace should not trigger
164+
yield return [
165+
"""
166+
using System;
167+
168+
struct TransientString : IDisposable {
169+
public void Dispose () { }
170+
}
171+
172+
class Test
173+
{
174+
void Method ()
175+
{
176+
var s = new TransientString ();
177+
}
178+
}
179+
"""];
180+
}
181+
182+
IEnumerator IEnumerable.GetEnumerator () => GetEnumerator ();
183+
}
184+
185+
[Theory]
186+
[AllSupportedPlatformsClassData<ErrorTestCases>]
187+
public async Task TransientWithoutUsingTests (ApplePlatform platform, string inputText)
188+
{
189+
var (compilation, _) = CreateCompilation (platform, sources: inputText);
190+
var diagnostics = await RunAnalyzer (new TransientDisposableAnalyzer (), compilation);
191+
var analyzerDiagnostics = diagnostics.Where (d => d.Id == "RBI0042").ToArray ();
192+
Assert.True (analyzerDiagnostics.Length != 0);
193+
}
194+
195+
[Theory]
196+
[AllSupportedPlatformsClassData<NoErrorTestCases>]
197+
public async Task TransientWithUsingTests (ApplePlatform platform, string inputText)
198+
{
199+
var (compilation, _) = CreateCompilation (platform, sources: inputText);
200+
var diagnostics = await RunAnalyzer (new TransientDisposableAnalyzer (), compilation);
201+
var analyzerDiagnostics = diagnostics.Where (d => d.Id == "RBI0042").ToArray ();
202+
Assert.Empty (analyzerDiagnostics);
203+
}
204+
}

0 commit comments

Comments
 (0)