Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,10 @@ private boolean setterWrapperNeedsSourceNullCheck(Assignment rhs, Type targetTyp
NullabilityResolver.Nullability targetNullability = resolver.getSetterNullability(
targetWriteAccessor.getElement(), this::targetDeclaringTypeIsNullMarked
);
Boolean jspecifyDecision = resolver.requiresNullCheck( sourceNullability, targetNullability );
NullabilityResolver.Nullability paramNullability = getMethodParamNullability( rhs );
Boolean jspecifyDecision = resolver.requiresNullCheck(
sourceNullability, targetNullability, paramNullability
);
if ( jspecifyDecision != null ) {
ctx.getMessager().note( 2,
jspecifyDecision
Expand Down Expand Up @@ -668,6 +671,31 @@ private NullabilityResolver.Nullability getSourceJSpecifyNullability() {
return NullabilityResolver.Nullability.UNKNOWN;
}

/**
* Resolves the JSpecify nullability of the source parameter of a reused method.
* When the assignment is a {@link MethodReference}, the source value is passed to that
* method's first source parameter. If the parameter has a real element (existing mapper
* method), nullability is resolved from annotations and enclosing scope. For synthetic
* parameters (forged methods), nullability is derived from the mapper's
* {@code @NullMarked} scope alone.
*/
private NullabilityResolver.Nullability getMethodParamNullability(Assignment rhs) {
if ( rhs instanceof MethodReference ) {
MethodReference methodRef = (MethodReference) rhs;
if ( methodRef.getSourceParameters().isEmpty() ) {
return NullabilityResolver.Nullability.UNKNOWN;
}
Element paramElement = methodRef.getSourceParameters().get( 0 ).getElement();
if ( paramElement != null ) {
return ctx.getNullabilityInMapperScope( paramElement );
}
return ctx.getTypeFactory().getType( ctx.getMapperTypeElement().asType() ).isNullMarked()
? NullabilityResolver.Nullability.NON_NULL
: NullabilityResolver.Nullability.UNKNOWN;
}
return NullabilityResolver.Nullability.UNKNOWN;
}

/**
* Resolves whether the type that declares the target write accessor (i.e. the bean that
* owns the setter or field) is in a JSpecify {@code @NullMarked} scope. This is the correct
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,18 +158,23 @@ public Nullability getSetterNullability(Element element, BooleanSupplier enclosi

/**
* Determines whether a null check is required for a property mapping based on JSpecify annotations
* on the source and target elements.
* on the source, target and —when the assignment reuses a method— the method parameter.
* <p>
* Only returns a non-null decision for the clear-cut cases:
* source {@code @NonNull} (skip check) or target {@code @NonNull} (always check).
* source {@code @NonNull} (skip check), target {@code @NonNull} (always check),
* or method parameter {@code @NonNull} (always check — the source value must not be
* passed as {@code null} to a non-null parameter).
* All other cases return {@code null} to defer to the existing {@code NullValueCheckStrategy}.
*
* @param sourceNullability the nullability of the source (getter return type / parameter)
* @param targetNullability the nullability of the target (setter parameter / field)
* @param paramNullability the nullability of the reused method's source parameter, or
* {@link Nullability#UNKNOWN} when the assignment is not a method call
* @return {@code Boolean.TRUE} if a null check is needed, {@code Boolean.FALSE} if it should be skipped,
* or {@code null} if JSpecify annotations are not present and the existing strategy should be used
*/
public Boolean requiresNullCheck(Nullability sourceNullability, Nullability targetNullability) {
public Boolean requiresNullCheck(Nullability sourceNullability, Nullability targetNullability,
Nullability paramNullability) {
if ( !enabled ) {
return null;
}
Expand All @@ -181,6 +186,10 @@ public Boolean requiresNullCheck(Nullability sourceNullability, Nullability targ
// Target requires non-null: always check (regardless of source annotation)
return Boolean.TRUE;
}
if ( paramNullability == Nullability.NON_NULL ) {
// Reused method parameter requires non-null: always check
return Boolean.TRUE;
}
// All other cases: defer to existing NullValueCheckStrategy
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.bugs._4077;

import org.jspecify.annotations.NullMarked;
import org.mapstruct.Mapper;

@Mapper
@NullMarked
public interface Issue4077Mapper {
Target map(Source source);

Target.Nested mapNested(Source.Nested source);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.bugs._4077;

import java.io.File;

import org.junit.jupiter.api.extension.RegisterExtension;
import org.mapstruct.ap.testutil.IssueKey;
import org.mapstruct.ap.testutil.ProcessorTest;
import org.mapstruct.ap.testutil.WithClasses;
import org.mapstruct.ap.testutil.WithJSpecify;
import org.mapstruct.ap.testutil.runner.GeneratedSource;

/**
* Reproducer for https://github.com/mapstruct/mapstruct/issues/4077.
*
* @author Agustin Ranieri
*/
@IssueKey( "4077" )
@WithJSpecify
public class Issue4077Test {

@RegisterExtension
final GeneratedSource generatedSource = new GeneratedSource();

@ProcessorTest
@WithClasses( { Issue4077Mapper.class, Source.class, Target.class } )
public void shouldAddNullCheckBeforeReusingMethod() {
String path = "src/test/resources/fixtures/org/mapstruct/ap/test/bugs/_4077/Issue4077MapperImpl.java";
generatedSource.forMapper( Issue4077Mapper.class ).hasSameMapperContent( new File( path ) );
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.bugs._4077;

import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

@NullMarked
public class Source {
private final @Nullable Nested nested;

public Source(@Nullable Nested nested) {
this.nested = nested;
}

public @Nullable Nested getNested() {
return nested;
}

public static class Nested {
private final String foo;
private final Integer bar;

public Nested(String foo, Integer bar) {
this.foo = foo;
this.bar = bar;
}

public String getFoo() {
return foo;
}

public Integer getBar() {
return bar;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.bugs._4077;

import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

@NullMarked
public class Target {
private final @Nullable Nested nested;

public Target(@Nullable Nested nested) {
this.nested = nested;
}

public @Nullable Nested getNested() {
return nested;
}

public static class Nested {
private final String foo;
private final Integer bar;

public Nested(String foo, Integer bar) {
this.foo = foo;
this.bar = bar;
}

public String getFoo() {
return foo;
}

public Integer getBar() {
return bar;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.bugs._4077;

import javax.annotation.processing.Generated;

@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "0000-00-00T00:00:00+0000",
comments = "version: , compiler: javac, environment: Java 21"
)
public class Issue4077MapperImpl implements Issue4077Mapper {

@Override
public Target map(Source source) {

Target.Nested nested = null;

if ( source.getNested() != null ) {
nested = mapNested( source.getNested() );
}

Target target = new Target( nested );

return target;
}

@Override
public Target.Nested mapNested(Source.Nested source) {

String foo = null;
Integer bar = null;

foo = source.getFoo();
bar = source.getBar();

Target.Nested nested = new Target.Nested( foo, bar );

return nested;
}
}
Loading