trim_trailing_whitespace.pony

use per = "collections/persistent"

use ast = "../../ast"
use lint = ".."

class val TrimTrailingWhitespace is lint.Rule
  fun val name(): String => lint.ConfigKey.trim_trailing_whitespace()

  fun val message(): String => "trailing whitespace"

  fun val should_apply(config: lint.Config val): Bool =>
    try
      config(lint.ConfigKey.trim_trailing_whitespace())?.lower() == "true"
    else
      false
    end

  fun val analyze(tree: ast.Node, issues: Seq[lint.Issue] iso)
    : (ast.Node, Seq[lint.Issue] iso^, ReadSeq[ast.TraverseError] val)
  =>
    let ws_seen = Array[ast.Path]
    let rule = this
    let issues' = Array[lint.Issue]
    let fn =
      object
        fun ref apply(node: ast.Node, path: ast.Path) =>
          if node.children().size() == 0 then
            match node
            | let t: ast.NodeWith[ast.Trivia] if
              (t.data().kind is ast.EndOfLineTrivia) or
              (t.data().kind is ast.EndOfFileTrivia)
            =>
              if ws_seen.size() > 0 then
                try issues'.push(lint.Issue(rule, ws_seen(0)?, path)) end
              end
              ws_seen.clear()
            | let t: ast.NodeWith[ast.Trivia] if
              t.data().kind is ast.WhiteSpaceTrivia
            =>
              ws_seen.push(path)
            else
              ws_seen.clear()
            end
          else
            for child in node.children().values() do
              this(child, path.prepend(child))
            end
          end
        end
    fn(tree, per.Cons[ast.Node](tree, per.Nil[ast.Node]))
    for issue in issues'.values() do
      issues.push(issue)
    end
    (tree, consume issues, [])

  fun val fix(tree: ast.Node, issues: ReadSeq[lint.Issue] val)
    : (ast.Node, ReadSeq[lint.Issue] val, ReadSeq[ast.TraverseError] val)
  =>
    var root: ast.Node = tree
    let unfixed: Array[lint.Issue] trn = Array[lint.Issue]
    let all_errors: Array[ast.TraverseError] trn = Array[ast.TraverseError]
    for issue in issues.values() do
      if issue.rule.name() == this.name() then
        let visitor = _TrailingWhitespaceVisitor(issue)
        (let new_root, let errors) =
          ast.SyntaxTree.traverse[None](consume visitor, None, root)
        if new_root is root then
          unfixed.push(issue)
          all_errors.push((root, "tree was unchanged by " + this.name()))
        else
          root = new_root
        end
        all_errors.append(errors)
      else
        unfixed.push(issue)
      end
    end

    if root is tree then
      (tree, consume unfixed, consume all_errors)
    else
      (root, _, let errors) = ast.SyntaxTree.add_line_info(root)
      for err in errors.values() do
        all_errors.push(err)
      end
      (root, consume unfixed, consume all_errors)
    end

class _TrailingWhitespaceVisitor is ast.Visitor[None]
  let _issue: lint.Issue

  var _in_trailing_ws: Bool = false

  new iso create(issue: lint.Issue) =>
    _issue = issue

  fun ref visit_pre(
    parent_state: None,
    node: ast.Node,
    path: ast.Path,
    errors: Array[ast.TraverseError] iso)
    : (None, Array[ast.TraverseError] iso^)
  =>
    (None, consume errors)

  fun ref visit_post(
    node_state: None,
    node: ast.Node,
    path: ast.Path,
    errors: Array[ast.TraverseError] iso,
    child_states: (ReadSeq[None] | None),
    new_children: (ast.NodeSeq | None) = None,
    update_map: (ast.ChildUpdateMap | None) = None)
    : (None, (ast.Node | None), Array[ast.TraverseError] iso^)
  =>
    // check for trailing whitespace
    if _issue.match_start(node) then
      _in_trailing_ws = true
    end
    if _issue.match_next(node) then
      _in_trailing_ws = false
    end

    // if we're a leaf and in trailing whitespace, delete us
    if _in_trailing_ws then
      match node
      | let t: ast.NodeWith[ast.Trivia] if
          (t.data().kind is ast.WhiteSpaceTrivia)
      =>
        return (None, None, consume errors)
      end
    end

    // if our children have been adjusted, clone us with the new children
    let new_node =
      match (new_children, update_map)
      | (let nc: ast.NodeSeq, let um: ast.ChildUpdateMap) =>
        node.clone(where new_children' = nc, update_map' = um)
      else
        node
      end
    (None, new_node, consume errors)