/**
 * @name Implicit operand conversion
 * @description Relying on implicit conversion of operands is error-prone and makes code
 *              hard to read.
 * @kind problem
 * @problem.severity warning
 * @id js/implicit-operand-conversion
 * @tags reliability
 *       readability
 *       external/cwe/cwe-704
 * @precision very-high
 */

import javascript
private import semmle.javascript.dataflow.InferredTypes

/**
 * An expression that appears in a syntactic position where its value may be
 * implicitly converted.
 */
abstract class ImplicitConversion extends DataFlow::AnalyzedNode {
  Expr parent;

  ImplicitConversion() {
    this.asExpr() = parent.getAChildExpr()
  }

  /**
   * Gets a description of the type(s) to which the value `v`, which is
   * a possible runtime value of this expression, is converted.
   *
   * This predicate only considers conversions that are likely to be
   * unintentional or have unexpected results, for example `null` being
   * converted to a string as part of a string concatenation.
   */
  abstract string getAnImplicitConversionTarget(AbstractValue v);
}

/**
 * An implicit conversion with a whitelist of types for which the implicit conversion
 * is harmless.
 */
abstract class ImplicitConversionWithWhitelist extends ImplicitConversion {
  /** Gets a type for which this implicit conversion is harmless. */
  abstract InferredType getAWhitelistedType();

  /**
   * Gets a description of the type(s) to which any value of this expression
   * is converted.
   */
  abstract string getConversionTarget();

  override string getAnImplicitConversionTarget(AbstractValue v) {
    v = getAValue() and
    not (v.getType() = getAWhitelistedType()) and
    result = getConversionTarget()
  }
}

/**
 * Property names in `in` expressions are converted to strings,
 * so they should be strings or numbers.
 */
class PropertyNameConversion extends ImplicitConversionWithWhitelist {
  PropertyNameConversion() {
    this.asExpr() = parent.(InExpr).getLeftOperand()
  }

  override InferredType getAWhitelistedType() {
    result = TTString() or result = TTNumber()
  }

  override string getConversionTarget() {
    result = "string"
  }
}

/**
 * Property names in index expressions are converted to strings,
 * so they should be Booleans, strings or numbers.
 */
class IndexExprConversion extends ImplicitConversionWithWhitelist {
  IndexExprConversion() {
    this.asExpr() = parent.(IndexExpr).getIndex()
  }

  override InferredType getAWhitelistedType() {
    result = TTBoolean() or result = TTString() or result = TTNumber()
  }

  override string getConversionTarget() {
    result = "string"
  }
}

/**
 * Expressions that are interpreted as objects shouldn't be primitive values.
 */
class ObjectConversion extends ImplicitConversionWithWhitelist {
  ObjectConversion() {
    this.asExpr() = parent.(InExpr).getRightOperand() or
    this.asExpr() = parent.(InstanceofExpr).getLeftOperand()
  }

  override InferredType getAWhitelistedType() {
    result instanceof NonPrimitiveType
  }

  override string getConversionTarget() {
    result = "object"
  }
}

/**
 * The right-hand operand of `instanceof` should be a function or class.
 */
class ConstructorConversion extends ImplicitConversionWithWhitelist {
  ConstructorConversion() {
    this.asExpr() = parent.(InstanceofExpr).getRightOperand()
  }

  override InferredType getAWhitelistedType() {
    result = TTFunction() or result = TTClass()
  }

  override string getConversionTarget() {
    result = "function"
  }
}

/**
 * Operands of relational operators are converted to strings or numbers,
 * and hence should be strings, numbers or Dates.
 */
class RelationalOperandConversion extends ImplicitConversionWithWhitelist {
  RelationalOperandConversion() {
    parent instanceof RelationalComparison
  }

  override InferredType getAWhitelistedType() {
    result = TTString() or result = TTNumber() or result = TTDate()
  }

  override string getConversionTarget() {
    result = "number or string"
  }
}

/**
 * Operands of arithmetic and bitwise operations are converted to numbers,
 * so they should be Booleans, numbers or Dates.
 */
class NumericConversion extends ImplicitConversion {
  NumericConversion() {
    parent instanceof BitwiseExpr or
    parent instanceof ArithmeticExpr and not parent instanceof AddExpr or
    parent instanceof CompoundAssignExpr and not parent instanceof AssignAddExpr or
    parent instanceof UpdateExpr
  }

  override string getAnImplicitConversionTarget(AbstractValue v) {
    v = getAValue() and
    not v.isCoercibleToNumber() and
    result = "number"
  }
}

/**
 * An expression whose value should not be `null` or `undefined`.
 */
abstract class NullOrUndefinedConversion extends ImplicitConversion {
  abstract string getConversionTarget();

  override string getAnImplicitConversionTarget(AbstractValue v) {
    v = getAValue() and
    (v instanceof AbstractNull or v instanceof AbstractUndefined) and
    result = getConversionTarget()
  }
}

/**
 * Operands of `+` or `+=` are converted to strings or numbers, and hence
 * should not be `null` or `undefined`.
 */
class PlusConversion extends NullOrUndefinedConversion {
  PlusConversion() {
    parent instanceof AddExpr or parent instanceof AssignAddExpr
  }

  override string getConversionTarget() {
    result = "number or string"
  }
}

/**
 * Template literal elements are converted to strings, and hence should not
 * be `null` or `undefined`.
 */
class TemplateElementConversion extends NullOrUndefinedConversion {
  TemplateElementConversion() {
    parent instanceof TemplateLiteral
  }

  override string getConversionTarget() {
    result = "string"
  }
}

from ImplicitConversion e, string convType
where convType = e.getAnImplicitConversionTarget(_) and
      forall (AbstractValue v | v = e.getAValue() | exists(e.getAnImplicitConversionTarget(v)))
select e, "This expression will be implicitly converted from " +
          e.ppTypes() + " to " + convType + "."