zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

large_numbers.ex (7185B)


      1 defmodule Credo.Check.Readability.LargeNumbers do
      2   use Credo.Check,
      3     base_priority: :high,
      4     tags: [:formatter],
      5     param_defaults: [
      6       only_greater_than: 9_999,
      7       trailing_digits: []
      8     ],
      9     explanations: [
     10       check: """
     11       Numbers can contain underscores for readability purposes.
     12       These do not affect the value of the number, but can help read large numbers
     13       more easily.
     14 
     15           141592654 # how large is this number?
     16 
     17           141_592_654 # ah, it's in the hundreds of millions!
     18 
     19       Like all `Readability` issues, this one is not a technical concern.
     20       But you can improve the odds of others reading and liking your code by making
     21       it easier to follow.
     22       """,
     23       params: [
     24         only_greater_than: "The check only reports numbers greater than this.",
     25         trailing_digits:
     26           "The check allows for the given number of trailing digits (can be a number, range or list)"
     27       ]
     28     ]
     29 
     30   @doc false
     31   # TODO: consider for experimental check front-loader (tokens)
     32   def run(%SourceFile{} = source_file, params) do
     33     min_number = Params.get(params, :only_greater_than, __MODULE__)
     34     issue_meta = IssueMeta.for(source_file, Keyword.merge(params, only_greater_than: min_number))
     35 
     36     allowed_trailing_digits =
     37       case Params.get(params, :trailing_digits, __MODULE__) do
     38         %Range{} = value -> Enum.to_list(value)
     39         value -> List.wrap(value)
     40       end
     41 
     42     source_file
     43     |> Credo.Code.to_tokens()
     44     |> collect_number_tokens([], min_number)
     45     |> find_issues([], allowed_trailing_digits, issue_meta)
     46   end
     47 
     48   defp collect_number_tokens([], acc, _), do: acc
     49 
     50   defp collect_number_tokens([head | t], acc, min_number) do
     51     acc =
     52       case number_token(head, min_number) do
     53         nil -> acc
     54         token -> acc ++ [token]
     55       end
     56 
     57     collect_number_tokens(t, acc, min_number)
     58   end
     59 
     60   # tuple for Elixir >= 1.10.0
     61   defp number_token({:flt, {_, _, number}, _} = tuple, min_number) when min_number < number do
     62     tuple
     63   end
     64 
     65   # tuple for Elixir >= 1.6.0
     66   defp number_token({:int, {_, _, number}, _} = tuple, min_number) when min_number < number do
     67     tuple
     68   end
     69 
     70   defp number_token({:float, {_, _, number}, _} = tuple, min_number) when min_number < number do
     71     tuple
     72   end
     73 
     74   # tuple for Elixir <= 1.5.x
     75   defp number_token({:number, _, number} = tuple, min_number) when min_number < number do
     76     tuple
     77   end
     78 
     79   defp number_token(_, _), do: nil
     80 
     81   defp find_issues([], acc, _allowed_trailing_digits, _issue_meta) do
     82     acc
     83   end
     84 
     85   # tuple for Elixir >= 1.10.0
     86   defp find_issues(
     87          [{:flt, {line_no, column1, number} = location, _} | t],
     88          acc,
     89          allowed_trailing_digits,
     90          issue_meta
     91        ) do
     92     acc =
     93       acc ++ find_issue(line_no, column1, location, number, allowed_trailing_digits, issue_meta)
     94 
     95     find_issues(t, acc, allowed_trailing_digits, issue_meta)
     96   end
     97 
     98   # tuple for Elixir >= 1.6.0
     99   defp find_issues(
    100          [{:int, {line_no, column1, number} = location, _} | t],
    101          acc,
    102          allowed_trailing_digits,
    103          issue_meta
    104        ) do
    105     acc =
    106       acc ++ find_issue(line_no, column1, location, number, allowed_trailing_digits, issue_meta)
    107 
    108     find_issues(t, acc, allowed_trailing_digits, issue_meta)
    109   end
    110 
    111   defp find_issues(
    112          [{:float, {line_no, column1, number} = location, _} | t],
    113          acc,
    114          allowed_trailing_digits,
    115          issue_meta
    116        ) do
    117     acc =
    118       acc ++ find_issue(line_no, column1, location, number, allowed_trailing_digits, issue_meta)
    119 
    120     find_issues(t, acc, allowed_trailing_digits, issue_meta)
    121   end
    122 
    123   # tuple for Elixir <= 1.5.x
    124   defp find_issues(
    125          [{:number, {line_no, column1, _column2} = location, number} | t],
    126          acc,
    127          allowed_trailing_digits,
    128          issue_meta
    129        ) do
    130     acc =
    131       acc ++ find_issue(line_no, column1, location, number, allowed_trailing_digits, issue_meta)
    132 
    133     find_issues(t, acc, allowed_trailing_digits, issue_meta)
    134   end
    135 
    136   defp find_issue(line_no, column1, location, number, allowed_trailing_digits, issue_meta) do
    137     source = source_fragment(location, issue_meta)
    138     underscored_versions = number_with_underscores(number, allowed_trailing_digits, source)
    139 
    140     if decimal_in_source?(source) && not Enum.member?(underscored_versions, source) do
    141       [
    142         issue_for(
    143           issue_meta,
    144           line_no,
    145           column1,
    146           source,
    147           underscored_versions
    148         )
    149       ]
    150     else
    151       []
    152     end
    153   end
    154 
    155   defp number_with_underscores(number, allowed_trailing_digits, _) when is_integer(number) do
    156     number
    157     |> to_string
    158     |> add_underscores_to_number_string(allowed_trailing_digits)
    159   end
    160 
    161   defp number_with_underscores(number, allowed_trailing_digits, source_fragment)
    162        when is_number(number) do
    163     case String.split(source_fragment, ".", parts: 2) do
    164       [num, decimal] ->
    165         add_underscores_to_number_string(num, allowed_trailing_digits)
    166         |> Enum.map(fn base -> Enum.join([base, decimal], ".") end)
    167 
    168       [num] ->
    169         add_underscores_to_number_string(num, allowed_trailing_digits)
    170     end
    171   end
    172 
    173   defp add_underscores_to_number_string(string, allowed_trailing_digits) do
    174     without_trailing_digits =
    175       string
    176       |> String.reverse()
    177       |> String.replace(~r/(\d{3})(?=\d)/, "\\1_")
    178       |> String.reverse()
    179 
    180     all_trailing_digit_versions =
    181       Enum.map(allowed_trailing_digits, fn trailing_digits ->
    182         if String.length(string) > trailing_digits do
    183           base =
    184             String.slice(string, 0..(-1 * trailing_digits - 1))
    185             |> String.reverse()
    186             |> String.replace(~r/(\d{3})(?=\d)/, "\\1_")
    187             |> String.reverse()
    188 
    189           trailing = String.slice(string, (-1 * trailing_digits)..-1)
    190 
    191           "#{base}_#{trailing}"
    192         end
    193       end)
    194 
    195     ([without_trailing_digits] ++ all_trailing_digit_versions)
    196     |> Enum.reject(&is_nil/1)
    197     |> Enum.uniq()
    198   end
    199 
    200   defp issue_for(issue_meta, line_no, column, trigger, expected) do
    201     params = IssueMeta.params(issue_meta)
    202     only_greater_than = Params.get(params, :only_greater_than, __MODULE__)
    203 
    204     format_issue(
    205       issue_meta,
    206       message:
    207         "Numbers larger than #{only_greater_than} should be written with underscores: #{Enum.join(expected, " or ")}",
    208       line_no: line_no,
    209       column: column,
    210       trigger: trigger
    211     )
    212   end
    213 
    214   defp decimal_in_source?(source) do
    215     case String.slice(source, 0, 2) do
    216       "0b" -> false
    217       "0o" -> false
    218       "0x" -> false
    219       "" -> false
    220       _ -> true
    221     end
    222   end
    223 
    224   defp source_fragment({line_no, column1, _}, issue_meta) do
    225     line =
    226       issue_meta
    227       |> IssueMeta.source_file()
    228       |> SourceFile.line_at(line_no)
    229 
    230     beginning_of_number =
    231       ~r/[^0-9_oxb]*([0-9_oxb]+$)/
    232       |> Regex.run(String.slice(line, 1..column1))
    233       |> List.wrap()
    234       |> List.last()
    235       |> to_string()
    236 
    237     ending_of_number =
    238       ~r/^([0-9_\.]+)/
    239       |> Regex.run(String.slice(line, (column1 + 1)..-1))
    240       |> List.wrap()
    241       |> List.last()
    242       |> to_string()
    243       |> String.replace(~r/\.\..*/, "")
    244 
    245     beginning_of_number <> ending_of_number
    246   end
    247 end