/**
 * @name Comparison between inconvertible types
 * @description An equality comparison between two values that cannot be meaningfully converted to
 *              the same type will always yield 'false', and an inequality comparison will always
 *              yield 'true'.
 * @kind problem
 * @problem.severity warning
 * @id js/comparison-between-incompatible-types
 * @tags reliability
 *       correctness
 *       external/cwe/cwe-570
 *       external/cwe/cwe-571
 * @precision high
 */

import javascript
private import semmle.javascript.dataflow.InferredTypes

/**
 * Holds if `left` and `right` are the left and right operands, respectively, of `nd`, which is
 * a comparison.
 *
 * Besides the usual comparison operators, `switch` statements are also considered to be comparisons,
 * with the switched-on expression being the right operand and all case labels the left operands.
 */
predicate comparisonOperands(ASTNode nd, Expr left, Expr right) {
  exists (Comparison cmp | cmp = nd | left = cmp.getLeftOperand() and right = cmp.getRightOperand()) or
  exists (SwitchStmt switch | switch = nd | right = switch.getExpr() and left = switch.getACase().getExpr())
}

/**
 * Holds if `av` may have a `toString` or `valueOf` method.
 */
predicate hasImplicitConversionMethod(DefiniteAbstractValue av) {
  // look for assignments to `toString` or `valueOf` on `av` or its prototypes
  exists (AnalyzedPropertyWrite apw, string p | p = "toString" or p = "valueOf" |
    apw.writes(av.getAPrototype*(), p, _)
  )
}

/**
 * Gets a type of `operand`, which is an operand of the strict equality test `eq`.
 */
InferredType strictEqualityOperandType(ASTNode eq, DataFlow::AnalyzedNode operand) {
  // strict equality tests do no conversion at all
  operand.asExpr() = eq.(StrictEqualityTest).getAChildExpr() and result = operand.getAType() or

  // switch behaves like a strict equality test
  exists (SwitchStmt switch | switch = eq |
    (operand.asExpr() = switch.getExpr() or operand.asExpr() = switch.getACase().getExpr()) and
    result = operand.getAType()
  )
}

/**
 * Holds if `operand` is an operand of the non-strict equality test or relational
 * operator `parent`, and may have a `toString` or `valueOf` method.
 */
predicate implicitlyConvertedOperand(ASTNode parent, DataFlow::AnalyzedNode operand) {
  (parent instanceof NonStrictEqualityTest or parent instanceof RelationalComparison) and
  operand.asExpr() = parent.getAChildExpr() and
  hasImplicitConversionMethod(operand.getAValue())
}

/**
 * Gets a type of `operand`, which is an operand of the non-strict equality test or
 * relational operator `parent`.
 */
InferredType nonStrictOperandType(ASTNode parent, DataFlow::AnalyzedNode operand) {
  // non-strict equality tests perform conversions
  operand.asExpr() = parent.(NonStrictEqualityTest).getAChildExpr() and
  exists (InferredType tp | tp = operand.getAValue().getType() |
    result = tp
    or
    // Booleans are converted to numbers
    tp = TTBoolean() and result = TTNumber()
    or
    // and so are strings
    tp = TTString() and
    // exclude cases where the string is guaranteed to coerce to NaN
    not exists(ConstantString l | l = operand.asExpr() | not exists(l.getStringValue().toFloat())) and
    result = TTNumber()
    or
    // Dates are converted to strings (which are guaranteed to coerce to NaN)
    tp = TTDate() and result = TTString()
    or
    // other objects are converted to strings, numbers or Booleans
    (tp = TTObject() or tp = TTFunction() or tp = TTClass() or tp = TTRegExp()) and
    (result = TTBoolean() or result = TTNumber() or result = TTString())
    or
    // `undefined` and `null` are equated
    tp = TTUndefined() and result = TTNull()
  )
  or
  // relational operators convert their operands to numbers or strings
  operand.asExpr() = parent.(RelationalComparison).getAChildExpr() and
  exists (AbstractValue v | v = operand.getAValue() |
    result = v.getType()
    or
    v.isCoercibleToNumber() and result = TTNumber()
  )
}

/**
 * Gets a type that `operand`, which is an operand of comparison `parent`,
 * could be converted to at runtime.
 */
InferredType convertedOperandType(ASTNode parent, DataFlow::AnalyzedNode operand) {
  result = strictEqualityOperandType(parent, operand)
  or
  // if `operand` might have `toString`/`valueOf`, just assume it could
  // convert to any type at all
  implicitlyConvertedOperand(parent, operand)
  or
  result = nonStrictOperandType(parent, operand)
}

/**
 * Holds if `left` and `right` are operands of comparison `cmp` having types
 * `leftTypes` and `rightTypes`, respectively, but there is no
 * common type they coerce to.
 */
predicate isHeterogeneousComparison(ASTNode cmp, DataFlow::AnalyzedNode left, DataFlow::AnalyzedNode right,
                                    string leftTypes, string rightTypes) {
  comparisonOperands(cmp, left.asExpr(), right.asExpr()) and
  not convertedOperandType(cmp, left) = convertedOperandType(cmp, right) and
  leftTypes = left.ppTypes() and rightTypes = right.ppTypes()
}

/**
 * Holds if `name` is a variable name that programmers consider a keyword.
 */
predicate isPseudoKeyword(string name) {
  name = "Infinity" or
  name = "NaN" or
  name = "undefined"
}

/**
 * Gets a user friendly description of `e`, if such a description exists.
 */
string getDescription(VarAccess e) {
  exists (string name | name = e.getName() |
    if isPseudoKeyword(name) then
      result = "'" + name + "'"
    else
      result  = "variable '" + name + "'"
  )
}

/**
 * Gets a user friendly description of `e`, `default` is the result if no such description exists.
 */
bindingset[default]
string getDescription(Expr e, string default) {
  if exists (getDescription(e)) then
    result = getDescription(e)
  else
    result = default
}

/**
 * Gets the simpler message of `message1` and `message2` guided by the corresponding `complexity1` and `complexity2`.
 */
bindingset[message1, message2, complexity1, complexity2]
string getTypeDescription(string message1, string message2, int complexity1, int complexity2) {
  if complexity1 > 4 and complexity2 <= 2 then
    result = message2
  else
    result = message1
}

/**
 * Holds if `e` directly uses a parameter's initial value as passed in from the caller.
 */
predicate isInitialParameterUse(Expr e) {
  // unlike `SimpleParameter.getAnInitialUse` this will not include uses we have refinement information for
  exists (SimpleParameter p, SsaExplicitDefinition ssa |
    ssa.getDef() = p and
    ssa.getVariable().getAUse() = e and
    not p.isRestParameter()
  )
}

/**
 * Holds if `e` is an expression that should not be considered in a heterogeneous comparison.
 *
 * We currently whitelist expressions that rely on inter-procedural parameter information.
 */
predicate whitelist(Expr e) {
  isInitialParameterUse(e)
}

from ASTNode cmp,
     DataFlow::AnalyzedNode left, DataFlow::AnalyzedNode right,
     string leftTypes, string rightTypes,
     string leftExprDescription, string rightExprDescription,
     int leftTypeCount, int rightTypeCount ,
     string leftTypeDescription, string rightTypeDescription
where isHeterogeneousComparison(cmp, left, right, leftTypes, rightTypes) and
      not whitelist(left.asExpr()) and
      not whitelist(right.asExpr()) and
      leftExprDescription = capitalize(getDescription(left.asExpr(), "this expression")) and
      rightExprDescription = getDescription(right.asExpr(), "an expression") and
      leftTypeCount = strictcount(left.getAType()) and
      rightTypeCount = strictcount(right.getAType()) and
      leftTypeDescription = getTypeDescription("is of type " + leftTypes, "cannot be of type " + rightTypes, leftTypeCount, rightTypeCount) and
      rightTypeDescription = getTypeDescription("of type " + rightTypes, ", which cannot be of type " + leftTypes, rightTypeCount, leftTypeCount)
select left, leftExprDescription + " " + leftTypeDescription + ", but it is compared to $@ " + rightTypeDescription + ".",
       right, rightExprDescription
