// CWE-22: Path concatenation with potentially user-controlled input.
// Heuristic-based: detects string concatenation involving path-like variables.

///|
priv struct Cwe022Visitor {
  findings : Array[Finding]
  file : String
  source : String
  severity : Severity
}

///|
pub fn cwe022_info() -> RuleInfo {
  {
    id: { cwe: 22, variant: "path-concat" },
    name: "Potential Path Traversal via String Concatenation",
    description: "String concatenation to build file paths without traversal protection may allow directory traversal attacks.",
    default_severity: Warning,
  }
}

///|
pub fn run_cwe022(
  impls : @list.List[@syntax.Impl],
  file : String,
  source : String,
  findings : Array[Finding],
  severity : Severity,
) -> Unit {
  let visitor : Cwe022Visitor = { findings, file, source, severity }
  impls.each(fn(impl_) { @syntax.IterVisitor::visit_Impl(visitor, impl_) })
}

///|
fn is_path_like_var(name : String) -> Bool {
  if name.contains("url") ||
    name.contains("route") ||
    name.contains("uri") ||
    name.contains("endpoint") ||
    name.contains("href") {
    return false
  }
  name.contains("path") ||
  name.contains("file") ||
  name.contains("dir") ||
  name.contains("filename") ||
  name.contains("filepath")
}

///|
fn is_input_like_var(name : String) -> Bool {
  name.contains("req") ||
  name.contains("param") ||
  name.contains("query") ||
  name.contains("input") ||
  name.contains("id") ||
  name.contains("slug") ||
  name.contains("name") ||
  name.contains("user")
}

///|
fn expr_var_name(expr : @syntax.Expr) -> String? {
  match expr {
    Ident(id~, ..) =>
      match id.name {
        Ident(name~) => Some(name)
        Dot(id~, ..) => Some(id)
      }
    Field(accessor~, ..) =>
      match accessor {
        Label(label) => Some(label.name)
        _ => None
      }
    _ => None
  }
}

///|
fn is_path_separator_string(expr : @syntax.Expr) -> Bool {
  match expr {
    Constant(c=String(s), ..) =>
      s == "/" || s == "\\" || s.has_prefix("/") || s.has_suffix("/")
    _ => false
  }
}

///|
impl @syntax.IterVisitor for Cwe022Visitor with fn visit_Expr_Infix(
  env,
  op~,
  lhs~,
  rhs~,
  loc~,
) {
  match op.name {
    Ident(name="+") => {
      let lhs_name = expr_var_name(lhs)
      let rhs_name = expr_var_name(rhs)
      let has_path_context = match (lhs_name, rhs_name) {
        (Some(l), Some(r)) =>
          (is_path_like_var(l) && is_input_like_var(r)) ||
          (is_input_like_var(l) && is_path_like_var(r))
        (Some(l), None) => is_path_like_var(l) && !is_any_constant(rhs)
        (None, Some(r)) => is_path_separator_string(lhs) && is_input_like_var(r)
        _ => false
      }
      if has_path_context {
        env.findings.push(
          make_finding(
            { cwe: 22, variant: "path-concat" },
            env.severity,
            Low,
            "String concatenation to build file path. Use path sanitization to prevent directory traversal (e.g., reject '..' segments).",
            env.file,
            env.source,
            loc,
          ),
        )
      }
    }
    _ => ()
  }
  @syntax.IterVisitor::base(env).visit_Expr_Infix(op~, lhs~, rhs~, loc~)
}