-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
C# 12: Primary constructors. #15474
C# 12: Primary constructors. #15474
Conversation
bbb6a78
to
0698879
Compare
0698879
to
b1adbf6
Compare
b1adbf6
to
8c08baa
Compare
8c08baa
to
aed5080
Compare
DCA was uneventful; Even though it is a bit discouraging that we didn't find more results, doing improvements like this should help us avoid our dataflow analysis to deteriorate over time when the new language feature becomes more widely adopted. |
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, great work! Some (mostly cosmetic) suggestions.
/** | ||
* Holds if this a primary constructor in source code. | ||
*/ | ||
predicate isPrimary() { |
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 we should just inline this into the charpred of PrimaryConstructor
.
pa instanceof ParameterRead and | ||
result = pa.getAControlFlowNode() |
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.
Alternatively
pa instanceof ParameterRead and | |
result = pa.getAControlFlowNode() | |
result = pa.(ParameterRead).getAControlFlowNode() |
* Gets the control flow node used for data flow purposes for the primary constructor | ||
* parameter access `pa`. | ||
*/ | ||
private ControlFlow::Node getPrimaryConstructorParameterCfn(ParameterAccess pa) { |
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.
Rename to getAPrimaryConstructorParameterCfn
.
@@ -37,6 +38,21 @@ predicate isArgumentNode(ArgumentNode arg, DataFlowCall c, ArgumentPosition pos) | |||
arg.argumentOf(c, pos) | |||
} | |||
|
|||
/** | |||
* Gets the control flow node used for data flow purposes for the primary constructor |
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.
Gets a
pa instanceof ParameterWrite and | ||
exists(AssignExpr ae | pa = ae.getLValue() and result = ae.getAControlFlowNode()) |
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.
This only takes direct assignments into account, and e.g. not assignments via out
arguments (perhaps we should add a test for that). AssignableDefinitions
are meant to cover all cases:
pa instanceof ParameterWrite and | |
exists(AssignExpr ae | pa = ae.getLValue() and result = ae.getAControlFlowNode()) | |
pa = any(AssignableDefinition def | result = def.getAControlFlowNode()).getTargetAccess() |
exists(InstanceParameterAccessNode n | n = node1 | | ||
n.getUnderlyingControlFlowNode() = node2.(ExprNode).getControlFlowNode() and | ||
n.getParameter() = c.(PrimaryConstructorParameterContent).getParameter() | ||
) 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.
exists(InstanceParameterAccessNode n | n = node1 | | |
n.getUnderlyingControlFlowNode() = node2.(ExprNode).getControlFlowNode() and | |
n.getParameter() = c.(PrimaryConstructorParameterContent).getParameter() | |
) and | |
node1 = any(InstanceParameterAccessNode n | | |
n.getUnderlyingControlFlowNode() = node2.(ExprNode).getControlFlowNode() and | |
n.getParameter() = c.(PrimaryConstructorParameterContent).getParameter() and | |
n.isPreUpdate() | |
) and |
exists(Node n1 | | ||
primaryConstructorParameterStore(_, c, n1) and n = n1.(PostUpdateNode).getPreUpdateNode() | ||
) |
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.
exists(Node n1 | | |
primaryConstructorParameterStore(_, c, n1) and n = n1.(PostUpdateNode).getPreUpdateNode() | |
) | |
n = any(PostUpdateNode n1 | primaryConstructorParameterStore(_, c, n1)).getPreUpdateNode() |
private class InstanceParameterAccessPostUpdateNode extends PostUpdateNode, | ||
InstanceParameterAccessNode | ||
{ | ||
private ControlFlow::Node cfg; |
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.
cfn
/** Gets the underlying parameter. */ | ||
Parameter getParameter() { result = p } | ||
|
||
override string toString() { result = "parameter field " + p.getName() } |
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 just say result = "parameter " + p.getName()
/** | ||
* @name Test for primary constructors | ||
*/ |
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.
No need to add this.
* the first access to o (1) is modeled as this.o_backing_field and | ||
* the second access to o (2) is modeled as this.o_backing_field = value. |
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.
Missing backticks.
* the first access to o (1) is modeled as this.o_backing_field and | ||
* the second access to o (2) is modeled as this.o_backing_field = value. | ||
* Both models need a pre-update this node, and the latter need an additional post-update this access, | ||
* all of which are represented by an InstanceParameterAccessNode node. |
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.
Missing backticks.
* ```csharp | ||
* public class C(object o) { } | ||
* ``` | ||
* we synthesize the primary constructor for C as |
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.
Missing backticks.
* ```csharp | ||
* public C(object o) => this.o_backing_field = o; | ||
* ``` | ||
* The synthesized (pre/post-update) this access is represented an PrimaryConstructorThisAccessNode node. |
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.
Missing backticks.
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.
@hvitved : Your changes look plausible to me.
DCA still looks good. |
In this PR we introduce library and data flow support for primary constructors.
Below we see an example of class
C1
with a primary constructor defined on line 1.The QL library now contains a class called
PrimaryConstructor
which contains exactly the primary constructors defined in source code. If it turns our that this implementation is insufficient we can extend the extractor to specifically extract whether a constructor is a primary constructor by inspecting the declaring syntax references using the the same "hack" as roslyn.Before discussing the details of the data flow implementation for primary constructors is is worth noting that the example class declaration of
C1
is roughly equivalent to (this is also what you will approximately get if you compile and de-compile the code):That is, we can think of class with a primary constructor as having a backing field for each primary constructor parameter and that the primary constructor as having a (synthetic) body that assigns the provided arguments to the backing fields.
Furthermore, each primary constructor parameter access (read/write) corresponds to reading or writing the corresponding backing field.
We use these observations to model data flow for primary constructors by introducing the following new content and dataflow node types
Content
PrimaryConstructorParameterContent
: As we don't have a "real" backing field for each primary constructor parameter we introduce a new content type that enables us to create "synthetic" backing fields for each primary constructor parameter.The data-flow implementation introduces two new types of data-flow nodes:
PrimaryConstructorThisAccessNode
: Used for this access and synthesises the body of the primary constructor (that is, dataflow from the primary constructor parameters into their synthetic backing fields).InstanceParameterAccessNode
: Used to synthesise reads and writes of the parameter synthetic backing fields. That is, for all primary constructor parameter accesses (reads and writes) within a class declaration we need to do the corresponding update to the synthetic backing fields.The dataflow implementation has some limitations and doesn't cover e.g.
Huge thanks to @hvitved for outlining and helping with the implementation.