/*
 * Copyright 2016 The Error Prone Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.errorprone.bugpatterns.nullness;

import static com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode.TEXT_MATCH;

import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.CompilationTestHelper;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** {@link ReturnMissingNullable}Test */
@RunWith(JUnit4.class)
public class ReturnMissingNullableTest {

  @Test
  public void literalNullReturn() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            public class LiteralNullReturnTest {
              public String getMessage(boolean b) {
                if (b) {
                  // BUG: Diagnostic contains: @Nullable
                  return null;
                } else {
                  return "negative";
                }
              }
            }
            """)
        .doTest();
  }

  @Test
  public void parenthesizedLiteralNullReturn() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            public class LiteralNullReturnTest {
              public String getMessage(boolean b) {
                if (b) {
                  // BUG: Diagnostic contains: @Nullable
                  return (null);
                } else {
                  return "negative";
                }
              }
            }
            """)
        .doTest();
  }

  @Test
  public void assignmentOfLiteralNullReturn() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            public class LiteralNullReturnTest {
              String cachedMessage;
              public String getMessage(boolean b) {
                if (b) {
                  // BUG: Diagnostic contains: @Nullable
                  return cachedMessage = null;
                } else {
                  return "negative";
                }
              }
            }
            """)
        .doTest();
  }

  @Test
  public void castLiteralNullReturn() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            public class LiteralNullReturnTest {
              public String getMessage(boolean b) {
                if (b) {
                  // BUG: Diagnostic contains: @Nullable
                  return (String) null;
                } else {
                  return "negative";
                }
              }
            }
            """)
        .doTest();
  }

  @Test
  public void conditionalLiteralNullReturn() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            public class LiteralNullReturnTest {
              public String getMessage(int x) {
                // BUG: Diagnostic contains: @Nullable
                return x >= 0 ? null : "negative";
              }
            }
            """)
        .doTest();
  }

  @Test
  public void parenthesizedConditionalLiteralNullReturn() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            public class LiteralNullReturnTest {
              public String getMessage(int x) {
                // BUG: Diagnostic contains: @Nullable
                return (x >= 0 ? null : "negative");
              }
            }
            """)
        .doTest();
  }

  @Test
  public void switchExpressionTree() {

    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            public class LiteralNullReturnTest {
              public String getMessage(int x) {
                // BUG: Diagnostic contains: @Nullable
                return switch (x) {
                  case 0 -> null;
                  default -> "non-zero";
                };
              }
            }
            """)
        .doTest();
  }

  @Test
  public void switchExpressionTree_negative() {

    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            public class LiteralNullReturnTest {
              public String getMessage(int x) {
                return switch (x) {
                  case 0 -> "zero";
                  default -> "non-zero";
                };
              }
            }
            """)
        .doTest();
  }

  @Test
  public void switchStatement() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            public class LiteralNullReturnTest {
              public String getMessage(int x) {
                switch (x) {
                  case 0:
                    // BUG: Diagnostic contains: @Nullable
                    return null;
                  default:
                    return "non-zero";
                }
              }
            }
            """)
        .doTest();
  }

  @Test
  public void switchStatement_negative() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            public class LiteralNullReturnTest {
              public String getMessage(int x) {
                switch (x) {
                  case 0:
                    return "zero";
                  default:
                    return "non-zero";
                }
              }
            }
            """)
        .doTest();
  }

  @Test
  public void voidReturn() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            abstract class LiteralNullReturnTest {
              Object get() {
                // BUG: Diagnostic contains: @Nullable
                return getVoid();
              }
              abstract Void getVoid();
            }
            """)
        .doTest();
  }

  @Test
  public void subtypeOfVoidReturn() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            abstract class LiteralNullReturnTest {
              Object get(Supplier<? extends Void> s) {
                // BUG: Diagnostic contains: @Nullable
                return s.get();
              }
              interface Supplier<T> {
                T get();
              }
            }
            """)
        .doTest();
  }

  @Test
  public void staticFinalFieldAboveUsage() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            abstract class LiteralNullReturnTest {
              static final Object NULL = null;
              Object get() {
                // BUG: Diagnostic contains: @Nullable
                return NULL;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void staticFinalFieldBelowUsage() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            abstract class LiteralNullReturnTest {
              Object get() {
                // BUG: Diagnostic contains: @Nullable
                return NULL;
              }
              static final Object NULL = null;
            }
            """)
        .doTest();
  }

  @Test
  public void instanceFinalField() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            abstract class LiteralNullReturnTest {
              final Object nullObject = null;
              Object get() {
                // BUG: Diagnostic contains: @Nullable
                return nullObject;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void memberSelectFinalField() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            abstract class LiteralNullReturnTest {
              final Object nullObject = null;
              Object get() {
                // BUG: Diagnostic contains: @Nullable
                return this.nullObject;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void multipleFilesFinalField() {
    createCompilationTestHelper()
        .addSourceLines(
            "Foo.java",
            """
            class Foo {
              final Object nullObject = null;
              Object get() {
                // BUG: Diagnostic contains: @Nullable
                return nullObject;
              }
            }
            """)
        .addSourceLines(
            "Bar.java",
            """
            class Bar {
              final Object nullObject = null;
              Object get() {
                // BUG: Diagnostic contains: @Nullable
                return nullObject;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void voidField() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            abstract class LiteralNullReturnTest {
              Void nullObject;
              Object get() {
                // BUG: Diagnostic contains: @Nullable
                return nullObject;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void typeAnnotatedArrayElement() {
    createRefactoringTestHelper()
        .addInputLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import org.checkerframework.checker.nullness.qual.Nullable;
            public class LiteralNullReturnTest {
              @Nullable String[] getMessage(boolean b, String[] s) {
                return b ? s : null;
              }
            }
            """)
        .addOutputLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import org.checkerframework.checker.nullness.qual.Nullable;
            public class LiteralNullReturnTest {
              @Nullable String @Nullable [] getMessage(boolean b, String[] s) {
                return b ? s : null;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void testTypeAnnotatedMultidimensionalArrayElement() {
    createRefactoringTestHelper()
        .addInputLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import org.checkerframework.checker.nullness.qual.Nullable;
            public class LiteralNullReturnTest {
              String [] @Nullable [] getMessage(boolean b, String[][] s) {
                return b ? s : null;
              }
            }
            """)
        .addOutputLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import org.checkerframework.checker.nullness.qual.Nullable;
            public class LiteralNullReturnTest {
              String @Nullable [] @Nullable [] getMessage(boolean b, String[][] s) {
                return b ? s : null;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void finalLocalVariable() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            abstract class LiteralNullReturnTest {
              Object get() {
                final Object nullObject = null;
                // BUG: Diagnostic contains: @Nullable
                return nullObject;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void effectivelyFinalLocalVariable() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            abstract class LiteralNullReturnTest {
              Object get() {
                Object nullObject = null;
                // BUG: Diagnostic contains: @Nullable
                return nullObject;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void finalLocalVariableComplexTree() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            abstract class LiteralNullReturnTest {
              Object get(boolean b1, boolean b2, Object someObject) {
                final Object nullObject = null;
                // BUG: Diagnostic contains: @Nullable
                return (b1 ? someObject : b2 ? nullObject : "");
              }
            }
            """)
        .doTest();
  }

  @Test
  public void returnXIfXIsNull() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            abstract class LiteralNullReturnTest {
              Object foo(Object o) {
                // BUG: Diagnostic contains: @Nullable
                return (o == null ? o : "");
              }
            }
            """)
        .doTest();
  }

  @Test
  public void returnXUnlessXIsXNotNull() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            abstract class LiteralNullReturnTest {
              Object foo(Object o) {
                // BUG: Diagnostic contains: @Nullable
                return (o != null ? "" : o);
              }
            }
            """)
        .doTest();
  }

  @Test
  public void returnXInsideIfNull() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            abstract class LiteralNullReturnTest {
              Object foo(Object o) {
                if (o == null) {
                  // BUG: Diagnostic contains: @Nullable
                  return o;
                }
                return "";
              }
            }
            """)
        .doTest();
  }

  @Test
  public void returnXInsideElseOfNotNull() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            abstract class LiteralNullReturnTest {
              Object foo(Object o) {
                if (o != null) {
                  return "";
                } else {
                  // BUG: Diagnostic contains: @Nullable
                  return o;
                }
              }
            }
            """)
        .doTest();
  }

  @Test
  public void returnFieldInsideIfNull() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            abstract class LiteralNullReturnTest {
              Object o;
              Object foo() {
                if (o == null) {
                  // BUG: Diagnostic contains: @Nullable
                  return o;
                }
                return "";
              }
            }
            """)
        .doTest();
  }

  @Test
  public void otherVerify() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import static com.google.common.base.Verify.verify;
            class LiteralNullReturnTest {
              public String getMessage(boolean b) {
                verify(b);
                // BUG: Diagnostic contains: @Nullable
                return null;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void orNull() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import com.google.common.base.Optional;
            class LiteralNullReturnTest {
              public String getMessage(Optional<String> m) {
                // BUG: Diagnostic contains: @Nullable
                return m.orNull();
              }
            }
            """)
        .doTest();
  }

  @Test
  public void orElseNull() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import java.util.Optional;
            class LiteralNullReturnTest {
              public String getMessage(Optional<String> m) {
                // BUG: Diagnostic contains: @Nullable
                return m.orElse(null);
              }
            }
            """)
        .doTest();
  }

  @Test
  public void implementsMapButAlwaysThrows() {
    createCompilationTestHelper()
        .addSourceLines(
            "MyMap.java",
            """
            import java.util.Map;
            abstract class MyMap<K, V> implements Map<K, V> {
              @Override
              public V put(K k, V v) {
                throw new UnsupportedOperationException();
              }
            }
            """)
        .doTest();
  }

  @Test
  public void implementsMapButDoNotCall() {
    createCompilationTestHelper()
        .addSourceLines(
            "MyMap.java",
            """
            import com.google.errorprone.annotations.DoNotCall;
            import java.util.Map;
            interface MyMap<K, V> extends Map<K, V> {
              @DoNotCall
              @Override
              V put(K k, V v);
            }
            """)
        .doTest();
  }

  @Test
  public void onlyIfAlreadyInScopeAndItIs() {
    createCompilationTestHelper()
        .setArgs("-XepOpt:Nullness:OnlyIfAnnotationAlreadyInScope=true")
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import org.checkerframework.checker.nullness.qual.Nullable;
            public class LiteralNullReturnTest {
              String getMessage(boolean b) {
                // BUG: Diagnostic contains: @Nullable
                return b ? "" : null;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void onlyStatementIsNullReturnButCannotBeOverridden() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            public final class LiteralNullReturnTest {
              public String getMessage() {
                // BUG: Diagnostic contains: @Nullable
                return null;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void arrayDeclaration() {
    createRefactoringTestHelper()
        .addInputLines(
            "in/com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import javax.annotation.Nullable;
            public class LiteralNullReturnTest {
              public String[] getMessage(boolean b) {
                return b ? null : new String[0];
              }
            }
            """)
        .addOutputLines(
            "out/com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import javax.annotation.Nullable;
            public class LiteralNullReturnTest {
              @Nullable public String[] getMessage(boolean b) {
                return b ? null : new String[0];
              }
            }
            """)
        .doTest();
  }

  @Test
  public void arrayTypeUse() {
    createRefactoringTestHelper()
        .addInputLines(
            "in/com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import org.checkerframework.checker.nullness.qual.Nullable;
            public class LiteralNullReturnTest {
              public String[] getMessage(boolean b) {
                return b ? null : new String[0];
              }
            }
            """)
        .addOutputLines(
            "out/com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import org.checkerframework.checker.nullness.qual.Nullable;
            public class LiteralNullReturnTest {
              public String @Nullable [] getMessage(boolean b) {
                return b ? null : new String[0];
              }
            }
            """)
        .doTest();
  }

  @Test
  public void arrayTypeUseTwoDimensional() {
    createRefactoringTestHelper()
        .addInputLines(
            "in/com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import org.checkerframework.checker.nullness.qual.Nullable;
            public class LiteralNullReturnTest {
              public String[][] getMessage(boolean b, String[][] s) {
                return b ? null : s;
              }
            }
            """)
        .addOutputLines(
            "out/com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import org.checkerframework.checker.nullness.qual.Nullable;
            public class LiteralNullReturnTest {
              public String @Nullable [][] getMessage(boolean b, String[][] s) {
                return b ? null : s;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void alreadyTypeAnnotatedInnerClassMemberSelect() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import org.checkerframework.checker.nullness.qual.Nullable;
            public class LiteralNullReturnTest {
              class Inner {}
              LiteralNullReturnTest.@Nullable Inner getMessage(boolean b, Inner i) {
                return b ? i : null;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void alreadyTypeAnnotatedInnerClassNonMemberSelect() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import org.checkerframework.checker.nullness.qual.Nullable;
            public class LiteralNullReturnTest {
              class Inner {}
              @Nullable Inner getMessage(boolean b, Inner i) {
                return b ? i : null;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void limitation_staticFinalFieldInitializedLater() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            abstract class LiteralNullReturnTest {
              static final Object NULL;
              static {
                NULL = null;
              }
              Object get() {
                return NULL;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void limitation_instanceFinalFieldInitializedLater() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            "package com.google.errorprone.bugpatterns.nullness;",
            "abstract class LiteralNullReturnTest {",
            "  final Object nullObject;",
            "  {",
            "    nullObject = null;", // or, more likely, in a constructor
            "  }",
            "  Object get() {",
            "    return nullObject;",
            "  }",
            "}")
        .doTest();
  }

  @Test
  public void limitation_finalLocalVariableInitializedLater() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            abstract class LiteralNullReturnTest {
              Object get() {
                final Object nullObject;
                nullObject = null;
                return nullObject;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void limitation_returnThisXInsideIfNull() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            abstract class LiteralNullReturnTest {
              Object o;
              Object foo() {
                if (this.o == null) {
                  return this.o;
                }
                return "";
              }
            }
            """)
        .doTest();
  }

  @Test
  public void removeSuppressWarnings_removeNullnessReturnWarning() {
    createRefactoringTestHelper()
        .setArgs("-XepOpt:Nullness:RemoveSuppressWarnings=true")
        .addInputLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            public class LiteralNullReturnTest {
              @SuppressWarnings("nullness:return")
              public String getMessage(boolean b) {
                if (b) {
                  return null;
                } else {
                  return "negative";
                }
              }
            }
            """)
        .addOutputLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import org.jspecify.annotations.Nullable;
            public class LiteralNullReturnTest {

              public @Nullable String getMessage(boolean b) {
                if (b) {
                  return null;
                } else {
                  return "negative";
                }
              }
            }
            """)
        .doTest(TEXT_MATCH);
  }

  @Test
  public void negativeCases_onlyStatementIsNullReturn() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            public class LiteralNullReturnTest {
              public String getMessage() {
                return null;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_typeVariableUsage() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            public class LiteralNullReturnTest {
              public <T> T getMessage(boolean b, T t) {
                return b ? null : t;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_alreadyAnnotated() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import javax.annotation.Nullable;
            public class LiteralNullReturnTest {
              @Nullable
              public String getMessage(boolean b) {
                return b ? "" :null;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_alreadyAnnotatedNullableDecl() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/anno/my/NullableDecl.java",
            """
            package com.google.anno.my;
            public @interface NullableDecl {}
            """)
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import com.google.anno.my.NullableDecl;
            public class LiteralNullReturnTest {
              @NullableDecl
              public String getMessage(boolean b) {
                return b ? "" :null;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_alreadyAnnotatedNullableType() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import org.checkerframework.checker.nullness.compatqual.NullableType;
            public class LiteralNullReturnTest {
              public @NullableType String getMessage(boolean b) {
                return b ? "" : null;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_alreadyTypeAnnotated() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/anno/my/Nullable.java",
            """
            package com.google.anno.my;
            import java.lang.annotation.ElementType;
            import java.lang.annotation.Target;
            @Target({ElementType.TYPE_USE})
            public @interface Nullable {}
            """)
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/TypeAnnoReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            public class TypeAnnoReturnTest {
              public @com.google.anno.my.Nullable String getMessage(boolean b) {
                return b ? "" : null;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_alreadyDeclarationAnnotatedArray() {
    createAggressiveCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import javax.annotation.Nullable;
            public class LiteralNullReturnTest {
              @Nullable
              String[] getMessage(boolean b, String[] s) {
                return b ? s : null;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_alreadyTypeAnnotatedArray() {
    createAggressiveCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import org.checkerframework.checker.nullness.qual.Nullable;
            public class LiteralNullReturnTest {
              String @Nullable [] getMessage(boolean b, String[] s) {
                return b ? s : null;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_alreadyTypeAnnotatedMemberSelect() {
    createAggressiveCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import org.checkerframework.checker.nullness.qual.Nullable;
            public class LiteralNullReturnTest {
              java.lang.@Nullable String getMessage(boolean b) {
                return b ? "" : null;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_checkNotNullNullableInput() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/NullReturnTest.java",
            "package com.google.errorprone.bugpatterns.nullness;",
            "import javax.annotation.Nullable;",
            "public class NullReturnTest {",
            "  @Nullable String message;",
            "  public String getMessage() {",
            "    return checkNotNull(message);",
            "  }",
            // One style of "check not null" method, whose type argument is unannotated, and accepts
            // a @Nullable input.
            "  private static <T> T checkNotNull(@Nullable T obj) {",
            "    if (obj==null) throw new NullPointerException();",
            "    return obj;",
            "  }",
            "}")
        .doTest();
  }

  @Test
  public void negativeCases_nonNullArrayWithNullableElements() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/anno/my/Nullable.java",
            """
            package com.google.anno.my;
            import java.lang.annotation.ElementType;
            import java.lang.annotation.Target;
            @Target({ElementType.TYPE_USE})
            public @interface Nullable {}
            """)
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/NullableParameterTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import com.google.anno.my.Nullable;
            public class NullableParameterTest {
              public String[] apply(@Nullable String[] message) {
                return message;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_nonNullLiteral() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            public class LiteralNullReturnTest {
              public String getMessage() {
                return "hello";
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_nonNullMethod() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/NonNullMethodTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            public class NonNullMethodTest {
              public String getMessage(int x) {
                return String.valueOf(x);
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_nonNullField() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/NonNullFieldTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            public class NonNullFieldTest {
              private String message;
              public String getMessage() {
                return message;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_nonNullParameter() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/NonNullParameterTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            public class NonNullParameterTest {
              public String apply(String message) {
                return message;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_this() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/ThisTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            public class ThisTest {
              private String message;
              public ThisTest setMessage(String message) {
                this.message = message;
                return this;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_capturedLocal() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/CapturedLocalTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            public abstract class CapturedLocalTest {
              public abstract String getMessage();
              public CapturedLocalTest withMessage(final String message) {
                return new CapturedLocalTest() {
                  public String getMessage() {
                    return message;
                  }
                };
              }
            }
            """)
        .doTest();
  }

  /**
   * Makes sure the check never flags methods returning a primitive. Returning null from them is a
   * bug, of course, but we're not trying to find those bugs in this check.
   */
  @Test
  public void negativeCases_primitiveReturnType() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/PrimitiveReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            public class PrimitiveReturnTest {
              public int getCount() {
                return (Integer) null;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_voidMethod() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/VoidMethodTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            public class VoidMethodTest {
              public void run(int iterations) {
                if (iterations <= 0) { return; }
                run(iterations - 1);
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_voidTypedMethod() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/VoidTypeTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            public class VoidTypeTest {
              public Void run(int iterations) {
                if (iterations <= 0) { return null; }
                run(iterations - 1);
                return null;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_nullableReturnInLambda() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/MissingNullableReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import javax.annotation.Nullable;
            public class MissingNullableReturnTest {
              public static final java.util.function.Function<String, String> IDENTITY =
                  (s -> { return s != null ? s : null; });
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_returnLambda() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/MissingNullableReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import javax.annotation.Nullable;
            public class MissingNullableReturnTest {
              public static java.util.function.Function<String, String> identity() {
                return s -> s;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_returnParenthesizedLambda() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/MissingNullableReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import javax.annotation.Nullable;
            public class MissingNullableReturnTest {
              public static java.util.function.Function<String, String> identity() {
                return (s -> s);
              }
            }
            """)
        .doTest();
  }

  // Regression test for b/110812469; verifies that untracked access paths that mix field access
  // and method invocation are "trusted" to yield nonNull values
  @Test
  public void negativeCases_mixedMethodFieldAccessPath() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/MissingNullableReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import javax.annotation.Nonnull;
            public class MissingNullableReturnTest {
              public @Nonnull MyClass test() {
                return ((MyClass) null).myMethod().myField;
              }
              abstract class MyClass {
                abstract MyClass myMethod();
                MyClass myField;
              }
            }
            """)
        .doTest();
  }

  // Regression test for b/113123074
  @Test
  public void negativeCases_delegate() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/MissingNullableReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import javax.annotation.Nullable;
            import java.util.Optional;
            public class MissingNullableReturnTest {
              public String get() {
                return getInternal(true, null);
              }
              private String getInternal(boolean flag, @Nullable Integer i) {
                return "hello";
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_lambda() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/MissingNullableReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import javax.annotation.Nullable;
            import java.util.concurrent.Callable;
            public class MissingNullableReturnTest {
              public Callable<?> get() {
                return () -> { return null; };
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_staticNonFinalField() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            abstract class LiteralNullReturnTest {
              static Object NULL = null;
              Object get() {
                return NULL;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_polyNull() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import org.checkerframework.checker.nullness.qual.PolyNull;
            public class LiteralNullReturnTest {
              public @PolyNull String getMessage(@PolyNull String s) {
                if (s == null) {
                  return null;
                } else {
                  return "negative";
                }
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_unreachableExit() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            class LiteralNullReturnTest {
              public String getMessage() {
                System.exit(1);
                return null;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_unreachableRuntimeExit() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            class LiteralNullReturnTest {
              public String getMessage() {
                Runtime.getRuntime().exit(1);
                return null;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_unreachableRuntimeHalt() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            class LiteralNullReturnTest {
              public String getMessage() {
                Runtime.getRuntime().halt(1);
                return null;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_unreachableAssertFail() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import static org.junit.Assert.fail;
            class LiteralNullReturnTest {
              public String getMessage() {
                fail();
                return null;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_unreachableTestCaseFail() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import static junit.framework.TestCase.fail;
            class LiteralNullReturnTest {
              public String getMessage() {
                fail();
                return null;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_unreachableFailNonCanonicalImport() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/foo/BarTestCase.java",
            """
            package foo;
            import junit.framework.TestCase;
            class BarTestCase extends TestCase {
            }
            """)
        .addSourceLines(
            "com/foo/OtherTestCase.java",
            """
            package foo;
            import static foo.BarTestCase.fail;
            class OtherTestCase {
              public String getMessage() {
                fail();
                return null;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_unreachableThrowExceptionMethod() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import static org.junit.Assert.fail;
            class LiteralNullReturnTest {
              void throwRuntimeException() {}
              public String getMessage() {
                throwRuntimeException();
                return null;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_unreachableCheckFalse() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import static com.google.common.base.Preconditions.checkState;
            class LiteralNullReturnTest {
              public String getMessage() {
                checkState(false);
                return null;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_unreachableVerifyFalse() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import static com.google.common.base.Verify.verify;
            class LiteralNullReturnTest {
              public String getMessage() {
                verify(false);
                return null;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_staticFinalNonNullField() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            abstract class LiteralNullReturnTest {
              static final Object SOMETHING = 1;
              Object get() {
                return SOMETHING;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_returnXIfXIsNotNull() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            abstract class LiteralNullReturnTest {
              Object foo(Object o) {
                return (o != null ? o : "");
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_returnXIfSameSymbolDifferentObjectIsNull() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            abstract class LiteralNullReturnTest {
              Object o;
              Object foo(LiteralNullReturnTest other) {
                return (o == null ? other.o : "");
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_returnXUnlessXIsXNull() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            abstract class LiteralNullReturnTest {
              Object foo(Object o) {
                return (o == null ? "" : o);
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_returnXInsideIfNotNull() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            abstract class LiteralNullReturnTest {
              Object foo(Object o) {
                if (o != null) {
                  return o;
                }
                return "";
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_returnXInsideIfNullElse() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            abstract class LiteralNullReturnTest {
              Object foo(Object o) {
                if (o == null) {
                  return "";
                } else {
                  return o;
                }
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_returnXInsideIfNullButAfterOtherStatement() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            abstract class LiteralNullReturnTest {
              Object foo(Object o) {
                if (o == null) {
                  o = "";
                  return o;
                }
                return "";
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_onlyIfAlreadyInScopeAndItIsNot() {
    createCompilationTestHelper()
        .setArgs("-XepOpt:Nullness:OnlyIfAnnotationAlreadyInScope=true")
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            public class LiteralNullReturnTest {
              String getMessage(boolean b) {
                return b ? "" : null;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_orElseNotNull() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import java.util.Optional;
            class LiteralNullReturnTest {
              public String getMessage(Optional<String> m) {
                return m.orElse("");
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_doesNotImplementMap() {
    createCompilationTestHelper()
        .addSourceLines(
            "NotMap.java",
            """
            interface NotMap<K, V> {
              String get(Object o);
              V replace(K k, V v);
            }
            """)
        .addSourceLines(
            "MyMap.java",
            """
            interface MyMap extends NotMap<Integer, Double> {
              @Override String get(Object o);
              @Override Double replace(Integer k, Double v);
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_suppressionForReturnTreeBased() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            import java.util.Optional;
            class LiteralNullReturnTest {
              @SuppressWarnings("ReturnMissingNullable")
              public String getMessage(Optional<String> m) {
                return m.orElse(null);
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_suppressionForMethodTreeBased() {
    createCompilationTestHelper()
        .addSourceLines(
            "NotMap.java",
            """
            interface NotMap {
              Integer get(String o);
            }
            """)
        .addSourceLines(
            "MyMap.java",
            """
            import java.util.Map;
            interface MyMap<K, V> extends Map<K, V>, NotMap {
              @SuppressWarnings("ReturnMissingNullable")
              @Override V get(Object o);
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_suppressionAboveMethodLevel() {
    createCompilationTestHelper()
        .addSourceLines(
            "NotMap.java",
            """
            interface NotMap {
              Integer get(String o);
            }
            """)
        .addSourceLines(
            "MyMap.java",
            """
            import java.util.Map;
            @SuppressWarnings("ReturnMissingNullable")
            interface MyMap<K, V> extends Map<K, V>, NotMap {
              @Override V get(Object o);
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_implementsMapButRunningInConservativeMode() {
    createCompilationTestHelper()
        .addSourceLines(
            "MyMap.java",
            """
            import java.util.Map;
            interface MyMap<K, V> extends Map<K, V> {
              @Override V get(Object o);
            }
            """)
        .doTest();
  }

  @Test
  public void returnSameSymbolDifferentObjectInsideIfNull() {
    createCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            abstract class LiteralNullReturnTest {
              Object o;
              Object foo(LiteralNullReturnTest other) {
                if (o == null) {
                  return other.o;
                }
                return "";
              }
            }
            """)
        .doTest();
  }

  @Test
  public void suggestNonJsr305Nullable() {
    createRefactoringTestHelper()
        .addInputLines(
            "in/Test.java",
            """
            class T {
              @Nullable private final Object obj1 = null;
              private final Object method(boolean b) { return b ? null : 0; }
              @interface Nullable {}
            }
            """)
        .addOutputLines(
            "out/Test.java",
            """
            class T {
              @Nullable private final Object obj1 = null;
              @Nullable private final Object method(boolean b) { return b ? null : 0; }
              @interface Nullable {}
            }
            """)
        .doTest();
  }

  @Test
  public void nonAnnotationNullable() {
    createRefactoringTestHelper()
        .addInputLines(
            "in/Test.java",
            """
            class T {
              private final Object method(boolean b) { return b ? null : 0; }
              class Nullable {}
            }
            """)
        .addOutputLines(
            "out/Test.java",
            """
class T {
  @org.jspecify.annotations.Nullable private final Object method(boolean b) { return b ? null : 0; }
  class Nullable {}
}
""")
        .doTest();
  }

  @Test
  public void multipleNullReturns() {
    createRefactoringTestHelper()
        .addInputLines(
            "in/Test.java",
            """
            class T {
              private final Object method(boolean b) {
                if (b) {
                  return null;
                } else {
                  return null;
                }
              }
            }
            """)
        .addOutputLines(
            "out/Test.java",
            """
            import org.jspecify.annotations.Nullable;
            class T {
              private final @Nullable Object method(boolean b) {
                if (b) {
                  return null;
                } else {
                  return null;
                }
              }
            }
            """)
        .doTest();
  }

  @Test
  public void memberSelectReturnType() {
    createRefactoringTestHelper()
        .addInputLines(
            "in/Test.java",
            """
            import org.checkerframework.checker.nullness.qual.Nullable;
            class T {
              java.lang.Object method(boolean b) {
                if (b) {
                  return null;
                } else {
                  return null;
                }
              }
            }
            """)
        .addOutputLines(
            "out/Test.java",
            """
            import org.checkerframework.checker.nullness.qual.Nullable;
            class T {
              java.lang.@Nullable Object method(boolean b) {
                if (b) {
                  return null;
                } else {
                  return null;
                }
              }
            }
            """)
        .doTest();
  }

  @Test
  public void annotationInsertedAfterModifiers() {
    createRefactoringTestHelper()
        .addInputLines(
            "in/Test.java",
            """
            import org.checkerframework.checker.nullness.qual.Nullable;
            class T {
              final Object method(boolean b) {
                if (b) {
                  return null;
                } else {
                  return null;
                }
              }
            }
            """)
        .addOutputLines(
            "out/Test.java",
            """
            import org.checkerframework.checker.nullness.qual.Nullable;
            class T {
              final @Nullable Object method(boolean b) {
                if (b) {
                  return null;
                } else {
                  return null;
                }
              }
            }
            """)
        .doTest(TEXT_MATCH);
  }

  @Test
  public void parameterizedMemberSelectReturnType() {
    createRefactoringTestHelper()
        .addInputLines(
            "in/Test.java",
            """
            import org.checkerframework.checker.nullness.qual.Nullable;
            class T {
              java.util.List<java.lang.Object> method(boolean b) {
                if (b) {
                  return null;
                } else {
                  return null;
                }
              }
            }
            """)
        .addOutputLines(
            "out/Test.java",
            """
            import org.checkerframework.checker.nullness.qual.Nullable;
            class T {
              java.util.@Nullable List<java.lang.Object> method(boolean b) {
                if (b) {
                  return null;
                } else {
                  return null;
                }
              }
            }
            """)
        .doTest();
  }

  @Test
  public void annotatedMemberSelectReturnType() {
    createRefactoringTestHelper()
        .addInputLines(
            "in/Test.java",
            """
            import org.checkerframework.checker.initialization.qual.UnderInitialization;
            import org.checkerframework.checker.nullness.qual.Nullable;
            class T {
              java.lang.@UnderInitialization Object method(boolean b) {
                if (b) {
                  return null;
                } else {
                  return null;
                }
              }
            }
            """)
        .addOutputLines(
            "out/Test.java",
            """
            import org.checkerframework.checker.initialization.qual.UnderInitialization;
            import org.checkerframework.checker.nullness.qual.Nullable;
            class T {
              java.lang.@Nullable @UnderInitialization Object method(boolean b) {
                if (b) {
                  return null;
                } else {
                  return null;
                }
              }
            }
            """)
        .doTest();
  }

  @Test
  public void annotationNotNamedNullable() {
    createRefactoringTestHelper()
        .setArgs("-XepOpt:Nullness:DefaultNullnessAnnotation=javax.annotation.CheckForNull")
        .addInputLines(
            "in/Test.java",
            """
            class T {
              Object method(boolean b) {
                if (b) {
                  return null;
                } else {
                  return null;
                }
              }
            }
            """)
        .addOutputLines(
            "out/Test.java",
            """
            import javax.annotation.CheckForNull;
            class T {
              @CheckForNull Object method(boolean b) {
                if (b) {
                  return null;
                } else {
                  return null;
                }
              }
            }
            """)
        .doTest();
  }

  @Test
  public void aggressive_onlyStatementIsNullReturn() {
    createAggressiveCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            public class LiteralNullReturnTest {
              public String getMessage() {
                // BUG: Diagnostic contains: @Nullable
                return null;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void aggressive_typeVariableUsage() {
    createAggressiveCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            public class LiteralNullReturnTest {
              public <T> T getMessage(boolean b, T t) {
                // BUG: Diagnostic contains: @Nullable
                return b ? null : t;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void aggressive_voidTypedMethod() {
    createAggressiveCompilationTestHelper()
        .addSourceLines(
            "com/google/errorprone/bugpatterns/nullness/VoidTypeTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            public class VoidTypeTest {
              public Void run(int iterations) {
                // BUG: Diagnostic contains: @Nullable
                if (iterations <= 0) { return null; }
                run(iterations - 1);
                // BUG: Diagnostic contains: @Nullable
                return null;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void negativeCases_doesNotRemoveNecessarySuppressWarnings() {
    createRefactoringTestHelper()
        .setArgs("-XepOpt:Nullness:RemoveSuppressWarnings=true")
        .addInputLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            public class LiteralNullReturnTest {
              @SuppressWarnings("nullness:argument")
              public String getMessage(boolean b) {
                if (b) {
                  doSomethingElse(null);
                 return "negative";
                } else {
                  return "negative";
                }
              }
              public void doSomethingElse(Object c) {
                  return;
              }
            }
            """)
        .addOutputLines(
            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
            """
            package com.google.errorprone.bugpatterns.nullness;
            public class LiteralNullReturnTest {
              @SuppressWarnings("nullness:argument")
              public String getMessage(boolean b) {
                if (b) {
                  doSomethingElse(null);
                 return "negative";
                } else {
                  return "negative";
                }
              }
              public void doSomethingElse(Object c) {
                  return;
              }
            }
            """)
        .doTest(TEXT_MATCH);
  }

  @Test
  public void aggressive_implementsMap() {
    createAggressiveCompilationTestHelper()
        .addSourceLines(
            "NotMap.java",
            """
            interface NotMap {
              Integer get(String o);
            }
            """)
        .addSourceLines(
            "MyMap.java",
            """
            import java.util.Map;
            interface MyMap<K, V> extends Map<K, V>, NotMap {
              // BUG: Diagnostic contains: @Nullable
              @Override V get(Object o);
              // BUG: Diagnostic contains: @Nullable
              @Override V replace(K k, V v);
              @Override boolean replace(K k, V expect, V update);
              @Override Integer get(String o);
            }
            """)
        .doTest();
  }

  private CompilationTestHelper createCompilationTestHelper() {
    return CompilationTestHelper.newInstance(ReturnMissingNullable.class, getClass());
  }

  private CompilationTestHelper createAggressiveCompilationTestHelper() {
    return createCompilationTestHelper().setArgs("-XepOpt:Nullness:Conservative=false");
  }

  private BugCheckerRefactoringTestHelper createRefactoringTestHelper() {
    return BugCheckerRefactoringTestHelper.newInstance(ReturnMissingNullable.class, getClass());
  }
}
