package dk.brics.jsrefactoring; import java.util.LinkedList; import java.util.List; import dk.brics.jsparser.AstUtil; import dk.brics.jsparser.TokenPair; import dk.brics.jsparser.analysis.DepthFirstAdapter; import dk.brics.jsparser.node.ABlock; import dk.brics.jsparser.node.ABlockStmt; import dk.brics.jsparser.node.ABody; import dk.brics.jsparser.node.AFunctionExp; import dk.brics.jsparser.node.ANormalObjectLiteralProperty; import dk.brics.jsparser.node.AObjectLiteralExp; import dk.brics.jsparser.node.AReturnStmt; import dk.brics.jsparser.node.AVarDecl; import dk.brics.jsparser.node.AVarDeclStmt; import dk.brics.jsparser.node.AVarForInLvalue; import dk.brics.jsparser.node.AVarForInit; import dk.brics.jsparser.node.IFunction; import dk.brics.jsparser.node.Node; import dk.brics.jsparser.node.PAssignOp; import dk.brics.jsparser.node.PBinop; import dk.brics.jsparser.node.PExp; import dk.brics.jsparser.node.PObjectLiteralProperty; import dk.brics.jsparser.node.PStmt; import dk.brics.jsparser.node.Start; import dk.brics.jsparser.node.TComma; import dk.brics.jsparser.node.TEndl; import dk.brics.jsparser.node.TWhitespace; import dk.brics.jsparser.node.Token; import dk.brics.jsparser.node.TokenEnum; /** * Utility class that inserts whitespace and indentation for newly created AST nodes. * * @author max.schaefer@comlab.ox.ac.uk */ public class PrettyPrinter { public static int DEFAULT_INDENTATION = 4; public static void pp(Node nd) { // try to guess a starting indentation int indent = 0; if(nd.parent() instanceof ABlock) { ABlock block = (ABlock)nd.parent(); int idx = block.getStatements().indexOf(nd); if(idx > 0) { indent = getIndent(block.getStatements().get(idx-1)); } else if(idx == 0 && block.getStatements().size() > 1) { indent = getIndent(block.getStatements().get(idx+1)); } } pp(nd, indent, DEFAULT_INDENTATION); } // insert default whitespace and newline into the given subtree // does not take account of existing whitespace public static void pp(Node nd, int startIndent, final int increment) { final int[] indent = new int[1]; indent[0] = startIndent; nd.apply(new DepthFirstAdapter() { @Override public void inABlockStmt(ABlockStmt stmt) { indent[0] += increment; AstUtil.insertTokenAfter(stmt.getLbrace(), new TEndl("\n")); super.inABlockStmt(stmt); } @Override public void outABlockStmt(ABlockStmt stmt) { indent[0] -= increment; AstUtil.insertTokenBefore(new TWhitespace(repeat(indent[0], ' ')), stmt.getRbrace()); AstUtil.insertTokenAfter(stmt.getRbrace(), new TEndl("\n")); super.outABlockStmt(stmt); } @Override public void inAObjectLiteralExp(AObjectLiteralExp lit) { indent[0] += increment; AstUtil.insertTokenAfter(lit.getLbrace(), new TEndl("\n")); super.inAObjectLiteralExp(lit); } @Override public void outAObjectLiteralExp(AObjectLiteralExp lit) { indent[0] -= increment; AstUtil.insertTokenBefore(new TWhitespace(repeat(indent[0], ' ')), lit.getRbrace()); super.outAObjectLiteralExp(lit); } @Override public void defaultOutPAssignOp(PAssignOp op) { // insert spaces around assignment operator TokenPair tks = AstUtil.getFirstAndLastToken(op); AstUtil.insertTokenBefore(new TWhitespace(" "), tks.first); AstUtil.insertTokenAfter(tks.last, new TWhitespace(" ")); } @Override public void defaultOutPBinop(PBinop op) { // insert spaces around binary operator TokenPair tks = AstUtil.getFirstAndLastToken(op); AstUtil.insertTokenBefore(new TWhitespace(" "), tks.first); AstUtil.insertTokenAfter(tks.last, new TWhitespace(" ")); } @Override public void inABody(ABody node) { indent[0] += increment; super.inABody(node); } @Override public void outABody(ABody node) { indent[0] -= increment; super.outABody(node); } @Override public void outAFunctionExp(AFunctionExp node) { // insert space after keyword "function" if(node.getName() != null) AstUtil.insertTokenAfter(node.getFunction(), new TWhitespace(" ")); // insert spaces before parameters (except the first one) for(int i=1;i<node.getParameters().size();++i) AstUtil.insertTokenBefore(new TWhitespace(" "), node.getParameters().get(i)); // insert space after closing parenthesis of argument list AstUtil.insertTokenAfter(node.getRparen(), new TWhitespace(" ")); // insert newline after opening brace AstUtil.insertTokenAfter(node.getLbrace(), new TEndl("\n")); // indent closing brace AstUtil.insertTokenBefore(new TWhitespace(repeat(indent[0], ' ')), node.getRbrace()); super.outAFunctionExp(node); } @Override public void outAReturnStmt(AReturnStmt node) { // insert space after keyword "return" AstUtil.insertTokenAfter(node.getReturn(), new TWhitespace(" ")); super.outAReturnStmt(node); } @Override public void outAVarDecl(AVarDecl node) { // insert space before name AstUtil.insertTokenBefore(new TWhitespace(" "), node.getName()); // insert space around "=", if exists if(node.getEq() != null) { AstUtil.insertTokenBefore(new TWhitespace(" "), node.getEq()); AstUtil.insertTokenAfter(node.getEq(), new TWhitespace(" ")); } super.outAVarDecl(node); } @Override public void outANormalObjectLiteralProperty(ANormalObjectLiteralProperty node) { AstUtil.insertTokenAfter(node.getColon(), new TWhitespace(" ")); super.outANormalObjectLiteralProperty(node); } @Override public void defaultInPStmt(PStmt node) { indent(node, indent[0]); } @Override public void defaultOutPStmt(PStmt node) { TokenPair tks = AstUtil.getFirstAndLastToken(node); AstUtil.insertTokenAfter(tks.last, new TEndl("\n")); } @Override public void defaultInPObjectLiteralProperty(PObjectLiteralProperty prop) { indent(prop, indent[0]); } @Override public void defaultOutPObjectLiteralProperty(PObjectLiteralProperty prop) { Token last = AstUtil.getFirstAndLastToken(prop).last; if(last.getNext() instanceof TComma) last = last.getNext(); AstUtil.insertTokenAfter(last, new TEndl("\n")); } // TODO: many missing }); } public static int getIndent(Node nd) { return getIndent(AstUtil.getFirstAndLastToken(nd).first); } public static int getIndent(Token tk) { int indent = 0; // skip back over ignorable tokens while(ignored(tk.getPrevious())) tk = tk.getPrevious(); // now sum up lengths of all ignorable tokens while(ignored(tk)) { indent += tk.getText().length(); tk = tk.getNext(); } return indent; } /** * Indents every line in the source text of <code>nd</code> by the given depth. */ public static void indent(Node nd, int depth) { TokenPair tokens = AstUtil.getFirstAndLastToken(nd); Token tk = tokens.first, last = tokens.last; outer: while(tk != null) { // no need to indent empty lines if(tk.kindToken() != TokenEnum.ENDL) AstUtil.insertTokenBefore(new TWhitespace(repeat(depth, ' ')), tk); while(tk != null && tk.kindToken() != TokenEnum.ENDL) { if(tk == last) break outer; tk = tk.getNext(); } if(tk != null) tk = tk.getNext(); } } private static String repeat(int times, char c) { StringBuffer buf = new StringBuffer(); for(int i=0;i<times;++i) buf.append(c); return buf.toString(); } // replaces the body of a function expression or declaration while maintaining the token chain public static void setBody(IFunction fun, ABody body) { fun.setBody(body); TokenPair tokens = AstUtil.getFirstAndLastToken(body); // find token that should come before the body Token before = getFollowingRealToken(fun.getLbrace()); if(before.kindToken() != TokenEnum.ENDL) before = before.getPrevious(); // find token that should come after the body Token after = getPrecedingRealToken(fun.getRbrace()); after = after.getNext(); if(tokens.first == null) { // empty body connect(before, after); } else { connect(before, getInitialTokenOfStmt(tokens)); connect(getFinalTokenOfStmt(tokens), after); } } // insert statement into block while maintaining token chain public static void insertStmtIntoBlock(ABlock block, int index, PStmt stmt) { LinkedList<PStmt> stmts = block.getStatements(); TokenPair tokens = AstUtil.getFirstAndLastToken(stmt); Token ini = getInitialTokenOfStmt(tokens), fin = getFinalTokenOfStmt(tokens); if(fin.kindToken() != TokenEnum.ENDL) { AstUtil.insertTokenAfter(fin, new TEndl("\n")); fin = fin.getNext(); } if(stmts.size() == index) { Token last = AstUtil.getFirstAndLastToken(block).last; Token anchor = last == null ? AstUtil.getFirstAndLastToken(block.parent()).last : getFollowingRealToken(last); if(anchor.kindToken()==TokenEnum.ENDL) anchor = anchor.getNext(); connect(anchor.getPrevious(), ini); connect(fin, anchor); if(index > 0) { int stmtIndent = getIndent(stmt), prevIndent = getIndent(stmts.get(index-1)); if(stmtIndent < prevIndent) indent(stmt, prevIndent - stmtIndent); } } else { PStmt next = stmts.get(index); Token anchor = getPrecedingRealToken(AstUtil.getFirstAndLastToken(next).first); connect(fin, anchor.getNext()); connect(anchor, ini); int stmtIndent = getIndent(stmt), nextIndent = getIndent(next); if(stmtIndent < nextIndent) indent(stmt, nextIndent - stmtIndent); } stmts.add(index, stmt); AstUtil.setRoot(stmt, block.getRoot()); } // insert a statement as a top-level statement into a script public static void insertStmtIntoScript(Start script, int index, PStmt stmt) { ABody body = script.getBody(); List<PStmt> stmts = body.getBlock().getStatements(); stmts.add(index, stmt); TokenPair tokens = AstUtil.getFirstAndLastToken(stmt); Token stmtIni = getInitialTokenOfStmt(tokens), stmtFin = getFinalTokenOfStmt(tokens); if(index == 0) { Token anchor = script.getEOF(); while(anchor.getPrevious() != null) anchor = anchor.getPrevious(); PrettyPrinter.connect(null, stmtIni); PrettyPrinter.connect(stmtFin, anchor); if(index > 0) { int stmtIndent = getIndent(stmt), prevIndent = getIndent(stmts.get(index-1)); if(stmtIndent < prevIndent) indent(stmt, prevIndent - stmtIndent); } } else { PStmt prev = stmts.get(index-1); Token prevStmtLast = getFinalTokenOfStmt(AstUtil.getFirstAndLastToken(prev)); Token prevStmtLastNext = prevStmtLast.getNext(); PrettyPrinter.connect(prevStmtLast, stmtIni); PrettyPrinter.connect(stmtFin, prevStmtLastNext); int stmtIndent = getIndent(stmt), prevIndent = getIndent(prev); if(stmtIndent < prevIndent) indent(stmt, prevIndent - stmtIndent); } } // remove a statement from a block while maintaining token chain // returns the first and last token removed public static TokenPair removeStmtFromBlock(ABlock block, int index) { LinkedList<PStmt> stmts = block.getStatements(); PStmt stmt = stmts.get(index); TokenPair tokens = AstUtil.getFirstAndLastToken(stmt); Token first = getInitialTokenOfStmt(tokens); Token last = getFinalTokenOfStmt(tokens); connect(first.getPrevious(), last.getNext()); stmts.remove(index); return new TokenPair(first, last); } // return the farthest token preceding tokens.first that is preceded by a non-ignorable // token (i.e., either null or something other than a comment or whitespace) private static Token getInitialTokenOfStmt(TokenPair tokens) { Token initial = tokens.first; while(ignored(initial.getPrevious())) initial = initial.getPrevious(); return initial; } public static Token getInitialToken(PStmt stmt) { return getInitialTokenOfStmt(AstUtil.getFirstAndLastToken(stmt)); } // return either the following end-of-line token or analogous to getInitialTokenOfStmt private static Token getFinalTokenOfStmt(TokenPair tokens) { Token fin = tokens.last; while(ignored(fin.getNext())) fin = fin.getNext(); if(fin.getNext() != null && fin.getNext().kindToken() == TokenEnum.ENDL) fin = fin.getNext(); return fin; } public static Token getFinalToken(PStmt stmt) { return getFinalTokenOfStmt(AstUtil.getFirstAndLastToken(stmt)); } // removes a variable declaration from its parent node while maintaining token chain // the parent has to be either a variable declaration statement or a for-loop init public static void removeVarDecl(AVarDecl decl) { Node parent = decl.parent(); boolean isLast = false; if(parent instanceof AVarDeclStmt) { AVarDeclStmt stmt = (AVarDeclStmt)parent; if(stmt.getDecls().size() == 1) { Node grandparent = stmt.parent(); if(grandparent instanceof ABlock) { int idx = ((ABlock)grandparent).getStatements().indexOf(parent); removeStmtFromBlock((ABlock)grandparent, idx); } else { AstUtil.replaceNode(stmt, NodeFactory.createEmptyStmt()); } return; } int idx = stmt.getDecls().indexOf(decl); isLast = idx == stmt.getDecls().size() - 1; stmt.getDecls().remove(idx); } else if(parent instanceof AVarForInit) { AVarForInit init = (AVarForInit)parent; if(init.getVarDecl().size() == 1) { AstUtil.replaceNode(init, NodeFactory.createEmptyForInit()); return; } int idx = init.getVarDecl().indexOf(decl); isLast = idx == init.getVarDecl().size() - 1; init.getVarDecl().remove(idx); } else if(parent instanceof AVarForInLvalue) { throw new IllegalArgumentException("Cannot remove variable declaration from for-in loop."); } TokenPair tokens = AstUtil.getFirstAndLastToken(decl); if(isLast) { Token preceding = getPrecedingRealToken(tokens.first.getPrevious()); if(preceding.kindToken() == TokenEnum.COMMA) preceding = preceding.getPrevious(); connect(preceding, tokens.last.getNext()); } else { Token following = getFollowingRealToken(tokens.last.getNext()); if(following.kindToken() == TokenEnum.COMMA) following = following.getNext(); connect(tokens.first.getPrevious(), following); } } // insert expression into argument list while maintaining token chain public static void insertExpIntoArglist(Token start, LinkedList<PExp> args, Token end, int index, PExp exp) { TokenPair tokens = AstUtil.getFirstAndLastToken(exp); TComma comma = new TComma(); TWhitespace ws = new TWhitespace(" "); connect(comma, ws); if(index == 0) { if(args.isEmpty()) { connect(start, tokens.first); connect(tokens.last, end); } else { connect(ws, start.getNext()); connect(tokens.last, comma); connect(start, tokens.first); } } else if(index == args.size()) { connect(end.getPrevious(), comma); connect(ws, tokens.first); connect(tokens.last, end); } else { PExp prev = args.get(index-1); start = findFollowingToken(AstUtil.getFirstAndLastToken(prev).last, TComma.class); connect(ws, start.getNext()); connect(tokens.last, comma); connect(start, tokens.first); } args.add(index, exp); AstUtil.setRoot(exp, start.getRoot()); } public static Token getPrecedingRealToken(Token tk) { tk = tk.getPrevious(); while(ignored(tk)) tk = tk.getPrevious(); return tk; } public static Token getFollowingRealToken(Token tk) { tk = tk.getNext(); while(ignored(tk)) tk = tk.getNext(); return tk; } private static boolean ignored(Token tk) { if(tk == null) return false; TokenEnum kind = tk.kindToken(); return kind == TokenEnum.WHITESPACE || kind == TokenEnum.SINGLELINECOMMENT || kind == TokenEnum.MULTILINECOMMENT; } @SuppressWarnings("unchecked") public static <T> T findFollowingToken(Token tk, Class<T> type) { while(tk != null && !type.isInstance(tk)) tk = tk.getNext(); return tk == null ? null : (T)tk; } public static void connect(Token tk1, Token tk2) { if(tk1 != null) tk1.setNext(tk2); if(tk2 != null) tk2.setPrevious(tk1); } }