code.ex (5421B)
1 defmodule Credo.Code do 2 @moduledoc """ 3 `Credo.Code` contains a lot of utility or helper functions that deal with the 4 analysis of - you guessed it - code. 5 6 Whenever a function serves a general purpose in this area, e.g. getting the 7 value of a module attribute inside a given module, we want to extract that 8 function and put it in the `Credo.Code` namespace, so others can utilize them 9 without reinventing the wheel. 10 """ 11 12 alias Credo.Code.Charlists 13 alias Credo.Code.Heredocs 14 alias Credo.Code.Sigils 15 alias Credo.Code.Strings 16 17 alias Credo.SourceFile 18 19 defmodule ParserError do 20 @moduledoc """ 21 This is an internal `Issue` raised by Credo when it finds itself unable to 22 parse the source code in a file. 23 """ 24 end 25 26 @doc """ 27 Prewalks a given `Credo.SourceFile`'s AST or a given AST. 28 29 Technically this is just a wrapper around `Macro.prewalk/3`. 30 """ 31 def prewalk(ast_or_source_file, fun, accumulator \\ []) 32 33 def prewalk(%SourceFile{} = source_file, fun, accumulator) do 34 source_file 35 |> SourceFile.ast() 36 |> prewalk(fun, accumulator) 37 end 38 39 def prewalk(source_ast, fun, accumulator) do 40 {_, accumulated} = Macro.prewalk(source_ast, accumulator, fun) 41 42 accumulated 43 end 44 45 @doc """ 46 Postwalks a given `Credo.SourceFile`'s AST or a given AST. 47 48 Technically this is just a wrapper around `Macro.postwalk/3`. 49 """ 50 def postwalk(ast_or_source_file, fun, accumulator \\ []) 51 52 def postwalk(%SourceFile{} = source_file, fun, accumulator) do 53 source_file 54 |> SourceFile.ast() 55 |> postwalk(fun, accumulator) 56 end 57 58 def postwalk(source_ast, fun, accumulator) do 59 {_, accumulated} = Macro.postwalk(source_ast, accumulator, fun) 60 61 accumulated 62 end 63 64 @doc """ 65 Returns an AST for a given `String` or `Credo.SourceFile`. 66 """ 67 def ast(string_or_source_file) 68 69 def ast(%SourceFile{filename: filename} = source_file) do 70 source_file 71 |> SourceFile.source() 72 |> ast(filename) 73 end 74 75 @doc false 76 def ast(source, filename \\ "nofilename") when is_binary(source) do 77 case Code.string_to_quoted(source, line: 1, columns: true, file: filename) do 78 {:ok, value} -> 79 {:ok, value} 80 81 {:error, error} -> 82 {:error, [issue_for(error, filename)]} 83 end 84 rescue 85 e in UnicodeConversionError -> 86 {:error, [issue_for({1, e.message, nil}, filename)]} 87 end 88 89 defp issue_for({line_no, error_message, _}, filename) do 90 %Credo.Issue{ 91 check: ParserError, 92 category: :error, 93 filename: filename, 94 message: error_message, 95 line_no: line_no 96 } 97 end 98 99 @doc """ 100 Converts a String or `Credo.SourceFile` into a List of tuples of `{line_no, line}`. 101 """ 102 def to_lines(string_or_source_file) 103 104 def to_lines(%SourceFile{} = source_file) do 105 source_file 106 |> SourceFile.source() 107 |> to_lines() 108 end 109 110 def to_lines(source) when is_binary(source) do 111 source 112 |> String.split("\n") 113 |> Enum.with_index() 114 |> Enum.map(fn {line, i} -> {i + 1, line} end) 115 end 116 117 @doc """ 118 Converts a String or `Credo.SourceFile` into a List of tokens using the `:elixir_tokenizer`. 119 """ 120 def to_tokens(string_or_source_file) 121 122 def to_tokens(%SourceFile{} = source_file) do 123 source_file 124 |> SourceFile.source() 125 |> to_tokens(source_file.filename) 126 end 127 128 def to_tokens(source, filename \\ "nofilename") when is_binary(source) do 129 source 130 |> String.to_charlist() 131 |> :elixir_tokenizer.tokenize(1, file: filename) 132 |> case do 133 # Elixir < 1.6 134 {_, _, _, tokens} -> 135 tokens 136 137 # Elixir >= 1.6 138 {:ok, tokens} -> 139 tokens 140 141 # Elixir >= 1.13 142 {:ok, _, _, _, tokens} -> 143 tokens 144 145 {:error, _, _, _, tokens} -> 146 tokens 147 end 148 end 149 150 @doc """ 151 Returns true if the given `child` AST node is part of the larger 152 `parent` AST node. 153 """ 154 def contains_child?(parent, child) do 155 Credo.Code.prewalk(parent, &find_child(&1, &2, child), false) 156 end 157 158 defp find_child({parent, _meta, child}, _acc, child), do: {parent, true} 159 160 defp find_child(parent, acc, child), do: {parent, acc || parent == child} 161 162 @doc """ 163 Takes a SourceFile and returns its source code stripped of all Strings and 164 Sigils. 165 """ 166 def clean_charlists_strings_and_sigils(source_file_or_source) do 167 {_source, filename} = Credo.SourceFile.source_and_filename(source_file_or_source) 168 169 source_file_or_source 170 |> Sigils.replace_with_spaces(" ", " ", filename) 171 |> Strings.replace_with_spaces(" ", " ", filename) 172 |> Heredocs.replace_with_spaces(" ", " ", "", filename) 173 |> Charlists.replace_with_spaces(" ", " ", filename) 174 end 175 176 @doc """ 177 Takes a SourceFile and returns its source code stripped of all Strings, Sigils 178 and code comments. 179 """ 180 def clean_charlists_strings_sigils_and_comments(source_file_or_source, sigil_replacement \\ " ") do 181 {_source, filename} = Credo.SourceFile.source_and_filename(source_file_or_source) 182 183 source_file_or_source 184 |> Heredocs.replace_with_spaces(" ", " ", "", filename) 185 |> Sigils.replace_with_spaces(sigil_replacement, " ", filename) 186 |> Strings.replace_with_spaces(" ", " ", filename) 187 |> Charlists.replace_with_spaces(" ", " ", filename) 188 |> String.replace(~r/(\A|[^\?])#.+/, "\\1") 189 end 190 191 @doc """ 192 Returns an AST without its metadata. 193 """ 194 def remove_metadata(ast) do 195 Macro.prewalk(ast, &Macro.update_meta(&1, fn _meta -> [] end)) 196 end 197 end