Sometimes we need to change the concept of a node during a refactoring or migration. If we simply replace the node with another node, we will break references to the original node. How do we change the concept of a node “in place”?
We can’t!
We cannot change the concept of a node directly. There are no API methods for it, and in fact it is stored in a final
field in the SNode
class.
Here is what we can do instead:
- Replace the node with another node that has the same ID.
- Move properties, references and children from the original node to the new node as needed.
- Fix any direct references that we know about.
A direct reference is a reference that internally stores a Java object reference to the target node. An indirect reference instead stores only the identifiers of the target node and its model and will look up the target node object when needed.
Any direct references that we don’t know about in step 3 will get broken, but it is usually not a problem because direct references are used only in a few specific scenarios:
- In the generator, if a reference macro returns a string rather than a node.
- When a node is detached (deleted), all references in the detached subtree will be made direct.
So as long as we a) do not perform these tricks in the generator (and we most probably don’t need to), and b) fix the reference targets of all the new descendants added in step 2, we should be safe.
The easy way
MPS provides a method that performs the three steps above, RefactoringRuntime#replaceWithNewConcept(originalNode, newConcept)
(MPS
link).
This method assumes that the existing node is fully compatible with the new concept and will copy over all the properties, references and children of the old node to the new node without checking whether they are appropriate for the new concept. This may or may not be what you want. It will also fix the references in the descendants that point to the original node to point to the new one.
If this behavior does not suit you, you can manually create a node with the specified ID and copy over the necessary features.
Setting the ID of a node after creation
We cannot set the ID of a node via Open API but we can cast the org.jetbrains.mps.openapi.model.SNode
interface to
jetbrains.mps.smodel.SNode
class and use its setId()
method:
((SNode) newNode).setId(oldNode/.getNodeId());
The node must not be part of any model at the moment of the call to setId()
.
Setting the ID of a node in a quotation
If we create nodes using a quotation (light or full), we can use a hidden feature to specify the ID of the newly created
node. Typing a left square bracket ([
) after a quotation will pop up two extra fields where we can specify the model
to use when creating the node and the ID of the new node.
(The model is a technical parameter that can be simply set to null. Specifying a model here does not add the node to the model after creation, it simply asks the model to create the node, but all current model implementations create nodes in the same way anyway.)
Fixing the references
The replaceWithNewConcept()
method described previously uses this code to fix reference targets in the new node:
Conclusion
We cannot change the concept of a node directly but there is a workaround that works. Just don’t do it during generation. (But you probably won’t need to, anyway.)