source_file.ex (4231B)
1 defmodule Credo.SourceFile do 2 @moduledoc """ 3 `SourceFile` structs represent a source file in the codebase. 4 """ 5 6 @type t :: %__MODULE__{ 7 filename: nil | String.t(), 8 hash: String.t(), 9 status: :valid | :invalid | :timed_out 10 } 11 12 alias Credo.Service.SourceFileAST 13 alias Credo.Service.SourceFileLines 14 alias Credo.Service.SourceFileSource 15 16 defstruct filename: nil, 17 hash: nil, 18 status: nil 19 20 defimpl Inspect, for: __MODULE__ do 21 def inspect(source_file, _opts) do 22 "%SourceFile<#{source_file.filename}>" 23 end 24 end 25 26 @doc """ 27 Returns a `SourceFile` struct for the given `source` code and `filename`. 28 """ 29 def parse(source, filename) do 30 filename = Path.relative_to_cwd(filename) 31 32 lines = Credo.Code.to_lines(source) 33 34 {valid, ast} = 35 case Credo.Code.ast(source) do 36 {:ok, ast} -> 37 {true, ast} 38 39 {:error, _errors} -> 40 {false, []} 41 end 42 43 hash = 44 :sha256 45 |> :crypto.hash(source) 46 |> Base.encode16() 47 48 source_file = %Credo.SourceFile{ 49 filename: filename, 50 hash: hash, 51 status: if(valid, do: :valid, else: :invalid) 52 } 53 54 SourceFileAST.put(source_file, ast) 55 SourceFileLines.put(source_file, lines) 56 SourceFileSource.put(source_file, source) 57 58 source_file 59 end 60 61 @spec timed_out(String.t()) :: t 62 def timed_out(filename) do 63 filename = Path.relative_to_cwd(filename) 64 65 %Credo.SourceFile{ 66 filename: filename, 67 hash: "timed_out:#{filename}", 68 status: :timed_out 69 } 70 end 71 72 @doc "Returns the AST for the given `source_file`." 73 def ast(source_file) 74 75 def ast(%__MODULE__{} = source_file) do 76 case SourceFileAST.get(source_file) do 77 {:ok, ast} -> 78 ast 79 80 _ -> 81 raise "Could not get source from ETS: #{source_file.filename}" 82 end 83 end 84 85 @doc "Returns the lines of source code for the given `source_file`." 86 def lines(source_file) 87 88 def lines(%__MODULE__{} = source_file) do 89 case SourceFileLines.get(source_file) do 90 {:ok, lines} -> 91 lines 92 93 _ -> 94 raise "Could not get source from ETS: #{source_file.filename}" 95 end 96 end 97 98 @doc "Returns the source code for the given `source_file`." 99 def source(source_file) 100 101 def source(%__MODULE__{} = source_file) do 102 case SourceFileSource.get(source_file) do 103 {:ok, source} -> 104 source 105 106 _ -> 107 raise "Could not get source from ETS: #{source_file.filename}" 108 end 109 end 110 111 @doc "Returns the source code and filename for the given `source_file_or_source`." 112 def source_and_filename(source_file_or_source, default_filename \\ "nofilename") 113 114 def source_and_filename(%__MODULE__{filename: filename} = source_file, _default_filename) do 115 {source(source_file), filename} 116 end 117 118 def source_and_filename(source, default_filename) when is_binary(source) do 119 {source, default_filename} 120 end 121 122 @doc """ 123 Returns the line at the given `line_no`. 124 125 NOTE: `line_no` is a 1-based index. 126 """ 127 def line_at(%__MODULE__{} = source_file, line_no) do 128 source_file 129 |> lines() 130 |> Enum.find_value(&find_line_at(&1, line_no)) 131 end 132 133 defp find_line_at({line_no, text}, line_no), do: text 134 defp find_line_at(_, _), do: nil 135 136 @doc """ 137 Returns the snippet at the given `line_no` between `column1` and `column2`. 138 139 NOTE: `line_no` is a 1-based index. 140 """ 141 def line_at(%__MODULE__{} = source_file, line_no, column1, column2) do 142 source_file 143 |> line_at(line_no) 144 |> String.slice(column1 - 1, column2 - column1) 145 end 146 147 @doc """ 148 Returns the column of the given `trigger` inside the given line. 149 150 NOTE: Both `line_no` and the returned index are 1-based. 151 """ 152 def column(source_file, line_no, trigger) 153 154 def column(%__MODULE__{} = source_file, line_no, trigger) 155 when is_binary(trigger) or is_atom(trigger) do 156 line = line_at(source_file, line_no) 157 158 regexed = 159 trigger 160 |> to_string 161 |> Regex.escape() 162 163 case Regex.run(~r/(\b|\(|\)|\,)(#{regexed})(\b|\(|\)|\,)/, line, return: :index) do 164 nil -> 165 nil 166 167 [_, _, {regexed_col, _regexed_length}, _] -> 168 regexed_col + 1 169 end 170 end 171 172 def column(_, _, _), do: nil 173 end