/**
 * @name Incorrect lazy initialization of a static field
 * @description Initializing a static field without synchronization can be problematic
 *              in a multi-threaded context.
 * @kind problem
 * @problem.severity warning
 * @precision low
 * @id java/lazy-initialization
 * @tags reliability
 *       correctness
 *       concurrency
 *       external/cwe/cwe-543
 *       external/cwe/cwe-609
 */

import java

/** A comparison (using `==`) with `null`. */
class NullEQExpr extends EQExpr { NullEQExpr() { exists(NullLiteral l | l.getParent() = this) } }

/** An assignment to a static field. */
class StaticFieldInit extends AssignExpr {
  StaticFieldInit() { exists(Field f | f.isStatic() | f.getAnAccess() = this.getDest()) }

  Field getField() { result.getAnAccess() = this.getDest() }

  IfStmt getAnEnclosingNullCheck() {
    result.getThen().getAChild*() = this.getEnclosingStmt() and
    result.getCondition().(NullEQExpr).getAChildExpr() = getField().getAnAccess()
  }

  IfStmt getNearestNullCheck() {
    result = getAnEnclosingNullCheck() and
    not result.getAChild+() = getAnEnclosingNullCheck()
  }
}

/** A field that is a candidate for a "lock object". */
class LockObjectField extends Field {
  LockObjectField() {
    this.isStatic() and
    forex(Callable init | init = this.getAnAssignedValue().getEnclosingCallable() |
      init instanceof StaticInitializer
    )
  }
}

/** A synchronized statement on a class literal. */
class ValidSynchStmt extends Stmt {
  ValidSynchStmt() {
    // It's OK to lock the enclosing class.
    this.(SynchronizedStmt).getExpr().(TypeLiteral).getTypeName().getType() = this
          .getEnclosingCallable()
          .getDeclaringType()
    or
    // It's OK to lock on a "lock object field".
    this.(SynchronizedStmt).getExpr().(FieldRead).getField() instanceof LockObjectField
    or
    // Locking via `ReentrantLock` lock object instead of synchronized statement.
    exists(TryStmt try, LockObjectField lockField |
      this = try.getBlock() and
      lockField.getType().(RefType).hasQualifiedName("java.util.concurrent.locks", "ReentrantLock") and
      exists(MethodAccess lockAction |
        lockAction.getQualifier() = lockField.getAnAccess() and
        lockAction.getMethod().getName() = "lock" and
        dominates(lockAction, this)
      ) and
      exists(MethodAccess unlockAction |
        unlockAction.getQualifier() = lockField.getAnAccess() and
        unlockAction.getMethod().getName() = "unlock" and
        postDominates(unlockAction, this)
      )
    )
  }
}

/** A static method. */
class StaticMethod extends Method {
  StaticMethod() { this.isStatic() }

  predicate bodyIsSynchronized() {
    this.isSynchronized() or
    this.getBody().getAChild() instanceof ValidSynchStmt
  }
}

from StaticMethod method, IfStmt i, StaticFieldInit init, string message
where
  i = init.getNearestNullCheck() and
  method = i.getEnclosingCallable() and
  not method.bodyIsSynchronized() and
  not method instanceof StaticInitializer and
  // There must be an unsynchronized read.
  exists(IfStmt unsyncNullCheck | unsyncNullCheck = init.getAnEnclosingNullCheck() |
    not unsyncNullCheck.getParent+() instanceof ValidSynchStmt
  ) and
  if (i.getParent+() instanceof ValidSynchStmt)
  then (
    not init.getField().isVolatile() and
    message = "The field must be volatile."
  ) else (
    if (i.getParent+() instanceof SynchronizedStmt)
    then (
      message = "Bad synchronization."
    ) else (
      message = "Missing synchronization."
    )
  )
select init, "Incorrect lazy initialization of static field $@: " + message, init.getField() as f,
  f.getName()
