From ac5adfea2ce87549c383d7822396adc2fae54e34 Mon Sep 17 00:00:00 2001
From: Pavel Vojtechovsky
Date: Mon, 23 Oct 2017 21:16:18 +0200
Subject: [PATCH] feature(test): SpoonMetaModel
---
.../java/spoon/metamodel/MMContainerType.java | 60 ++
src/test/java/spoon/metamodel/MMField.java | 241 +++++++
src/test/java/spoon/metamodel/MMType.java | 126 ++++
src/test/java/spoon/metamodel/MMTypeKind.java | 33 +
.../java/spoon/metamodel/SpoonMetaModel.java | 625 ++++++++++++++++++
5 files changed, 1085 insertions(+)
create mode 100644 src/test/java/spoon/metamodel/MMContainerType.java
create mode 100644 src/test/java/spoon/metamodel/MMField.java
create mode 100644 src/test/java/spoon/metamodel/MMType.java
create mode 100644 src/test/java/spoon/metamodel/MMTypeKind.java
create mode 100644 src/test/java/spoon/metamodel/SpoonMetaModel.java
diff --git a/src/test/java/spoon/metamodel/MMContainerType.java b/src/test/java/spoon/metamodel/MMContainerType.java
new file mode 100644
index 00000000000..8757367bdc9
--- /dev/null
+++ b/src/test/java/spoon/metamodel/MMContainerType.java
@@ -0,0 +1,60 @@
+/**
+ * Copyright (C) 2006-2017 INRIA and contributors
+ * Spoon - http://spoon.gforge.inria.fr/
+ *
+ * This software is governed by the CeCILL-C License under French law and
+ * abiding by the rules of distribution of free software. You can use, modify
+ * and/or redistribute the software under the terms of the CeCILL-C license as
+ * circulated by CEA, CNRS and INRIA at http://www.cecill.info.
+ *
+ * This program 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 CeCILL-C License for more details.
+ *
+ * The fact that you are presently reading this means that you have had
+ * knowledge of the CeCILL-C license and that you accept its terms.
+ */
+package spoon.metamodel;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Represents type of container used for field value.
+ */
+public enum MMContainerType {
+ /**
+ * it is a single value field
+ * Example: CtClassImpl.simpleName
+ */
+ SINGLE,
+ /**
+ * It is a list of values
+ * Example: CtClassImpl.typeMembers
+ */
+ LIST,
+ /**
+ * It is a set of values
+ * Example: CtPackageImpl.types
+ */
+ SET,
+ /**
+ * It is a map of values
+ * Example: CtAnnotationImpl.elementValues
+ */
+ MAP;
+
+ public static MMContainerType valueOf(Class> valueClass) {
+ if (List.class.isAssignableFrom(valueClass)) {
+ return LIST;
+ }
+ if (Map.class.isAssignableFrom(valueClass)) {
+ return MAP;
+ }
+ if (Set.class.isAssignableFrom(valueClass)) {
+ return SET;
+ }
+ return SINGLE;
+ }
+}
diff --git a/src/test/java/spoon/metamodel/MMField.java b/src/test/java/spoon/metamodel/MMField.java
new file mode 100644
index 00000000000..4c1e35b6f06
--- /dev/null
+++ b/src/test/java/spoon/metamodel/MMField.java
@@ -0,0 +1,241 @@
+/**
+ * Copyright (C) 2006-2017 INRIA and contributors
+ * Spoon - http://spoon.gforge.inria.fr/
+ *
+ * This software is governed by the CeCILL-C License under French law and
+ * abiding by the rules of distribution of free software. You can use, modify
+ * and/or redistribute the software under the terms of the CeCILL-C license as
+ * circulated by CEA, CNRS and INRIA at http://www.cecill.info.
+ *
+ * This program 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 CeCILL-C License for more details.
+ *
+ * The fact that you are presently reading this means that you have had
+ * knowledge of the CeCILL-C license and that you accept its terms.
+ */
+package spoon.metamodel;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.log4j.spi.RootLogger;
+
+import spoon.SpoonException;
+import spoon.reflect.declaration.CtMethod;
+import spoon.reflect.path.CtRole;
+import spoon.reflect.reference.CtTypeReference;
+import spoon.support.DerivedProperty;
+
+import static spoon.metamodel.SpoonMetaModel.addUniqueObject;
+import static spoon.metamodel.SpoonMetaModel.getOrCreate;
+
+/**
+ * Represents a field of Spoon model type.
+ * Each MMField belongs to one MMType
+ */
+public class MMField {
+ /**
+ * Name of the field
+ */
+ final String name;
+ /**
+ * {@link CtRole} of the field
+ */
+ final CtRole role;
+ /**
+ * The list of {@link MMType}s which contains this field
+ */
+ final MMType ownerType;
+ /**
+ * Type of value container [single, list, set, map]
+ */
+ MMContainerType valueContainerType;
+ /**
+ * The type of value of this field - can be Set, List, Map or any non collection type
+ */
+ private CtTypeReference> valueType;
+ /**
+ * The item type of value of this field - can be non collection type
+ */
+ private CtTypeReference> itemValueType;
+
+ Boolean derived;
+
+ public CtMethod> get;
+ public CtMethod> set;
+ public CtMethod> add;
+ public CtMethod> addFirst;
+ public CtMethod> addLast;
+ public CtMethod> addOn;
+ public CtMethod> remove;
+
+
+ /**
+ * methods of this field defined directly on ownerType.
+ * There is PropertyGetter or PropertySetter annotation with `role` of this {@link MMField}
+ */
+ final List> roleMethods = new ArrayList<>();
+ final Map>> roleMethodsBySignature = new HashMap<>();
+ /**
+ * List of fields with same `role`, from super type of `ownerType` {@link MMType}
+ */
+ final List superFields = new ArrayList<>();
+
+ /**
+ * own and inherited methods grouped by method signature and the methods
+ * in list ordered by own methods are first
+ */
+ final Map>> allRoleMethodsBySignature = new HashMap<>();
+
+ MMField(String name, CtRole role, MMType ownerType) {
+ super();
+ this.name = name;
+ this.role = role;
+ this.ownerType = ownerType;
+ }
+
+ void addMethod(CtMethod> method) {
+ roleMethods.add(method);
+ String signature = method.getSignature();
+ addUniqueObject(getOrCreate(roleMethodsBySignature, signature, () -> new ArrayList<>()), method);
+ addUniqueObject(getOrCreate(allRoleMethodsBySignature, signature,() -> new ArrayList<>()), method);
+ }
+
+ void addSuperField(MMField superMMField) {
+ if (addUniqueObject(superFields, superMMField)) {
+ for (Map.Entry>> e : superMMField.allRoleMethodsBySignature.entrySet()) {
+ getOrCreate(allRoleMethodsBySignature, e.getKey(), () -> new ArrayList<>()).addAll(e.getValue());
+ }
+ }
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public CtRole getRole() {
+ return role;
+ }
+
+ public MMType getOwnerType() {
+ return ownerType;
+ }
+
+ public MMContainerType getValueContainerType() {
+ return valueContainerType;
+ }
+
+ public CtTypeReference> getValueType() {
+ return valueType;
+ }
+
+ void setValueType(CtTypeReference> valueType) {
+ this.valueType = valueType;
+ this.valueContainerType = MMContainerType.valueOf(valueType.getActualClass());
+ if (valueContainerType != MMContainerType.SINGLE) {
+ if(valueContainerType == MMContainerType.MAP) {
+ itemValueType = valueType.getActualTypeArguments().get(1);
+ if (String.class.getName().equals(valueType.getActualTypeArguments().get(0).getQualifiedName()) == false) {
+ throw new SpoonException("Unexpected container of type: " + valueType.toString());
+ }
+ } else {
+ itemValueType = valueType.getActualTypeArguments().get(0);
+ }
+
+ } else {
+ itemValueType = valueType;
+ }
+ }
+
+ public CtTypeReference> getItemValueType() {
+ return itemValueType;
+ }
+ public void setItemValueType(CtTypeReference> itemValueType) {
+ this.itemValueType = itemValueType;
+ }
+
+ public boolean isDerived() {
+ if (derived == null) {
+ if(role == CtRole.FIELD) {
+ this.getClass();
+ }
+ CtTypeReference derivedProperty = get.getFactory().createCtTypeReference(DerivedProperty.class);
+ //if DerivedProperty is found on any getter of this type, then this field is derived
+ boolean isConreteMethod = false;
+ List> ownGetterMethods = roleMethodsBySignature.get(get.getSignature());
+ if (ownGetterMethods != null) {
+ for (CtMethod> ctMethod : ownGetterMethods) {
+ if(ctMethod.getAnnotation(derivedProperty) != null) {
+ derived = Boolean.TRUE;
+ return true;
+ }
+ isConreteMethod = isConreteMethod || ctMethod.getBody() != null;
+ }
+ if (isConreteMethod) {
+ //there exists a implementation of getter for this field in this type and there is no DerivedProperty here, so it is not derived!
+ derived = Boolean.FALSE;
+ return false;
+ }
+ }
+ //inherit derived property from super type
+ //if DerivedProperty annotation is not found on any get method, then it is not derived
+ derived = Boolean.FALSE;
+ //check all super fields. If any of them is derived then this field is derived too
+ for (MMField superField : superFields) {
+ if (superField.isDerived()) {
+ derived = Boolean.TRUE;
+ break;
+ }
+ }
+ }
+ return derived;
+ }
+
+ public List> getRoleMethods() {
+ return roleMethods;
+ }
+
+ public List getSuperFields() {
+ return superFields;
+ }
+
+ public Map>> getAllRoleMethodsBySignature() {
+ return allRoleMethodsBySignature;
+ }
+
+ @Override
+ public String toString() {
+ return ownerType.getName() + "#" + getName() + "<" + valueType + ">";
+ }
+
+ /**
+ * @return the super MMField which has same valueType and which is in root of the most implementations
+ */
+ public MMField getRootSuperField() {
+ List potentialRootSuperFields = new ArrayList<>();
+ if (roleMethods.size() > 0) {
+ potentialRootSuperFields.add(this);
+ }
+ superFields.forEach(superField -> {
+ addUniqueObject(potentialRootSuperFields, superField.getRootSuperField());
+ });
+ int idx = 0;
+ if (potentialRootSuperFields.size() > 1) {
+ CtTypeReference> expectedValueType = this.valueType.getTypeErasure();
+ for (int i = 1; i < potentialRootSuperFields.size(); i++) {
+ MMField superField = potentialRootSuperFields.get(i);
+ if (superField.valueType.getTypeErasure().equals(expectedValueType) == false) {
+ break;
+ }
+ idx = i;
+ }
+ }
+// if (role == CtRole.ASSIGNMENT && ownerType.getName().equals("CtLocalVariable")) {
+// this.getClass();
+// }
+ return potentialRootSuperFields.get(idx);
+ }
+}
diff --git a/src/test/java/spoon/metamodel/MMType.java b/src/test/java/spoon/metamodel/MMType.java
new file mode 100644
index 00000000000..aa415567002
--- /dev/null
+++ b/src/test/java/spoon/metamodel/MMType.java
@@ -0,0 +1,126 @@
+/**
+ * Copyright (C) 2006-2017 INRIA and contributors
+ * Spoon - http://spoon.gforge.inria.fr/
+ *
+ * This software is governed by the CeCILL-C License under French law and
+ * abiding by the rules of distribution of free software. You can use, modify
+ * and/or redistribute the software under the terms of the CeCILL-C license as
+ * circulated by CEA, CNRS and INRIA at http://www.cecill.info.
+ *
+ * This program 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 CeCILL-C License for more details.
+ *
+ * The fact that you are presently reading this means that you have had
+ * knowledge of the CeCILL-C license and that you accept its terms.
+ */
+package spoon.metamodel;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import spoon.SpoonException;
+import spoon.reflect.declaration.CtClass;
+import spoon.reflect.declaration.CtInterface;
+import spoon.reflect.declaration.CtMethod;
+import spoon.reflect.declaration.ModifierKind;
+import spoon.reflect.path.CtRole;
+
+import static spoon.metamodel.SpoonMetaModel.addUniqueObject;
+
+/**
+ * Represents a type of Spoon model class
+ */
+public class MMType {
+ /**
+ * Kind of this type
+ */
+ MMTypeKind kind;
+ /**
+ * Name of the type
+ */
+ String name;
+ /**
+ * List of fields ordered same like CtScanner scans them
+ */
+ final Map role2field = new LinkedHashMap<>();
+
+ /**
+ * List of super types of this type
+ */
+ private final List superTypes = new ArrayList<>();
+ private final List subTypes = new ArrayList<>();
+
+ /**
+ * The {@link CtClass} linked to this {@link MMType}. Is null in case of class without interface
+ */
+ CtClass> modelClass;
+ /**
+ * The {@link CtInterface} linked to this {@link MMType}. Is null in case of interface without class
+ */
+ CtInterface> modelInteface;
+
+ /**
+ * own methods of MMType, which does not belong to any role
+ */
+ final List> otherMethods = new ArrayList<>();
+
+ MMType() {
+ super();
+ }
+
+ MMField getOrCreateMMField(CtRole role) {
+ return SpoonMetaModel.getOrCreate(role2field, role, () -> new MMField(role.getCamelCaseName(), role, this));
+ }
+
+ public MMTypeKind getKind() {
+ if (kind == null) {
+ if (modelClass != null && modelClass.hasModifier(ModifierKind.ABSTRACT) == false) {
+ kind = MMTypeKind.LEAF;
+ } else {
+ kind = MMTypeKind.ABSTRACT;
+ }
+ }
+ return kind;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Map getRole2field() {
+ return role2field;
+ }
+
+ public List getSuperTypes() {
+ return superTypes;
+ }
+
+ void addSuperType(MMType superType) {
+ if (superType == this) {
+ throw new SpoonException("Cannot add supertype to itself");
+ }
+ if (addUniqueObject(superTypes, superType)) {
+ superType.subTypes.add(this);
+ superType.role2field.forEach((role, superMMField) -> {
+ MMField mmField = getOrCreateMMField(role);
+ mmField.addSuperField(superMMField);
+ });
+ }
+ }
+
+ public CtClass> getModelClass() {
+ return modelClass;
+ }
+
+ public CtInterface> getModelInteface() {
+ return modelInteface;
+ }
+
+ @Override
+ public String toString() {
+ return getName();
+ }
+}
diff --git a/src/test/java/spoon/metamodel/MMTypeKind.java b/src/test/java/spoon/metamodel/MMTypeKind.java
new file mode 100644
index 00000000000..aab982941a0
--- /dev/null
+++ b/src/test/java/spoon/metamodel/MMTypeKind.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright (C) 2006-2017 INRIA and contributors
+ * Spoon - http://spoon.gforge.inria.fr/
+ *
+ * This software is governed by the CeCILL-C License under French law and
+ * abiding by the rules of distribution of free software. You can use, modify
+ * and/or redistribute the software under the terms of the CeCILL-C license as
+ * circulated by CEA, CNRS and INRIA at http://www.cecill.info.
+ *
+ * This program 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 CeCILL-C License for more details.
+ *
+ * The fact that you are presently reading this means that you have had
+ * knowledge of the CeCILL-C license and that you accept its terms.
+ */
+package spoon.metamodel;
+
+/**
+ * Represents type of Spoon model class
+ */
+public enum MMTypeKind {
+ /**
+ * Kind of type which represents leaf of Spoon model.
+ * Examples: CtClass, CtField, CtThrow
+ */
+ LEAF,
+ /**
+ * Kind of type which represents some abstract concept of Spoon model
+ * Examples: CtExecutable, CtReference, CtBodyHolder, ...
+ */
+ ABSTRACT;
+}
diff --git a/src/test/java/spoon/metamodel/SpoonMetaModel.java b/src/test/java/spoon/metamodel/SpoonMetaModel.java
new file mode 100644
index 00000000000..ecfc1b11a0f
--- /dev/null
+++ b/src/test/java/spoon/metamodel/SpoonMetaModel.java
@@ -0,0 +1,625 @@
+/**
+ * Copyright (C) 2006-2017 INRIA and contributors
+ * Spoon - http://spoon.gforge.inria.fr/
+ *
+ * This software is governed by the CeCILL-C License under French law and
+ * abiding by the rules of distribution of free software. You can use, modify
+ * and/or redistribute the software under the terms of the CeCILL-C license as
+ * circulated by CEA, CNRS and INRIA at http://www.cecill.info.
+ *
+ * This program 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 CeCILL-C License for more details.
+ *
+ * The fact that you are presently reading this means that you have had
+ * knowledge of the CeCILL-C license and that you accept its terms.
+ */
+package spoon.metamodel;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.lang.annotation.Annotation;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import spoon.Launcher;
+import spoon.SpoonException;
+import spoon.reflect.annotations.PropertyGetter;
+import spoon.reflect.annotations.PropertySetter;
+import spoon.reflect.code.CtStatement;
+import spoon.reflect.declaration.CtAnnotation;
+import spoon.reflect.declaration.CtClass;
+import spoon.reflect.declaration.CtInterface;
+import spoon.reflect.declaration.CtMethod;
+import spoon.reflect.declaration.CtParameter;
+import spoon.reflect.declaration.CtType;
+import spoon.reflect.declaration.CtTypeParameter;
+import spoon.reflect.factory.Factory;
+import spoon.reflect.path.CtRole;
+import spoon.reflect.reference.CtTypeParameterReference;
+import spoon.reflect.reference.CtTypeReference;
+import spoon.reflect.visitor.PrinterHelper;
+import spoon.reflect.visitor.filter.AllTypeMembersFunction;
+import spoon.reflect.visitor.filter.TypeFilter;
+import spoon.support.visitor.ClassTypingContext;
+
+/**
+ * Represents a Spoon meta model
+ */
+public class SpoonMetaModel {
+ public static final String CLASS_SUFFIX = "Impl";
+ /**
+ * qualified names of packages which contains interfaces of spoon model
+ */
+ public static final Set MODEL_IFACE_PACKAGES = new HashSet<>(Arrays.asList(
+ "spoon.reflect.code",
+ "spoon.reflect.declaration",
+ "spoon.reflect.reference"));
+
+ private final Factory factory;
+ private PrinterHelper problems;
+ private final Map> allUnhandledMethodSignatures = new HashMap<>();
+
+ /**
+ * {@link MMType}s by name
+ */
+ private final Map name2mmType = new HashMap<>();
+
+ private final CtTypeReference> statementRef;
+ private final CtTypeReference> iterableRef;
+ private final CtTypeReference> mapRef;
+
+ /**
+ * Parses spoon sources and creates factory with spoon model
+ */
+ public SpoonMetaModel() {
+ this(null);
+ }
+
+ /**
+ * @param factory already loaded factory with all Spoon model types
+ */
+ public SpoonMetaModel(Factory factory) {
+ this.factory = factory = (factory == null ? createFactory() : factory);
+ statementRef = factory.createCtTypeReference(CtStatement.class);
+ iterableRef = factory.createCtTypeReference(Iterable.class);
+ mapRef = factory.createCtTypeReference(Map.class);
+ problems = new PrinterHelper(factory.getEnvironment());
+
+ factory.getModel().getRootPackage().filterChildren(new TypeFilter<>(CtInterface.class))
+ .forEach((CtInterface> iface) -> {
+ if (MODEL_IFACE_PACKAGES.contains(iface.getPackage().getQualifiedName())) {
+ getOrCreateMMType(iface);
+ }
+ });
+
+ //collect all method signatures marked as as PropertyGetter or PropertySetter which are not assigned to MMField
+ if (allUnhandledMethodSignatures.size() > 0) {
+ problems.write("Unhandled method signatures:").writeln();
+ problems.incTab();
+ forEachSortedEntry(allUnhandledMethodSignatures, (k, v) -> problems.write(getDeclaringTypeSignature(v)).writeln());
+ problems.decTab();
+ }
+ //detect unused CtRoles
+ Set unhandledRoles = new HashSet<>(Arrays.asList(CtRole.values()));
+ name2mmType.values().forEach(mmType2 -> {
+ unhandledRoles.removeAll(mmType2.role2field.keySet());
+ });
+
+ List unhandledRoleNames = new ArrayList();
+ unhandledRoles.forEach(it -> unhandledRoleNames.add(it.name()));
+ forEachSorted(unhandledRoleNames, role -> {
+ problems.write("Unused CtRole." + role + " found").writeln();
+ });
+ }
+
+ protected Factory createFactory() {
+ final Launcher launcher = new Launcher();
+ launcher.getEnvironment().setNoClasspath(true);
+ launcher.getEnvironment().setCommentEnabled(true);
+ // Spoon model interfaces
+ launcher.addInputResource("./src/main/java/spoon/reflect/code");
+ launcher.addInputResource("./src/main/java/spoon/reflect/declaration");
+ launcher.addInputResource("./src/main/java/spoon/reflect/reference");
+ launcher.addInputResource("src/main/java/spoon/support/reflect/declaration");
+ launcher.addInputResource("src/main/java/spoon/support/reflect/code");
+ launcher.addInputResource("src/main/java/spoon/support/reflect/reference");
+ launcher.buildModel();
+ return launcher.getFactory();
+ }
+
+ private MMType getOrCreateMMType(CtType> type) {
+ String mmTypeName = getMMTypeIntefaceName(type);
+ MMType mmType = getOrCreate(name2mmType, mmTypeName, () -> new MMType());
+ if (mmType.name == null) {
+ //it is not initialized yet. Do it now
+ mmType.name = mmTypeName;
+ if (type instanceof CtInterface>) {
+ CtInterface> iface = (CtInterface>) type;
+ mmType.modelInteface = iface;
+ mmType.modelClass = getImplementationOfInterface(iface);
+ } else if (type instanceof CtClass>) {
+ CtClass> clazz = (CtClass>) type;
+ mmType.modelClass = clazz;
+ mmType.modelInteface = getInterfaceOfImplementation(clazz);
+ } else {
+ throw new SpoonException("Unexpected spoon model type: " + type.getQualifiedName());
+ }
+
+ if (mmType.modelClass != null) {
+ addFieldsOfType(mmType, mmType.modelClass);
+ }
+ if (mmType.modelInteface != null) {
+ //add fields of interface too. They are not added by above call of addFieldsOfType, because the MMType already exists in name2mmType
+ addFieldsOfType(mmType, mmType.modelInteface);
+ }
+ detectMethodKinds(mmType);
+ }
+ return mmType;
+ }
+
+ private void detectMethodKinds(MMType mmType) {
+ // collect getters, setters, ...
+ for (MMField mmField : mmType.role2field.values()) {
+ Set unhandledSignatures = new HashSet<>(mmField.allRoleMethodsBySignature.keySet());
+ // look for getter
+ forEachValueWithKeyPrefix(mmField.allRoleMethodsBySignature, new String[] { "get", "is" }, (k, v) -> {
+ if (v.get(0).getParameters().isEmpty() == false) {
+ // ignore getters with some parameters
+ return;
+ }
+ createFieldHandler(unhandledSignatures, mmField, null, -1, () -> mmField.get, (m) -> mmField.get = m, "get()")
+ .accept(k, v);
+ });
+
+ mmField.setValueType(mmField.get == null ? null : mmField.get.getType());
+ if (mmField.getValueType() instanceof CtTypeParameterReference) {
+ CtTypeReference> boundingType = ((CtTypeParameterReference) mmField.getValueType()).getBoundingType();
+ if (boundingType == null) {
+ boundingType = factory.Type().OBJECT;
+ }
+ mmField.setValueType(boundingType);
+ }
+ if (mmField.getValueType() == null) {
+ problems.write("Missing getter for " + mmField.ownerType.getName() + " and CtRole." + mmField.role)
+ .writeln();
+ }
+ // look for setter
+ forEachValueWithKeyPrefix(mmField.allRoleMethodsBySignature, "set", (k, v) -> {
+ if (v.get(0).getParameters().size() == 1) {
+ // setters with 1 parameter equal to getter return value
+ createFieldHandler(unhandledSignatures, mmField, mmField.getValueType(), 0, () -> mmField.set,
+ (m) -> mmField.set = m, "set(value)").accept(k, v);
+ return;
+ }
+ });
+ if (mmField.set == null) {
+ problems.write("Missing setter for " + mmField.ownerType.getName() + " and CtRole." + mmField.role)
+ .writeln();
+ }
+
+ if (mmField.getValueContainerType() != MMContainerType.SINGLE) {
+ forEachValueWithKeyPrefix(mmField.allRoleMethodsBySignature, new String[] { "add", "insert" },
+ (k, v) -> {
+ if (v.get(0).getParameters().size() == 1) {
+ if (v.get(0).getSimpleName().endsWith("AtTop")
+ || v.get(0).getSimpleName().endsWith("Begin")) {
+ // adders with 1 parameter and fitting
+ // value type
+ createFieldHandler(unhandledSignatures, mmField, mmField.getItemValueType(), 0,
+ () -> mmField.addFirst, (m) -> mmField.addFirst = m, "addFirst(value)")
+ .accept(k, v);
+ return;
+ } else if (v.get(0).getSimpleName().endsWith("AtBottom")
+ || v.get(0).getSimpleName().endsWith("End")) {
+ // adders with 1 parameter and fitting
+ // value type
+ createFieldHandler(unhandledSignatures, mmField, mmField.getItemValueType(), 0,
+ () -> mmField.addLast, (m) -> mmField.addLast = m, "addLast(value)").accept(k,
+ v);
+ return;
+ } else {
+ // adders with 1 parameter and fitting
+ // value type
+ createFieldHandler(unhandledSignatures, mmField, mmField.getItemValueType(), 0,
+ () -> mmField.add, (m) -> mmField.add = m, "add(value)").accept(k, v);
+ return;
+ }
+ }
+ if (v.get(0).getParameters().size() == 2) {
+ // adders with 2 parameters. First int
+ if (v.get(0).getParameters().get(0).getType().getSimpleName().equals("int")) {
+ createFieldHandler(unhandledSignatures, mmField, mmField.getItemValueType(), 1,
+ () -> mmField.addOn, (m) -> mmField.addOn = m, "addOn(int, value)").accept(k,
+ v);
+ }
+ return;
+ }
+ });
+ forEachValueWithKeyPrefix(mmField.allRoleMethodsBySignature, "remove", (k, v) -> {
+ if (v.get(0).getParameters().size() == 1) {
+ createFieldHandler(unhandledSignatures, mmField, mmField.getItemValueType(), 0, () -> mmField.remove,
+ (m) -> mmField.remove = m, "remove(value)").accept(k, v);
+ }
+ });
+ }
+ unhandledSignatures.forEach(key -> {
+ CtMethod> representant = mmField.allRoleMethodsBySignature.get(key).get(0);
+ getOrCreate(allUnhandledMethodSignatures, getDeclaringTypeSignature(representant),
+ () -> representant);
+ });
+ }
+ }
+
+ /**
+ * @return all the problems found in spoon meta model
+ */
+ public String getProblems() {
+ return problems.toString();
+ }
+
+ private void forEachValueWithKeyPrefix(Map map, String prefix, BiConsumer consumer) {
+ forEachValueWithKeyPrefix(map, new String[] { prefix }, consumer);
+ }
+
+ private void forEachValueWithKeyPrefix(Map map, String[] prefixes, BiConsumer consumer) {
+ for (Map.Entry e : map.entrySet()) {
+ for (String prefix : prefixes) {
+ if (e.getKey().startsWith(prefix)) {
+ consumer.accept(e.getKey(), e.getValue());
+ }
+ }
+ }
+ }
+
+ private BiConsumer>> createFieldHandler(Set unhandledSignatures,
+ MMField mmField, CtTypeReference valueType, int valueParamIdx, Supplier> fieldGetter,
+ Consumer> fieldSetter, String fieldName) {
+ return (k, v) -> {
+ //Look for first method which has a body
+ CtMethod> method = getConcreteMethod(v);
+ // use getTypeErasure() comparison, because some getters and setters
+ // does not fit exactly
+ List> params = method.getParameters();
+ boolean checkParameterType = params.size() > 0 || valueType != null;
+ CtTypeReference> newValueType = typeMatches(valueType,
+ checkParameterType ? params.get(valueParamIdx).getType() : null);
+ if (newValueType != null || checkParameterType == false) {
+ // the method parameter matches with expected valueType
+ if (newValueType != valueType) {
+ // there is new value type, we have to update it in mmField
+ if (mmField.getValueType() == valueType) {
+ mmField.setValueType(newValueType);
+ } else if (mmField.getItemValueType() == valueType) {
+ mmField.setItemValueType(newValueType);
+ } else {
+ throw new SpoonException("Cannot update valueType");
+ }
+ }
+ unhandledSignatures.remove(k);
+ if (fieldGetter.get() == null) {
+ // we do not have method yet for this field
+ fieldSetter.accept(method);
+ } else {
+ // there is already a method for this field
+ if (valueType != null) {
+ // 1) choose the method with more fitting valueType
+ boolean oldMatches = valueType
+ .equals(fieldGetter.get().getParameters().get(valueParamIdx).getType());
+ boolean newMatches = valueType.equals(method.getParameters().get(valueParamIdx).getType());
+ if (oldMatches != newMatches) {
+ if (oldMatches) {
+ // old is matching
+ // add new as unhandled signature
+ unhandledSignatures.add(k);
+ } else {
+ // new is matching
+ // add old as unhandled signature
+ unhandledSignatures.add(fieldGetter.get().getSignature());
+ // and set new is current field value
+ fieldSetter.accept(method);
+ }
+ // do report problem. The conflict was resolved
+ return;
+ }
+ } else if (params.isEmpty()) {
+ // the value type is not known yet and there is no input
+ // parameter. We are resolving getter field.
+ // choose the getter whose return value is a collection
+ // of second one
+ CtTypeReference> oldReturnType = fieldGetter.get().getType();
+ CtTypeReference> newReturnType = method.getType();
+ boolean isOldIterable = oldReturnType.isSubtypeOf(iterableRef);
+ boolean isNewIterable = newReturnType.isSubtypeOf(iterableRef);
+ if (isOldIterable != isNewIterable) {
+ // they are not some. Only one of them is iterable
+ if (isOldIterable) {
+ if (isIterableOf(oldReturnType, newReturnType)) {
+ // use old method, which is multivalue
+ // represantation of new method
+ // OK - no conflict. Finish
+ return;
+ }
+ } else {
+ if (isIterableOf(newReturnType, oldReturnType)) {
+ // use new method, which is multivalue
+ // represantation of old method
+ fieldSetter.accept(method);
+ // OK - no conflict. Finish
+ return;
+ }
+ }
+ // else report ambiguity
+ }
+ }
+
+ problems.write(mmField.ownerType.getName() + " has ambiguous " + fieldName + " :").writeln().incTab()
+ .write(fieldGetter.get().getType() + "#" + getDeclaringTypeSignature(fieldGetter.get()))
+ .writeln().write(method.getType() + "#" + getDeclaringTypeSignature(method)).writeln()
+ .decTab();
+ }
+ }
+ };
+ }
+
+ /**
+ * @param methods
+ * @return first non abstract method from the list. If there is none then it returns first method
+ */
+ private CtMethod> getConcreteMethod(List> methods) {
+ for (CtMethod> method : methods) {
+ if (method.getBody() != null) {
+ return method;
+ }
+ }
+ return methods.get(0);
+ }
+
+ /**
+ * @param valueType
+ * @return item type of the valueType. If valueType is not a
+ * {@link Iterable} then return null
+ */
+ private CtTypeReference> getItemValueType(CtTypeReference> valueType) {
+ if (valueType != null) {
+ if (valueType.isSubtypeOf(iterableRef) && valueType.getActualTypeArguments().size() == 1) {
+ return valueType.getActualTypeArguments().get(0);
+ } else if (valueType.isSubtypeOf(mapRef) && valueType.getActualTypeArguments().size() == 2) {
+ return valueType.getActualTypeArguments().get(1);
+ }
+ }
+ return null;
+ }
+
+ private Set propertyGetterAndSetter = new HashSet<>(
+ Arrays.asList(PropertyGetter.class.getName(), PropertySetter.class.getName()));
+
+ private void addFieldsOfType(MMType mmType, CtType> ctType) {
+ ctType.getTypeMembers().forEach(typeMember -> {
+ if (typeMember instanceof CtMethod>) {
+ CtMethod> method = (CtMethod>) typeMember;
+ CtRole role = getRoleOfMethod(method);
+ if (role != null) {
+ MMField field = mmType.getOrCreateMMField(role);
+ field.addMethod(method);
+ } else {
+ mmType.otherMethods.add(method);
+ }
+ }
+ });
+ addFieldsOfSuperType(mmType, ctType.getSuperclass());
+ ctType.getSuperInterfaces().forEach(superIfaceRef -> addFieldsOfSuperType(mmType, superIfaceRef));
+ }
+
+ private static Set EXPECTED_TYPES_NOT_IN_CLASSPATH = new HashSet<>(Arrays.asList(
+ "java.lang.Cloneable",
+ "spoon.processing.FactoryAccessor",
+ "spoon.reflect.visitor.CtVisitable",
+ "spoon.reflect.visitor.chain.CtQueryable",
+ "spoon.template.TemplateParameter",
+ "java.lang.Iterable"));
+
+
+ /**
+ * add all fields of `superTypeRef` into `mmType`
+ * @param mmType sub type
+ * @param superTypeRef super type
+ */
+ private void addFieldsOfSuperType(MMType mmType, CtTypeReference> superTypeRef) {
+ if (superTypeRef == null) {
+ return;
+ }
+ CtType> superType = superTypeRef.getDeclaration();
+ if(superType == null) {
+ if(EXPECTED_TYPES_NOT_IN_CLASSPATH.contains(superTypeRef.getQualifiedName()) == false) {
+ problems.write("Type not in model: " + superTypeRef.getQualifiedName()).writeln();
+ }
+ return;
+ }
+ MMType superMMType = getOrCreateMMType(superType);
+ if (superMMType != mmType) {
+ mmType.addSuperType(superMMType);
+ }
+ }
+
+ /**
+ * @param type
+ * @return name of {@link MMType}, which represents Spoon model {@link CtType}
+ */
+ public static String getMMTypeIntefaceName(CtType> type) {
+ String name = type.getSimpleName();
+ if (name.endsWith(CLASS_SUFFIX)) {
+ name = name.substring(0, name.length() - CLASS_SUFFIX.length());
+ }
+ return name;
+ }
+
+ static V getOrCreate(Map map, K key, Supplier valueCreator) {
+ V value = map.get(key);
+ if (value == null) {
+ value = valueCreator.get();
+ map.put(key, value);
+ }
+ return value;
+ }
+ static boolean addUniqueObject(Collection col, T o) {
+ if (containsObject(col, o)) {
+ return false;
+ }
+ col.add(o);
+ return true;
+ }
+ static boolean containsObject(Iterable extends Object> iter, Object o) {
+ for (Object object : iter) {
+ if (object == o) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @param iface
+ * @return {@link CtClass} of Spoon model which implements the spoon model interface. null if there is no implementation.
+ */
+ public static CtClass> getImplementationOfInterface(CtInterface> iface) {
+ String impl = iface.getQualifiedName().replace("spoon.reflect", "spoon.support.reflect") + CLASS_SUFFIX;
+ return (CtClass>) iface.getFactory().Type().get(impl);
+ }
+
+ /**
+ * @param impl
+ * @return {@link CtInterface} of Spoon model which represents API of the spoon model class. null if there is no implementation.
+ */
+ public static CtInterface> getInterfaceOfImplementation(CtClass> impl) {
+ String iface = impl.getQualifiedName();
+ if (iface.endsWith(CLASS_SUFFIX) == false || iface.startsWith("spoon.support.reflect.") == false) {
+ throw new SpoonException("Unexpected spoon model implementation class: " + impl.getQualifiedName());
+ }
+ iface = iface.substring(0, iface.length() - CLASS_SUFFIX.length());
+ iface = iface.replace("spoon.support.reflect", "spoon.reflect");
+ return (CtInterface>) impl.getFactory().Type().get(iface);
+ }
+
+ /**
+ * @param method to be checked method
+ * @return {@link CtRole} of spoon model method. Looking into all super class/interface implementations of this method
+ */
+ public static CtRole getRoleOfMethod(CtMethod> method) {
+ Factory f = method.getFactory();
+ CtAnnotation getter = getInheritedAnnotation(method, f.createCtTypeReference(PropertyGetter.class));
+ if (getter != null) {
+ return getter.getActualAnnotation().role();
+ }
+ CtAnnotation setter = getInheritedAnnotation(method, f.createCtTypeReference(PropertySetter.class));
+ if (setter != null) {
+ return setter.getActualAnnotation().role();
+ }
+ return null;
+ }
+
+ /**
+ * Looks for method in superClass and superInterface hierarchy for the method with required annotationType
+ * @param method
+ * @param annotationType
+ * @return
+ */
+ private static CtAnnotation getInheritedAnnotation(CtMethod> method, CtTypeReference annotationType) {
+ CtAnnotation annotation = method.getAnnotation(annotationType);
+ if (annotation == null) {
+ CtType> declType = method.getDeclaringType();
+ final ClassTypingContext ctc = new ClassTypingContext(declType);
+ annotation = declType.map(new AllTypeMembersFunction(CtMethod.class)).map((CtMethod> currentMethod) -> {
+ if (method == currentMethod) {
+ return null;
+ }
+ if (ctc.isSameSignature(method, currentMethod)) {
+ CtAnnotation annotation2 = currentMethod.getAnnotation(annotationType);
+ if (annotation2 != null) {
+ return annotation2;
+ }
+ }
+ return null;
+ }).first();
+ }
+ return annotation;
+ }
+ private String getDeclaringTypeSignature(CtMethod m) {
+ return m.getDeclaringType().getSimpleName() + "#" + m.getSignature();
+ }
+ /**
+ * Checks whether expectedType and realType are matching.
+ *
+ * @param expectedType
+ * @param realType
+ * @return new expectedType or null if it is not matching
+ */
+ private CtTypeReference> typeMatches(CtTypeReference> expectedType, CtTypeReference> realType) {
+ if (expectedType == null) {
+ return realType;
+ }
+ if (expectedType.getTypeErasure().equals(realType.getTypeErasure())) {
+ return expectedType;
+ }
+ if (expectedType.getSimpleName().equals("CtBlock") && statementRef.equals(realType.getTypeErasure())) {
+ /*
+ * These are compatible setters too CtBlock CtCatch#get() void
+ * CtCatch#set(CtStatement)
+ */
+ return statementRef;
+ }
+ if (expectedType.isSubtypeOf(realType)) {
+ /*
+ * CtFieldReference CtFieldAccess#getVariable() CtFieldAccess
+ * inherits from CtVariableAccess which has
+ * #setVariable(CtVariableReference) it is OK to use expected
+ * type CtFieldReference, when setter has CtVariableReference
+ */
+ return expectedType;
+ }
+ return null;
+ }
+ /**
+ * @param iterableType
+ * @param itemType
+ * @return true if itemType can be iterated by iterableType
+ */
+ private boolean isIterableOf(CtTypeReference> iterableType, CtTypeReference> itemType) {
+ if (iterableType.getActualTypeArguments().size() == 1) {
+ CtTypeReference> itemTypeOfIterable = iterableType.getActualTypeArguments().get(0);
+ return itemType.isSubtypeOf(itemTypeOfIterable);
+ }
+ return false;
+ }
+ private static void forEachSortedEntry(Map map, BiConsumer consumer) {
+ List sortedKeys = new ArrayList<>(map.keySet());
+ sortedKeys.sort((a, b) -> a.compareTo(b));
+ for (String key : sortedKeys) {
+ consumer.accept(key, map.get(key));
+ }
+ }
+
+ private static void forEachSorted(Collection iter, Consumer consumer) {
+ List sortedKeys = new ArrayList<>(iter);
+ sortedKeys.sort((a, b) -> a.compareTo(b));
+ for (String key : sortedKeys) {
+ consumer.accept(key);
+ }
+ }
+
+ public Collection getMMTypes() {
+ return Collections.unmodifiableCollection(name2mmType.values());
+ }
+}