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); } } }