processing.mode.java.preproc.PdeParseTreeListener.java Source code

Java tutorial

Introduction

Here is the source code for processing.mode.java.preproc.PdeParseTreeListener.java

Source

/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */

/*
  Part of the Processing project - http://processing.org
    
  Copyright (c) 2019 The Processing Foundation
    
  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License version 2
  as published by the Free Software Foundation.
    
  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.
    
  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software Foundation,
  Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

package processing.mode.java.preproc;

import java.util.*;

import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.misc.Interval;

import org.antlr.v4.runtime.tree.ParseTree;
import processing.core.PApplet;
import processing.mode.java.pdex.TextTransform;
import processing.mode.java.preproc.PdePreprocessor.Mode;
import processing.mode.java.preproc.code.*;

/**
 * ANTLR tree traversal listener that preforms code rewrites as part of sketch preprocessing.
 *
 * <p>
 *   ANTLR tree traversal listener that preforms code rewrites as part of sketch preprocessing,
 *   turning sketch source into compilable Java code. Note that this emits both the Java source
 *   when using javac directly as part of {JavaBuild} as well as {TextTransform.Edit}s when using
 *   the JDT via the {PreprocessingService}.
 * </p>
 */
public class PdeParseTreeListener extends ProcessingBaseListener {

    private final static String VERSION_STR = "3.0.0";
    private static final String SIZE_METHOD_NAME = "size";
    private static final String FULLSCREEN_METHOD_NAME = "fullScreen";
    private final int tabSize;

    private int headerOffset;

    private String sketchName;
    private boolean isTested;
    private TokenStreamRewriter rewriter;

    protected Mode mode = Mode.JAVA;
    private boolean foundMain;

    private int lineOffset;

    private ArrayList<String> coreImports = new ArrayList<>();
    private ArrayList<String> defaultImports = new ArrayList<>();
    private ArrayList<String> codeFolderImports = new ArrayList<>();
    private ArrayList<String> foundImports = new ArrayList<>();
    private ArrayList<TextTransform.Edit> edits = new ArrayList<>();

    private String sketchWidth;
    private String sketchHeight;
    private String sketchRenderer;

    private boolean sizeRequiresRewrite = false;
    private boolean sizeIsFullscreen = false;
    private RewriteResult headerResult;
    private RewriteResult footerResult;

    /**
     * Create a new listener.
     *
     * @param tokens The tokens over which to rewrite.
     * @param newSketchName The name of the sketch being traversed.
     * @param newTabSize Size of tab / indent.
     */
    PdeParseTreeListener(TokenStream tokens, String newSketchName, int newTabSize) {
        rewriter = new TokenStreamRewriter(tokens);
        sketchName = newSketchName;
        tabSize = newTabSize;
    }

    /**
     * Indicate imports for code folders.
     *
     * @param codeFolderImports List of imports for sources sitting in the sketch code folder.
     */
    public void setCodeFolderImports(List<String> codeFolderImports) {
        this.codeFolderImports.clear();
        this.codeFolderImports.addAll(codeFolderImports);
    }

    /**
     * Indicate list of imports required for all sketches to be inserted in preprocessing.
     *
     * @param coreImports The list of imports required for all sketches.
     */
    public void setCoreImports(String[] coreImports) {
        setCoreImports(Arrays.asList(coreImports));
    }

    /**
     * Indicate list of imports required for all sketches to be inserted in preprocessing.
     *
     * @param coreImports The list of imports required for all sketches.
     */
    public void setCoreImports(List<String> coreImports) {
        this.coreImports.clear();
        this.coreImports.addAll(coreImports);
    }

    /**
     * Indicate list of default convenience imports.
     *
     * <p>
     *    Indicate list of imports that are not required for sketch operation but included for the
     *    user's convenience regardless.
     * </p>
     *
     * @param defaultImports The list of imports to include for user convenience.
     */
    public void setDefaultImports(String[] defaultImports) {
        setDefaultImports(Arrays.asList(defaultImports));
    }

    /**
     * Indicate list of default convenience imports.
     *
     * <p>
     *    Indicate list of imports that are not required for sketch operation but included for the
     *    user's convenience regardless.
     * </p>
     *
     * @param defaultImports The list of imports to include for user convenience.
     */
    public void setDefaultImports(List<String> defaultImports) {
        this.defaultImports.clear();
        this.defaultImports.addAll(defaultImports);
    }

    /**
     * Indicate if running in unit tests.
     *
     * @param isTested True if running as part of tests and false otherwise.
     */
    public void setTested(boolean isTested) {
        this.isTested = isTested;
    }

    /**
     * Determine if the user provided their own "main" method.
     *
     * @return True if the sketch code provides a main method. False otherwise.
     */
    public boolean foundMain() {
        return foundMain;
    }

    /**
     * Get the sketch code transformed to grammatical Java.
     *
     * @return Complete sketch code as Java.
     */
    public String getOutputProgram() {
        return rewriter.getText();
    }

    /**
     * Get the rewriter used by this listener.
     *
     * @return Listener's rewriter.
     */
    public TokenStreamRewriter getRewriter() {
        return rewriter;
    }

    /**
     * Get the result of the last preprocessing.
     *
     * @return The result of the last preprocessing.
     */
    public PreprocessorResult getResult() {
        List<String> allImports = new ArrayList<>();

        allImports.addAll(coreImports);
        allImports.addAll(defaultImports);
        allImports.addAll(codeFolderImports);
        allImports.addAll(foundImports);

        List<TextTransform.Edit> allEdits = new ArrayList<>();
        allEdits.addAll(headerResult.getEdits());
        allEdits.addAll(edits);
        allEdits.addAll(footerResult.getEdits());

        return new PreprocessorResult(mode, lineOffset, sketchName, allImports, allEdits, sketchWidth,
                sketchHeight);
    }

    // --------------------------------------------------- listener impl

    /**
     * Endpoint for ANTLR to call when having finished parsing a processing sketch.
     *
     * @param ctx The context from ANTLR for the processing sketch.
     */
    public void exitProcessingSketch(ProcessingParser.ProcessingSketchContext ctx) {
        // header
        RewriteParams rewriteParams = createRewriteParams();

        RewriterCodeGenerator codeGen = new RewriterCodeGenerator(tabSize);

        headerResult = codeGen.writeHeader(rewriter, rewriteParams);

        lineOffset = headerResult.getLineOffset();

        // footer
        TokenStream tokenStream = rewriter.getTokenStream();
        int tokens = tokenStream.size();
        int length = tokenStream.get(tokens - 1).getStopIndex();

        footerResult = codeGen.writeFooter(rewriter, rewriteParams, length);
    }

    /**
     * Endpoint for ANTLR to call when finished parsing a method invocatino.
     *
     * @param ctx The ANTLR context for the method call.
     */
    public void exitMethodCall(ProcessingParser.MethodCallContext ctx) {
        String methodName = ctx.getChild(0).getText();

        if (SIZE_METHOD_NAME.equals(methodName) || FULLSCREEN_METHOD_NAME.equals(methodName)) {
            handleSizeCall(ctx);
        }
    }

    /**
     * Manage parsing out a size or fullscreen call.
     *
     * @param ctx The context of the call.
     */
    private void handleSizeCall(ParserRuleContext ctx) {
        ParserRuleContext testCtx = ctx.getParent().getParent().getParent().getParent();

        boolean isInGlobal = testCtx instanceof ProcessingParser.StaticProcessingSketchContext;

        boolean isInSetup;
        if (!isInGlobal) {
            ParserRuleContext methodDeclaration = testCtx.getParent().getParent();

            isInSetup = isMethodSetup(methodDeclaration);
        } else {
            isInSetup = false;
        }

        ParseTree argsContext = ctx.getChild(2);

        boolean thisRequiresRewrite = false;

        boolean isSize = ctx.getChild(0).getText().equals(SIZE_METHOD_NAME);
        boolean isFullscreen = ctx.getChild(0).getText().equals(FULLSCREEN_METHOD_NAME);

        if (isInGlobal || isInSetup) {
            thisRequiresRewrite = true;

            if (isSize && argsContext.getChildCount() > 2) {
                sketchWidth = argsContext.getChild(0).getText();
                if (PApplet.parseInt(sketchWidth, -1) == -1 && !sketchWidth.equals("displayWidth")) {
                    thisRequiresRewrite = false;
                }

                sketchHeight = argsContext.getChild(2).getText();
                if (PApplet.parseInt(sketchHeight, -1) == -1 && !sketchHeight.equals("displayHeight")) {
                    thisRequiresRewrite = false;
                }

                if (argsContext.getChildCount() > 3) {
                    sketchRenderer = argsContext.getChild(4).getText();
                    if (!(sketchRenderer.equals("P2D") || sketchRenderer.equals("P3D")
                            || sketchRenderer.equals("OPENGL") || sketchRenderer.equals("JAVA2D")
                            || sketchRenderer.equals("FX2D"))) {
                        thisRequiresRewrite = false;
                    }
                }
            }

            if (isFullscreen) {
                sketchWidth = "displayWidth";
                sketchWidth = "displayHeight";

                thisRequiresRewrite = true;
                sizeIsFullscreen = true;

                if (argsContext.getChildCount() > 0) {
                    sketchRenderer = argsContext.getChild(0).getText();
                    if (!(sketchRenderer.equals("P2D") || sketchRenderer.equals("P3D")
                            || sketchRenderer.equals("OPENGL") || sketchRenderer.equals("JAVA2D")
                            || sketchRenderer.equals("FX2D"))) {
                        thisRequiresRewrite = false;
                    }
                }
            }
        }

        if (thisRequiresRewrite) {
            createDelete(ctx.start, ctx.stop);
            createInsertAfter(ctx.stop, "/* size commented out by preprocessor */");
            sizeRequiresRewrite = true;
        }
    }

    /**
     * Determine if a method declaration is for setup.
     *
     * @param declaration The method declaration to parse.
     * @return True if setup and false otherwise.
     */
    private boolean isMethodSetup(ParserRuleContext declaration) {
        if (declaration.getChildCount() < 2) {
            return false;
        }
        return declaration.getChild(1).getText().equals("setup");
    }

    /**
     * Endpoint for ANTLR to call when finished parsing an import declaration.
     *
     * <p>
     *    Endpoint for ANTLR to call when finished parsing an import declaration, remvoing those
     *    declarations from sketch body so that they can be included in the header.
     * </p>
     *
     * @param ctx ANTLR context for the import declaration.
     */
    public void exitImportDeclaration(ProcessingParser.ImportDeclarationContext ctx) {
        ProcessingParser.QualifiedNameContext startCtx = null;

        for (int i = 0; i < ctx.getChildCount(); i++) {
            ParseTree candidate = ctx.getChild(i);
            if (candidate instanceof ProcessingParser.QualifiedNameContext) {
                startCtx = (ProcessingParser.QualifiedNameContext) ctx.getChild(i);
            }
        }

        if (startCtx == null) {
            return;
        }

        Interval interval = new Interval(startCtx.start.getStartIndex(), ctx.stop.getStopIndex());
        String importString = ctx.start.getInputStream().getText(interval);
        String importStringNoSemi = importString.substring(0, importString.length() - 1);
        foundImports.add(importStringNoSemi);

        createDelete(ctx.start, ctx.stop);
    }

    /**
     * Endpoint for ANTLR to call after parsing a decimal point literal.
     *
     * <p>
     *   Endpoint for ANTLR to call when finished parsing a floating point literal, adding an 'f' at
     *   the end to force it float instead of double for API compatability.
     * </p>
     *
     * @param ctx ANTLR context for the literal.
     */
    public void exitFloatLiteral(ProcessingParser.FloatLiteralContext ctx) {
        String cTxt = ctx.getText().toLowerCase();
        if (!cTxt.endsWith("f") && !cTxt.endsWith("d")) {
            createInsertAfter(ctx.stop, "f");
        }
    }

    /**
     * Endpoint for ANTLR to call after parsing a static processing sketch.
     *
     * <p>
     *   Endpoint for ANTLR to call after parsing a static processing sketch, informing this parser
     *   that it is operating on a static sketch (no method or class declarations) so that it writes
     *   the correct header / footer.
     * </p>
     *
     * @param ctx ANTLR context for the sketch.
     */
    public void exitStaticProcessingSketch(ProcessingParser.StaticProcessingSketchContext ctx) {
        mode = Mode.STATIC;
    }

    /**
     * Endpoint for ANTLR to call after parsing a "active" processing sketch.
     *
     * <p>
     *   Endpoint for ANTLR to call after parsing a "active" processing sketch, informing this parser
     *   that it is operating on an active sketch so that it writes the correct header / footer.
     * </p>
     *
     * @param ctx ANTLR context for the sketch.
     */
    public void exitActiveProcessingSketch(ProcessingParser.ActiveProcessingSketchContext ctx) {
        mode = Mode.ACTIVE;
    }

    /**
     * Endpoint for ANTLR to call after parsing a method declaration.
     *
     * <p>
     *   Endpoint for ANTLR to call after parsing a method declaration, making any method "public"
     *   that has:
     *
     *   <ul>
     *     <li>no other access modifier</li>
     *     <li>return type "void"</li>
     *     <li>is either in the context of the sketch class</li>
     *     <li>is in the context of a class definition that extends PApplet</li>
     *   </ul>
     * </p>
     *
     * @param ctx ANTLR context for the method declaration
     */
    public void exitMethodDeclaration(ProcessingParser.MethodDeclarationContext ctx) {
        ParserRuleContext memCtx = ctx.getParent();
        ParserRuleContext clsBdyDclCtx = memCtx.getParent();
        ParserRuleContext clsBdyCtx = clsBdyDclCtx.getParent();
        ParserRuleContext clsDclCtx = clsBdyCtx.getParent();

        boolean inSketchContext = clsBdyCtx instanceof ProcessingParser.StaticProcessingSketchContext
                || clsBdyCtx instanceof ProcessingParser.ActiveProcessingSketchContext;

        boolean inPAppletContext = inSketchContext || (clsDclCtx instanceof ProcessingParser.ClassDeclarationContext
                && clsDclCtx.getChildCount() >= 4 && clsDclCtx.getChild(2).getText().equals("extends")
                && clsDclCtx.getChild(3).getText().endsWith("PApplet"));

        // Find modifiers
        ParserRuleContext possibleModifiers = ctx;

        while (!(possibleModifiers instanceof ProcessingParser.ClassBodyDeclarationContext)) {
            possibleModifiers = possibleModifiers.getParent();
        }

        // Look for visibility modifiers and annotations
        boolean hasVisibilityModifier = false;

        int numChildren = possibleModifiers.getChildCount();

        ParserRuleContext annoationPoint = null;

        for (int i = 0; i < numChildren; i++) {
            boolean childIsVisibility;

            ParseTree child = possibleModifiers.getChild(i);
            String childText = child.getText();

            childIsVisibility = childText.equals("public");
            childIsVisibility = childIsVisibility || childText.equals("private");
            childIsVisibility = childIsVisibility || childText.equals("protected");

            hasVisibilityModifier = hasVisibilityModifier || childIsVisibility;

            boolean isModifier = child instanceof ProcessingParser.ModifierContext;
            if (isModifier && isAnnoation((ProcessingParser.ModifierContext) child)) {
                annoationPoint = (ParserRuleContext) child;
            }
        }

        // Insert at start of method or after annoation
        if (!hasVisibilityModifier) {
            if (annoationPoint == null) {
                createInsertBefore(possibleModifiers.getStart(), " public ");
            } else {
                createInsertAfter(annoationPoint.getStop(), " public ");
            }
        }

        // Check if this was main
        if ((inSketchContext || inPAppletContext) && hasVisibilityModifier
                && ctx.getChild(1).getText().equals("main")) {
            foundMain = true;
        }
    }

    /**
     * Check if this contains an annation.
     *
     * @param child The modifier context to check.
     * @return True if annotation. False otherwise
     */
    private boolean isAnnoation(ProcessingParser.ModifierContext context) {
        if (context.getChildCount() == 0) {
            return false;
        }

        ProcessingParser.ClassOrInterfaceModifierContext classModifierCtx;
        if (!(context.getChild(0) instanceof ProcessingParser.ClassOrInterfaceModifierContext)) {
            return false;
        }

        classModifierCtx = (ProcessingParser.ClassOrInterfaceModifierContext) context.getChild(0);

        return classModifierCtx.getChild(0) instanceof ProcessingParser.AnnotationContext;
    }

    /**
     * Endpoint for ANTLR to call after parsing a primitive type name.
     *
     * <p>
     *   Endpoint for ANTLR to call after parsing a primitive type name, possibly converting that type
     *   to a parse function as part of the Processing API.
     * </p>
     *
     * @param ctx ANTLR context for the primitive name token.
     */
    public void exitFunctionWithPrimitiveTypeName(ProcessingParser.FunctionWithPrimitiveTypeNameContext ctx) {

        String fn = ctx.getChild(0).getText();
        if (!fn.equals("color")) {
            fn = "PApplet.parse" + fn.substring(0, 1).toUpperCase() + fn.substring(1);
            createInsertBefore(ctx.start, fn);
            createDelete(ctx.start);
        }
    }

    /**
     * Endpoint for ANTLR to call after parsing a color primitive token.
     *
     * <p>
     *   Endpoint for ANTLR to call after parsing a color primitive token, fixing "color type" to be
     *   "int" as part of the processing API.
     * </p>
     *
     * @param ctx ANTLR context for the type token.
     */
    public void exitColorPrimitiveType(ProcessingParser.ColorPrimitiveTypeContext ctx) {
        if (ctx.getText().equals("color")) {
            createInsertBefore(ctx.start, "int");
            createDelete(ctx.start, ctx.stop);
        }
    }

    /**
     * Endpoint for ANTLR to call after parsing a hex color literal.
     *
     * @param ctx ANTLR context for the literal.
     */
    public void exitHexColorLiteral(ProcessingParser.HexColorLiteralContext ctx) {
        if (ctx.getText().length() == 7) {
            createInsertBefore(ctx.start, ctx.getText().toUpperCase().replace("#", "0xFF"));
        } else {
            createInsertBefore(ctx.start, ctx.getText().toUpperCase().replace("#", "0x"));
        }

        createDelete(ctx.start, ctx.stop);
    }

    // -- Wrappers around CodeEditOperationUtil --

    /**
     * Insert text before a token.
     *
     * @param location The token before which code should be added.
     * @param text The text to add.
     */
    private void createInsertBefore(Token location, String text) {
        edits.add(CodeEditOperationUtil.createInsertBefore(location, text, rewriter));
    }

    /**
     * Insert text before a location in code.
     *
     * @param locationToken Character offset from start.
     * @param locationOffset
     * @param text Text to add.
     */
    private void createInsertBefore(int locationToken, int locationOffset, String text) {
        edits.add(CodeEditOperationUtil.createInsertBefore(locationToken, locationOffset, text, rewriter));
    }

    /**
     * Insert text after a location in code.
     *
     * @param location The token after which to insert code.
     * @param text The text to insert.
     */
    private void createInsertAfter(Token location, String text) {
        edits.add(CodeEditOperationUtil.createInsertAfter(location, text, rewriter));
    }

    /**
     * Delete from a token to a token inclusive.
     *
     * @param start First token to delete.
     * @param stop Last token to delete.
     */
    private void createDelete(Token start, Token stop) {
        edits.add(CodeEditOperationUtil.createDelete(start, stop, rewriter));
    }

    /**
     * Delete a single token.
     *
     * @param location Token to delete.
     */
    private void createDelete(Token location) {
        edits.add(CodeEditOperationUtil.createDelete(location, rewriter));
    }

    /**
     * Create parameters required by the RewriterCodeGenerator.
     *
     * @return Newly created rewrite params.
     */
    private RewriteParams createRewriteParams() {
        RewriteParamsBuilder builder = new RewriteParamsBuilder(VERSION_STR);

        builder.setSketchName(sketchName);
        builder.setIsTested(isTested);
        builder.setRewriter(rewriter);
        builder.setMode(mode);
        builder.setFoundMain(foundMain);
        builder.setLineOffset(lineOffset);
        builder.setSketchWidth(sketchWidth);
        builder.setSketchHeight(sketchHeight);
        builder.setSketchRenderer(sketchRenderer);
        builder.setIsSizeValidInGlobal(sizeRequiresRewrite);
        builder.setIsSizeFullscreen(sizeIsFullscreen);

        builder.addCoreImports(coreImports);
        builder.addDefaultImports(defaultImports);
        builder.addCodeFolderImports(codeFolderImports);
        builder.addFoundImports(foundImports);

        return builder.build();
    }
}