Skip to content

Commit aff8cb8

Browse files
committed
Add --warnings-as-errors flag for non-zero exit code
1 parent ae05a4f commit aff8cb8

File tree

10 files changed

+304
-45
lines changed

10 files changed

+304
-45
lines changed

lib/ex_doc/cli.ex

Lines changed: 40 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,25 @@ defmodule ExDoc.CLI do
3535
quiet: :boolean,
3636
source_ref: :string,
3737
source_url: :string,
38-
version: :boolean
38+
version: :boolean,
39+
warnings_as_errors: :boolean
3940
]
4041
)
4142

42-
if List.keymember?(opts, :version, 0) do
43-
IO.puts("ExDoc v#{ExDoc.version()}")
44-
else
45-
generate(args, opts, generator)
43+
cond do
44+
List.keymember?(opts, :version, 0) ->
45+
IO.puts("ExDoc v#{ExDoc.version()}")
46+
47+
opts[:warnings_as_errors] == true and ExDoc.Utils.warned?() ->
48+
IO.puts(
49+
:stderr,
50+
"Doc generation failed due to warnings while using the --warnings-as-errors option"
51+
)
52+
53+
exit({:shutdown, 1})
54+
55+
true ->
56+
generate(args, opts, generator)
4657
end
4758
end
4859

@@ -164,29 +175,30 @@ defmodule ExDoc.CLI do
164175
ex_doc "Project" "1.0.0" "_build/dev/lib/project/ebin" -c "docs.exs"
165176
166177
Options:
167-
PROJECT Project name
168-
VERSION Version number
169-
BEAMS Path to compiled beam files
170-
--canonical Indicate the preferred URL with rel="canonical" link element
171-
-c, --config Give configuration through a file instead of a command line.
172-
See "Custom config" section below for more information.
173-
-f, --formatter Docs formatter to use (html or epub), default: html and epub
174-
--homepage-url URL to link to for the site name
175-
--language Identify the primary language of the documents, its value must be
176-
a valid [BCP 47](https://tools.ietf.org/html/bcp47) language tag, default: "en"
177-
-l, --logo Path to the image logo of the project (only PNG or JPEG accepted)
178-
The image size will be 64x64 and copied to the assets directory
179-
-m, --main The entry-point page in docs, default: "api-reference"
180-
-o, --output Path to output docs, default: "doc"
181-
--package Hex package name
182-
--paths Prepends the given path to Erlang code path. The path might contain a glob
183-
pattern but in that case, remember to quote it: --paths "_build/dev/lib/*/ebin".
184-
This option can be given multiple times
185-
--proglang The project's programming language, default: "elixir"
186-
-q, --quiet Only output warnings and errors
187-
--source-ref Branch/commit/tag used for source link inference, default: "master"
188-
-u, --source-url URL to the source code
189-
-v, --version Print ExDoc version
178+
PROJECT Project name.
179+
VERSION Version number.
180+
BEAMS Path to compiled beam files.
181+
--canonical Indicate the preferred URL with rel="canonical" link element.
182+
-c, --config Give configuration through a file instead of a command line.
183+
See "Custom config" section below for more information.
184+
-f, --formatter Docs formatter to use (html or epub), default: html and epub.
185+
--homepage-url URL to link to for the site name.
186+
--language Identify the primary language of the documents, its value must be
187+
a valid [BCP 47](https://tools.ietf.org/html/bcp47) language tag, default: "en".
188+
-l, --logo Path to the image logo of the project (only PNG or JPEG accepted).
189+
The image size will be 64x64 and copied to the assets directory.
190+
-m, --main The entry-point page in docs, default: "api-reference".
191+
-o, --output Path to output docs, default: "doc".
192+
--package Hex package name.
193+
--paths Prepends the given path to Erlang code path. The path might contain a glob
194+
pattern but in that case, remember to quote it: --paths "_build/dev/lib/*/ebin".
195+
This option can be given multiple times.
196+
--proglang The project's programming language, default: "elixir".
197+
-q, --quiet Only output warnings and errors.
198+
--source-ref Branch/commit/tag used for source link inference, default: "master".
199+
-u, --source-url URL to the source code.
200+
-v, --version Print ExDoc version.
201+
--warnings-as-errors Exit with non-zero status if doc generation has one or more warnings.
190202
191203
## Custom config
192204

lib/ex_doc/config.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ defmodule ExDoc.Config do
4444
source_url: nil,
4545
source_url_pattern: nil,
4646
title: nil,
47-
version: nil
47+
version: nil,
48+
warnings_as_errors: false
4849

4950
@type t :: %__MODULE__{
5051
annotations_for_docs: (map() -> list()),
@@ -82,7 +83,8 @@ defmodule ExDoc.Config do
8283
source_url: nil | String.t(),
8384
source_url_pattern: nil | String.t(),
8485
title: nil | String.t(),
85-
version: nil | String.t()
86+
version: nil | String.t(),
87+
warnings_as_errors: boolean()
8688
}
8789

8890
@spec build(String.t(), String.t(), Keyword.t()) :: ExDoc.Config.t()

lib/ex_doc/formatter/html.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ defmodule ExDoc.Formatter.HTML do
246246
html = Templates.extra_template(config, node, extra_type(extension), nodes_map, refs)
247247

248248
if File.regular?(output) do
249-
ExDoc.Utils.warn("warning: file #{Path.relative_to_cwd(output)} already exists", [])
249+
ExDoc.Utils.warn("file #{Path.relative_to_cwd(output)} already exists", [])
250250
end
251251

252252
File.write!(output, html)

lib/ex_doc/formatter/html/templates.ex

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,7 @@ defmodule ExDoc.Formatter.HTML.Templates do
193193
end
194194

195195
def module_summary(module_node) do
196-
entries =
197-
docs_groups(module_node.docs_groups, module_node.docs ++ module_node.typespecs)
196+
entries = docs_groups(module_node.docs_groups, module_node.docs ++ module_node.typespecs)
198197

199198
Enum.reject(entries, fn {_type, nodes} -> nodes == [] end)
200199
end

lib/ex_doc/utils.ex

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,42 @@
11
defmodule ExDoc.Utils do
22
@moduledoc false
33

4+
@elixir_gte_1_14? Version.match?(System.version(), ">= 1.14.0")
5+
46
@doc """
57
Emits a warning.
68
"""
7-
if Version.match?(System.version(), ">= 1.14.0") do
8-
def warn(message, stacktrace_info) do
9+
def warn(message, stacktrace_info) do
10+
put_warned()
11+
12+
# TODO: remove check when we require Elixir v1.14
13+
if @elixir_gte_1_14? do
914
IO.warn(message, stacktrace_info)
10-
end
11-
else
12-
def warn(message, _stacktrace_info) do
15+
else
1316
IO.warn(message, [])
1417
end
1518
end
1619

20+
@doc false
21+
def put_warned() do
22+
unless warned?() do
23+
:persistent_term.put({__MODULE__, :warned?}, true)
24+
end
25+
26+
true
27+
end
28+
29+
@doc false
30+
def delete_warned() do
31+
if warned?() do
32+
:persistent_term.put({__MODULE__, :warned?}, false)
33+
end
34+
end
35+
36+
def warned?() do
37+
:persistent_term.get({__MODULE__, :warned?}, false)
38+
end
39+
1740
@doc """
1841
Runs the `before_closing_head_tag` callback.
1942
"""

lib/mix/tasks/docs.ex

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ defmodule Mix.Tasks.Docs do
2727
* `--proglang` - Chooses the main programming language: `elixir`
2828
or `erlang`
2929
30+
* `--warnings-as-errors` - Exits with non-zero exit code if any warnings are found
31+
3032
The command line options have higher precedence than the options
3133
specified in your `mix.exs` file below.
3234
@@ -323,7 +325,8 @@ defmodule Mix.Tasks.Docs do
323325
language: :string,
324326
open: :boolean,
325327
output: :string,
326-
proglang: :string
328+
proglang: :string,
329+
warnings_as_errors: :boolean
327330
]
328331

329332
@aliases [
@@ -391,7 +394,16 @@ defmodule Mix.Tasks.Docs do
391394
browser_open(index)
392395
end
393396

394-
index
397+
if options[:warnings_as_errors] == true and ExDoc.Utils.warned?() do
398+
Mix.shell().info([
399+
:red,
400+
"Doc generation failed due to warnings while using the --warnings-as-errors option"
401+
])
402+
403+
exit({:shutdown, 1})
404+
else
405+
index
406+
end
395407
end
396408
end
397409

test/ex_doc/cli_test.exs

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
defmodule ExDoc.CLITest do
2-
use ExUnit.Case, async: true
2+
use ExUnit.Case, async: false
3+
34
import ExUnit.CaptureIO
45

56
@ebin "_build/test/lib/ex_doc/ebin"
@@ -67,6 +68,70 @@ defmodule ExDoc.CLITest do
6768
assert io == "ExDoc v#{ExDoc.version()}\n"
6869
end
6970

71+
describe "--warnings-as-errors" do
72+
@describetag :warnings
73+
74+
test "exits with code 0 when no warnings" do
75+
ExDoc.Utils.delete_warned()
76+
77+
{[html, epub], _io} = run(["ExDoc", "1.2.3", @ebin, "--warnings-as-errors"])
78+
79+
assert html ==
80+
{"ExDoc", "1.2.3",
81+
[
82+
formatter: "html",
83+
formatters: ["html", "epub"],
84+
apps: [:ex_doc],
85+
source_beam: @ebin,
86+
warnings_as_errors: true
87+
]}
88+
89+
assert epub ==
90+
{"ExDoc", "1.2.3",
91+
[
92+
formatter: "epub",
93+
formatters: ["html", "epub"],
94+
apps: [:ex_doc],
95+
source_beam: @ebin,
96+
warnings_as_errors: true
97+
]}
98+
end
99+
100+
test "exits with 1 when there is a warning" do
101+
ExDoc.Utils.put_warned()
102+
103+
fun = fn ->
104+
run(["ExDoc", "1.2.3", @ebin, "--warnings-as-errors"])
105+
end
106+
107+
io =
108+
capture_io(:stderr, fn ->
109+
assert catch_exit(fun.()) == {:shutdown, 1}
110+
end)
111+
112+
assert io =~
113+
"Doc generation failed due to warnings while using the --warnings-as-errors option\n"
114+
end
115+
116+
test "exits with 1 when there are multiple warnings" do
117+
ExDoc.Utils.put_warned()
118+
ExDoc.Utils.put_warned()
119+
ExDoc.Utils.put_warned()
120+
121+
fun = fn ->
122+
run(["ExDoc", "1.2.3", @ebin, "--warnings-as-errors"])
123+
end
124+
125+
io =
126+
capture_io(:stderr, fn ->
127+
assert catch_exit(fun.()) == {:shutdown, 1}
128+
end)
129+
130+
assert io =~
131+
"Doc generation failed due to warnings while using the --warnings-as-errors option\n"
132+
end
133+
end
134+
70135
test "too many arguments" do
71136
assert catch_exit(run(["ExDoc", "1.2.3", "/", "kaboom"])) == {:shutdown, 1}
72137
end

test/ex_doc/formatter/epub_test.exs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
defmodule ExDoc.Formatter.EPUBTest do
2-
use ExUnit.Case, async: true
2+
use ExUnit.Case, async: false
3+
4+
import ExUnit.CaptureIO
35

46
@moduletag :tmp_dir
57

@@ -248,4 +250,47 @@ defmodule ExDoc.Formatter.EPUBTest do
248250
after
249251
File.rm_rf!("test/tmp/epub_assets")
250252
end
253+
254+
describe "warnings" do
255+
@describetag :warnings
256+
257+
test "multiple warnings are registered when using warnings_as_errors: true", context do
258+
ExDoc.Utils.delete_warned()
259+
260+
output =
261+
capture_io(:stderr, fn ->
262+
generate_docs(
263+
doc_config(context,
264+
skip_undefined_reference_warnings_on: [],
265+
warnings_as_errors: true
266+
)
267+
)
268+
end)
269+
270+
# TODO: remove check when we require Elixir v1.16
271+
if Version.match?(System.version(), ">= 1.16.0-rc") do
272+
assert output =~ ~S|moduledoc `Warnings.bar/0`|
273+
assert output =~ ~S|typedoc `Warnings.bar/0`|
274+
assert output =~ ~S|doc callback `Warnings.bar/0`|
275+
assert output =~ ~S|doc `Warnings.bar/0`|
276+
end
277+
278+
assert ExDoc.Utils.warned?() == true
279+
end
280+
281+
test "warnings are registered even with warnings_as_errors: false", context do
282+
ExDoc.Utils.delete_warned()
283+
284+
capture_io(:stderr, fn ->
285+
generate_docs(
286+
doc_config(context,
287+
skip_undefined_reference_warnings_on: [],
288+
warnings_as_errors: false
289+
)
290+
)
291+
end)
292+
293+
assert ExDoc.Utils.warned?() == true
294+
end
295+
end
251296
end

0 commit comments

Comments
 (0)