@@ -17,12 +17,12 @@ public class DisposableInConstructorAnalyzer : DiagnosticAnalyzer
1717{
1818 private static readonly DiagnosticDescriptor Rule = new (
1919 id : DiagnosticIds . DisposableInConstructor ,
20- title : "Exception in constructor with disposable " ,
21- messageFormat : "Constructor initializes disposable field '{0}' but doesn't handle exceptions. Resources may leak if constructor fails " ,
22- category : "Reliability " ,
20+ title : "Disposable created in constructor is not disposed " ,
21+ messageFormat : "Disposable '{0}' created in constructor is not stored or disposed " ,
22+ category : "Resource Management " ,
2323 defaultSeverity : DiagnosticSeverity . Warning ,
2424 isEnabledByDefault : true ,
25- description : "When initializing disposable fields in constructors, wrap initialization in try-catch to dispose resources if construction fails ." ) ;
25+ description : "Disposables created in constructors should either be stored for later disposal or wrapped in a using/ try-finally to avoid leaks ." ) ;
2626
2727 public override ImmutableArray < DiagnosticDescriptor > SupportedDiagnostics => ImmutableArray . Create ( Rule ) ;
2828
@@ -43,48 +43,182 @@ private void AnalyzeOperationBlockStart(OperationBlockStartAnalysisContext conte
4343 if ( method . MethodKind != MethodKind . Constructor )
4444 return ;
4545
46- var disposableFieldAssignments = new List < IFieldSymbol > ( ) ;
47- var hasTryCatch = false ;
46+ var trackedLocals = new Dictionary < ILocalSymbol , LocalInfo > ( SymbolEqualityComparer . Default ) ;
4847
4948 context . RegisterOperationAction ( operationContext =>
5049 {
51- // Track disposable field assignments
52- if ( operationContext . Operation is IAssignmentOperation assignment )
50+ var creation = ( IObjectCreationOperation ) operationContext . Operation ;
51+
52+ if ( ! DisposableHelper . IsAnyDisposableType ( creation . Type ) )
53+ return ;
54+
55+ if ( DisposableHelper . IsInUsingStatement ( creation . Syntax ) )
56+ return ;
57+
58+ if ( IsAssignedToInstanceMember ( creation ) )
59+ return ;
60+
61+ if ( IsPartOfBaseConstructorInitializer ( creation ) )
62+ return ;
63+
64+ var local = GetAssignedLocal ( creation ) ;
65+ if ( local != null )
5366 {
54- if ( assignment . Target is IFieldReferenceOperation fieldRef )
67+ trackedLocals [ local ] = new LocalInfo ( creation . Syntax . GetLocation ( ) ) ;
68+ }
69+ } , OperationKind . ObjectCreation ) ;
70+
71+ // Track explicit disposal calls
72+ context . RegisterOperationAction ( operationContext =>
73+ {
74+ var operation = operationContext . Operation ;
75+ if ( DisposableHelper . IsDisposalCall ( operation , out _ ) )
76+ {
77+ var local = GetTargetLocal ( operation ) ;
78+ if ( local != null && trackedLocals . TryGetValue ( local , out var info ) )
5579 {
56- if ( DisposableHelper . IsAnyDisposableType ( fieldRef . Field . Type ) )
57- {
58- // Check if RHS creates a disposable
59- if ( assignment . Value is IObjectCreationOperation )
60- {
61- disposableFieldAssignments . Add ( fieldRef . Field ) ;
62- }
63- }
80+ info . IsDisposed = true ;
81+ trackedLocals [ local ] = info ;
6482 }
6583 }
84+ } , OperationKind . Invocation , OperationKind . ConditionalAccess ) ;
6685
67- // Check for try-catch blocks
68- if ( operationContext . Operation is ITryOperation )
86+ // Track simple escape scenarios (assignment to field/property or return)
87+ context . RegisterOperationAction ( operationContext =>
88+ {
89+ switch ( operationContext . Operation )
6990 {
70- hasTryCatch = true ;
91+ case IAssignmentOperation assignment :
92+ if ( assignment . Value is ILocalReferenceOperation localRef &&
93+ trackedLocals . TryGetValue ( localRef . Local , out var info ) &&
94+ ( assignment . Target is IFieldReferenceOperation or IPropertyReferenceOperation ) )
95+ {
96+ info . Escapes = true ;
97+ trackedLocals [ localRef . Local ] = info ;
98+ }
99+ break ;
100+
101+ case IReturnOperation returnOp :
102+ if ( returnOp . ReturnedValue is ILocalReferenceOperation returnedLocal &&
103+ trackedLocals . TryGetValue ( returnedLocal . Local , out var info2 ) )
104+ {
105+ info2 . Escapes = true ;
106+ trackedLocals [ returnedLocal . Local ] = info2 ;
107+ }
108+ break ;
71109 }
72- } , OperationKind . SimpleAssignment , OperationKind . Try ) ;
110+ } , OperationKind . SimpleAssignment , OperationKind . Return ) ;
73111
74112 context . RegisterOperationBlockEndAction ( blockEndContext =>
75113 {
76- // If we have disposable field assignments and no try-catch, warn
77- if ( disposableFieldAssignments . Count > 0 && ! hasTryCatch )
114+ if ( trackedLocals . Count == 0 )
115+ return ;
116+
117+ foreach ( var kvp in trackedLocals )
118+ {
119+ var local = kvp . Key ;
120+ var info = kvp . Value ;
121+
122+ if ( info . IsDisposed || info . Escapes )
123+ continue ;
124+
125+ var diagnostic = Diagnostic . Create (
126+ Rule ,
127+ info . Location ,
128+ local . Type . Name ) ;
129+ blockEndContext . ReportDiagnostic ( diagnostic ) ;
130+ }
131+ } ) ;
132+ }
133+
134+ private static ILocalSymbol ? GetAssignedLocal ( IObjectCreationOperation creation )
135+ {
136+ var parent = creation . Parent ;
137+ while ( parent != null )
138+ {
139+ switch ( parent )
78140 {
79- foreach ( var field in disposableFieldAssignments )
141+ case IVariableDeclaratorOperation declarator :
142+ return declarator . Symbol as ILocalSymbol ;
143+ case IVariableInitializerOperation initializer when initializer . Parent is IVariableDeclaratorOperation declarator :
144+ return declarator . Symbol as ILocalSymbol ;
145+ case IAssignmentOperation assignment when assignment . Target is ILocalReferenceOperation localRef :
146+ return localRef . Local ;
147+ }
148+ parent = parent . Parent ;
149+ }
150+ return null ;
151+ }
152+
153+ private static bool IsAssignedToInstanceMember ( IObjectCreationOperation creation )
154+ {
155+ var parent = creation . Parent ;
156+ while ( parent != null )
157+ {
158+ if ( parent is IAssignmentOperation assignment )
159+ {
160+ if ( assignment . Target is IFieldReferenceOperation fieldRef &&
161+ fieldRef . Instance is IInstanceReferenceOperation )
80162 {
81- var diagnostic = Diagnostic . Create (
82- Rule ,
83- method . Locations . FirstOrDefault ( ) ,
84- field . Name ) ;
85- blockEndContext . ReportDiagnostic ( diagnostic ) ;
163+ return true ;
164+ }
165+
166+ if ( assignment . Target is IPropertyReferenceOperation propertyRef &&
167+ propertyRef . Instance is IInstanceReferenceOperation )
168+ {
169+ return true ;
86170 }
87171 }
88- } ) ;
172+
173+ parent = parent . Parent ;
174+ }
175+
176+ return false ;
177+ }
178+
179+ private static bool IsPartOfBaseConstructorInitializer ( IObjectCreationOperation creation )
180+ {
181+ var parent = creation . Parent ;
182+ while ( parent != null )
183+ {
184+ if ( parent is IArgumentOperation argument &&
185+ argument . Parent is IInvocationOperation invocation &&
186+ invocation . TargetMethod . MethodKind == MethodKind . Constructor &&
187+ invocation . Parent is IConstructorBodyOperation )
188+ {
189+ return true ;
190+ }
191+ parent = parent . Parent ;
192+ }
193+
194+ return false ;
195+ }
196+
197+ private static ILocalSymbol ? GetTargetLocal ( IOperation operation )
198+ {
199+ switch ( operation )
200+ {
201+ case IInvocationOperation invocation when invocation . Instance is ILocalReferenceOperation localRef :
202+ return localRef . Local ;
203+ case IConditionalAccessOperation conditional when conditional . Operation is ILocalReferenceOperation localRef &&
204+ conditional . WhenNotNull is IInvocationOperation :
205+ return localRef . Local ;
206+ }
207+
208+ return null ;
209+ }
210+
211+ private struct LocalInfo
212+ {
213+ public LocalInfo ( Location location )
214+ {
215+ Location = location ;
216+ IsDisposed = false ;
217+ Escapes = false ;
218+ }
219+
220+ public Location Location { get ; }
221+ public bool IsDisposed { get ; set ; }
222+ public bool Escapes { get ; set ; }
89223 }
90224}
0 commit comments