Skip to content

Skip UseMapOf when values may be null or keys may repeat - #1382

Open
henriquejsza wants to merge 1 commit into
openrewrite:mainfrom
henriquejsza:use-map-of-skip-nullable-duplicate-1307
Open

henriquejsza wants to merge 1 commit into
openrewrite:mainfrom
henriquejsza:use-map-of-skip-nullable-duplicate-1307

Conversation

@henriquejsza

Copy link
Copy Markdown

What's changed?

UseMapOf recognises two input shapes: the anonymous-class form
(new HashMap<>() {{ put(k, v); ... }}) and the prose new HashMap<>() + put(..)
chain. Both could emit a Map.of(..) / Map.ofEntries(..) call that throws at
runtime where the original HashMap did not:

  • The anonymous-class form only skipped a put when an argument was the literal
    null. A nullable parameter or method-call value (as in the issue's
    put("owner", dataOwner)) was still folded into Map.of(..), which throws
    NullPointerException per JEP 269.
  • Neither form checked for duplicate keys. Two puts with the same key produced a
    Map.of(..) / Map.ofEntries(..) call that throws IllegalArgumentException,
    where a HashMap.put(..) chain had just kept the last write.

This change:

  • Reuses the prose form's isProvablyNonNull check for the anonymous-class form,
    so both forms only convert when every key and value is statically known to be
    non-null.
  • Bails both forms when two keys that can be compared statically
    (SemanticallyEqual) are equal.
  • Teaches isProvablyNonNull to also accept an initialized static final
    constant, so existing conversions with a constant key (e.g. put(BLAH, "foo"))
    keep working rather than regressing under the stricter nullability guard.

The safe cases the recipe converts today are unchanged.

What's your motivation?

Fixes #1307.

Anything in particular you'd like reviewers to focus on?

  • The duplicate-key guard uses SemanticallyEqual.areEqual(..) on the key
    expressions, so it only rejects keys it can prove equal (identical literals,
    identical constant references); genuinely distinct runtime values are still
    converted.
  • Extending isProvablyNonNull to accept static final + HasInit fields keeps
    the pre-existing changeDoubleBraceInitForNonStringTypes behaviour; let me know
    if you'd prefer a narrower condition.

Any additional context

This issue is cross-referenced from the recipe-quality tracking issue #1330 (item
D1). It is scoped to the null-value and duplicate-key hazards only; the
large-map / maxEntries concern (#1310) is handled separately in #1368.

Checklist

  • I've added unit tests to cover both positive and negative cases
  • I've read and applied the recipe conventions and best practices
  • I've run ./gradlew build locally, and committed any resulting changes to recipes.csv
  • I've formatted the lines I changed, without reformatting code I didn't touch

…te#1307)

The anonymous-class form only rejected literal `null` arguments, so a put with a
nullable parameter or method-call value was still folded into `Map.of(..)`, which
throws a `NullPointerException`. Neither form checked for duplicate keys, so two
puts with the same key produced a `Map.of(..)`/`Map.ofEntries(..)` call that throws
`IllegalArgumentException` at runtime, where the original `HashMap` kept the last
write.

Reuse the prose form's `isProvablyNonNull` check for the anonymous-class form, and
bail both forms when two statically comparable keys are equal. `isProvablyNonNull`
now also accepts initialized `static final` constants so existing constant-key
conversions keep working.

Signed-off-by: henriquejsza <henriquejsza@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

Recipe UseMapOf generates Map.of() with nullable values or duplicate keys, causing runtime NullPointerException

1 participant