scope.ex (3505B)
1 defmodule Credo.Code.Scope do 2 @moduledoc """ 3 This module provides helper functions to determine the scope name at a certain 4 point in the analysed code. 5 """ 6 7 @def_ops [:def, :defp, :defmacro] 8 9 @doc """ 10 Returns the module part of a scope. 11 12 iex> Credo.Code.Scope.mod_name("Credo.Code") 13 "Credo.Code" 14 15 iex> Credo.Code.Scope.mod_name("Credo.Code.ast") 16 "Credo.Code" 17 18 """ 19 def mod_name(nil), do: nil 20 21 def mod_name(scope_name) do 22 names = String.split(scope_name, ".") 23 base_name = List.last(names) 24 25 if String.match?(base_name, ~r/^[_a-z]/) do 26 names 27 |> Enum.slice(0..(length(names) - 2)) 28 |> Enum.join(".") 29 else 30 scope_name 31 end 32 end 33 34 @doc """ 35 Returns the scope for the given line as a tuple consisting of the call to 36 define the scope (`:defmodule`, `:def`, `:defp` or `:defmacro`) and the 37 name of the scope. 38 39 Examples: 40 41 {:defmodule, "Foo.Bar"} 42 {:def, "Foo.Bar.baz"} 43 """ 44 def name(_ast, line: 0), do: nil 45 46 def name(ast, line: line) do 47 ast 48 |> scope_info_list() 49 |> name_from_scope_info_list(line) 50 end 51 52 @doc false 53 def name_from_scope_info_list(scope_info_list, line) do 54 result = 55 Enum.find(scope_info_list, fn 56 {line_no, _op, _arguments} when line_no <= line -> true 57 _ -> false 58 end) 59 60 case result do 61 {_line_no, op, arguments} -> 62 name = Credo.Code.Name.full(arguments) 63 {op, name} 64 65 _ -> 66 {nil, ""} 67 end 68 end 69 70 @doc false 71 def scope_info_list(ast) do 72 {_, scope_info_list} = Macro.prewalk(ast, [], &traverse_modules(&1, &2, nil, nil)) 73 74 Enum.reverse(scope_info_list) 75 end 76 77 defp traverse_modules({:defmodule, meta, arguments} = ast, acc, current_scope, _current_op) 78 when is_list(arguments) do 79 new_scope_part = Credo.Code.Module.name(ast) 80 81 scope_name = 82 [current_scope, new_scope_part] 83 |> Enum.reject(&is_nil/1) 84 |> Credo.Code.Name.full() 85 86 defmodule_scope_info = {meta[:line], :defmodule, scope_name} 87 88 {_, def_scope_infos} = 89 Macro.prewalk(arguments, [], &traverse_defs(&1, &2, scope_name, :defmodule)) 90 91 new_acc = (acc ++ [defmodule_scope_info]) ++ def_scope_infos 92 93 {nil, new_acc} 94 end 95 96 defp traverse_modules({_op, meta, _arguments} = ast, acc, current_scope, current_op) do 97 scope_info = {meta[:line], current_op, current_scope} 98 99 {ast, acc ++ [scope_info]} 100 end 101 102 defp traverse_modules(ast, acc, _current_scope, _current_op) do 103 {ast, acc} 104 end 105 106 defp traverse_defs({:defmodule, _meta, arguments} = ast, acc, current_scope, _current_op) 107 when is_list(arguments) do 108 {_, scopes} = Macro.prewalk(ast, [], &traverse_modules(&1, &2, current_scope, :defmodule)) 109 110 {nil, acc ++ scopes} 111 end 112 113 for op <- @def_ops do 114 defp traverse_defs({unquote(op), meta, arguments} = ast, acc, current_scope, _current_op) 115 when is_list(arguments) do 116 new_scope_part = Credo.Code.Module.def_name(ast) 117 118 scope_name = 119 [current_scope, new_scope_part] 120 |> Enum.reject(&is_nil/1) 121 |> Credo.Code.Name.full() 122 123 scope_info = {meta[:line], unquote(op), scope_name} 124 125 new_acc = acc ++ [scope_info] 126 127 {nil, new_acc} 128 end 129 end 130 131 defp traverse_defs({_op, meta, _arguments} = ast, acc, current_scope, current_op) do 132 scope_info = {meta[:line], current_op, current_scope} 133 134 {ast, acc ++ [scope_info]} 135 end 136 137 defp traverse_defs(ast, acc, _current_scope, _current_op) do 138 {ast, acc} 139 end 140 end