import java
import semmle.code.java.deadcode.DeadEnumConstant
import semmle.code.java.deadcode.DeadCodeCustomizations
import semmle.code.java.deadcode.DeadField
import semmle.code.java.deadcode.EntryPoints

/**
 * Holds if the given callable has any liveness causes.
 */
predicate isLive(Callable c) {
  exists(EntryPoint e | c = e.getALiveCallable())
  or
  exists(Callable live | isLive(live) | live = possibleLivenessCause(c))
}

/**
 * Compute a list of callables such that the liveness of any result
 * would imply the liveness of `c`.
 */
Callable possibleLivenessCause(Callable c, string reason) {
  c.(Method).overridesOrInstantiates(result.(Method)) and
  reason = "is overridden or instantiated by"
  or
  result.calls(c) and reason = "calls"
  or
  result.callsConstructor(c.(Constructor)) and reason = "calls constructor"
  or
  exists(ClassInstanceExpr e | e.getEnclosingCallable() = result |
    e.getConstructor() = c and reason = "constructs"
  )
  or
  c = result.getSourceDeclaration() and c != result and reason = "instantiates"
  or
  c.hasName("<clinit>") and
  reason = "class initialization" and
  exists(RefType clintedType | c = clintedType.getASupertype*().getACallable() |
    result.getDeclaringType() = clintedType or
    result.getAnAccessedField().getDeclaringType() = clintedType
  )
  or
  c.hasName("<obinit>") and
  reason = "object initialization" and
  result = c.getDeclaringType().getAConstructor()
}

Callable possibleLivenessCause(Callable c) { result = possibleLivenessCause(c, _) }

/**
 * A dead root is not live, and has no liveness causes.
 *
 * Dead roots are reported for dead classes and dead methods to help verify that classes and
 * methods with dependencies are actually dead. A dead class or method may have no dead roots, if
 * it is involved in a dead code cycle.
 */
class DeadRoot extends Callable {
  DeadRoot() {
    not isLive(this) and
    // Not a dead root if there exists at least one liveness cause that is not this method.
    not exists(Callable c | c = possibleLivenessCause(this) and c != this)
  }
}

/**
 * For a dead callable, we identify all the possible dead roots.
 *
 * For dead callables which are either part of dead code cycles, or are only depended upon by
 * callables in dead cycles, there will be no dead roots.
 */
DeadRoot getADeadRoot(Callable c) {
  not isLive(c) and
  (
    result = c or
    result = getADeadRoot(possibleLivenessCause(c))
  )
}

/**
 * A constructor that is only declared to override the public accessibility of
 * the default constructor generated by the compiler.
 */
class SuppressedConstructor extends Constructor {
  SuppressedConstructor() {
    // Must be private or protected to suppress it.
    (
      isPrivate()
      or
      // A protected, suppressed constructor only makes sense in a non-abstract class.
      isProtected() and not getDeclaringType().isAbstract()
    ) and
    // Must be no-arg in order to replace the compiler generated default constructor.
    getNumberOfParameters() = 0 and
    // Not the compiler-generated constructor itself.
    not isDefaultConstructor() and
    // Verify that there is only one statement, which is the `super()` call. This exists
    // even for empty constructors.
    getBody().(Block).getNumStmt() = 1 and
    getBody().(Block).getAStmt().(SuperConstructorInvocationStmt).getNumArgument() = 0 and
    // A constructor that is called is not acting to suppress the default constructor. We permit
    // calls from suppressed and default constructors - in both cases, they can only come from
    // sub-class constructors.
    not exists(Call c |
      c.getCallee().getSourceDeclaration() = this and
      not c.getCaller() instanceof SuppressedConstructor and
      not c.getCaller().(Constructor).isDefaultConstructor()
    ) and
    // If other constructors are declared, then no compiler-generated constructor is added, so
    // this constructor is not acting to suppress the default compiler-generated constructor.
    not exists(Constructor other | other = getDeclaringType().getAConstructor() and other != this)
  }
}

/**
 * A namespace class is one that is used purely as a container for static classes, methods and fields.
 */
class NamespaceClass extends RefType {
  NamespaceClass() {
    fromSource() and
    // All members, apart from the default constructor and, if present, a "suppressed" constructor
    // must be static. There must be at least one member apart from the permitted constructors.
    forex(Member m |
      m.getDeclaringType() = this and
      not m.(Constructor).isDefaultConstructor() and
      not m instanceof SuppressedConstructor
    |
      m.isStatic()
    ) and
    // Must only extend other namespace classes, or `Object`.
    forall(RefType r | r = getASupertype() | r instanceof TypeObject or r instanceof NamespaceClass)
  }
}

/**
 * A `ClassOrInterface` type that is from source.
 *
 * This represents the set of classes and interfaces for which we will determine liveness. Each
 * `SourceClassOrInterfacce` will either be a `LiveClass` or `DeadClass`.
 */
library class SourceClassOrInterface extends ClassOrInterface {
  SourceClassOrInterface() { this.fromSource() }
}

/**
 * A source class or interface is live if it fulfills one of the following criteria:
 *
 *  - It, or a sub-class, contains a live callable.
 *  - It contains a live field.
 *  - It is a namespace class and it contains a live nested class.
 *  - It is a whitelisted class.
 *  - It is an annotation class - these are assumed to be always live.
 *  - It is an anonymous class - these classes are dead if and only if the outer method is dead.
 */
class LiveClass extends SourceClassOrInterface {
  LiveClass() {
    exists(Callable c | c.getDeclaringType().getASupertype*().getSourceDeclaration() = this |
      isLive(c)
    )
    or
    exists(LiveField f | f.getDeclaringType() = this |
      // A `serialVersionUID` field is considered to be a live field, but is
      // not be enough to be make this class live.
      not f instanceof SerialVersionUIDField
    )
    or
    // If this is a namespace class, it is live if there is at least one live nested class.
    // The definition of `NamespaceClass` is such, that the nested classes must all be static.
    // Static methods are handled above.
    this instanceof NamespaceClass and
    exists(NestedType r | r.getEnclosingType() = this | r instanceof LiveClass)
    or
    // An annotation on the class is reflectively accessed.
    exists(ReflectiveAnnotationAccess reflectiveAnnotationAccess |
      this = reflectiveAnnotationAccess.getInferredClassType() and
      isLive(reflectiveAnnotationAccess.getEnclosingCallable())
    )
    or
    this instanceof AnonymousClass
    or
    this instanceof WhitelistedLiveClass
    or
    this instanceof AnnotationType
  }
}

/**
 * A class is dead if it is from source, and contains no live callables and no live fields. Nested
 * classes make the outer class live if and only if the outer class is considered to be present for
 * namespace purposes only, and the nested class is static.
 *
 * Nested instance classes require no special handling. If the nested instance class accesses fields
 * or methods on the outer class, then these will already be marked as live fields and methods. If
 * it accesses no methods or fields from the outer, then the nested class can be made static, and
 * moved into another file.
 */
class DeadClass extends SourceClassOrInterface {
  DeadClass() { not this instanceof LiveClass }

  /**
   * Identify all the "dead" roots of this dead class.
   */
  DeadRoot getADeadRoot() { result = getADeadRoot(getACallable()) }

  /**
   * Holds if this dead class is only used within the class itself.
   */
  predicate isUnusedOutsideClass() {
    // Accessed externally if any callable in the class has a possible liveness cause outside the
    // class. Only one step is required.
    not exists(Callable c |
      c = possibleLivenessCause(getACallable()) and
      not c = getACallable()
    )
  }
}

/**
 * A class which is dead, but should be considered as live.
 *
 * This should be used for cases where the class is dead, but should not be removed - for example,
 * because it may be useful in the future. If a class is marked as dead when it is live, the
 * callable or field that makes the class live should be marked as an entry point by either
 * extending `CallableEntryPoint` or `ReflectivelyReadField`, instead of whitelisting the class.
 */
abstract class WhitelistedLiveClass extends RefType { }

/**
 * A method is dead if it is from source, has no liveness causes, is not a compiler generated
 * method and is not a dead method with a purpose, such as a constructor designed to suppress the
 * default constructor.
 */
class DeadMethod extends Callable {
  DeadMethod() {
    fromSource() and
    not isLive(this) and
    not this.(Constructor).isDefaultConstructor() and
    // Ignore `SuppressedConstructor`s in `NamespaceClass`es. There is no reason to use a suppressed
    // constructor in other cases.
    not (
      this instanceof SuppressedConstructor and this.getDeclaringType() instanceof NamespaceClass
    ) and
    not (
      this.(Method).isAbstract() and
      exists(Method m | m.overridesOrInstantiates+(this.(Method)) | isLive(m))
    ) and
    // A getter or setter associated with a live JPA field.
    //
    // These getters and setters are often generated in an ad-hoc way by the developer, which leads to
    // methods that are theoretically dead, but uninteresting. We therefore ignore them, so long as
    // they are "simple".
    not exists(JPAReadField readField | this.getDeclaringType() = readField.getDeclaringType() |
      this.(GetterMethod).getField() = readField or
      this.(SetterMethod).getField() = readField
    )
  }

  /**
   * Holds if this dead method is already within the scope of a dead class.
   */
  predicate isInDeadScope() {
    // We do not need to consider whitelisting because whitelisted classes should not have dead
    // methods reported.
    this.getDeclaringType() instanceof DeadClass
  }

  /**
   * Identify all the "dead" roots of this dead callable.
   */
  DeadRoot getADeadRoot() { result = getADeadRoot(this) }
}

class RootdefCallable extends Callable {
  RootdefCallable() {
    this.fromSource() and
    not this.(Method).overridesOrInstantiates(_)
  }

  Parameter unusedParameter() {
    exists(int i | result = this.getParameter(i) |
      not exists(result.getAnAccess()) and
      not overrideAccess(this, i)
    )
  }

  predicate whitelisted() {
    // Main methods must have a `String[]` argument.
    this instanceof MainMethod
    or
    // Premain methods must have certain arguments.
    this instanceof PreMainMethod
    or
    // Abstract, native and interface methods obviously won't access their own
    // parameters, so don't flag unless we can see an overriding method with
    // a body that also doesn't.
    not hasUsefulBody(this) and
    not exists(Method m | hasUsefulBody(m) | m.overridesOrInstantiates+(this))
    or
    // Methods that are the target of a member reference need to implement
    // the exact signature of the resulting functional interface.
    exists(MemberRefExpr mre | mre.getReferencedCallable() = this)
    or
    this.getAnAnnotation() instanceof OverrideAnnotation
  }
}

pragma[nomagic]
private predicate overrideAccess(Callable c, int i) {
  exists(Method m | m.overridesOrInstantiates+(c) | exists(m.getParameter(i).getAnAccess()))
}

/**
 * A predicate to find non-trivial method implementations.
 * (A trivial implementation is either abstract, or it just
 * throws `UnsupportedOperationException` or similar.)
 */
predicate hasUsefulBody(Callable c) {
  exists(c.getBody()) and
  not c.getBody().getAChild() instanceof ThrowStmt
}
