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:

  1. Replace the node with another node that has the same ID.
  2. Move properties, references and children from the original node to the new node as needed.
  3. 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.)