project.ex (10132B)
1 defmodule Dialyxir.Project do 2 @moduledoc false 3 import Dialyxir.Output, only: [info: 1, error: 1] 4 5 alias Dialyxir.FilterMap 6 7 def plts_list(deps, include_project \\ true, exclude_core \\ false) do 8 elixir_apps = [:elixir] 9 erlang_apps = [:erts, :kernel, :stdlib, :crypto] 10 11 core_plts = 12 if exclude_core do 13 [] 14 else 15 [{elixir_plt(), elixir_apps}, {erlang_plt(), erlang_apps}] 16 end 17 18 if include_project do 19 [{plt_file(), deps ++ elixir_apps ++ erlang_apps} | core_plts] 20 else 21 core_plts 22 end 23 end 24 25 def plt_file() do 26 plt_path(dialyzer_config()[:plt_file]) || deps_plt() 27 end 28 29 defp plt_path(file) when is_binary(file), do: Path.expand(file) 30 defp plt_path({:no_warn, file}) when is_binary(file), do: Path.expand(file) 31 defp plt_path(_), do: false 32 33 def check_config do 34 if is_binary(dialyzer_config()[:plt_file]) do 35 info(""" 36 Notice: :plt_file is deprecated as Dialyxir now uses project-private PLT files by default. 37 If you want to use this setting without seeing this warning, provide it in a pair 38 with the :no_warn key e.g. `dialyzer: plt_file: {:no_warn, "~/mypltfile"}` 39 """) 40 end 41 end 42 43 def cons_apps do 44 # compile & load all deps paths 45 Mix.Tasks.Deps.Loadpaths.run([]) 46 # compile & load current project paths 47 Mix.Task.run("compile") 48 apps = plt_apps() || plt_add_apps() ++ include_deps() 49 50 apps 51 |> Enum.sort() 52 |> Enum.uniq() 53 |> Kernel.--(plt_ignore_apps()) 54 end 55 56 def dialyzer_files do 57 beam_files = 58 dialyzer_paths() 59 |> Enum.flat_map(&beam_files_with_paths/1) 60 |> Map.new() 61 62 consolidated_files = 63 Mix.Project.consolidation_path() 64 |> beam_files_with_paths() 65 |> Enum.filter(fn {file_name, _path} -> beam_files |> Map.has_key?(file_name) end) 66 |> Map.new() 67 68 beam_files 69 |> Map.merge(consolidated_files) 70 |> Enum.map(fn {_file, path} -> path end) 71 |> reject_exclude_files() 72 |> Enum.map(&to_charlist(&1)) 73 end 74 75 defp reject_exclude_files(files) do 76 file_exclusions = dialyzer_config()[:exclude_files] || [] 77 78 Enum.reject(files, fn file -> 79 :lists.any( 80 fn reject_file_pattern -> 81 re = <<reject_file_pattern::binary, "$">> 82 result = :re.run(file, re) 83 84 case result do 85 {:match, _captured} -> true 86 :nomatch -> false 87 end 88 end, 89 file_exclusions 90 ) 91 end) 92 end 93 94 defp dialyzer_paths do 95 paths = dialyzer_config()[:paths] || default_paths() 96 excluded_paths = dialyzer_config()[:excluded_paths] || [] 97 Enum.map(paths -- excluded_paths, &String.to_charlist/1) 98 end 99 100 defp beam_files_with_paths(path) do 101 path |> Path.join("*.beam") |> Path.wildcard() |> Enum.map(&{Path.basename(&1), &1}) 102 end 103 104 def dialyzer_removed_defaults do 105 dialyzer_config()[:remove_defaults] || [] 106 end 107 108 def dialyzer_flags do 109 Mix.Project.config()[:dialyzer][:flags] || [] 110 end 111 112 defp skip?({file, warning, line}, {file, warning, line, _}), do: true 113 defp skip?({file, warning}, {file, warning, _, _}), do: true 114 defp skip?({file}, {file, _, _, _}), do: true 115 defp skip?({short_description, warning, line}, {_, warning, line, short_description}), do: true 116 defp skip?({short_description, warning}, {_, warning, _, short_description}), do: true 117 defp skip?({short_description}, {_, _, _, short_description}), do: true 118 119 defp skip?(%Regex{} = pattern, {_, _, _, short_description}) do 120 Regex.match?(pattern, short_description) 121 end 122 123 defp skip?(_, _), do: false 124 125 def filter_warning?({file, warning, line, short_description}, filter_map = %FilterMap{}) do 126 {matching_filters, _non_matching_filters} = 127 filter_map 128 |> FilterMap.filters() 129 |> Enum.split_with(&skip?(&1, {file, warning, line, short_description})) 130 131 {not Enum.empty?(matching_filters), matching_filters} 132 end 133 134 def filter_map(args) do 135 cond do 136 legacy_ignore_warnings?() -> 137 %FilterMap{} 138 139 dialyzer_ignore_warnings() == nil && !File.exists?(default_ignore_warnings()) -> 140 %FilterMap{} 141 142 true -> 143 ignore_file = dialyzer_ignore_warnings() || default_ignore_warnings() 144 145 FilterMap.from_file(ignore_file, list_unused_filters?(args), ignore_exit_status?(args)) 146 end 147 end 148 149 def filter_legacy_warnings(output) do 150 ignore_file = dialyzer_ignore_warnings() 151 152 if legacy_ignore_warnings?() do 153 pattern = File.read!(ignore_file) 154 filter_legacy_warnings(output, pattern) 155 else 156 output 157 end 158 end 159 160 def filter_legacy_warnings(output, nil), do: output 161 def filter_legacy_warnings(output, ""), do: output 162 163 def filter_legacy_warnings(output, pattern) do 164 lines = Enum.map(output, &String.trim_trailing/1) 165 166 patterns = 167 pattern 168 |> String.trim_trailing("\n") 169 |> String.split("\n") 170 |> Enum.reject(&(&1 == "")) 171 172 try do 173 Enum.reject(lines, fn line -> 174 Enum.any?(patterns, &String.contains?(line, &1)) 175 end) 176 rescue 177 _ -> 178 output 179 end 180 end 181 182 @spec legacy_ignore_warnings?() :: boolean 183 defp legacy_ignore_warnings?() do 184 case dialyzer_ignore_warnings() do 185 nil -> 186 false 187 188 ignore_file -> 189 !String.ends_with?(ignore_file, ".exs") 190 end 191 end 192 193 def default_ignore_warnings() do 194 ".dialyzer_ignore.exs" 195 end 196 197 def dialyzer_ignore_warnings() do 198 dialyzer_config()[:ignore_warnings] 199 end 200 201 def list_unused_filters?(args) do 202 case Keyword.fetch(args, :list_unused_filters) do 203 {:ok, list_unused_filters} when not is_nil(list_unused_filters) -> 204 list_unused_filters 205 206 _else -> 207 dialyzer_config()[:list_unused_filters] 208 end 209 end 210 211 defp ignore_exit_status?(args) do 212 args[:ignore_exit_status] 213 end 214 215 def elixir_plt() do 216 global_plt("erlang-#{otp_vsn()}_elixir-#{System.version()}") 217 end 218 219 def erlang_plt(), do: global_plt("erlang-" <> otp_vsn()) 220 221 defp otp_vsn() do 222 major = :erlang.system_info(:otp_release) |> List.to_string() 223 vsn_file = Path.join([:code.root_dir(), "releases", major, "OTP_VERSION"]) 224 225 try do 226 {:ok, contents} = File.read(vsn_file) 227 String.split(contents, "\n", trim: true) 228 else 229 [full] -> 230 full 231 232 _ -> 233 major 234 catch 235 :error, _ -> 236 major 237 end 238 end 239 240 def deps_plt() do 241 name = "erlang-#{otp_vsn()}_elixir-#{System.version()}_deps-#{build_env()}" 242 local_plt(name) 243 end 244 245 defp build_env() do 246 config = Mix.Project.config() 247 248 case Keyword.fetch!(config, :build_per_environment) do 249 true -> Atom.to_string(Mix.env()) 250 false -> "shared" 251 end 252 end 253 254 defp global_plt(name) do 255 Path.join(core_path(), "dialyxir_" <> name <> ".plt") 256 end 257 258 defp core_path(), do: dialyzer_config()[:plt_core_path] || Mix.Utils.mix_home() 259 260 defp local_plt(name) do 261 Path.join(local_path(), "dialyxir_" <> name <> ".plt") 262 end 263 264 defp local_path(), do: dialyzer_config()[:plt_local_path] || Mix.Project.build_path() 265 266 defp default_paths() do 267 reduce_umbrella_children([], fn paths -> 268 [Mix.Project.compile_path() | paths] 269 end) 270 end 271 272 defp plt_apps, do: dialyzer_config()[:plt_apps] |> load_apps() 273 defp plt_add_apps, do: dialyzer_config()[:plt_add_apps] || [] |> load_apps() 274 defp plt_ignore_apps, do: dialyzer_config()[:plt_ignore_apps] || [] 275 276 defp load_apps(nil), do: nil 277 278 defp load_apps(apps) do 279 Enum.each(apps, &Application.load/1) 280 apps 281 end 282 283 defp include_deps do 284 method = dialyzer_config()[:plt_add_deps] 285 286 reduce_umbrella_children([], fn deps -> 287 deps ++ 288 case method do 289 false -> 290 [] 291 292 # compatibility 293 true -> 294 deps_project() ++ deps_app(false) 295 296 :project -> 297 info( 298 "Dialyxir has deprecated plt_add_deps: :project in favor of apps_direct, which includes only runtime dependencies." 299 ) 300 301 deps_project() ++ deps_app(false) 302 303 :apps_direct -> 304 deps_app(false) 305 306 :transitive -> 307 info( 308 "Dialyxir has deprecated plt_add_deps: :transitive in favor of app_tree, which includes only runtime dependencies." 309 ) 310 311 deps_transitive() ++ deps_app(true) 312 313 _app_tree -> 314 deps_app(true) 315 end 316 end) 317 end 318 319 defp deps_project do 320 Mix.Project.config()[:deps] 321 |> Enum.filter(&env_dep(&1)) 322 |> Enum.map(&elem(&1, 0)) 323 end 324 325 defp deps_transitive do 326 Mix.Project.deps_paths() 327 |> Map.keys() 328 end 329 330 @spec deps_app(boolean()) :: [atom] 331 defp deps_app(recursive) do 332 app = Mix.Project.config()[:app] 333 deps_app(app, recursive) 334 end 335 336 @spec deps_app(atom(), boolean()) :: [atom] 337 defp deps_app(app, recursive) do 338 with_each = 339 if recursive do 340 &deps_app(&1, true) 341 else 342 fn _ -> [] end 343 end 344 345 case Application.load(app) do 346 :ok -> 347 nil 348 349 {:error, {:already_loaded, _}} -> 350 nil 351 352 {:error, err} -> 353 nil 354 error("Error loading #{app}, dependency list may be incomplete.\n #{inspect(err)}") 355 end 356 357 case Application.spec(app, :applications) do 358 [] -> 359 [] 360 361 nil -> 362 [] 363 364 this_apps -> 365 Enum.map(this_apps, with_each) 366 |> List.flatten() 367 |> Enum.concat(this_apps) 368 end 369 end 370 371 defp env_dep(dep) do 372 only_envs = dep_only(dep) 373 only_envs == nil || Mix.env() in List.wrap(only_envs) 374 end 375 376 defp dep_only({_, opts}) when is_list(opts), do: opts[:only] 377 defp dep_only({_, _, opts}) when is_list(opts), do: opts[:only] 378 defp dep_only(_), do: nil 379 380 @spec reduce_umbrella_children(list(), (list() -> list())) :: list() 381 defp reduce_umbrella_children(acc, f) do 382 if Mix.Project.umbrella?() do 383 children = Mix.Dep.Umbrella.loaded() 384 385 Enum.reduce(children, acc, fn child, acc -> 386 Mix.Project.in_project(child.app, child.opts[:path], fn _ -> 387 reduce_umbrella_children(acc, f) 388 end) 389 end) 390 else 391 f.(acc) 392 end 393 end 394 395 defp dialyzer_config(), do: Mix.Project.config()[:dialyzer] 396 end