/**
 * @name Inherited equals() in subclass with added fields
 * @description If a class overrides 'Object.equals', and a subclass defines additional fields
 *              to those it inherits but does not re-define 'equals', the results of 'equals'
 *              may be wrong.
 * @kind problem
 * @problem.severity warning
 * @precision low
 * @id java/inherited-equals-with-added-fields
 * @tags reliability
 *       correctness
 */

import java

predicate okForEquals(Class c) {
  c.getAMethod() instanceof EqualsMethod
  or
  (
    not exists(c.getAField()) and
    okForEquals(c.getASupertype())
  )
}

/** Holds if method `em` implements a reference equality check. */
predicate checksReferenceEquality(EqualsMethod em) {
  // `java.lang.Object.equals` is the prototypical reference equality implementation.
  em.getDeclaringType() instanceof TypeObject
  or
  // Custom reference equality implementations observed in open-source projects.
  exists(SingletonBlock blk, EQExpr eq |
    blk = em.getBody() and
    eq.getAnOperand() instanceof ThisAccess and
    eq.getAnOperand().(VarAccess).getVariable() = em.getParameter(0) and
    (
      // `{ return (ojb==this); }`
      eq = blk.getStmt().(ReturnStmt).getResult().getProperExpr()
      or
      // `{ if (ojb==this) return true; else return false; }`
      exists(IfStmt ifStmt | ifStmt = blk.getStmt() |
        eq = ifStmt.getCondition().getProperExpr() and
        ifStmt.getThen().(ReturnStmt).getResult().(BooleanLiteral).getBooleanValue() = true and
        ifStmt.getElse().(ReturnStmt).getResult().(BooleanLiteral).getBooleanValue() = false
      )
    )
  )
  or
  // Check whether `em` delegates to another method checking reference equality.
  // More precisely, we check whether the body of `em` is of the form `return super.equals(o);`,
  // where `o` is the (only) parameter of `em`, and the invoked method is a reference equality check.
  exists(SuperMethodAccess sup |
    sup = em.getBody().(SingletonBlock).getStmt().(ReturnStmt).getResult() and
    sup.getArgument(0) = em.getParameter(0).getAnAccess() and
    checksReferenceEquality(sup.getCallee())
  )
}

predicate unsupportedEquals(EqualsMethod em) {
  em.getBody().(SingletonBlock).getStmt() instanceof ThrowStmt
}

predicate overridesDelegateEquals(EqualsMethod em, Class c) {
  exists(Method override, Method delegate |
    // The `equals` method (declared in the supertype) contains
    // a call to a `delegate` method on the same type ...
    em.calls(delegate) and
    delegate.getDeclaringType() = em.getDeclaringType() and
    // ... and the `delegate` method is overridden in the subtype `c`
    // by a method that reads at least one added field.
    override.getDeclaringType() = c and
    exists(Method overridden | overridden.getSourceDeclaration() = delegate |
      override.overrides(overridden)
    ) and
    readsOwnField(override)
  )
}

predicate readsOwnField(Method m) { m.reads(m.getDeclaringType().getAField()) }

from Class c, InstanceField f, EqualsMethod em
where
  not exists(EqualsMethod m | m.getDeclaringType() = c) and
  okForEquals(c.getASupertype()) and
  exists(Method m | m.getSourceDeclaration() = em | c.inherits(m)) and
  exists(em.getBody()) and
  not checksReferenceEquality(em) and
  not unsupportedEquals(em) and
  not overridesDelegateEquals(em, c) and
  f.getDeclaringType() = c and
  c.fromSource() and
  not c instanceof EnumType and
  not f.isFinal()
select c, c.getName() + " inherits $@ but adds $@.", em.getSourceDeclaration(),
  em.getDeclaringType().getName() + "." + em.getName(), f, "the field " + f.getName()
