com.satisfyingstructures.J2S.J2SConvertBasicFor.java Source code

Java tutorial

Introduction

Here is the source code for com.satisfyingstructures.J2S.J2SConvertBasicFor.java

Source

/*
The MIT License (MIT)
    
Copyright (c) 2016 Torsten Louland
    
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
    
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
    
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

/**
 * Created by Torsten Louland on 22/10/2016.
 */
package com.satisfyingstructures.J2S;

import com.satisfyingstructures.J2S.antlr.Java8Parser;
import com.satisfyingstructures.J2S.antlr.Java8BaseVisitor;

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

import java.util.List;
import java.util.ArrayList;

public class J2SConvertBasicFor {

    protected final J2SRewriter rewriter;

    public J2SConvertBasicFor(J2SRewriter rewriter) {
        this.rewriter = rewriter;
    }

    public static void convert(ParserRuleContext ctx, J2SRewriter rewriter) {
        J2SConvertBasicFor converter = new J2SConvertBasicFor(rewriter);
        String couldNotConvertBecause = converter.convertBasicForStatementToForInLoopOrSayWhyNot(ctx);
        if (null != couldNotConvertBecause) {
            // Insert the original as a comment
            TerminalNode tnA = ctx.getToken(Java8Parser.FOR, 0), tnB = ctx.getToken(Java8Parser.RPAREN, 0);
            if (null != tnA && null != tnB) {
                CharStream cs = tnA.getSymbol().getInputStream();
                String s = cs.getText(Interval.of(tnA.getSymbol().getStartIndex(), tnB.getSymbol().getStopIndex()));
                rewriter.insertComment(s, ctx, J2SRewriter.CommentWhere.beforeLineBreak);
                rewriter.insertComment(
                        "...not converted to a for-in expression because " + couldNotConvertBecause + ".", ctx,
                        J2SRewriter.CommentWhere.beforeLineBreak);
            }
            converter.convertBasicForStatementToWhileLoop(ctx);
        }
    }

    private void convertBasicForStatementToWhileLoop(ParserRuleContext ctx) {
        BasicForToWhileConverter cvt = new BasicForToWhileConverter();
        cvt.convert(ctx);
    }

    private class BasicForToWhileConverter extends Java8BaseVisitor<Object> {
        /*  Swift 3.0 eliminated the for(;;) statement from the language (for strong business cases such as 'It is rarely
        used', 'not very Swift-like' and 'The value of this construct is limited'). It is difficult and not always
        possible to map it to a for tuple in sequence loop, so this class implements the fallback conversion to an
        equivalent if ungainly while loop.
            
        We convert
            for ( forInit ; expression ; forNext )
                statement
        to
            do {
                forInit()
                outer: while expression() {
                    inner: do {
                        statement
                    }
                    forNext()
                }
            }
            
        We only need the inner do scope if there are nested continue statements. If this is the case,
        then we convert continue to {break inner} and break to {break outer}. If this is not the case, then we don't
        need the while loop label and we don't need to convert break statements.
            
        We only need the outer do scope if variables are declared in the forInit, so as to avoid
        redeclaration of variables in the enclosing scope, and then only if the for statement has siblings at the
        same level, i.e. it is within a block.
            
        There are also degenerate cases where scopes are not needed because forInit, expression and forNext are
        optional and statement can be empty.
            
        We start by visiting the for statement subtree and collecting all the break and continue contexts that apply
        to the scope of the for statement, i.e., that do not apply to sub-scopes, and do not already jump out to a
        labelled scope.
            
        Then we decide whether we need to create the additional do scopes, and process accordingly.
        */
        List<Java8Parser.BreakStatementContext> breakStatementsToConvert;
        List<Java8Parser.ContinueStatementContext> continueStatementsToConvert;
        int switchDepth = 0;

        //
        private void convert(ParserRuleContext ctx) {
            int forRule = ctx.getRuleIndex();
            if (forRule != Java8Parser.RULE_basicForStatement
                    && forRule != Java8Parser.RULE_basicForStatementNoShortIf)
                return; // not our expected parameter type
            if (null != breakStatementsToConvert)
                return; // we're already busy

            // Init instance variables
            switchDepth = 0;
            breakStatementsToConvert = new ArrayList<>();
            continueStatementsToConvert = new ArrayList<>();

            // Get to know more about our for statement
            // 'for' '(' forInit? ';' expression? ';' forUpdate? ')' ( statement | statementNoShortIf )
            Boolean noShortIf = forRule == Java8Parser.RULE_basicForStatementNoShortIf;
            Java8Parser.ForInitContext forInitCtx = ctx.getChild(Java8Parser.ForInitContext.class, 0);
            Java8Parser.ExpressionContext expressionCtx = ctx.getChild(Java8Parser.ExpressionContext.class, 0);
            Java8Parser.ForUpdateContext forUpdateCtx = ctx.getChild(Java8Parser.ForUpdateContext.class, 0);
            ParserRuleContext statementCtx = ctx.getChild(
                    noShortIf ? Java8Parser.StatementNoShortIfContext.class : Java8Parser.StatementContext.class,
                    0);
            ParserRuleContext statementSubCtx = statementCtx.getChild(ParserRuleContext.class, 0);
            ParserRuleContext statementSubSubCtx = statementSubCtx.getChild(ParserRuleContext.class, 0);
            Boolean statementisEmpty = statementSubSubCtx.getRuleIndex() == Java8Parser.RULE_emptyStatement;

            // Assess whether we need an inner scope by visiting the subtree and gathering break and continue statements
            if (null != forUpdateCtx && !statementisEmpty)
                ctx.accept(this);
            Boolean needInnerScope = continueStatementsToConvert.size() > 0;

            // Assess whether we need an outer scope by checking if we have a forInit that declares local variables
            int forInitSubrule = null != forInitCtx ? forInitCtx.getChild(ParserRuleContext.class, 0).getRuleIndex()
                    : 0;
            Boolean needOuterScope = forInitSubrule == Java8Parser.RULE_localVariableDeclaration;

            // Assess and set up labels
            ParserRuleContext labelForCtx = null; // pre-existing label if any
            String labelFor = null;
            if (needInnerScope || needOuterScope) { // If we are adding any labels, we need to know if there is a pre-existing label.
                ParserRuleContext parentCtx = ctx.getParent(); // forStatement || forStatementNoShortIf
                ParserRuleContext gparenCtx = parentCtx.getParent(); // statement || statementNoShortIf
                ParserRuleContext enclosingCtx = gparenCtx.getParent(); // ??? labeledStatement || labeledStatementNoShortIf
                int enclosingRule = enclosingCtx.getRuleIndex();
                if (enclosingRule == Java8Parser.RULE_labeledStatement
                        || enclosingRule == Java8Parser.RULE_labeledStatementNoShortIf) {
                    labelForCtx = enclosingCtx;
                    labelFor = labelForCtx.getToken(Java8Parser.Identifier, 0).getText();
                    // now step out to first enclosing context that is not a label
                    while (enclosingRule == Java8Parser.RULE_labeledStatement
                            || enclosingRule == Java8Parser.RULE_labeledStatementNoShortIf) {
                        enclosingCtx = enclosingCtx.getParent().getParent();
                        enclosingRule = enclosingCtx.getRuleIndex();
                    }
                }
                // If we don't have siblings at our enclosing scope level, then we don't need to restrict our local
                // // variable definition with an outer scope.
                if (enclosingRule != Java8Parser.RULE_blockStatement
                        || enclosingCtx.getParent().getChildCount() == 1)
                    needOuterScope = false;
            }
            String labelInner = null;
            String labelOuter = null;
            if (needInnerScope) {
                if (null != labelFor) {
                    labelOuter = labelFor;
                    labelInner = labelOuter + "_statement";
                } else // make standard label suffixed with line number of first token
                {
                    int n = ctx.start.getStartIndex() % 1000;
                    labelOuter = "loop_" + n;
                    labelInner = "statement_" + n;
                }
            }

            /*  Do the token mapping
                
            We have four mappings based on NI(=needInnerScope) and NO(=needOuterScope), and within that we also have
            to give the right treatment for the optionality of forInit, expression and forUpdate.
                
            From:
                            'for' '(' forInit? ';' expression? ';' forUpdate? ')' statement
            To:
            -   -           (forInit '; ')? 'while' expression ?: 'true' ('{' statement forUpdate '}' | statement)
            NI  -           (forInit '; ')? labelOuter ': while' expression ?: 'true'
                                '{ ' labelInner ': do {' statement '}; ' forUpdate '}'
            -   NO          'do {' forInit '; while' expression ?: 'true'
                                ('{' statement forUpdate '}' | statement) '}'
            NI  NO          'do {' forInit '; ' labelOuter ': while' expression ?: 'true'
                                '{ ' labelInner ': do {' statement '}; ' forUpdate '}}'
                
            We also have tricky complications:
                1) The for may already be labelled. If so, and we need to add an outer scope, then the label has to
                still refer to the for loop and not the new scope. We either have to wrap the labelled statement in
                the new scope or suppress the label and replicate it inside the new scope.
                2) This method is invoked when the listener is hearing an exit message, i.e. after stepping out from
                a traversed subtree. This means that other listener methods will have recorded changes to the
                subtree in the rewriter. In particular,
                    a) J2SwiftListener.wrapStatementInBracesIfNecessary() may have enclosed our statement in curly
                    braces, and because we need to insert the closing of a new inner scope inside those braces, we
                    need to detect and handle the cases of pre-existing and newly added braces separately.
                    b) when we move forUpdate to be after statement, we need to getText for the forUpdate from the
                    rewriter, so that we preserve what has already been transformed.
                
            Conversions for each token:
            T1 'for'    : if needOuterScope 'do {' else deleted
            T2 '('      : delete
            T3 forInit? : unchanged
            T4 ';'      : set to '' if !forInit,
                        plus
                          if labelOuter, append ' '+labelOuter+':' ,
                          else
                          if labelFor, append ' '+labelFor+':'
                          and if needOuterScope && labelFor delete labelForCtx.Identifier and ':',
                        plus
                          append ' while'
                        plus
                          if !expression, append ' true'
            T5 expression? : unchanged
            T6 ';'      : delete
            T7 forUpdate: if present, getText of this token from rewriter and save for later, then delete forUpdate
            T8 ')'      : if needInnerScope replace with '{ do', otherwise delete
            T9 statement: (this should alsways start and stop with braces - if not pre-existing, then by processing)
                          if !needInnerScope we need to insert the saved forUpdate text before the closing brace,
                          else append ' ' + saved forUpdate text;
                        plus
                          if needOuterScope append '}'
                
            If needInnerScope, we also need to add destination labels to continue and break statements
            */

            // Add destination labels to continue and break statements. Do this now, before we operate on statementCtx
            // as a whole.
            if (needInnerScope) {
                for (Java8Parser.ContinueStatementContext continueCtx : continueStatementsToConvert)
                    rewriter.replace(continueCtx.start, "break " + labelInner);
                for (Java8Parser.BreakStatementContext breakCtx : breakStatementsToConvert)
                    rewriter.replace(breakCtx.start, "break " + labelOuter);
            }

            TerminalNode tn;
            Token token;
            String replacement;
            int startIdx, stopIdx;

            // T1 'for'    : if needOuterScope 'do {' else deleted
            token = (tn = ctx.getToken(Java8Parser.FOR, 0)).getSymbol();
            if (needOuterScope)
                rewriter.replace(token, "do {");
            else
                rewriter.deleteAndAdjustWhitespace(token);

            // T2 '('      : delete
            //  token = (tn = ctx.getToken(Java8Parser.LPAREN, 0)).getSymbol();
            //  rewriter.deleteAndAdjustWhitespace(token);
            // ...now deleted separately

            // T3 no change

            // T4 ';' - see comment above
            token = (tn = ctx.getToken(Java8Parser.SEMI, 0)).getSymbol();
            replacement = null != forInitCtx ? ";" : "";
            if (null != labelFor) // already labelled
            {
                if (needOuterScope) // and need to move the label inside the new outer scope
                {
                    replacement += " " + labelFor + ":";
                    // delete the one outside
                    rewriter.replace(labelForCtx.getToken(Java8Parser.Identifier, 0).getSymbol(), null);
                    rewriter.replace(labelForCtx.getToken(Java8Parser.COLON, 0).getSymbol(), null);
                }
            } else if (null != labelOuter) // adding new label
                replacement += " " + labelOuter + ":";
            replacement += " while ";
            if (null == expressionCtx)
                replacement += " true";
            rewriter.replaceAndAdjustWhitespace(token, replacement);

            // T5 expression? : unchanged

            // T6 ';'      : delete
            token = (tn = ctx.getToken(Java8Parser.SEMI, 1)).getSymbol();
            rewriter.deleteAndAdjustWhitespace(token);

            // T7 forUpdate: if present, getText of this token from rewriter and save for later, then delete forUpdate
            String forUpdateText = null;
            if (null != forUpdateCtx) {
                forUpdateText = rewriter.getText(forUpdateCtx);
                rewriter.delete(forUpdateCtx);
            }

            // T8 ')'      : if needInnerScope replace with '{ do', otherwise delete
            token = (tn = ctx.getToken(Java8Parser.RPAREN, 0)).getSymbol();
            //  rewriter.replace(token, needInnerScope ? "{ "+labelInner+": do" : null);
            // ...now deleted separately
            if (needInnerScope)
                rewriter.insertAfter(token, "{ " + labelInner + ": do ");

            // T9 statement: insert forUpdateText before closing brace if !needInnerScope, else append "until false" + forUpdateText + close brace
            token = null;
            Interval interval;
            interval = Interval.of(statementCtx.stop.getTokenIndex(), statementCtx.stop.getTokenIndex());
            //  interval = Interval.of(statementCtx.start.getTokenIndex(), statementCtx.stop.getTokenIndex());
            replacement = rewriter.getText(interval);
            int i = replacement.lastIndexOf("}");
            String beforeBrace = i == -1 ? replacement : replacement.substring(0, i++);
            int l = i == -1 ? 0 : replacement.length() - i;
            String afterBrace = l > 0 ? replacement.substring(i, l) : "";
            if (null != forUpdateText) {
                if (needInnerScope)
                    afterBrace = "; " + forUpdateText + "; } " + afterBrace;
                else
                    beforeBrace += " " + forUpdateText + "; ";
            }
            if (needOuterScope)
                afterBrace += " }";
            replacement = beforeBrace + "}" + afterBrace;
            rewriter.replaceAndAdjustWhitespace(interval.a, interval.b, replacement);
        }

        // labelling
        @Override
        public Object visitBreakStatement(Java8Parser.BreakStatementContext ctx) {
            if (null == ctx.Identifier()) // add destination if the break has not already got one
                breakStatementsToConvert.add(ctx);
            return null;
        }

        @Override
        public Object visitContinueStatement(Java8Parser.ContinueStatementContext ctx) {
            if (null == ctx.Identifier()) // add destination if the continue has not already got one
                continueStatementsToConvert.add(ctx);
            return null;
        }

        // skip - these subtrees start a new scope for break and continue:
        @Override
        public Object visitWhileStatement(Java8Parser.WhileStatementContext ctx) {
            return null;
        }

        @Override
        public Object visitForStatement(Java8Parser.ForStatementContext ctx) {
            return null;
        }

        // skip - per statement type
        @Override
        public Object visitStatementWithoutTrailingSubstatement(
                Java8Parser.StatementWithoutTrailingSubstatementContext ctx) {
            int statementSubRule = ctx.getChild(ParserRuleContext.class, 0).getRuleIndex();
            switch (statementSubRule) {
            //  We comment out the cases that we do not want to descend into
            case Java8Parser.RULE_breakStatement: // we want to process this
                if (switchDepth > 0) // but only if its not inside a switch
                    break;
            case Java8Parser.RULE_continueStatement: // we want to process this
            case Java8Parser.RULE_block: // can contain deeper nested break and continue
                //  case Java8Parser.RULE_emptyStatement:           // cannot contain break and continue
                //  case Java8Parser.RULE_expressionStatement:      // cannot contain break and continue
                //  case Java8Parser.RULE_assertStatement:          // cannot contain break and continue
                //  case Java8Parser.RULE_doStatement:              // starts new scope for both break and continue
                //  case Java8Parser.RULE_returnStatement:          // cannot contain break and continue
            case Java8Parser.RULE_synchronizedStatement: // can contain deeper nested break and continue
                //  case Java8Parser.RULE_throwStatement:           // cannot contain break and continue
            case Java8Parser.RULE_tryStatement: // can contain deeper nested break and continue
                visitChildren(ctx);
                break;
            case Java8Parser.RULE_switchStatement: // starts new scope for break, but not continue
                switchDepth++;
                visitChildren(ctx);
                switchDepth--;
                break;
            default:
                break;
            }
            return null;
        }
    }

    private String convertBasicForStatementToForInLoopOrSayWhyNot(ParserRuleContext ctx) {
        int forRule = ctx.getRuleIndex();
        if (forRule != Java8Parser.RULE_basicForStatement && forRule != Java8Parser.RULE_basicForStatementNoShortIf)
            return "statement kind is not as expected"; // not our expected parameter type
        // Get to know more about our for statement
        // 'for' '(' forInit? ';' expression? ';' forUpdate? ')' ( statement | statementNoShortIf )
        Boolean noShortIf = forRule == Java8Parser.RULE_basicForStatementNoShortIf;
        Java8Parser.ForInitContext forInitCtx = ctx.getChild(Java8Parser.ForInitContext.class, 0);
        Java8Parser.ExpressionContext expressionCtx = ctx.getChild(Java8Parser.ExpressionContext.class, 0);
        Java8Parser.ForUpdateContext forUpdateCtx = ctx.getChild(Java8Parser.ForUpdateContext.class, 0);
        ParserRuleContext statementCtx = ctx.getChild(
                noShortIf ? Java8Parser.StatementNoShortIfContext.class : Java8Parser.StatementContext.class, 0);
        ParserRuleContext statementSubCtx = statementCtx.getChild(ParserRuleContext.class, 0);
        ParserRuleContext statementSubSubCtx = statementSubCtx.getChild(ParserRuleContext.class, 0);
        Boolean statementisEmpty = statementSubSubCtx.getRuleIndex() == Java8Parser.RULE_emptyStatement;
        /*
        'for' '(' forInit? ';' expression? ';' forUpdate? ')' ( statement | statementNoShortIf )
            
        Swift 3.0 has got rid of for(;;) statements for stong business cases such as...
            'It is rarely used'
            'not very Swift-like'
            'The value of this construct is limited'
        ...and other total crap.
            
        We can convert simple equivalents of
            for ( i = startvalue ; i < endvalue ; i += step)
        to
            for i in start..<end
        or
            for i in start.stride(to: end by: step)
            
        To identify this we look for
        1) have a forUpdate, which...
            a) operates on a single loop variable
                forUpdate().statementExpressionList().statementExpression().count()==1
            b) incorporates increment or decrement by a constant step (++i,i++,i+=step,--i,i--,i-=step,)
                statementExpression rule is RULE_(assignment|preinc|postinc|predec|postdec)
            c) operates on the same variable tested in expression (compare - 2b)
        2) have an expression, which...
            a) should be a simple comparison (<,<=,!=,>,>=, implicit non-zero)
            b) one side should be same as the loop var (compare - 1c)
            c) other side should not mutate within the loop - we can't tell this, too difficult
        3) forInit
            a) must be
                i) empty(start with loop var existing value), or
                ii) simple init of a single loop var, or
                iii) simple declaration of a loop var
        */
        // 1) Update statement. We need one...
        if (null == forUpdateCtx)
            return "it lacks an update statement";
        // 1a) and it must operate on a single variable
        if (forUpdateCtx.statementExpressionList().getChildCount() != 1)
            return "there is more than one expression in the update statement";
        // 1b) and it must be a simple increment or decrement
        Java8Parser.StatementExpressionContext updateStatementExpressionCtx = forUpdateCtx.statementExpressionList()
                .statementExpression(0);
        //  statementExpression : assignment | preIncrementExpression | preDecrementExpression
        //                                   | postIncrementExpression | postDecrementExpression
        //                                   | methodInvocation | classInstanceCreationExpression
        ParserRuleContext updateExpressionCtx = updateStatementExpressionCtx.getChild(ParserRuleContext.class, 0);
        int updateExpressionRule = updateExpressionCtx.getRuleIndex();
        boolean ascending_sequence;
        boolean open_interval;
        ParserRuleContext stepExpressionCtx = null;
        switch (updateExpressionRule) {

        // unaryExpression : preIncrementExpression | preDecrementExpression
        //                 | '+' unaryExpression | '-' unaryExpression
        //                 | unaryExpressionNotPlusMinus
        // preDecrementExpression : '--' unaryExpression
        // preIncrementExpression : '++' unaryExpression
        case Java8Parser.RULE_preDecrementExpression:
            ascending_sequence = false;
            break;
        case Java8Parser.RULE_preIncrementExpression:
            ascending_sequence = true;
            break;

        // postfixExpression : ( primary | expressionName ) ( '++' | '--')*
        // postIncrementExpression : postfixExpression '++'
        // postDecrementExpression : postfixExpression '--'
        case Java8Parser.RULE_postDecrementExpression:
            ascending_sequence = false;
            break;
        case Java8Parser.RULE_postIncrementExpression:
            ascending_sequence = true;
            break;

        // assignment : leftHandSide assignmentOperator expression
        // leftHandSide : expressionName | fieldAccess | arrayAccess
        case Java8Parser.RULE_assignment:
            if (null != updateStatementExpressionCtx.assignment().leftHandSide().arrayAccess())
                return "cant convert a loop variable that is an array element";
            TerminalNode node = updateStatementExpressionCtx.assignment().assignmentOperator()
                    .getChild(TerminalNode.class, 0);
            switch (node.getSymbol().getType()) {
            case Java8Parser.ADD_ASSIGN:
                ascending_sequence = true;
                break;
            case Java8Parser.SUB_ASSIGN:
                ascending_sequence = false;
                break;
            case Java8Parser.ASSIGN: // possibilities too complex to warrant extracting simple a=a+1 cases
            default:
                return "potentially too complex to create a sequence from this update operation";
            }
            stepExpressionCtx = updateStatementExpressionCtx.assignment().expression();
            break;
        default: // methodInvocation | classInstanceCreationExpression
            return "the expression in the update statement is too complex";
        }
        // In each of the cases that we have not rejected, the loop variable is in the first child rule context of the
        // update statement. Get the text of the variable, rather than analysing the graph any further, as the
        // possibilities are endless; all that we require is that the loop variable text matches that in the text
        // expression and the init expression.
        ParserRuleContext loopVariable_updated_Ctx = updateExpressionCtx.getChild(ParserRuleContext.class, 0);
        String loopVariableTxt = loopVariable_updated_Ctx.getText(); // we want original text

        // 2) Expression
        if (null == expressionCtx)
            return "it lacks a test expression";
        // expression : lambdaExpression | assignmentExpression
        if (null != expressionCtx.lambdaExpression())
            return "cannot convert a lambda expression";
        // assignmentExpression : conditionalExpression | assignment
        if (null != expressionCtx.assignmentExpression().assignment())
            return "cannot convert an assignment within the test expression";
        // 2a) must be a simple relation:
        // Descend the chain of expression rule pass-through branches until we find the one that is significant, then
        // test to see if expression contains a terminal that is one of !=, <, <=, >, >=.
        ParserRuleContext testExpressionCtx = J2SGrammarUtils.descendToSignificantExpression(expressionCtx);
        int testExpressionRule = testExpressionCtx.getRuleIndex();
        TerminalNode node = testExpressionCtx.getChild(TerminalNode.class, 0);
        int testOperatorType = null != node ? node.getSymbol().getType() : 0;
        switch (testOperatorType) {
        case Java8Parser.NOTEQUAL:
            open_interval = true;
            break;
        case Java8Parser.LE:
            open_interval = false;
            break;
        case Java8Parser.GE:
            open_interval = false;
            break;

        case Java8Parser.LT: // can occur in relational and shift expressions
        case Java8Parser.GT: // can occur in relational and shift expressions
            if (testExpressionRule == Java8Parser.RULE_relationalExpression) {
                open_interval = true;
                break;
            }
        default:
            return "can only convert test expressions that use !=, <, <=, > or >=";
        }
        // 2b) relation must be testing same var as changed in update expression
        // The loop variable could be on the left or the right of the comparison operator
        int i;
        ParserRuleContext loopVariable_tested_Ctx = null;
        for (i = 0; i < 2; i++) {
            loopVariable_tested_Ctx = testExpressionCtx.getChild(ParserRuleContext.class, i);
            if (null != loopVariable_tested_Ctx && loopVariableTxt.equals(loopVariable_tested_Ctx.getText()))
                break; // found matching loop variable
            loopVariable_tested_Ctx = null;
        }
        if (null == loopVariable_tested_Ctx || (i == 1 && testExpressionCtx.getChildCount() > 3))
            return "the test expression must be testing the same variable as changed in update expression";
        ParserRuleContext terminalValueCtx = testExpressionCtx.getChild(ParserRuleContext.class, i ^ 1);
        // 2c) the terminal value side should not mutate within the loop
        // - way too difficult for us to determine this

        // 3) Loop init expression. Must be either...
        ParserRuleContext initialValueCtx;
        if (null == forInitCtx) // a) empty
        {
            // Using the loop variable's existing value from outside the scope
            initialValueCtx = loopVariable_tested_Ctx;
        } else if (null != forInitCtx.statementExpressionList()) // b) a simple init of a single loop var
        {
            /*
                    // Could not convert...
                    // for (i = 0; i<10; i++)
                    // ...to a for..in statement because can only work with an assignment expression for loop variable initialisation.
                    i = 0; while i<10  {j += 1 i += 1; }
            */
            if (forInitCtx.statementExpressionList().getChildCount() != 1)
                return "can only work with initialisation of a single loop variable";
            Java8Parser.StatementExpressionContext initExpressionCtx = forInitCtx.statementExpressionList()
                    .statementExpression(0);
            if (null == initExpressionCtx.assignment())
                return "can only work with an assignment expression for loop variable initialisation";
            if (!loopVariableTxt.equals(initExpressionCtx.assignment().leftHandSide().getText()))
                return "the initialised variable is different from the updated variable"; // different to the loop variable
            initialValueCtx = initExpressionCtx.assignment().expression();
        } else if (null != forInitCtx.localVariableDeclaration()) // c) a simple decl of a single loop var
        {
            // localVariableDeclaration : variableModifier* unannType variableDeclaratorList
            Java8Parser.VariableDeclaratorListContext vdlc = forInitCtx.localVariableDeclaration()
                    .variableDeclaratorList();
            // variableDeclaratorList : variableDeclarator (',' variableDeclarator)*
            if (vdlc.getChildCount() != 1)
                return "can only work with declaration of a single loop variable";
            Java8Parser.VariableDeclaratorContext vdc = vdlc.variableDeclarator(0);
            // variableDeclarator : variableDeclaratorId ('=' variableInitializer)?
            if (!loopVariableTxt.equals(vdc.getChild(0).getText()))
                return "the declared loop variable is be different from the updated variable";
            initialValueCtx = vdc.variableInitializer();
            if (null == initialValueCtx)
                return "there is no initialiser for the loop variable";
        } else
            return "loop initialisation is in unexpected form";

        // Now we have all the components we need
        String forInLoopText;
        // Use actual text with replacements
        String initialValueTxt = rewriter.getText(initialValueCtx);
        String terminalValueTxt = rewriter.getText(terminalValueCtx);
        // !!!: watch out...
        // if we use the actual text from the update expression, we can find that the pre/post-inc/dec has been
        // converted to the add/sub-assign form and because structure is lost when rewriting, the new form can
        // stick to the variable when we retrieve it. There's no easy solution for this (and any similar occurrences),
        // but we side step it by getting the text of loop variable from the test expression:
        loopVariableTxt = rewriter.getText(loopVariable_tested_Ctx);
        if (null != stepExpressionCtx || !ascending_sequence) {
            String stepExpressionText = stepExpressionCtx == null ? "-1"
                    : ascending_sequence ? rewriter.getText(stepExpressionCtx)
                            : "-(" + rewriter.getText(stepExpressionCtx) + ")";
            forInLoopText = "for " + loopVariableTxt + " in " + loopVariableTxt + ".stride(from: " + initialValueTxt
                    + (open_interval ? ", to: " : ", through: ") + terminalValueTxt + ", by: " + stepExpressionText
                    + ")";
        } else {
            forInLoopText = "for " + loopVariableTxt + " in " + initialValueTxt
                    + (open_interval ? " ..< " : " ... ") + terminalValueTxt;
        }

        Token startToken = ctx.getToken(Java8Parser.FOR, 0).getSymbol();
        Token endToken = ctx.getToken(Java8Parser.RPAREN, 0).getSymbol();

        CharStream cs = startToken.getInputStream();
        String originalExpressionTxt = cs.getText(Interval.of(startToken.getStartIndex(), endToken.getStopIndex()));
        rewriter.insertComment(originalExpressionTxt + " converted to", ctx,
                J2SRewriter.CommentWhere.beforeLineBreak);

        int startIndex = startToken.getTokenIndex();
        int endIndex = endToken.getTokenIndex();

        // Problem: (see notes in J2SRewriter.replaceAndAdjustWhitespace) Before converting to for-in, the loop will
        // also have had parentheses removed (and other transforms); rewriter may have coallesced some of the changes
        // so that the old end boundary no longer exists. (- a shortcoming of TokenStreamRewriter)
        // Workaround: test if endIndex is straddled by changed interval, and if so, extend our interval to the end of
        // the change. (Pretty horrendous to have to work around this here, but I don't yet see an easy way of fixing
        // the underlying problem or a generalised way of working around it.)
        Interval interval = rewriter.getChangedIntervalContaining(endIndex, endIndex);
        if (null != interval && interval.a <= endIndex && interval.b > endIndex)
            endIndex = interval.b;

        rewriter.replaceAndAdjustWhitespace(startIndex, endIndex, forInLoopText);

        return null;
    }
}