Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Clean up JSpecify note messages and Javadoc accuracy
Split the single PROPERTYMAPPING_JSPECIFY_NOTE template into dedicated
Message entries so the English phrasing lives in the enum rather than
being passed as format arguments from every call site.

Also correct Javadoc that overstated behavior:
- Type#isNullMarked: module support is conditional on JPMS, and TypeFactory
  does not intern Type instances, so the "O(1)" / "interned" claim was
  misleading. Reword to memoization + caller guidance.
- JSpecifyConstants: make NULL_MARKED_FQN / NULL_UNMARKED_FQN wording
  symmetric about closest-annotation precedence.
- PropertyMapping constructor-param check: clarify that the diagnostic is
  a hard compile error, not a passive note.

Loosen JSpecifyVerboseNoteTest regex to match the nullability enum values
positionally rather than pinning exact constant names.
  • Loading branch information
filiphr committed Apr 13, 2026
commit f485467c69d5c8893f8cd91f2ed5294aa3a2ff21
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,8 @@ else if ( !sourceParameter.getType().isPrimitive() ) {
() -> ctx.getTypeFactory().getType( ctx.getMapperTypeElement().asType() ).isNullMarked() )
== NullabilityUtils.Nullability.NON_NULL ) {
ctx.getMessager().note( 2,
Message.PROPERTYMAPPING_JSPECIFY_NOTE,
"skipping method-level null guard",
sourceParameter.getName(),
"parameter is @NonNull"
Message.PROPERTYMAPPING_JSPECIFY_SKIP_METHOD_GUARD_NON_NULL_PARAM,
sourceParameter.getName()
);
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,11 +275,12 @@ public PropertyMapping build() {
assignment = forge();
}

// JSpecify: report error when a source that is not guaranteed @NonNull (either explicit
// @Nullable or unknown nullability outside a @NullMarked scope) is mapped to a @NonNull
// constructor parameter without a default value. A null check would leave the variable
// at null, violating the @NonNull contract, and passing the value through is equally
// invalid — defaultValue / defaultExpression are the supported remedies.
// JSpecify: raise a hard compile error when a source that is not guaranteed @NonNull
// (either explicit @Nullable or unknown nullability outside a @NullMarked scope) is
// mapped to a @NonNull constructor parameter without a default value. Neither a null
// check (which would leave the variable null, violating the @NonNull contract) nor
// passing the value through is safe here — defaultValue / defaultExpression are the
// supported remedies.
// Skip when assignment resolution already failed: the user sees the primary
// "can't find mapping" error and a duplicate would only obscure the root cause.
if ( assignment != null
Expand Down Expand Up @@ -536,10 +537,8 @@ private boolean setterWrapperNeedsSourceNullCheck(Assignment rhs, Type targetTyp
NullabilityUtils.Nullability sourceNullability = getSourceJSpecifyNullability();
if ( sourceNullability == NullabilityUtils.Nullability.NON_NULL ) {
ctx.getMessager().note( 2,
Message.PROPERTYMAPPING_JSPECIFY_NOTE,
"skipping null check",
targetPropertyName,
"source is @NonNull"
Message.PROPERTYMAPPING_JSPECIFY_SKIP_NULL_CHECK_NON_NULL_SOURCE,
targetPropertyName
);
return false;
}
Expand Down Expand Up @@ -576,10 +575,12 @@ private boolean setterWrapperNeedsSourceNullCheck(Assignment rhs, Type targetTyp
Boolean jspecifyDecision = NullabilityUtils.requiresNullCheck( sourceNullability, targetNullability );
if ( jspecifyDecision != null ) {
ctx.getMessager().note( 2,
Message.PROPERTYMAPPING_JSPECIFY_NOTE,
jspecifyDecision ? "adding null check" : "skipping null check",
jspecifyDecision
? Message.PROPERTYMAPPING_JSPECIFY_ADD_NULL_CHECK
: Message.PROPERTYMAPPING_JSPECIFY_SKIP_NULL_CHECK,
targetPropertyName,
"source=" + sourceNullability + ", target=" + targetNullability
sourceNullability,
targetNullability
);
return jspecifyDecision;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -329,13 +329,17 @@ public boolean isString() {

/**
* Whether this type is within a JSpecify {@code @NullMarked} scope. Walks the enclosing-element
* chain (type, enclosing classes, package, module) and returns at the first annotation found:
* {@code @NullMarked} wins over nothing, but a closer {@code @NullUnmarked} overrides an outer
* {@code @NullMarked}. The result is cached on this {@code Type} instance (which is interned by
* {@code TypeFactory}), so repeated calls are {@code O(1)}.
* chain (this type, outer classes, package) and returns at the first {@code @NullMarked} or
* {@code @NullUnmarked} encountered — the closest annotation wins. Module-level annotations
* are only reached when the compiler populates {@code PackageElement.getEnclosingElement()}
* with a {@link javax.lang.model.element.ModuleElement} (JPMS only).
* <p>
* The result is memoized on this {@code Type} instance. {@link TypeFactory#getType} does not
* intern {@code Type} instances, so callers that invoke this repeatedly should cache the
* {@code Type} reference or the result.
*
* @return {@code true} if this type or an enclosing element has {@code @NullMarked} and no
* closer enclosing element has {@code @NullUnmarked}; {@code false} otherwise
* @return {@code true} if the closest enclosing annotation is {@code @NullMarked};
* {@code false} if it is {@code @NullUnmarked} or if no such annotation was found
*/
public boolean isNullMarked() {
if ( isNullMarked == null ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ public final class JSpecifyConstants {

/**
* Fully qualified name of {@code org.jspecify.annotations.NullMarked}. Its presence on an
* element (or any enclosing element) means unannotated types in that scope are effectively
* {@code @NonNull}.
* element (or an enclosing element closer than any {@code @NullUnmarked} one) means unannotated
* types in that scope are effectively {@code @NonNull}.
*/
public static final String NULL_MARKED_FQN = "org.jspecify.annotations.NullMarked";

/**
* Fully qualified name of {@code org.jspecify.annotations.NullUnmarked}. Its presence on an
* element (or an enclosing element closer than a {@code @NullMarked} one) reverts the scope
* element (or an enclosing element closer than any {@code @NullMarked} one) reverts the scope
* back to unknown nullability.
*/
public static final String NULL_UNMARKED_FQN = "org.jspecify.annotations.NullUnmarked";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,10 @@ public enum Message {
PROPERTYMAPPING_EXPRESSION_AND_CONDITION_QUALIFIED_BY_NAME_BOTH_DEFINED( "Expression and condition qualified by name are both defined in @Mapping, either define an expression or a condition qualified by name." ),
PROPERTYMAPPING_TARGET_HAS_NO_TARGET_PROPERTIES( "No target property found for target \"%s\".", Diagnostic.Kind.WARNING ),
PROPERTYMAPPING_NULLABLE_SOURCE_TO_NON_NULL_CONSTRUCTOR_PARAM( "Can't map potentially nullable source property \"%s\" to @NonNull constructor parameter \"%s\". Consider adding a defaultValue or defaultExpression." ),
PROPERTYMAPPING_JSPECIFY_NOTE( "JSpecify %s for property \"%s\": %s.", Diagnostic.Kind.NOTE ),
PROPERTYMAPPING_JSPECIFY_SKIP_NULL_CHECK_NON_NULL_SOURCE( "JSpecify skipping null check for property \"%s\": source is @NonNull.", Diagnostic.Kind.NOTE ),
PROPERTYMAPPING_JSPECIFY_ADD_NULL_CHECK( "JSpecify adding null check for property \"%s\": source=%s, target=%s.", Diagnostic.Kind.NOTE ),
PROPERTYMAPPING_JSPECIFY_SKIP_NULL_CHECK( "JSpecify skipping null check for property \"%s\": source=%s, target=%s.", Diagnostic.Kind.NOTE ),
PROPERTYMAPPING_JSPECIFY_SKIP_METHOD_GUARD_NON_NULL_PARAM( "JSpecify skipping method-level null guard for property \"%s\": parameter is @NonNull.", Diagnostic.Kind.NOTE ),

CONVERSION_LOSSY_WARNING( "%s has a possibly lossy conversion from %s to %s.", Diagnostic.Kind.WARNING ),
CONVERSION_LOSSY_ERROR( "Can't map %s. It has a possibly lossy conversion from %s to %s." ),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class JSpecifyVerboseNoteTest {
@ExpectedNote("^-- MapStruct: JSpecify skipping null check for property \"nonNullTarget\": "
+ "source is @NonNull\\.$")
@ExpectedNote("^-- MapStruct: JSpecify adding null check for property \"nonNullTargetFromNullable\": "
+ "source=NULLABLE, target=NON_NULL\\.$")
+ "source=\\w+, target=\\w+\\.$")
public void emitsSetterDecisionNotes() {
}

Expand Down