/* * Copyright 2003-2016 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.lang.pattern; import jetbrains.mps.util.IterableUtil; import jetbrains.mps.util.Pair; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.openapi.language.SContainmentLink; import org.jetbrains.mps.openapi.language.SProperty; import org.jetbrains.mps.openapi.language.SReferenceLink; import org.jetbrains.mps.openapi.model.SNode; import org.jetbrains.mps.openapi.model.SNodeReference; import org.jetbrains.mps.openapi.model.SReference; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Match/extract values of a node against pattern/sample node. * Not thread-safe. * <p/> * IMPLEMENTATION NODE: Pattern node passed to {@link #match(SNode, SNode)} doesn't need to have * set properties/association/aggregation we are going to capture/iterate with distinct matcher. * @author Artem Tikhomirov * @since 3.4 */ public final class NodeMatcher { private final ChildMatcher myParent; private final ValueContainer myValues; private Map<SProperty, String> myPropertyToVariableName; private Map<SReferenceLink, String> myReferenceToVariableName; private Map<SContainmentLink, ChildMatcher> myChildExtractors; private List<Pair<SNode, NodeMatcher>> myDisjunction; private String myPatternVarName; private boolean myMatchAny = false; public NodeMatcher(@NotNull ValueContainer vc) { myValues = vc; myParent = null; } /*package*/ NodeMatcher(@NotNull ChildMatcher parent) { myValues = parent.getValues(); myParent = parent; } // XXX I could have introduced PropertyExtractor with single useful #capture(String) method (plus #done()), but it's just too much public NodeMatcher property(@NotNull SProperty p, @NotNull String patternVarName) { if (myPropertyToVariableName == null) { myPropertyToVariableName = new HashMap<SProperty, String>(8); } myPropertyToVariableName.put(p, patternVarName); return this; } public NodeMatcher capture(String nodeVarName) { myPatternVarName = nodeVarName; return this; } /** * 'wildcard' node match (aka '_') * @return <code>this</code> */ public NodeMatcher any() { myMatchAny = true; return this; } // see #property(), could have added RefExtractor, if needed public NodeMatcher association(@NotNull SReferenceLink l, @NotNull String linkVarName) { if (myReferenceToVariableName == null) { myReferenceToVariableName = new HashMap<SReferenceLink, String>(8); } myReferenceToVariableName.put(l, linkVarName); return this; } public ChildMatcher child(@NotNull SContainmentLink role) { if (myChildExtractors == null) { myChildExtractors = new HashMap<SContainmentLink, ChildMatcher>(8); } ChildMatcher childExtractor = myChildExtractors.get(role); if (childExtractor == null) { myChildExtractors.put(role, childExtractor = new ChildMatcher(this)); } return childExtractor; } /** * Tells to match disjunction of nodes using supplied alternatives. * <code>NodeMatcher</code> with alternatives doesn't check children, associations or properties. It's still possible, though, to * capture value of the node with {@link #capture(String)}. * * <p/>When matching, each disjunct is processed in order they were added to the matcher, the first one to match cancels processing of other * (i.e. similar to Java's || operator). * * @param patternNode pattern to match disjunct against. * @param disjunct ordered matcher sequence, consulted one my one until match is found. * @return <code>this</code> */ public NodeMatcher disjunct(@NotNull SNode patternNode, @NotNull NodeMatcher disjunct) { assert patternNode.getModel() == null : "expect pattern nodes to hand in the air not to address model access"; if (myDisjunction == null) { myDisjunction = new ArrayList<Pair<SNode, NodeMatcher>>(4); } myDisjunction.add(new Pair<SNode, NodeMatcher>(patternNode, disjunct)); return this; } public ChildMatcher done() { return myParent; } /*package*/ ValueContainer getValues() { return myValues; } public boolean match(@NotNull SNode pattern, @NotNull SNode against) { return internalMatch(pattern, against); } /** * relaxed version of {@link #match(SNode, SNode)} that accounts for missing child in pattern node, * when e.g. a disjunction or wildcard would match that of actual. * Shall be used from within {@link ChildMatcher} only! */ /*package*/ boolean internalMatch(@Nullable SNode pattern, SNode against) { if (myMatchAny) { return true; } final boolean rv; if (myDisjunction != null) { rv = matchDisjunction(against); // technically, we can always try matchDisjunction before matchStructure, just don't see // any reason to - generated code didn't go deeper than OrPattern. } else { rv = matchStructure(pattern, against); } if (rv && myPatternVarName != null) { getValues().put(myPatternVarName, against); } return rv; } private boolean matchStructure(SNode pattern, SNode against) { if (myPatternVarName != null && myChildExtractors == null && myPropertyToVariableName == null && myReferenceToVariableName == null) { // FIXME if it's a mere accessor to a node to capture it, do not check concept match. Though IMO it's a defect in original implementation, // it's left here for the time being to get existing tests pass before I fix them with a distinct change // The case is "if (#v1) { #v2 }", where v2 attributes ExpressionStatement, and input comes as an empty Statement. // After some consideration and discussion, it seems we need to cover both scenarios (explicitly picked by user when // pattern variable is assigned), and this alternative shall remain default for compatibility with existing code. return true; } if (pattern == null) { return false; } if (!against.getConcept().isSubConceptOf(pattern.getConcept())) { return false; } // properties Map<SProperty, String> prop2var = myPropertyToVariableName == null ? Collections.<SProperty,String>emptyMap() : myPropertyToVariableName; ArrayList<SProperty> propsToCheck = new ArrayList<SProperty>(prop2var.keySet()); propsToCheck.addAll(IterableUtil.asCollection(pattern.getProperties())); for (SProperty p : propsToCheck) { if (prop2var.containsKey(p)) { getValues().put(prop2var.get(p), against.getProperty(p)); } else { if (!pattern.getProperty(p).equals(against.getProperty(p))) { return false; } } } // // references final Map<SReferenceLink, String> ref2var = myReferenceToVariableName == null ? Collections.<SReferenceLink, String>emptyMap() : myReferenceToVariableName; ArrayList<SReferenceLink> refsToCheck = new ArrayList<SReferenceLink>(ref2var.keySet()); for (SReference r : pattern.getReferences()) { refsToCheck.add(r.getLink()); } for (SReferenceLink r : refsToCheck) { SReference r2 = against.getReference(r); SNodeReference actualTarget = r2 == null ? null : r2.getTargetNodeReference(); if (ref2var.containsKey(r)) { getValues().put(ref2var.get(r), actualTarget, r2 == null ? null : r2.getTargetNode()); } else { final SReference expectedReference = pattern.getReference(r); assert expectedReference != null : "otherwise how did it get into refsToCheck"; final SNodeReference expectedTarget = expectedReference.getTargetNodeReference(); if (!expectedTarget.equals(actualTarget)) { return false; } } } // // children ArrayList<SContainmentLink> knownChildRoles = new ArrayList<SContainmentLink>(); // patterns are generally small and don't specify vast child hierarchies in different roles, list is sufficient to hold 1-2 roles for (SNode child = pattern.getFirstChild(); child != null; child = child.getNextSibling()) { final SContainmentLink cl = child.getContainmentLink(); if (!knownChildRoles.contains(cl)) { knownChildRoles.add(cl); } } final Map<SContainmentLink, ChildMatcher> ce = myChildExtractors == null ? Collections.<SContainmentLink,ChildMatcher>emptyMap() : myChildExtractors; knownChildRoles.addAll(ce.keySet()); final ChildMatcher defaultChildExtractor = new ChildMatcher(this); for (SContainmentLink l : knownChildRoles) { ChildMatcher childExtractor = ce.get(l); if (childExtractor == null) { childExtractor = defaultChildExtractor; } if (!childExtractor.match(IterableUtil.asList(pattern.getChildren(l)), IterableUtil.asList(against.getChildren(l)))) { return false; }; } return true; } private boolean matchDisjunction(SNode against) { for (Pair<SNode, NodeMatcher> pair : myDisjunction) { if (pair.o2.match(pair.o1, against)) { return true; } } return false; } }