/* * Copyright 2000-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 com.intellij.extapi.psi; import com.intellij.lang.ASTNode; import com.intellij.lang.Language; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Attachment; import com.intellij.openapi.diagnostic.RuntimeExceptionWithAttachments; import com.intellij.openapi.progress.ProgressIndicatorProvider; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectCoreUtil; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.RecursionManager; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiInvalidElementAccessException; import com.intellij.psi.impl.DebugUtil; import com.intellij.psi.impl.PsiManagerEx; import com.intellij.psi.impl.source.PsiFileImpl; import com.intellij.psi.impl.source.SubstrateRef; import com.intellij.psi.impl.source.tree.CompositeElement; import com.intellij.psi.impl.source.tree.FileElement; import com.intellij.psi.impl.source.tree.RecursiveTreeElementWalkingVisitor; import com.intellij.psi.impl.source.tree.SharedImplUtil; import com.intellij.psi.stubs.*; import com.intellij.psi.tree.IElementType; import com.intellij.psi.tree.TokenSet; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.ArrayFactory; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.List; /** * A base class for PSI elements that support both stub and AST substrates. The purpose of stubs is to hold the most important information * (like element names, access modifiers, function parameters etc), save it in the index and use it during code analysis instead of parsing * the AST, which can be quite expensive. Ideally, only the files loaded in the editor should ever be parsed, all other information that's * needed e.g. for resolving references in those files, should be taken from stub-based PSI.<p/> * * During indexing, this element is created from text (using {@link #StubBasedPsiElementBase(ASTNode)}), * then a stub with relevant information is built from it * ({@link IStubElementType#createStub(PsiElement, StubElement)}), and this stub is saved in the * index. Then a StubIndex query returns an instance of this element based on a stub * (created with {@link #StubBasedPsiElementBase(StubElement, IStubElementType)} constructor. To the clients, such element looks exactly * like the one created from AST, all the methods should work the same way.<p/> * * Code analysis clients should be careful not to invoke methods on this class that can't be implemented using only the information from * stubs. Such case is supported: {@link #getNode()} will switch the implementation from stub to AST substrate and get this information, but * this is slow performance-wise. So stubs should be designed so that they hold all the information that's relevant for * reference resolution and other code analysis.<p/> * * The subclasses should be careful not to switch to AST prematurely. For example, {@link #getParentByStub()} should be used as much * as possible in overridden {@link #getParent()}, and getStubOrPsiChildren methods should be preferred over {@link #getChildren()}.<p/> * * After switching to AST, {@link #getStub()} will return null, but {@link #getGreenStub()} can still be used to retrieve stub objects if they're needed. * The AST itself is not held on a strong reference and can be garbage-collected. This makes it possible to hold many stub-based PSI elements * in the memory at once, but results in occasionally expensive {@link #getNode()} calls that have to load and parse the AST anew. * * @see IStubElementType * @see com.intellij.psi.impl.source.PsiFileWithStubSupport */ public class StubBasedPsiElementBase<T extends StubElement> extends ASTDelegatePsiElement { public static final Key<String> CREATION_TRACE = Key.create("CREATION_TRACE"); public static final boolean ourTraceStubAstBinding = "true".equals(System.getProperty("trace.stub.ast.binding", "false")); private volatile SubstrateRef mySubstrateRef; private volatile int myStubIndex = -1; private final IElementType myElementType; public StubBasedPsiElementBase(@NotNull T stub, @NotNull IStubElementType nodeType) { mySubstrateRef = new SubstrateRef.StubRef(stub); myElementType = nodeType; } public StubBasedPsiElementBase(@NotNull ASTNode node) { mySubstrateRef = SubstrateRef.createAstStrongRef(node); myElementType = node.getElementType(); } /** * This constructor is created to allow inheriting from this class in JVM languages which doesn't support multiple constructors (e.g. Scala). * If your language does support multiple constructors use {@link #StubBasedPsiElementBase(StubElement, IStubElementType)} and * {@link #StubBasedPsiElementBase(ASTNode)} instead. */ public StubBasedPsiElementBase(T stub, IElementType nodeType, ASTNode node) { if (stub != null) { if (nodeType == null) throw new IllegalArgumentException("null cannot be passed to 'nodeType' when 'stub' is non-null"); if (node != null) throw new IllegalArgumentException("null must be passed to 'node' parameter when 'stub' is non-null"); mySubstrateRef = new SubstrateRef.StubRef(stub); myElementType = nodeType; } else { if (node == null) throw new IllegalArgumentException("'stub' and 'node' parameters cannot be null both"); if (nodeType != null) throw new IllegalArgumentException("null must be passed to 'nodeType' parameter when 'node' is non-null"); mySubstrateRef = SubstrateRef.createAstStrongRef(node); myElementType = node.getElementType(); } } /** * Ensures this element is AST-based. This is an expensive operation that might take significant time and allocate lots of objects, * so it should be to be avoided if possible. * * @return an AST node corresponding to this element. If the element is currently operating via stubs, * this causes AST to be loaded for the whole file and all stub-based PSI elements in this file (including the current one) * to be switched from stub to AST. So, after this call {@link #getStub()} will return null. */ @Override @NotNull public ASTNode getNode() { if (mySubstrateRef instanceof SubstrateRef.StubRef) { ApplicationManager.getApplication().assertReadAccessAllowed(); PsiFileImpl file = (PsiFileImpl)getContainingFile(); if (!file.isValid()) throw new PsiInvalidElementAccessException(this); FileElement treeElement = file.getTreeElement(); if (treeElement != null && mySubstrateRef instanceof SubstrateRef.StubRef) { return notBoundInExistingAst(file, treeElement); } treeElement = file.calcTreeElement(); if (mySubstrateRef instanceof SubstrateRef.StubRef) { return failedToBindStubToAst(file, treeElement); } } return mySubstrateRef.getNode(); } private ASTNode failedToBindStubToAst(@NotNull PsiFileImpl file, @NotNull final FileElement fileElement) { VirtualFile vFile = file.getVirtualFile(); StubTree stubTree = file.getStubTree(); final String stubString = stubTree != null ? ((PsiFileStubImpl)stubTree.getRoot()).printTree() : null; final String astString = RecursionManager.doPreventingRecursion("failedToBindStubToAst", true, () -> DebugUtil.treeToString(fileElement, true)); @NonNls final String message = "Failed to bind stub to AST for element " + getClass() + " in " + (vFile == null ? "<unknown file>" : vFile.getPath()) + "\nFile:\n" + file + "@" + System.identityHashCode(file); final String creationTraces = ourTraceStubAstBinding ? dumpCreationTraces(fileElement) : null; List<Attachment> attachments = new ArrayList<>(); if (stubString != null) { attachments.add(new Attachment("stubTree.txt", stubString)); } if (astString != null) { attachments.add(new Attachment("ast.txt", astString)); } if (creationTraces != null) { attachments.add(new Attachment("creationTraces.txt", creationTraces)); } throw new RuntimeExceptionWithAttachments(message, attachments.toArray(Attachment.EMPTY_ARRAY)); } @NotNull private String dumpCreationTraces(@NotNull FileElement fileElement) { final StringBuilder traces = new StringBuilder("\nNow " + Thread.currentThread() + "\n"); traces.append("My creation trace:\n").append(getUserData(CREATION_TRACE)); traces.append("AST creation traces:\n"); fileElement.acceptTree(new RecursiveTreeElementWalkingVisitor(false) { @Override public void visitComposite(CompositeElement composite) { PsiElement psi = composite.getPsi(); if (psi != null) { traces.append(psi).append("@").append(System.identityHashCode(psi)).append("\n"); String trace = psi.getUserData(CREATION_TRACE); if (trace != null) { traces.append(trace).append("\n"); } } super.visitComposite(composite); } }); return traces.toString(); } @SuppressWarnings({"NonConstantStringShouldBeStringBuffer", "StringConcatenationInLoop"}) private ASTNode notBoundInExistingAst(@NotNull PsiFileImpl file, @NotNull FileElement treeElement) { String message = "file=" + file + "; tree=" + treeElement; PsiElement each = this; while (each != null) { message += "\n each of class " + each.getClass() + "; valid=" + each.isValid(); if (each instanceof StubBasedPsiElementBase) { message += "; ref=" + ((StubBasedPsiElementBase)each).mySubstrateRef; each = ((StubBasedPsiElementBase)each).getParentByStub(); } else { if (each instanceof PsiFile) { message += "; same file=" + (each == file) + "; current tree= " + file.getTreeElement() + "; stubTree=" + file.getStubTree() + "; physical=" + file.isPhysical(); } break; } } StubElement eachStub = getStub(); while (eachStub != null) { message += "\n each stub " + (eachStub instanceof PsiFileStubImpl ? ((PsiFileStubImpl)eachStub).getDiagnostics() : eachStub); eachStub = eachStub.getParentStub(); } if (ourTraceStubAstBinding) { message += dumpCreationTraces(treeElement); } throw new AssertionError(message); } /** * Don't invoke this method, it's public for implementation reasons. */ public final void setNode(@NotNull ASTNode node) { mySubstrateRef = SubstrateRef.createAstStrongRef(node); } /** * Don't invoke this method, it's public for implementation reasons. */ public final void setSubstrateRef(@NotNull SubstrateRef substrateRef) { mySubstrateRef = substrateRef; myStubIndex = -1; } /** * Don't invoke this method, it's public for implementation reasons. */ public final void setStubIndex(int stubIndex) { myStubIndex = stubIndex; } /** * Don't invoke this method, it's public for implementation reasons. */ public int getStubIndex() { return myStubIndex; } /** * Don't invoke this method, it's public for implementation reasons. */ @NotNull public final SubstrateRef getSubstrateRef() { return mySubstrateRef; } @NotNull @Override public Language getLanguage() { return myElementType.getLanguage(); } @Override @NotNull public PsiFile getContainingFile() { try { return mySubstrateRef.getContainingFile(); } catch (PsiInvalidElementAccessException e) { if (PsiInvalidElementAccessException.getInvalidationTrace(this) != null) { throw new PsiInvalidElementAccessException(this, e); } else { throw e; } } } @Override public boolean isWritable() { return getContainingFile().isWritable(); } @Override public boolean isValid() { return mySubstrateRef.isValid(); } @Override public PsiManagerEx getManager() { Project project = ProjectCoreUtil.theOnlyOpenProject(); if (project != null) { return PsiManagerEx.getInstanceEx(project); } return (PsiManagerEx)getContainingFile().getManager(); } @Override @NotNull public Project getProject() { Project project = ProjectCoreUtil.theOnlyOpenProject(); if (project != null) { return project; } return getContainingFile().getProject(); } @Override public boolean isPhysical() { return getContainingFile().isPhysical(); } @Override public PsiElement getContext() { T stub = getStub(); if (stub != null) { if (!(stub instanceof PsiFileStub)) { return stub.getParentStub().getPsi(); } } return super.getContext(); } /** * Please consider using {@link #getParent()} instead, because this method can return different results before and after AST is loaded. * @return a PSI element taken from parent stub (if present) or parent AST node. */ protected final PsiElement getParentByStub() { final StubElement<?> stub = getStub(); if (stub != null) { return stub.getParentStub().getPsi(); } return SharedImplUtil.getParent(getNode()); } /** * Please use {@link #getParent()} instead */ @Deprecated protected final PsiElement getParentByTree() { return SharedImplUtil.getParent(getNode()); } /** * @return the parent of this element. Uses stub hierarchy if possible, but might cause an expensive switch to AST * if the parent stub doesn't correspond to the parent AST node. */ @Override public PsiElement getParent() { T stub = getGreenStub(); if (stub != null && !((ObjectStubBase)stub).isDangling()) { return stub.getParentStub().getPsi(); } return SharedImplUtil.getParent(getNode()); } @NotNull public IStubElementType getElementType() { if (!(myElementType instanceof IStubElementType)) { throw new ClassCastException("Not a stub type: " + myElementType + " in " + getClass()); } return (IStubElementType)myElementType; } /** * Note: for most clients (where the logic doesn't crucially differ for stub and AST cases), {@link #getGreenStub()} should be preferred. * @return the stub that this element is built upon, or null if the element is currently AST-based. The latter can happen * if the file text was loaded from the very beginning, or if it was loaded via {@link #getNode()} on this or any other element * in the containing file. */ @Nullable public T getStub() { ProgressIndicatorProvider.checkCanceled(); // Hope, this is called often //noinspection unchecked return (T)mySubstrateRef.getStub(myStubIndex); } /** * Like {@link #getStub()}, but can return a non-null value after the element has been switched to AST. Can be used * to retrieve the information which is cheaper to get from a stub than by tree traversal. * @see PsiFileImpl#getGreenStub() */ @Nullable public final T getGreenStub() { ProgressIndicatorProvider.checkCanceled(); // Hope, this is called often //noinspection unchecked return (T)mySubstrateRef.getGreenStub(myStubIndex); } /** * @return a child of specified type, taken from stubs (if this element is currently stub-based) or AST (otherwise). */ @Nullable public <Psi extends PsiElement> Psi getStubOrPsiChild(@NotNull IStubElementType<? extends StubElement, Psi> elementType) { T stub = getGreenStub(); if (stub != null) { //noinspection unchecked final StubElement<Psi> element = stub.findChildStubByType(elementType); if (element != null) { return element.getPsi(); } } else { final ASTNode childNode = getNode().findChildByType(elementType); if (childNode != null) { //noinspection unchecked return (Psi)childNode.getPsi(); } } return null; } /** * @return a not-null child of specified type, taken from stubs (if this element is currently stub-based) or AST (otherwise). */ @NotNull public <S extends StubElement, Psi extends PsiElement> Psi getRequiredStubOrPsiChild(@NotNull IStubElementType<S, Psi> elementType) { Psi result = getStubOrPsiChild(elementType); assert result != null: "Missing required child of type " + elementType + "; tree: "+DebugUtil.psiToString(this, false); return result; } /** * @return children of specified type, taken from stubs (if this element is currently stub-based) or AST (otherwise). */ @NotNull public <S extends StubElement, Psi extends PsiElement> Psi[] getStubOrPsiChildren(@NotNull IStubElementType<S, ? extends Psi> elementType, @NotNull Psi[] array) { T stub = getGreenStub(); if (stub != null) { //noinspection unchecked return (Psi[])stub.getChildrenByType(elementType, array); } else { final ASTNode[] nodes = SharedImplUtil.getChildrenOfType(getNode(), elementType); //noinspection unchecked Psi[] psiElements = (Psi[])Array.newInstance(array.getClass().getComponentType(), nodes.length); for (int i = 0; i < nodes.length; i++) { //noinspection unchecked psiElements[i] = (Psi)nodes[i].getPsi(); } return psiElements; } } /** * @return children of specified type, taken from stubs (if this element is currently stub-based) or AST (otherwise). */ @NotNull public <S extends StubElement, Psi extends PsiElement> Psi[] getStubOrPsiChildren(@NotNull IStubElementType<S, ? extends Psi> elementType, @NotNull ArrayFactory<Psi> f) { T stub = getGreenStub(); if (stub != null) { //noinspection unchecked return (Psi[])stub.getChildrenByType(elementType, f); } else { final ASTNode[] nodes = SharedImplUtil.getChildrenOfType(getNode(), elementType); Psi[] psiElements = f.create(nodes.length); for (int i = 0; i < nodes.length; i++) { //noinspection unchecked psiElements[i] = (Psi)nodes[i].getPsi(); } return psiElements; } } /** * @return children of specified type, taken from stubs (if this element is currently stub-based) or AST (otherwise). */ @NotNull public <Psi extends PsiElement> Psi[] getStubOrPsiChildren(@NotNull TokenSet filter, @NotNull Psi[] array) { T stub = getGreenStub(); if (stub != null) { //noinspection unchecked return (Psi[])stub.getChildrenByType(filter, array); } else { final ASTNode[] nodes = getNode().getChildren(filter); //noinspection unchecked Psi[] psiElements = (Psi[])Array.newInstance(array.getClass().getComponentType(), nodes.length); for (int i = 0; i < nodes.length; i++) { //noinspection unchecked psiElements[i] = (Psi)nodes[i].getPsi(); } return psiElements; } } /** * @return children of specified type, taken from stubs (if this element is currently stub-based) or AST (otherwise). */ @NotNull public <Psi extends PsiElement> Psi[] getStubOrPsiChildren(@NotNull TokenSet filter, @NotNull ArrayFactory<Psi> f) { T stub = getGreenStub(); if (stub != null) { //noinspection unchecked return (Psi[])stub.getChildrenByType(filter, f); } else { final ASTNode[] nodes = getNode().getChildren(filter); Psi[] psiElements = f.create(nodes.length); for (int i = 0; i < nodes.length; i++) { //noinspection unchecked psiElements[i] = (Psi)nodes[i].getPsi(); } return psiElements; } } /** * @return a first ancestor of specified type, in stub hierarchy (if this element is currently stub-based) or AST hierarchy (otherwise). */ @Nullable protected <E extends PsiElement> E getStubOrPsiParentOfType(@NotNull Class<E> parentClass) { T stub = getStub(); if (stub != null) { //noinspection unchecked return (E)stub.getParentStubOfType(parentClass); } return PsiTreeUtil.getParentOfType(this, parentClass); } @Override protected Object clone() { final StubBasedPsiElementBase copy = (StubBasedPsiElementBase)super.clone(); copy.mySubstrateRef = SubstrateRef.createAstStrongRef(getNode()); return copy; } }