-
Notifications
You must be signed in to change notification settings - Fork 1.9k
C#: Replace initializer splitting with an ObjectInitMethod. #20922
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
49eed3a to
0ef7bee
Compare
michaelnebel
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Uh, very neat!
Just some quick initial comments. It appears that there are some tests failing.
| { | ||
| internal sealed class ObjectInitMethod : CachedEntity, IMethodEntity | ||
| { | ||
| Type ContainingType { get; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private?
| this.ContainingType = containingType; | ||
| } | ||
|
|
||
| public string Name => "<object initializer>"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can be made a readonly field.
|
Do you also intend to model some synthetic assignments in the new synthetic method? |
The assignments are already extracted as top-level expressions in the enclosing class. We could move them into this new method - that's what Java does, but Java also allow one to write a chunk of code that goes directly into the object initializer, so the situation there is slightly different. However, I opted not to do that since it would be a much more invasive change and require us to change any current QL code that relies on identifying field initializers as they would have moved and would look different. Though, if we were building from scratch then I would have put the assignments inside the method, but as it stands I don't think it's worth it to make the change. |
bed13c3 to
f7ff3f5
Compare
3ee3160 to
70440f3
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR replaces the initializer splitting mechanism with a dedicated object initializer method to improve how field initializers are executed by constructors. Instead of copying field initializers to all relevant constructors' CFGs, they now reside in a single <object initializer> method that is called from constructors that invoke base constructors. This ensures initializers run exactly once per object construction.
Key changes:
- Introduces
<object initializer>methods in the extractor for classes with field initializers - Modifies CFG to place field initializers in the object initializer method
- Fixes bug where field initializers were running after
basecalls instead of before
Reviewed changes
Copilot reviewed 94 out of 95 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| csharp/ql/test/library-tests/obinit/ObInit.ql | Adds query predicates to test object initializer methods and their calls |
| csharp/ql/test/library-tests/obinit/obinit.cs | New test file with classes demonstrating field initializer behavior across constructor chains |
| csharp/ql/test/library-tests/obinit/ObInit.expected | Expected test results showing object initializer method creation and CFG structure |
| csharp/ql/test/library-tests/obinit/Flow.ql | Adds data flow test to verify field initializer values flow correctly |
| csharp/ql/test/library-tests/obinit/Flow.expected | Expected data flow results showing field values propagating from source to sink |
| Multiple test .expected files | Updates across test suite reflecting addition of <object initializer> methods in AST ordering and data flow |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review for a chance to win a $100 gift card. Take the survey.
70440f3 to
1f0a3b7
Compare
1f0a3b7 to
5d63b6e
Compare
michaelnebel
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like DCA identified some "new" missing call targets.
|
|
||
| public override void WriteId(EscapingTextWriter trapFile) | ||
| { | ||
| trapFile.WriteSubId(ContainingType); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this work as expected for generics (I suspect it does due to how WriteId looks like for constructors)? Just wondering whether you actively considered that case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe extracted Callable bodies are 1-1 with their corresponding lines of code, ie. we extract only one Constructor body regardless of generics. And since ObjectInitMethod extraction is triggered by inserting a call in those constructor bodies we will also only extract one ObjectInitMethod. That is at least my understanding. And I think I've located the line that ensures this: In Method.cs the call to ExtractInitializers is guarded by IsSourceDeclaration.
| this.ContainingType = containingType; | ||
| } | ||
|
|
||
| public static readonly string Name = "<object initializer>"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private
hvitved
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall LGTM, thanks for fixing this long standing tech debt.
| */ | ||
| class InitializedInstanceMember extends Member { | ||
| InitializedInstanceMember() { | ||
| exists(AssignExpr ae | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would make ae a field and then just return that in getInitializer.
| } | ||
|
|
||
| /** | ||
| * Gets the last member initializer expression for non-static constructor `c` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
QL doc is outdated.
| not c instanceof GotoCompletion | ||
| or | ||
| last(InitializerSplitting::lastConstructorInitializer(scope, _), last, c) and | ||
| last(callable.(Constructor).getInitializer(), last, c) and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't there also be a case for getObjectInitializerCall?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that would resolve the deadEnd inconsistency at csharp/ql/test/library-tests/standalone/errorrecovery/CONSISTENCY/CfgConsistency.expected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I guess we can add that as a safety mechanism for broken code. I didn't add it originally since such a case can only occur when the doesn't compile. In correct C# code there will always be a callable.(Constructor).getInitializer() after a call to an ObjectInitMethod.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We actually need two additional cases to completely cover the case when the Constructor.getInitializer() is missing - both here for when there's no body, and as a step when there is. I've added both with suitable comments.
| @@ -0,0 +1,22 @@ | |||
| import csharp | |||
|
|
|||
| module FlowConfig implements DataFlow::ConfigSig { | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this test should use inline expectations, see e.g. FieldFlow.ql.
Is that in any way related to this PR or is it somehow spurious? I don't see how my changes would cause that, but I may be missing something? |
Looked at bit closer: You are right, they are most likely spurious. The affected projects also have fluctuations in other DCA stats (for instance extractor messages). So probably fine to ignore. |
michaelnebel
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice work @aschackmull ! Thank you!
Field initializers need to be executed by the constructor. In case of multiple constructors that call each other, only those constructors that call
baseconstructors should execute field initializers in order to run the initializers exactly once for each constructed object. Currently this is done using splitting to copy all the initializers to the CFGs of all the relevant constructors. This PR changes this to happen in a single method, called "object initializer", which is then called from all the relevant constructors.base).basecall - not after.