package org.elixir_lang.beam.psi; import com.ericsson.otp.erlang.OtpErlangDecodeException; import com.intellij.lang.ASTNode; import com.intellij.lang.FileASTNode; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Document; import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.fileTypes.FileType; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.Pair; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.*; import com.intellij.psi.impl.source.PsiFileImpl; import com.intellij.psi.impl.source.PsiFileWithStubSupport; import com.intellij.psi.impl.source.SourceTreeToPsiMap; import com.intellij.psi.impl.source.resolve.FileContextUtil; import com.intellij.psi.impl.source.tree.TreeElement; import com.intellij.psi.scope.ElementClassHint; import com.intellij.psi.scope.PsiScopeProcessor; import com.intellij.psi.search.PsiElementProcessor; import com.intellij.psi.stubs.*; import com.intellij.psi.util.PsiUtilCore; import com.intellij.reference.SoftReference; import com.intellij.util.ArrayUtil; import com.intellij.util.IncorrectOperationException; import org.elixir_lang.ElixirLanguage; import org.elixir_lang.beam.Beam; import org.elixir_lang.beam.MacroNameArity; import org.elixir_lang.beam.chunk.Atoms; import org.elixir_lang.beam.chunk.Exports; import org.elixir_lang.beam.chunk.exports.Export; import org.elixir_lang.beam.psi.impl.ModuleElementImpl; import org.elixir_lang.beam.psi.impl.ModuleImpl; import org.elixir_lang.beam.psi.impl.ModuleStubImpl; import org.elixir_lang.beam.psi.impl.CallDefinitionStubImpl; import org.elixir_lang.beam.psi.stubs.CallDefinitionStub; import org.elixir_lang.beam.psi.stubs.ModuleStub; import org.elixir_lang.psi.ElixirFile; import org.elixir_lang.psi.ModuleOwner; import org.elixir_lang.psi.call.CanonicallyNamed; import org.elixir_lang.psi.stub.impl.ElixirFileStubImpl; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.util.List; import java.util.SortedMap; import java.util.SortedSet; import static com.intellij.reference.SoftReference.dereference; import static org.elixir_lang.beam.Decompiler.defmoduleArgument; import static org.elixir_lang.beam.psi.stubs.ModuleStubElementTypes.MODULE; // See com.intellij.psi.impl.compiled.ClsFileImpl public class BeamFileImpl extends ModuleElementImpl implements ModuleOwner, PsiCompiledFile, PsiFileWithStubSupport { private static final Logger LOGGER = Logger.getInstance(BeamFileImpl.class); private static final String CAN_NOT_MODIFY_MESSAGE = "Cannot modify decompiled Beam files"; private static final String BANNER = "# Source code recreated from a .beam file by IntelliJ Elixir\n" + "# Function clause bodies is not available\n"; private static final Key<Document> MODULE_DOCUMENT_LINK_KEY = Key.create("module.document.link"); @NotNull private final FileViewProvider fileViewProvider; private final boolean isForDecompiling; /** * NOTE: you absolutely MUST NOT hold PsiLock under the mirror lock */ private final Object mirrorLock = new Object(); private final Object stubLock = new Object(); private volatile TreeElement mirrorFileElement; private SoftReference<StubTree> stub; public BeamFileImpl(@NotNull FileViewProvider fileViewProvider) { this(fileViewProvider, false); } private BeamFileImpl(@NotNull FileViewProvider fileViewProvider, boolean isForDecompiling) { this.fileViewProvider = fileViewProvider; this.isForDecompiling = isForDecompiling; } public static PsiFileStub<?> buildFileStub(byte[] bytes) { ElixirFileStubImpl stub = new ElixirFileStubImpl(); Beam beam = null; try { beam = Beam.from(bytes); } catch (IOException e) { LOGGER.error(e); } catch (OtpErlangDecodeException e) { LOGGER.error(e); } ModuleStub moduleStub = buildModuleStub(stub, beam); if (moduleStub == null) { stub = null; } return stub; } @Nullable private static ModuleStub buildModuleStub(PsiFileStub<ElixirFile> parentStub, Beam beam) { ModuleStub moduleStub = null; if (beam != null) { Atoms atoms = beam.atoms(); if (atoms != null) { String moduleName = atoms.moduleName(); if (moduleName != null) { String name = defmoduleArgument(moduleName); moduleStub = new ModuleStubImpl(parentStub, name); buildCallDefinitions(moduleStub, beam, atoms); } } } return moduleStub; } private static void buildCallDefinitions(@NotNull ModuleStub parentStub, @NotNull Beam beam, @NotNull Atoms atoms) { Exports exports = beam.exports(); if (exports != null) { SortedSet<MacroNameArity> macroNameAritySortedSet = exports.macroNameAritySortedSet(atoms); for (MacroNameArity macroNameArity : macroNameAritySortedSet) { buildCallDefinition(parentStub, macroNameArity); } } } @NotNull private static CallDefinitionStub buildCallDefinition(@NotNull ModuleStub parentStub, @NotNull MacroNameArity macroNameArity) { //noinspection ConstantConditions return buildCallDefinition(parentStub, macroNameArity.macro, macroNameArity.name, macroNameArity.arity); } @NotNull private static CallDefinitionStub buildCallDefinition(@NotNull ModuleStub parentStub, @NotNull String macro, @NotNull String name, @NotNull Integer arity) { return new CallDefinitionStubImpl(parentStub, macro, name, arity); } @Override public PsiFile getDecompiledPsiFile() { return (PsiFile) getMirror(); } /** * Returns the virtual file corresponding to the PSI file. * * @return the virtual file, or null if the file exists only in memory. */ @Override public VirtualFile getVirtualFile() { return fileViewProvider.getVirtualFile(); } @NotNull @Override public String getName() { return getVirtualFile().getName(); } /** * Renames the element. * * @param name the new element name. * @return the element corresponding to this element after the rename (either <code>this</code> * or a different element if the rename caused the element to be replaced). * @throws IncorrectOperationException if the modification is not supported or not possible for some reason. */ @Override public PsiElement setName(@NonNls @NotNull String name) throws IncorrectOperationException { throw new IncorrectOperationException(CAN_NOT_MODIFY_MESSAGE); } @Override public boolean processChildren(PsiElementProcessor<PsiFileSystemItem> processor) { return true; } /** * Returns the directory containing the file. * * @return the containing directory, or null if the file exists only in memory. */ @Override public PsiDirectory getContainingDirectory() { VirtualFile parentFile = getVirtualFile().getParent(); PsiDirectory containingDirectory = null; if (parentFile != null) { containingDirectory = getManager().findDirectory(parentFile); } return containingDirectory; } @Override public boolean isDirectory() { return false; } /** * Returns the PSI manager for the project to which the PSI element belongs. * * @return the PSI manager instance. */ @Override public PsiManager getManager() { return fileViewProvider.getManager(); } /** * Returns the array of children for the PSI element. * Important: In some implementations children are only composite elements, i.e. not a leaf elements * * @return the array of child elements. */ @NotNull @Override public PsiElement[] getChildren() { return modulars(); } @NotNull public CanonicallyNamed[] modulars() { return (CanonicallyNamed[]) getStub().getChildrenByType(MODULE, new ModuleImpl[1]); } public PsiFileStub getStub() { return getStubTree().getRoot(); } @NotNull @Override public StubTree getStubTree() { ApplicationManager.getApplication().assertReadAccessAllowed(); StubTree stubTree = dereference(stub); if (stubTree != null) return stubTree; // build newStub out of lock to avoid deadlock StubTree newStubTree = (StubTree) StubTreeLoader .getInstance() .readOrBuild(getProject(), getVirtualFile(), this); if (newStubTree == null) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("No stub for BEAM file in index: " + getVirtualFile().getPresentableUrl()); } newStubTree = new StubTree(new ElixirFileStubImpl()); } synchronized (stubLock) { stubTree = dereference(stub); if (stubTree != null) return stubTree; stubTree = newStubTree; @SuppressWarnings("unchecked") PsiFileStubImpl<PsiFile> fileStub = (PsiFileStubImpl) stubTree.getRoot(); fileStub.setPsi(this); stub = new SoftReference<StubTree>(stubTree); } return stubTree; } @Nullable @Override public ASTNode findTreeForStub(StubTree stubTree, StubElement<?> stubElement) { return null; } @Override public PsiDirectory getParent() { return getContainingDirectory(); } /** * Returns the first child of the PSI element. * * @return the first child, or null if the element has no children. */ @Override public PsiElement getFirstChild() { @SuppressWarnings("unchecked") final List<StubElement> children = getStub().getChildrenStubs(); PsiElement firstChild = null; if (!children.isEmpty()) { firstChild = children.get(0).getPsi(); } return firstChild; } /** * Returns the last child of the PSI element. * * @return the last child, or null if the element has no children. */ @Override public PsiElement getLastChild() { @SuppressWarnings("unchecked") final List<StubElement> children = getStub().getChildrenStubs(); PsiElement lastChild = null; if (!children.isEmpty()) { lastChild = children.get(children.size() - 1).getPsi(); } return lastChild; } /** * Returns the next sibling of the PSI element. * * @return the next sibling, or null if the node is the last in the list of siblings. */ @Override public PsiElement getNextSibling() { @SuppressWarnings("ConstantConditions") final PsiElement[] siblings = getParent().getChildren(); final int i = ArrayUtil.indexOf(siblings, this); PsiElement nextSibling; if (i < 0 || i >= siblings.length - 1) { nextSibling = null; } else { nextSibling = siblings[i + 1]; } return nextSibling; } /** * Returns the previous sibling of the PSI element. * * @return the previous sibling, or null if the node is the first in the list of siblings. */ @Override public PsiElement getPrevSibling() { @SuppressWarnings("ConstantConditions") final PsiElement[] siblings = getParent().getChildren(); final int i = ArrayUtil.indexOf(siblings, this); PsiElement prevSibling; if (i < 1) { prevSibling = null; } else { prevSibling = siblings[i - 1]; } return prevSibling; } /** * Returns the file containing the PSI element. * * @return the file instance, or null if the PSI element is not contained in a file (for example, * the element represents a package or directory). * @throws PsiInvalidElementAccessException if this element is invalid */ @Override public PsiFile getContainingFile() throws PsiInvalidElementAccessException { if (!isValid()) { throw new PsiInvalidElementAccessException(this); } return this; } @Override public void appendMirrorText(@NotNull StringBuilder buffer, int indentLevel) { buffer.append(BANNER); CanonicallyNamed[] modulars = modulars(); if (modulars.length > 0) { appendText(modulars[0], buffer, 0); } } /** * Creates a copy of the file containing the PSI element and returns the corresponding * element in the created copy. Resolve operations performed on elements in the copy * of the file will resolve to elements in the copy, not in the original file. * * @return the element in the file copy corresponding to this element. */ @Override public PsiElement copy() { return this; } /** * Checks if this PSI element is valid. Valid elements and their hierarchy members * can be accessed for reading and writing. Valid elements can still correspond to * underlying documents whose text is different, when those documents have been changed * and not yet committed ({@link PsiDocumentManager#commitDocument(Document)}). * (In this case an attempt to change PSI will result in an exception). * <p> * Any access to invalid elements results in {@link PsiInvalidElementAccessException}. * <p> * Once invalid, elements can't become valid again. * <p> * Elements become invalid in following cases: * <ul> * <li>They have been deleted via PSI operation ({@link #delete()})</li> * <li>They have been deleted as a result of an incremental reparse (document commit)</li> * <li>Their containing file has been changed externally, or renamed so that its PSI had to be rebuilt from scratch</li> * </ul> * * @return true if the element is valid, false otherwise. * @see PsiUtilCore#ensureValid(PsiElement) */ @Override public boolean isValid() { return isForDecompiling || getVirtualFile().isValid(); } /** * Checks if the contents of the element can be modified (if it belongs to a * non-read-only source file.) * * @return true if the element can be modified, false otherwise. */ @Override public boolean isWritable() { return false; } /** * Passes the declarations contained in this PSI element and its children * for processing to the specified scope processor. * * @param processor the processor receiving the declarations. * @param lastParent the child of this element has been processed during the previous * step of the tree up walk (declarations under this element do not need * to be processed again) * @param place the original element from which the tree up walk was initiated. @return true if the declaration processing should continue or false if it should be stopped. */ @Override public boolean processDeclarations(@NotNull PsiScopeProcessor processor, @NotNull ResolveState state, @Nullable PsiElement lastParent, @NotNull PsiElement place) { processor.handleEvent(PsiScopeProcessor.Event.SET_DECLARATION_HOLDER, this); final ElementClassHint classHint = processor.getHint(ElementClassHint.KEY); boolean keepProcessing = true; if (classHint == null || classHint.shouldProcess(ElementClassHint.DeclarationKind.CLASS)) { for (CanonicallyNamed modular : modulars()) { if (!processor.execute(modular, state)) { keepProcessing = false; break; } } } return keepProcessing; } /** * Returns the element which should be used as the parent of this element in a tree up * walk during a resolve operation. For most elements, this returns <code>getParent()</code>, * but the context can be overridden for some elements like code fragments (see * {@link PsiElementFactory#createCodeBlockCodeFragment(String, PsiElement, boolean)}). * * @return the resolve context element. */ @Nullable @Override public PsiElement getContext() { return FileContextUtil.getFileContext(this); } /** * Checks if an actual source or class file corresponds to the element. Non-physical elements include, * for example, PSI elements created for the watch expressions in the debugger. * Non-physical elements do not generate tree change events. * Also, {@link PsiDocumentManager#getDocument(PsiFile)} returns null for non-physical elements. * Not to be confused with {@link FileViewProvider#isPhysical()}. * * @return true if the element is physical, false otherwise. */ @Override public boolean isPhysical() { return true; } /** * Gets the modification stamp value. Modification stamp is a value changed by any modification * of the content of the file. Note that it is not related to the file modification time. * * @return the modification stamp value * @see VirtualFile#getModificationStamp() */ @Override public long getModificationStamp() { return getVirtualFile().getModificationStamp(); } /** * Returns the same file. * * @return the same file */ @NotNull @Override public PsiFile getOriginalFile() { return this; } /** * Returns the file type for the file. * * @return {@link org.elixir_lang.beam.FileType#INSTANCE} */ @NotNull @Override public FileType getFileType() { return org.elixir_lang.beam.FileType.INSTANCE; } /** * This file. * * @return a single-element array containing <code>this</code> * @deprecated Use {@link FileViewProvider#getAllFiles()} instead. */ @NotNull @Override public PsiFile[] getPsiRoots() { return new PsiFile[]{this}; } @NotNull @Override public FileViewProvider getViewProvider() { return fileViewProvider; } @Override public FileASTNode getNode() { return null; } /** * Called by the PSI framework when the contents of the file changes. Can be used to invalidate * file-level caches. If you override this method, you <b>must</b> call the base class implementation. */ @Override public void subtreeChanged() { /* .beam files are assumed not to change internally without the file itself changing, which the file event system will pickup */ } /** * Checks if it is possible to rename the element to the specified name, * and throws an exception if the rename is not possible. Does not actually modify anything. * * @param name the new name to check the renaming possibility for. * @throws IncorrectOperationException if the rename is not supported or not possible for some reason. */ @Override public void checkSetName(String name) throws IncorrectOperationException { throw new IncorrectOperationException(CAN_NOT_MODIFY_MESSAGE); } /** * Returns the corresponding PSI element in a decompiled file created by IDEA from * the library element. * * @return the counterpart of the element in decompiled file. */ @Override public PsiElement getMirror() { TreeElement mirrorTreeElement = mirrorFileElement; if (mirrorTreeElement == null) { synchronized (mirrorLock) { mirrorTreeElement = mirrorFileElement; if (mirrorTreeElement == null) { VirtualFile file = getVirtualFile(); String fileName = file.getNameWithoutExtension() + ".ex"; final Document document = FileDocumentManager.getInstance().getDocument(file); assert document != null : file.getUrl(); CharSequence mirrorText = document.getImmutableCharSequence(); PsiFileFactory factory = PsiFileFactory.getInstance(getManager().getProject()); PsiFile mirror = factory.createFileFromText( fileName, ElixirLanguage.INSTANCE, mirrorText, false, false ); mirrorTreeElement = SourceTreeToPsiMap.psiToTreeNotNull(mirror); try { final TreeElement finalMirrorTreeElement = mirrorTreeElement; ProgressManager.getInstance().executeNonCancelableSection(new Runnable() { public void run() { setMirror(finalMirrorTreeElement); putUserData(MODULE_DOCUMENT_LINK_KEY, document); } }); } catch (InvalidMirrorException e) { //noinspection ThrowableResultOfMethodCallIgnored LOGGER.error(file.getUrl(), e); } ((PsiFileImpl) mirror).setOriginalFile(this); mirrorFileElement = mirrorTreeElement; } } } return mirrorTreeElement.getPsi(); } @Override public void setMirror(@NotNull TreeElement element) throws InvalidMirrorException { PsiElement mirrorElement = SourceTreeToPsiMap.treeToPsiNotNull(element); if (!(mirrorElement instanceof ElixirFile)) { throw new InvalidMirrorException("Unexpected mirror file: " + mirrorElement); } ElixirFile mirrorFile = (ElixirFile) mirrorElement; setMirrors(modulars(), mirrorFile.modulars()); } }