Skip to content

Commit 8d63cc4

Browse files
Add --warnings-as-errors flag (#1831)
1 parent f2369e0 commit 8d63cc4

File tree

13 files changed

+386
-50
lines changed

13 files changed

+386
-50
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
@@ -49,7 +49,8 @@ defmodule ExDoc.Config do
4949
source_url: nil,
5050
source_url_pattern: nil,
5151
title: nil,
52-
version: nil
52+
version: nil,
53+
warnings_as_errors: false
5354

5455
@type t :: %__MODULE__{
5556
annotations_for_docs: (map() -> list()),
@@ -88,7 +89,8 @@ defmodule ExDoc.Config do
8889
source_url: nil | String.t(),
8990
source_url_pattern: nil | String.t(),
9091
title: nil | String.t(),
91-
version: nil | String.t()
92+
version: nil | String.t(),
93+
warnings_as_errors: boolean()
9294
}
9395

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

lib/ex_doc/formatter/epub.ex

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@ defmodule ExDoc.Formatter.EPUB do
55
@assets_dir "OEBPS/assets"
66
alias __MODULE__.{Assets, Templates}
77
alias ExDoc.Formatter.HTML
8+
alias ExDoc.Utils
89

910
@doc """
10-
Generate EPUB documentation for the given modules.
11+
Generates EPUB documentation for the given modules.
1112
"""
1213
@spec run([ExDoc.ModuleNode.t()], [ExDoc.ModuleNode.t()], ExDoc.Config.t()) :: String.t()
1314
def run(project_nodes, filtered_modules, config) when is_map(config) do
15+
Utils.unset_warned()
16+
1417
config = normalize_config(config)
1518
File.rm_rf!(config.output)
1619
File.mkdir_p!(Path.join(config.output, "OEBPS"))
@@ -66,7 +69,7 @@ defmodule ExDoc.Formatter.EPUB do
6669
html = Templates.extra_template(config, title, title_content, content)
6770

6871
if File.regular?(output) do
69-
ExDoc.Utils.warn("file #{Path.relative_to_cwd(output)} already exists", [])
72+
Utils.warn("file #{Path.relative_to_cwd(output)} already exists", [])
7073
end
7174

7275
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

@@ -528,7 +530,7 @@ defmodule ExDoc.Formatter.HTML do
528530

529531
defp generate_redirect(filename, config, redirect_to) do
530532
unless case_sensitive_file_regular?("#{config.output}/#{redirect_to}") do
531-
ExDoc.Utils.warn("#{filename} redirects to #{redirect_to}, which does not exist", [])
533+
Utils.warn("#{filename} redirects to #{redirect_to}, which does not exist", [])
532534
end
533535

534536
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
@@ -325,7 +327,8 @@ defmodule Mix.Tasks.Docs do
325327
language: :string,
326328
open: :boolean,
327329
output: :string,
328-
proglang: :string
330+
proglang: :string,
331+
warnings_as_errors: :boolean
329332
]
330333

331334
@aliases [
@@ -383,17 +386,52 @@ defmodule Mix.Tasks.Docs do
383386
|> normalize_formatters()
384387
|> put_package(config)
385388

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

388-
for formatter <- options[:formatters] do
389-
index = generator.(project, version, Keyword.put(options, :formatter, formatter))
390-
Mix.shell().info([:green, "View #{inspect(formatter)} docs at #{inspect(index)}"])
398+
results =
399+
for formatter <- options[:formatters] do
400+
index = generator.(project, version, Keyword.put(options, :formatter, formatter))
401+
Mix.shell().info([:green, "View #{inspect(formatter)} docs at #{inspect(index)}"])
391402

392-
if cli_opts[:open] do
393-
browser_open(index)
403+
if cli_opts[:open] do
404+
browser_open(index)
405+
end
406+
407+
if options[:warnings_as_errors] == true and ExDoc.Utils.warned?() do
408+
{:error, %{reason: :warnings_as_errors, formatter: formatter}}
409+
else
410+
{:ok, index}
411+
end
394412
end
395413

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

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

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

0 commit comments

Comments
 (0)