@@ -2,7 +2,7 @@ defmodule Mix.Tasks.Xref do
2
2
use Mix.Task
3
3
4
4
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 ]
6
6
7
7
@ shortdoc "Performs cross reference checks"
8
8
@ recursive true
@@ -12,18 +12,45 @@ defmodule Mix.Tasks.Xref do
12
12
13
13
## Xref modes
14
14
15
- The following commands are available:
15
+ The following commands and options are available:
16
16
17
17
* `warnings` - prints warnings for violated cross reference checks
18
+
18
19
* `unreachable` - prints all unreachable "file:line: module.function/arity" entries
20
+
19
21
* `callers CALLEE` - prints all references of `CALLEE`, which can be one of: `Module`,
20
22
`Module.function`, or `Module.function/arity`
21
23
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
23
47
24
48
* `--no-compile` - do not compile even if files require compilation
49
+
25
50
* `--no-deps-check` - do not check dependencies
51
+
26
52
* `--no-archives-check` - do not check archives
53
+
27
54
* `--no-elixir-version-check` - do not check the Elixir version from mix.exs
28
55
29
56
## Configuration
@@ -36,7 +63,8 @@ defmodule Mix.Tasks.Xref do
36
63
"""
37
64
38
65
@ 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 ]
40
68
41
69
@ doc """
42
70
Runs this task.
@@ -57,8 +85,10 @@ defmodule Mix.Tasks.Xref do
57
85
unreachable ( )
58
86
[ "callers" , callee ] ->
59
87
callers ( callee )
88
+ [ "graph" ] ->
89
+ graph ( opts )
60
90
_ ->
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 "
62
92
end
63
93
end
64
94
@@ -88,6 +118,12 @@ defmodule Mix.Tasks.Xref do
88
118
:ok
89
119
end
90
120
121
+ defp graph ( opts ) do
122
+ write_graph ( file_references ( ) , excluded ( opts ) , opts )
123
+
124
+ :ok
125
+ end
126
+
91
127
## Unreachable
92
128
93
129
defp unreachable ( pair_fun ) do
@@ -283,6 +319,124 @@ defmodule Mix.Tasks.Xref do
283
319
Mix . raise message
284
320
end
285
321
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
+
286
440
## Helpers
287
441
288
442
defp each_source_entries ( entries_fun , pair_fun ) do
0 commit comments