@@ -2,7 +2,7 @@ defmodule Mix.Tasks.Xref do
22 use Mix.Task
33
44 alias Mix.Tasks.Compile.Elixir , as: E
5- import Mix.Compilers.Elixir , only: [ read_manifest: 2 , source: 1 , source: 2 ]
5+ import Mix.Compilers.Elixir , only: [ read_manifest: 2 , source: 1 , source: 2 , module: 1 ]
66
77 @ shortdoc "Performs cross reference checks"
88 @ recursive true
@@ -12,18 +12,45 @@ defmodule Mix.Tasks.Xref do
1212
1313 ## Xref modes
1414
15- The following commands are available:
15+ The following commands and options are available:
1616
1717 * `warnings` - prints warnings for violated cross reference checks
18+
1819 * `unreachable` - prints all unreachable "file:line: module.function/arity" entries
20+
1921 * `callers CALLEE` - prints all references of `CALLEE`, which can be one of: `Module`,
2022 `Module.function`, or `Module.function/arity`
2123
22- ## Command line options
24+ * `graph` - prints the file reference graph. By default, an edge from `A` to `B` indicates
25+ that `A` depends on `B`
26+
27+ * `--exclude` - paths to exclude
28+
29+ * `--source` - display only files for which there is a path from the
30+ given source file
31+
32+ * `--sink` - display only files for which there is a path to the
33+ given sink file.
34+
35+ * `--format` - can be set to one of:
36+
37+ * `pretty` - use Unicode codepoints for formatting the graph. This is the default except on
38+ Windows
39+
40+ * `plain` - do not use Unicode codepoints for formatting the graph. This is the default on
41+ Windows
42+
43+ * `dot` - produces a DOT graph description in `xref_graph.dot` in the
44+ current directory. Warning: this will override any previously generated file
45+
46+ ## Options for all commands
2347
2448 * `--no-compile` - do not compile even if files require compilation
49+
2550 * `--no-deps-check` - do not check dependencies
51+
2652 * `--no-archives-check` - do not check archives
53+
2754 * `--no-elixir-version-check` - do not check the Elixir version from mix.exs
2855
2956 ## Configuration
@@ -36,7 +63,8 @@ defmodule Mix.Tasks.Xref do
3663 """
3764
3865 @ switches [ compile: :boolean , deps_check: :boolean , archives_check: :boolean ,
39- elixir_version_check: :boolean ]
66+ elixir_version_check: :boolean , exclude: :keep , format: :string ,
67+ source: :string , sink: :string ]
4068
4169 @ doc """
4270 Runs this task.
@@ -57,8 +85,10 @@ defmodule Mix.Tasks.Xref do
5785 unreachable ( )
5886 [ "callers" , callee ] ->
5987 callers ( callee )
88+ [ "graph" ] ->
89+ graph ( opts )
6090 _ ->
61- Mix . raise "xref expects one of the following commands: warnings, unreachable, callers CALLEE "
91+ Mix . raise "xref doesn't support this command, see mix help xref for more information "
6292 end
6393 end
6494
@@ -88,6 +118,12 @@ defmodule Mix.Tasks.Xref do
88118 :ok
89119 end
90120
121+ defp graph ( opts ) do
122+ write_graph ( file_references ( ) , excluded ( opts ) , opts )
123+
124+ :ok
125+ end
126+
91127 ## Unreachable
92128
93129 defp unreachable ( pair_fun ) do
@@ -283,6 +319,124 @@ defmodule Mix.Tasks.Xref do
283319 Mix . raise message
284320 end
285321
322+ ## Graph helpers
323+
324+ defp excluded ( opts ) do
325+ Keyword . get_values ( opts , :exclude )
326+ |> Enum . flat_map ( & [ { & 1 , nil } , { & 1 , "(compile)" } , { & 1 , "(runtime)" } ] )
327+ end
328+
329+ defp file_references ( ) do
330+ module_sources =
331+ for manifest <- E . manifests ( ) ,
332+ manifest_data = read_manifest ( manifest , "" ) ,
333+ module ( module: module , source: source ) <- manifest_data ,
334+ source = Enum . find ( manifest_data , & match? ( source ( source: ^ source ) , & 1 ) ) ,
335+ do: { module , source } ,
336+ into: % { }
337+
338+ all_modules = MapSet . new ( module_sources , & elem ( & 1 , 0 ) )
339+
340+ Map . new module_sources , fn { module , source } ->
341+ source ( runtime_references: runtime , compile_references: compile , source: file ) = source
342+ compile_references =
343+ compile
344+ |> MapSet . new ( )
345+ |> MapSet . delete ( module )
346+ |> MapSet . intersection ( all_modules )
347+ |> Enum . filter ( & module_sources [ & 1 ] != source )
348+ |> Enum . map ( & { source ( module_sources [ & 1 ] , :source ) , "(compile)" } )
349+
350+ runtime_references =
351+ runtime
352+ |> MapSet . new ( )
353+ |> MapSet . delete ( module )
354+ |> MapSet . intersection ( all_modules )
355+ |> Enum . filter ( & module_sources [ & 1 ] != source )
356+ |> Enum . map ( & { source ( module_sources [ & 1 ] , :source ) , nil } )
357+
358+ { file , compile_references ++ runtime_references }
359+ end
360+ end
361+
362+ defp write_graph ( file_references , excluded , opts ) do
363+ { root , file_references } =
364+ case { opts [ :source ] , opts [ :sink ] } do
365+ { nil , nil } ->
366+ { Enum . map ( file_references , & { elem ( & 1 , 0 ) , nil } ) -- excluded , file_references }
367+
368+ { source , nil } ->
369+ if file_references [ source ] do
370+ { [ { source , nil } ] , file_references }
371+ else
372+ Mix . raise "Source could not be found: #{ source } "
373+ end
374+
375+ { nil , sink } ->
376+ if file_references [ sink ] do
377+ file_references = filter_for_sink ( file_references , sink )
378+ roots =
379+ file_references
380+ |> Map . delete ( sink )
381+ |> Enum . map ( & { elem ( & 1 , 0 ) , nil } )
382+ { roots -- excluded , file_references }
383+ else
384+ Mix . raise "Sink could not be found: #{ sink } "
385+ end
386+
387+ { _ , _ } ->
388+ Mix . raise "mix xref graph expects only one of --source and --sink"
389+ end
390+
391+ callback =
392+ fn { file , type } ->
393+ children = Map . get ( file_references , file , [ ] )
394+ { { file , type } , children -- excluded }
395+ end
396+
397+ if opts [ :format ] == "dot" do
398+ Mix.Utils . write_dot_graph! ( "xref_graph.dot" , "xref graph" ,
399+ root , callback , opts )
400+ """
401+ Generated "xref_graph.dot" in the current directory. To generate a PNG:
402+
403+ dot -Tpng xref_graph.dot -o xref_graph.png
404+
405+ For more options see http://www.graphviz.org/.
406+ """
407+ |> String . trim_trailing ( )
408+ |> Mix . shell . info ( )
409+ else
410+ Mix.Utils . print_tree ( root , callback , opts )
411+ end
412+ end
413+
414+ defp filter_for_sink ( file_references , sink ) do
415+ file_references
416+ |> invert_references ( )
417+ |> do_filter_for_sink ( [ { sink , nil } ] , % { } )
418+ |> invert_references ( )
419+ end
420+
421+ defp do_filter_for_sink ( file_references , new_nodes , acc ) do
422+ Enum . reduce new_nodes , acc , fn { new_node_name , _type } , acc ->
423+ new_nodes = file_references [ new_node_name ]
424+ if acc [ new_node_name ] || ! new_nodes do
425+ acc
426+ else
427+ do_filter_for_sink ( file_references , new_nodes , Map . put ( acc , new_node_name , new_nodes ) )
428+ end
429+ end
430+ end
431+
432+ defp invert_references ( file_references ) do
433+ Enum . reduce file_references , % { } , fn { file , references } , acc ->
434+ Enum . reduce references , acc , fn { reference , type } , acc ->
435+ Map . update ( acc , reference , [ { file , type } ] , & [ { file , type } | & 1 ] )
436+ end
437+ end
438+ end
439+
286440 ## Helpers
287441
288442 defp each_source_entries ( entries_fun , pair_fun ) do
0 commit comments