Quickfix for java:S6880 produces code on which tests fail

Please provide

  • Operating system: Linux
  • SonarQube for IntelliJ plugin version: 12.2.0.84584

IntelliJ version:

IntelliJ IDEA 2026.1.1
Build #IU-261.23567.138, built on April 23, 2026
Source revision: a789291cd24df
Licensed to JB Alumni / Pasha Finkelshteyn
You have a perpetual fallback license for this version.
Subscription is active until December 31, 2029.
Runtime version: 25.0.2+10-b329.117 amd64137.0.17-261-b81
VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o.
Toolkit: sun.awt.wl.WLToolkit
Linux 7.0.2-2-cachyos
CachyOS; glibc: 2.43; desktop: KDE (plasmashell 6.6.4)
Exception reporter ID: 2026-04-11_0f2ae742-e8de-4a5d-a94d-806de8a9e7d0
GC: G1 Young Generation, G1 Concurrent GC, G1 Old Generation
Memory: 2048M
Cores: 16
Registry:
  ide.experimental.ui=true
  llm.chat.agent.codex.install.acp.server.version=0.0.40
  trace.state.event.service.url=https://api.jetbrains.cloud/trace-status
  llm.chat.agent.codex.install.codex.version=0.125.0
Non-Bundled Plugins:
  org.jetbrains.junie (261.1218.186)
  JavaScript (261.23567.138)
  com.intellij.java (261.23567.138)
  org.jetbrains.completion.full.line (261.23567.160)
  com.intellij.spring (261.23567.176)
  Subversion (261.23567.176)
  hg4idea (261.23567.28)
  com.intellij.wiremock (261.1.0)
  PerforceDirectPlugin (261.23567.28)
  com.intellij.mcpServer (261.23567.174)
  com.intellij.ml.llm (261.23567.138)
  com.intellij.spring.debugger (261.23567.28)
  org.jetbrains.plugins.node-remote-interpreter (261.23567.143)
  com.intellij.spring.websocket (261.22158.182)
  com.intellij.nativeDebug (261.23567.80)
  com.intellij.reactivestreams (261.22158.182)
  org.sonarlint.idea (12.2.0.84584)
Kotlin: 261.23567.138-IJ
Current Desktop: KDE

  • Programming language you’re coding in: Java
  • Is connected mode used:
    • SonarQube Cloud, SonarQube Server, or SonarQube Community Build? (if one of the latter two, which version?): no/ I don’t know

And a thorough description of the problem / question:

In the following code:


    @SuppressWarnings("unchecked")
    static Map<String, Object> deepMerge(Map<String, Object> base, Map<String, Object> overlay) {
        var out = new LinkedHashMap<>(base);
        for (var e : overlay.entrySet()) {
            var k = e.getKey();
            var v = e.getValue();
            var existing = out.get(k);
            if (existing instanceof Map<?, ?> em && v instanceof Map<?, ?> vm) {
                out.put(k, deepMerge((Map<String, Object>) em, (Map<String, Object>) vm));
            } else if (existing instanceof List<?> el && v instanceof List<?> vl) {
                var combined = new ArrayList<Object>(el);
                combined.addAll(vl);
                out.put(k, combined);
            } else {
                out.put(k, v);
            }
        }
        return out;
    }

all the tests work correctly, however when I apply the suggested quick fix, changing code to

    @SuppressWarnings("unchecked")
    static Map<String, Object> deepMerge(Map<String, Object> base, Map<String, Object> overlay) {
        var out = new LinkedHashMap<>(base);
        for (var e : overlay.entrySet()) {
            var k = e.getKey();
            var v = e.getValue();
            var existing = out.get(k);
            switch (existing) {
                case Map<?, ?> em when v instanceof Map<?, ?> vm -> {
                    out.put(k, deepMerge((Map<String, Object>) em, (Map<String, Object>) vm));
                }
                case List<?> el when v instanceof List<?> vl -> {
                    var combined = new ArrayList<Object>(el);
                    combined.addAll(vl);
                    out.put(k, combined);
                }
                default -> {
                    out.put(k, v);
                }
            }
        }
        return out;
    }

tests start to fail with

java.lang.NullPointerException
	at java.base/java.util.Objects.requireNonNull(Objects.java:220)
	at io.guessit.config.ConfigLoader.deepMerge(ConfigLoader.java:106)
	at io.guessit.config.ConfigLoader.load(ConfigLoader.java:25)
	at io.guessit.Guessit.<init>(Guessit.java:20)
	at io.guessit.Guessit.withOptions(Guessit.java:24)
	at io.guessit.Guessit.parse(Guessit.java:29)
	at io.guessit.parity.YmlParityTest.ymlParity(YmlParityTest.java:45)
	at java.base/java.lang.reflect.Method.invoke(Method.java:565)
	at java.base/java.util.Optional.ifPresent(Optional.java:178)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:186)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:214)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:186)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:186)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:214)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:186)
	at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:197)
	at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:197)
	at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1716)
	at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:803)
	at java.base/java.util.stream.ReferencePipeline$7$1FlatMap.accept(ReferencePipeline.java:293)
	at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:197)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:214)
	at java.base/java.util.Iterator.forEachRemaining(Iterator.java:133)
	at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1939)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:570)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:560)
	at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:153)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:176)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:265)
	at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:632)
	at java.base/java.util.stream.ReferencePipeline$7$1FlatMap.accept(ReferencePipeline.java:293)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:214)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:214)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:214)
	at java.base/java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:1024)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:570)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:560)
	at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:153)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:176)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:265)
	at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:632)
	at java.base/java.util.stream.ReferencePipeline$7$1FlatMap.accept(ReferencePipeline.java:293)
	at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1716)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:570)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:560)
	at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:153)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:176)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:265)
	at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:632)
	at java.base/java.util.stream.ReferencePipeline$7$1FlatMap.accept(ReferencePipeline.java:293)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:214)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:214)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:214)
	at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1716)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:570)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:560)
	at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:153)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:176)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:265)
	at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:632)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1604)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1604)

This happens because this change changes the semantics: switch(existing) throws NPE is existing is null.
Since we start with a (usually) empty out (var out = new LinkedHashMap<>(base);) existing is usually null, so this code will usually fail.

Hi Pasha,

You are right: instanceof is null-safe while a switch expression without a case null arm throws a NullPointerException, so the quickfix was silently changing the semantics of the code.
I have created the bug ticket SONARJAVA-6341 to track the fix.

Thank you for the detailed report and the clear reproducer,

Alban