# Copyright 2023 Adobe. All rights reserved. # This file is licensed to you under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. You may obtain a copy # of the License at http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software distributed under # the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS # OF ANY KIND, either express or implied. See the License for the specific language # governing permissions and limitations under the License. defmodule Styler.Style.Defs do @moduledoc """ Styles function heads so that they're as small as possible. The goal is that a function head fits on a single line. This isn't a Credo issue, and the formatter is fine with either approach. But Styler has opinions! Ex: This long declaration def foo(%{ bar: baz }) do ... end Becomes def foo(%{bar: baz}) do ... end """ @behaviour Styler.Style alias Styler.Style alias Styler.Zipper # a def with no body like # # def example(foo, bar \\ nil) # def run({{def, meta, [head]}, _} = zipper, ctx) when def in [:def, :defp] do {_fn_name, head_meta, _children} = head first_line = meta[:line] last_line = head_meta[:closing][:line] if first_line == last_line do # Already collapsed {:skip, zipper, ctx} else comments = Style.displace_comments(ctx.comments, first_line..last_line) node = {def, meta, [flatten_head(head, meta[:line])]} {:skip, Zipper.replace(zipper, node), %{ctx | comments: comments}} end end # all the other kinds of defs! def run({{def, def_meta, [head, body]}, _} = zipper, ctx) when def in [:def, :defp] do {def_line, do_line, end_line} = if def_meta[:do] do # This is a def with a do block, like # # def example(foo, bar \\ nil) do # :ok # end # def_line = def_meta[:line] do_line = def_meta[:do][:line] end_line = def_meta[:end][:line] {def_line, do_line, end_line} else # This is a def with a keyword do, like # # def example(foo, bar \\ nil), do: :ok # [{{:__block__, do_meta, [:do]}, {_, body_meta, _}}] = body def_line = def_meta[:line] do_line = do_meta[:line] end_line = body_meta[:closing][:line] || do_meta[:line] {def_line, do_line, end_line} end delta = def_line - do_line move_up = &(&1 + delta) set_to_def_line = fn _ -> def_line end cond do def_line == end_line -> # Already collapsed {:skip, zipper, ctx} def_meta[:do] -> # We're working on a def do ... end def_meta = def_meta |> Keyword.replace_lazy(:do, &Keyword.update!(&1, :line, set_to_def_line)) |> Keyword.replace_lazy(:end, &Keyword.update!(&1, :line, move_up)) head = flatten_head(head, def_line) body = update_all_meta(body, shift_lines(move_up)) node = {def, def_meta, [head, body]} comments = ctx.comments |> Style.displace_comments(def_line..do_line) |> Style.shift_comments(do_line..end_line, delta) # @TODO this skips checking the body, which can be incorrect if therey's a `quote do def do ...` inside of it {:skip, Zipper.replace(zipper, node), %{ctx | comments: comments}} true -> # We're working on a Keyword def do: head = flatten_head(head, def_line) body = update_all_meta(body, collapse_lines(set_to_def_line)) node = {def, def_meta, [head, body]} comments = Style.displace_comments(ctx.comments, def_line..end_line) # @TODO this skips checking the body, which can be incorrect if therey's a `quote do def do ...` inside of it {:skip, Zipper.replace(zipper, node), %{ctx | comments: comments}} end end def run(zipper, ctx), do: {:cont, zipper, ctx} defp collapse_lines(line_mover) do fn meta -> meta |> Keyword.replace_lazy(:line, line_mover) |> Keyword.replace_lazy(:closing, &Keyword.replace_lazy(&1, :line, line_mover)) |> Keyword.delete(:newlines) end end defp shift_lines(line_mover) do fn meta -> meta |> Keyword.replace_lazy(:line, line_mover) |> Keyword.replace_lazy(:closing, &Keyword.replace_lazy(&1, :line, line_mover)) end end defp flatten_head(head, line) do update_all_meta(head, fn meta -> meta |> Keyword.replace(:line, line) |> Keyword.replace(:closing, line: line) |> Keyword.replace(:last, line: line) |> Keyword.delete(:newlines) end) end defp update_all_meta(node, meta_fun) do node |> Zipper.zip() |> Zipper.traverse(fn {{node, meta, children}, _} = zipper -> Zipper.replace(zipper, {node, meta_fun.(meta), children}) zipper -> zipper end) |> Zipper.root() end end