core/kernel/source/jetbrains/mps/smodel/DynamicReference.java (196 lines of code) (raw):
/*
* Copyright 2003-2025 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package jetbrains.mps.smodel;
import jetbrains.mps.extapi.model.ResolveInfoExt;
import jetbrains.mps.logging.Logger;
import jetbrains.mps.scope.ErrorScope;
import jetbrains.mps.scope.Scope;
import jetbrains.mps.smodel.AssociationData.DynamicPtr;
import jetbrains.mps.smodel.AssociationData.DynamicPtrWithOrigin;
import jetbrains.mps.smodel.AssociationData.SNodeAssociationUpdate;
import jetbrains.mps.smodel.constraints.ModelConstraints;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.annotations.Immutable;
import org.jetbrains.mps.openapi.language.SReferenceLink;
import org.jetbrains.mps.openapi.model.ResolveInfo;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeReference;
import org.jetbrains.mps.openapi.model.SReference;
import org.jetbrains.mps.openapi.module.SRepository;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
* FIXME Either stop extending {@code SReferenceBase} (there's no use of its mature/young myImmatureTargetNode and myTargetModelReference)
* or move respective fields/code into {@code StaticReference} subclass (then, j.m.smodel.SReference shall cease as it
* (a) confusing with openapi counterpart; (b) duplicates {@code SReferenceBase}
* JFI, there's code that filters node references based on {@code SReferenceBase} e.g. to setTargetSModelReference, shall decide if it's correct with respect
* to the aforementioned change in superclass
* XXX what makes it live in [kernel]? Is it only ModelConstraints or anything else? Can I refactor it to keep the class in [smodel]?
*/
public final class DynamicReference extends jetbrains.mps.smodel.SReference {
private static final Logger LOG = Logger.getLogger(DynamicReference.class);
private DynamicPtr myData;
// this is for tracking loops in dynref resolving, typically arising from interaction
// between type system rules and scopes
private static final ThreadLocal<Set<DynamicReference>> currentlyResolved = new ThreadLocal<Set<DynamicReference>>() {
@Override
protected Set<DynamicReference> initialValue() {
return new HashSet<>();
}
};
// we also keep track of references for which we call reportErrorWithOrigin
// we need this because it will call source node's getPresentation() which in turn might resolve us again
// we don't want to report loop in this case, rather just return null
private static final ThreadLocal<Set<DynamicReference>> currentlySourceNodeLogged = new ThreadLocal<Set<DynamicReference>>() {
@Override
protected Set<DynamicReference> initialValue() {
return new HashSet<>();
}
};
/**
* @deprecated Use {@link SNode#setReference(SReferenceLink, ResolveInfo)} instead, with {@link ResolveInfo#of(String)}
*/
@Deprecated(forRemoval = true, since = "2024.1")
public static DynamicReference createDynamicReference(@NotNull SReferenceLink role, @NotNull SNode sourceNode, @Nullable String modelName, String resolveInfo) {
return new DynamicReference(role, sourceNode, new DynamicPtr(resolveInfo));
}
/**
* Use this factory method to create a link with {@code DynamicReferenceOrigin} instead of combination
* {@code create()} + {@code setOrigin()}.
* @since 2022.2
*/
public static DynamicReference create(@NotNull SReferenceLink role, @NotNull SNode sourceNode, String resolveInfo, @NotNull DynamicReferenceOrigin origin) {
return new DynamicReference(role, sourceNode, new DynamicPtrWithOrigin(resolveInfo, origin.getTemplate(), origin.getInputNode()));
}
/*package*/ DynamicReference(@NotNull SReferenceLink role, @NotNull SNode sourceNode, @NotNull DynamicPtr data) {
super(role, sourceNode);
myData = data;
}
@Override
public SModelReference getTargetSModelReference() {
// don't be shy, tell there's no target model reference right away, rather than let superclass to try to make it indirect
// with no-op #makeMature(). Now, with targetModelReference field moved to StaticReference, the only reason to have method
// implementation here is abstract method placeholder in SReferenceBase. Perhaps, this implementation (== null) shall be there.
//
// FWIW, I don't quite get the idea of null target model of DynamicReferences, however, it's the way it was.
// Besides, one of the uses of the method is to refresh node's references the moment model reference changes,
// and to support it properly we shall override setTargetSModelReference to no-op instead. The problem is #getTargetSModelReference
// might be quite expensive for dynamic nodes during bulk updates.
//
return null;
}
@Override
protected SNode getTargetNode_internal(ProblemReporter report) {
// seems like getTargetNode() doesn't make sense if source node is detached
if (mySourceNode.getModel() == null) {
report.error("Taking target node of dynamic reference whose source node is not in a model");
return null;
}
final SRepository owner = mySourceNode.getModel().getRepository();
// XXX perhaps, shall return null right away if owner == null. No point to resolve
// a reference from a model that is not yet part of a repository
final Set<DynamicReference> currentRefs = currentlyResolved.get();
final Set<DynamicReference> loggedRefs = currentlySourceNodeLogged.get();
// FIXME use of (this) works as long as equals/hashCode is right. Consider using another identity object
// or come up with another mechanism to avoid stack overflow and reference resolution cycles
if (currentRefs.contains(this)) {
// loop detected!
if (!loggedRefs.contains(this)) {
// it's not spurious loop, via logging. it's real, let's complain
LOG.errorWithTrace("Loop detected in dynamic references (number of current dyn. refs: " + currentRefs.size() + ")");
}
return null;
}
currentRefs.add(this);
try {
if (getResolveInfo() == null) {
reportErrorWithOrigin("bad reference: no resolve info", report);
return null;
}
final Scope scope;
if (owner instanceof ReferenceScopeHelper.Source) {
scope = ((ReferenceScopeHelper.Source) owner).getReferenceScopeHelper().getScope(this);
} else {
scope = ModelConstraints.getScope(this);
}
if (scope instanceof ErrorScope) {
reportErrorWithOrigin("cannot obtain scope for reference `" + getRole() + "': " + ((ErrorScope) scope).getMessage(), report);
return null;
}
SNode targetNode = null;
try {
targetNode = scope.resolve(getSourceNode(), getResolveInfo());
} catch (Throwable t) {
LOG.warning("Exception was thrown while dynamic reference resolving", t);
}
if (targetNode == null) {
reportErrorWithOrigin("cannot resolve reference by string: '" + getResolveInfo() + "'", report);
}
return targetNode;
} finally {
// cleaning up our loop checking stuff
currentRefs.remove(this);
}
}
@Override
public SNodeReference getTargetNodeReference() {
SNode targetNode = getTargetNode_internal(new ProblemReporter() {});
if (targetNode == null) {
return new SNodePointer(null);
}
return targetNode.getReference();
}
private void reportErrorWithOrigin(String message, ProblemReporter report) {
Set<DynamicReference> refs = currentlySourceNodeLogged.get();
try {
refs.add(this);
if (myData instanceof DynamicPtrWithOrigin) {
final DynamicPtrWithOrigin dpo = (DynamicPtrWithOrigin) myData;
List<ProblemDescription> result = new ArrayList<>(2);
if (dpo.getOriginInput() != null) {
result.add(new ProblemDescription(dpo.getOriginInput(), " -- was input: " + dpo.getOriginInput()));
}
if (dpo.getOriginTemplate() != null) {
result.add(new ProblemDescription(dpo.getOriginTemplate(), " -- was template: " + dpo.getOriginTemplate()));
}
if (result.size() > 0) {
report.error(message, result.toArray(new ProblemDescription[0]));
return;
}
}
report.error(message);
} finally {
refs.remove(this);
}
}
public String getResolveInfo() {
return myData.getRI();
}
public void setResolveInfo(String info) {
if (Objects.equals(myData.getRI(), info)) {
return;
}
setData(myData.withRI(info == null ? null : info.intern()));
}
@NotNull
@Override
public ResolveInfo describeTarget() {
// myData is immutable
return new DRI(myData);
}
@Nullable
public DynamicReferenceOrigin getOrigin() {
DynamicReferenceOrigin origin = null;
if (myData instanceof DynamicPtrWithOrigin) {
final DynamicPtrWithOrigin dpo = (DynamicPtrWithOrigin) myData;
origin = new DynamicReferenceOrigin(dpo.getOriginTemplate(), dpo.getOriginInput());
}
return origin;
}
/**
* XXX change in logic: now could use this method for a reference already associated with a node,
* not for a newly created reference. FIXME perhaps, could change setData() to account for this case
*/
public void setOrigin(@Nullable DynamicReferenceOrigin origin) {
if (origin == null) {
if (myData instanceof DynamicPtrWithOrigin) {
setData(new DynamicPtr(myData.getRI()));
} // else no reason to do anything
} else {
setData(new DynamicPtrWithOrigin(myData.getRI(), origin.getTemplate(), origin.getInputNode()));
}
}
@Override
/*package*/ AssociationData getData() {
return myData;
}
private void setData(DynamicPtr data) {
((SNodeAssociationUpdate) mySourceNode).updateAssociation(getLink(), myData, data);
myData = data;
}
@Immutable
public static class DynamicReferenceOrigin {
private final SNodeReference template;
private final SNodeReference inputNode;
public DynamicReferenceOrigin(SNodeReference template, SNodeReference inputNode) {
this.template = template;
this.inputNode = inputNode;
}
public SNodeReference getTemplate() {
return template;
}
public SNodeReference getInputNode() {
return inputNode;
}
}
private static class DRI implements ResolveInfo, ResolveInfoExt {
private final DynamicPtr myResolveInfo;
private DRI(DynamicPtr resolveInfo) {
myResolveInfo = resolveInfo;
}
@Override
public SReference create(@NotNull SNode source, @NotNull SReferenceLink link) {
return new DynamicReference(link, source, myResolveInfo);
}
}
}