/**
* Copyright (C) 2013-2014 Olaf Lessenich
* Copyright (C) 2014-2015 University of Passau, Germany
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*
* Contributors:
* Olaf Lessenich <lessenic@fim.uni-passau.de>
* Georg Seibt <seibt@fim.uni-passau.de>
*/
package de.fosd.jdime.artifact.ast;
import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import de.fosd.jdime.artifact.Artifact;
import de.fosd.jdime.artifact.ArtifactList;
import de.fosd.jdime.artifact.Artifacts;
import de.fosd.jdime.artifact.file.FileArtifact;
import de.fosd.jdime.config.merge.MergeContext;
import de.fosd.jdime.config.merge.MergeScenario;
import de.fosd.jdime.config.merge.Revision;
import de.fosd.jdime.merge.Merge;
import de.fosd.jdime.operations.ConflictOperation;
import de.fosd.jdime.operations.MergeOperation;
import de.fosd.jdime.operations.Operation;
import de.fosd.jdime.stats.KeyEnums;
import de.fosd.jdime.stats.MergeScenarioStatistics;
import org.jastadd.extendj.ast.ASTNode;
import org.jastadd.extendj.ast.BytecodeParser;
import org.jastadd.extendj.ast.BytecodeReader;
import org.jastadd.extendj.ast.ClassDecl;
import org.jastadd.extendj.ast.ConstructorDecl;
import org.jastadd.extendj.ast.ImportDecl;
import org.jastadd.extendj.ast.InterfaceDecl;
import org.jastadd.extendj.ast.JavaParser;
import org.jastadd.extendj.ast.Literal;
import org.jastadd.extendj.ast.MethodDecl;
import org.jastadd.extendj.ast.Program;
import org.jastadd.extendj.ast.TryStmt;
import static de.fosd.jdime.strdump.DumpMode.PLAINTEXT_TREE;
/**
* @author Olaf Lessenich
*
*/
public class ASTNodeArtifact extends Artifact<ASTNodeArtifact> {
private static final Logger LOG = Logger.getLogger(ASTNodeArtifact.class.getCanonicalName());
private boolean initialized = false;
/**
* Initializes parser.
*
* @param p
* program
*/
private static void initParser(Program p) {
JavaParser parser = (is, fileName) -> new org.jastadd.extendj.parser.JavaParser().parse(is, fileName);
BytecodeReader bytecodeParser = (is, fullName, program) -> new BytecodeParser(is, fullName).parse(null, null, program);
p.initJavaParser(parser);
p.initBytecodeReader(bytecodeParser);
}
/**
* Parses the content of the given <code>FileArtifact</code> to an AST. If the <code>artifact</code> is empty,
* an empty <code>ASTNode</code> obtained via {@link ASTNode#ASTNode()} will be returned.
*
* @param artifact
* the <code>FileArtifact</code> to parse
* @return the root of the resulting AST
*/
private static ASTNode<?> parse(FileArtifact artifact) {
ASTNode<?> astNode;
if (artifact.isEmpty()) {
astNode = new ASTNode<>();
} else {
Program p = initProgram();
try {
p.addSourceFile(artifact.getPath());
} catch (IOException e) {
throw new RuntimeException(e);
}
astNode = p;
}
return astNode;
}
/**
* Initializes a program.
*
* @return program
*/
private static Program initProgram() {
Program program = new Program();
program.state().reset();
initParser(program);
return program;
}
/**
* @param artifact
* artifact to create program from
* @return ASTNodeArtifact
*/
public static ASTNodeArtifact createProgram(ASTNodeArtifact artifact) {
assert (artifact.astnode != null);
assert (artifact.astnode instanceof Program);
Program old = (Program) artifact.astnode;
Program program;
try {
program = old.clone();
} catch (CloneNotSupportedException e) {
program = new Program();
}
ASTNodeArtifact p = new ASTNodeArtifact(artifact.getRevision(), program);
p.deleteChildren();
return p;
}
/**
* Encapsulated ASTNode.
*/
private ASTNode<?> astnode = null;
/**
* Constructs a new <code>ASTNodeArtifact</code> (tree) representing the AST of the code in <code>artifact</code>.
* All members of the tree will be in the same <code>Revision</code> as <code>artifact</code>.
*
* @param artifact
* the <code>FileArtifact</code> containing the code to be parsed
*/
public ASTNodeArtifact(FileArtifact artifact) {
this(artifact.getRevision(), new AtomicInteger()::getAndIncrement, parse(artifact));
}
/**
* Constructs a new <code>ASTNodeArtifact</code> encapsulating an empty <code>ASTNode</code> obtained via
* {@link ASTNode#ASTNode()}.
*
* @param revision
* the <code>Revision</code> for this <code>ASTNodeArtifact</code>
*/
private ASTNodeArtifact(Revision revision) {
this(revision, new AtomicInteger()::getAndIncrement, new ASTNode<>());
}
/**
* Constructs a new <code>ASTNodeArtifact</code> encapsulating the given <code>ASTNode</code>. Children
* <code>ASTNodeArtifact</code>s for all the children of <code>astNode</code> will be added.
*
* @param revision
* the <code>Revision</code> for this <code>ASTNodeArtifact</code>
* @param astNode
* the <code>ASTNode</code> to encapsulate
*/
private ASTNodeArtifact(Revision revision, ASTNode<?> astNode) {
this(revision, new AtomicInteger()::getAndIncrement, astNode);
}
/**
* Constructs a new <code>ASTNodeArtifact</code> encapsulating the given <code>ASTNode</code>. Children
* <code>ASTNodeArtifact</code>s for all the children of <code>astNode</code> will be added.
*
* @param revision
* the <code>Revision</code> for this <code>ASTNodeArtifact</code>
* @param number
* supplies first the number for this artifact and then in DFS order the number for its children
* @param astNode
* the <code>ASTNode</code> to encapsulate
*/
private ASTNodeArtifact(Revision revision, Supplier<Integer> number, ASTNode<?> astNode) {
super(revision, number.get());
this.astnode = astNode;
initializeChildren(number);
}
private void initializeChildren(Supplier<Integer> number) {
ArtifactList<ASTNodeArtifact> children = new ArtifactList<>();
for (int i = 0; i < astnode.getNumChild(); i++) {
if (astnode != null) {
ASTNodeArtifact child = new ASTNodeArtifact(getRevision(), number, astnode.getChild(i));
child.setParent(this);
children.add(child);
if (!child.initialized) {
child.initializeChildren(number);
}
}
}
setChildren(children);
initialized = true;
}
/**
* Returns the encapsulated JastAddJ ASTNode
*
* @return encapsulated ASTNode object from JastAddJ
*/
public final ASTNode<?> getASTNode() {
return astnode;
}
@Override
public ASTNodeArtifact clone() {
assert (exists());
ASTNodeArtifact clone = null;
try {
clone = new ASTNodeArtifact(getRevision(), astnode.clone());
clone.setRevision(getRevision());
clone.setNumber(getNumber());
clone.cloneMatches(this);
ArtifactList<ASTNodeArtifact> cloneChildren = new ArtifactList<>();
for (ASTNodeArtifact child : children) {
ASTNodeArtifact cloneChild = child.clone();
cloneChild.astnode.setParent(clone.astnode);
cloneChild.setParent(clone);
cloneChildren.add(cloneChild);
}
clone.setChildren(cloneChildren);
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
assert (clone.exists());
return clone;
}
@Override
public ASTNodeArtifact addChild(ASTNodeArtifact child) {
LOG.finest(() -> String.format("%s.addChild(%s)", getId(), child.getId()));
assert (this.exists());
assert (child.exists());
child.setParent(this);
children.add(child);
return child;
}
@Override
public ASTNodeArtifact createEmptyArtifact(Revision revision) {
return new ASTNodeArtifact(revision);
}
public void deleteChildren() {
while (hasChildren()) {
ASTNodeArtifact child = getChild(0);
child.astnode = null;
children.remove(0);
}
}
@Override
public String prettyPrint() {
assert (astnode != null);
try {
rebuildAST();
astnode.flushCaches();
astnode.flushTreeCache();
} catch (Exception e) {
LOG.severe("Exception caught during prettyPrint(): " + e);
LOG.log(Level.SEVERE, e.getMessage(), e);
}
if (LOG.isLoggable(Level.FINEST)) {
System.out.println(Artifacts.root(this).dump(PLAINTEXT_TREE));
}
String indent = isRoot() ? "" : astnode.extractIndent();
String prettyprint = indent + astnode.prettyPrint();
if (prettyprint.trim().length() == 0) {
throw new RuntimeException("Error: Could not pretty-print file!");
}
return prettyprint;
}
@Override
public final boolean exists() {
return astnode != null;
}
@Override
public final String getId() {
return getRevision() + ":" + getNumber();
}
@Override
public KeyEnums.Type getType() {
if (isMethod()) {
return KeyEnums.Type.METHOD;
} else if (isClass()) {
return KeyEnums.Type.CLASS;
} else if (astnode instanceof TryStmt){
return KeyEnums.Type.TRY;
} else {
return KeyEnums.Type.NODE;
}
}
@Override
public KeyEnums.Level getLevel() {
KeyEnums.Type type = getType();
if (type == KeyEnums.Type.METHOD) {
return KeyEnums.Level.METHOD;
} else if (type == KeyEnums.Type.CLASS) {
return KeyEnums.Level.CLASS;
} else {
if (getParent() == null) {
return KeyEnums.Level.TOP;
} else {
return getParent().getLevel();
}
}
}
@Override
public void mergeOpStatistics(MergeScenarioStatistics mScenarioStatistics, MergeContext mergeContext) {
mScenarioStatistics.getTypeStatistics(getRevision(), getType()).incrementNumMerged();
mScenarioStatistics.getLevelStatistics(getRevision(), getLevel()).incrementNumMerged();
}
/**
* Returns whether this <code>ASTNodeArtifact</code> represents a method declaration.
*
* @return true iff this is a method declaration
*/
private boolean isMethod() {
return astnode instanceof MethodDecl || astnode instanceof ConstructorDecl;
}
/**
* Returns whether the <code>ASTNodeArtifact</code> is within a method.
*
* @return true iff the <code>ASTNodeArtifact</code> is within a method
*/
public boolean isWithinMethod() {
ASTNodeArtifact parent = getParent();
return parent != null && (parent.isMethod() || parent.isWithinMethod());
}
/**
* Returns whether this <code>ASTNodeArtifact</code> represents a class or interface declaration.
*
* @return true iff this is a class or method declaration
*/
private boolean isClass() {
return astnode instanceof ClassDecl || astnode instanceof InterfaceDecl;
}
@Override
public Optional<Supplier<String>> getUniqueLabel() {
boolean hasLabel = ImportDecl.class.isAssignableFrom(astnode.getClass())
|| Literal.class.isAssignableFrom(astnode.getClass());
return hasLabel ? Optional.of(() -> astnode.dumpString()) : Optional.empty();
}
@Override
public final boolean isEmpty() {
return !hasChildren();
}
@Override
public final boolean isLeaf() {
// TODO Auto-generated method stub
return false;
}
/**
* Returns whether declaration order is significant for this node.
*
* @return whether declaration order is significant for this node
*/
@Override
public final boolean isOrdered() {
return astnode.isOrdered();
}
/**
* Returns whether a node matches another node.
*
* @param other
* node to compare with.
* @return true if the node matches another node.
*/
@Override
public final boolean matches(final ASTNodeArtifact other) {
assert (astnode != null);
assert (other != null);
assert (other.astnode != null);
LOG.finest(() -> "match(" + getId() + ", " + other.getId() + ")");
LOG.finest(() -> {
return String.format("Try Matching: {%s} and {%s}",
astnode.getMatchingRepresentation(),
other.astnode.getMatchingRepresentation());
});
return astnode.matches(other.astnode);
}
@Override
public void merge(MergeOperation<ASTNodeArtifact> operation, MergeContext context) {
Objects.requireNonNull(operation, "operation must not be null!");
Objects.requireNonNull(context, "context must not be null!");
MergeScenario<ASTNodeArtifact> triple = operation.getMergeScenario();
ASTNodeArtifact left = triple.getLeft();
ASTNodeArtifact right = triple.getRight();
ASTNodeArtifact target = operation.getTarget();
boolean safeMerge = true;
int numChildNoTransform;
try {
numChildNoTransform = target.astnode.getClass().newInstance().getNumChildNoTransform();
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
if (!isRoot() && numChildNoTransform > 0) {
// this language element has a fixed number of children, we need to be careful with this one
// as it might cause lots of issues while being pretty-printed
boolean leftChanges = left.isChange();
boolean rightChanges = right.isChange();
for (int i = 0; !leftChanges && i < left.getNumChildren(); i++) {
leftChanges = left.getChild(i).isChange();
}
for (int i = 0; !rightChanges && i < right.getNumChildren(); i++) {
rightChanges = right.getChild(i).isChange();
}
if (leftChanges && rightChanges) {
// this one might be trouble
if (left.getNumChildren() == right.getNumChildren()) {
// so far so good
for (int i = 0; i < left.getNumChildren(); i++) {
if (!left.getChild(i).astnode.getClass().getName().equals(right.getChild(i).astnode.getClass().getName())) {
// no good, this might get us some ClassCastExceptions
safeMerge = false;
}
}
} else {
// no way ;)
safeMerge = false;
}
}
}
if (safeMerge) {
Merge<ASTNodeArtifact> merge = new Merge<>();
LOG.finest(() -> "Merging ASTs " + operation.getMergeScenario());
merge.merge(operation, context);
} else {
LOG.finest(() -> String.format("Target %s expects a fixed amount of children.", target.getId()));
LOG.finest(() -> String.format("Both %s and %s contain changes.", left.getId(), right.getId()));
LOG.finest(() -> "We are scared of this node and report a conflict instead of performing the merge.");
// to be safe, we will report a conflict instead of merging
ASTNodeArtifact targetParent = target.getParent();
targetParent.removeChild(target);
Operation<ASTNodeArtifact> conflictOp = new ConflictOperation<>(left, right, targetParent,
left.getRevision().getName(), right.getRevision().getName());
conflictOp.apply(context);
}
if (!context.isQuiet() && context.hasOutput()) {
System.out.print(context.getStdIn());
}
}
/**
* Removes a child.
*
* @param child
* child that should be removed
*/
private void removeChild(final ASTNodeArtifact child) {
LOG.finest(() -> String.format("[%s] Removing child %s", getId(), child.getId()));
LOG.finest(() -> String.format("Children before removal: %s", getChildren()));
Iterator<ASTNodeArtifact> it = children.iterator();
ASTNodeArtifact elem;
while (it.hasNext()) {
elem = it.next();
if (elem == child) {
it.remove();
}
}
LOG.finest(() -> String.format("Children after removal: %s", getChildren()));
}
/**
* Rebuild the encapsulated ASTNode tree top down. This should be only
* called at the root node
*/
private void rebuildAST() {
LOG.finest(() -> String.format("%s.rebuildAST()", getId()));
int oldNumChildren = astnode.getNumChildNoTransform();
if (isConflict()) {
astnode.isConflict = true;
astnode.jdimeId = getId();
if (left != null) {
left.rebuildAST();
astnode.left = left.astnode;
} else {
/* FIXME: this is actually a bug.
* JDime should use an empty ASTNode with the correct revision information.
*/
}
if (right != null) {
right.rebuildAST();
astnode.right = right.astnode;
} else {
/* FIXME: this is actually a bug.
* JDime should use an empty ASTNode with the correct revision information.
*/
}
}
if (isChoice()) {
astnode.isChoice = true;
astnode.jdimeId = getId();
astnode.variants = new LinkedHashMap<String, ASTNode<?>>();
for (String condition : variants.keySet()) {
ASTNodeArtifact variant = variants.get(condition);
variant.rebuildAST();
astnode.variants.put(condition, variant.astnode);
}
}
ASTNode<?>[] newChildren = new ASTNode<?>[getNumChildren()];
for (int i = 0; i < getNumChildren(); i++) {
ASTNodeArtifact child = getChild(i);
newChildren[i] = child.astnode;
newChildren[i].setParent(astnode);
child.rebuildAST();
}
astnode.jdimeChanges = hasChanges();
astnode.jdimeId = getId();
astnode.setChildren(newChildren);
if (LOG.isLoggable(Level.FINEST)) {
LOG.finest(() -> String.format("jdime: %d, astnode.before: %d, astnode.after: %d children", getNumChildren(), oldNumChildren,
astnode.getNumChildNoTransform()));
if (getNumChildren() != astnode.getNumChildNoTransform()) {
LOG.finest("mismatch between jdime and astnode for " + getId() + "(" + astnode.dumpString() + ")");
}
if (oldNumChildren != astnode.getNumChildNoTransform()) {
LOG.finest("Number of children has changed");
}
}
if (!isConflict() && getNumChildren() != astnode.getNumChildNoTransform()) {
StringBuilder elements = new StringBuilder();
for (Revision r : getMatches().keySet()) {
if (elements.length() > 0) {
elements.append(", ");
}
elements.append(getMatching(r).getMatchingArtifact(this).getId());
}
LOG.severe("Mismatch of getNumChildren() and astnode.getNumChildren()---" +
"This is either a bug in ExtendJ or in JDime! Inspect AST element " +
getId() + " (" + elements.toString() + ") to look into this issue.");
}
}
@Override
public final String toString() {
return astnode.dumpString();
}
@Override
public ASTNodeArtifact createConflictArtifact(ASTNodeArtifact left, ASTNodeArtifact right) {
ASTNodeArtifact conflict;
if (left != null) {
conflict = new ASTNodeArtifact(MergeScenario.CONFLICT, left.astnode.treeCopyNoTransform());
} else {
conflict = new ASTNodeArtifact(MergeScenario.CONFLICT, right.astnode.treeCopyNoTransform());
}
conflict.setConflict(left, right);
return conflict;
}
@Override
public ASTNodeArtifact createChoiceArtifact(String condition, ASTNodeArtifact artifact) {
LOG.fine("Creating choice node");
ASTNodeArtifact choice = new ASTNodeArtifact(MergeScenario.CHOICE, artifact.astnode.treeCopyNoTransform());
choice.setChoice(condition, artifact);
return choice;
}
}