Skip to content

Commit 32a9cf1

Browse files
committed
Add --warnings-as-errors flag for non-zero exit code
1 parent f8075a3 commit 32a9cf1

File tree

14 files changed

+389
-51
lines changed

14 files changed

+389
-51
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ ex_doc-*.tar
2424

2525
node_modules/
2626
/test/fixtures/umbrella/_build/
27+
/test/fixtures/single/_build/
28+
/test/fixtures/single/doc/
2729
/test/tmp/
2830
/tmp/
2931
/npm-debug.log

lib/ex_doc/cli.ex

Lines changed: 54 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,37 @@ 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

4243
if List.keymember?(opts, :version, 0) do
4344
IO.puts("ExDoc v#{ExDoc.version()}")
4445
else
45-
generate(args, opts, generator)
46+
results = generate(args, opts, generator)
47+
error_results = Enum.filter(results, &(elem(&1, 0) == :error))
48+
49+
if error_results == [] do
50+
Enum.map(results, fn {:ok, value} -> value end)
51+
else
52+
formatters = Enum.map(error_results, &elem(&1, 1).formatter)
53+
54+
format_message =
55+
case formatters do
56+
[formatter] -> "#{formatter} format"
57+
_ -> "#{Enum.join(formatters, ", ")} formats"
58+
end
59+
60+
message =
61+
"Documents have been generated, but generation for #{format_message} failed due to warnings while using the --warnings-as-errors option."
62+
63+
message_formatted = IO.ANSI.format([:red, message, :reset])
64+
65+
IO.puts(:stderr, message_formatted)
66+
67+
exit({:shutdown, 1})
68+
end
4669
end
4770
end
4871

@@ -71,7 +94,11 @@ defmodule ExDoc.CLI do
7194
quiet? ||
7295
IO.puts(IO.ANSI.format([:green, "View #{inspect(formatter)} docs at #{inspect(index)}"]))
7396

74-
index
97+
if opts[:warnings_as_errors] == true and ExDoc.Utils.warned?() do
98+
{:error, %{reason: :warnings_as_errors, formatter: formatter}}
99+
else
100+
{:ok, index}
101+
end
75102
end
76103
end
77104

@@ -164,29 +191,30 @@ defmodule ExDoc.CLI do
164191
ex_doc "Project" "1.0.0" "_build/dev/lib/project/ebin" -c "docs.exs"
165192
166193
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 a logo image for the project. Must be PNG, JPEG or SVG. The image will
178-
be placed in the output "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
194+
PROJECT Project name
195+
VERSION Version number
196+
BEAMS Path to compiled beam files
197+
--canonical Indicate the preferred URL with rel="canonical" link element
198+
-c, --config Give configuration through a file instead of a command line.
199+
See "Custom config" section below for more information.
200+
-f, --formatter Docs formatter to use (html or epub), default: html and epub
201+
--homepage-url URL to link to for the site name
202+
--language Identify the primary language of the documents, its value must be
203+
a valid [BCP 47](https://tools.ietf.org/html/bcp47) language tag, default: "en"
204+
-l, --logo Path to a logo image for the project. Must be PNG, JPEG or SVG. The image will
205+
be placed in the output "assets" directory.
206+
-m, --main The entry-point page in docs, default: "api-reference"
207+
-o, --output Path to output docs, default: "doc"
208+
--package Hex package name
209+
--paths Prepends the given path to Erlang code path. The path might contain a glob
210+
pattern but in that case, remember to quote it: --paths "_build/dev/lib/*/ebin".
211+
This option can be given multiple times.
212+
--proglang The project's programming language, default: "elixir".
213+
-q, --quiet Only output warnings and errors.
214+
--source-ref Branch/commit/tag used for source link inference, default: "master".
215+
-u, --source-url URL to the source code.
216+
-v, --version Print ExDoc version.
217+
--warnings-as-errors Exit with non-zero status if doc generation produces warnings.
190218
191219
## Custom config
192220

lib/ex_doc/config.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ defmodule ExDoc.Config do
4848
source_url: nil,
4949
source_url_pattern: nil,
5050
title: nil,
51-
version: nil
51+
version: nil,
52+
warnings_as_errors: false
5253

5354
@type t :: %__MODULE__{
5455
annotations_for_docs: (map() -> list()),
@@ -86,7 +87,8 @@ defmodule ExDoc.Config do
8687
source_url: nil | String.t(),
8788
source_url_pattern: nil | String.t(),
8889
title: nil | String.t(),
89-
version: nil | String.t()
90+
version: nil | String.t(),
91+
warnings_as_errors: boolean()
9092
}
9193

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

lib/ex_doc/formatter/epub.ex

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@ defmodule ExDoc.Formatter.EPUB do
22
@moduledoc false
33

44
@mimetype "application/epub+zip"
5+
56
alias __MODULE__.{Assets, Templates}
67
alias ExDoc.Formatter.HTML
8+
alias ExDoc.Utils
79

810
@doc """
9-
Generate EPUB documentation for the given modules.
11+
Generates EPUB documentation for the given modules.
1012
"""
1113
@spec run([ExDoc.ModuleNode.t()], [ExDoc.ModuleNode.t()], ExDoc.Config.t()) :: String.t()
1214
def run(project_nodes, filtered_modules, config) when is_map(config) do
15+
Utils.unset_warned()
16+
1317
config = normalize_config(config)
1418
File.rm_rf!(config.output)
1519
File.mkdir_p!(Path.join(config.output, "OEBPS"))
@@ -66,7 +70,7 @@ defmodule ExDoc.Formatter.EPUB do
6670
html = Templates.extra_template(config, title, title_content, content)
6771

6872
if File.regular?(output) do
69-
ExDoc.Utils.warn("file #{Path.relative_to_cwd(output)} already exists", [])
73+
Utils.warn("file #{Path.relative_to_cwd(output)} already exists", [])
7074
end
7175

7276
File.write!(output, html)

lib/ex_doc/formatter/html.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ defmodule ExDoc.Formatter.HTML do
88
@assets_dir "assets"
99

1010
@doc """
11-
Generate HTML documentation for the given modules.
11+
Generates HTML documentation for the given modules.
1212
"""
1313
@spec run([ExDoc.ModuleNode.t()], [ExDoc.ModuleNode.t()], ExDoc.Config.t()) :: String.t()
1414
def run(project_nodes, filtered_modules, config) when is_map(config) do
15+
Utils.unset_warned()
16+
1517
config = normalize_config(config)
1618
config = %{config | output: Path.expand(config.output)}
1719

@@ -509,7 +511,7 @@ defmodule ExDoc.Formatter.HTML do
509511

510512
defp generate_redirect(filename, config, redirect_to) do
511513
unless case_sensitive_file_regular?("#{config.output}/#{redirect_to}") do
512-
ExDoc.Utils.warn("#{filename} redirects to #{redirect_to}, which does not exist", [])
514+
Utils.warn("#{filename} redirects to #{redirect_to}, which does not exist", [])
513515
end
514516

515517
content = Templates.redirect_template(config, redirect_to)

lib/ex_doc/utils.ex

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,49 @@
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+
set_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 """
21+
Stores that a warning has been generated.
22+
"""
23+
def set_warned() do
24+
unless warned?() do
25+
:persistent_term.put({__MODULE__, :warned?}, true)
26+
end
27+
28+
true
29+
end
30+
31+
@doc """
32+
Removes that a warning has been generated.
33+
"""
34+
def unset_warned() do
35+
if warned?() do
36+
:persistent_term.put({__MODULE__, :warned?}, false)
37+
end
38+
end
39+
40+
@doc """
41+
Returns `true` if any warning has been generated during the document building. Otherwise returns `false`.
42+
"""
43+
def warned?() do
44+
:persistent_term.get({__MODULE__, :warned?}, false)
45+
end
46+
1747
@doc """
1848
Runs the `before_closing_head_tag` callback.
1949
"""

lib/mix/tasks/docs.ex

Lines changed: 45 additions & 7 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 [
@@ -381,17 +384,52 @@ defmodule Mix.Tasks.Docs do
381384
|> normalize_formatters()
382385
|> put_package(config)
383386

387+
Code.prepend_path(options[:source_beam])
388+
389+
for path <- Keyword.get_values(options, :paths),
390+
path <- Path.wildcard(path) do
391+
Code.prepend_path(path)
392+
end
393+
384394
Mix.shell().info("Generating docs...")
385395

386-
for formatter <- options[:formatters] do
387-
index = generator.(project, version, Keyword.put(options, :formatter, formatter))
388-
Mix.shell().info([:green, "View #{inspect(formatter)} docs at #{inspect(index)}"])
396+
results =
397+
for formatter <- options[:formatters] do
398+
index = generator.(project, version, Keyword.put(options, :formatter, formatter))
399+
Mix.shell().info([:green, "View #{inspect(formatter)} docs at #{inspect(index)}"])
389400

390-
if cli_opts[:open] do
391-
browser_open(index)
401+
if cli_opts[:open] do
402+
browser_open(index)
403+
end
404+
405+
if options[:warnings_as_errors] == true and ExDoc.Utils.warned?() do
406+
{:error, %{reason: :warnings_as_errors, formatter: formatter}}
407+
else
408+
{:ok, index}
409+
end
392410
end
393411

394-
index
412+
error_results = Enum.filter(results, &(elem(&1, 0) == :error))
413+
414+
if error_results == [] do
415+
Enum.map(results, fn {:ok, value} -> value end)
416+
else
417+
formatters = Enum.map(error_results, &elem(&1, 1).formatter)
418+
419+
format_message =
420+
case formatters do
421+
[formatter] -> "#{formatter} format"
422+
_ -> "#{Enum.join(formatters, ", ")} formats"
423+
end
424+
425+
message =
426+
"Documents have been generated, but generation for #{format_message} failed due to warnings while using the --warnings-as-errors option."
427+
428+
message_formatted = IO.ANSI.format([:red, message, :reset])
429+
430+
IO.puts(:stderr, message_formatted)
431+
432+
exit({:shutdown, 1})
395433
end
396434
end
397435

mix.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ defmodule ExDoc.Mixfile do
4141
{:makeup_c, ">= 0.1.1", optional: true},
4242
{:makeup_html, ">= 0.0.0", only: :dev},
4343
{:jason, "~> 1.2", only: :test},
44-
{:floki, "~> 0.0", only: :test},
44+
# TODO: Relax Floki requirement once we no longer support Elixir v1.12
45+
{:floki, "~> 0.35.0", only: :test},
4546
{:easyhtml, "~> 0.0", only: :test}
4647
]
4748
end

test/ex_doc/formatter/epub_test.exs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
defmodule ExDoc.Formatter.EPUBTest do
2-
use ExUnit.Case, async: true
2+
use ExUnit.Case, async: false
3+
4+
import ExUnit.CaptureIO
5+
6+
alias ExDoc.Utils
37

48
@moduletag :tmp_dir
59

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

0 commit comments

Comments
 (0)