///|
pub fn has_labelled_arg(
  args : @list.List[@syntax.Argument],
  name : String,
) -> Bool {
  args.any(fn(arg) {
    match arg.kind {
      Labelled(label) => label.name == name
      LabelledPun(label) => label.name == name
      LabelledOption(label~, ..) => label.name == name
      LabelledOptionPun(label~, ..) => label.name == name
      _ => false
    }
  })
}

///|
pub fn has_labelled_bool_arg(
  args : @list.List[@syntax.Argument],
  name : String,
  value : Bool,
) -> Bool {
  args.any(fn(arg) {
    let name_matches = match arg.kind {
      Labelled(label) => label.name == name
      _ => false
    }
    if !name_matches {
      return false
    }
    match arg.value {
      Constant(c=Bool(b), ..) => b == value
      _ => false
    }
  })
}

///|
pub fn get_labelled_string_value(
  args : @list.List[@syntax.Argument],
  name : String,
) -> String? {
  let mut result : String? = None
  args.each(fn(arg) {
    let name_matches = match arg.kind {
      Labelled(label) => label.name == name
      _ => false
    }
    if name_matches {
      match arg.value {
        Constant(c=String(s), ..) => result = Some(s)
        _ => ()
      }
    }
  })
  result
}

///|
pub fn is_constant_string(expr : @syntax.Expr) -> Bool {
  match expr {
    Constant(c=String(_), ..) => true
    _ => false
  }
}

///|
pub fn is_any_constant(expr : @syntax.Expr) -> Bool {
  match expr {
    Constant(..) => true
    _ => false
  }
}

///|
pub fn is_constant_ident(name : @syntax.LongIdent) -> Bool {
  let n = match name {
    Ident(name~) => name
    Dot(id~, ..) => id
  }
  if n.length() == 0 {
    return false
  }
  let first = n[0]
  let upper_a : UInt16 = 65
  let upper_z : UInt16 = 90
  if first >= upper_a && first <= upper_z {
    return true
  }
  let underscore : UInt16 = 95
  let all_upper_or_underscore = fn() -> Bool {
    for i = 0; i < n.length(); i = i + 1 {
      let c = n[i]
      if !((c >= upper_a && c <= upper_z) ||
        c == underscore ||
        (c >= 48 && c <= 57)) {
        return false
      }
    }
    true
  }
  all_upper_or_underscore()
}

///|
fn is_sanitize_call_name(name : String) -> Bool {
  name.contains("sanitize") ||
  name.contains("escape") ||
  name.contains("encode")
}

///|
fn is_crlf_like(s : String) -> Bool {
  s == "\\r" ||
  s == "\\n" ||
  s == "\\r\\n" ||
  s == "\r" ||
  s == "\n" ||
  s == "\r\n"
}

///|
fn is_crlf_stripping_chain(expr : @syntax.Expr) -> Bool {
  match expr {
    DotApply(self~, method_name~, args~, ..) =>
      if method_name.name == "replace_all" || method_name.name == "replace" {
        let old_val = get_labelled_string_value(args, "old")
        match old_val {
          Some(s) =>
            if is_crlf_like(s) {
              is_crlf_stripping_chain(self)
            } else {
              false
            }
          _ => false
        }
      } else {
        false
      }
    Ident(..) | Field(..) | Constant(..) => true
    _ => false
  }
}

///|
fn apply_func_name(func : @syntax.Expr) -> String? {
  match func {
    Ident(id~, ..) =>
      match id.name {
        Ident(name~) => Some(name)
        Dot(id~, ..) => Some(id)
      }
    _ => None
  }
}

///|
fn any_arg_is_sanitized(args : @list.List[@syntax.Argument]) -> Bool {
  let mut result = false
  args.each(fn(arg) {
    if is_sanitized_expr(arg.value) || is_safe_value(arg.value) {
      result = true
    }
  })
  result
}

///|
pub fn is_sanitized_expr(expr : @syntax.Expr) -> Bool {
  match expr {
    Apply(func~, args~, ..) =>
      match apply_func_name(func) {
        Some(name) =>
          if is_sanitize_call_name(name) {
            true
          } else {
            any_arg_is_sanitized(args)
          }
        _ => false
      }
    DotApply(method_name~, ..) =>
      if is_sanitize_call_name(method_name.name) {
        true
      } else {
        is_crlf_stripping_chain(expr)
      }
    _ => is_crlf_stripping_chain(expr)
  }
}

///|
fn is_safe_source_text(s : String) -> Bool {
  if s.length() == 0 {
    return true
  }
  let first = s[0]
  let upper_a : UInt16 = 65
  let upper_z : UInt16 = 90
  if first >= upper_a && first <= upper_z {
    return true
  }
  false
}

///|
fn interp_elems_are_safe(elems : @list.List[@syntax.InterpElem]) -> Bool {
  let mut safe = true
  elems.each(fn(elem) {
    match elem {
      Literal(..) => ()
      Expr(expr~, ..) => if !is_safe_value(expr) { safe = false }
      Source(src) => if !is_safe_source_text(src.source) { safe = false }
    }
  })
  safe
}

///|
fn match_branches_all_constant(cases : @list.List[@syntax.Case]) -> Bool {
  let mut all_const = true
  cases.each(fn(case) {
    match case {
      { body, .. } => if !is_safe_value(body) { all_const = false }
    }
  })
  all_const
}

///|
pub fn is_safe_value(expr : @syntax.Expr) -> Bool {
  match expr {
    Constant(..) => true
    Constr(..) => true
    Ident(id~, ..) => is_constant_ident(id.name)
    Interp(elems~, ..) => interp_elems_are_safe(elems)
    Field(..) => true
    Match(cases~, ..) => match_branches_all_constant(cases)
    _ => is_sanitized_expr(expr)
  }
}

///|
pub fn get_source_line(source : String, line_num : Int) -> String {
  let idx = line_num - 1
  let mut i = 0
  for line in source.split("\n") {
    if i == idx {
      return line.to_owned()
    }
    i += 1
  }
  ""
}

///|
fn make_finding(
  rule_id : RuleId,
  severity : Severity,
  confidence : Confidence,
  message : String,
  file : String,
  source : String,
  loc : @basic.Location,
) -> Finding {
  let line = loc.start.lnum
  let col = loc.start.column()
  let end_line = loc.end.lnum
  let end_col = loc.end.column()
  let snippet = get_source_line(source, line)
  {
    rule_id,
    severity,
    confidence,
    message,
    file,
    line,
    column: col,
    end_line,
    end_column: end_col,
    snippet,
    fingerprint: compute_fingerprint(rule_id, snippet),
  }
}

///|
pub fn compute_fingerprint(rule_id : RuleId, snippet : String) -> String {
  let input = rule_id_str(rule_id) + ":" + snippet
  let mut h : UInt = 2166136261U
  for i = 0; i < input.length(); i = i + 1 {
    h = h.lxor(input[i].to_uint())
    h = h * 16777619U
  }
  h.to_string()
}

///|
fn is_security_relevant_replace(args : @list.List[@syntax.Argument]) -> Bool {
  let old_val = get_labelled_string_value(args, "old")
  let new_val = get_labelled_string_value(args, "new")
  match (old_val, new_val) {
    (Some(old), Some(new_str)) => {
      let is_metachar = old == "<" || old == ">" || old == "&"
      let is_quote = old == "\"" || old == "'"
      let is_entity = new_str.has_prefix("&") && new_str.has_suffix(";")
      let is_percent = new_str.has_prefix("%")
      if is_metachar && (is_entity || is_percent) {
        return true
      }
      if is_quote && is_entity {
        return true
      }
      if old.has_prefix("<") && old.has_suffix(">") && new_str.length() == 0 {
        return true
      }
      false
    }
    _ => false
  }
}

///|
fn is_cookie_value_empty(args : @list.List[@syntax.Argument]) -> Bool {
  let mut pos_idx = 0
  let mut result = false
  args.each(fn(arg) {
    match arg.kind {
      Labelled(_)
      | LabelledPun(_)
      | LabelledOption(..)
      | LabelledOptionPun(..) => ()
      _ => {
        if pos_idx == 1 {
          match arg.value {
            Constant(c=String(s), ..) => if s.length() == 0 { result = true }
            _ => ()
          }
        }
        pos_idx += 1
      }
    }
  })
  result
}