max_line_length.ex (4504B)
1 defmodule Credo.Check.Readability.MaxLineLength do 2 use Credo.Check, 3 base_priority: :low, 4 tags: [:formatter], 5 param_defaults: [ 6 max_length: 120, 7 ignore_definitions: true, 8 ignore_heredocs: true, 9 ignore_specs: false, 10 ignore_sigils: true, 11 ignore_strings: true, 12 ignore_urls: true 13 ], 14 explanations: [ 15 check: """ 16 Checks for the length of lines. 17 18 Ignores function definitions and (multi-)line strings by default. 19 """, 20 params: [ 21 max_length: "The maximum number of characters a line may consist of.", 22 ignore_definitions: "Set to `true` to ignore lines including function definitions.", 23 ignore_specs: "Set to `true` to ignore lines including `@spec`s.", 24 ignore_sigils: "Set to `true` to ignore lines that are sigils, e.g. regular expressions.", 25 ignore_strings: "Set to `true` to ignore lines that are strings or in heredocs.", 26 ignore_urls: "Set to `true` to ignore lines that contain urls." 27 ] 28 ] 29 30 alias Credo.Code.Heredocs 31 alias Credo.Code.Sigils 32 alias Credo.Code.Strings 33 34 @def_ops [:def, :defp, :defmacro] 35 @url_regex ~r/[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&\/\/=]*)/ 36 37 @doc false 38 @impl true 39 def run(%SourceFile{} = source_file, params) do 40 issue_meta = IssueMeta.for(source_file, params) 41 max_length = Params.get(params, :max_length, __MODULE__) 42 43 ignore_definitions = Params.get(params, :ignore_definitions, __MODULE__) 44 45 ignore_specs = Params.get(params, :ignore_specs, __MODULE__) 46 ignore_sigils = Params.get(params, :ignore_sigils, __MODULE__) 47 ignore_strings = Params.get(params, :ignore_strings, __MODULE__) 48 ignore_heredocs = Params.get(params, :ignore_heredocs, __MODULE__) 49 ignore_urls = Params.get(params, :ignore_urls, __MODULE__) 50 51 definitions = Credo.Code.prewalk(source_file, &find_definitions/2) 52 specs = Credo.Code.prewalk(source_file, &find_specs/2) 53 54 source = 55 if ignore_heredocs do 56 Heredocs.replace_with_spaces(source_file, "") 57 else 58 SourceFile.source(source_file) 59 end 60 61 source = 62 if ignore_sigils do 63 Sigils.replace_with_spaces(source, "") 64 else 65 source 66 end 67 68 lines = Credo.Code.to_lines(source) 69 70 lines_for_comparison = 71 if ignore_strings do 72 source 73 |> Strings.replace_with_spaces("", " ", source_file.filename) 74 |> Credo.Code.to_lines() 75 else 76 lines 77 end 78 79 lines_for_comparison = 80 if ignore_urls do 81 Enum.reject(lines_for_comparison, fn {_, line} -> line =~ @url_regex end) 82 else 83 lines_for_comparison 84 end 85 86 Enum.reduce(lines_for_comparison, [], fn {line_no, line_for_comparison}, issues -> 87 if String.length(line_for_comparison) > max_length do 88 if refute_issue?(line_no, definitions, ignore_definitions, specs, ignore_specs) do 89 issues 90 else 91 {_, line} = Enum.at(lines, line_no - 1) 92 93 [issue_for(line_no, max_length, line, issue_meta) | issues] 94 end 95 else 96 issues 97 end 98 end) 99 end 100 101 # TODO: consider for experimental check front-loader (ast) 102 for op <- @def_ops do 103 defp find_definitions({unquote(op), meta, arguments} = ast, definitions) 104 when is_list(arguments) do 105 {ast, [meta[:line] | definitions]} 106 end 107 end 108 109 defp find_definitions(ast, definitions) do 110 {ast, definitions} 111 end 112 113 # TODO: consider for experimental check front-loader (ast) 114 defp find_specs({:spec, meta, arguments} = ast, specs) when is_list(arguments) do 115 {ast, [meta[:line] | specs]} 116 end 117 118 defp find_specs(ast, specs) do 119 {ast, specs} 120 end 121 122 defp refute_issue?(line_no, definitions, ignore_definitions, specs, ignore_specs) do 123 ignore_definitions? = 124 if ignore_definitions do 125 Enum.member?(definitions, line_no) 126 else 127 false 128 end 129 130 ignore_specs? = 131 if ignore_specs do 132 Enum.member?(specs, line_no) 133 else 134 false 135 end 136 137 ignore_definitions? || ignore_specs? 138 end 139 140 defp issue_for(line_no, max_length, line, issue_meta) do 141 line_length = String.length(line) 142 column = max_length + 1 143 trigger = String.slice(line, max_length, line_length - max_length) 144 145 format_issue( 146 issue_meta, 147 message: "Line is too long (max is #{max_length}, was #{line_length}).", 148 line_no: line_no, 149 column: column, 150 trigger: trigger 151 ) 152 end 153 end