diff --git a/gradle/versions.gradle b/gradle/versions.gradle index 76830d37..5cb72add 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -18,5 +18,6 @@ ext.versions = [ 'gson': '[2.9.1,2.11)', 'websocket_jakarta': '2.0.0', 'websocket': '1.0', - 'junit': '4.13.2' + 'junit': '4.13.2', + 'archunit': '1.0.1' ] diff --git a/org.eclipse.lsp4j.debug/build.gradle b/org.eclipse.lsp4j.debug/build.gradle index c93adebb..561561a6 100644 --- a/org.eclipse.lsp4j.debug/build.gradle +++ b/org.eclipse.lsp4j.debug/build.gradle @@ -14,9 +14,12 @@ ext.title = 'LSP4J Debug' description = 'Java bindings for the Debug Server Protocol' dependencies { - api project(":org.eclipse.lsp4j.generator") + compileOnly project(":org.eclipse.lsp4j.generator") api project(":org.eclipse.lsp4j.jsonrpc.debug") testImplementation "junit:junit:$versions.junit" + testImplementation "org.eclipse.xtend:org.eclipse.xtend.lib:$versions.xtend_lib" + testImplementation project(":org.eclipse.lsp4j.generator") + testImplementation "com.tngtech.archunit:archunit:$versions.archunit" } jar { @@ -24,5 +27,5 @@ jar { } jar.bnd ( - 'Import-Package': "com.google.common.*;version=\"$versions.guava\",com.google.gson.*;version=\"$versions.gson\",*", + 'Import-Package': "com.google.gson.*;version=\"$versions.gson\",*", ) diff --git a/org.eclipse.lsp4j.debug/src/main/java/org/eclipse/lsp4j/debug/util/ToStringBuilder.java b/org.eclipse.lsp4j.debug/src/main/java/org/eclipse/lsp4j/debug/util/ToStringBuilder.java new file mode 100644 index 00000000..513b4325 --- /dev/null +++ b/org.eclipse.lsp4j.debug/src/main/java/org/eclipse/lsp4j/debug/util/ToStringBuilder.java @@ -0,0 +1,372 @@ +package org.eclipse.lsp4j.debug.util; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.List; + + +/** + * Helps with the construction of good {@link Object#toString()} representations. + *

You can customize the output using the builder-style methods {@link ToStringBuilder#singleLine()} {@link ToStringBuilder#skipNulls()} and {@link ToStringBuilder#hideFieldNames()}.

+ *

You can either directly list fields to include via {@link ToStringBuilder#add(String, Object)} and {@link ToStringBuilder#add(Object)} + * or you can let the builder do it automatically using reflection, either including the fields declared in this class or including all superclasses.

+ *

The builder will automatically handle cycles in the object tree. It also pretty prints arrays and Iterables.

+ * + * This class is not thread safe. + * @since 2.7 + */ +public final class ToStringBuilder { + + public static class ToStringContext { + + public final static ToStringContext INSTANCE = new ToStringContext(); + + private final static ThreadLocal> currentlyProcessed = new ThreadLocal>() { + @Override + public IdentityHashMap initialValue() { + return new IdentityHashMap(); + } + }; + + public boolean startProcessing(final Object obj) { + return ToStringContext.currentlyProcessed.get().put(obj, Boolean.TRUE) == null; + } + + public void endProcessing(final Object obj) { + ToStringContext.currentlyProcessed.get().remove(obj); + } + } + + private static ToStringContext toStringContext = ToStringContext.INSTANCE; + + private final Object instance; + + private final String typeName; + + private boolean multiLine = true; + + private boolean skipNulls = false; + + private boolean showFieldNames = true; + + private boolean prettyPrint = true; + + private final List parts = new ArrayList(); + + /** + * Creates a new ToStringBuilder for the given object. If you don't use reflection, then this instance + * is only used for obtaining its classes' simple name. + * + * @param instance the object to convert to a String + */ + public ToStringBuilder(final Object instance) { + this.instance = instance; + this.typeName = instance.getClass().getSimpleName(); + } + + /** + * Fields are printed on a single line, separated by commas instead of newlines + * @return this + */ + public ToStringBuilder singleLine() { + this.multiLine = false; + return this; + } + + /** + * Fields with null values will be excluded from the output + * @return this + */ + public ToStringBuilder skipNulls() { + this.skipNulls = true; + return this; + } + + /** + * Field names will not be included in the output. Useful for small classes. + * @return this + */ + public ToStringBuilder hideFieldNames() { + this.showFieldNames = false; + return this; + } + + /** + * By default, Iterables, Arrays and multiline Strings are pretty-printed. + * Switching to their normal representation makes the toString method significantly faster. + * @since 2.9 + * @return this + */ + public ToStringBuilder verbatimValues() { + this.prettyPrint = false; + return this; + } + + /** + * Adds all fields declared directly in the object's class to the output + * @return this + */ + public ToStringBuilder addDeclaredFields() { + Field[] fields = instance.getClass().getDeclaredFields(); + for(Field field : fields) { + addField(field); + } + return this; + } + + /** + * Adds all fields declared in the object's class and its superclasses to the output. + * @return this + */ + public ToStringBuilder addAllFields() { + List fields = getAllDeclaredFields(instance.getClass()); + for(Field field : fields) { + addField(field); + } + return this; + } + + /** + * @param fieldName the name of the field to add to the output using reflection + * @return this + */ + public ToStringBuilder addField(final String fieldName) { + List fields = getAllDeclaredFields(instance.getClass()); + for(Field field : fields) { + if(fieldName.equals(field.getName())) { + addField(field); + break; + } + } + return this; + } + + private ToStringBuilder addField(final Field field) { + if (!Modifier.isStatic(field.getModifiers())) { + field.setAccessible(true); + try { + add(field.getName(), field.get(instance)); + } catch(IllegalAccessException e) { + sneakyThrow(e); + } + } + return this; + } + + @SuppressWarnings("unchecked") + private static void sneakyThrow(Throwable t) throws T { + throw (T) t; + } + + /** + * @param value the value to add to the output + * @param fieldName the field name to list the value under + * @return this + */ + public ToStringBuilder add(final String fieldName, final Object value) { + return addPart(fieldName, value); + } + + /** + * @param value the value to add to the output without a field name + * @return this + */ + public ToStringBuilder add(final Object value) { + return addPart(value); + } + + private Part addPart() { + final Part p = new Part(); + this.parts.add(p); + return p; + } + + private ToStringBuilder addPart(final Object value) { + final Part p = this.addPart(); + p.value = value; + return this; + } + + private ToStringBuilder addPart(final String fieldName, final Object value) { + final Part p = this.addPart(); + p.fieldName = fieldName; + p.value = value; + return this; + } + + /** + * @return the String representation of the processed object + */ + @Override + public String toString() { + boolean startProcessing = ToStringBuilder.toStringContext.startProcessing(this.instance); + if (!startProcessing) { + return this.toSimpleReferenceString(this.instance); + } + try { + final IndentationAwareStringBuilder builder = new IndentationAwareStringBuilder(); + builder.append(typeName).append(" "); + builder.append("["); + String nextSeparator = ""; + if (multiLine) { + builder.increaseIndent(); + } + for (Part part : parts) { + if (!skipNulls || part.value != null) { + if (multiLine) { + builder.newLine(); + } else { + builder.append(nextSeparator); + nextSeparator = ", "; + } + if (part.fieldName != null && this.showFieldNames) { + builder.append(part.fieldName).append(" = "); + } + this.internalToString(part.value, builder); + } + } + if (multiLine) { + builder.decreaseIndent().newLine(); + } + builder.append("]"); + return builder.toString(); + } finally { + ToStringBuilder.toStringContext.endProcessing(this.instance); + } + } + + private void internalToString(final Object object, final IndentationAwareStringBuilder sb) { + if (prettyPrint) { + if (object instanceof Iterable) { + serializeIterable((Iterable)object, sb); + } else if (object instanceof Object[]) { + sb.append(Arrays.toString((Object[])object)); + } else if (object instanceof byte[]) { + sb.append(Arrays.toString((byte[])object)); + } else if (object instanceof char[]) { + sb.append(Arrays.toString((char[])object)); + } else if (object instanceof int[]) { + sb.append(Arrays.toString((int[])object)); + } else if (object instanceof boolean[]) { + sb.append(Arrays.toString((boolean[])object)); + } else if (object instanceof long[]) { + sb.append(Arrays.toString((long[])object)); + } else if (object instanceof float[]) { + sb.append(Arrays.toString((float[])object)); + } else if (object instanceof double[]) { + sb.append(Arrays.toString((double[])object)); + } else if (object instanceof CharSequence) { + sb.append("\"").append(((CharSequence)object).toString().replace("\n", "\\n").replace("\r", "\\r")).append("\""); + } else if (object instanceof Enum) { + sb.append(((Enum)object).name()); + } else { + sb.append(String.valueOf(object)); + } + } else { + sb.append(String.valueOf(object)); + } + } + + private void serializeIterable(final Iterable object, final IndentationAwareStringBuilder sb) { + final Iterator iterator = object.iterator(); + sb.append(object.getClass().getSimpleName()).append(" ("); + if (multiLine) { + sb.increaseIndent(); + } + boolean wasEmpty = true; + while (iterator.hasNext()) { + wasEmpty = false; + if (multiLine) { + sb.newLine(); + } + this.internalToString(iterator.next(), sb); + if (iterator.hasNext()) { + sb.append(","); + } + } + if (multiLine) { + sb.decreaseIndent(); + } + if (!wasEmpty && this.multiLine) { + sb.newLine(); + } + sb.append(")"); + } + + private String toSimpleReferenceString(final Object obj) { + String simpleName = obj.getClass().getSimpleName(); + int identityHashCode = System.identityHashCode(obj); + return simpleName + "@" + Integer.valueOf(identityHashCode); + } + + private List getAllDeclaredFields(final Class clazz) { + final ArrayList result = new ArrayList<>(); + + for(Class current = clazz; current != null; current = current.getSuperclass()) { + Field[] declaredFields = current.getDeclaredFields(); + result.addAll(Arrays.asList(declaredFields)); + + } + return result; + } + + private static final class Part { + private String fieldName; + private Object value; + } + + private static class IndentationAwareStringBuilder { + private final StringBuilder builder = new StringBuilder(); + + private final String indentationString = " "; + + private final String newLineString = "\n"; + + private int indentation = 0; + + public IndentationAwareStringBuilder increaseIndent() { + indentation++; + return this; + } + + public IndentationAwareStringBuilder decreaseIndent() { + indentation--; + return this; + } + + public IndentationAwareStringBuilder append(final CharSequence string) { + if (indentation > 0) { + String indented = string.toString().replace( + newLineString, + newLineString + repeat(indentationString, indentation) + ); + builder.append(indented); + } else { + builder.append(string); + } + return this; + } + + public IndentationAwareStringBuilder newLine() { + builder.append(newLineString). + append(repeat(this.indentationString, this.indentation)); + return this; + } + + @Override + public String toString() { + return this.builder.toString(); + } + + private String repeat(String string, int count) { + StringBuilder result = new StringBuilder(); + for(int i=0; i < count; i++) { + result.append(string); + } + return result.toString(); + } + } +} \ No newline at end of file diff --git a/org.eclipse.lsp4j.debug/src/test/java/org/eclipse/lsp4j/debug/test/Lsp4jDebugArchitectureTest.java b/org.eclipse.lsp4j.debug/src/test/java/org/eclipse/lsp4j/debug/test/Lsp4jDebugArchitectureTest.java new file mode 100644 index 00000000..f5e17d30 --- /dev/null +++ b/org.eclipse.lsp4j.debug/src/test/java/org/eclipse/lsp4j/debug/test/Lsp4jDebugArchitectureTest.java @@ -0,0 +1,69 @@ +package org.eclipse.lsp4j.debug.test; + +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAPackage; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; +import static com.tngtech.archunit.base.DescribedPredicate.not; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.lang.ArchRule; + +/** + * Test to make sure we don't depend to xbase.lib, xtend.lib and guava at + * runtime + * + * @author Christian Dietrich + */ +public class Lsp4jDebugArchitectureTest { + + private static JavaClasses importedClasses; + + @BeforeClass + public static void scan() { + importedClasses = new ClassFileImporter().importPackages("org.eclipse.lsp4j.debug"); + } + + @AfterClass + public static void cleanup() { + importedClasses = null; + } + + @Test + public void testNoDependenyToXbaseLib() { + assertNoDependencyToPackage("org.eclipse.xtext.."); + } + + @Test + public void testNoDependenyToXtendLib() { + assertNoDependencyToPackage("org.eclipse.xtend.."); + } + + @Test + public void testNoDependenyToXtend2Lib() { + assertNoDependencyToPackage("org.eclipse.xtend2.."); + } + + @Test + public void testNoDependenyToGuava() { + assertNoDependencyToPackage("com.google.common.."); + } + + @Test + public void testNoDependenyToGenerator() { + assertNoDependencyToPackage("org.eclipse.lsp4j.generator.."); + } + + private void assertNoDependencyToPackage(String badPackage) { + ArchRule rule = noClasses() + .that(resideInAPackage("org.eclipse.lsp4j.debug..") + .and(not(resideInAPackage("org.eclipse.lsp4j.generator..")))) + .and(not(resideInAPackage("org.eclipse.lsp4j.debug.test.."))).should().dependOnClassesThat() + .resideInAPackage(badPackage); + rule.check(importedClasses); + } + +} diff --git a/org.eclipse.lsp4j.generator/src/main/java/org/eclipse/lsp4j/generator/JsonRpcDataProcessor.xtend b/org.eclipse.lsp4j.generator/src/main/java/org/eclipse/lsp4j/generator/JsonRpcDataProcessor.xtend index ce296a79..fef82ee2 100644 --- a/org.eclipse.lsp4j.generator/src/main/java/org/eclipse/lsp4j/generator/JsonRpcDataProcessor.xtend +++ b/org.eclipse.lsp4j.generator/src/main/java/org/eclipse/lsp4j/generator/JsonRpcDataProcessor.xtend @@ -11,6 +11,7 @@ ******************************************************************************/ package org.eclipse.lsp4j.generator +import com.google.gson.annotations.JsonAdapter import org.eclipse.lsp4j.jsonrpc.validation.NonNull import org.eclipse.xtend.lib.annotations.AccessorsProcessor import org.eclipse.xtend.lib.annotations.EqualsHashCodeProcessor @@ -22,8 +23,6 @@ import org.eclipse.xtend.lib.macro.declaration.MutableClassDeclaration import org.eclipse.xtend.lib.macro.declaration.MutableFieldDeclaration import org.eclipse.xtend.lib.macro.declaration.Type import org.eclipse.xtend.lib.macro.declaration.Visibility -import org.eclipse.xtext.xbase.lib.util.ToStringBuilder -import com.google.gson.annotations.JsonAdapter class JsonRpcDataProcessor extends AbstractClassProcessor { @@ -44,7 +43,12 @@ class JsonRpcDataProcessor extends AbstractClassProcessor { val fields = impl.declaredFields.filter[!static] equalsHashCodeUtil.addEquals(impl, fields, shouldIncludeSuper) equalsHashCodeUtil.addHashCode(impl, fields, shouldIncludeSuper) - + impl.getDeclaredMethods.forEach [ method | + val purified = method.findAnnotation(Pure.findTypeGlobally) + if (purified !== null) { + method.removeAnnotation(purified) + } + ] return impl } @@ -154,10 +158,13 @@ class JsonRpcDataProcessor extends AbstractClassProcessor { impl.addMethod("toString") [ returnType = string addAnnotation(newAnnotationReference(Override)) - addAnnotation(newAnnotationReference(Pure)) val accessorsUtil = new AccessorsProcessor.Util(context) + val fqn = impl.qualifiedName + val char dot = '.' + val pkg = fqn.substring(0, fqn.lastIndexOf(dot)) + val toStringBuilderClassName = (pkg+".util.ToStringBuilder") body = ''' - «ToStringBuilder» b = new «ToStringBuilder»(this); + «toStringBuilderClassName.newTypeReference()» b = new «toStringBuilderClassName.newTypeReference()»(this); «FOR field : toStringFields» b.add("«field.simpleName»", «IF field.declaringType == impl»this.«field.simpleName»«ELSE»« accessorsUtil.getGetterName(field)»()«ENDIF»); diff --git a/org.eclipse.lsp4j.generator/src/main/java/org/eclipse/lsp4j/generator/TypeAdapterImplProcessor.xtend b/org.eclipse.lsp4j.generator/src/main/java/org/eclipse/lsp4j/generator/TypeAdapterImplProcessor.xtend index 665a7bd5..e0e01888 100644 --- a/org.eclipse.lsp4j.generator/src/main/java/org/eclipse/lsp4j/generator/TypeAdapterImplProcessor.xtend +++ b/org.eclipse.lsp4j.generator/src/main/java/org/eclipse/lsp4j/generator/TypeAdapterImplProcessor.xtend @@ -31,6 +31,9 @@ class TypeAdapterImplProcessor extends AbstractClassProcessor { val targetType = typeAdapterImplAnnotation.getClassValue('value') generateImpl(annotatedClass, targetType, context) generateFactory(findClass(annotatedClass.qualifiedName + '.Factory'), annotatedClass, targetType, context) + annotatedClass.removeAnnotation(annotatedClass.annotations.findFirst [ + annotationTypeDeclaration == TypeAdapterImpl.findTypeGlobally + ]) } protected def generateImpl(MutableClassDeclaration impl, TypeReference targetType, extension TransformationContext context) { diff --git a/org.eclipse.lsp4j/build.gradle b/org.eclipse.lsp4j/build.gradle index 8c86f8ef..fadb0481 100644 --- a/org.eclipse.lsp4j/build.gradle +++ b/org.eclipse.lsp4j/build.gradle @@ -17,9 +17,11 @@ sourceCompatibility = '1.8' targetCompatibility = '1.8' dependencies { - api project(":org.eclipse.lsp4j.generator") + compileOnly project(":org.eclipse.lsp4j.generator") api project(":org.eclipse.lsp4j.jsonrpc") testImplementation "junit:junit:$versions.junit" + testImplementation project(":org.eclipse.lsp4j.generator") + testImplementation "com.tngtech.archunit:archunit:$versions.archunit" } jar { @@ -27,5 +29,5 @@ jar { } jar.bnd ( - 'Import-Package': "com.google.common.*;version=\"$versions.guava\",com.google.gson.*;version=\"$versions.gson\",*" + 'Import-Package': "com.google.gson.*;version=\"$versions.gson\",*" ) diff --git a/org.eclipse.lsp4j/src/main/java/org/eclipse/lsp4j/Protocol.xtend b/org.eclipse.lsp4j/src/main/java/org/eclipse/lsp4j/Protocol.xtend index 34891262..91a5bc64 100644 --- a/org.eclipse.lsp4j/src/main/java/org/eclipse/lsp4j/Protocol.xtend +++ b/org.eclipse.lsp4j/src/main/java/org/eclipse/lsp4j/Protocol.xtend @@ -4406,7 +4406,7 @@ class FormattingOptions extends LinkedHashMap getProperties() { - val properties = newLinkedHashMap + val properties = new LinkedHashMap for (entry : entrySet) { val value = switch it: entry.value { case isFirst: getFirst diff --git a/org.eclipse.lsp4j/src/main/java/org/eclipse/lsp4j/adapters/HoverTypeAdapter.xtend b/org.eclipse.lsp4j/src/main/java/org/eclipse/lsp4j/adapters/HoverTypeAdapter.xtend index 94ddde3b..bd786f3a 100644 --- a/org.eclipse.lsp4j/src/main/java/org/eclipse/lsp4j/adapters/HoverTypeAdapter.xtend +++ b/org.eclipse.lsp4j/src/main/java/org/eclipse/lsp4j/adapters/HoverTypeAdapter.xtend @@ -1,5 +1,5 @@ /****************************************************************************** - * Copyright (c) 2018 TypeFox and others. + * Copyright (c) 2018, 2023 TypeFox and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at @@ -24,6 +24,7 @@ import org.eclipse.lsp4j.MarkedString import org.eclipse.lsp4j.MarkupContent import org.eclipse.lsp4j.generator.TypeAdapterImpl import org.eclipse.lsp4j.jsonrpc.messages.Either +import java.util.ArrayList /** * A type adapter for the Hover protocol type. @@ -36,16 +37,18 @@ class HoverTypeAdapter { protected def readContents(JsonReader in) throws IOException { val nextToken = in.peek() - if (nextToken == JsonToken.STRING) { - val List> value = newArrayList(Either.forLeft(in.nextString)) + if (nextToken === JsonToken.STRING) { + val List> value = new ArrayList>() + value.add(Either.forLeft(in.nextString)) return Either.forLeft(value) - } else if (nextToken == JsonToken.BEGIN_ARRAY) { + } else if (nextToken === JsonToken.BEGIN_ARRAY) { val value = gson.fromJson(in, LIST_STRING_MARKEDSTRING.type) return Either.forLeft(value) } else { val object = JsonParser.parseReader(in) as JsonObject if (object.has("language")) { - val List> value = newArrayList(Either.forRight(gson.fromJson(object, MarkedString))) + val List> value = new ArrayList>() + value.add(Either.forRight(gson.fromJson(object, MarkedString))) return Either.forLeft(value) } else { return Either.forRight(gson.fromJson(object, MarkupContent)) diff --git a/org.eclipse.lsp4j/src/main/java/org/eclipse/lsp4j/util/ToStringBuilder.java b/org.eclipse.lsp4j/src/main/java/org/eclipse/lsp4j/util/ToStringBuilder.java new file mode 100644 index 00000000..6276f595 --- /dev/null +++ b/org.eclipse.lsp4j/src/main/java/org/eclipse/lsp4j/util/ToStringBuilder.java @@ -0,0 +1,380 @@ +/** + * Copyright (c) 2014, 2018 itemis AG (http://www.itemis.eu) and others. + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.lsp4j.util; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.List; + + +/** + * Helps with the construction of good {@link Object#toString()} representations. + *

You can customize the output using the builder-style methods {@link ToStringBuilder#singleLine()} {@link ToStringBuilder#skipNulls()} and {@link ToStringBuilder#hideFieldNames()}.

+ *

You can either directly list fields to include via {@link ToStringBuilder#add(String, Object)} and {@link ToStringBuilder#add(Object)} + * or you can let the builder do it automatically using reflection, either including the fields declared in this class or including all superclasses.

+ *

The builder will automatically handle cycles in the object tree. It also pretty prints arrays and Iterables.

+ * + * This class is not thread safe. + * @since 2.7 + */ +public final class ToStringBuilder { + + public static class ToStringContext { + + public final static ToStringContext INSTANCE = new ToStringContext(); + + private final static ThreadLocal> currentlyProcessed = new ThreadLocal>() { + @Override + public IdentityHashMap initialValue() { + return new IdentityHashMap(); + } + }; + + public boolean startProcessing(final Object obj) { + return ToStringContext.currentlyProcessed.get().put(obj, Boolean.TRUE) == null; + } + + public void endProcessing(final Object obj) { + ToStringContext.currentlyProcessed.get().remove(obj); + } + } + + private static ToStringContext toStringContext = ToStringContext.INSTANCE; + + private final Object instance; + + private final String typeName; + + private boolean multiLine = true; + + private boolean skipNulls = false; + + private boolean showFieldNames = true; + + private boolean prettyPrint = true; + + private final List parts = new ArrayList(); + + /** + * Creates a new ToStringBuilder for the given object. If you don't use reflection, then this instance + * is only used for obtaining its classes' simple name. + * + * @param instance the object to convert to a String + */ + public ToStringBuilder(final Object instance) { + this.instance = instance; + this.typeName = instance.getClass().getSimpleName(); + } + + /** + * Fields are printed on a single line, separated by commas instead of newlines + * @return this + */ + public ToStringBuilder singleLine() { + this.multiLine = false; + return this; + } + + /** + * Fields with null values will be excluded from the output + * @return this + */ + public ToStringBuilder skipNulls() { + this.skipNulls = true; + return this; + } + + /** + * Field names will not be included in the output. Useful for small classes. + * @return this + */ + public ToStringBuilder hideFieldNames() { + this.showFieldNames = false; + return this; + } + + /** + * By default, Iterables, Arrays and multiline Strings are pretty-printed. + * Switching to their normal representation makes the toString method significantly faster. + * @since 2.9 + * @return this + */ + public ToStringBuilder verbatimValues() { + this.prettyPrint = false; + return this; + } + + /** + * Adds all fields declared directly in the object's class to the output + * @return this + */ + public ToStringBuilder addDeclaredFields() { + Field[] fields = instance.getClass().getDeclaredFields(); + for(Field field : fields) { + addField(field); + } + return this; + } + + /** + * Adds all fields declared in the object's class and its superclasses to the output. + * @return this + */ + public ToStringBuilder addAllFields() { + List fields = getAllDeclaredFields(instance.getClass()); + for(Field field : fields) { + addField(field); + } + return this; + } + + /** + * @param fieldName the name of the field to add to the output using reflection + * @return this + */ + public ToStringBuilder addField(final String fieldName) { + List fields = getAllDeclaredFields(instance.getClass()); + for(Field field : fields) { + if(fieldName.equals(field.getName())) { + addField(field); + break; + } + } + return this; + } + + private ToStringBuilder addField(final Field field) { + if (!Modifier.isStatic(field.getModifiers())) { + field.setAccessible(true); + try { + add(field.getName(), field.get(instance)); + } catch(IllegalAccessException e) { + sneakyThrow(e); + } + } + return this; + } + + @SuppressWarnings("unchecked") + private static void sneakyThrow(Throwable t) throws T { + throw (T) t; + } + + /** + * @param value the value to add to the output + * @param fieldName the field name to list the value under + * @return this + */ + public ToStringBuilder add(final String fieldName, final Object value) { + return addPart(fieldName, value); + } + + /** + * @param value the value to add to the output without a field name + * @return this + */ + public ToStringBuilder add(final Object value) { + return addPart(value); + } + + private Part addPart() { + final Part p = new Part(); + this.parts.add(p); + return p; + } + + private ToStringBuilder addPart(final Object value) { + final Part p = this.addPart(); + p.value = value; + return this; + } + + private ToStringBuilder addPart(final String fieldName, final Object value) { + final Part p = this.addPart(); + p.fieldName = fieldName; + p.value = value; + return this; + } + + /** + * @return the String representation of the processed object + */ + @Override + public String toString() { + boolean startProcessing = ToStringBuilder.toStringContext.startProcessing(this.instance); + if (!startProcessing) { + return this.toSimpleReferenceString(this.instance); + } + try { + final IndentationAwareStringBuilder builder = new IndentationAwareStringBuilder(); + builder.append(typeName).append(" "); + builder.append("["); + String nextSeparator = ""; + if (multiLine) { + builder.increaseIndent(); + } + for (Part part : parts) { + if (!skipNulls || part.value != null) { + if (multiLine) { + builder.newLine(); + } else { + builder.append(nextSeparator); + nextSeparator = ", "; + } + if (part.fieldName != null && this.showFieldNames) { + builder.append(part.fieldName).append(" = "); + } + this.internalToString(part.value, builder); + } + } + if (multiLine) { + builder.decreaseIndent().newLine(); + } + builder.append("]"); + return builder.toString(); + } finally { + ToStringBuilder.toStringContext.endProcessing(this.instance); + } + } + + private void internalToString(final Object object, final IndentationAwareStringBuilder sb) { + if (prettyPrint) { + if (object instanceof Iterable) { + serializeIterable((Iterable)object, sb); + } else if (object instanceof Object[]) { + sb.append(Arrays.toString((Object[])object)); + } else if (object instanceof byte[]) { + sb.append(Arrays.toString((byte[])object)); + } else if (object instanceof char[]) { + sb.append(Arrays.toString((char[])object)); + } else if (object instanceof int[]) { + sb.append(Arrays.toString((int[])object)); + } else if (object instanceof boolean[]) { + sb.append(Arrays.toString((boolean[])object)); + } else if (object instanceof long[]) { + sb.append(Arrays.toString((long[])object)); + } else if (object instanceof float[]) { + sb.append(Arrays.toString((float[])object)); + } else if (object instanceof double[]) { + sb.append(Arrays.toString((double[])object)); + } else if (object instanceof CharSequence) { + sb.append("\"").append(((CharSequence)object).toString().replace("\n", "\\n").replace("\r", "\\r")).append("\""); + } else if (object instanceof Enum) { + sb.append(((Enum)object).name()); + } else { + sb.append(String.valueOf(object)); + } + } else { + sb.append(String.valueOf(object)); + } + } + + private void serializeIterable(final Iterable object, final IndentationAwareStringBuilder sb) { + final Iterator iterator = object.iterator(); + sb.append(object.getClass().getSimpleName()).append(" ("); + if (multiLine) { + sb.increaseIndent(); + } + boolean wasEmpty = true; + while (iterator.hasNext()) { + wasEmpty = false; + if (multiLine) { + sb.newLine(); + } + this.internalToString(iterator.next(), sb); + if (iterator.hasNext()) { + sb.append(","); + } + } + if (multiLine) { + sb.decreaseIndent(); + } + if (!wasEmpty && this.multiLine) { + sb.newLine(); + } + sb.append(")"); + } + + private String toSimpleReferenceString(final Object obj) { + String simpleName = obj.getClass().getSimpleName(); + int identityHashCode = System.identityHashCode(obj); + return simpleName + "@" + Integer.valueOf(identityHashCode); + } + + private List getAllDeclaredFields(final Class clazz) { + final ArrayList result = new ArrayList<>(); + + for(Class current = clazz; current != null; current = current.getSuperclass()) { + Field[] declaredFields = current.getDeclaredFields(); + result.addAll(Arrays.asList(declaredFields)); + + } + return result; + } + + private static final class Part { + private String fieldName; + private Object value; + } + + private static class IndentationAwareStringBuilder { + private final StringBuilder builder = new StringBuilder(); + + private final String indentationString = " "; + + private final String newLineString = "\n"; + + private int indentation = 0; + + public IndentationAwareStringBuilder increaseIndent() { + indentation++; + return this; + } + + public IndentationAwareStringBuilder decreaseIndent() { + indentation--; + return this; + } + + public IndentationAwareStringBuilder append(final CharSequence string) { + if (indentation > 0) { + String indented = string.toString().replace( + newLineString, + newLineString + repeat(indentationString, indentation) + ); + builder.append(indented); + } else { + builder.append(string); + } + return this; + } + + public IndentationAwareStringBuilder newLine() { + builder.append(newLineString). + append(repeat(this.indentationString, this.indentation)); + return this; + } + + @Override + public String toString() { + return this.builder.toString(); + } + + private String repeat(String string, int count) { + StringBuilder result = new StringBuilder(); + for(int i=0; i < count; i++) { + result.append(string); + } + return result.toString(); + } + } +} \ No newline at end of file diff --git a/org.eclipse.lsp4j/src/test/java/org/eclipse/lsp4j/test/Lsp4jArchitectureTest.java b/org.eclipse.lsp4j/src/test/java/org/eclipse/lsp4j/test/Lsp4jArchitectureTest.java new file mode 100644 index 00000000..18257562 --- /dev/null +++ b/org.eclipse.lsp4j/src/test/java/org/eclipse/lsp4j/test/Lsp4jArchitectureTest.java @@ -0,0 +1,80 @@ +/****************************************************************************** + * Copyright (c) 2023 itemis AG and others. + * + * 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, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.test; + +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAPackage; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; +import static com.tngtech.archunit.base.DescribedPredicate.not; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.lang.ArchRule; + +/** + * Test to make sure we don't depend to xbase.lib, xtend.lib and guava at + * runtime + * + * @author Christian Dietrich + */ +public class Lsp4jArchitectureTest { + + private static JavaClasses importedClasses; + + @BeforeClass + public static void scan() { + importedClasses = new ClassFileImporter().importPackages("org.eclipse.lsp4j"); + } + + @AfterClass + public static void cleanup() { + importedClasses = null; + } + + @Test + public void testNoDependenyToXbaseLib() { + assertNoDependencyToPackage("org.eclipse.xtext.."); + } + + @Test + public void testNoDependenyToXtendLib() { + assertNoDependencyToPackage("org.eclipse.xtend.."); + } + + @Test + public void testNoDependenyToXtend2Lib() { + assertNoDependencyToPackage("org.eclipse.xtend2.."); + } + + @Test + public void testNoDependenyToGuava() { + assertNoDependencyToPackage("com.google.common.."); + } + + @Test + public void testNoDependenyToGenerator() { + assertNoDependencyToPackage("org.eclipse.lsp4j.generator.."); + } + + private void assertNoDependencyToPackage(String badPackage) { + ArchRule rule = noClasses() + .that(resideInAPackage("org.eclipse.lsp4j..") + .and(not(resideInAPackage("org.eclipse.lsp4j.generator..")))) + .and(not(resideInAPackage("org.eclipse.lsp4j.test.."))).should().dependOnClassesThat() + .resideInAPackage(badPackage); + rule.check(importedClasses); + } + +}