/
task.compiler.ex
149 lines (119 loc) · 4.6 KB
/
task.compiler.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
defmodule Mix.Task.Compiler do
@moduledoc """
This module defines the behaviour for a Mix task that does compilation.
A Mix compiler task can be defined by simply using `Mix.Task.Compiler`
in a module whose name starts with `Mix.Tasks.Compile.` and defining
the [`run/1`](`c:run/1`) function:
defmodule Mix.Tasks.Compile.MyLanguage do
use Mix.Task.Compiler
def run(_args) do
:ok
end
end
The [`run/1`](`c:run/1`) function returns an atom indicating the status of the
compilation, and optionally can also return a list of "diagnostics"
such as warnings or compilation errors. Doing this enables code
editors to display issues inline without having to analyze the
command-line output.
If the compiler uses manifest files to track stale sources, it should
define `manifests/0`, and if it writes any output to disk it should
also define `clean/0`.
A compiler supports the same attributes for configuration and
documentation as a regular Mix task. See `Mix.Task` for more information.
"""
defmodule Diagnostic do
@moduledoc """
Diagnostic information such as a warning or compilation error.
"""
@type t :: %__MODULE__{
file: Path.t(),
severity: severity,
message: String.t(),
position: position,
compiler_name: String.t(),
details: any
}
@typedoc """
Severity of a diagnostic:
* `:error` - An issue that caused compilation to fail
* `:warning` - An issue that did not cause failure but suggests the
programmer may have made a mistake
* `:hint` - A suggestion for style or good practices that is not as
severe as a warning
* `:information` - Any other information relevant to compilation that
does not fit into the above categories
"""
@type severity :: :error | :warning | :information | :hint
@typedoc """
Where in a file the diagnostic applies. Can be either a line number,
a range specified as `{start_line, start_col, end_line, end_col}`,
or `nil` if unknown.
Line numbers are one-based, and column numbers in a range are zero-based and refer
to the cursor position at the start of the character at that index. For example,
to indicate that a diagnostic applies to the first `n` characters of the
first line, the range would be `{1, 0, 1, n}`.
"""
@type position ::
nil
| pos_integer
| {pos_integer, non_neg_integer, pos_integer, non_neg_integer}
@enforce_keys [:file, :severity, :message, :position, :compiler_name]
defstruct [:file, :severity, :message, :position, :compiler_name, :details]
end
@type status :: :ok | :noop | :error
@doc """
Receives command-line arguments and performs compilation. If it
produces errors, warnings, or any other diagnostic information,
it should return a tuple with the status and a list of diagnostics.
"""
@callback run([binary]) :: status | {status, [Diagnostic.t()]}
@doc """
Lists manifest files for the compiler.
"""
@callback manifests() :: [Path.t()]
@doc """
Removes build artifacts and manifests.
"""
@callback clean() :: any
@optional_callbacks clean: 0, manifests: 0
@doc """
Adds a callback that runs after a given compiler.
The callback is invoked after the compiler runs and
it receives a tuple with current status and the list
of diagnostic. It must return the updated status and
diagnostics.
"""
@doc since: "1.10.0"
@spec after_compiler(atom, ({status, [Diagnostic.t()]} -> {status, [Diagnostic.t()]})) :: :ok
def after_compiler(name, fun) when is_atom(name) and is_function(fun, 1) do
Mix.ProjectStack.prepend_after_compiler(name, fun)
end
@doc false
defmacro __using__(_opts) do
quote do
Enum.each(
Mix.Task.supported_attributes(),
&Module.register_attribute(__MODULE__, &1, persist: true)
)
@behaviour Mix.Task.Compiler
end
end
# Normalize the compiler result to a diagnostic tuple.
@doc false
def normalize(result, name) do
case result do
{status, diagnostics} when status in [:ok, :noop, :error] and is_list(diagnostics) ->
{status, diagnostics}
# ok/noop can come from tasks that have already run
_ when result in [:ok, :noop] ->
{result, []}
_ ->
# TODO: Convert this to an error on v2.0
Mix.shell().error(
"warning: Mix compiler #{inspect(name)} was supposed to return " <>
"{:ok | :noop | :error, [diagnostic]} but it returned #{inspect(result)}"
)
{:noop, []}
end
end
end