Skip to content

Commit

Permalink
GROOVY-11196: tap/with/identity propagates null not NullObject
Browse files Browse the repository at this point in the history
3_0_X backport
  • Loading branch information
eric-milles committed Oct 18, 2023
1 parent 26af4c1 commit a353d35
Show file tree
Hide file tree
Showing 3 changed files with 333 additions and 171 deletions.
45 changes: 22 additions & 23 deletions src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java
Original file line number Diff line number Diff line change
Expand Up @@ -255,14 +255,14 @@ public static boolean is(Object self, Object other) {
* @see #with(Object, Closure)
* @since 1.0
*/
public static <T,U> T identity(
public static <T, U> T identity(
@DelegatesTo.Target("self") U self,
@DelegatesTo(value=DelegatesTo.Target.class,
target="self",
strategy=Closure.DELEGATE_FIRST)
@ClosureParams(FirstParam.class)
Closure<T> closure) {
return DefaultGroovyMethods.with(self, closure);
Closure<T> closure) {
return with(self, closure);
}

/**
Expand Down Expand Up @@ -300,7 +300,7 @@ public static <T,U> T identity(
* @since 1.5.0
*/
@SuppressWarnings("unchecked")
public static <T,U> T with(
public static <T, U> T with(
@DelegatesTo.Target("self") U self,
@DelegatesTo(value=DelegatesTo.Target.class,
target="self",
Expand Down Expand Up @@ -348,20 +348,22 @@ public static <T,U> T with(
* @see #tap(Object, Closure)
* @since 2.5.0
*/
public static <T,U extends T, V extends T> T with(
public static <T, U extends T, V extends T> T with(
@DelegatesTo.Target("self") U self,
boolean returning,
@DelegatesTo(value=DelegatesTo.Target.class,
target="self",
strategy=Closure.DELEGATE_FIRST)
@ClosureParams(FirstParam.class)
Closure<T> closure) {
@SuppressWarnings("unchecked")
final Closure<V> clonedClosure = (Closure<V>) closure.clone();
clonedClosure.setResolveStrategy(Closure.DELEGATE_FIRST);
clonedClosure.setDelegate(self);
V result = clonedClosure.call(self);
return returning ? self : result;
Closure<T> closure) { // TODO: T -> V
if (self == NullObject.getNullObject()) {
self = null; // GROOVY-4526, et al.
}
@SuppressWarnings("unchecked") Closure<V> mutableClosure = (Closure<V>) closure.clone();
mutableClosure.setResolveStrategy(Closure.DELEGATE_FIRST);
mutableClosure.setDelegate(self);
V rv = mutableClosure.call(self);
return returning ? self : rv;
}

/**
Expand Down Expand Up @@ -394,7 +396,7 @@ public static <T,U extends T, V extends T> T with(
* @since 2.5.0
*/
@SuppressWarnings("unchecked")
public static <T,U> U tap(
public static <T, U> U tap(
@DelegatesTo.Target("self") U self,
@DelegatesTo(value=DelegatesTo.Target.class,
target="self",
Expand Down Expand Up @@ -18085,16 +18087,13 @@ public static MetaClass getMetaClass(GroovyObject obj) {
* @since 1.6.0
*/
public static void setMetaClass(Class self, MetaClass metaClass) {
final MetaClassRegistry metaClassRegistry = GroovySystem.getMetaClassRegistry();
if (metaClass == null)
metaClassRegistry.removeMetaClass(self);
else {
if (metaClass instanceof HandleMetaClass) {
metaClassRegistry.setMetaClass(self, ((HandleMetaClass)metaClass).getAdaptee());
} else {
metaClassRegistry.setMetaClass(self, metaClass);
}
if (self==NullObject.class) {
if (metaClass == null) {
GroovySystem.getMetaClassRegistry().removeMetaClass(self);
} else {
MetaClass mc = metaClass instanceof HandleMetaClass
? ((HandleMetaClass)metaClass).getAdaptee() : metaClass;
GroovySystem.getMetaClassRegistry().setMetaClass(self,mc);
if (NullObject.class.equals(self)) { // GROOVY-3803
NullObject.getNullObject().setMetaClass(metaClass);
}
}
Expand Down
162 changes: 84 additions & 78 deletions src/main/java/org/codehaus/groovy/runtime/NullObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,152 +25,158 @@
import java.util.Iterator;

public class NullObject extends GroovyObjectSupport {
private static final NullObject INSTANCE = new NullObject();

/**
* private constructor
*/
private NullObject() {
}
private static final NullObject INSTANCE = new NullObject();

/**
* get the NullObject reference
* Returns the NullObject reference.
*
* @return the null object
*/
public static NullObject getNullObject() {
return INSTANCE;
}

private NullObject() {
}

//--------------------------------------------------------------------------

/**
* Since this is implemented as a singleton, we should avoid the
* use of the clone method
* Since this is implemented as a singleton, avoid the use of the clone method.
*
* @return never
* @throws NullPointerException
*/
@Override
public Object clone() {
throw new NullPointerException("Cannot invoke method clone() on null object");
}

/**
* Tries to get a property on null, which will always fail
* null is only equal to null.
*
* @param property - the property to get
* @return a NPE
* @param o the reference object with which to compare
* @return true if this object is the same as the to argument
*/
public Object getProperty(String property) {
throw new NullPointerException("Cannot get property '" + property + "' on null object");
@Override
public boolean equals(final Object o) {
return o == null;
}

/**
* Allows the closure to be called for NullObject
*
* @param closure the closure to call on the object
* @return result of calling the closure
* @return never
* @throws NullPointerException
*/
public <T> T with( Closure<T> closure ) {
return DefaultGroovyMethods.with( null, closure ) ;
@Override
public int hashCode() {
throw new NullPointerException("Cannot invoke method hashCode() on null object");
}

@Override
public String toString() {
return "null";
}

/**
* Tries to set a property on null, which will always fail
* Tries to get a property on null, which fails.
*
* @param property - the proprty to set
* @param newValue - the new value of the property
* @return never
* @throws NullPointerException
*/
public void setProperty(String property, Object newValue) {
throw new NullPointerException("Cannot set property '" + property + "' on null object");
@Override
public Object getProperty(final String name) {
throw new NullPointerException("Cannot get property '" + name + "' on null object");
}

/**
* Tries to invoke a method on null, which will always fail
* Tries to set a property on null, which fails.
*
* @param name the name of the method to invoke
* @param args - arguments to the method
* @return a NPE
* @throws NullPointerException
*/
public Object invokeMethod(String name, Object args) {
throw new NullPointerException("Cannot invoke method " + name + "() on null object");
@Override
public void setProperty(final String name, final Object value) {
throw new NullPointerException("Cannot set property '" + name + "' on null object");
}

/**
* null is only equal to null
* Tries to invoke a method on null, which falis.
*
* @param to - the reference object with which to compare
* @return - true if this object is the same as the to argument
* @return never
* @throws NullPointerException
*/
public boolean equals(Object to) {
return to == null;
@Override
public Object invokeMethod(final String name, final Object arguments) {
throw new NullPointerException("Cannot invoke method " + name + "() on null object");
}

//--------------------------------------------------------------------------

/**
* iterator() method to be able to iterate on null.
* Note: this part is from Invoker
* A null object coerces to false.
*
* @return an iterator for an empty list
* @return false
*/
public Iterator iterator() {
return Collections.EMPTY_LIST.iterator();
public boolean asBoolean() {
return false;
}

/**
* Allows to add a String to null.
* The result is concatenated String of the result of calling
* toString() on this object and the String in the parameter.
* Type conversion method for null.
*
* @param s - the String to concatenate
* @return the concatenated string
* @return null
*/
public Object plus(String s) {
return getMetaClass().invokeMethod(this, "toString", new Object[]{}) + s;
public Object asType(final Class c) {
return null;
}

/**
* Fallback for null+null.
* The result is always a NPE. The plus(String) version will catch
* the case of adding a non null String to null.
* Tests for equal references.
*
* @param o - the Object
* @return nothing
* @return true if object is null
*/
public Object plus(Object o) {
throw new NullPointerException("Cannot execute null+" + o);
public boolean is(final Object o) {
return equals(o);
}

/**
* The method "is" is used to test for equal references.
* This method will return true only if the given parameter
* is null
* Provides ability to iterate on null.
*
* @param other - the object to test
* @return true if other is null
* @return an empty iterator
*/
public boolean is(Object other) {
return other == null;
public Iterator iterator() {
return Collections.emptyIterator();
}

/**
* Type conversion method for null.
* Fallback for {@code null+null}. The {@link plus(String)} variant catches
* the case of adding a non-null String to null.
*
* @param c - the class to convert to
* @return always null
* @return never
* @throws NullPointerException
*/
public Object asType(Class c) {
return null;
public Object plus(final Object o) {
throw new NullPointerException("Cannot execute null+" + o);
}

/**
* A null object always coerces to false.
*
* @return false
* Allows to add a String to null.
* The result is concatenated String of the result of calling
* toString() on this object and the String in the parameter.
*
* @return the concatenated string
*/
public boolean asBoolean() {
return false;
}

public String toString() {
return "null";
public Object plus(final String s) {
return getMetaClass().invokeMethod(this, "toString", new Object[0]) + s;
}

public int hashCode() {
throw new NullPointerException("Cannot invoke method hashCode() on null object");
/**
* Allows the closure to be called for NullObject.
*
* @param closure the closure to call on the object
* @return result of calling the closure
*/
public <T> T with(final Closure<T> closure) {
return DefaultGroovyMethods.with(null, closure); // GROOVY-4526, GROOVY-4985
}
}
Loading

0 comments on commit a353d35

Please sign in to comment.