blob: 6e2f00a4c819d24122869260a2499d6a0f18de18 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2010 xored software, Inc.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* xored software, Inc. - initial API and Implementation (Vladislav Kuzkokov)
*******************************************************************************/
package org.eclipse.dltk.javascript.core.dom.rewrite;
import static org.eclipse.jface.text.TextUtilities.determineLineDelimiter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.dltk.compiler.util.Util;
import org.eclipse.dltk.javascript.ast.MultiLineComment;
import org.eclipse.dltk.javascript.core.dom.BinaryExpression;
import org.eclipse.dltk.javascript.core.dom.BinaryOperator;
import org.eclipse.dltk.javascript.core.dom.CatchClause;
import org.eclipse.dltk.javascript.core.dom.DomPackage;
import org.eclipse.dltk.javascript.core.dom.FunctionExpression;
import org.eclipse.dltk.javascript.core.dom.Node;
import org.eclipse.dltk.javascript.core.dom.Source;
import org.eclipse.dltk.javascript.core.dom.Statement;
import org.eclipse.dltk.javascript.core.dom.TryStatement;
import org.eclipse.dltk.javascript.core.dom.UnaryExpression;
import org.eclipse.dltk.javascript.core.dom.UnaryOperator;
import org.eclipse.dltk.javascript.core.dom.VariableDeclaration;
import org.eclipse.dltk.javascript.core.dom.util.DomSwitch;
import org.eclipse.emf.common.util.BasicEList;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.EcorePackage;
import org.eclipse.emf.ecore.change.ChangeDescription;
import org.eclipse.emf.ecore.change.ChangeKind;
import org.eclipse.emf.ecore.change.FeatureChange;
import org.eclipse.emf.ecore.change.ListChange;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.text.edits.DeleteEdit;
import org.eclipse.text.edits.InsertEdit;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEdit;
public class RewriteAnalyzer extends DomSwitch<Boolean> {
private final ChangeDescription cd;
private final Set<Node> generated = new HashSet<Node>();
private final String text;
protected final String lineDelimiter;
private final TextEdit edit;
private MultiLineComment[] comments;
public RewriteAnalyzer(ChangeDescription cd, String text) {
this(cd, text, determineLineDelimiter(text, Util.LINE_SEPARATOR));
}
public RewriteAnalyzer(ChangeDescription cd, String text,
String lineDelimiter) {
this.cd = cd;
this.text = text;
this.lineDelimiter = lineDelimiter;
this.edit = new MultiTextEdit();
}
public void rewrite(Source source) {
final CommentContainer container = (CommentContainer) EcoreUtil
.getExistingAdapter(source, CommentContainer.class);
this.comments = container != null ? container.comments : null;
rewrite((Node) source);
}
void rewrite(Node node) {
doSwitch(node);
for (EObject obj : node.eContents())
if (!generated.contains(obj))
rewrite((Node) obj);
}
public TextEdit getEdit() {
return edit;
}
protected void addEdit(TextEdit edit, Node node) {
this.edit.addChild(edit);
}
// Processes EReference only
private void processFeature(Node node, FeatureChange fc) {
if (fc.getFeature() instanceof EAttribute) {
final EAttribute attribute = (EAttribute) fc.getFeature();
if (!attribute.isMany()
&& attribute.getEAttributeType() == EcorePackage.Literals.ESTRING
&& EcoreUtil.getAnnotation(attribute, null, "value") != null) {
addEdit(new ReplaceEdit(node.getBegin(), node.getEnd()
- node.getBegin(), (String) node.eGet(attribute)), node);
}
return;
}
if (!fc.getFeature().isMany()) {
Node n = (Node) node.eGet(fc.getFeature());
Node o = (Node) fc.getReferenceValue();
int off = o == null ? calcOffset(node, fc.getFeature()) : o
.getBegin();
int len = o == null ? 0 : o.getEnd() - o.getBegin();
String value = n == null ? "" : generate(n, node, o == null, off);
addEdit(new ReplaceEdit(off, len, value), n);
return;
}
@SuppressWarnings("unchecked")
EList<? extends Node> dst = (EList<? extends Node>) node.eGet(fc
.getFeature());
EList<Object> src = new BasicEList<Object>();
src.addAll(dst);
Set<Node> deleted = new HashSet<Node>();
Set<Node> generated = new HashSet<Node>();
for (ListChange lc : fc.getListChanges()) {
if (lc.getKind() != ChangeKind.ADD_LITERAL)
generated.add((Node) src.get(lc.getIndex()));
if (lc.getKind() == ChangeKind.MOVE_LITERAL)
deleted.add((Node) src.get(lc.getIndex()));
lc.apply(src);
if (lc.getKind() == ChangeKind.ADD_LITERAL)
deleted.add((Node) src.get(lc.getIndex()));
}
if (fc.getListChanges().isEmpty()) {
src.clear();
generated.addAll(dst);
}
List<Node> original = new ArrayList<Node>(src.size());
for (Object obj : src)
original.add((Node) obj);
Node last = null;
for (Node item : original)
if (!deleted.contains(item))
last = item;
// DELETING ELEMENTS
// 1) general case: a,(b,)c,(d,)(e,)f
// 2) at the end: a,(b,)c,d(,e)(,f)
// 3) all of it: (a)(,b)(,c)
// we delete element with following separator so that this will work
// with statements and line feeds/semicolons.
boolean isLast = last == null;
for (int i = 0; i < original.size(); i++) {
Node item = original.get(i);
if (deleted.contains(item)) {
int off = isLast && i != 0 ? original.get(i - 1).getEnd()
: getBegin(item);
int end = isLast ? item.getEnd()
: getBegin(original.get(i + 1));
addEdit(new DeleteEdit(off, end - off), item);
} else
isLast = item == last;
}
// OFFSETS
// each element is added at the beginning of the next element
// or at the end
List<Integer> offs = new ArrayList<Integer>();
int cur = original.isEmpty() ? calcOffset(node, fc.getFeature())
: original.get(original.size() - 1).getEnd();
for (int i = dst.size() - 1; i >= 0; i--) {
Node item = dst.get(i);
if (!generated.contains(item)) {
cur = item.getBegin();
}
offs.add(cur);
}
Collections.reverse(offs);
// ADDING ELEMENTS
// adding is done by the same rules as deleting
// cases 1-3 are processed in generateElement
isLast = last == null;
for (int i = 0; i < dst.size(); i++) {
Node item = dst.get(i);
if (generated.contains(item)) {
int off = offs.get(i);
addEdit(new InsertEdit(off, generateElement(item, i == 0,
isLast, off)), item);
} else
isLast = item == last;
}
}
private int getBegin(Node node) {
int value = node.getBegin();
if (comments != null) {
final MultiLineComment comment = findComment(value);
if (comment != null) {
for (int i = comment.end(); i < value; ++i) {
if (!Character.isWhitespace(text.charAt(i))) {
return value;
}
}
return comment.start();
}
}
return value;
}
private MultiLineComment findComment(int value) {
int low = 0;
int high = comments.length;
while (low < high) {
final int mid = (low + high) >>> 1;
final MultiLineComment comment = comments[mid];
final int end = comment.end();
if (end > value) {
high = mid;
} else if (end < value) {
low = mid + 1;
} else {
return comment;
}
}
return low > 0 ? comments[low - 1] : null;
}
@Override
public Boolean caseNode(Node node) {
if (cd.getObjectChanges().get(node) != null)
for (FeatureChange fc : cd.getObjectChanges().get(node))
processFeature(node, fc);
return true;
}
@Override
public Boolean caseUnaryExpression(UnaryExpression node) {
if (cd.getObjectChanges().get(node) != null)
for (FeatureChange fc : cd.getObjectChanges().get(node))
if (fc.getFeature() == DomPackage.Literals.UNARY_EXPRESSION__OPERATION) {
UnaryOperator n = node.getOperation();
UnaryOperator o = (UnaryOperator) fc.getValue();
int len = o.toString().length();
if (isPostfix(o))
addEdit(new DeleteEdit(node.getEnd() - len, len), node);
else
addEdit(new DeleteEdit(node.getBegin(), len), node);
if (isPostfix(n))
addEdit(new InsertEdit(node.getEnd(), n.toString()),
node);
else {
String r = n.toString();
if (isTextUnary(n))
r += ' ';
addEdit(new InsertEdit(node.getBegin(), r), node);
}
} else
processFeature(node, fc);
return true;
}
static boolean isPostfix(Object op) {
return op == UnaryOperator.POSTFIX_INC
|| op == UnaryOperator.POSTFIX_DEC;
}
static boolean isTextUnary(Object op) {
return op == UnaryOperator.DELETE || op == UnaryOperator.VOID
|| op == UnaryOperator.TYPEOF || op == UnaryOperator.YIELD;
}
@Override
public Boolean caseBinaryExpression(BinaryExpression node) {
if (cd.getObjectChanges().get(node) != null)
for (FeatureChange fc : cd.getObjectChanges().get(node))
if (fc.getFeature() == DomPackage.Literals.BINARY_EXPRESSION__OPERATION) {
String r = node.getOperation().toString();
if (isTextBinary(fc.getValue()))
r = ' ' + r + ' ';
addEdit(new ReplaceEdit(node.getOperatorPosition(), fc
.getValue().toString().length(), r), node);
} else
processFeature(node, fc);
return true;
}
static boolean isTextBinary(Object op) {
return op == BinaryOperator.IN || op == BinaryOperator.INSTANCEOF;
}
// calculates offset for null references and empty lists
private int calcOffset(Node node, EStructuralFeature sf) {
EReference ref = (EReference) sf;
switch (ref.getEContainingClass().getClassifierID()) {
case DomPackage.SOURCE:
return node.getBegin();
case DomPackage.BLOCK_STATEMENT:
return node.getEnd() - 1; // skip right brace
case DomPackage.VARIABLE_STATEMENT:
return node.getBegin() + 3; // skip "var"
case DomPackage.CONST_STATEMENT:
return node.getBegin() + 5; // skip "const"
case DomPackage.VARIABLE_DECLARATION:
case DomPackage.IF_STATEMENT:
case DomPackage.CONTINUE_STATEMENT:
case DomPackage.BREAK_STATEMENT:
case DomPackage.RETURN_STATEMENT:
case DomPackage.SWITCH_ELEMENT:
case DomPackage.PARAMETER:
return node.getEnd();
case DomPackage.CALL_EXPRESSION:
return node.getEnd() - 1;
case DomPackage.SWITCH_STATEMENT:
throw new IllegalStateException("Empty switch statement");
case DomPackage.CATCH_CLAUSE:
CatchClause cc = (CatchClause) node;
return cc.getException().getEnd();
case DomPackage.TRY_STATEMENT:
TryStatement ts = (TryStatement) node;
if (ts.getFinallyClause() != null)
return ts.getFinallyClause().getBegin();
return node.getEnd();
case DomPackage.FUNCTION_EXPRESSION:
FunctionExpression expr = (FunctionExpression) node;
if (ref == DomPackage.Literals.FUNCTION_EXPRESSION__IDENTIFIER)
return expr.getParametersPosition() - 1;
else
return expr.getParametersPosition();
}
return -1;
}
static abstract class NodeSeparator {
public abstract void appendTo(Generator gen);
}
static final NodeSeparator NEWLINE = new NodeSeparator() {
@Override
public void appendTo(Generator gen) {
gen.newLine();
}
};
static final NodeSeparator COMMA = new NodeSeparator() {
@Override
public void appendTo(Generator gen) {
gen.append(",");
}
};
static final NodeSeparator COMMA_NEWLINE = new NodeSeparator() {
@Override
public void appendTo(Generator gen) {
gen.append(",");
gen.newLine();
}
};
public String generateElement(Node node, boolean first, boolean last,
int pos) {
Generator gen = new Generator(cd, text, pos, lineDelimiter);
final NodeSeparator separator;
if (node instanceof Statement) {
separator = NEWLINE;
} else if (node instanceof VariableDeclaration
&& ((VariableDeclaration) node).getInitializer() != null) {
separator = COMMA_NEWLINE;
} else {
separator = COMMA;
}
if (!first && last)
separator.appendTo(gen);
gen.generate(node);
if (!last)
separator.appendTo(gen);
generated.add(node);
return gen.toString();
}
public String generate(Node node, Node parent, boolean wasNull, int pos) {
Generator gen = new Generator(cd, text, pos, lineDelimiter);
if (wasNull
&& parent.eClass() == DomPackage.Literals.VARIABLE_DECLARATION)
gen.append("=");
if (wasNull
&& parent.eClass() == DomPackage.Literals.FUNCTION_EXPRESSION)
gen.append(" ");
if (wasNull
&& node.eContainmentFeature() == DomPackage.Literals.CATCH_CLAUSE__FILTER)
gen.append(" if ");
gen.generate(node);
generated.add(node);
return gen.toString();
}
}