com.ansorgit.plugins.bash.lang.parser.BashPsiBuilder.java Source code

Java tutorial

Introduction

Here is the source code for com.ansorgit.plugins.bash.lang.parser.BashPsiBuilder.java

Source

/*
 * Copyright 2013 Joachim Ansorg, mail@ansorg-it.com
 * File: BashPsiBuilder.java, Class: BashPsiBuilder
 * Last modified: 2013-04-10
 *
 * 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.ansorgit.plugins.bash.lang.parser;

import com.ansorgit.plugins.bash.lang.BashVersion;
import com.ansorgit.plugins.bash.lang.lexer.BashTokenTypes;
import com.intellij.lang.PsiBuilder;
import com.intellij.lang.WhitespacesAndCommentsBinder;
import com.intellij.lang.impl.PsiBuilderAdapter;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.psi.tree.IElementType;
import com.intellij.util.containers.LimitedPool;
import com.intellij.util.containers.Stack;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.Nullable;

/**
 * The PsiBuilder which has been enhanced to be more helpful for Bash parsing.
 * It provides the possiblity to enable whitespace tokens on demand.
 * <p/>
 * <p/>
 * Date: 10.04.2009
 * Time: 00:35:17
 *
 * @author Joachim Ansorg
 */
public final class BashPsiBuilder extends PsiBuilderAdapter implements PsiBuilder {
    static final Logger log = Logger.getInstance("#bash.BashPsiBuilder");

    private final Stack<Boolean> errorsStatusStack = new Stack<Boolean>();
    private final BashTokenRemapper tokenRemapper;
    private final BashVersion bashVersion;

    //reuse markers in the pool, the PsiBuilder allocates a lot of marker. Markers can be cleaned fairly simply so we will reuse them
    //the original PsiBUilderImpl does it in a similair way
    private final LimitedPool<BashPsiMarker> markerPool = new LimitedPool<BashPsiMarker>(750,
            new LimitedPool.ObjectFactory<BashPsiMarker>() {
                @Override
                public BashPsiMarker create() {
                    return new BashPsiMarker();
                }

                @Override
                public void cleanup(final BashPsiMarker startMarker) {
                    startMarker.clean();
                }
            });
    private final BackquoteData backquoteData = new BackquoteData();
    private final HereDocData hereDocData = new HereDocData();
    private final ParsingStateData parsingStateData = new ParsingStateData();
    private final Project project;

    public BashPsiBuilder(Project project, PsiBuilder wrappedBuilder, BashVersion bashVersion) {
        super(wrappedBuilder);

        this.project = project;
        this.bashVersion = bashVersion;
        this.tokenRemapper = new BashTokenRemapper(this);
        setTokenTypeRemapper(tokenRemapper);
    }

    /**
     * @param enableWhitespace
     * @return
     */
    @Nullable
    public String getTokenText(boolean enableWhitespace) {
        if (enableWhitespace && rawLookup(0) == BashTokenTypes.WHITESPACE) {
            int startOffset = rawTokenTypeStart(0);
            if (startOffset == -1) {
                //first token
                startOffset = 0;
            }

            int length = rawTokenTypeStart(1) - startOffset;
            if (length == 1) {
                return "";
            }

            return StringUtils.repeat(" ", length);
        }

        return getTokenText();
    }

    @Nullable
    public final IElementType getTokenType(boolean withWhitespace) {
        return withWhitespace ? rawLookup(0) : getTokenType();
    }

    @Nullable
    public IElementType getRemappingTokenType() {
        try {
            parsingStateData.enterSimpleCommand();

            return getTokenType();
        } finally {
            parsingStateData.leaveSimpleCommand();
        }
    }

    /**
     * Eats all the following newline tokens.
     *
     * @return True if at least one newline has been read.
     */
    public boolean eatOptionalNewlines() {
        return eatOptionalNewlines(-1);
    }

    /**
     * Reads all line feed tokens of the stream until either there are no more newlines
     * or that maxNewLines is reached.
     *
     * @param maxNewlines The maximum amount of newlines which should be read.
     *                    A value of -1 means there's no limit in read line feed tokens.
     * @return True if at least one newline has been read.
     */
    public boolean eatOptionalNewlines(int maxNewlines) {
        return eatOptionalNewlines(maxNewlines, false);
    }

    public boolean eatOptionalNewlines(int maxNewlines, boolean withWhitespace) {
        if (maxNewlines < 0) {
            maxNewlines = Integer.MAX_VALUE;
        }

        boolean hasNewline;
        int readNewlines;

        if (withWhitespace) {
            //read whitespace tokens
            hasNewline = rawLookup(0) == BashTokenTypes.LINE_FEED;
            readNewlines = 0;
            while (rawLookup(0) == BashTokenTypes.LINE_FEED && readNewlines < maxNewlines) {
                advanceLexer();
                readNewlines++;
            }
        } else {
            //do not read whitespace tokens, step over it
            hasNewline = getTokenType() == BashTokenTypes.LINE_FEED;
            readNewlines = 0;
            while (getTokenType() == BashTokenTypes.LINE_FEED && readNewlines < maxNewlines) {
                advanceLexer();
                readNewlines++;
            }
        }

        return hasNewline;
    }

    public boolean isBash4() {
        return BashVersion.Bash_v4.equals(bashVersion);
    }

    public void remapShebangToComment() {
        tokenRemapper.enableShebangToCommentMapping();
    }

    /**
     * Returns the backquote data which holds the state of the backquote command parsing.
     *
     * @return The state for the backquote command parsing.
     */
    public BackquoteData getBackquoteData() {
        return backquoteData;
    }

    public HereDocData getHereDocData() {
        return hereDocData;
    }

    public final ParsingStateData getParsingState() {
        return parsingStateData;
    }

    /**
     * BashPsiBuilder supports nested levels of error reporting. Error reporting can
     * be switched of on demand to make test functions possible which
     * call the regular parsing functions to check whether the upcoming stream of tokens is
     * valid.
     * Each call to this method has to be followed by a call to leaveLastErrorLevel() .
     *
     * @param status True if error reporting shoudl be switched on.
     *               False if no errors should be added to the resulting tree.
     */
    public void enterNewErrorLevel(boolean status) {
        errorsStatusStack.push(status);
    }

    /**
     * Removes the last error reporting state from the stack of saved states.
     */
    public void leaveLastErrorLevel() {
        if (!errorsStatusStack.isEmpty()) {
            errorsStatusStack.pop();
        }
    }

    /**
     * Overridden method which only reports errors if error reporting is switched on.
     *
     * @param message The message to report.
     */
    public void error(String message) {
        if (isErrorReportingEnabled()) {
            myDelegate.error(message);
        } else if (log.isDebugEnabled()) {
            log.debug("Supressed psi error: " + message);
        }
    }

    public Project getProject() {
        return project;
    }

    /**
     * Returns an enhanced marker which knows about the error reporting state.
     * The error only adds errors to the tree if error reporting is enabled.
     * The marker has the same state as the markers returned by the wrapped PsiBuilder.
     * It only intercepts the error message reporting if necessary.
     *
     * @return The new marker.
     */
    @Override
    public Marker mark() {
        BashPsiMarker marker = markerPool.alloc();
        marker.psiBuilder = this;
        marker.original = myDelegate.mark();

        return marker;
    }

    /**
     * Returns the state of error reporting.
     *
     * @return True if errors should be reported. False if not.
     */
    boolean isErrorReportingEnabled() {
        return errorsStatusStack.isEmpty() || errorsStatusStack.peek();
    }

    private void recycle(BashPsiMarker marker) {
        markerPool.recycle(marker);
    }

    /**
     * An enhanced marker which takes care of error reporting.
     *
     * @author Joachim Ansorg, mail@ansorg-it.com
     */
    private static final class BashPsiMarker implements Marker {
        BashPsiBuilder psiBuilder;
        Marker original;

        public BashPsiMarker() {
        }

        @Override
        public void doneBefore(IElementType type, Marker beforeCandidate) {
            //IntelliJ's API assumes that before is a StartMarker and not another implementation
            //thus we have to pass the original marker
            Marker before = beforeCandidate instanceof BashPsiMarker ? ((BashPsiMarker) beforeCandidate).original
                    : beforeCandidate;

            original.doneBefore(type, before);
            psiBuilder.recycle(this);
        }

        @Override
        public void error(final String errorMessage) {
            if (psiBuilder.isErrorReportingEnabled()) {
                original.error(errorMessage);
            } else {
                drop();

                if (log.isDebugEnabled()) {
                    log.debug("Marker: suppressed error " + errorMessage);
                }
            }
        }

        @Override
        public void drop() {
            original.drop();

            psiBuilder.recycle(this);
        }

        @Override
        public void done(IElementType type) {
            original.done(type);

            psiBuilder.recycle(this);
        }

        @Override
        public void doneBefore(IElementType type, Marker before, String errorMessage) {
            original.doneBefore(type, before, errorMessage);

            psiBuilder.recycle(this);
        }

        @Override
        public void errorBefore(String message, Marker marker) {
            original.errorBefore(message, marker);

            psiBuilder.recycle(this);
        }

        @Override
        public void rollbackTo() {
            original.rollbackTo();

            psiBuilder.recycle(this);
        }

        public void clean() {
            this.original = null;
            this.psiBuilder = null;
        }

        public void collapse(IElementType iElementType) {
            original.collapse(iElementType);

            psiBuilder.recycle(this);
        }

        public Marker precede() {
            return original.precede();
        }

        public void setCustomEdgeTokenBinders(@Nullable WhitespacesAndCommentsBinder whitespacesAndCommentsBinder,
                @Nullable WhitespacesAndCommentsBinder whitespacesAndCommentsBinder1) {
            original.setCustomEdgeTokenBinders(whitespacesAndCommentsBinder, whitespacesAndCommentsBinder1);
        }
    }
}