diff --git a/src/main/java/spoon/generating/replace/ReplacementVisitor.java b/src/main/java/spoon/generating/replace/ReplacementVisitor.java index c5615276fa7..41282c4aa77 100644 --- a/src/main/java/spoon/generating/replace/ReplacementVisitor.java +++ b/src/main/java/spoon/generating/replace/ReplacementVisitor.java @@ -126,13 +126,7 @@ private void replaceInListIfExist(List listProtected, R private void replaceElementIfExist(CtElement candidate, ReplaceListener listener) { if (candidate == original) { - if (replace.size() > 1) { - throw new SpoonException("Cannot replace single value by multiple values in " + listener.getClass().getSimpleName()); - } - CtElement val = null; - if (replace.size() == 1) { - val = replace.iterator().next(); - } + CtElement val = candidate.getFactory().Code().convertToSingle(replace); listener.set(val); if (val != null) { val.setParent(candidate.getParent()); diff --git a/src/main/java/spoon/reflect/factory/CodeFactory.java b/src/main/java/spoon/reflect/factory/CodeFactory.java index 48f5a836962..3d548ab9826 100644 --- a/src/main/java/spoon/reflect/factory/CodeFactory.java +++ b/src/main/java/spoon/reflect/factory/CodeFactory.java @@ -16,6 +16,7 @@ */ package spoon.reflect.factory; +import spoon.SpoonException; import spoon.reflect.code.BinaryOperatorKind; import spoon.reflect.code.CtAssignment; import spoon.reflect.code.CtBinaryOperator; @@ -43,6 +44,7 @@ import spoon.reflect.code.CtVariableAccess; import spoon.reflect.declaration.CtAnnotation; import spoon.reflect.declaration.CtClass; +import spoon.reflect.declaration.CtElement; import spoon.reflect.declaration.CtField; import spoon.reflect.declaration.CtNamedElement; import spoon.reflect.declaration.CtVariable; @@ -60,6 +62,7 @@ import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.HashSet; @@ -714,4 +717,44 @@ public CtJavaDocTag createJavaDocTag(String content, CtJavaDocTag.TagType type) return docTag.setContent(content.trim()).setType(type); } + /** + * Converts collection of elements to single element. + * It actually supports conversion of {@link CtStatement}s to {@link CtBlock} + * + * @param elements a to be converted collection of elements + * @return single element which represents input `elements` + * @throws SpoonException if conversion is not possible + */ + @SuppressWarnings("unchecked") + public T convertToSingle(Collection elements) { + if (elements.isEmpty()) { + return null; + } + if (elements.size() == 1) { + return (T) elements.iterator().next(); + } + if (isAllSubtypeOf(elements, CtStatement.class)) { + CtBlock block = factory.Core().createBlock(); + block.setStatements(toList((Collection) elements)); + return (T) block; + } + throw new SpoonException("Cannot convert collection to single"); + } + + private List toList(Collection col) { + if (col instanceof List) { + return (List) col; + } + return new ArrayList<>(col); + } + + private boolean isAllSubtypeOf(Collection elements, Class clazz) { + for (Object ele : elements) { + if (clazz.isInstance(ele) == false) { + return false; + } + } + return true; + } + } diff --git a/src/main/java/spoon/support/template/SubstitutionVisitor.java b/src/main/java/spoon/support/template/SubstitutionVisitor.java index ae34653b26d..50789e05db1 100644 --- a/src/main/java/spoon/support/template/SubstitutionVisitor.java +++ b/src/main/java/spoon/support/template/SubstitutionVisitor.java @@ -18,6 +18,8 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -31,10 +33,10 @@ import spoon.reflect.code.CtFieldWrite; import spoon.reflect.code.CtForEach; import spoon.reflect.code.CtInvocation; +import spoon.reflect.code.CtLiteral; import spoon.reflect.code.CtReturn; import spoon.reflect.code.CtStatement; import spoon.reflect.code.CtThisAccess; -import spoon.reflect.code.CtVariableAccess; import spoon.reflect.declaration.CtElement; import spoon.reflect.declaration.CtExecutable; import spoon.reflect.declaration.CtNamedElement; @@ -47,8 +49,6 @@ import spoon.reflect.reference.CtTypeReference; import spoon.reflect.visitor.CtInheritanceScanner; import spoon.reflect.visitor.CtScanner; -import spoon.reflect.visitor.Query; -import spoon.reflect.visitor.filter.VariableAccessFilter; import spoon.template.Parameter; import spoon.template.Template; import spoon.template.TemplateParameter; @@ -86,12 +86,11 @@ class DoNotFurtherTemplateThisElement extends SpoonException { */ public class SubstitutionVisitor extends CtScanner { - public class InheritanceSustitutionScanner extends CtInheritanceScanner { + private Context context; - SubstitutionVisitor parent = null; + public class InheritanceSustitutionScanner extends CtInheritanceScanner { - public InheritanceSustitutionScanner(SubstitutionVisitor parent) { - this.parent = parent; + public InheritanceSustitutionScanner() { } /** @@ -105,7 +104,7 @@ public void scanCtExecutable(CtExecutable e) { boolean wasChanged = false; for (CtParameter parameter : e.getParameters()) { @SuppressWarnings({ "rawtypes", "unchecked" }) - List> list = (List) getParameterValueAsList(CtParameter.class, getParameterValue(parameter.getSimpleName())); + List> list = (List) getParameterValueAsList(CtParameter.class, context.getParameterValue(parameter.getSimpleName())); if (list == null) { //it is normal parameter, keep it. substitutedParams.add(parameter); @@ -128,28 +127,27 @@ public void scanCtExecutable(CtExecutable e) { @Override public void scanCtNamedElement(CtNamedElement element) { if (element.getDocComment() != null) { - element.setDocComment(substituteName(element.getDocComment())); + element.setDocComment(context.substituteName(element.getDocComment())); } // replace parameters in names - element.setSimpleName(substituteName(element.getSimpleName())); + element.setSimpleName(context.substituteName(element.getSimpleName())); super.scanCtNamedElement(element); } @Override public void scanCtReference(CtReference reference) { - reference.setSimpleName(substituteName(reference.getSimpleName())); - super.scanCtReference(reference); - } - - private String substituteName(String name) { - for (Map.Entry e : namesToValues.entrySet()) { - String pname = e.getKey(); - if (name.contains(pname)) { - String value = getParameterValueAsString(e.getValue()); - name = name.replace(pname, value); + Object value = context.getParameterValue(reference.getSimpleName()); + if (value != null) { + if (value instanceof CtLiteral) { + //replace reference with literal + CtLiteral literal = (CtLiteral) value; + CtExpression expr = reference.getParent(CtExpression.class); + context.replace(expr, literal); + throw new DoNotFurtherTemplateThisElement(expr); } } - return name; + reference.setSimpleName(context.substituteName(reference.getSimpleName())); + super.scanCtReference(reference); } /** statically inline foreach */ @@ -158,22 +156,26 @@ private String substituteName(String name) { public void visitCtForEach(CtForEach foreach) { if (foreach.getExpression() instanceof CtFieldAccess) { CtFieldAccess fa = (CtFieldAccess) foreach.getExpression(); - Object value = getParameterValue(fa.getVariable().getSimpleName()); + Object value = context.getParameterValue(fa.getVariable().getSimpleName()); if (value != null) { + //create local context which holds local substitution parameter + Context localContext = createContext(); List list = getParameterValueAsList(CtExpression.class, value); - CtBlock l = foreach.getFactory().Core().createBlock(); CtStatement body = foreach.getBody(); + String newParamName = foreach.getVariable().getSimpleName(); + List newStatements = new ArrayList<>(); for (CtExpression element : list) { - CtStatement b = body.clone(); - for (CtVariableAccess va : Query.getElements(b, new VariableAccessFilter<>(foreach.getVariable().getReference()))) { - va.replace(element); - } - if (b instanceof CtBlock && ((CtBlock) b).getStatements().size() == 1) { - b = ((CtBlock) b).getStatement(0); + localContext.putParameter(newParamName, element); + if (body instanceof CtBlock) { + CtBlock foreachBlock = (CtBlock) body; + for (CtStatement st : foreachBlock.getStatements()) { + newStatements.addAll(localContext.substitute(st.clone())); + } + } else { + newStatements.addAll(localContext.substitute(body.clone())); } - l.addStatement(b); } - replace(foreach, l); + context.replace(foreach, newStatements); throw new DoNotFurtherTemplateThisElement(foreach); } } @@ -196,16 +198,16 @@ private void visitFieldAccess(final CtFieldAccess fieldAccess) { if ("length".equals(ref.getSimpleName())) { if (fieldAccess.getTarget() instanceof CtFieldAccess) { ref = ((CtFieldAccess) fieldAccess.getTarget()).getVariable(); - Object value = getParameterValue(ref.getSimpleName()); + Object value = context.getParameterValue(ref.getSimpleName()); if (value != null) { //the items of this list are not cloned List list = getParameterValueAsList(Object.class, value); - replace(fieldAccess, (CtExpression) fieldAccess.getFactory().Code().createLiteral(list.size())); + context.replace(fieldAccess, (CtExpression) fieldAccess.getFactory().Code().createLiteral(list.size())); throw new DoNotFurtherTemplateThisElement(fieldAccess); } } } - Object v = getParameterValue(ref.getSimpleName()); + Object v = context.getParameterValue(ref.getSimpleName()); if (v != null) { // replace direct field parameter accesses Object value = getParameterValueAtIndex(Object.class, v, Parameters.getIndex(fieldAccess)); @@ -215,19 +217,19 @@ private void visitFieldAccess(final CtFieldAccess fieldAccess) { } if (!(value instanceof TemplateParameter)) { if (value instanceof Class) { - replace(toReplace, factory.Code() + context.replace(toReplace, factory.Code() .createClassAccess(factory.Type().createReference(((Class) value).getName()))); } else if (value instanceof Enum) { CtTypeReference enumType = factory.Type().createReference(value.getClass()); - replace(toReplace, factory.Code().createVariableRead( + context.replace(toReplace, factory.Code().createVariableRead( factory.Field().createReference(enumType, enumType, ((Enum) value).name()), true)); } else if ((value != null) && value.getClass().isArray()) { - replace(toReplace, factory.Code().createLiteralArray((Object[]) value)); + context.replace(toReplace, factory.Code().createLiteralArray((Object[]) value)); } else { - replace(toReplace, factory.Code().createLiteral(value)); + context.replace(toReplace, factory.Code().createLiteral(value)); } } else { - replace(toReplace, toReplace.clone()); + context.replace(toReplace, toReplace.clone()); } // do not visit if replaced throw new DoNotFurtherTemplateThisElement(fieldAccess); @@ -252,21 +254,20 @@ public void visitCtInvocation(CtInvocation invocation) { } if ((fa != null) && (fa.getTarget() == null || fa.getTarget() instanceof CtThisAccess)) { CtCodeElement r = getParameterValueAtIndex(CtCodeElement.class, - getParameterValue(fa.getVariable().getSimpleName()), Parameters.getIndex(fa)); + context.getParameterValue(fa.getVariable().getSimpleName()), Parameters.getIndex(fa)); + List subst = null; if (r != null) { - // substitute in the replacement (for fixing type - // references - // and - // for recursive substitution) - r.accept(parent); + subst = createContext().substitute(r); } - if (invocation.isParentInitialized() && (invocation.getParent() instanceof CtReturn) && (r instanceof CtBlock)) { - // block template parameters in returns should + if (invocation.isParentInitialized() && (invocation.getParent() instanceof CtReturn) + && (subst.size() > 1 || (subst.size() == 1 && subst.get(0) instanceof CtBlock)) + ) { + // multiple code blocks or block template parameters in returns should // replace // the return ((CtReturn) invocation.getParent()).replace((CtStatement) r); } else { - replace(invocation, r); + context.replace(invocation, subst); } } // do not visit the invocation if replaced @@ -282,7 +283,7 @@ public void visitCtInvocation(CtInvocation invocation) { */ @Override public void visitCtTypeReference(CtTypeReference reference) { - Object o = getParameterValue(reference.getSimpleName()); + Object o = context.getParameterValue(reference.getSimpleName()); if (o != null) { // replace type parameters // TODO: this would probably not work with inner classes!!! @@ -306,14 +307,6 @@ public void visitCtTypeReference(CtTypeReference reference) { CtExecutableReference S; - Map namesToValues; - - /** - * represents root element, which is target of the substitution. - * It can be substituted too. - */ - CtElement result; - /** * Creates new substitution visitor based on instance of Template, * which defines template model and template parameters @@ -341,11 +334,11 @@ public SubstitutionVisitor(Factory f, CtType targetType, Template template * the parameter names and values which will be used during substitution */ public SubstitutionVisitor(Factory f, Map templateParameters) { - this.inheritanceScanner = new InheritanceSustitutionScanner(this); + this.inheritanceScanner = new InheritanceSustitutionScanner(); this.factory = f; S = factory.Executable().createReference(factory.Type().createReference(TemplateParameter.class), factory.Type().createTypeParameterReference("T"), "S"); - this.namesToValues = templateParameters; + this.context = new Context(null).putParameters(templateParameters); } /** @@ -366,6 +359,10 @@ public void scan(CtElement element) { // and then scan the children for doing the templating as well in them super.scan(element); } catch (DoNotFurtherTemplateThisElement ignore) { + if (element != ignore.skipped) { + //we have to skip more + throw ignore; + } } } @@ -375,23 +372,10 @@ public void scan(CtElement element) { * @param element to be substituted model * @return substituted model */ - public E substitute(E element) { - result = element; - scan(element); - return (E) result; - } - - private void replace(CtElement toBeReplaced, CtElement replacement) { - if (result == toBeReplaced) { - result = replacement; - } else { - toBeReplaced.replace(replacement); - } + public List substitute(E element) { + return createContext().substitute(element); } - private Object getParameterValue(String parameterName) { - return namesToValues.get(parameterName); - } /** * 1) Converts `parameterValue` to List using these rules *
    @@ -521,4 +505,118 @@ private static T getParameterValueAtIndex(Class itemClass, Object paramet //convert and clone the returned item return getParameterValueAsClass(itemClass, parameterValue); } + + protected Context createContext() { + //by default each new context has same input like parent and modifies same collection like parent context + return new Context(this.context); + } + + protected class Context { + final Context parentContext; + /** + * represents root element, which is target of the substitution. + * It can be substituted too. + */ + CtElement input; + /** + * represents replacement of the `input`. + * it is null if input was not replaced + */ + List result; + Map parameterNameToValue; + + public Context(Context parent) { + this.parentContext = parent; + } + + public Context putParameter(String name, Object value) { + if (parameterNameToValue == null) { + parameterNameToValue = new LinkedHashMap<>(); + } + parameterNameToValue.put(name, value); + return this; + } + + public Context putParameters(Map parameters) { + if (parameters != null && parameters.isEmpty() == false) { + if (parameterNameToValue == null) { + parameterNameToValue = new LinkedHashMap<>(); + } + parameterNameToValue.putAll(parameters); + } + return this; + } + + public Object getParameterValue(String parameterName) { + if (parameterNameToValue != null) { + Object value = parameterNameToValue.get(parameterName); + if (value != null) { + return value; + } + } + if (parentContext != null) { + return parentContext.getParameterValue(parameterName); + } + return null; + } + + public void replace(E toBeReplaced, E replacement) { + if (replacement == null) { + replace(toBeReplaced, Collections.emptyList()); + } else { + replace(toBeReplaced, Collections.singletonList(replacement)); + } + } + @SuppressWarnings("unchecked") + public void replace(E toBeReplaced, List replacements) { + if (input == toBeReplaced) { + if (result != null) { + throw new SpoonException("Illegal state. SubstitutionVisitor.Context#result was already replaced!"); + } + result = (List) replacements; + } else { + toBeReplaced.replace(replacements); + } + } + + @SuppressWarnings("unchecked") + public List substitute(E element) { + if (input != null) { + throw new SpoonException("Illegal state. SubstitutionVisitor.Context#input is already set."); + } + input = element; + result = null; + if (context != parentContext) { + throw new SpoonException("Illegal state. Context != parentContext"); + } + try { + context = this; + scan(element); + if (result != null) { + return (List) result; + } + return Collections.singletonList((E) input); + } finally { + context = this.parentContext; + input = null; + } + } + + public String substituteName(String name) { + if (parameterNameToValue != null) { + for (Map.Entry e : parameterNameToValue.entrySet()) { + String pname = e.getKey(); + if (name.contains(pname)) { + String value = getParameterValueAsString(e.getValue()); + name = name.replace(pname, value); + } + } + } + if (parentContext != null) { + name = parentContext.substituteName(name); + } + return name; + } + + } } diff --git a/src/main/java/spoon/template/StatementTemplate.java b/src/main/java/spoon/template/StatementTemplate.java index 906565d1136..ab4f2d39ba0 100644 --- a/src/main/java/spoon/template/StatementTemplate.java +++ b/src/main/java/spoon/template/StatementTemplate.java @@ -16,6 +16,9 @@ */ package spoon.template; +import java.util.List; + +import spoon.reflect.code.CtBlock; import spoon.reflect.code.CtStatement; import spoon.reflect.declaration.CtClass; import spoon.reflect.declaration.CtType; @@ -43,7 +46,15 @@ public CtStatement apply(CtType targetType) { CtClass c = Substitution.getTemplateCtClass(targetType, this); // we substitute the first statement of method statement CtStatement result = c.getMethod("statement").getBody().getStatements().get(0).clone(); - return new SubstitutionVisitor(c.getFactory(), targetType, this).substitute(result); + List statements = new SubstitutionVisitor(c.getFactory(), targetType, this).substitute(result); + if (statements.size() == 1) { + return statements.get(0); + } + CtBlock block = targetType.getFactory().Core().createBlock(); + if (statements.size() > 0) { + block.setStatements(statements); + } + return block; } public Void S() { diff --git a/src/main/java/spoon/template/Substitution.java b/src/main/java/spoon/template/Substitution.java index c07fca37314..c162d5e3627 100644 --- a/src/main/java/spoon/template/Substitution.java +++ b/src/main/java/spoon/template/Substitution.java @@ -112,9 +112,11 @@ public static CtType insertType(CtPackage targetPackage, String typeName, final Factory f = templateOfType.getFactory(); final Map extendedParams = new HashMap(templateParameters); extendedParams.put(templateOfType.getSimpleName(), f.Type().createReference(targetPackage.getQualifiedName() + "." + typeName)); - CtType generated = new SubstitutionVisitor(f, extendedParams).substitute(templateOfType.clone()); - targetPackage.addType(generated); - return generated; + List> generated = new SubstitutionVisitor(f, extendedParams).substitute(templateOfType.clone()); + for (CtType ctType : generated) { + targetPackage.addType(ctType); + } + return targetPackage.getType(typeName); } /** @@ -532,7 +534,8 @@ public static E substitute(CtType targetType, Template< throw new RuntimeException("target is null in substitution"); } E result = (E) code.clone(); - return new SubstitutionVisitor(targetType.getFactory(), targetType, template).substitute(result); + List results = new SubstitutionVisitor(targetType.getFactory(), targetType, template).substitute(result); + return targetType.getFactory().Code().convertToSingle(results); } /** @@ -579,7 +582,8 @@ public static > T substitute(Template template, T templat T result = (T) templateType.clone(); result.setPositions(null); // result.setParent(templateType.getParent()); - return new SubstitutionVisitor(templateType.getFactory(), result, template).substitute(result); + List results = new SubstitutionVisitor(templateType.getFactory(), result, template).substitute(result); + return templateType.getFactory().Code().convertToSingle(results); } /** diff --git a/src/test/java/spoon/test/template/TemplateTest.java b/src/test/java/spoon/test/template/TemplateTest.java index 0e4fb25bf11..c176396455d 100644 --- a/src/test/java/spoon/test/template/TemplateTest.java +++ b/src/test/java/spoon/test/template/TemplateTest.java @@ -147,9 +147,8 @@ public void testTemplateInheritance() throws Exception { assertEquals("toBeOverriden()", methodWithTemplatedParameters.getBody().getStatement(3).toString()); // contract: foreach are inlined - CtBlock templatedForEach = methodWithTemplatedParameters.getBody().getStatement(4); - assertEquals("java.lang.System.out.println(0)", templatedForEach.getStatement(0).toString()); - assertEquals("java.lang.System.out.println(1)", templatedForEach.getStatement(1).toString()); + assertEquals("java.lang.System.out.println(0)", methodWithTemplatedParameters.getBody().getStatement(4).toString()); + assertEquals("java.lang.System.out.println(1)", methodWithTemplatedParameters.getBody().getStatement(5).toString()); } @Test @@ -399,7 +398,9 @@ public void testExtensionDecoupledSubstitutionVisitor() throws Exception { params.put("_classname_", aTargetType.getSimpleName()) ; params.put("_methodName_", toBeLoggedMethod.getSimpleName()); params.put("_block_", toBeLoggedMethod.getBody()); - final CtMethod aMethod = new SubstitutionVisitor(factory, params).substitute(aTemplateModel.clone()); + final List> aMethods = new SubstitutionVisitor(factory, params).substitute(aTemplateModel.clone()); + assertEquals(1, aMethods.size()); + final CtMethod aMethod = aMethods.get(0); assertTrue(aMethod.getBody().getStatement(0) instanceof CtTry); final CtTry aTry = (CtTry) aMethod.getBody().getStatement(0); assertTrue(aTry.getFinalizer().getStatement(0) instanceof CtInvocation);